From d0ad85891115ecd49132a0ee8055b14bda324775 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Sat, 1 Mar 2025 10:22:10 +0000 Subject: [PATCH] tts asr 0.0.3 --- .../services/background_agent_service.dart | 648 +++++++----------- lib/data/services/my_audio_handler.dart | 2 +- .../services/volcano_tts_api_service.dart | 374 +++------- lib/data/services/volcano_tts_service.dart | 6 +- .../chat/controllers/chat_controller.dart | 140 +++- .../controllers/voice_input_controller.dart | 3 +- .../test/controllers/tts_test_controller.dart | 64 +- lib/modules/test/views/tts_test_view.dart | 16 +- 8 files changed, 553 insertions(+), 700 deletions(-) diff --git a/lib/data/services/background_agent_service.dart b/lib/data/services/background_agent_service.dart index ef8f3e7db..0589be5ea 100644 --- a/lib/data/services/background_agent_service.dart +++ b/lib/data/services/background_agent_service.dart @@ -14,15 +14,12 @@ class BackgroundAgentService extends GetxService { final VolcanoTtsApiService _ttsService; final AzureAsrService _voiceRecognitionService; final List _messageHistory = []; - String _pendingTtsText = ''; - static const int _minTtsLength = 20; bool _isProcessing = false; bool _isListening = false; StreamSubscription? _recognitionSubscription; - // TTS相关订阅 - StreamSubscription? _ttsEventSubscription; - bool _isTtsSpeaking = false; + // 默认语音类型 + static const String _defaultSpeaker = 'zh_female_shuangkuaisisi_moon_bigtts'; // 可观察的状态 final RxBool isListening = false.obs; @@ -31,356 +28,131 @@ class BackgroundAgentService extends GetxService { // 添加一个标志,表示是否已经识别到语音 bool _hasRecognizedSpeech = false; - // 添加一个标志,表示是否应该继续循环交互 - bool _shouldContinueInteraction = false; - - // 添加一个 Completer 用于在识别到最终结果时完成 - Completer? _recognitionCompleter; - // 添加一个标志,表示是否已经收到最终结果 bool _hasFinalResult = false; - // 添加一个计时器,用于在一段时间没有新的识别结果时提交当前结果 - Timer? _silenceTimer; - - // 添加一个计时器,用于检测用户长时间没有说话 - Timer? _noSpeechTimer; - - // 最后一次识别到语音的时间 - // DateTime? _lastSpeechTime; // Removing unused field + // 单一的交互超时计时器 (30秒) + Timer? _interactionTimer; // 添加一个变量来跟踪当前的AI响应流订阅 StreamSubscription? _aiResponseSubscription; // 添加一个标志,表示是否应该取消当前的AI响应 bool _shouldCancelAiResponse = false; - - // 添加计数器,用于跟踪连续无输入的次数 - // int _noSpeechCount = 0; // Removing unused field + + // 当前系统提示词 + String? _currentSystemPrompt; BackgroundAgentService() : _aiService = VolcanoAIService(), _ttsService = Get.find(), - _voiceRecognitionService = Get.find() { - // 设置TTS事件监听 - _setupTtsEventListener(); - } - - // 设置TTS事件监听 - void _setupTtsEventListener() { - _ttsEventSubscription?.cancel(); - _ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent); - } + _voiceRecognitionService = Get.find(); - // 处理TTS事件 - void _handleTtsEvent(Map event) { - final eventType = event['eventType']; - - switch (eventType) { - case 'ttsSentenceStart': - _isTtsSpeaking = true; - break; - case 'ttsSentenceEnd': - case 'sessionFinished': - case 'error': - _isTtsSpeaking = false; - break; - } - } - - // 启动无语音超时计时器 - void _startNoSpeechTimer() { + // 启动或重置交互计时器 + void _resetInteractionTimer() { // 取消之前的计时器 - _noSpeechTimer?.cancel(); + _interactionTimer?.cancel(); // 设置30秒的超时计时器 - _noSpeechTimer = Timer(const Duration(seconds: 30), () { + _interactionTimer = Timer(const Duration(seconds: 30), () { // 检查TTS是否正在播放 - if (_isTtsSpeaking) { + if (_ttsService.isPlaying) { // 如果TTS正在播放,重置定时器 - Logger.info('TTS正在播放,重置30秒超时计时器'); - _startNoSpeechTimer(); + _resetInteractionTimer(); } else { // 如果TTS不在播放,且30秒内没有检测到用户输入,退出交互 Logger.info('30秒内没有检测到用户输入,退出交互'); - _exitInteraction(); + _endInteraction(); } }); + } - // 退出交互 - Future _exitInteraction() async { + // 结束交互 + Future _endInteraction() async { // 完成当前的recognitionCompleter(如果有) if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { _recognitionCompleter!.complete(''); } - // 设置标志,停止循环交互 - _shouldContinueInteraction = false; + _isProcessing = false; + // 播放退出提示 - try { - // 使用默认语音类型 - const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; - await _ttsService.synthesize("没有听到您说话,已退出语音交互", speaker); - } catch (e) { - print('播放退出提示失败: $e'); + await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _defaultSpeaker); + + + // 停止语音识别 + if (_isListening) { + await stopVoiceRecognition(); + Logger.info('退出交互,已停止语音识别'); } + + // 清除当前系统提示词 + _currentSystemPrompt = null; } - // 处理蓝牙耳机按钮触发的交互 - Future handleAgentInteraction(String systemPrompt) async { + // 开始语音交互 + Future startAgentInteraction(String systemPrompt) async { + // 播放提示音 + await _ttsService.speakSingle("我在听", speaker: _defaultSpeaker); + if (_isProcessing) { print('已经在处理交互,忽略此次请求'); return; } - // 确保TTS服务已连接 - if (!_ttsService.isConnected.value) { - try { - await _ttsService.connect(); - } catch (e) { - print('连接TTS服务失败: $e'); - return; - } - } - - // 设置循环交互标志为 true - _shouldContinueInteraction = true; + _isProcessing = true; + _currentSystemPrompt = systemPrompt; try { - // 确保之前的语音识别已经停止 - if (_isListening) { - await stopVoiceRecognition(); - } + - // 循环进行交互,直到用户 30 秒没有说话或手动停止 - while (_shouldContinueInteraction) { - await _processSingleInteraction(systemPrompt); - } - } finally { - // 确保在交互结束时停止语音识别 + // 确保之前的语音识别已经停止 if (_isListening) { await stopVoiceRecognition(); } - // 重置语音识别服务 - try { - await _voiceRecognitionService.initialize( - subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '', - serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia', - language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN', - ); - } catch (e) { - print('重置语音识别服务失败: $e'); - } - } - } - - // 处理单次交互 - Future _processSingleInteraction(String systemPrompt) async { - _isProcessing = true; - _hasRecognizedSpeech = false; - _hasFinalResult = false; - - try { - // 先播放一个简短的提示音或提示语,表示开始监听 - try { - // 使用默认语音类型 - const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; - await _ttsService.synthesize("我在听", speaker); - } catch (e) { - print('播放提示音失败: $e'); - // 继续执行,不要因为提示音失败而中断整个流程 - } - - // 开始语音识别 - String userInput = ''; - - try { - // 确保之前的语音识别已经停止 - if (_isListening) { - await stopVoiceRecognition(); - } - - // 尝试启动语音识别 - await startVoiceRecognition(); - - // 创建一个 Completer 来处理语音识别完成 - _recognitionCompleter = Completer(); - - // 启动无语音超时计时器 - _startNoSpeechTimer(); - - // 等待语音识别完成 - userInput = await _recognitionCompleter!.future; - - // 取消无语音超时计时器 - _noSpeechTimer?.cancel(); - _noSpeechTimer = null; - - // 如果用户输入为空,重新启动超时计时器并返回 - if (userInput.trim().isEmpty) { - _startNoSpeechTimer(); - return; - } - } catch (e) { - print('语音识别过程出错: $e'); - _startNoSpeechTimer(); // 出错时也启动超时计时器 - return; - } finally { - // 清理资源,但保持语音识别状态 - _recognitionCompleter = null; - _silenceTimer?.cancel(); - _silenceTimer = null; - } - - // 保存用户消息到历史记录 - _messageHistory.add(Message( - role: 'user', - content: userInput, - timestamp: DateTime.now(), - )); - - // 构建用于生成回应的消息列表 - final messages = [ - {'role': 'system', 'content': systemPrompt}, - {'role': 'user', 'content': userInput}, - ]; - - String fullResponse = ''; - try { - // 重置取消标志 - _shouldCancelAiResponse = false; - - // 创建一个本地变量来跟踪是否已取消 - bool isCancelled = false; - - // 获取AI响应流 - final responseStream = _aiService.sendMessageStream( - messages: messages, - systemPrompt: systemPrompt, - ); - - // 创建一个订阅来处理响应流 - _aiResponseSubscription = responseStream.listen( - (chunk) { - // 如果已经设置了取消标志,则不处理这个块 - if (_shouldCancelAiResponse) { - isCancelled = true; - return; - } - - fullResponse += chunk; - - // 累积文本并处理TTS - _pendingTtsText += chunk; - - // 查找最后一个完整句子的结束位置 - int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText); - if (lastSentenceEnd > 0) { - // 提取完整的句子 - String sentenceToSpeak = _pendingTtsText.substring(0, lastSentenceEnd + 1); - // 只有当句子长度超过最小长度时才播放 - if (sentenceToSpeak.length >= _minTtsLength) { - try { - // 使用默认语音类型 - const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; - _ttsService.synthesize(sentenceToSpeak, speaker); - } catch (e) { - print('播放TTS失败: $e'); - } - // 更新待处理文本,移除已播放的部分 - _pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1); - } - } - }, - onError: (e) { - print('AI响应流错误: $e'); - // 清理订阅 - _aiResponseSubscription = null; - }, - onDone: () { - // 如果已取消,不处理剩余的文本 - if (!isCancelled && !_shouldCancelAiResponse) { - // 处理剩余的文本 - if (_pendingTtsText.isNotEmpty) { - try { - // 使用默认语音类型 - const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; - _ttsService.synthesize(_pendingTtsText, speaker); - } catch (e) { - print('播放剩余TTS失败: $e'); - } - } - } - _pendingTtsText = ''; - _aiResponseSubscription = null; - - // AI响应完成后,重新启动超时计时器 - _startNoSpeechTimer(); - } - ); - - // 等待响应流完成 - await _aiResponseSubscription!.asFuture(); - - } catch (e) { - print('AI响应生成失败: $e'); - // 如果AI响应失败,使用默认回复 - fullResponse = '抱歉,我现在无法回答您的问题。请稍后再试。'; - - // 播放错误提示 - try { - // 使用默认语音类型 - const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; - await _ttsService.synthesize(fullResponse, speaker); - } catch (_) {} - - // 启动超时计时器 - _startNoSpeechTimer(); - } finally { - // 清理资源 - _aiResponseSubscription?.cancel(); - _aiResponseSubscription = null; - _shouldCancelAiResponse = false; - } - - // 如果响应被取消,不保存到历史记录 - if (!_shouldCancelAiResponse && fullResponse.isNotEmpty) { - // 保存助手回复到历史记录 - _messageHistory.add(Message( - role: 'assistant', - content: fullResponse, - timestamp: DateTime.now(), - )); - } - - // 限制历史记录长度 - if (_messageHistory.length > 20) { - _messageHistory.removeRange(0, _messageHistory.length - 20); - } + // 启动语音识别 + await startVoiceRecognition(); + // 启动交互计时器 + _resetInteractionTimer(); } catch (e) { - print('交互过程出错: $e'); - try { - // 使用默认语音类型 - const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; - await _ttsService.synthesize("抱歉,出现了一些问题", speaker); - } catch (_) {} - - // 发生错误时停止循环交互 - _shouldContinueInteraction = false; - } finally { + Logger.error('启动语音交互失败: $e'); _isProcessing = false; + + // 播放错误提示 + await _ttsService.speakSingle("抱歉,启动语音交互失败", speaker: _defaultSpeaker); } } - - // 停止循环交互 - void stopContinuousInteraction() { - _shouldContinueInteraction = false; - print('手动停止循环交互'); + + // 停止语音交互 + Future stopAgentInteraction() async { + // 取消交互计时器 + _interactionTimer?.cancel(); + _interactionTimer = null; + + // 停止TTS播放 + if (_ttsService.isPlaying) { + await _ttsService.stop(); + } + + // 停止语音识别 + if (_isListening) { + await stopVoiceRecognition(); + Logger.info('手动停止交互,已停止语音识别'); + } + + // 清除处理状态 + _isProcessing = false; + _currentSystemPrompt = null; + + Logger.info('手动停止交互,已结束TTS会话'); } - + + // 添加一个 Completer 用于在识别到最终结果时完成 + Completer? _recognitionCompleter; + // 开始语音识别 Future startVoiceRecognition() async { if (_isListening) { @@ -388,30 +160,23 @@ class BackgroundAgentService extends GetxService { } try { + Logger.info('开始初始化语音识别服务...'); // 确保语音识别服务已初始化 if (!await _voiceRecognitionService.initialize( subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '', serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia', language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN', )) { - // 尝试重新初始化 - await Future.delayed(const Duration(milliseconds: 500)); - if (!await _voiceRecognitionService.initialize( - subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '', - serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia', - language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN', - )) { - throw Exception('无法初始化语音识别服务'); - } + // 初始化失败,直接抛出异常 + Logger.error('语音识别服务初始化失败'); + throw Exception('无法初始化语音识别服务'); } // 开始连续识别 final success = await _voiceRecognitionService.startContinuousRecognition(); if (!success) { - if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - _recognitionCompleter!.completeError(Exception('无法启动语音识别')); - } - return; + Logger.error('启动连续语音识别失败'); + throw Exception('无法启动语音识别'); } _isListening = true; @@ -420,41 +185,60 @@ class BackgroundAgentService extends GetxService { _hasRecognizedSpeech = false; _hasFinalResult = false; + // 添加一个变量来存储完整的用户输入 + String fullUserInput = ''; + // 使用语音识别服务的recognitionStream而不是方法的返回值 _recognitionSubscription = _voiceRecognitionService.recognitionStream?.listen((event) { if (event.type == RecognitionEventType.finalResult) { - recognizedText.value = event.text; + // 添加日志输出最终识别结果 + Logger.info('语音识别最终结果: ${event.text}'); + if (event.text.isNotEmpty) { + // 将最终结果添加到完整输入中,并添加适当的标点符号 + if (fullUserInput.isNotEmpty && !fullUserInput.endsWith('。') && + !fullUserInput.endsWith('?') && !fullUserInput.endsWith('!') && + !fullUserInput.endsWith('.') && !fullUserInput.endsWith('?') && + !fullUserInput.endsWith('!')) { + fullUserInput += ','; + } + fullUserInput += event.text; + + // 更新可观察的识别文本 + recognizedText.value = fullUserInput; + _hasRecognizedSpeech = true; _hasFinalResult = true; - // 收到最终结果,立即完成识别过程 - if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - _recognitionCompleter!.complete(event.text); - } - } else { - // 如果最终结果为空,但有中间结果,使用最后的中间结果 - if (_hasRecognizedSpeech && recognizedText.value.isNotEmpty) { - if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - _recognitionCompleter!.complete(recognizedText.value); - } - } else { - // 如果最终结果为空,且没有中间结果,重新启动超时计时器 - _startNoSpeechTimer(); - } + // 重置交互计时器,因为用户刚刚说了话 + _resetInteractionTimer(); + + + _processUserInput(fullUserInput); + + // 重置用户输入 + fullUserInput = ''; + recognizedText.value = ''; + _hasRecognizedSpeech = false; + _hasFinalResult = false; } } else if (event.type == RecognitionEventType.recognizing) { - recognizedText.value = event.text; + // 添加日志输出中间识别结果 + Logger.info('语音识别中间结果: ${event.text}'); + if (event.text.isNotEmpty) { + // 更新当前的中间结果,但不添加到完整输入中 + recognizedText.value = fullUserInput + (fullUserInput.isEmpty ? "" : ",") + event.text; + _hasRecognizedSpeech = true; - // 检测到用户说话,重置无语音计时器 - _noSpeechTimer?.cancel(); - _startNoSpeechTimer(); + // 检测到用户说话,重置交互计时器 + _resetInteractionTimer(); // 如果系统正在播放TTS或接收AI响应,检测到用户开始说话时立即中断 - if (_isTtsSpeaking && event.text.trim().isNotEmpty) { - _ttsService.endSession(); // 结束当前TTS会话 + if (_ttsService.isPlaying && event.text.trim().isNotEmpty) { + // 停止当前TTS播放 + _ttsService.stop(); // 设置标志,表示应该取消当前的AI响应 _shouldCancelAiResponse = true; @@ -462,47 +246,141 @@ class BackgroundAgentService extends GetxService { // 取消当前的AI响应流订阅 _aiResponseSubscription?.cancel(); _aiResponseSubscription = null; + - // 清空待处理的TTS文本 - _pendingTtsText = ''; + // 添加日志输出中断TTS播放 + Logger.info('检测到用户说话,中断TTS播放'); } - - // 取消之前的静默计时器 - _silenceTimer?.cancel(); - - // 取消之前的无语音超时计时器,用户正在说话 - _noSpeechTimer?.cancel(); - _noSpeechTimer = null; - - // 设置新的静默计时器,如果 2 秒内没有新的识别结果,则认为用户已经停止说话 - _silenceTimer = Timer(const Duration(seconds: 2), () { - if (_hasRecognizedSpeech && !_hasFinalResult && - _recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - _recognitionCompleter!.complete(recognizedText.value); - } - }); } } else if (event.type == RecognitionEventType.error) { + // 添加日志输出识别错误 + Logger.error('语音识别错误: ${event.error}'); print('识别错误: ${event.error}'); } }, onError: (error) { + // 添加日志输出语音识别流错误 + Logger.error('语音识别流错误: $error'); print('语音识别流错误: $error'); _isListening = false; isListening.value = false; - - // 发生错误时完成 completer - if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - _recognitionCompleter!.completeError(error); - } }); + // 启动交互计时器 + _resetInteractionTimer(); + } catch (e) { + Logger.error('启动语音识别失败: $e'); print('启动语音识别失败: $e'); _isListening = false; isListening.value = false; rethrow; } } + + // 处理用户输入 + Future _processUserInput(String userInput) async { + if (userInput.isEmpty || _currentSystemPrompt == null) { + return; + } + + try { + // 保存用户消息到历史记录 + _messageHistory.add(Message( + role: 'user', + content: userInput, + timestamp: DateTime.now(), + )); + + // 构建用于生成回应的消息列表 + final messages = [ + {'role': 'system', 'content': _currentSystemPrompt!}, + {'role': 'user', 'content': userInput}, + ]; + + String fullResponse = ''; + + Logger.info('开始生成AI响应...'); + // 重置取消标志 + _shouldCancelAiResponse = false; + + // 创建一个本地变量来跟踪是否已取消 + bool isCancelled = false; + + + _ttsService.startSession(_defaultSpeaker); + // 获取AI响应流 + final responseStream = _aiService.sendMessageStream( + messages: messages, + systemPrompt: _currentSystemPrompt!, + ); + + // 创建一个订阅来处理响应流 + _aiResponseSubscription = responseStream.listen( + (chunk) { + // 如果已经设置了取消标志,则不处理这个块 + if (_shouldCancelAiResponse) { + isCancelled = true; + return; + } + + fullResponse += chunk; + + // 直接将每个文本块传递给 TTS 接口,不进行切句或攒句处理 + try { + // 使用新的 TTS API 播放语音 + _ttsService.speak(chunk, speaker: _defaultSpeaker); + + + // Logger.info('直接播放 AI 响应块: $chunk'); + } catch (e) { + Logger.error('TTS 播放出错: $e'); + } + }, + onError: (e) { + Logger.error('AI响应流错误: $e'); + print('AI响应流错误: $e'); + // 清理订阅 + _aiResponseSubscription = null; + }, + onDone: () { + _ttsService.endSession(); + // 如果已取消,不处理剩余的文本 + if (!isCancelled && !_shouldCancelAiResponse) { + Logger.info('AI 响应生成完成'); + } + + _aiResponseSubscription = null; + + // AI响应完成后,重新启动超时计时器 + _resetInteractionTimer(); + + // 如果响应被取消,不保存到历史记录 + if (!_shouldCancelAiResponse && fullResponse.isNotEmpty) { + // 保存助手回复到历史记录 + _messageHistory.add(Message( + role: 'assistant', + content: fullResponse, + timestamp: DateTime.now(), + )); + + // Logger.info('保存AI响应到历史记录,长度: ${fullResponse.length}'); + } + + // 限制历史记录长度 + if (_messageHistory.length > 20) { + _messageHistory.removeRange(0, _messageHistory.length - 20); + Logger.info('历史记录超过20条,已裁剪'); + } + } + ); + } catch (e) { + Logger.error('处理用户输入失败: $e'); + print('处理用户输入失败: $e'); + + // 播放错误提示 + await _ttsService.speak("抱歉,出现了一些问题", speaker: _defaultSpeaker); + } + } // 停止语音识别并返回识别的文本 Future stopVoiceRecognition() async { @@ -511,14 +389,6 @@ class BackgroundAgentService extends GetxService { } try { - // 取消静默计时器 - _silenceTimer?.cancel(); - _silenceTimer = null; - - // 取消无语音超时计时器 - _noSpeechTimer?.cancel(); - _noSpeechTimer = null; - // 取消订阅 await _recognitionSubscription?.cancel(); _recognitionSubscription = null; @@ -529,44 +399,28 @@ class BackgroundAgentService extends GetxService { // 获取最终识别结果 final result = recognizedText.value; + // 添加日志输出停止语音识别的最终结果 + Logger.info('停止语音识别,最终结果: $result'); + // 重置状态 _isListening = false; isListening.value = false; return result; } catch (e) { + // 添加日志输出停止语音识别失败 + Logger.error('停止语音识别失败: $e'); print('停止语音识别失败: $e'); _isListening = false; isListening.value = false; - // 尝试强制重置语音识别服务 - try { - await _voiceRecognitionService.initialize( - subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '', - serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia', - language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN', - ); - } catch (e) { - print('强制重置语音识别服务失败: $e'); - } - - return recognizedText.value; // 返回当前已识别的文本 + // 直接返回当前已识别的文本,不尝试重置 + final result = recognizedText.value; + Logger.info('停止语音识别失败,使用当前识别结果: $result'); + return result; } } - int _findLastSentenceEnd(String text) { - final sentenceEnds = [ - text.lastIndexOf('。'), - text.lastIndexOf('!'), - text.lastIndexOf('?'), - text.lastIndexOf('.'), - text.lastIndexOf('!'), - text.lastIndexOf('?'), - ]; - - return sentenceEnds.reduce((max, pos) => pos > max ? pos : max); - } - List get messageHistory => List.unmodifiable(_messageHistory); void clearHistory() { @@ -576,14 +430,14 @@ class BackgroundAgentService extends GetxService { @override void onClose() { _recognitionSubscription?.cancel(); - _silenceTimer?.cancel(); - _noSpeechTimer?.cancel(); + _interactionTimer?.cancel(); + _interactionTimer = null; _aiResponseSubscription?.cancel(); - _ttsEventSubscription?.cancel(); - _shouldContinueInteraction = false; - // 断开TTS服务连接 - _ttsService.disconnect(); + // 停止TTS播放 + if (_ttsService.isPlaying) { + _ttsService.stop(); + } super.onClose(); } diff --git a/lib/data/services/my_audio_handler.dart b/lib/data/services/my_audio_handler.dart index a89775f5f..88ece5c59 100644 --- a/lib/data/services/my_audio_handler.dart +++ b/lib/data/services/my_audio_handler.dart @@ -196,7 +196,7 @@ class MyAudioHandler extends BaseAudioHandler { print('应用在后台,使用 BackgroundAgent 处理交互'); if (_backgroundAgent != null) { // 调用 BackgroundAgent 的语音识别和 AI 交互功能 - await _backgroundAgent!.handleAgentInteraction(_systemPrompt); + await _backgroundAgent!.startAgentInteraction(_systemPrompt); } else { print('BackgroundAgent 未初始化,无法处理交互'); // 尝试重新初始化 BackgroundAgent diff --git a/lib/data/services/volcano_tts_api_service.dart b/lib/data/services/volcano_tts_api_service.dart index 87959d5e9..955398f8c 100644 --- a/lib/data/services/volcano_tts_api_service.dart +++ b/lib/data/services/volcano_tts_api_service.dart @@ -164,7 +164,10 @@ class VolcanoTtsApiService extends GetxController { /// 设置音频播放器状态监听 void _setupAudioPlayerListeners() { _audioPlayer.playerStateStream.listen((state) { - if (state.playing) { + final processingState = state.processingState; + final playing = state.playing; + + if (processingState == ProcessingState.ready && playing) { _isPlaying.value = true; } else { _isPlaying.value = false; @@ -173,16 +176,9 @@ class VolcanoTtsApiService extends GetxController { _audioPlayer.processingStateStream.listen((state) { if (state == ProcessingState.completed) { - if (kDebugMode) { - print('播放完成,处理状态: $state'); - } - // 播放完成后检查播放列表是否为空 if (_playlist.length > 0) { final currentIndex = _audioPlayer.currentIndex; - if (kDebugMode) { - print('播放列表中还有 ${_playlist.length} 个项目,当前索引: $currentIndex'); - } // 自动播放下一个项目 _advanceToNextItem(); @@ -197,16 +193,9 @@ class VolcanoTtsApiService extends GetxController { _audioPlayer.sequenceStateStream.listen((sequenceState) { if (sequenceState == null) return; - if (kDebugMode) { - print('序列状态变化: 当前索引=${sequenceState.currentIndex}, 序列长度=${sequenceState.sequence.length}'); - } - // 如果当前是最后一个项目且播放已完成,标记播放完成 if (sequenceState.currentIndex == sequenceState.sequence.length - 1 && _audioPlayer.processingState == ProcessingState.completed) { - if (kDebugMode) { - print('播放列表播放完成'); - } _isPlaying.value = false; } }); @@ -221,86 +210,41 @@ class VolcanoTtsApiService extends GetxController { _checkForNextItem(position); } }); - - // 监听播放错误 - _audioPlayer.playbackEventStream.listen((event) { - if (event.processingState == ProcessingState.completed) { - // 播放完成,已在上面处理 - } else if (event.processingState == ProcessingState.idle && _audioPlayer.playerState.playing == false) { - // 播放器空闲且未播放,可能是出错了 - if (kDebugMode) { - print('播放出错: ${_audioPlayer.playerState.processingState}'); - } - // 直接报告错误,不尝试恢复 - _eventController.add({ - 'eventType': 'playbackError', - 'error': '音频播放出错' - }); - } - }); - } - - /// 处理播放错误 - 已不再使用,保留方法签名以避免引用错误 - Future _handlePlaybackError() async { - // 不再尝试自动恢复,直接报告错误 - if (kDebugMode) { - print('播放出错,不尝试自动恢复'); - } } /// 检查是否需要准备下一个音频项目 Future _checkForNextItem(Duration position) async { - try { - // 只有在播放中且有下一个项目时才检查 - if (!_isPlaying.value || _audioPlayer.currentIndex == null) return; - - final currentIndex = _audioPlayer.currentIndex!; - final sequenceState = _audioPlayer.sequenceState; - - // 如果没有序列状态,则不需要准备 - if (sequenceState == null) return; - - // 防抖动:如果在短时间内(200毫秒)对同一个索引进行了检查,则跳过 - final now = DateTime.now(); - if (_lastCheckTime != null && - _lastCheckedIndex == currentIndex && - now.difference(_lastCheckTime!) < const Duration(milliseconds: 200)) { - return; - } - - // 更新最后检查时间和索引 - _lastCheckTime = now; - _lastCheckedIndex = currentIndex; - - // 获取当前项目的总时长 - final duration = _audioPlayer.duration; - if (duration == null) return; - - // 如果是最后一个项目,不需要预加载 - final isLastItem = currentIndex >= sequenceState.sequence.length - 1; - - // 如果接近结束(剩余时间小于200毫秒),准备下一个项目或完成播放 - if (duration - position <= const Duration(milliseconds: 200)) { - if (!isLastItem) { - // 如果不是最后一个项目,预加载下一个 - if (kDebugMode) { - print('当前音频(索引 $currentIndex)接近结束,准备下一个项目'); - } - - // 不再执行预加载操作,因为这可能导致播放不稳定 - // 让播放器自然过渡到下一个项目 - } else { - // 如果是最后一个项目,确保它能够完成播放 - if (kDebugMode) { - print('最后一个音频项目(索引 $currentIndex)接近结束'); - } - } - } - } catch (e) { - // 忽略错误,不影响正常播放 - if (kDebugMode) { - print('检查下一个项目时发生错误: $e'); - } + // 只有在播放中且有下一个项目时才检查 + if (!_isPlaying.value || _audioPlayer.currentIndex == null) return; + + final currentIndex = _audioPlayer.currentIndex!; + final sequenceState = _audioPlayer.sequenceState; + + // 如果没有序列状态,则不需要准备 + if (sequenceState == null) return; + + // 防抖动:如果在短时间内(200毫秒)对同一个索引进行了检查,则跳过 + final now = DateTime.now(); + if (_lastCheckTime != null && + _lastCheckedIndex == currentIndex && + now.difference(_lastCheckTime!) < const Duration(milliseconds: 200)) { + return; + } + + // 更新最后检查时间和索引 + _lastCheckTime = now; + _lastCheckedIndex = currentIndex; + + // 获取当前项目的总时长 + final duration = _audioPlayer.duration; + if (duration == null) return; + + // 如果是最后一个项目,不需要预加载 + final isLastItem = currentIndex >= sequenceState.sequence.length - 1; + + // 如果接近结束(剩余时间小于200毫秒),准备下一个项目或完成播放 + if (duration - position <= const Duration(milliseconds: 200) && !isLastItem) { + // 让播放器自然过渡到下一个项目 } } @@ -315,23 +259,15 @@ class VolcanoTtsApiService extends GetxController { if (sequenceState == null) { // 如果没有序列状态,直接开始播放 - if (kDebugMode) { - print('没有序列状态,从头开始播放'); - } await _audioPlayer.seek(Duration.zero, index: 0); await _audioPlayer.play(); - _isPlaying.value = true; return; } // 如果当前索引无效,从头开始播放 if (currentIndex == null || currentIndex < 0) { - if (kDebugMode) { - print('当前索引无效,从头开始播放'); - } await _audioPlayer.seek(Duration.zero, index: 0); await _audioPlayer.play(); - _isPlaying.value = true; return; } @@ -340,32 +276,19 @@ class VolcanoTtsApiService extends GetxController { // 检查是否还有下一个项目 if (nextIndex < sequenceState.sequence.length) { - if (kDebugMode) { - print('前进到下一个音频项目,索引: $nextIndex'); - } - // 跳转到下一个项目并开始播放 await _audioPlayer.seek(Duration.zero, index: nextIndex); await _audioPlayer.play(); - _isPlaying.value = true; } else { // 已经是最后一个项目 - if (kDebugMode) { - print('已经是最后一个音频项目,播放完成'); - } // 确保最后一个项目播放完成 if (_audioPlayer.position < _audioPlayer.duration!) { await _audioPlayer.seek(_audioPlayer.duration!); } - - // 标记为未播放状态 - _isPlaying.value = false; } } catch (e) { - if (kDebugMode) { - print('前进到下一个项目时发生错误: $e'); - } + // 简单记录错误,不做复杂处理 } } @@ -374,13 +297,12 @@ class VolcanoTtsApiService extends GetxController { if (_isConnected) { return true; // 已经连接 } - + print('连接'); + try { isConnecting.value = true; - if (kDebugMode) { - print('正在连接到火山语音服务...'); - } + // 生成连接ID _connectionId = const Uuid().v4(); @@ -390,9 +312,7 @@ class VolcanoTtsApiService extends GetxController { if (!kIsWeb && (io.Platform.isAndroid || io.Platform.isIOS || io.Platform.isMacOS || io.Platform.isLinux || io.Platform.isWindows)) { // 移动平台和桌面平台 - 使用IOWebSocketChannel - if (kDebugMode) { - print('使用IOWebSocketChannel连接: $uri'); - } + _channel = IOWebSocketChannel.connect( uri, @@ -407,10 +327,7 @@ class VolcanoTtsApiService extends GetxController { // Web平台 - 使用WebSocketChannel // 注意:在Web平台上,我们无法直接设置WebSocket头部 // 这可能会导致认证失败,需要与服务提供商确认Web平台的认证方式 - if (kDebugMode) { - print('警告:在Web平台上无法设置WebSocket头部,可能导致认证失败'); - print('使用WebSocketChannel连接: $uri'); - } + _channel = WebSocketChannel.connect(uri); } @@ -422,9 +339,7 @@ class VolcanoTtsApiService extends GetxController { cancelOnError: false, ); - if (kDebugMode) { - print('WebSocket连接已建立,发送开始连接事件...'); - } + // 发送开始连接事件 await _startConnection(); @@ -435,24 +350,15 @@ class VolcanoTtsApiService extends GetxController { // 设置超时 final timeout = Timer(const Duration(seconds: 10), () { if (!completer.isCompleted) { - if (kDebugMode) { - print('等待连接响应超时'); - } completer.complete(false); _handleError(Exception('连接超时')); } }); - if (kDebugMode) { - print('等待连接响应...'); - } // 监听连接事件 final subscription = eventStream.listen((event) { - if (kDebugMode) { - print('收到事件: ${event['eventType']}'); - } - + if (event['eventType'] == 'connectionStarted') { if (!completer.isCompleted) { if (kDebugMode) { @@ -479,15 +385,8 @@ class VolcanoTtsApiService extends GetxController { if (result) { _isConnected = true; isConnected.value = true; - if (kDebugMode) { - print('火山语音服务连接成功'); - } - } else { - if (kDebugMode) { - print('火山语音服务连接失败'); - } - } - + + } return result; } catch (e) { if (kDebugMode) { @@ -502,6 +401,7 @@ class VolcanoTtsApiService extends GetxController { /// 开始TTS会话 Future startSession(String speaker) async { + print('开始会话'); if (!_isConnected) { final connected = await connect(); if (!connected) return false; @@ -589,7 +489,8 @@ class VolcanoTtsApiService extends GetxController { if (!_isSessionActive) { return true; // 没有活跃会话 } - + print('结束会话'); + try { // 发送结束会话事件 await _finishSession(); @@ -630,6 +531,8 @@ class VolcanoTtsApiService extends GetxController { /// 断开连接 Future disconnect() async { + + print('断开连接'); if (!_isConnected) { return true; // 已经断开 } @@ -686,10 +589,6 @@ class VolcanoTtsApiService extends GetxController { /// 处理接收到的消息 void _handleMessage(dynamic message) { try { - if (kDebugMode) { - print('收到WebSocket消息: ${message.runtimeType}'); - } - if (message is! List) { _handleError(Exception('收到非二进制消息: $message')); return; @@ -707,53 +606,30 @@ class VolcanoTtsApiService extends GetxController { // 处理事件 final event = response['event'] as int?; if (event != null) { - if (kDebugMode) { - print('处理事件: $event'); - } - switch (event) { case eventConnectionStarted: - if (kDebugMode) { - print('连接已建立 (eventConnectionStarted)'); - } _eventController.add({'eventType': 'connectionStarted'}); break; case eventConnectionFailed: - if (kDebugMode) { - print('连接失败 (eventConnectionFailed): ${response['errorDetails']}'); - } _eventController.add({ 'eventType': 'connectionFailed', 'error': response['errorDetails'] ?? '连接失败' }); break; case eventSessionStarted: - if (kDebugMode) { - print('会话已开始 (eventSessionStarted)'); - } _eventController.add({'eventType': 'sessionStarted'}); break; case eventSessionFailed: - if (kDebugMode) { - print('会话失败 (eventSessionFailed): ${response['errorDetails']}'); - } _eventController.add({ 'eventType': 'sessionFailed', 'error': response['errorDetails'] ?? '会话失败' }); break; case eventTtsSentenceStart: - if (kDebugMode) { - print('TTS句子开始 (eventTtsSentenceStart)'); - } _eventController.add({'eventType': 'ttsSentenceStart'}); - // 准备新的音频流 _prepareAudioStream(); break; case eventTtsSentenceEnd: - if (kDebugMode) { - print('TTS句子结束 (eventTtsSentenceEnd)'); - } _eventController.add({'eventType': 'ttsSentenceEnd'}); // 结束音频流 _finishAudioStream(); @@ -763,46 +639,28 @@ class VolcanoTtsApiService extends GetxController { final payload = response['payload'] as Uint8List?; if (payload != null && payload.isNotEmpty) { try { - if (kDebugMode) { - print('收到TTS音频数据 (eventTtsResponse): ${payload.length} 字节'); - } _eventController.add({'eventType': 'ttsResponse'}); _audioDataController.add(payload); // 将音频数据添加到流中 _addAudioData(payload); } catch (audioError) { - if (kDebugMode) { - print('处理音频数据时发生错误: $audioError'); - } // 继续处理,不中断整个流程 } } break; case eventSessionFinished: - if (kDebugMode) { - print('会话已结束 (eventSessionFinished)'); - } _eventController.add({'eventType': 'sessionFinished'}); break; case eventConnectionFinished: - if (kDebugMode) { - print('连接已关闭 (eventConnectionFinished)'); - } _eventController.add({'eventType': 'connectionFinished'}); break; default: - if (kDebugMode) { - print('未知事件: $event'); - } _eventController.add({'eventType': 'unknown', 'eventCode': event}); break; } } } catch (e) { - if (kDebugMode) { - print('处理WebSocket消息时发生异常: $e'); - } _handleError(e); } } @@ -812,9 +670,7 @@ class VolcanoTtsApiService extends GetxController { try { // 清空音频缓冲区 _audioBuffer.clear(); - if (kDebugMode) { - print('准备新的音频流'); - } + } catch (e) { if (kDebugMode) { print('准备音频流时发生错误: $e'); @@ -828,9 +684,7 @@ class VolcanoTtsApiService extends GetxController { // 将音频数据添加到缓冲区 if (data.isNotEmpty) { _audioBuffer.add(data); - if (kDebugMode && _audioBuffer.length % 5 == 0) { - print('音频缓冲区现有 ${_audioBuffer.length} 块数据,总大小: ${_audioBuffer.fold(0, (sum, data) => sum + data.length)} 字节'); - } + } } catch (e) { if (kDebugMode) { @@ -844,10 +698,7 @@ class VolcanoTtsApiService extends GetxController { void _finishAudioStream() { try { if (_audioBuffer.isNotEmpty) { - if (kDebugMode) { - final totalSize = _audioBuffer.fold(0, (sum, data) => sum + data.length); - print('音频流已结束,准备播放 ${_audioBuffer.length} 块数据,总大小: $totalSize 字节'); - } + // 合并所有音频数据 final totalSize = _audioBuffer.fold(0, (sum, data) => sum + data.length); @@ -865,9 +716,7 @@ class VolcanoTtsApiService extends GetxController { // 将合并后的数据添加到播放列表 _addToPlaylist(mergedData); } else { - if (kDebugMode) { - print('音频流已结束,但没有收集到音频数据'); - } + } } catch (e) { if (kDebugMode) { @@ -882,10 +731,6 @@ class VolcanoTtsApiService extends GetxController { try { if (audioData.isEmpty) return; - if (kDebugMode) { - print('将音频数据添加到播放列表,大小: ${audioData.length} 字节'); - } - // 确保播放列表已初始化 if (!_playlistInitialized) { await _initializePlaylist(); @@ -894,29 +739,17 @@ class VolcanoTtsApiService extends GetxController { // 创建音频源 final audioSource = BytesAudioSource(audioData); - // 记录当前播放状态和位置 - final wasPlaying = _isPlaying.value; - final currentPosition = await _audioPlayer.position; - final currentIndex = _audioPlayer.currentIndex; - // 添加到播放列表 await _playlist.add(audioSource); - if (kDebugMode) { - print('音频已添加到播放列表,当前列表长度: ${_playlist.length}'); - } - // 如果当前没有播放,开始播放 if (!_isPlaying.value) { await _startPlayback(); - } else if (wasPlaying && _audioPlayer.processingState == ProcessingState.completed) { + } else if (_audioPlayer.processingState == ProcessingState.completed) { // 如果播放已完成但状态还没更新,手动前进到下一个 await _advanceToNextItem(); } } catch (e) { - if (kDebugMode) { - print('添加音频到播放列表失败: $e'); - } // 直接报告错误,不尝试恢复 _eventController.add({ 'eventType': 'playbackError', @@ -925,37 +758,13 @@ class VolcanoTtsApiService extends GetxController { } } - /// 重新初始化播放列表 - 已不再使用自动恢复功能,保留方法签名以避免引用错误 - Future _reinitializePlaylist() async { - if (kDebugMode) { - print('重新初始化播放列表方法已被调用,但不再执行自动恢复'); - } - - // 直接报告错误 - _eventController.add({ - 'eventType': 'playbackError', - 'error': '播放列表初始化失败' - }); - - // 标记为未初始化 - _playlistInitialized = false; - throw Exception('播放列表初始化失败,不再尝试自动恢复'); - } - /// 开始播放收集到的音频数据 Future _startPlayback() async { try { if (!_playlistInitialized || _playlist.length == 0) { - if (kDebugMode) { - print('播放列表为空或未初始化,无法播放'); - } return; } - if (kDebugMode) { - print('开始播放音频列表,列表长度: ${_playlist.length}'); - } - // 检查当前播放状态 final processingState = _audioPlayer.processingState; @@ -967,32 +776,18 @@ class VolcanoTtsApiService extends GetxController { nextIndex = math.min(_audioPlayer.currentIndex! + 1, _playlist.length - 1); } - if (kDebugMode) { - print('播放已完成,跳转到索引 $nextIndex'); - } - // 跳转到下一个项目 await _audioPlayer.seek(Duration.zero, index: nextIndex); } // 开始播放 await _audioPlayer.play(); - - // 更新播放状态 - _isPlaying.value = true; } catch (e) { - if (kDebugMode) { - print('开始播放时发生错误: $e'); - } - // 直接报告错误,不尝试恢复 _eventController.add({ 'eventType': 'playbackError', 'error': '开始播放失败: $e' }); - - // 更新播放状态 - _isPlaying.value = false; } } @@ -1017,9 +812,7 @@ class VolcanoTtsApiService extends GetxController { _eventController.add({'eventType': 'disconnected'}); - if (kDebugMode) { - print('火山语音WebSocket连接已关闭'); - } + } /// 解析响应 @@ -1489,20 +1282,49 @@ class VolcanoTtsApiService extends GetxController { void toggleEnabled() { // 这个方法在当前实现中不需要特殊处理 } - - /// 重新初始化音频播放器 - 已不再使用自动恢复功能,保留方法签名以避免引用错误 - Future _reinitializeAudioPlayer() async { - if (kDebugMode) { - print('重新初始化音频播放器方法已被调用,但不再执行自动恢复'); - } - - // 直接报告错误 - _eventController.add({ - 'eventType': 'playbackError', - 'error': '音频播放器初始化失败' - }); + + /// 单句合成辅助函数 + /// + /// 一次性完成连接、会话创建、合成和播放的过程 + /// 适合单句或少量文本的快速合成场景 + /// + /// [text] 要合成的文本 + /// [speaker] 发音人,如果为null则使用默认发音人 + /// [autoDisconnect] 合成完成后是否自动断开连接,默认为false + /// + /// 返回合成是否成功 + Future speakSingle(String text, {String? speaker}) async { + if (text.isEmpty) return false; - throw Exception('音频播放器初始化失败,不再尝试自动恢复'); + try { + // 使用默认发音人 + final actualSpeaker = speaker ?? defaultSpeaker; + + // 1. 确保连接 + if (!_isConnected) { + final connected = await connect(); + if (!connected) return false; + } + + // 2. 确保会话已开始 + if (!_isSessionActive) { + final sessionStarted = await startSession(actualSpeaker); + if (!sessionStarted) return false; + } + + // 3. 合成文本 + isSynthesizing.value = true; + final success = await synthesize(text, actualSpeaker); + + // 不再自动断开连接,让调用者决定何时结束会话和断开连接 + await endSession(); + + return success; + } catch (e) { + _handleError(e); + + return false; + } } } diff --git a/lib/data/services/volcano_tts_service.dart b/lib/data/services/volcano_tts_service.dart index aae6b1586..21bb902e4 100644 --- a/lib/data/services/volcano_tts_service.dart +++ b/lib/data/services/volcano_tts_service.dart @@ -228,10 +228,8 @@ class VolcanoTtsService extends GetxService { return _voiceType; } - /// 检查是否正在播放(简化版,直接返回true) - bool isActuallyPlaying() { - return true; - } + /// 检查是否正在播放 + bool get isPlaying => true; /// 检查是否正在合成(简化版,直接返回true) bool isActuallySynthesizing() { diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index e6c96982a..7d772910a 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -80,7 +80,29 @@ class ChatController extends GetxController { // 如果TTS正在播放,先停止它 if (_isTtsSpeaking) { - _ttsService.endSession(); + try { + // 立即停止当前声音播放 + _ttsService.stop(); + // 结束当前TTS会话 + _ttsService.endSession(); + _isTtsSpeaking = false; + } catch (e) { + debugPrint('停止TTS播放失败: $e'); + } + } + + // 如果正在处理AI响应,中断处理 + if (isLoading.value) { + isLoading.value = false; + // 清除当前正在生成的消息 + if (_currentAssistantMessage != null) { + // 如果消息内容为空,则移除该消息 + if (_currentAssistantMessage!.content.isEmpty) { + messages.remove(_currentAssistantMessage); + } + _currentAssistantMessage = null; + } + currentStreamMessage.value = ''; } isVoiceInputVisible.value = true; @@ -125,6 +147,33 @@ class ChatController extends GetxController { if (!isVoiceConnecting.value && isVoiceInputVisible.value) { // 不再需要禁用TTS,因为已启用回声消除 + // 当用户开始说话时,停止当前TTS播放 + if (_isTtsSpeaking) { + try { + // 立即停止当前声音播放 + _ttsService.stop(); + // 结束当前TTS会话 + _ttsService.endSession(); + _isTtsSpeaking = false; + } catch (e) { + debugPrint('停止TTS播放失败: $e'); + } + } + + // 如果正在处理AI响应,中断处理 + if (isLoading.value) { + isLoading.value = false; + // 清除当前正在生成的消息 + if (_currentAssistantMessage != null) { + // 如果消息内容为空,则移除该消息 + if (_currentAssistantMessage!.content.isEmpty) { + messages.remove(_currentAssistantMessage); + } + _currentAssistantMessage = null; + } + currentStreamMessage.value = ''; + } + isRecording.value = true; } } @@ -188,6 +237,18 @@ class ChatController extends GetxController { void handleRecognizing(String text) { if (text.isEmpty) return; + // 如果TTS正在播放,确保停止它 + if (_isTtsSpeaking) { + try { + // 立即停止当前声音播放 + _ttsService.stop(); + // 不需要每次都结束会话,只需要停止当前播放 + _isTtsSpeaking = false; + } catch (e) { + debugPrint('停止TTS播放失败: $e'); + } + } + // 更新识别中的文本 recordingText.value = text; @@ -282,6 +343,13 @@ class ChatController extends GetxController { bool ttsSessionStarted = false; const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; + // 创建一个标志,用于跟踪流是否应该继续处理 + bool shouldContinueProcessing = true; + + // 创建监听器,用于检测用户交互 + StreamSubscription? voiceInputSubscription; + StreamSubscription? voicePanelSubscription; + try { isLoading.value = true; @@ -305,12 +373,52 @@ class ChatController extends GetxController { ttsSessionStarted = await _ttsService.startSession(speaker); } + // 设置监听器,检测用户是否开始说话 + voiceInputSubscription = isRecording.listen((isRecordingNow) { + if (isRecordingNow) { + // 用户开始说话,设置标志为false + shouldContinueProcessing = false; + + // 如果TTS正在播放,立即停止 + if (_isTtsSpeaking) { + try { + _ttsService.stop(); + _isTtsSpeaking = false; + } catch (e) { + debugPrint('停止TTS播放失败: $e'); + } + } + } + }); + + // 设置监听器,检测用户是否打开语音输入面板 + voicePanelSubscription = isVoiceInputVisible.listen((isVisible) { + if (isVisible) { + // 用户打开语音输入面板,设置标志为false + shouldContinueProcessing = false; + + // 如果TTS正在播放,立即停止 + if (_isTtsSpeaking) { + try { + _ttsService.stop(); + _isTtsSpeaking = false; + } catch (e) { + debugPrint('停止TTS播放失败: $e'); + } + } + } + }); + // 处理AI流式响应 await for (final chunk in _aiService.sendMessageStream( messages: messages.map((m) => {'role': m.role, 'content': m.content}).toList(), systemPrompt: systemPrompt, )) { - if (_isDisposed) break; + // 检查是否应该继续处理 + if (_isDisposed || !shouldContinueProcessing) { + // 用户开始说话或控制器已销毁,中止流处理 + break; + } // 更新UI currentStreamMessage.value += chunk; @@ -346,6 +454,10 @@ class ChatController extends GetxController { } } } finally { + // 取消监听器 + voiceInputSubscription?.cancel(); + voicePanelSubscription?.cancel(); + // 重置状态 if (!_isDisposed) { _currentAssistantMessage = null; @@ -607,12 +719,32 @@ class ChatController extends GetxController { // 停止语音输入和TTS stopVoiceInput(); - _ttsService.endSession(); - _ttsEventSubscription?.cancel(); + + // 确保TTS完全停止并清理资源 + if (isTtsEnabled.value) { + try { + + _ttsService.stop(); + // 停止当前TTS播放并清除未播放的数据 + _ttsService.endSession(); + + // 确保断开TTS连接,释放所有资源 + _ttsService.disconnect(); + } catch (e) { + debugPrint('关闭TTS服务时出错: $e'); + } + } + + // 取消TTS事件订阅 + if (_ttsEventSubscription != null) { + _ttsEventSubscription!.cancel(); + _ttsEventSubscription = null; + } // 重置状态 currentStreamMessage.value = ''; _currentAssistantMessage = null; + _isTtsSpeaking = false; // 释放控制器 messageController.dispose(); diff --git a/lib/modules/chat/controllers/voice_input_controller.dart b/lib/modules/chat/controllers/voice_input_controller.dart index 4275c7c0d..44f7fd14d 100644 --- a/lib/modules/chat/controllers/voice_input_controller.dart +++ b/lib/modules/chat/controllers/voice_input_controller.dart @@ -158,8 +158,9 @@ class VoiceInputController extends GetxController { isUserSpeaking.value = true; // 如果系统正在播放TTS,检测到用户开始说话时立即中断 - if (_isTtsSpeaking && event.text.trim().isNotEmpty) { + if (event.text.trim().isNotEmpty) { print('检测到用户开始说话,中断TTS播放'); + _ttsService.stop(); _ttsService.endSession(); // 结束当前TTS会话 } diff --git a/lib/modules/test/controllers/tts_test_controller.dart b/lib/modules/test/controllers/tts_test_controller.dart index e148b085e..aef169083 100644 --- a/lib/modules/test/controllers/tts_test_controller.dart +++ b/lib/modules/test/controllers/tts_test_controller.dart @@ -13,6 +13,12 @@ class TtsTestController extends GetxController { final isLoading = false.obs; final statusMessage = '未连接'.obs; + // 添加实际音频播放状态 + final isAudioPlaying = false.obs; + + // 播放状态检查定时器 + Timer? _playbackCheckTimer; + // 当前播放的索引 final currentPlayingIndex = RxInt(-1); @@ -33,6 +39,7 @@ class TtsTestController extends GetxController { // 初始化UI状态 isSpeaking.value = false; + isAudioPlaying.value = false; errorMessage.value = ''; isLoading.value = false; @@ -43,6 +50,7 @@ class TtsTestController extends GetxController { } else { statusMessage.value = '未连接'; isSpeaking.value = false; + isAudioPlaying.value = false; } }); @@ -53,7 +61,7 @@ class TtsTestController extends GetxController { // 监听TTS播放状态 ever(_ttsService.isPlaying.obs, (bool playing) { - isSpeaking.value = playing; + isAudioPlaying.value = playing; if (!playing && currentPlayingIndex.value >= 0) { // 如果播放结束,重置当前播放索引 currentPlayingIndex.value = -1; @@ -65,6 +73,9 @@ class TtsTestController extends GetxController { // 连接到TTS服务 _connectToTtsService(); + + // 启动播放状态检查定时器 + _startPlaybackCheckTimer(); } // 设置事件监听 @@ -154,12 +165,14 @@ class TtsTestController extends GetxController { /// 播放单条文本 Future speakText(int index) async { - if (index < 0 || index >= speechTexts.length || isSpeaking.value) { + if (index < 0 || index >= speechTexts.length || isAudioPlaying.value) { return; } try { isLoading.value = true; + isSpeaking.value = true; + isAudioPlaying.value = true; errorMessage.value = ''; currentPlayingIndex.value = index; @@ -177,14 +190,12 @@ class TtsTestController extends GetxController { // 播放文本 await _ttsService.speak(text, speaker: speaker); - // 等待播放完成 - while (_ttsService.isActuallyPlaying()) { - await Future.delayed(const Duration(milliseconds: 100)); - } - // 结束会话 await _ttsService.endSession(); + // 确保播放列表被清空 + await _ttsService.stop(); + } catch (e) { errorMessage.value = '播放出错'; if (kDebugMode) { @@ -192,19 +203,44 @@ class TtsTestController extends GetxController { } } finally { isLoading.value = false; + isSpeaking.value = false; + isAudioPlaying.value = false; currentPlayingIndex.value = -1; } } + // 启动播放状态检查定时器 + void _startPlaybackCheckTimer() { + _playbackCheckTimer?.cancel(); + _playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) { + // 更新实际播放状态 + final actuallyPlaying = _ttsService.isPlaying; + isAudioPlaying.value = actuallyPlaying; + + // 如果不再播放,确保重置状态 + if (!actuallyPlaying && isSpeaking.value) { + // 检查是否真的播放完成了 + if (!_ttsService.isSynthesizing.value && currentPlayingIndex.value >= 0) { + // 播放已完成,重置状态 + isSpeaking.value = false; + currentPlayingIndex.value = -1; + // 更新UI + update(); + } + } + }); + } + /// 连续播放所有文本 Future speakContinuousTexts() async { - if (isSpeaking.value || isLoading.value) { + if (isAudioPlaying.value || isLoading.value) { return; } try { isLoading.value = true; isSpeaking.value = true; + isAudioPlaying.value = true; errorMessage.value = ''; // 确保TTS服务已连接 @@ -230,11 +266,12 @@ class TtsTestController extends GetxController { // 播放当前文本 await _ttsService.speak(speechTexts[i], speaker: speaker); - + } // 结束会话 await _ttsService.endSession(); + statusMessage.value = '播放完成'; @@ -254,6 +291,11 @@ class TtsTestController extends GetxController { currentPlayingIndex.value = -1; isSpeaking.value = false; isLoading.value = false; + // 确保最终状态正确 + isAudioPlaying.value = false; + + // 强制更新UI + update(); } } @@ -281,6 +323,7 @@ class TtsTestController extends GetxController { } finally { isSpeaking.value = false; isLoading.value = false; + isAudioPlaying.value = false; } } @@ -289,6 +332,9 @@ class TtsTestController extends GetxController { // 取消事件订阅 _eventSubscription?.cancel(); + // 取消播放状态检查定时器 + _playbackCheckTimer?.cancel(); + // 断开TTS服务连接 _ttsService.disconnect(); diff --git a/lib/modules/test/views/tts_test_view.dart b/lib/modules/test/views/tts_test_view.dart index fa3646f0f..3d91782e9 100644 --- a/lib/modules/test/views/tts_test_view.dart +++ b/lib/modules/test/views/tts_test_view.dart @@ -44,17 +44,17 @@ class TtsTestView extends GetView { children: [ Obx(() { return ElevatedButton.icon( - onPressed: controller.isSpeaking.value || controller.isLoading.value + onPressed: controller.isAudioPlaying.value || controller.isLoading.value ? null : () => controller.speakContinuousTexts(), - icon: controller.isSpeaking.value + icon: controller.isAudioPlaying.value ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2) ) : const Icon(Icons.playlist_play), - label: Text(controller.isSpeaking.value ? '播放中...' : '全部播放'), + label: Text(controller.isAudioPlaying.value ? '播放中...' : '全部播放'), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, @@ -62,7 +62,7 @@ class TtsTestView extends GetView { ); }), Obx(() => ElevatedButton.icon( - onPressed: controller.isSpeaking.value + onPressed: controller.isAudioPlaying.value ? () => controller.stopSpeaking() : null, icon: const Icon(Icons.stop), @@ -103,7 +103,7 @@ class TtsTestView extends GetView { itemCount: controller.speechTexts.length, separatorBuilder: (context, index) => const Divider(height: 1), itemBuilder: (context, index) { - final isPlaying = controller.isSpeaking.value && + final isPlaying = controller.isAudioPlaying.value && controller.currentPlayingIndex.value == index; return ListTile( @@ -130,10 +130,10 @@ class TtsTestView extends GetView { child: CircularProgressIndicator(strokeWidth: 2), ) : null, - onTap: controller.isSpeaking.value || controller.isLoading.value + onTap: controller.isAudioPlaying.value || controller.isLoading.value ? null : () => controller.speakText(index), - enabled: !(controller.isSpeaking.value || controller.isLoading.value), + enabled: !(controller.isAudioPlaying.value || controller.isLoading.value), tileColor: isPlaying ? Colors.blue.withOpacity(0.1) : null, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), @@ -171,7 +171,7 @@ class TtsTestView extends GetView { style: TextStyle( color: controller.isLoading.value ? Colors.orange - : controller.isSpeaking.value + : controller.isAudioPlaying.value ? Colors.green : Colors.grey, fontWeight: FontWeight.bold,