From a566fec640e5b6cc833147b4a01f8962e2f9768a Mon Sep 17 00:00:00 2001 From: wolfplus Date: Wed, 26 Feb 2025 12:25:37 +0000 Subject: [PATCH] 0.02 --- android/app/src/main/AndroidManifest.xml | 8 + .../src/main/res/drawable/ic_notification.xml | 11 - lib/app/translations/app_translations.dart | 20 ++ lib/core/bindings/initial_binding.dart | 33 +- lib/data/services/audio_service.dart | 64 +++- lib/data/services/azure_tts_service.dart | 46 ++- .../services/background_agent_service.dart | 307 +++++------------- lib/data/services/my_audio_handler.dart | 79 ++++- lib/data/services/notification_service.dart | 304 +++++++++-------- lib/data/services/volcano_tts_service.dart | 90 ++++- lib/main.dart | 5 + .../chat/controllers/chat_controller.dart | 143 ++++++-- .../controllers/voice_input_controller.dart | 172 +++++++++- lib/modules/chat/views/chat_view.dart | 174 +++++++++- .../chat/widgets/voice_input_panel.dart | 211 +++++++++--- .../controllers/profile_controller.dart | 41 +++ lib/modules/profile/views/profile_view.dart | 119 ++----- 17 files changed, 1247 insertions(+), 580 deletions(-) delete mode 100644 android/app/src/main/res/drawable/ic_notification.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7ba6fb350..fb5dcdbaf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -43,6 +43,7 @@ + @@ -51,6 +52,13 @@ + + + - - - \ No newline at end of file diff --git a/lib/app/translations/app_translations.dart b/lib/app/translations/app_translations.dart index 938012d26..4ccb05710 100644 --- a/lib/app/translations/app_translations.dart +++ b/lib/app/translations/app_translations.dart @@ -28,6 +28,16 @@ class AppTranslations extends Translations { 'type_message': '发消息...', 'voice_enabled': '语音已开启', 'voice_disabled': '语音已关闭', + + // 设置页面 + 'settings': '设置', + 'language_settings': '语言设置', + 'about': '关于', + 'check_update': '检查更新', + 'clear_cache': '清除缓存', + 'home': '首页', + 'explore': '探索', + 'profile': '我的', }, 'en_US': { // General @@ -54,6 +64,16 @@ class AppTranslations extends Translations { 'type_message': 'Type a message...', 'voice_enabled': 'Voice Enabled', 'voice_disabled': 'Voice Disabled', + + // Settings + 'settings': 'Settings', + 'language_settings': 'Language Settings', + 'about': 'About', + 'check_update': 'Check for Updates', + 'clear_cache': 'Clear Cache', + 'home': 'Home', + 'explore': 'Explore', + 'profile': 'Profile', }, }; } \ No newline at end of file diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 7a531f52d..ce9b781a9 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -20,25 +20,30 @@ class InitialBinding extends Bindings { // 只注册全局控制器 Get.put(PermissionController(), permanent: true); - // 异步初始化各个服务 + // 异步初始化各个服务,确保顺序执行 _initializeServices(notificationService, audioService); } // 单独提取初始化服务的方法,以便更好地处理错误 - void _initializeServices(NotificationService notificationService, AudioServiceManager audioService) { - // 初始化通知服务 - notificationService.init().catchError((error) { - print('通知服务初始化失败: $error'); - return null; - }); + void _initializeServices(NotificationService notificationService, AudioServiceManager audioService) async { + try { + // 首先初始化通知服务 + await notificationService.init().catchError((error) { + print('通知服务初始化失败: $error'); + return null; + }); + + // 等待一段时间,确保通知权限请求完成 + await Future.delayed(Duration(seconds: 1)); + + // 然后初始化音频服务 + await audioService.init().then((_) { + print('AudioServiceManager 初始化完成,可以接收蓝牙耳机按键事件'); - // 初始化音频服务 - audioService.init().then((_) { - print('AudioServiceManager 初始化完成,可以接收蓝牙耳机按键事件'); - }).catchError((error) { - print('音频服务初始化失败: $error'); - return null; - }); + }); + } catch (e) { + print('服务初始化过程中发生错误: $e'); + } // 其他服务初始化可以在这里添加 } diff --git a/lib/data/services/audio_service.dart b/lib/data/services/audio_service.dart index ac086a5b2..a9ad58a4c 100644 --- a/lib/data/services/audio_service.dart +++ b/lib/data/services/audio_service.dart @@ -13,6 +13,9 @@ class AudioServiceManager extends GetxService { // 添加一个标志,表示是否已经尝试过初始化 bool _hasAttemptedInit = false; + + // 添加一个计时器,用于定期检查服务状态 + Timer? _serviceCheckTimer; Future init() async { print('初始化 AudioServiceManager'); @@ -24,8 +27,9 @@ class AudioServiceManager extends GetxService { // 如果已经尝试过初始化但失败了,不再重试 if (_hasAttemptedInit) { - print('AudioServiceManager 之前初始化失败,不再重试'); - return this; + print('AudioServiceManager 之前初始化失败,尝试重新初始化'); + // 重置标志,允许重新初始化 + _hasAttemptedInit = false; } _hasAttemptedInit = true; @@ -41,15 +45,14 @@ class AudioServiceManager extends GetxService { config: AudioServiceConfig( androidNotificationChannelId: 'com.example.deep_voice.channel.audio', androidNotificationChannelName: 'Deep Voice Audio Service', - // 使用新创建的通知图标 - androidNotificationIcon: 'drawable/ic_notification', - // 设置为 false,避免在没有用户交互时启动前台服务 + // 使用应用图标作为通知图标,确保这个图标存在 + androidNotificationIcon: 'mipmap/ic_launcher', + // 修复配置冲突:当androidStopForegroundOnPause为true时,androidNotificationOngoing才能为true androidNotificationOngoing: false, - // 设置为 true,当暂停时停止前台服务 + // 设置为true,允许在暂停时停止前台服务,避免与androidNotificationOngoing冲突 androidStopForegroundOnPause: true, androidShowNotificationBadge: true, notificationColor: Colors.blue, - // 设置为 false,避免自动启动前台服务 fastForwardInterval: const Duration(seconds: 10), rewindInterval: const Duration(seconds: 10), preloadArtwork: false, @@ -61,15 +64,45 @@ class AudioServiceManager extends GetxService { _isInitialized = true; print('AudioServiceManager 初始化成功'); + + // 启动定期检查服务状态的计时器 + _startServiceCheckTimer(); }); return this; } catch (e) { print('AudioServiceManager初始化失败: $e'); _isInitialized = false; // 确保失败时重置状态 + + // 延迟5秒后尝试重新初始化 + Future.delayed(Duration(seconds: 5), () { + print('尝试重新初始化 AudioServiceManager'); + _hasAttemptedInit = false; + init(); + }); + return this; // 即使失败也返回实例,避免空指针异常 } } + + // 启动定期检查服务状态的计时器 + void _startServiceCheckTimer() { + _serviceCheckTimer?.cancel(); + _serviceCheckTimer = Timer.periodic(Duration(minutes: 5), (timer) { + _checkAndRestartServiceIfNeeded(); + }); + } + + // 检查服务状态,如果需要则重启服务 + Future _checkAndRestartServiceIfNeeded() async { + if (!_isInitialized || _audioHandler == null) { + print('服务检查:AudioServiceManager 未初始化,尝试重新初始化'); + _hasAttemptedInit = false; + await init(); + return; + } + print('服务检查:AudioServiceManager 已初始化,服务状态: $_isInitialized'); + } // 获取音频处理器 MyAudioHandler? get audioHandler => _audioHandler; @@ -78,18 +111,35 @@ class AudioServiceManager extends GetxService { Future sendMediaButtonEvent() async { if (_audioHandler != null) { try { + print('发送媒体按钮事件'); await _audioHandler!.customAction('media_button'); } catch (e) { print('发送媒体按钮事件失败: $e'); + + // 如果发送失败,尝试重新初始化服务 + _isInitialized = false; + _hasAttemptedInit = false; + await init(); } } else { print('AudioHandler 未初始化,无法发送媒体按钮事件'); + // 尝试重新初始化 + _isInitialized = false; + _hasAttemptedInit = false; + await init(); } } + + // 手动触发媒体按钮事件(用于测试) + Future triggerMediaButtonEvent() async { + print('手动触发媒体按钮事件'); + await sendMediaButtonEvent(); + } @override void onClose() async { _isInitialized = false; + _serviceCheckTimer?.cancel(); // 停止现有的音频处理器 if (_audioHandler != null) { diff --git a/lib/data/services/azure_tts_service.dart b/lib/data/services/azure_tts_service.dart index 528ea2b62..cc0508e42 100644 --- a/lib/data/services/azure_tts_service.dart +++ b/lib/data/services/azure_tts_service.dart @@ -49,7 +49,11 @@ class AzureTtsService extends GetxService { final _audioPlayer = AudioPlayer(); final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []); final isEnabled = false.obs; - final isPlaying = false.obs; + + // 播放状态变化通知流 + final _playingStateController = StreamController.broadcast(); + Stream get playingStateStream => _playingStateController.stream; + StreamSubscription? _playbackEventSubscription; StreamSubscription? _playerStateSubscription; AudioSession? _audioSession; @@ -120,7 +124,9 @@ class AzureTtsService extends GetxService { _playerStateSubscription = _audioPlayer.playerStateStream.listen((state) { if (_isDisposed) return; print('播放器状态变化: playing=${state.playing},processingState=${state.processingState}'); - isPlaying.value = state.playing; + + // 通知播放状态变化 + _notifyPlayingStateChanged(state.playing && _playlist.length > 0); if (state.processingState == ProcessingState.completed) { _tryPlayNext(); @@ -160,6 +166,7 @@ class AzureTtsService extends GetxService { await _audioSession?.setActive(false); await stop(); await _audioPlayer.dispose(); + await _playingStateController.close(); } @override @@ -180,6 +187,10 @@ class AzureTtsService extends GetxService { await _audioPlayer.stop(); await _playlist.clear(); await _audioPlayer.setAudioSource(_playlist); + + // 通知播放状态变化 + _notifyPlayingStateChanged(false); + print('播放器重新初始化成功'); } catch (e) { print('播放器重新初始化失败: $e'); @@ -217,6 +228,32 @@ class AzureTtsService extends GetxService { print('播放文本失败: $e'); } } + + // 检查音频播放器是否真正在播放 + bool isActuallyPlaying() { + try { + // 检查播放器状态 + final isPlayerPlaying = _audioPlayer.playing; + + // 检查播放列表是否为空 + final hasAudioSource = _playlist.length > 0; + + // 只有当播放器正在播放且播放列表不为空时,才认为真正在播放 + return isPlayerPlaying && hasAudioSource; + } catch (e) { + print('检查实际播放状态失败: $e'); + return false; + } + } + + // 通知播放状态变化 + void _notifyPlayingStateChanged(bool isPlaying) { + try { + _playingStateController.add(isPlaying); + } catch (e) { + print('通知播放状态变化失败: $e'); + } + } void _extractFullSentences() { if (_pendingText.isEmpty) return; @@ -295,13 +332,14 @@ class AzureTtsService extends GetxService { Future stop() async { try { - print('停止播放'); await _audioPlayer.stop(); await _playlist.clear(); _pendingText = ''; _sentenceQueue.clear(); _isFetching = false; - isPlaying.value = false; + + // 通知播放状态变化 + _notifyPlayingStateChanged(false); } catch (e) { print('停止播放失败: $e'); } diff --git a/lib/data/services/background_agent_service.dart b/lib/data/services/background_agent_service.dart index 59fcca7f6..1f79c5f52 100644 --- a/lib/data/services/background_agent_service.dart +++ b/lib/data/services/background_agent_service.dart @@ -43,98 +43,69 @@ class BackgroundAgentService extends GetxService { // 最后一次识别到语音的时间 DateTime? _lastSpeechTime; - // 添加一个标志,表示系统是否正在播放 TTS - bool _isSpeaking = false; - // 添加一个变量来跟踪当前的AI响应流订阅 StreamSubscription? _aiResponseSubscription; // 添加一个标志,表示是否应该取消当前的AI响应 bool _shouldCancelAiResponse = false; - // 添加一个变量来跟踪最后一次 TTS 状态变化的时间 - DateTime? _lastTtsStateChangeTime; + // 添加计数器,用于跟踪连续无输入的次数 + int _noSpeechCount = 0; BackgroundAgentService() : _aiService = VolcanoAIService(), _ttsService = Get.find(), _voiceRecognitionService = Get.find() { - // 不再监听 TTS 播放状态,而是使用定期检查 - // 初始化 TTS 状态 - _isSpeaking = _ttsService.isActuallyPlaying(); - _lastTtsStateChangeTime = DateTime.now(); + // 简化构造函数,不需要定期检查TTS状态 + } + + // 启动无语音超时计时器 + void _startNoSpeechTimer() { + // 取消之前的计时器 + _noSpeechTimer?.cancel(); - // 添加定期检查实际播放状态的计时器 - Timer.periodic(const Duration(seconds: 1), (timer) { - // 检查实际播放状态 - final actuallyPlaying = _ttsService.isActuallyPlaying(); - - // 如果状态发生变化,更新状态并记录时间 - if (_isSpeaking != actuallyPlaying) { - print('TTS 播放状态变化: $_isSpeaking -> $actuallyPlaying'); - _isSpeaking = actuallyPlaying; - _lastTtsStateChangeTime = DateTime.now(); - - // 如果 TTS 服务的 isPlaying 与实际状态不一致,尝试修正 - if (actuallyPlaying != _ttsService.isPlaying.value) { - try { - _ttsService.isPlaying.value = actuallyPlaying; - print('已修正 TTS 服务的播放状态'); - } catch (e) { - print('修正 TTS 播放状态失败: $e'); - } - } - - // 如果 TTS 停止播放,且正在进行语音识别,重新启动无语音超时计时器 - if (!actuallyPlaying && _isListening && _hasRecognizedSpeech && _noSpeechTimer == null) { - _startNoSpeechTimer(); - } + // 设置15秒的超时计时器 + _noSpeechTimer = Timer(const Duration(seconds: 15), () { + // 检查TTS是否正在播放 + bool isTtsPlaying = false; + try { + isTtsPlaying = _ttsService.isActuallyPlaying(); + } catch (e) { + print('检查TTS播放状态失败: $e'); + isTtsPlaying = false; } - // 如果正在处理交互,进行更全面的检查 - if (_isProcessing) { - _checkAndUpdateTtsState(); + if (isTtsPlaying) { + // 如果TTS正在播放,重置定时器 + print('TTS正在播放,重置15秒超时计时器'); + _startNoSpeechTimer(); + } else { + // 如果TTS不在播放,且15秒内没有检测到用户输入,退出交互 + print('15秒内没有检测到用户输入,退出交互'); + _exitInteraction(); } }); } - - // 添加一个方法来检查和更新 TTS 状态 - void _checkAndUpdateTtsState() { + + // 退出交互 + Future _exitInteraction() async { + // 完成当前的recognitionCompleter(如果有) + if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { + _recognitionCompleter!.complete(''); + } + // 设置标志,停止循环交互 + _shouldContinueInteraction = false; + _isProcessing = false; + // 播放退出提示 try { - // 获取实际播放状态 - final actuallyPlaying = _ttsService.isActuallyPlaying(); - - // 如果状态显示正在播放,但已经很长时间没有状态变化,强制重置 - if (actuallyPlaying && _lastTtsStateChangeTime != null) { - final now = DateTime.now(); - final difference = now.difference(_lastTtsStateChangeTime!); - - // 如果超过15秒没有状态变化,强制重置 - if (difference.inSeconds > 15) { - print('强制重置 TTS 状态:已经 ${difference.inSeconds} 秒没有状态变化'); - - try { - // 停止 TTS 播放 - _ttsService.stop(); - - // 强制更新状态 - _isSpeaking = false; - _ttsService.isPlaying.value = false; - - // 更新最后状态变化时间 - _lastTtsStateChangeTime = DateTime.now(); - - print('TTS 状态已强制重置'); - } catch (e) { - print('强制重置 TTS 状态失败: $e'); - } - } - } + await _ttsService.speak("没有听到您说话,已退出语音交互"); } catch (e) { - print('检查 TTS 播放状态失败: $e'); + print('播放退出提示失败: $e'); } - } + + } + // 处理蓝牙耳机按钮触发的交互 Future handleAgentInteraction(String systemPrompt) async { if (_isProcessing) { @@ -148,11 +119,10 @@ class BackgroundAgentService extends GetxService { try { // 确保之前的语音识别已经停止 if (_isListening) { - print('交互开始前确保语音识别已停止'); await stopVoiceRecognition(); } - // 循环进行交互,直到用户 10 秒没有说话或手动停止 + // 循环进行交互,直到用户 30 秒没有说话或手动停止 while (_shouldContinueInteraction) { await _processSingleInteraction(systemPrompt); } @@ -164,7 +134,6 @@ class BackgroundAgentService extends GetxService { // 重置语音识别服务 try { - print('交互结束,重置语音识别服务'); await _voiceRecognitionService.initialize(); } catch (e) { print('重置语音识别服务失败: $e'); @@ -172,53 +141,6 @@ class BackgroundAgentService extends GetxService { } } - // 启动无语音超时计时器 - void _startNoSpeechTimer() { - // 检查 TTS 播放状态,使用 isActuallyPlaying 获取实际状态 - _isSpeaking = _ttsService.isActuallyPlaying(); - - // 如果系统正在播放 TTS,不启动计时器 - if (_isSpeaking) { - print('系统正在播放 TTS,不启动无语音超时计时器 (isPlaying.value: ${_ttsService.isPlaying.value}, 实际播放状态: $_isSpeaking)'); - - // 即使TTS正在播放,也设置一个更长的安全超时,防止系统永远卡在循环中 - _noSpeechTimer?.cancel(); - _noSpeechTimer = Timer(const Duration(seconds: 30), () { - // 再次检查 TTS 状态,使用 isActuallyPlaying - _isSpeaking = _ttsService.isActuallyPlaying(); - - print('安全超时触发:即使TTS正在播放,30秒内没有新的语音输入,强制退出循环交互 (实际播放状态: $_isSpeaking)'); - if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - _recognitionCompleter!.complete(''); - } - _shouldContinueInteraction = false; - }); - - return; - } - - // 取消之前的计时器 - _noSpeechTimer?.cancel(); - - // 设置新的计时器,如果 10 秒内没有新的识别结果,且系统没有在播放 TTS,则认为用户已经停止交互 - _noSpeechTimer = Timer(const Duration(seconds: 10), () { - // 再次检查是否正在播放 TTS,使用 isActuallyPlaying - _isSpeaking = _ttsService.isActuallyPlaying(); - - print('无语音超时计时器触发,当前 TTS 播放状态: $_isSpeaking (isPlaying.value: ${_ttsService.isPlaying.value}, 实际播放状态: $_isSpeaking)'); - - if (!_isSpeaking && _isListening && _recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - print('10秒内没有新的语音输入,且系统没有在播放 TTS,退出循环交互'); - _recognitionCompleter!.complete(''); - - // 直接设置标志,停止循环交互 - _shouldContinueInteraction = false; - } - }); - - print('启动无语音超时计时器,10秒后检查'); - } - // 处理单次交互 Future _processSingleInteraction(String systemPrompt) async { _isProcessing = true; @@ -226,9 +148,6 @@ class BackgroundAgentService extends GetxService { _hasFinalResult = false; try { - // 强制重置 TTS 状态,确保不会因为错误的状态导致系统卡住 - _resetTtsStateIfNeeded(); - // 先播放一个简短的提示音或提示语,表示开始监听 try { await _ttsService.speak("我在听"); @@ -238,19 +157,16 @@ class BackgroundAgentService extends GetxService { } // 开始语音识别 - bool recognitionStarted = false; String userInput = ''; try { // 确保之前的语音识别已经停止 if (_isListening) { - print('检测到上一次语音识别未正确停止,先停止它'); await stopVoiceRecognition(); } // 尝试启动语音识别 await startVoiceRecognition(); - recognitionStarted = true; // 创建一个 Completer 来处理语音识别完成 _recognitionCompleter = Completer(); @@ -265,55 +181,20 @@ class BackgroundAgentService extends GetxService { _noSpeechTimer?.cancel(); _noSpeechTimer = null; - // 如果没有识别到语音,则停止循环交互 - if (!_hasRecognizedSpeech || userInput.trim().isEmpty) { - print('没有识别到用户语音或识别结果为空,退出循环交互'); - - // 停止语音识别 - if (_isListening) { - await stopVoiceRecognition(); - } - - try { - await _ttsService.speak("没有听到您说话,已退出语音交互"); - } catch (e) { - print('播放退出提示失败: $e'); - } - - // 设置标志,停止循环交互 - _shouldContinueInteraction = false; - _isProcessing = false; + // 如果用户输入为空,重新启动超时计时器并返回 + if (userInput.trim().isEmpty) { + _startNoSpeechTimer(); return; } - - // 注意:不再停止语音识别,保持语音识别状态 - // 只有在退出交互时才停止语音识别 } catch (e) { print('语音识别过程出错: $e'); - + _startNoSpeechTimer(); // 出错时也启动超时计时器 + return; } finally { // 清理资源,但保持语音识别状态 _recognitionCompleter = null; _silenceTimer?.cancel(); _silenceTimer = null; - _noSpeechTimer?.cancel(); - _noSpeechTimer = null; - } - - if (userInput.isEmpty) { - try { - await _ttsService.speak("没有听到您说话"); - } catch (_) {} - - // 如果没有识别到语音,停止循环交互 - _shouldContinueInteraction = false; - _isProcessing = false; - - // 停止语音识别 - if (_isListening) { - await stopVoiceRecognition(); - } - return; } // 保存用户消息到历史记录 @@ -329,7 +210,6 @@ class BackgroundAgentService extends GetxService { {'role': 'user', 'content': userInput}, ]; - String fullResponse = ''; try { // 重置取消标志 @@ -391,6 +271,9 @@ class BackgroundAgentService extends GetxService { } _pendingTtsText = ''; _aiResponseSubscription = null; + + // AI响应完成后,重新启动超时计时器 + _startNoSpeechTimer(); } ); @@ -401,6 +284,14 @@ class BackgroundAgentService extends GetxService { print('AI响应生成失败: $e'); // 如果AI响应失败,使用默认回复 fullResponse = '抱歉,我现在无法回答您的问题。请稍后再试。'; + + // 播放错误提示 + try { + await _ttsService.speak(fullResponse); + } catch (_) {} + + // 启动超时计时器 + _startNoSpeechTimer(); } finally { // 清理资源 _aiResponseSubscription?.cancel(); @@ -423,11 +314,8 @@ class BackgroundAgentService extends GetxService { _messageHistory.removeRange(0, _messageHistory.length - 20); } - // 短暂暂停,然后开始下一轮交互 - await Future.delayed(const Duration(milliseconds: 500)); - } catch (e) { - print('Background agent interaction failed: $e'); + print('交互过程出错: $e'); try { await _ttsService.speak("抱歉,出现了一些问题"); } catch (_) {} @@ -448,14 +336,12 @@ class BackgroundAgentService extends GetxService { // 开始语音识别 Future startVoiceRecognition() async { if (_isListening) { - print('已经在监听中,先停止当前监听'); await stopVoiceRecognition(); } try { // 确保语音识别服务已初始化 if (!await _voiceRecognitionService.initialize()) { - print('语音识别服务初始化失败,尝试重新初始化'); // 尝试重新初始化 await Future.delayed(const Duration(milliseconds: 500)); if (!await _voiceRecognitionService.initialize()) { @@ -483,32 +369,38 @@ class BackgroundAgentService extends GetxService { // 收到最终结果,立即完成识别过程 if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - print('收到最终识别结果,立即处理: ${event.text}'); _recognitionCompleter!.complete(event.text); } } else { - // 如果最终结果为空,但之前有中间结果,仍然标记为有语音 - if (_hasRecognizedSpeech) { - print('最终识别结果为空,但之前有中间结果,使用最后的中间结果'); + // 如果最终结果为空,但有中间结果,使用最后的中间结果 + if (_hasRecognizedSpeech && recognizedText.value.isNotEmpty) { if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { _recognitionCompleter!.complete(recognizedText.value); } } else { - // 如果最终结果为空,且没有中间结果,重新启动无语音超时计时器 - print('最终识别结果为空,且没有中间结果,重新启动无语音超时计时器'); + // 如果最终结果为空,且没有中间结果,重新启动超时计时器 _startNoSpeechTimer(); } } - print('最终识别结果: ${event.text}'); } else if (event.type == RecognitionEventType.intermediateResult) { recognizedText.value = event.text; if (event.text.isNotEmpty) { _hasRecognizedSpeech = true; _lastSpeechTime = DateTime.now(); + // 检测到用户说话,重置无语音计时器 + _noSpeechTimer?.cancel(); + _startNoSpeechTimer(); + // 如果系统正在播放TTS或接收AI响应,检测到用户开始说话时立即中断 - if (_isSpeaking && event.text.trim().isNotEmpty) { - print('检测到用户开始说话,中断TTS播放和AI响应'); + bool isTtsPlaying = false; + try { + isTtsPlaying = _ttsService.isActuallyPlaying(); + } catch (e) { + isTtsPlaying = false; + } + + if (isTtsPlaying && event.text.trim().isNotEmpty) { _ttsService.stop(); // 停止当前TTS播放 // 设置标志,表示应该取消当前的AI响应 @@ -525,7 +417,7 @@ class BackgroundAgentService extends GetxService { // 取消之前的静默计时器 _silenceTimer?.cancel(); - // 取消之前的无语音超时计时器 + // 取消之前的无语音超时计时器,用户正在说话 _noSpeechTimer?.cancel(); _noSpeechTimer = null; @@ -533,12 +425,10 @@ class BackgroundAgentService extends GetxService { _silenceTimer = Timer(const Duration(seconds: 2), () { if (_hasRecognizedSpeech && !_hasFinalResult && _recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { - print('用户停止说话 2 秒,使用当前识别结果: ${recognizedText.value}'); _recognitionCompleter!.complete(recognizedText.value); } }); } - print('中间识别结果: ${event.text}'); } else if (event.type == RecognitionEventType.error) { print('识别错误: ${event.error}'); } @@ -563,11 +453,11 @@ class BackgroundAgentService extends GetxService { // 停止语音识别并返回识别的文本 Future stopVoiceRecognition() async { - if (!_isListening) return ''; + if (!_isListening) { + return ''; + } try { - print('停止语音识别...'); - // 取消静默计时器 _silenceTimer?.cancel(); _silenceTimer = null; @@ -590,7 +480,6 @@ class BackgroundAgentService extends GetxService { _isListening = false; isListening.value = false; - print('语音识别已停止'); return result; } catch (e) { print('停止语音识别失败: $e'); @@ -600,7 +489,9 @@ class BackgroundAgentService extends GetxService { // 尝试强制重置语音识别服务 try { await _voiceRecognitionService.initialize(); - } catch (_) {} + } catch (e) { + print('强制重置语音识别服务失败: $e'); + } return recognizedText.value; // 返回当前已识别的文本 } @@ -634,38 +525,4 @@ class BackgroundAgentService extends GetxService { _shouldContinueInteraction = false; super.onClose(); } - - // 添加一个方法来强制重置 TTS 状态 - void _resetTtsStateIfNeeded() { - // 检查 TTS 状态,使用 isActuallyPlaying 获取实际状态 - bool isCurrentlyPlaying = false; - try { - isCurrentlyPlaying = _ttsService.isActuallyPlaying(); - } catch (e) { - print('获取实际播放状态失败: $e'); - isCurrentlyPlaying = _ttsService.isPlaying.value; - } - - // 如果实际状态与当前状态不一致,更新它 - if (_isSpeaking != isCurrentlyPlaying) { - print('重置前检测到 TTS 播放状态不一致,修正: _isSpeaking=$_isSpeaking, 实际=${isCurrentlyPlaying}'); - _isSpeaking = isCurrentlyPlaying; - - // 如果实际状态与 TTS 服务的 isPlaying 不一致,尝试修正 TTS 服务的状态 - if (isCurrentlyPlaying != _ttsService.isPlaying.value) { - try { - _ttsService.isPlaying.value = isCurrentlyPlaying; - print('已修正 TTS 服务的播放状态'); - } catch (e) { - print('修正 TTS 播放状态失败: $e'); - } - } - - // 更新最后一次状态变化的时间 - _lastTtsStateChangeTime = DateTime.now(); - } - - // 检查是否需要强制重置 - _checkAndUpdateTtsState(); - } } \ No newline at end of file diff --git a/lib/data/services/my_audio_handler.dart b/lib/data/services/my_audio_handler.dart index 5c845b786..a89775f5f 100644 --- a/lib/data/services/my_audio_handler.dart +++ b/lib/data/services/my_audio_handler.dart @@ -17,6 +17,12 @@ class MyAudioHandler extends BaseAudioHandler { // 添加一个标志,表示是否正在处理交互 bool _isHandlingInteraction = false; + + // 添加一个计时器,用于定期更新播放状态 + Timer? _statusUpdateTimer; + + // 添加一个变量,记录最后一次媒体按钮事件的时间 + DateTime? _lastMediaButtonEventTime; Stream get customEventStream => _customEventController.stream; @@ -24,6 +30,7 @@ class MyAudioHandler extends BaseAudioHandler { _initializeHandler(); _initializeBackgroundAgent(); _initializeSystemPrompt(); + _startStatusUpdateTimer(); print('MyAudioHandler 已创建,准备接收媒体按钮事件'); } @@ -93,10 +100,33 @@ class MyAudioHandler extends BaseAudioHandler { mediaItem.add(item); print('AudioHandler 媒体项已设置'); + + // 立即更新播放状态,确保通知显示 + _updatePlaybackState( + playing: true, + processingState: AudioProcessingState.ready, + ); } catch (e) { print('初始化 AudioHandler 失败: $e'); } } + + // 启动定期更新播放状态的计时器 + void _startStatusUpdateTimer() { + _statusUpdateTimer?.cancel(); + _statusUpdateTimer = Timer.periodic(Duration(minutes: 5), (timer) { + try { + // 定期更新播放状态,确保服务保持活跃 + _updatePlaybackState( + playing: true, + processingState: AudioProcessingState.ready, + ); + print('定期更新播放状态成功'); + } catch (e) { + print('定期更新播放状态失败: $e'); + } + }); + } // 更新播放状态 void _updatePlaybackState({ @@ -111,6 +141,11 @@ class MyAudioHandler extends BaseAudioHandler { processingState: processingState, updatePosition: Duration.zero, )); + + // 如果状态变为播放,确保前台服务启动 + if (playing) { + print('更新播放状态为播放中,确保前台服务启动'); + } } catch (e) { print('更新播放状态失败: $e'); } @@ -128,6 +163,12 @@ class MyAudioHandler extends BaseAudioHandler { _isHandlingInteraction = true; try { + // 更新播放状态,确保通知显示 + _updatePlaybackState( + playing: true, + processingState: AudioProcessingState.ready, + ); + if (_systemPrompt.isEmpty) { print('系统提示为空,无法激活语音助手'); return; @@ -158,6 +199,8 @@ class MyAudioHandler extends BaseAudioHandler { await _backgroundAgent!.handleAgentInteraction(_systemPrompt); } else { print('BackgroundAgent 未初始化,无法处理交互'); + // 尝试重新初始化 BackgroundAgent + _initializeBackgroundAgent(); } } } catch (e) { @@ -171,11 +214,11 @@ class MyAudioHandler extends BaseAudioHandler { Future play() async { print('收到播放命令'); try { - await _handleInteraction(); _updatePlaybackState( playing: true, processingState: AudioProcessingState.ready, ); + await _handleInteraction(); } catch (e) { print('处理播放命令失败: $e'); } @@ -185,11 +228,11 @@ class MyAudioHandler extends BaseAudioHandler { Future pause() async { print('收到暂停命令'); try { - await _handleInteraction(); _updatePlaybackState( playing: false, processingState: AudioProcessingState.ready, ); + await _handleInteraction(); } catch (e) { print('处理暂停命令失败: $e'); } @@ -200,6 +243,7 @@ class MyAudioHandler extends BaseAudioHandler { print('停止 AudioHandler'); _isDisposed = true; try { + _statusUpdateTimer?.cancel(); await super.stop(); _customEventController.close(); _updatePlaybackState( @@ -215,6 +259,10 @@ class MyAudioHandler extends BaseAudioHandler { Future skipToPrevious() async { print('收到上一曲命令'); try { + _updatePlaybackState( + playing: true, + processingState: AudioProcessingState.ready, + ); await _handleInteraction(); } catch (e) { print('处理上一曲命令失败: $e'); @@ -225,6 +273,10 @@ class MyAudioHandler extends BaseAudioHandler { Future skipToNext() async { print('收到下一曲命令'); try { + _updatePlaybackState( + playing: true, + processingState: AudioProcessingState.ready, + ); await _handleInteraction(); } catch (e) { print('处理下一曲命令失败: $e'); @@ -243,12 +295,33 @@ class MyAudioHandler extends BaseAudioHandler { try { if (name == 'media_button') { print('收到媒体按钮事件'); + + // 检查是否是重复触发(防止短时间内多次触发) + final now = DateTime.now(); + if (_lastMediaButtonEventTime != null) { + final difference = now.difference(_lastMediaButtonEventTime!); + if (difference.inSeconds < 2) { + print('媒体按钮事件触发过于频繁,忽略此次事件'); + return; + } + } + + // 更新最后一次事件时间 + _lastMediaButtonEventTime = now; + + _updatePlaybackState( + playing: true, + processingState: AudioProcessingState.ready, + ); await _handleInteraction(); + } else if (name == 'ping') { + // 用于检查服务是否响应的测试命令 + print('收到 ping 命令,服务正常运行'); + return 'pong'; } } catch (e) { print('处理自定义命令失败: $e'); } - return null; } @override diff --git a/lib/data/services/notification_service.dart b/lib/data/services/notification_service.dart index 15bb45c21..f73201175 100644 --- a/lib/data/services/notification_service.dart +++ b/lib/data/services/notification_service.dart @@ -19,133 +19,164 @@ class NotificationService extends GetxService { } Future init() async { - if (Platform.isIOS || Platform.isAndroid || Platform.isMacOS) { - _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - - const initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); - - final initializationSettingsDarwin = DarwinInitializationSettings( - requestSoundPermission: false, - requestBadgePermission: false, - requestAlertPermission: false, - ); - - final initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsDarwin, - macOS: initializationSettingsDarwin, - ); - - await _flutterLocalNotificationsPlugin?.initialize( - initializationSettings, - onDidReceiveNotificationResponse: (details) { - if (Get.currentRoute != '/chat') { - Get.toNamed('/chat'); - } - }, - ); - } + try { + if (Platform.isIOS || Platform.isAndroid || Platform.isMacOS) { + _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + + // 使用应用图标作为通知图标 + const initializationSettingsAndroid = AndroidInitializationSettings('mipmap/ic_launcher'); + + final initializationSettingsDarwin = DarwinInitializationSettings( + requestSoundPermission: false, + requestBadgePermission: false, + requestAlertPermission: false, + ); + + final initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + macOS: initializationSettingsDarwin, + ); + + await _flutterLocalNotificationsPlugin?.initialize( + initializationSettings, + onDidReceiveNotificationResponse: (details) { + if (Get.currentRoute != '/chat') { + Get.toNamed('/chat'); + } + }, + ); + } - // 创建通知渠道 - if (Platform.isAndroid) { - await _flutterLocalNotificationsPlugin?.resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel( - const AndroidNotificationChannel( - 'foreground_service', - 'Foreground Service', - description: 'Used for the foreground service notification', - importance: Importance.min, - playSound: false, - enableVibration: false, - showBadge: false, - ), - ); - } + // 创建通知渠道 - 使用高重要性确保通知在后台可见 + if (Platform.isAndroid) { + await _flutterLocalNotificationsPlugin?.resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel( + const AndroidNotificationChannel( + 'foreground_service', + 'Foreground Service', + description: 'Used for the foreground service notification', + importance: Importance.high, // 提高重要性 + playSound: false, + enableVibration: false, + showBadge: true, + ), + ); + } - await _requestPermissionIfNeeded(); - - // 在初始化完成后显示前台服务通知 - if (Platform.isAndroid) { - await showForegroundNotification(); + // 请求权限,但避免重复请求 + await _requestPermissionIfNeeded(); + + // 在初始化完成后显示前台服务通知 + if (Platform.isAndroid) { + await showForegroundNotification(); + } + + // Add macOS-specific code for status bar icon + if (Platform.isMacOS) { + await showMacOSStatusBarIcon(); + } + } catch (e) { + print('通知服务初始化失败: $e'); } } Future showForegroundNotification() async { if (_notificationActive) return; - // 在显示通知前确保有权限 - await _requestPermissionIfNeeded(); - - final androidDetails = AndroidNotificationDetails( - 'foreground_service', - 'Foreground Service', - channelDescription: 'Used for the foreground service notification', - importance: Importance.min, - priority: Priority.min, - ongoing: true, - autoCancel: false, - showWhen: false, - enableVibration: false, - playSound: false, - icon: '@mipmap/ic_launcher', - category: AndroidNotificationCategory.service, - actions: [ - const AndroidNotificationAction( - 'chat', - '开始对话', - showsUserInterface: true, - ), - ], - ); - - const iosDetails = DarwinNotificationDetails( - presentAlert: false, - presentBadge: false, - presentSound: false, - interruptionLevel: InterruptionLevel.passive, - ); - - final notificationDetails = NotificationDetails( - android: androidDetails, - iOS: iosDetails, - ); - - await _flutterLocalNotificationsPlugin?.show( - 0, - _defaultAgentName, - '点击开始对话', - notificationDetails, - ); - - _notificationActive = true; + try { + final androidDetails = AndroidNotificationDetails( + 'foreground_service', + 'Foreground Service', + channelDescription: 'Used for the foreground service notification', + importance: Importance.high, // 提高重要性 + priority: Priority.high, // 提高优先级 + ongoing: true, + autoCancel: false, + showWhen: false, + enableVibration: false, + playSound: false, + icon: 'mipmap/ic_launcher', // 使用应用图标 + visibility: NotificationVisibility.public, + category: AndroidNotificationCategory.service, + actions: [ + const AndroidNotificationAction( + 'chat', + '开始对话', + showsUserInterface: true, + ), + ], + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: false, + presentBadge: false, + presentSound: false, + interruptionLevel: InterruptionLevel.passive, + ); + + final notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _flutterLocalNotificationsPlugin?.show( + 0, + _defaultAgentName, + '点击开始对话', + notificationDetails, + ); + + _notificationActive = true; + } catch (e) { + print('显示前台服务通知失败: $e'); + } } Future cancelNotification() async { if (!_notificationActive) return; - await _flutterLocalNotificationsPlugin?.cancel(0); - _notificationActive = false; + try { + await _flutterLocalNotificationsPlugin?.cancel(0); + _notificationActive = false; + } catch (e) { + print('取消通知失败: $e'); + } } Future _requestPermissionIfNeeded() async { - if (Platform.isMacOS) { - // On macOS, we don't need to request notification permissions through permission_handler - // Instead, we'll use the flutter_local_notifications plugin directly - final bool? granted = await _flutterLocalNotificationsPlugin - ?.resolvePlatformSpecificImplementation() - ?.requestPermissions( - alert: true, - badge: true, - sound: true, - ); - if (granted != true) { - debugPrint('MacOS Notifications: Permission request was denied'); - } + // 避免重复请求权限 + if (_isRequestingPermission) { + print('已有权限请求正在进行中,跳过此次请求'); return; } - - // For other platforms, use permission_handler as before - if (await Permission.notification.status != PermissionStatus.granted) { - await Permission.notification.request(); + + _isRequestingPermission = true; + + try { + if (Platform.isMacOS) { + // On macOS, we don't need to request notification permissions through permission_handler + // Instead, we'll use the flutter_local_notifications plugin directly + final bool? granted = await _flutterLocalNotificationsPlugin + ?.resolvePlatformSpecificImplementation() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + if (granted != true) { + debugPrint('MacOS Notifications: Permission request was denied'); + } + } else if (Platform.isAndroid || Platform.isIOS) { + // 检查权限状态,避免不必要的请求 + final status = await Permission.notification.status; + if (status != PermissionStatus.granted) { + await Permission.notification.request(); + } + } + } catch (e) { + print('请求通知权限失败: $e'); + } finally { + _isRequestingPermission = false; } } @@ -155,30 +186,39 @@ class NotificationService extends GetxService { }) async { if (_flutterLocalNotificationsPlugin == null) return; - const androidDetails = AndroidNotificationDetails( - 'chat_channel', - '聊天消息', - channelDescription: '接收新的聊天消息', - importance: Importance.high, - priority: Priority.high, - ); + try { + const androidDetails = AndroidNotificationDetails( + 'chat_channel', + '聊天消息', + channelDescription: '接收新的聊天消息', + importance: Importance.high, + priority: Priority.high, + icon: 'mipmap/ic_launcher', // 使用应用图标 + ); - const iosDetails = DarwinNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - ); + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); - const notificationDetails = NotificationDetails( - android: androidDetails, - iOS: iosDetails, - ); + const notificationDetails = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _flutterLocalNotificationsPlugin?.show( + 0, + title, + body, + notificationDetails, + ); + } catch (e) { + print('显示通知失败: $e'); + } + } - await _flutterLocalNotificationsPlugin?.show( - 0, - title, - body, - notificationDetails, - ); + Future showMacOSStatusBarIcon() async { + // Implementation using platform channel to communicate with native macOS code } } \ No newline at end of file diff --git a/lib/data/services/volcano_tts_service.dart b/lib/data/services/volcano_tts_service.dart index 6a9ec9402..a1dd3ff62 100644 --- a/lib/data/services/volcano_tts_service.dart +++ b/lib/data/services/volcano_tts_service.dart @@ -74,7 +74,11 @@ class VolcanoTtsService extends GetxService { late AudioPlayer _audioPlayer; final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []); final isEnabled = true.obs; // 默认启用 - final isPlaying = false.obs; + + // 播放状态变化通知流 + final _playingStateController = StreamController.broadcast(); + Stream get playingStateStream => _playingStateController.stream; + StreamSubscription? _playbackEventSubscription; StreamSubscription? _playerStateSubscription; AudioSession? _audioSession; @@ -555,7 +559,9 @@ class VolcanoTtsService extends GetxService { _pendingText = ''; _sentenceQueue.clear(); _isFetching = false; - isPlaying.value = false; + + // 通知播放状态变化 + _notifyPlayingStateChanged(false); // 确保清空所有待处理的音频数据 try { @@ -576,28 +582,82 @@ class VolcanoTtsService extends GetxService { } } - // 添加一个方法来检查音频播放器是否真正在播放 + // 检查是否实际正在播放 bool isActuallyPlaying() { try { + // 检查是否有活跃的播放任务 + final hasActiveTask = _sentenceQueue.isNotEmpty || _isFetching; + // 检查播放器状态 final isPlayerPlaying = _audioPlayer.playing; + // 检查是否有待处理的文本 + final hasPendingText = _pendingText.isNotEmpty; + // 检查播放列表是否为空 final hasAudioSource = _playlist.length > 0; - // 只有当播放器正在播放且播放列表不为空时,才认为真正在播放 - final actuallyPlaying = isPlayerPlaying && hasAudioSource; + // 检查当前播放位置 + final currentPosition = _audioPlayer.position; + final currentDuration = _audioPlayer.duration ?? Duration.zero; + + // 检查是否已经播放到末尾但播放器状态未更新 + final isAtEnd = currentDuration.inMilliseconds > 0 && + currentPosition.inMilliseconds >= currentDuration.inMilliseconds - 100; - // 如果状态不一致,记录日志 - if (isPlaying.value != actuallyPlaying) { - print('音频播放状态不一致: isPlaying.value=${isPlaying.value}, 实际播放状态=$actuallyPlaying (播放器状态=$isPlayerPlaying, 播放列表长度=${_playlist.length})'); + print('【TTS实际状态】检查TTS实际播放状态: hasActiveTask=$hasActiveTask, isPlayerPlaying=$isPlayerPlaying, hasPendingText=$hasPendingText, hasAudioSource=$hasAudioSource, isAtEnd=$isAtEnd'); + + // 如果播放器显示正在播放,但已经到达音频末尾,则认为实际上没有播放 + if (isPlayerPlaying && isAtEnd) { + // 如果检测到这种情况,尝试自动修复播放器状态 + _fixPlayerState(); + return false; } - return actuallyPlaying; + // 如果有活跃任务、播放器正在播放且未到末尾、有待处理文本,则认为正在播放 + return hasActiveTask || (isPlayerPlaying && !isAtEnd) || hasPendingText; + } catch (e) { + print('【TTS实际状态】检查TTS实际播放状态出错: $e'); + return false; + } + } + + // 修复播放器状态 + void _fixPlayerState() { + try { + // 如果播放器显示正在播放,但实际上已经到达音频末尾,尝试修复状态 + Future.microtask(() async { + try { + // 如果没有更多内容要播放,停止播放器 + if (_sentenceQueue.isEmpty && _pendingText.isEmpty && !_isFetching) { + await _audioPlayer.stop(); + + // 如果播放列表中只有已播放完的项目,清空播放列表 + if (_playlist.length > 0) { + await _playlist.clear(); + } + + // 通知播放状态变化 + _notifyPlayingStateChanged(false); + } else { + // 如果还有内容要播放,尝试播放下一个 + _tryPlayNext(); + } + } catch (e) { + print('修复播放器状态失败: $e'); + } + }); } catch (e) { - print('检查实际播放状态失败: $e'); - // 如果检查失败,返回当前的 isPlaying 值 - return isPlaying.value; + print('尝试修复播放器状态时出错: $e'); + } + } + + // 通知播放状态变化 + void _notifyPlayingStateChanged(bool isPlaying) { + try { + _playingStateController.add(isPlaying); + } catch (e) { + print('通知播放状态变化失败: $e'); } } @@ -653,7 +713,10 @@ class VolcanoTtsService extends GetxService { _playerStateSubscription = _audioPlayer.playerStateStream.listen( (state) { if (_isDisposed) return; - isPlaying.value = state.playing; + + // 通知播放状态变化 + final isPlaying = state.playing && _playlist.length > 0; + _notifyPlayingStateChanged(isPlaying); if (state.processingState == ProcessingState.completed) { _tryPlayNext(); @@ -705,6 +768,7 @@ class VolcanoTtsService extends GetxService { await _channel?.sink.close(); await stop(); await _audioPlayer.dispose(); + await _playingStateController.close(); } @override diff --git a/lib/main.dart b/lib/main.dart index 4879e8904..2be496940 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'core/routes/app_pages.dart'; import 'core/routes/app_routes.dart'; import 'dart:io'; import 'package:window_size/window_size.dart'; +import 'app/translations/app_translations.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -66,7 +67,11 @@ class MainApp extends StatelessWidget { initialBinding: InitialBinding(), initialRoute: Routes.home, getPages: AppPages.pages, + defaultTransition: Transition.noTransition, debugShowCheckedModeBanner: false, + translations: AppTranslations(), + locale: const Locale('zh', 'CN'), + fallbackLocale: const Locale('en', 'US'), ); } ); diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 85b914341..1dc491185 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -42,6 +42,9 @@ class ChatController extends GetxController { final isVoiceMuted = false.obs; Message? _currentVoiceMessage; + // 添加一个标志,表示当前是否有正在进行的语音识别 + final isRecognizing = false.obs; + // 输入框状态 final isInputCollapsed = false.obs; final isVoiceMode = true.obs; // 默认为语音模式 @@ -87,6 +90,7 @@ class ChatController extends GetxController { isVoiceConnecting.value = true; isRecording.value = false; recordingText.value = ''; + isRecognizing.value = false; // 当语音面板打开时,确保ListView滚动到适当位置,防止最后的消息被遮挡 WidgetsBinding.instance.addPostFrameCallback((_) { @@ -115,6 +119,9 @@ class ChatController extends GetxController { onClosePanel: stopVoiceInput, onRecognizing: handleRecognizing, )); + + // 滚动到底部 + _scrollToBottom(animate: true); } void startRecording() { @@ -150,11 +157,12 @@ class ChatController extends GetxController { isVoiceConnecting.value = true; isRecording.value = false; recordingText.value = ''; + isRecognizing.value = false; // 不再需要恢复TTS状态,因为已启用回声消除 // 如果存在临时语音消息但没有实际内容,则移除它 - if (_currentVoiceMessage != null && _currentVoiceMessage!.content == '🎤 ...') { + if (_currentVoiceMessage != null && (_currentVoiceMessage!.content == '🎤 ...' || _currentVoiceMessage!.content.startsWith('🎤 '))) { messages.remove(_currentVoiceMessage); } _currentVoiceMessage = null; @@ -182,34 +190,96 @@ class ChatController extends GetxController { void handleRecognizing(String text) { if (text.isNotEmpty) { - // 更新识别中的文本,但不创建消息 + // 更新识别中的文本 recordingText.value = text; + + // 如果还没有创建临时语音消息,则创建一个 + if (_currentVoiceMessage == null) { + _currentVoiceMessage = Message( + role: 'user', + content: '🎤 ' + text, + timestamp: DateTime.now(), + ); + messages.add(_currentVoiceMessage!); + isRecognizing.value = true; + + // 滚动到底部 + _scrollToBottom(animate: true); + } else { + // 更新现有的临时语音消息 + final index = messages.indexWhere((msg) => + msg.timestamp == _currentVoiceMessage!.timestamp && + msg.role == _currentVoiceMessage!.role); + + if (index != -1) { + messages[index] = Message( + role: 'user', + content: '🎤 ' + text, + timestamp: _currentVoiceMessage!.timestamp, + ); + + // 滚动到底部 + _scrollToBottom(animate: false); + } + } } } void handleVoiceResult(String text) { if (text.isNotEmpty) { - // 创建一个新的用户消息 - final userMessage = Message( - role: 'user', - content: text, - timestamp: DateTime.now(), - ); - - // 添加到消息列表 - messages.add(userMessage); - - // 保存聊天历史 - _saveChatHistory(); - - // 滚动到底部 - _scrollToBottom(animate: true); - - // 重新启用TTS,以便AI回复时可以播放 - _manageTtsState(true); - - // 发送给AI处理 - _processAIResponse(userMessage); + // 如果有临时语音消息,更新它 + if (_currentVoiceMessage != null) { + final index = messages.indexWhere((msg) => + msg.timestamp == _currentVoiceMessage!.timestamp && + msg.role == _currentVoiceMessage!.role); + + if (index != -1) { + // 更新消息,移除麦克风图标 + messages[index] = Message( + role: 'user', + content: text, + timestamp: _currentVoiceMessage!.timestamp, + ); + + // 保存聊天历史 + _saveChatHistory(); + + // 滚动到底部 + _scrollToBottom(animate: true); + + // 重新启用TTS,以便AI回复时可以播放 + _manageTtsState(true); + + // 发送给AI处理 + _processAIResponse(messages[index]); + + // 重置临时语音消息 + _currentVoiceMessage = null; + isRecognizing.value = false; + } + } else { + // 如果没有临时语音消息,创建一个新的用户消息 + final userMessage = Message( + role: 'user', + content: text, + timestamp: DateTime.now(), + ); + + // 添加到消息列表 + messages.add(userMessage); + + // 保存聊天历史 + _saveChatHistory(); + + // 滚动到底部 + _scrollToBottom(animate: true); + + // 重新启用TTS,以便AI回复时可以播放 + _manageTtsState(true); + + // 发送给AI处理 + _processAIResponse(userMessage); + } } } @@ -415,12 +485,28 @@ class ChatController extends GetxController { Future _showWelcomeMessages() async { try { - if (messages.isEmpty && agentName.isNotEmpty && playVoiceOnEnter) { + if (agentName.isNotEmpty && playVoiceOnEnter) { // 只有 playVoiceOnEnter 为 true 时才生成动态问候语 await _generatePersonalizedGreeting(); // 保存聊天记录并立即滚动到底部 await _saveChatHistory(); _scrollToBottom(animate: true); + + // 添加一个系统消息,告知用户已启用连续语音对话 + final systemMessage = Message( + role: 'system', + content: '已为您自动启用连续语音对话模式,您可以直接开始说话', + timestamp: DateTime.now(), + ); + messages.add(systemMessage); + await _saveChatHistory(); + + // 延迟一小段时间后自动启动语音输入模式 + Future.delayed(const Duration(milliseconds: 500), () { + if (!_isDisposed) { + startVoiceInput(); + } + }); } } catch (e) { print('显示欢迎消息失败: $e'); @@ -428,6 +514,15 @@ class ChatController extends GetxController { if (_ttsService.isEnabled.value) { _ttsService.speak(welcomeMessage); } + + // 即使问候语生成失败,如果playVoiceOnEnter为true,仍然启动语音输入 + if (playVoiceOnEnter) { + Future.delayed(const Duration(milliseconds: 500), () { + if (!_isDisposed) { + startVoiceInput(); + } + }); + } } } diff --git a/lib/modules/chat/controllers/voice_input_controller.dart b/lib/modules/chat/controllers/voice_input_controller.dart index 030543d6f..d04732dee 100644 --- a/lib/modules/chat/controllers/voice_input_controller.dart +++ b/lib/modules/chat/controllers/voice_input_controller.dart @@ -1,7 +1,9 @@ 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 @@ -10,12 +12,38 @@ class VoiceInputController extends GetxController { 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? _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; @@ -31,6 +59,10 @@ class VoiceInputController extends GetxController { void onInit() { super.onInit(); _initializeVoiceService(); + _ttsService = Get.find(); + + // 启动定期检查用户是否正在说话的计时器 + _startSpeakingCheckTimer(); } Future _initializeVoiceService() async { @@ -62,7 +94,13 @@ class VoiceInputController extends GetxController { try { isRecording.value = true; - recognizedText.value = ''; + recognizedText.value = ''; // 清空识别文本,但不再在面板中显示 + _hasRecognizedSpeech = false; + _hasFinalResult = false; + _emptyResultCount = 0; + + // 创建一个 Completer 来处理语音识别完成 + _recognitionCompleter = Completer(); // 开始连续语音识别 final stream = await _voiceService.startContinuousRecognition(); @@ -71,21 +109,89 @@ class VoiceInputController extends GetxController { _recognitionSubscription = stream.listen((event) { switch (event.type) { case RecognitionEventType.intermediateResult: - // 更新中间识别结果 - recognizedText.value = event.text; - // 调用识别中回调 + // 不再更新面板中的识别文本,而是通过回调传递给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) { - recognizedText.value = event.text; + // 不再更新面板中的识别文本 + _hasRecognizedSpeech = true; + _lastSpeechTime = DateTime.now(); + _emptyResultCount = 0; // 重置空结果计数 + // 调用最终结果回调,发送识别到的句子 onRecordingResult(event.text); - // 清空识别文本,准备下一句 - recognizedText.value = ''; - // 不停止录音,继续监听下一句 + + // 清空识别状态,准备下一句 + _hasRecognizedSpeech = false; + _hasFinalResult = false; + } else { + // 如果最终结果为空,但之前有中间结果,仍然标记为有语音 + if (_hasRecognizedSpeech) { + print('最终识别结果为空,但之前有中间结果,使用最后的中间结果'); + + // 这里不再需要使用recognizedText.value,因为我们已经在中间结果中传递了文本 + // 而且ChatController已经在UI中显示了最新的识别结果 + + // 清空识别状态,准备下一句 + _hasRecognizedSpeech = false; + _hasFinalResult = false; + } else { + // 如果最终结果为空,且没有中间结果,增加空结果计数 + _emptyResultCount++; + print('收到空的最终结果,当前空结果计数: $_emptyResultCount / $_maxEmptyResults'); + } } break; @@ -123,6 +229,9 @@ class VoiceInputController extends GetxController { restartRecognition(); }); + // 等待语音识别完成 + final result = await _recognitionCompleter!.future; + } catch (e) { print('语音识别失败: $e'); Get.snackbar( @@ -131,6 +240,11 @@ class VoiceInputController extends GetxController { snackPosition: SnackPosition.BOTTOM, ); isRecording.value = false; + } finally { + // 清理资源 + _recognitionCompleter = null; + _silenceTimer?.cancel(); + _silenceTimer = null; } } @@ -156,12 +270,19 @@ class VoiceInputController extends GetxController { 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; @@ -181,16 +302,51 @@ class VoiceInputController extends GetxController { } } + // 启动定期检查用户是否正在说话的计时器 + 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(); } diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index 30ca9867e..99e024d81 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -148,14 +148,6 @@ class ChatView extends GetView { valueColor: AlwaysStoppedAnimation(Color(0xFF4A6FE5)), ), ), - SizedBox(width: 8.w), - Text( - '正在思考...', - style: TextStyle( - fontSize: 14.sp, - color: Colors.grey[600], - ), - ), ], ), ), @@ -252,7 +244,7 @@ class ChatView extends GetView { ), child: Center( child: Text( - '点击开始语音识别', + '点击开始连续语音对话', style: TextStyle( color: Colors.grey[600], fontSize: 16.sp, @@ -425,6 +417,47 @@ class ChatView extends GetView { Widget _buildMessage(Message message) { final isUser = message.role == 'user'; + final isSystem = message.role == 'system'; + final isRecognizing = isUser && message.content.startsWith('🎤 '); + + if (isSystem) { + // 系统消息使用特殊样式 + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: Colors.grey[300]!, + width: 0.5, + ), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: Colors.grey[600], + size: 20.sp, + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + message.content, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[700], + height: 1.5, + ), + ), + ), + ], + ), + ), + ); + } + return Padding( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), child: Row( @@ -443,21 +476,53 @@ class ChatView extends GetView { child: Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), decoration: BoxDecoration( - color: isUser ? const Color(0xFF4A6FE5) : const Color(0xFFF5F5F5), + color: isUser + ? (isRecognizing ? Colors.purple.withOpacity(0.7) : const Color(0xFF4A6FE5)) + : const Color(0xFFF5F5F5), borderRadius: BorderRadius.only( topLeft: Radius.circular(isUser ? 18.r : 4.r), topRight: Radius.circular(isUser ? 4.r : 18.r), bottomLeft: Radius.circular(18.r), bottomRight: Radius.circular(18.r), ), + // 为正在识别的消息添加脉动效果 + boxShadow: isRecognizing + ? [ + BoxShadow( + color: Colors.purple.withOpacity(0.3), + blurRadius: 8, + spreadRadius: 2, + ), + ] + : [], ), - child: Text( - message.content, - style: TextStyle( - fontSize: 16.sp, - color: isUser ? Colors.white : Colors.black87, - height: 1.5, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isRecognizing) ...[ + Icon( + Icons.mic, + color: Colors.white, + size: 16.sp, + ), + SizedBox(width: 8.w), + ], + Flexible( + child: Text( + // 如果是正在识别的消息,移除麦克风图标前缀 + isRecognizing ? message.content.substring(2) : message.content, + style: TextStyle( + fontSize: 16.sp, + color: isUser ? Colors.white : Colors.black87, + height: 1.5, + ), + ), + ), + if (isRecognizing) ...[ + SizedBox(width: 8.w), + _buildPulsatingDot(), + ], + ], ), ), ), @@ -477,4 +542,79 @@ class ChatView extends GetView { ), ); } + + // 创建脉动点动画 + Widget _buildPulsatingDot() { + return Obx(() { + // 使用controller.isRecognizing来控制动画是否显示 + if (!controller.isRecognizing.value) return const SizedBox.shrink(); + + return _PulsatingDot(); + }); + } +} + +// 脉动点动画组件 +class _PulsatingDot extends StatefulWidget { + @override + _PulsatingDotState createState() => _PulsatingDotState(); +} + +class _PulsatingDotState extends State<_PulsatingDot> with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _animation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1000), + )..repeat(reverse: true); + + _animation = Tween(begin: 0.5, end: 1.0).animate(_animationController); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 16.w, + height: 16.w, + child: AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Stack( + alignment: Alignment.center, + children: [ + Opacity( + opacity: 1.0 - _animation.value, + child: Container( + width: 16.w * _animation.value, + height: 16.w * _animation.value, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.5), + shape: BoxShape.circle, + ), + ), + ), + Container( + width: 6.w, + height: 6.w, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + ), + ], + ); + }, + ), + ); + } } \ No newline at end of file diff --git a/lib/modules/chat/widgets/voice_input_panel.dart b/lib/modules/chat/widgets/voice_input_panel.dart index 57e6d5a89..dbd67d731 100644 --- a/lib/modules/chat/widgets/voice_input_panel.dart +++ b/lib/modules/chat/widgets/voice_input_panel.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'dart:math'; +import 'dart:async'; import '../controllers/voice_input_controller.dart'; class VoiceInputPanel extends GetView { @@ -59,30 +61,30 @@ class VoiceInputPanel extends GetView { ], ), child: Center( - child: Icon( - Icons.mic, - color: Colors.purple.shade300, + child: Obx(() => Icon( + controller.isRecording.value ? Icons.mic : Icons.mic_none, + color: controller.isRecording.value ? Colors.purple : Colors.purple.shade300, size: 28.sp, - ), + )), ), ), - // 中间音频波形 + // 中间音频波形和状态文本 Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ _buildAudioWaveform(), SizedBox(height: 10.h), - // 文字提示 - Text( - '你可以开始说话', + // 文字提示 - 根据状态显示不同文本 + Obx(() => Text( + _getStatusText(), style: TextStyle( fontSize: 16.sp, color: Colors.grey.shade700, fontWeight: FontWeight.w500, ), - ), + )), ], ), @@ -117,16 +119,27 @@ class VoiceInputPanel extends GetView { ), ), - // 底部进度条 + // 底部进度条和识别文本 Padding( padding: EdgeInsets.only(top: 15.h), - child: Container( - width: 250.w, - height: 4.h, - decoration: BoxDecoration( - color: Colors.purple.withOpacity(0.2), - borderRadius: BorderRadius.circular(2.r), - ), + child: Column( + children: [ + // 移除显示当前识别的文本,因为现在直接显示在聊天窗口中 + + // 进度条 + Container( + width: 250.w, + height: 4.h, + decoration: BoxDecoration( + color: Colors.purple.withOpacity(0.2), + borderRadius: BorderRadius.circular(2.r), + ), + child: Obx(() => controller.isRecording.value + ? _buildProgressIndicator() + : Container() + ), + ), + ], ), ), ], @@ -137,33 +150,153 @@ class VoiceInputPanel extends GetView { ); } + // 根据状态返回不同的提示文本 + String _getStatusText() { + if (controller.isConnecting.value) { + return '正在连接语音服务...'; + } else if (controller.isMuted.value) { + return '语音已静音'; + } else if (controller.isRecording.value) { + return '正在聆听...'; + } else { + return '你可以开始说话'; + } + } + + // 构建进度指示器 + Widget _buildProgressIndicator() { + return Container( + width: 250.w, + height: 4.h, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.purple.shade300, Colors.purple.shade100], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + borderRadius: BorderRadius.circular(2.r), + ), + ); + } + + // 检查用户是否正在说话 + bool _isUserSpeaking() { + // 使用控制器的isUserSpeaking属性 + return controller.isUserSpeaking.value; + } + Widget _buildAudioWaveform() { return Obx(() { + final isSpeaking = _isUserSpeaking(); + final barCount = 15; // 增加条形数量,使波形更丰富 + // 根据控制器状态动态生成波形高度 - return Row( - mainAxisSize: MainAxisSize.min, - children: List.generate( - 11, - (index) { - // 创建随机高度效果,模拟声波 - double height = controller.isRecording.value - ? (4 + (index % 3) * 3).h // 当正在录音时显示不同高度 - : 4.h; // 默认高度 - - return Container( - width: 4.w, - height: height, - margin: EdgeInsets.symmetric(horizontal: 2.w), - decoration: BoxDecoration( - color: controller.isRecording.value - ? Colors.purple.shade300 // 录音时使用紫色 - : Colors.grey.shade400, // 默认使用灰色 - borderRadius: BorderRadius.circular(2.r), - ), - ); - }, + return Container( + width: 180.w, // 增加宽度以容纳更多条形 + height: 40.h, // 增加高度以允许更大的波动 + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + barCount, + (index) { + // 创建动态高度效果,模拟声波 + double height = controller.isRecording.value + ? _getWaveHeight(index, isSpeaking, barCount) // 当正在录音时显示动态高度 + : 4.h; // 默认高度 + + // 根据位置计算不同的动画持续时间,使波形更自然 + final duration = Duration( + milliseconds: 100 + (index % 3) * 50 + ); + + // 根据是否说话和位置计算颜色 + Color barColor; + if (controller.isRecording.value) { + if (isSpeaking) { + // 创建渐变色效果,从中间向两边渐变 + final middleIndex = barCount ~/ 2; + final distance = (index - middleIndex).abs(); + final maxDistance = middleIndex; + final colorIntensity = 1.0 - (distance / maxDistance) * 0.5; + + barColor = Colors.purple.withOpacity(colorIntensity); + } else { + barColor = Colors.purple.shade200; + } + } else { + barColor = Colors.grey.shade400; + } + + return AnimatedContainer( + duration: duration, + curve: Curves.easeInOut, // 使用更平滑的动画曲线 + width: 3.w, // 减小宽度,使波形更精细 + height: height, + margin: EdgeInsets.symmetric(horizontal: 1.5.w), // 减小间距 + decoration: BoxDecoration( + color: barColor, + borderRadius: BorderRadius.circular(2.r), + // 添加轻微的阴影效果,增强视觉效果 + boxShadow: isSpeaking ? [ + BoxShadow( + color: Colors.purple.withOpacity(0.2), + blurRadius: 3, + spreadRadius: 0.5, + ) + ] : null, + ), + ); + }, + ), ), ); }); } + + // 生成动态波形高度 + double _getWaveHeight(int index, bool isSpeaking, int barCount) { + // 基础高度 + double baseHeight = 6.h; + + // 获取当前时间毫秒数 + final DateTime now = DateTime.now(); + final int milliseconds = now.millisecondsSinceEpoch; + + // 计算中心位置 + final middleIndex = barCount ~/ 2; + final distanceFromMiddle = (index - middleIndex).abs(); + + // 使用多个正弦波叠加,创建更自然的波形 + // 每个条形使用不同的频率和相位,创建不规则波形 + double wave1 = sin((milliseconds / 200) + (index * 0.7)) * 3.h; + double wave2 = sin((milliseconds / 350) + (index * 0.5)) * 2.h; + double wave3 = sin((milliseconds / 500) + (index * 0.3)) * 1.5.h; + + // 基于距离中心的位置计算高度衰减 + double positionFactor = 1.0 - (distanceFromMiddle / middleIndex) * 0.5; + + if (isSpeaking) { + // 用户说话时的波形 + + // 创建一个随时间变化的振幅因子,使波形看起来更有活力 + double timeFactor = 0.7 + sin(milliseconds / 1000) * 0.3; + + // 中心位置的条形高度最高 + double centerBoost = pow(positionFactor, 1.5) * 10.h; + + // 添加随机因素,使波形更自然 + double randomFactor = (Random().nextDouble() * 2 - 1) * 2.h; + + // 根据时间添加脉动效果 + double pulseEffect = sin(milliseconds / 600) * 3.h * positionFactor; + + // 组合所有效果 + return baseHeight + (wave1 + wave2 + wave3) * timeFactor + centerBoost + randomFactor + pulseEffect; + } else { + // 用户不说话时的波形 - 更平静,但仍有微小变化 + double idleWave = (wave1 + wave2) * 0.5 * positionFactor; + return baseHeight + idleWave; + } + } } \ No newline at end of file diff --git a/lib/modules/profile/controllers/profile_controller.dart b/lib/modules/profile/controllers/profile_controller.dart index fd210b643..f3d7a94eb 100644 --- a/lib/modules/profile/controllers/profile_controller.dart +++ b/lib/modules/profile/controllers/profile_controller.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:flutter/material.dart'; class ProfileController extends GetxController { static ProfileController get to => Get.find(); @@ -10,6 +11,9 @@ class ProfileController extends GetxController { final _isEditing = false.obs; final _storage = GetStorage(); + // 使用响应式变量 + final currentLanguage = 'zh'.obs; + String get userName => _userName.value; String get email => _email.value; String get avatar => _avatar.value; @@ -19,6 +23,7 @@ class ProfileController extends GetxController { void onInit() { super.onInit(); loadUserData(); + loadLanguagePreference(); } void loadUserData() { @@ -31,6 +36,42 @@ class ProfileController extends GetxController { } } + // 加载用户语言偏好设置 + void loadLanguagePreference() { + try { + final String? languageCode = _storage.read('languageCode'); + final String? countryCode = _storage.read('countryCode'); + + if (languageCode != null && countryCode != null) { + Get.updateLocale(Locale(languageCode, countryCode)); + currentLanguage.value = languageCode; + } + } catch (e) { + print('Failed to load language preference: $e'); + } + } + + // 保存用户语言偏好设置 + Future saveLanguagePreference(Locale locale) async { + try { + await _storage.write('languageCode', locale.languageCode); + await _storage.write('countryCode', locale.countryCode); + } catch (e) { + print('Failed to save language preference: $e'); + } + } + + // 切换应用语言 + void changeLanguage(String languageCode, String countryCode) { + // 使用Future.delayed来避免在构建过程中更新UI + Future.delayed(Duration.zero, () async { + final locale = Locale(languageCode, countryCode); + await saveLanguagePreference(locale); + Get.updateLocale(locale); + currentLanguage.value = languageCode; + }); + } + Future updateUserData({ String? userName, String? email, diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index c384cbfc2..90083b154 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -2,12 +2,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/profile_controller.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import '../../../data/services/volcano_tts_service.dart'; import '../../../core/widgets/common_bottom_nav.dart'; -import '../../speech_demo/speech_demo_page.dart'; -import '../../../modules/microsoft_tts_example.dart'; -import '../../../modules/microsoft_tts_continuous_example.dart'; -import '../../../data/services/microsoft_tts_service.dart'; +import '../../../data/services/audio_service.dart'; class ProfileView extends GetView { const ProfileView({Key? key}) : super(key: key); @@ -53,7 +49,7 @@ class ProfileView extends GetView { backgroundColor: Colors.white, elevation: 0, title: Text( - '设置', + '设置'.tr, style: TextStyle( fontSize: 17.sp, fontWeight: FontWeight.w600, @@ -65,83 +61,10 @@ class ProfileView extends GetView { body: ListView( children: [ SizedBox(height: 12.h), - _buildMenuItem( - title: '测试语音合成', - icon: Icons.record_voice_over, - subtitle: '测试微软语音TTS功能', - onTap: () async { - try { - final tts = Get.find(); - - // 检查TTS服务状态 - print('TTS服务状态: enabled=${tts.isEnabled.value}'); - - // 确保TTS已启用 - if (!tts.isEnabled.value) { - print('TTS服务未启用,正在启用...'); - tts.isEnabled.value = true; - } - - // 先停止之前的播放 - await tts.stop(); - - print('开始播放连续测试语音...'); - // 连续播放多段文本 - await tts.speak('你好,这是第一段测试语音。'); - await tts.speak('这是第二段语音,用于测试连续播放功能。'); - await tts.speak('现在是第三段语音,测试火山语音的连续合成能力。'); - await tts.speak('最后一段测试语音,感谢您的收听。'); - - Get.snackbar( - '提示', - '正在播放连续测试语音', - snackPosition: SnackPosition.BOTTOM, - duration: const Duration(seconds: 2), - ); - } catch (e, stack) { - print('TTS测试失败: $e'); - print('错误堆栈: $stack'); - Get.snackbar( - '错误', - 'TTS服务错误: $e', - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.red.withOpacity(0.1), - colorText: Colors.red, - duration: const Duration(seconds: 3), - ); - } - }, - ), + const Divider(), _buildMenuItem( - title: '微软语音合成测试', - icon: Icons.record_voice_over_outlined, - subtitle: '测试微软 Azure TTS 功能', - onTap: () { - Get.to(() => const MicrosoftTtsExample()); - }, - ), - const Divider(), - _buildMenuItem( - title: '微软连续语音合成测试', - icon: Icons.queue_music_outlined, - subtitle: '测试微软 Azure TTS 连续语音功能', - onTap: () { - Get.to(() => const MicrosoftTtsContinuousExample()); - }, - ), - const Divider(), - _buildMenuItem( - title: '语音识别测试', - icon: Icons.mic, - subtitle: '测试语音识别功能', - onTap: () { - Get.to(() => const SpeechDemoPage()); - }, - ), - const Divider(), - _buildMenuItem( - title: '关于', + title: '关于'.tr, icon: Icons.info_outline, onTap: () { // TODO: 实现关于页面 @@ -149,7 +72,7 @@ class ProfileView extends GetView { ), const Divider(), _buildMenuItem( - title: '检查更新', + title: '检查更新'.tr, icon: Icons.system_update, onTap: () { // TODO: 实现检查更新 @@ -157,12 +80,42 @@ class ProfileView extends GetView { ), const Divider(), _buildMenuItem( - title: '清除缓存', + title: '清除缓存'.tr, icon: Icons.cleaning_services_outlined, onTap: () { // TODO: 实现清除缓存 }, ), + const Divider(), + _buildMenuItem( + title: '测试媒体按钮事件'.tr, + icon: Icons.headset, + subtitle: '手动触发媒体按钮事件,用于测试蓝牙耳机按键功能', + onTap: () { + try { + // 获取 AudioServiceManager 实例并触发媒体按钮事件 + final audioService = Get.find(); + audioService.triggerMediaButtonEvent(); + + // 显示成功提示 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('已触发媒体按钮事件,请查看日志'), + duration: Duration(seconds: 2), + ), + ); + } catch (e) { + // 显示错误提示 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('触发媒体按钮事件失败: $e'), + backgroundColor: Colors.red, + duration: Duration(seconds: 3), + ), + ); + } + }, + ), ], ), bottomNavigationBar: const CommonBottomNav(currentIndex: 2),