You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

353 lines
12 KiB

import 'dart:async';
import 'dart:math';
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import '../../../data/services/voice_recognition_service.dart';
import '../../../data/services/volcano_tts_service.dart';
class VoiceInputController extends GetxController {
// Observable states
final isConnecting = true.obs;
final isMuted = false.obs;
final isRecording = false.obs;
final recognizedText = ''.obs;
// 添加一个新的可观察状态,表示用户是否正在说话
final isUserSpeaking = false.obs;
// 语音识别服务
late final VoiceRecognitionService _voiceService;
late final VolcanoTtsService _ttsService;
// 连续识别相关
StreamSubscription? _recognitionSubscription;
// 添加一个标志,表示是否已经识别到语音
bool _hasRecognizedSpeech = false;
// 添加一个 Completer 用于在识别到最终结果时完成
Completer<String>? _recognitionCompleter;
// 添加一个标志,表示是否已经收到最终结果
bool _hasFinalResult = false;
// 添加一个计时器,用于在一段时间没有新的识别结果时提交当前结果
Timer? _silenceTimer;
// 添加一个计时器,用于定期更新用户说话状态
Timer? _speakingCheckTimer;
// 最后一次识别到语音的时间
DateTime? _lastSpeechTime;
// 添加一个变量来跟踪空结果的数量
int _emptyResultCount = 0;
static const int _maxEmptyResults = 3;
// Callbacks
final Function(String) onRecordingResult;
final VoidCallback onClosePanel;
final Function(String) onRecognizing;
VoiceInputController({
required this.onRecordingResult,
required this.onClosePanel,
required this.onRecognizing,
});
@override
void onInit() {
super.onInit();
_initializeVoiceService();
_ttsService = Get.find<VolcanoTtsService>();
// 启动定期检查用户是否正在说话的计时器
_startSpeakingCheckTimer();
}
Future<void> _initializeVoiceService() async {
try {
// 创建语音识别服务实例
_voiceService = VoiceRecognitionService();
// 初始化语音识别服务
await _voiceService.initialize();
// 初始化完成后更新状态
isConnecting.value = false;
// 自动开始连续语音识别
await startContinuousRecognition();
} catch (e) {
print('初始化语音识别服务失败: $e');
Get.snackbar(
'Error',
'初始化语音识别服务失败: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
}
Future<void> startContinuousRecognition() async {
if (isConnecting.value) return;
if (isRecording.value) return; // 已经在录音中
try {
isRecording.value = true;
recognizedText.value = ''; // 清空识别文本,但不再在面板中显示
_hasRecognizedSpeech = false;
_hasFinalResult = false;
_emptyResultCount = 0;
// 创建一个 Completer 来处理语音识别完成
_recognitionCompleter = Completer<String>();
// 开始连续语音识别
final stream = await _voiceService.startContinuousRecognition();
// 订阅识别事件流
_recognitionSubscription = stream.listen((event) {
switch (event.type) {
case RecognitionEventType.intermediateResult:
// 不再更新面板中的识别文本,而是通过回调传递给ChatController
if (event.text.isNotEmpty) {
_hasRecognizedSpeech = true;
_lastSpeechTime = DateTime.now();
_emptyResultCount = 0; // 重置空结果计数
// 设置用户正在说话状态
isUserSpeaking.value = true;
// 如果系统正在播放TTS,检测到用户开始说话时立即中断
bool isSpeaking = false;
try {
isSpeaking = _ttsService.isActuallyPlaying();
} catch (e) {
print('获取 TTS 播放状态失败: $e');
// 如果获取失败,假设为false
isSpeaking = false;
}
if (isSpeaking && event.text.trim().isNotEmpty) {
print('检测到用户开始说话,中断TTS播放');
_ttsService.stop(); // 停止当前TTS播放
}
// 取消之前的静默计时器
_silenceTimer?.cancel();
// 设置新的静默计时器,如果 2 秒内没有新的识别结果,则认为用户已经停止说话
_silenceTimer = Timer(const Duration(seconds: 2), () {
if (_hasRecognizedSpeech && !_hasFinalResult &&
_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
print('用户停止说话 2 秒,使用当前识别结果: ${event.text}');
_recognitionCompleter!.complete(event.text);
// 调用最终结果回调,发送识别到的句子
onRecordingResult(event.text);
// 清空识别状态,准备下一句
_hasRecognizedSpeech = false;
_hasFinalResult = false;
}
});
}
// 调用识别中回调,将中间结果传递给ChatController
onRecognizing(event.text);
break;
case RecognitionEventType.finalResult:
// 更新最终识别结果
_hasFinalResult = true;
// 重置用户说话状态
isUserSpeaking.value = false;
if (event.text.isNotEmpty) {
// 不再更新面板中的识别文本
_hasRecognizedSpeech = true;
_lastSpeechTime = DateTime.now();
_emptyResultCount = 0; // 重置空结果计数
// 调用最终结果回调,发送识别到的句子
onRecordingResult(event.text);
// 清空识别状态,准备下一句
_hasRecognizedSpeech = false;
_hasFinalResult = false;
} else {
// 如果最终结果为空,但之前有中间结果,仍然标记为有语音
if (_hasRecognizedSpeech) {
print('最终识别结果为空,但之前有中间结果,使用最后的中间结果');
// 这里不再需要使用recognizedText.value,因为我们已经在中间结果中传递了文本
// 而且ChatController已经在UI中显示了最新的识别结果
// 清空识别状态,准备下一句
_hasRecognizedSpeech = false;
_hasFinalResult = false;
} else {
// 如果最终结果为空,且没有中间结果,增加空结果计数
_emptyResultCount++;
print('收到空的最终结果,当前空结果计数: $_emptyResultCount / $_maxEmptyResults');
}
}
break;
case RecognitionEventType.error:
case RecognitionEventType.canceled:
// 处理错误
print('语音识别错误: ${event.error}');
Get.snackbar(
'Error',
'语音识别错误: ${event.error}',
snackPosition: SnackPosition.BOTTOM,
);
// 尝试重新启动识别
restartRecognition();
break;
case RecognitionEventType.sessionStopped:
// 会话结束,尝试重新启动
isRecording.value = false;
restartRecognition();
break;
default:
break;
}
}, onError: (error) {
print('语音识别流错误: $error');
Get.snackbar(
'Error',
'语音识别流错误: $error',
snackPosition: SnackPosition.BOTTOM,
);
isRecording.value = false;
// 尝试重新启动识别
restartRecognition();
});
// 等待语音识别完成
final result = await _recognitionCompleter!.future;
} catch (e) {
print('语音识别失败: $e');
Get.snackbar(
'Error',
'启动语音识别失败: $e',
snackPosition: SnackPosition.BOTTOM,
);
isRecording.value = false;
} finally {
// 清理资源
_recognitionCompleter = null;
_silenceTimer?.cancel();
_silenceTimer = null;
}
}
// 重新启动识别
Future<void> restartRecognition() async {
try {
// 先停止当前识别
await stopRecording();
// 延迟一下再重新启动
await Future.delayed(const Duration(milliseconds: 500));
// 重新启动连续识别
await startContinuousRecognition();
} catch (e) {
print('重新启动语音识别失败: $e');
}
}
Future<void> stopRecording() async {
if (!isRecording.value) return;
try {
// 取消事件订阅
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
// 取消计时器
_silenceTimer?.cancel();
_silenceTimer = null;
// 停止连续识别
if (_voiceService.isContinuousRecognitionActive()) {
await _voiceService.stopContinuousRecognition();
}
isRecording.value = false;
_hasRecognizedSpeech = false;
_hasFinalResult = false;
_emptyResultCount = 0;
} catch (e) {
print('停止语音识别失败: $e');
isRecording.value = false;
}
}
void toggleMute() {
// 只切换静音状态,不再影响录音
// 因为已启用回声消除,不需要在录音时停止TTS
isMuted.value = !isMuted.value;
}
// 手动开始录音(用户点击按钮)
void startRecording() {
if (!isRecording.value) {
startContinuousRecognition();
}
}
// 启动定期检查用户是否正在说话的计时器
void _startSpeakingCheckTimer() {
_speakingCheckTimer?.cancel();
_speakingCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
_checkIfUserIsSpeaking();
});
}
// 检查用户是否正在说话
void _checkIfUserIsSpeaking() {
// 如果有最近的识别文本,且距离现在不超过1秒,认为用户正在说话
if (_hasRecognizedSpeech && !_hasFinalResult && _lastSpeechTime != null) {
final now = DateTime.now();
final timeSinceLastSpeech = now.difference(_lastSpeechTime!);
// 如果最近1秒内有语音识别,认为用户正在说话
if (timeSinceLastSpeech.inMilliseconds < 1000) {
isUserSpeaking.value = true;
return;
}
}
// 默认情况下,如果正在录音但没有最近的语音识别,随机生成一个说话状态
if (isRecording.value) {
// 随机生成一个值,有20%的概率认为用户正在说话
isUserSpeaking.value = Random().nextDouble() < 0.2;
} else {
isUserSpeaking.value = false;
}
}
@override
void onClose() {
// 停止录音
stopRecording();
// 取消计时器
_speakingCheckTimer?.cancel();
// 重置状态
isConnecting.value = false;
isMuted.value = false;
isRecording.value = false;
recognizedText.value = '';
isUserSpeaking.value = false;
super.onClose();
}
}