Browse Source

0.02

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
dde443f332
  1. 8
      android/app/src/main/AndroidManifest.xml
  2. 11
      android/app/src/main/res/drawable/ic_notification.xml
  3. 20
      lib/app/translations/app_translations.dart
  4. 33
      lib/core/bindings/initial_binding.dart
  5. 64
      lib/data/services/audio_service.dart
  6. 46
      lib/data/services/azure_tts_service.dart
  7. 307
      lib/data/services/background_agent_service.dart
  8. 79
      lib/data/services/my_audio_handler.dart
  9. 304
      lib/data/services/notification_service.dart
  10. 90
      lib/data/services/volcano_tts_service.dart
  11. 5
      lib/main.dart
  12. 143
      lib/modules/chat/controllers/chat_controller.dart
  13. 172
      lib/modules/chat/controllers/voice_input_controller.dart
  14. 174
      lib/modules/chat/views/chat_view.dart
  15. 211
      lib/modules/chat/widgets/voice_input_panel.dart
  16. 41
      lib/modules/profile/controllers/profile_controller.dart
  17. 119
      lib/modules/profile/views/profile_view.dart

8
android/app/src/main/AndroidManifest.xml

@ -43,6 +43,7 @@
</intent-filter> </intent-filter>
</service> </service>
<!-- 确保媒体按钮接收器正确配置 -->
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver" <receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true" android:exported="true"
android:enabled="true"> android:enabled="true">
@ -51,6 +52,13 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- 添加通知服务 -->
<service
android:name="com.dexterous.flutterlocalnotifications.ForegroundService"
android:exported="false"
android:stopWithTask="false"
android:foregroundServiceType="mediaPlayback" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

11
android/app/src/main/res/drawable/ic_notification.xml

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="#FFFFFF">
<path
android:fillColor="#FFFFFF"
android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
</vector>

20
lib/app/translations/app_translations.dart

@ -28,6 +28,16 @@ class AppTranslations extends Translations {
'type_message': '发消息...', 'type_message': '发消息...',
'voice_enabled': '语音已开启', 'voice_enabled': '语音已开启',
'voice_disabled': '语音已关闭', 'voice_disabled': '语音已关闭',
// 设置页面
'settings': '设置',
'language_settings': '语言设置',
'about': '关于',
'check_update': '检查更新',
'clear_cache': '清除缓存',
'home': '首页',
'explore': '探索',
'profile': '我的',
}, },
'en_US': { 'en_US': {
// General // General
@ -54,6 +64,16 @@ class AppTranslations extends Translations {
'type_message': 'Type a message...', 'type_message': 'Type a message...',
'voice_enabled': 'Voice Enabled', 'voice_enabled': 'Voice Enabled',
'voice_disabled': 'Voice Disabled', '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',
}, },
}; };
} }

33
lib/core/bindings/initial_binding.dart

@ -20,25 +20,30 @@ class InitialBinding extends Bindings {
// 只注册全局控制器 // 只注册全局控制器
Get.put(PermissionController(), permanent: true); Get.put(PermissionController(), permanent: true);
// 异步初始化各个服务 // 异步初始化各个服务,确保顺序执行
_initializeServices(notificationService, audioService); _initializeServices(notificationService, audioService);
} }
// 单独提取初始化服务的方法,以便更好地处理错误 // 单独提取初始化服务的方法,以便更好地处理错误
void _initializeServices(NotificationService notificationService, AudioServiceManager audioService) { void _initializeServices(NotificationService notificationService, AudioServiceManager audioService) async {
// 初始化通知服务 try {
notificationService.init().catchError((error) { // 首先初始化通知服务
print('通知服务初始化失败: $error'); await notificationService.init().catchError((error) {
return null; print('通知服务初始化失败: $error');
}); return null;
});
// 等待一段时间,确保通知权限请求完成
await Future.delayed(Duration(seconds: 1));
// 然后初始化音频服务
await audioService.init().then((_) {
print('AudioServiceManager 初始化完成,可以接收蓝牙耳机按键事件');
// 初始化音频服务 });
audioService.init().then((_) { } catch (e) {
print('AudioServiceManager 初始化完成,可以接收蓝牙耳机按键事件'); print('服务初始化过程中发生错误: $e');
}).catchError((error) { }
print('音频服务初始化失败: $error');
return null;
});
// 其他服务初始化可以在这里添加 // 其他服务初始化可以在这里添加
} }

64
lib/data/services/audio_service.dart

@ -13,6 +13,9 @@ class AudioServiceManager extends GetxService {
// 添加一个标志,表示是否已经尝试过初始化 // 添加一个标志,表示是否已经尝试过初始化
bool _hasAttemptedInit = false; bool _hasAttemptedInit = false;
// 添加一个计时器,用于定期检查服务状态
Timer? _serviceCheckTimer;
Future<AudioServiceManager> init() async { Future<AudioServiceManager> init() async {
print('初始化 AudioServiceManager'); print('初始化 AudioServiceManager');
@ -24,8 +27,9 @@ class AudioServiceManager extends GetxService {
// 如果已经尝试过初始化但失败了,不再重试 // 如果已经尝试过初始化但失败了,不再重试
if (_hasAttemptedInit) { if (_hasAttemptedInit) {
print('AudioServiceManager 之前初始化失败,不再重试'); print('AudioServiceManager 之前初始化失败,尝试重新初始化');
return this; // 重置标志,允许重新初始化
_hasAttemptedInit = false;
} }
_hasAttemptedInit = true; _hasAttemptedInit = true;
@ -41,15 +45,14 @@ class AudioServiceManager extends GetxService {
config: AudioServiceConfig( config: AudioServiceConfig(
androidNotificationChannelId: 'com.example.deep_voice.channel.audio', androidNotificationChannelId: 'com.example.deep_voice.channel.audio',
androidNotificationChannelName: 'Deep Voice Audio Service', androidNotificationChannelName: 'Deep Voice Audio Service',
// 使用新创建的通知图标 // 使用应用图标作为通知图标,确保这个图标存在
androidNotificationIcon: 'drawable/ic_notification', androidNotificationIcon: 'mipmap/ic_launcher',
// 设置为 false,避免在没有用户交互时启动前台服务 // 修复配置冲突:当androidStopForegroundOnPause为true时,androidNotificationOngoing才能为true
androidNotificationOngoing: false, androidNotificationOngoing: false,
// 设置为 true,当暂停时停止前台服务 // 设置为true,允许在暂停时停止前台服务,避免与androidNotificationOngoing冲突
androidStopForegroundOnPause: true, androidStopForegroundOnPause: true,
androidShowNotificationBadge: true, androidShowNotificationBadge: true,
notificationColor: Colors.blue, notificationColor: Colors.blue,
// 设置为 false,避免自动启动前台服务
fastForwardInterval: const Duration(seconds: 10), fastForwardInterval: const Duration(seconds: 10),
rewindInterval: const Duration(seconds: 10), rewindInterval: const Duration(seconds: 10),
preloadArtwork: false, preloadArtwork: false,
@ -61,15 +64,45 @@ class AudioServiceManager extends GetxService {
_isInitialized = true; _isInitialized = true;
print('AudioServiceManager 初始化成功'); print('AudioServiceManager 初始化成功');
// 启动定期检查服务状态的计时器
_startServiceCheckTimer();
}); });
return this; return this;
} catch (e) { } catch (e) {
print('AudioServiceManager初始化失败: $e'); print('AudioServiceManager初始化失败: $e');
_isInitialized = false; // 确保失败时重置状态 _isInitialized = false; // 确保失败时重置状态
// 延迟5秒后尝试重新初始化
Future.delayed(Duration(seconds: 5), () {
print('尝试重新初始化 AudioServiceManager');
_hasAttemptedInit = false;
init();
});
return this; // 即使失败也返回实例,避免空指针异常 return this; // 即使失败也返回实例,避免空指针异常
} }
} }
// 启动定期检查服务状态的计时器
void _startServiceCheckTimer() {
_serviceCheckTimer?.cancel();
_serviceCheckTimer = Timer.periodic(Duration(minutes: 5), (timer) {
_checkAndRestartServiceIfNeeded();
});
}
// 检查服务状态,如果需要则重启服务
Future<void> _checkAndRestartServiceIfNeeded() async {
if (!_isInitialized || _audioHandler == null) {
print('服务检查:AudioServiceManager 未初始化,尝试重新初始化');
_hasAttemptedInit = false;
await init();
return;
}
print('服务检查:AudioServiceManager 已初始化,服务状态: $_isInitialized');
}
// 获取音频处理器 // 获取音频处理器
MyAudioHandler? get audioHandler => _audioHandler; MyAudioHandler? get audioHandler => _audioHandler;
@ -78,18 +111,35 @@ class AudioServiceManager extends GetxService {
Future<void> sendMediaButtonEvent() async { Future<void> sendMediaButtonEvent() async {
if (_audioHandler != null) { if (_audioHandler != null) {
try { try {
print('发送媒体按钮事件');
await _audioHandler!.customAction('media_button'); await _audioHandler!.customAction('media_button');
} catch (e) { } catch (e) {
print('发送媒体按钮事件失败: $e'); print('发送媒体按钮事件失败: $e');
// 如果发送失败,尝试重新初始化服务
_isInitialized = false;
_hasAttemptedInit = false;
await init();
} }
} else { } else {
print('AudioHandler 未初始化,无法发送媒体按钮事件'); print('AudioHandler 未初始化,无法发送媒体按钮事件');
// 尝试重新初始化
_isInitialized = false;
_hasAttemptedInit = false;
await init();
} }
} }
// 手动触发媒体按钮事件(用于测试)
Future<void> triggerMediaButtonEvent() async {
print('手动触发媒体按钮事件');
await sendMediaButtonEvent();
}
@override @override
void onClose() async { void onClose() async {
_isInitialized = false; _isInitialized = false;
_serviceCheckTimer?.cancel();
// 停止现有的音频处理器 // 停止现有的音频处理器
if (_audioHandler != null) { if (_audioHandler != null) {

46
lib/data/services/azure_tts_service.dart

@ -49,7 +49,11 @@ class AzureTtsService extends GetxService {
final _audioPlayer = AudioPlayer(); final _audioPlayer = AudioPlayer();
final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []); final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []);
final isEnabled = false.obs; final isEnabled = false.obs;
final isPlaying = false.obs;
// 播放状态变化通知流
final _playingStateController = StreamController<bool>.broadcast();
Stream<bool> get playingStateStream => _playingStateController.stream;
StreamSubscription? _playbackEventSubscription; StreamSubscription? _playbackEventSubscription;
StreamSubscription? _playerStateSubscription; StreamSubscription? _playerStateSubscription;
AudioSession? _audioSession; AudioSession? _audioSession;
@ -120,7 +124,9 @@ class AzureTtsService extends GetxService {
_playerStateSubscription = _audioPlayer.playerStateStream.listen((state) { _playerStateSubscription = _audioPlayer.playerStateStream.listen((state) {
if (_isDisposed) return; if (_isDisposed) return;
print('播放器状态变化: playing=${state.playing},processingState=${state.processingState}'); print('播放器状态变化: playing=${state.playing},processingState=${state.processingState}');
isPlaying.value = state.playing;
// 通知播放状态变化
_notifyPlayingStateChanged(state.playing && _playlist.length > 0);
if (state.processingState == ProcessingState.completed) { if (state.processingState == ProcessingState.completed) {
_tryPlayNext(); _tryPlayNext();
@ -160,6 +166,7 @@ class AzureTtsService extends GetxService {
await _audioSession?.setActive(false); await _audioSession?.setActive(false);
await stop(); await stop();
await _audioPlayer.dispose(); await _audioPlayer.dispose();
await _playingStateController.close();
} }
@override @override
@ -180,6 +187,10 @@ class AzureTtsService extends GetxService {
await _audioPlayer.stop(); await _audioPlayer.stop();
await _playlist.clear(); await _playlist.clear();
await _audioPlayer.setAudioSource(_playlist); await _audioPlayer.setAudioSource(_playlist);
// 通知播放状态变化
_notifyPlayingStateChanged(false);
print('播放器重新初始化成功'); print('播放器重新初始化成功');
} catch (e) { } catch (e) {
print('播放器重新初始化失败: $e'); print('播放器重新初始化失败: $e');
@ -217,6 +228,32 @@ class AzureTtsService extends GetxService {
print('播放文本失败: $e'); 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() { void _extractFullSentences() {
if (_pendingText.isEmpty) return; if (_pendingText.isEmpty) return;
@ -295,13 +332,14 @@ class AzureTtsService extends GetxService {
Future<void> stop() async { Future<void> stop() async {
try { try {
print('停止播放');
await _audioPlayer.stop(); await _audioPlayer.stop();
await _playlist.clear(); await _playlist.clear();
_pendingText = ''; _pendingText = '';
_sentenceQueue.clear(); _sentenceQueue.clear();
_isFetching = false; _isFetching = false;
isPlaying.value = false;
// 通知播放状态变化
_notifyPlayingStateChanged(false);
} catch (e) { } catch (e) {
print('停止播放失败: $e'); print('停止播放失败: $e');
} }

307
lib/data/services/background_agent_service.dart

@ -43,98 +43,69 @@ class BackgroundAgentService extends GetxService {
// 最后一次识别到语音的时间 // 最后一次识别到语音的时间
DateTime? _lastSpeechTime; DateTime? _lastSpeechTime;
// 添加一个标志,表示系统是否正在播放 TTS
bool _isSpeaking = false;
// 添加一个变量来跟踪当前的AI响应流订阅 // 添加一个变量来跟踪当前的AI响应流订阅
StreamSubscription? _aiResponseSubscription; StreamSubscription? _aiResponseSubscription;
// 添加一个标志,表示是否应该取消当前的AI响应 // 添加一个标志,表示是否应该取消当前的AI响应
bool _shouldCancelAiResponse = false; bool _shouldCancelAiResponse = false;
// 添加一个变量来跟踪最后一次 TTS 状态变化的时间 // 添加计数器,用于跟踪连续无输入的次数
DateTime? _lastTtsStateChangeTime; int _noSpeechCount = 0;
BackgroundAgentService() BackgroundAgentService()
: _aiService = VolcanoAIService(), : _aiService = VolcanoAIService(),
_ttsService = Get.find<VolcanoTtsService>(), _ttsService = Get.find<VolcanoTtsService>(),
_voiceRecognitionService = Get.find<VoiceRecognitionService>() { _voiceRecognitionService = Get.find<VoiceRecognitionService>() {
// 不再监听 TTS 播放状态,而是使用定期检查 // 简化构造函数,不需要定期检查TTS状态
// 初始化 TTS 状态 }
_isSpeaking = _ttsService.isActuallyPlaying();
_lastTtsStateChangeTime = DateTime.now(); // 启动无语音超时计时器
void _startNoSpeechTimer() {
// 取消之前的计时器
_noSpeechTimer?.cancel();
// 添加定期检查实际播放状态的计时器 // 设置15秒的超时计时器
Timer.periodic(const Duration(seconds: 1), (timer) { _noSpeechTimer = Timer(const Duration(seconds: 15), () {
// 检查实际播放状态 // 检查TTS是否正在播放
final actuallyPlaying = _ttsService.isActuallyPlaying(); bool isTtsPlaying = false;
try {
// 如果状态发生变化,更新状态并记录时间 isTtsPlaying = _ttsService.isActuallyPlaying();
if (_isSpeaking != actuallyPlaying) { } catch (e) {
print('TTS 播放状态变化: $_isSpeaking -> $actuallyPlaying'); print('检查TTS播放状态失败: $e');
_isSpeaking = actuallyPlaying; isTtsPlaying = false;
_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();
}
} }
// 如果正在处理交互,进行更全面的检查 if (isTtsPlaying) {
if (_isProcessing) { // 如果TTS正在播放,重置定时器
_checkAndUpdateTtsState(); print('TTS正在播放,重置15秒超时计时器');
_startNoSpeechTimer();
} else {
// 如果TTS不在播放,且15秒内没有检测到用户输入,退出交互
print('15秒内没有检测到用户输入,退出交互');
_exitInteraction();
} }
}); });
} }
// 添加一个方法来检查和更新 TTS 状态 // 退出交互
void _checkAndUpdateTtsState() { Future<void> _exitInteraction() async {
// 完成当前的recognitionCompleter(如果有)
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.complete('');
}
// 设置标志,停止循环交互
_shouldContinueInteraction = false;
_isProcessing = false;
// 播放退出提示
try { try {
// 获取实际播放状态 await _ttsService.speak("没有听到您说话,已退出语音交互");
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');
}
}
}
} catch (e) { } catch (e) {
print('检查 TTS 播放状态失败: $e'); print('播放退出提示失败: $e');
} }
}
}
// 处理蓝牙耳机按钮触发的交互 // 处理蓝牙耳机按钮触发的交互
Future<void> handleAgentInteraction(String systemPrompt) async { Future<void> handleAgentInteraction(String systemPrompt) async {
if (_isProcessing) { if (_isProcessing) {
@ -148,11 +119,10 @@ class BackgroundAgentService extends GetxService {
try { try {
// 确保之前的语音识别已经停止 // 确保之前的语音识别已经停止
if (_isListening) { if (_isListening) {
print('交互开始前确保语音识别已停止');
await stopVoiceRecognition(); await stopVoiceRecognition();
} }
// 循环进行交互,直到用户 10 秒没有说话或手动停止 // 循环进行交互,直到用户 30 秒没有说话或手动停止
while (_shouldContinueInteraction) { while (_shouldContinueInteraction) {
await _processSingleInteraction(systemPrompt); await _processSingleInteraction(systemPrompt);
} }
@ -164,7 +134,6 @@ class BackgroundAgentService extends GetxService {
// 重置语音识别服务 // 重置语音识别服务
try { try {
print('交互结束,重置语音识别服务');
await _voiceRecognitionService.initialize(); await _voiceRecognitionService.initialize();
} catch (e) { } catch (e) {
print('重置语音识别服务失败: $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<void> _processSingleInteraction(String systemPrompt) async { Future<void> _processSingleInteraction(String systemPrompt) async {
_isProcessing = true; _isProcessing = true;
@ -226,9 +148,6 @@ class BackgroundAgentService extends GetxService {
_hasFinalResult = false; _hasFinalResult = false;
try { try {
// 强制重置 TTS 状态,确保不会因为错误的状态导致系统卡住
_resetTtsStateIfNeeded();
// 先播放一个简短的提示音或提示语,表示开始监听 // 先播放一个简短的提示音或提示语,表示开始监听
try { try {
await _ttsService.speak("我在听"); await _ttsService.speak("我在听");
@ -238,19 +157,16 @@ class BackgroundAgentService extends GetxService {
} }
// 开始语音识别 // 开始语音识别
bool recognitionStarted = false;
String userInput = ''; String userInput = '';
try { try {
// 确保之前的语音识别已经停止 // 确保之前的语音识别已经停止
if (_isListening) { if (_isListening) {
print('检测到上一次语音识别未正确停止,先停止它');
await stopVoiceRecognition(); await stopVoiceRecognition();
} }
// 尝试启动语音识别 // 尝试启动语音识别
await startVoiceRecognition(); await startVoiceRecognition();
recognitionStarted = true;
// 创建一个 Completer 来处理语音识别完成 // 创建一个 Completer 来处理语音识别完成
_recognitionCompleter = Completer<String>(); _recognitionCompleter = Completer<String>();
@ -265,55 +181,20 @@ class BackgroundAgentService extends GetxService {
_noSpeechTimer?.cancel(); _noSpeechTimer?.cancel();
_noSpeechTimer = null; _noSpeechTimer = null;
// 如果没有识别到语音,则停止循环交互 // 如果用户输入为空,重新启动超时计时器并返回
if (!_hasRecognizedSpeech || userInput.trim().isEmpty) { if (userInput.trim().isEmpty) {
print('没有识别到用户语音或识别结果为空,退出循环交互'); _startNoSpeechTimer();
// 停止语音识别
if (_isListening) {
await stopVoiceRecognition();
}
try {
await _ttsService.speak("没有听到您说话,已退出语音交互");
} catch (e) {
print('播放退出提示失败: $e');
}
// 设置标志,停止循环交互
_shouldContinueInteraction = false;
_isProcessing = false;
return; return;
} }
// 注意:不再停止语音识别,保持语音识别状态
// 只有在退出交互时才停止语音识别
} catch (e) { } catch (e) {
print('语音识别过程出错: $e'); print('语音识别过程出错: $e');
_startNoSpeechTimer(); // 出错时也启动超时计时器
return;
} finally { } finally {
// 清理资源,但保持语音识别状态 // 清理资源,但保持语音识别状态
_recognitionCompleter = null; _recognitionCompleter = null;
_silenceTimer?.cancel(); _silenceTimer?.cancel();
_silenceTimer = null; _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}, {'role': 'user', 'content': userInput},
]; ];
String fullResponse = ''; String fullResponse = '';
try { try {
// 重置取消标志 // 重置取消标志
@ -391,6 +271,9 @@ class BackgroundAgentService extends GetxService {
} }
_pendingTtsText = ''; _pendingTtsText = '';
_aiResponseSubscription = null; _aiResponseSubscription = null;
// AI响应完成后,重新启动超时计时器
_startNoSpeechTimer();
} }
); );
@ -401,6 +284,14 @@ class BackgroundAgentService extends GetxService {
print('AI响应生成失败: $e'); print('AI响应生成失败: $e');
// 如果AI响应失败,使用默认回复 // 如果AI响应失败,使用默认回复
fullResponse = '抱歉,我现在无法回答您的问题。请稍后再试。'; fullResponse = '抱歉,我现在无法回答您的问题。请稍后再试。';
// 播放错误提示
try {
await _ttsService.speak(fullResponse);
} catch (_) {}
// 启动超时计时器
_startNoSpeechTimer();
} finally { } finally {
// 清理资源 // 清理资源
_aiResponseSubscription?.cancel(); _aiResponseSubscription?.cancel();
@ -423,11 +314,8 @@ class BackgroundAgentService extends GetxService {
_messageHistory.removeRange(0, _messageHistory.length - 20); _messageHistory.removeRange(0, _messageHistory.length - 20);
} }
// 短暂暂停,然后开始下一轮交互
await Future.delayed(const Duration(milliseconds: 500));
} catch (e) { } catch (e) {
print('Background agent interaction failed: $e'); print('交互过程出错: $e');
try { try {
await _ttsService.speak("抱歉,出现了一些问题"); await _ttsService.speak("抱歉,出现了一些问题");
} catch (_) {} } catch (_) {}
@ -448,14 +336,12 @@ class BackgroundAgentService extends GetxService {
// 开始语音识别 // 开始语音识别
Future<void> startVoiceRecognition() async { Future<void> startVoiceRecognition() async {
if (_isListening) { if (_isListening) {
print('已经在监听中,先停止当前监听');
await stopVoiceRecognition(); await stopVoiceRecognition();
} }
try { try {
// 确保语音识别服务已初始化 // 确保语音识别服务已初始化
if (!await _voiceRecognitionService.initialize()) { if (!await _voiceRecognitionService.initialize()) {
print('语音识别服务初始化失败,尝试重新初始化');
// 尝试重新初始化 // 尝试重新初始化
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
if (!await _voiceRecognitionService.initialize()) { if (!await _voiceRecognitionService.initialize()) {
@ -483,32 +369,38 @@ class BackgroundAgentService extends GetxService {
// 收到最终结果,立即完成识别过程 // 收到最终结果,立即完成识别过程
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
print('收到最终识别结果,立即处理: ${event.text}');
_recognitionCompleter!.complete(event.text); _recognitionCompleter!.complete(event.text);
} }
} else { } else {
// 如果最终结果为空,但之前有中间结果,仍然标记为有语音 // 如果最终结果为空,但有中间结果,使用最后的中间结果
if (_hasRecognizedSpeech) { if (_hasRecognizedSpeech && recognizedText.value.isNotEmpty) {
print('最终识别结果为空,但之前有中间结果,使用最后的中间结果');
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.complete(recognizedText.value); _recognitionCompleter!.complete(recognizedText.value);
} }
} else { } else {
// 如果最终结果为空,且没有中间结果,重新启动无语音超时计时器 // 如果最终结果为空,且没有中间结果,重新启动超时计时器
print('最终识别结果为空,且没有中间结果,重新启动无语音超时计时器');
_startNoSpeechTimer(); _startNoSpeechTimer();
} }
} }
print('最终识别结果: ${event.text}');
} else if (event.type == RecognitionEventType.intermediateResult) { } else if (event.type == RecognitionEventType.intermediateResult) {
recognizedText.value = event.text; recognizedText.value = event.text;
if (event.text.isNotEmpty) { if (event.text.isNotEmpty) {
_hasRecognizedSpeech = true; _hasRecognizedSpeech = true;
_lastSpeechTime = DateTime.now(); _lastSpeechTime = DateTime.now();
// 检测到用户说话,重置无语音计时器
_noSpeechTimer?.cancel();
_startNoSpeechTimer();
// 如果系统正在播放TTS或接收AI响应,检测到用户开始说话时立即中断 // 如果系统正在播放TTS或接收AI响应,检测到用户开始说话时立即中断
if (_isSpeaking && event.text.trim().isNotEmpty) { bool isTtsPlaying = false;
print('检测到用户开始说话,中断TTS播放和AI响应'); try {
isTtsPlaying = _ttsService.isActuallyPlaying();
} catch (e) {
isTtsPlaying = false;
}
if (isTtsPlaying && event.text.trim().isNotEmpty) {
_ttsService.stop(); // 停止当前TTS播放 _ttsService.stop(); // 停止当前TTS播放
// 设置标志,表示应该取消当前的AI响应 // 设置标志,表示应该取消当前的AI响应
@ -525,7 +417,7 @@ class BackgroundAgentService extends GetxService {
// 取消之前的静默计时器 // 取消之前的静默计时器
_silenceTimer?.cancel(); _silenceTimer?.cancel();
// 取消之前的无语音超时计时器 // 取消之前的无语音超时计时器,用户正在说话
_noSpeechTimer?.cancel(); _noSpeechTimer?.cancel();
_noSpeechTimer = null; _noSpeechTimer = null;
@ -533,12 +425,10 @@ class BackgroundAgentService extends GetxService {
_silenceTimer = Timer(const Duration(seconds: 2), () { _silenceTimer = Timer(const Duration(seconds: 2), () {
if (_hasRecognizedSpeech && !_hasFinalResult && if (_hasRecognizedSpeech && !_hasFinalResult &&
_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) { _recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
print('用户停止说话 2 秒,使用当前识别结果: ${recognizedText.value}');
_recognitionCompleter!.complete(recognizedText.value); _recognitionCompleter!.complete(recognizedText.value);
} }
}); });
} }
print('中间识别结果: ${event.text}');
} else if (event.type == RecognitionEventType.error) { } else if (event.type == RecognitionEventType.error) {
print('识别错误: ${event.error}'); print('识别错误: ${event.error}');
} }
@ -563,11 +453,11 @@ class BackgroundAgentService extends GetxService {
// 停止语音识别并返回识别的文本 // 停止语音识别并返回识别的文本
Future<String> stopVoiceRecognition() async { Future<String> stopVoiceRecognition() async {
if (!_isListening) return ''; if (!_isListening) {
return '';
}
try { try {
print('停止语音识别...');
// 取消静默计时器 // 取消静默计时器
_silenceTimer?.cancel(); _silenceTimer?.cancel();
_silenceTimer = null; _silenceTimer = null;
@ -590,7 +480,6 @@ class BackgroundAgentService extends GetxService {
_isListening = false; _isListening = false;
isListening.value = false; isListening.value = false;
print('语音识别已停止');
return result; return result;
} catch (e) { } catch (e) {
print('停止语音识别失败: $e'); print('停止语音识别失败: $e');
@ -600,7 +489,9 @@ class BackgroundAgentService extends GetxService {
// 尝试强制重置语音识别服务 // 尝试强制重置语音识别服务
try { try {
await _voiceRecognitionService.initialize(); await _voiceRecognitionService.initialize();
} catch (_) {} } catch (e) {
print('强制重置语音识别服务失败: $e');
}
return recognizedText.value; // 返回当前已识别的文本 return recognizedText.value; // 返回当前已识别的文本
} }
@ -634,38 +525,4 @@ class BackgroundAgentService extends GetxService {
_shouldContinueInteraction = false; _shouldContinueInteraction = false;
super.onClose(); 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();
}
} }

79
lib/data/services/my_audio_handler.dart

@ -17,6 +17,12 @@ class MyAudioHandler extends BaseAudioHandler {
// 添加一个标志,表示是否正在处理交互 // 添加一个标志,表示是否正在处理交互
bool _isHandlingInteraction = false; bool _isHandlingInteraction = false;
// 添加一个计时器,用于定期更新播放状态
Timer? _statusUpdateTimer;
// 添加一个变量,记录最后一次媒体按钮事件的时间
DateTime? _lastMediaButtonEventTime;
Stream<dynamic> get customEventStream => _customEventController.stream; Stream<dynamic> get customEventStream => _customEventController.stream;
@ -24,6 +30,7 @@ class MyAudioHandler extends BaseAudioHandler {
_initializeHandler(); _initializeHandler();
_initializeBackgroundAgent(); _initializeBackgroundAgent();
_initializeSystemPrompt(); _initializeSystemPrompt();
_startStatusUpdateTimer();
print('MyAudioHandler 已创建,准备接收媒体按钮事件'); print('MyAudioHandler 已创建,准备接收媒体按钮事件');
} }
@ -93,10 +100,33 @@ class MyAudioHandler extends BaseAudioHandler {
mediaItem.add(item); mediaItem.add(item);
print('AudioHandler 媒体项已设置'); print('AudioHandler 媒体项已设置');
// 立即更新播放状态,确保通知显示
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
} catch (e) { } catch (e) {
print('初始化 AudioHandler 失败: $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({ void _updatePlaybackState({
@ -111,6 +141,11 @@ class MyAudioHandler extends BaseAudioHandler {
processingState: processingState, processingState: processingState,
updatePosition: Duration.zero, updatePosition: Duration.zero,
)); ));
// 如果状态变为播放,确保前台服务启动
if (playing) {
print('更新播放状态为播放中,确保前台服务启动');
}
} catch (e) { } catch (e) {
print('更新播放状态失败: $e'); print('更新播放状态失败: $e');
} }
@ -128,6 +163,12 @@ class MyAudioHandler extends BaseAudioHandler {
_isHandlingInteraction = true; _isHandlingInteraction = true;
try { try {
// 更新播放状态,确保通知显示
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
if (_systemPrompt.isEmpty) { if (_systemPrompt.isEmpty) {
print('系统提示为空,无法激活语音助手'); print('系统提示为空,无法激活语音助手');
return; return;
@ -158,6 +199,8 @@ class MyAudioHandler extends BaseAudioHandler {
await _backgroundAgent!.handleAgentInteraction(_systemPrompt); await _backgroundAgent!.handleAgentInteraction(_systemPrompt);
} else { } else {
print('BackgroundAgent 未初始化,无法处理交互'); print('BackgroundAgent 未初始化,无法处理交互');
// 尝试重新初始化 BackgroundAgent
_initializeBackgroundAgent();
} }
} }
} catch (e) { } catch (e) {
@ -171,11 +214,11 @@ class MyAudioHandler extends BaseAudioHandler {
Future<void> play() async { Future<void> play() async {
print('收到播放命令'); print('收到播放命令');
try { try {
await _handleInteraction();
_updatePlaybackState( _updatePlaybackState(
playing: true, playing: true,
processingState: AudioProcessingState.ready, processingState: AudioProcessingState.ready,
); );
await _handleInteraction();
} catch (e) { } catch (e) {
print('处理播放命令失败: $e'); print('处理播放命令失败: $e');
} }
@ -185,11 +228,11 @@ class MyAudioHandler extends BaseAudioHandler {
Future<void> pause() async { Future<void> pause() async {
print('收到暂停命令'); print('收到暂停命令');
try { try {
await _handleInteraction();
_updatePlaybackState( _updatePlaybackState(
playing: false, playing: false,
processingState: AudioProcessingState.ready, processingState: AudioProcessingState.ready,
); );
await _handleInteraction();
} catch (e) { } catch (e) {
print('处理暂停命令失败: $e'); print('处理暂停命令失败: $e');
} }
@ -200,6 +243,7 @@ class MyAudioHandler extends BaseAudioHandler {
print('停止 AudioHandler'); print('停止 AudioHandler');
_isDisposed = true; _isDisposed = true;
try { try {
_statusUpdateTimer?.cancel();
await super.stop(); await super.stop();
_customEventController.close(); _customEventController.close();
_updatePlaybackState( _updatePlaybackState(
@ -215,6 +259,10 @@ class MyAudioHandler extends BaseAudioHandler {
Future<void> skipToPrevious() async { Future<void> skipToPrevious() async {
print('收到上一曲命令'); print('收到上一曲命令');
try { try {
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
await _handleInteraction(); await _handleInteraction();
} catch (e) { } catch (e) {
print('处理上一曲命令失败: $e'); print('处理上一曲命令失败: $e');
@ -225,6 +273,10 @@ class MyAudioHandler extends BaseAudioHandler {
Future<void> skipToNext() async { Future<void> skipToNext() async {
print('收到下一曲命令'); print('收到下一曲命令');
try { try {
_updatePlaybackState(
playing: true,
processingState: AudioProcessingState.ready,
);
await _handleInteraction(); await _handleInteraction();
} catch (e) { } catch (e) {
print('处理下一曲命令失败: $e'); print('处理下一曲命令失败: $e');
@ -243,12 +295,33 @@ class MyAudioHandler extends BaseAudioHandler {
try { try {
if (name == 'media_button') { if (name == 'media_button') {
print('收到媒体按钮事件'); 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(); await _handleInteraction();
} else if (name == 'ping') {
// 用于检查服务是否响应的测试命令
print('收到 ping 命令,服务正常运行');
return 'pong';
} }
} catch (e) { } catch (e) {
print('处理自定义命令失败: $e'); print('处理自定义命令失败: $e');
} }
return null;
} }
@override @override

304
lib/data/services/notification_service.dart

@ -19,133 +19,164 @@ class NotificationService extends GetxService {
} }
Future<void> init() async { Future<void> init() async {
if (Platform.isIOS || Platform.isAndroid || Platform.isMacOS) { try {
_flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); if (Platform.isIOS || Platform.isAndroid || Platform.isMacOS) {
_flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
const initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher');
// 使用应用图标作为通知图标
final initializationSettingsDarwin = DarwinInitializationSettings( const initializationSettingsAndroid = AndroidInitializationSettings('mipmap/ic_launcher');
requestSoundPermission: false,
requestBadgePermission: false, final initializationSettingsDarwin = DarwinInitializationSettings(
requestAlertPermission: false, requestSoundPermission: false,
); requestBadgePermission: false,
requestAlertPermission: false,
final initializationSettings = InitializationSettings( );
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin, final initializationSettings = InitializationSettings(
macOS: initializationSettingsDarwin, android: initializationSettingsAndroid,
); iOS: initializationSettingsDarwin,
macOS: initializationSettingsDarwin,
await _flutterLocalNotificationsPlugin?.initialize( );
initializationSettings,
onDidReceiveNotificationResponse: (details) { await _flutterLocalNotificationsPlugin?.initialize(
if (Get.currentRoute != '/chat') { initializationSettings,
Get.toNamed('/chat'); onDidReceiveNotificationResponse: (details) {
} if (Get.currentRoute != '/chat') {
}, Get.toNamed('/chat');
); }
} },
);
}
// 创建通知渠道 // 创建通知渠道 - 使用高重要性确保通知在后台可见
if (Platform.isAndroid) { if (Platform.isAndroid) {
await _flutterLocalNotificationsPlugin?.resolvePlatformSpecificImplementation< await _flutterLocalNotificationsPlugin?.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel( AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(
const AndroidNotificationChannel( const AndroidNotificationChannel(
'foreground_service', 'foreground_service',
'Foreground Service', 'Foreground Service',
description: 'Used for the foreground service notification', description: 'Used for the foreground service notification',
importance: Importance.min, importance: Importance.high, // 提高重要性
playSound: false, playSound: false,
enableVibration: false, enableVibration: false,
showBadge: false, showBadge: true,
), ),
); );
} }
await _requestPermissionIfNeeded(); // 请求权限,但避免重复请求
await _requestPermissionIfNeeded();
// 在初始化完成后显示前台服务通知
if (Platform.isAndroid) { // 在初始化完成后显示前台服务通知
await showForegroundNotification(); if (Platform.isAndroid) {
await showForegroundNotification();
}
// Add macOS-specific code for status bar icon
if (Platform.isMacOS) {
await showMacOSStatusBarIcon();
}
} catch (e) {
print('通知服务初始化失败: $e');
} }
} }
Future<void> showForegroundNotification() async { Future<void> showForegroundNotification() async {
if (_notificationActive) return; if (_notificationActive) return;
// 在显示通知前确保有权限 try {
await _requestPermissionIfNeeded(); final androidDetails = AndroidNotificationDetails(
'foreground_service',
final androidDetails = AndroidNotificationDetails( 'Foreground Service',
'foreground_service', channelDescription: 'Used for the foreground service notification',
'Foreground Service', importance: Importance.high, // 提高重要性
channelDescription: 'Used for the foreground service notification', priority: Priority.high, // 提高优先级
importance: Importance.min, ongoing: true,
priority: Priority.min, autoCancel: false,
ongoing: true, showWhen: false,
autoCancel: false, enableVibration: false,
showWhen: false, playSound: false,
enableVibration: false, icon: 'mipmap/ic_launcher', // 使用应用图标
playSound: false, visibility: NotificationVisibility.public,
icon: '@mipmap/ic_launcher', category: AndroidNotificationCategory.service,
category: AndroidNotificationCategory.service, actions: [
actions: [ const AndroidNotificationAction(
const AndroidNotificationAction( 'chat',
'chat', '开始对话',
'开始对话', showsUserInterface: true,
showsUserInterface: true, ),
), ],
], );
);
const iosDetails = DarwinNotificationDetails(
const iosDetails = DarwinNotificationDetails( presentAlert: false,
presentAlert: false, presentBadge: false,
presentBadge: false, presentSound: false,
presentSound: false, interruptionLevel: InterruptionLevel.passive,
interruptionLevel: InterruptionLevel.passive, );
);
final notificationDetails = NotificationDetails(
final notificationDetails = NotificationDetails( android: androidDetails,
android: androidDetails, iOS: iosDetails,
iOS: iosDetails, );
);
await _flutterLocalNotificationsPlugin?.show(
await _flutterLocalNotificationsPlugin?.show( 0,
0, _defaultAgentName,
_defaultAgentName, '点击开始对话',
'点击开始对话', notificationDetails,
notificationDetails, );
);
_notificationActive = true;
_notificationActive = true; } catch (e) {
print('显示前台服务通知失败: $e');
}
} }
Future<void> cancelNotification() async { Future<void> cancelNotification() async {
if (!_notificationActive) return; if (!_notificationActive) return;
await _flutterLocalNotificationsPlugin?.cancel(0); try {
_notificationActive = false; await _flutterLocalNotificationsPlugin?.cancel(0);
_notificationActive = false;
} catch (e) {
print('取消通知失败: $e');
}
} }
Future<void> _requestPermissionIfNeeded() async { Future<void> _requestPermissionIfNeeded() async {
if (Platform.isMacOS) { // 避免重复请求权限
// On macOS, we don't need to request notification permissions through permission_handler if (_isRequestingPermission) {
// Instead, we'll use the flutter_local_notifications plugin directly print('已有权限请求正在进行中,跳过此次请求');
final bool? granted = await _flutterLocalNotificationsPlugin
?.resolvePlatformSpecificImplementation<MacOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
if (granted != true) {
debugPrint('MacOS Notifications: Permission request was denied');
}
return; return;
} }
// For other platforms, use permission_handler as before _isRequestingPermission = true;
if (await Permission.notification.status != PermissionStatus.granted) {
await Permission.notification.request(); 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<MacOSFlutterLocalNotificationsPlugin>()
?.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 { }) async {
if (_flutterLocalNotificationsPlugin == null) return; if (_flutterLocalNotificationsPlugin == null) return;
const androidDetails = AndroidNotificationDetails( try {
'chat_channel', const androidDetails = AndroidNotificationDetails(
'聊天消息', 'chat_channel',
channelDescription: '接收新的聊天消息', '聊天消息',
importance: Importance.high, channelDescription: '接收新的聊天消息',
priority: Priority.high, importance: Importance.high,
); priority: Priority.high,
icon: 'mipmap/ic_launcher', // 使用应用图标
);
const iosDetails = DarwinNotificationDetails( const iosDetails = DarwinNotificationDetails(
presentAlert: true, presentAlert: true,
presentBadge: true, presentBadge: true,
presentSound: true, presentSound: true,
); );
const notificationDetails = NotificationDetails( const notificationDetails = NotificationDetails(
android: androidDetails, android: androidDetails,
iOS: iosDetails, iOS: iosDetails,
); );
await _flutterLocalNotificationsPlugin?.show(
0,
title,
body,
notificationDetails,
);
} catch (e) {
print('显示通知失败: $e');
}
}
await _flutterLocalNotificationsPlugin?.show( Future<void> showMacOSStatusBarIcon() async {
0, // Implementation using platform channel to communicate with native macOS code
title,
body,
notificationDetails,
);
} }
} }

90
lib/data/services/volcano_tts_service.dart

@ -74,7 +74,11 @@ class VolcanoTtsService extends GetxService {
late AudioPlayer _audioPlayer; late AudioPlayer _audioPlayer;
final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []); final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []);
final isEnabled = true.obs; // 默认启用 final isEnabled = true.obs; // 默认启用
final isPlaying = false.obs;
// 播放状态变化通知流
final _playingStateController = StreamController<bool>.broadcast();
Stream<bool> get playingStateStream => _playingStateController.stream;
StreamSubscription? _playbackEventSubscription; StreamSubscription? _playbackEventSubscription;
StreamSubscription? _playerStateSubscription; StreamSubscription? _playerStateSubscription;
AudioSession? _audioSession; AudioSession? _audioSession;
@ -555,7 +559,9 @@ class VolcanoTtsService extends GetxService {
_pendingText = ''; _pendingText = '';
_sentenceQueue.clear(); _sentenceQueue.clear();
_isFetching = false; _isFetching = false;
isPlaying.value = false;
// 通知播放状态变化
_notifyPlayingStateChanged(false);
// 确保清空所有待处理的音频数据 // 确保清空所有待处理的音频数据
try { try {
@ -576,28 +582,82 @@ class VolcanoTtsService extends GetxService {
} }
} }
// 添加一个方法来检查音频播放器是否真正在播放 // 检查是否实际正在播放
bool isActuallyPlaying() { bool isActuallyPlaying() {
try { try {
// 检查是否有活跃的播放任务
final hasActiveTask = _sentenceQueue.isNotEmpty || _isFetching;
// 检查播放器状态 // 检查播放器状态
final isPlayerPlaying = _audioPlayer.playing; final isPlayerPlaying = _audioPlayer.playing;
// 检查是否有待处理的文本
final hasPendingText = _pendingText.isNotEmpty;
// 检查播放列表是否为空 // 检查播放列表是否为空
final hasAudioSource = _playlist.length > 0; 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;
// 如果状态不一致,记录日志 print('【TTS实际状态】检查TTS实际播放状态: hasActiveTask=$hasActiveTask, isPlayerPlaying=$isPlayerPlaying, hasPendingText=$hasPendingText, hasAudioSource=$hasAudioSource, isAtEnd=$isAtEnd');
if (isPlaying.value != actuallyPlaying) {
print('音频播放状态不一致: isPlaying.value=${isPlaying.value}, 实际播放状态=$actuallyPlaying (播放器状态=$isPlayerPlaying, 播放列表长度=${_playlist.length})'); // 如果播放器显示正在播放,但已经到达音频末尾,则认为实际上没有播放
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) { } catch (e) {
print('检查实际播放状态失败: $e'); print('尝试修复播放器状态时出错: $e');
// 如果检查失败,返回当前的 isPlaying 值 }
return isPlaying.value; }
// 通知播放状态变化
void _notifyPlayingStateChanged(bool isPlaying) {
try {
_playingStateController.add(isPlaying);
} catch (e) {
print('通知播放状态变化失败: $e');
} }
} }
@ -653,7 +713,10 @@ class VolcanoTtsService extends GetxService {
_playerStateSubscription = _audioPlayer.playerStateStream.listen( _playerStateSubscription = _audioPlayer.playerStateStream.listen(
(state) { (state) {
if (_isDisposed) return; if (_isDisposed) return;
isPlaying.value = state.playing;
// 通知播放状态变化
final isPlaying = state.playing && _playlist.length > 0;
_notifyPlayingStateChanged(isPlaying);
if (state.processingState == ProcessingState.completed) { if (state.processingState == ProcessingState.completed) {
_tryPlayNext(); _tryPlayNext();
@ -705,6 +768,7 @@ class VolcanoTtsService extends GetxService {
await _channel?.sink.close(); await _channel?.sink.close();
await stop(); await stop();
await _audioPlayer.dispose(); await _audioPlayer.dispose();
await _playingStateController.close();
} }
@override @override

5
lib/main.dart

@ -8,6 +8,7 @@ import 'core/routes/app_pages.dart';
import 'core/routes/app_routes.dart'; import 'core/routes/app_routes.dart';
import 'dart:io'; import 'dart:io';
import 'package:window_size/window_size.dart'; import 'package:window_size/window_size.dart';
import 'app/translations/app_translations.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -66,7 +67,11 @@ class MainApp extends StatelessWidget {
initialBinding: InitialBinding(), initialBinding: InitialBinding(),
initialRoute: Routes.home, initialRoute: Routes.home,
getPages: AppPages.pages, getPages: AppPages.pages,
defaultTransition: Transition.noTransition,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
translations: AppTranslations(),
locale: const Locale('zh', 'CN'),
fallbackLocale: const Locale('en', 'US'),
); );
} }
); );

143
lib/modules/chat/controllers/chat_controller.dart

@ -42,6 +42,9 @@ class ChatController extends GetxController {
final isVoiceMuted = false.obs; final isVoiceMuted = false.obs;
Message? _currentVoiceMessage; Message? _currentVoiceMessage;
// 添加一个标志,表示当前是否有正在进行的语音识别
final isRecognizing = false.obs;
// 输入框状态 // 输入框状态
final isInputCollapsed = false.obs; final isInputCollapsed = false.obs;
final isVoiceMode = true.obs; // 默认为语音模式 final isVoiceMode = true.obs; // 默认为语音模式
@ -87,6 +90,7 @@ class ChatController extends GetxController {
isVoiceConnecting.value = true; isVoiceConnecting.value = true;
isRecording.value = false; isRecording.value = false;
recordingText.value = ''; recordingText.value = '';
isRecognizing.value = false;
// 当语音面板打开时,确保ListView滚动到适当位置,防止最后的消息被遮挡 // 当语音面板打开时,确保ListView滚动到适当位置,防止最后的消息被遮挡
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -115,6 +119,9 @@ class ChatController extends GetxController {
onClosePanel: stopVoiceInput, onClosePanel: stopVoiceInput,
onRecognizing: handleRecognizing, onRecognizing: handleRecognizing,
)); ));
// 滚动到底部
_scrollToBottom(animate: true);
} }
void startRecording() { void startRecording() {
@ -150,11 +157,12 @@ class ChatController extends GetxController {
isVoiceConnecting.value = true; isVoiceConnecting.value = true;
isRecording.value = false; isRecording.value = false;
recordingText.value = ''; recordingText.value = '';
isRecognizing.value = false;
// 不再需要恢复TTS状态,因为已启用回声消除 // 不再需要恢复TTS状态,因为已启用回声消除
// 如果存在临时语音消息但没有实际内容,则移除它 // 如果存在临时语音消息但没有实际内容,则移除它
if (_currentVoiceMessage != null && _currentVoiceMessage!.content == '🎤 ...') { if (_currentVoiceMessage != null && (_currentVoiceMessage!.content == '🎤 ...' || _currentVoiceMessage!.content.startsWith('🎤 '))) {
messages.remove(_currentVoiceMessage); messages.remove(_currentVoiceMessage);
} }
_currentVoiceMessage = null; _currentVoiceMessage = null;
@ -182,34 +190,96 @@ class ChatController extends GetxController {
void handleRecognizing(String text) { void handleRecognizing(String text) {
if (text.isNotEmpty) { if (text.isNotEmpty) {
// 更新识别中的文本,但不创建消息 // 更新识别中的文本
recordingText.value = text; 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) { void handleVoiceResult(String text) {
if (text.isNotEmpty) { if (text.isNotEmpty) {
// 创建一个新的用户消息 // 如果有临时语音消息,更新它
final userMessage = Message( if (_currentVoiceMessage != null) {
role: 'user', final index = messages.indexWhere((msg) =>
content: text, msg.timestamp == _currentVoiceMessage!.timestamp &&
timestamp: DateTime.now(), msg.role == _currentVoiceMessage!.role);
);
if (index != -1) {
// 添加到消息列表 // 更新消息,移除麦克风图标
messages.add(userMessage); messages[index] = Message(
role: 'user',
// 保存聊天历史 content: text,
_saveChatHistory(); timestamp: _currentVoiceMessage!.timestamp,
);
// 滚动到底部
_scrollToBottom(animate: true); // 保存聊天历史
_saveChatHistory();
// 重新启用TTS,以便AI回复时可以播放
_manageTtsState(true); // 滚动到底部
_scrollToBottom(animate: true);
// 发送给AI处理
_processAIResponse(userMessage); // 重新启用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<void> _showWelcomeMessages() async { Future<void> _showWelcomeMessages() async {
try { try {
if (messages.isEmpty && agentName.isNotEmpty && playVoiceOnEnter) { if (agentName.isNotEmpty && playVoiceOnEnter) {
// 只有 playVoiceOnEnter 为 true 时才生成动态问候语 // 只有 playVoiceOnEnter 为 true 时才生成动态问候语
await _generatePersonalizedGreeting(); await _generatePersonalizedGreeting();
// 保存聊天记录并立即滚动到底部 // 保存聊天记录并立即滚动到底部
await _saveChatHistory(); await _saveChatHistory();
_scrollToBottom(animate: true); _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) { } catch (e) {
print('显示欢迎消息失败: $e'); print('显示欢迎消息失败: $e');
@ -428,6 +514,15 @@ class ChatController extends GetxController {
if (_ttsService.isEnabled.value) { if (_ttsService.isEnabled.value) {
_ttsService.speak(welcomeMessage); _ttsService.speak(welcomeMessage);
} }
// 即使问候语生成失败,如果playVoiceOnEnter为true,仍然启动语音输入
if (playVoiceOnEnter) {
Future.delayed(const Duration(milliseconds: 500), () {
if (!_isDisposed) {
startVoiceInput();
}
});
}
} }
} }

172
lib/modules/chat/controllers/voice_input_controller.dart

@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import '../../../data/services/voice_recognition_service.dart'; import '../../../data/services/voice_recognition_service.dart';
import '../../../data/services/volcano_tts_service.dart';
class VoiceInputController extends GetxController { class VoiceInputController extends GetxController {
// Observable states // Observable states
@ -10,12 +12,38 @@ class VoiceInputController extends GetxController {
final isRecording = false.obs; final isRecording = false.obs;
final recognizedText = ''.obs; final recognizedText = ''.obs;
// 添加一个新的可观察状态,表示用户是否正在说话
final isUserSpeaking = false.obs;
// 语音识别服务 // 语音识别服务
late final VoiceRecognitionService _voiceService; late final VoiceRecognitionService _voiceService;
late final VolcanoTtsService _ttsService;
// 连续识别相关 // 连续识别相关
StreamSubscription? _recognitionSubscription; StreamSubscription? _recognitionSubscription;
// 添加一个标志,表示是否已经识别到语音
bool _hasRecognizedSpeech = false;
// 添加一个 Completer 用于在识别到最终结果时完成
Completer<String>? _recognitionCompleter;
// 添加一个标志,表示是否已经收到最终结果
bool _hasFinalResult = false;
// 添加一个计时器,用于在一段时间没有新的识别结果时提交当前结果
Timer? _silenceTimer;
// 添加一个计时器,用于定期更新用户说话状态
Timer? _speakingCheckTimer;
// 最后一次识别到语音的时间
DateTime? _lastSpeechTime;
// 添加一个变量来跟踪空结果的数量
int _emptyResultCount = 0;
static const int _maxEmptyResults = 3;
// Callbacks // Callbacks
final Function(String) onRecordingResult; final Function(String) onRecordingResult;
final VoidCallback onClosePanel; final VoidCallback onClosePanel;
@ -31,6 +59,10 @@ class VoiceInputController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
_initializeVoiceService(); _initializeVoiceService();
_ttsService = Get.find<VolcanoTtsService>();
// 启动定期检查用户是否正在说话的计时器
_startSpeakingCheckTimer();
} }
Future<void> _initializeVoiceService() async { Future<void> _initializeVoiceService() async {
@ -62,7 +94,13 @@ class VoiceInputController extends GetxController {
try { try {
isRecording.value = true; isRecording.value = true;
recognizedText.value = ''; recognizedText.value = ''; // 清空识别文本,但不再在面板中显示
_hasRecognizedSpeech = false;
_hasFinalResult = false;
_emptyResultCount = 0;
// 创建一个 Completer 来处理语音识别完成
_recognitionCompleter = Completer<String>();
// 开始连续语音识别 // 开始连续语音识别
final stream = await _voiceService.startContinuousRecognition(); final stream = await _voiceService.startContinuousRecognition();
@ -71,21 +109,89 @@ class VoiceInputController extends GetxController {
_recognitionSubscription = stream.listen((event) { _recognitionSubscription = stream.listen((event) {
switch (event.type) { switch (event.type) {
case RecognitionEventType.intermediateResult: case RecognitionEventType.intermediateResult:
// 更新中间识别结果 // 不再更新面板中的识别文本,而是通过回调传递给ChatController
recognizedText.value = event.text; 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); onRecognizing(event.text);
break; break;
case RecognitionEventType.finalResult: case RecognitionEventType.finalResult:
// 更新最终识别结果 // 更新最终识别结果
_hasFinalResult = true;
// 重置用户说话状态
isUserSpeaking.value = false;
if (event.text.isNotEmpty) { if (event.text.isNotEmpty) {
recognizedText.value = event.text; // 不再更新面板中的识别文本
_hasRecognizedSpeech = true;
_lastSpeechTime = DateTime.now();
_emptyResultCount = 0; // 重置空结果计数
// 调用最终结果回调,发送识别到的句子 // 调用最终结果回调,发送识别到的句子
onRecordingResult(event.text); 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; break;
@ -123,6 +229,9 @@ class VoiceInputController extends GetxController {
restartRecognition(); restartRecognition();
}); });
// 等待语音识别完成
final result = await _recognitionCompleter!.future;
} catch (e) { } catch (e) {
print('语音识别失败: $e'); print('语音识别失败: $e');
Get.snackbar( Get.snackbar(
@ -131,6 +240,11 @@ class VoiceInputController extends GetxController {
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
); );
isRecording.value = false; isRecording.value = false;
} finally {
// 清理资源
_recognitionCompleter = null;
_silenceTimer?.cancel();
_silenceTimer = null;
} }
} }
@ -156,12 +270,19 @@ class VoiceInputController extends GetxController {
await _recognitionSubscription?.cancel(); await _recognitionSubscription?.cancel();
_recognitionSubscription = null; _recognitionSubscription = null;
// 取消计时器
_silenceTimer?.cancel();
_silenceTimer = null;
// 停止连续识别 // 停止连续识别
if (_voiceService.isContinuousRecognitionActive()) { if (_voiceService.isContinuousRecognitionActive()) {
await _voiceService.stopContinuousRecognition(); await _voiceService.stopContinuousRecognition();
} }
isRecording.value = false; isRecording.value = false;
_hasRecognizedSpeech = false;
_hasFinalResult = false;
_emptyResultCount = 0;
} catch (e) { } catch (e) {
print('停止语音识别失败: $e'); print('停止语音识别失败: $e');
isRecording.value = false; 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 @override
void onClose() { void onClose() {
// 停止录音 // 停止录音
stopRecording(); stopRecording();
// 取消计时器
_speakingCheckTimer?.cancel();
// 重置状态 // 重置状态
isConnecting.value = false; isConnecting.value = false;
isMuted.value = false; isMuted.value = false;
isRecording.value = false; isRecording.value = false;
recognizedText.value = ''; recognizedText.value = '';
isUserSpeaking.value = false;
super.onClose(); super.onClose();
} }

174
lib/modules/chat/views/chat_view.dart

@ -148,14 +148,6 @@ class ChatView extends GetView<ChatController> {
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF4A6FE5)), valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF4A6FE5)),
), ),
), ),
SizedBox(width: 8.w),
Text(
'正在思考...',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
),
], ],
), ),
), ),
@ -252,7 +244,7 @@ class ChatView extends GetView<ChatController> {
), ),
child: Center( child: Center(
child: Text( child: Text(
'点击开始语音识别', '点击开始连续语音对话',
style: TextStyle( style: TextStyle(
color: Colors.grey[600], color: Colors.grey[600],
fontSize: 16.sp, fontSize: 16.sp,
@ -425,6 +417,47 @@ class ChatView extends GetView<ChatController> {
Widget _buildMessage(Message message) { Widget _buildMessage(Message message) {
final isUser = message.role == 'user'; 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( return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Row( child: Row(
@ -443,21 +476,53 @@ class ChatView extends GetView<ChatController> {
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
decoration: BoxDecoration( 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( borderRadius: BorderRadius.only(
topLeft: Radius.circular(isUser ? 18.r : 4.r), topLeft: Radius.circular(isUser ? 18.r : 4.r),
topRight: Radius.circular(isUser ? 4.r : 18.r), topRight: Radius.circular(isUser ? 4.r : 18.r),
bottomLeft: Radius.circular(18.r), bottomLeft: Radius.circular(18.r),
bottomRight: Radius.circular(18.r), bottomRight: Radius.circular(18.r),
), ),
// 为正在识别的消息添加脉动效果
boxShadow: isRecognizing
? [
BoxShadow(
color: Colors.purple.withOpacity(0.3),
blurRadius: 8,
spreadRadius: 2,
),
]
: [],
), ),
child: Text( child: Row(
message.content, mainAxisSize: MainAxisSize.min,
style: TextStyle( children: [
fontSize: 16.sp, if (isRecognizing) ...[
color: isUser ? Colors.white : Colors.black87, Icon(
height: 1.5, 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<ChatController> {
), ),
); );
} }
// 创建脉动点动画
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<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
)..repeat(reverse: true);
_animation = Tween<double>(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,
),
),
],
);
},
),
);
}
} }

211
lib/modules/chat/widgets/voice_input_panel.dart

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:math';
import 'dart:async';
import '../controllers/voice_input_controller.dart'; import '../controllers/voice_input_controller.dart';
class VoiceInputPanel extends GetView<VoiceInputController> { class VoiceInputPanel extends GetView<VoiceInputController> {
@ -59,30 +61,30 @@ class VoiceInputPanel extends GetView<VoiceInputController> {
], ],
), ),
child: Center( child: Center(
child: Icon( child: Obx(() => Icon(
Icons.mic, controller.isRecording.value ? Icons.mic : Icons.mic_none,
color: Colors.purple.shade300, color: controller.isRecording.value ? Colors.purple : Colors.purple.shade300,
size: 28.sp, size: 28.sp,
), )),
), ),
), ),
// 中间音频波形 // 中间音频波形和状态文本
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
_buildAudioWaveform(), _buildAudioWaveform(),
SizedBox(height: 10.h), SizedBox(height: 10.h),
// 文字提示 // 文字提示 - 根据状态显示不同文本
Text( Obx(() => Text(
'你可以开始说话', _getStatusText(),
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
color: Colors.grey.shade700, color: Colors.grey.shade700,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), )),
], ],
), ),
@ -117,16 +119,27 @@ class VoiceInputPanel extends GetView<VoiceInputController> {
), ),
), ),
// 底部进度条 // 底部进度条和识别文本
Padding( Padding(
padding: EdgeInsets.only(top: 15.h), padding: EdgeInsets.only(top: 15.h),
child: Container( child: Column(
width: 250.w, children: [
height: 4.h, // 移除显示当前识别的文本,因为现在直接显示在聊天窗口中
decoration: BoxDecoration(
color: Colors.purple.withOpacity(0.2), // 进度条
borderRadius: BorderRadius.circular(2.r), 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<VoiceInputController> {
); );
} }
// 根据状态返回不同的提示文本
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() { Widget _buildAudioWaveform() {
return Obx(() { return Obx(() {
final isSpeaking = _isUserSpeaking();
final barCount = 15; // 增加条形数量,使波形更丰富
// 根据控制器状态动态生成波形高度 // 根据控制器状态动态生成波形高度
return Row( return Container(
mainAxisSize: MainAxisSize.min, width: 180.w, // 增加宽度以容纳更多条形
children: List.generate( height: 40.h, // 增加高度以允许更大的波动
11, child: Row(
(index) { mainAxisSize: MainAxisSize.min,
// 创建随机高度效果,模拟声波 mainAxisAlignment: MainAxisAlignment.center,
double height = controller.isRecording.value children: List.generate(
? (4 + (index % 3) * 3).h // 当正在录音时显示不同高度 barCount,
: 4.h; // 默认高度 (index) {
// 创建动态高度效果,模拟声波
return Container( double height = controller.isRecording.value
width: 4.w, ? _getWaveHeight(index, isSpeaking, barCount) // 当正在录音时显示动态高度
height: height, : 4.h; // 默认高度
margin: EdgeInsets.symmetric(horizontal: 2.w),
decoration: BoxDecoration( // 根据位置计算不同的动画持续时间,使波形更自然
color: controller.isRecording.value final duration = Duration(
? Colors.purple.shade300 // 录音时使用紫色 milliseconds: 100 + (index % 3) * 50
: Colors.grey.shade400, // 默认使用灰色 );
borderRadius: BorderRadius.circular(2.r),
), // 根据是否说话和位置计算颜色
); 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;
}
}
} }

41
lib/modules/profile/controllers/profile_controller.dart

@ -1,5 +1,6 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:flutter/material.dart';
class ProfileController extends GetxController { class ProfileController extends GetxController {
static ProfileController get to => Get.find(); static ProfileController get to => Get.find();
@ -10,6 +11,9 @@ class ProfileController extends GetxController {
final _isEditing = false.obs; final _isEditing = false.obs;
final _storage = GetStorage(); final _storage = GetStorage();
// 使用响应式变量
final currentLanguage = 'zh'.obs;
String get userName => _userName.value; String get userName => _userName.value;
String get email => _email.value; String get email => _email.value;
String get avatar => _avatar.value; String get avatar => _avatar.value;
@ -19,6 +23,7 @@ class ProfileController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
loadUserData(); loadUserData();
loadLanguagePreference();
} }
void loadUserData() { 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<void> 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<void> updateUserData({ Future<void> updateUserData({
String? userName, String? userName,
String? email, String? email,

119
lib/modules/profile/views/profile_view.dart

@ -2,12 +2,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/profile_controller.dart'; import '../controllers/profile_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../data/services/volcano_tts_service.dart';
import '../../../core/widgets/common_bottom_nav.dart'; import '../../../core/widgets/common_bottom_nav.dart';
import '../../speech_demo/speech_demo_page.dart'; import '../../../data/services/audio_service.dart';
import '../../../modules/microsoft_tts_example.dart';
import '../../../modules/microsoft_tts_continuous_example.dart';
import '../../../data/services/microsoft_tts_service.dart';
class ProfileView extends GetView<ProfileController> { class ProfileView extends GetView<ProfileController> {
const ProfileView({Key? key}) : super(key: key); const ProfileView({Key? key}) : super(key: key);
@ -53,7 +49,7 @@ class ProfileView extends GetView<ProfileController> {
backgroundColor: Colors.white, backgroundColor: Colors.white,
elevation: 0, elevation: 0,
title: Text( title: Text(
'设置', '设置'.tr,
style: TextStyle( style: TextStyle(
fontSize: 17.sp, fontSize: 17.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -65,83 +61,10 @@ class ProfileView extends GetView<ProfileController> {
body: ListView( body: ListView(
children: [ children: [
SizedBox(height: 12.h), SizedBox(height: 12.h),
_buildMenuItem(
title: '测试语音合成',
icon: Icons.record_voice_over,
subtitle: '测试微软语音TTS功能',
onTap: () async {
try {
final tts = Get.find<MicrosoftTtsService>();
// 检查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(), const Divider(),
_buildMenuItem( _buildMenuItem(
title: '微软语音合成测试', title: '关于'.tr,
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: '关于',
icon: Icons.info_outline, icon: Icons.info_outline,
onTap: () { onTap: () {
// TODO: 实现关于页面 // TODO: 实现关于页面
@ -149,7 +72,7 @@ class ProfileView extends GetView<ProfileController> {
), ),
const Divider(), const Divider(),
_buildMenuItem( _buildMenuItem(
title: '检查更新', title: '检查更新'.tr,
icon: Icons.system_update, icon: Icons.system_update,
onTap: () { onTap: () {
// TODO: 实现检查更新 // TODO: 实现检查更新
@ -157,12 +80,42 @@ class ProfileView extends GetView<ProfileController> {
), ),
const Divider(), const Divider(),
_buildMenuItem( _buildMenuItem(
title: '清除缓存', title: '清除缓存'.tr,
icon: Icons.cleaning_services_outlined, icon: Icons.cleaning_services_outlined,
onTap: () { onTap: () {
// TODO: 实现清除缓存 // TODO: 实现清除缓存
}, },
), ),
const Divider(),
_buildMenuItem(
title: '测试媒体按钮事件'.tr,
icon: Icons.headset,
subtitle: '手动触发媒体按钮事件,用于测试蓝牙耳机按键功能',
onTap: () {
try {
// 获取 AudioServiceManager 实例并触发媒体按钮事件
final audioService = Get.find<AudioServiceManager>();
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), bottomNavigationBar: const CommonBottomNav(currentIndex: 2),

Loading…
Cancel
Save