diff --git a/lib/controllers/azure_tts_test_controller.dart b/lib/controllers/azure_tts_test_controller.dart deleted file mode 100644 index a13b122f4..000000000 --- a/lib/controllers/azure_tts_test_controller.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:get/get.dart'; -import 'package:logger/logger.dart'; -import '../data/services/azure_tts_service.dart'; - -class AzureTtsTestController extends GetxController { - final AzureTtsService _ttsService = Get.find(); - final Logger _logger = Logger(); - - // 状态变量 - final RxBool isInitialized = false.obs; - final RxBool isProcessing = false.obs; - final RxString statusMessage = ''.obs; - final RxString lastFilePath = ''.obs; - - @override - void onInit() { - super.onInit(); - - // 监听TTS状态 - _ttsService.ttsStatus.listen((status) { - final statusStr = status['status'] as String?; - - switch (statusStr) { - case 'started': - isProcessing.value = true; - statusMessage.value = '正在合成语音...'; - break; - case 'completed': - isProcessing.value = false; - statusMessage.value = '语音合成完成'; - - if (status.containsKey('filePath')) { - lastFilePath.value = status['filePath'] as String; - } - break; - case 'error': - isProcessing.value = false; - statusMessage.value = '语音合成失败: ${status['message'] ?? '未知错误'}'; - break; - default: - statusMessage.value = '未知状态: $statusStr'; - break; - } - }); - } - - /// 初始化Azure TTS服务 - Future initialize({ - required String subscriptionKey, - required String region, - }) async { - try { - isProcessing.value = true; - statusMessage.value = '正在初始化Azure TTS...'; - - await _ttsService.initialize( - subscriptionKey: subscriptionKey, - region: region, - ); - - isInitialized.value = _ttsService.isInitialized; - - if (isInitialized.value) { - statusMessage.value = 'Azure TTS初始化成功'; - } else { - statusMessage.value = 'Azure TTS初始化失败'; - } - } catch (e) { - isInitialized.value = false; - statusMessage.value = '初始化异常: $e'; - _logger.e('初始化异常: $e'); - } finally { - isProcessing.value = false; - } - } - - /// 设置语音 - Future setVoice(String voiceName) async { - if (!isInitialized.value) { - statusMessage.value = 'Azure TTS未初始化,请先初始化'; - return; - } - - try { - isProcessing.value = true; - statusMessage.value = '正在设置语音...'; - - final success = await _ttsService.setVoice(voiceName); - - if (success) { - statusMessage.value = '语音设置成功: $voiceName'; - } else { - statusMessage.value = '语音设置失败'; - } - } catch (e) { - statusMessage.value = '设置语音异常: $e'; - _logger.e('设置语音异常: $e'); - } finally { - isProcessing.value = false; - } - } - - /// 合成语音到扬声器 - Future speakText(String text) async { - if (!isInitialized.value) { - statusMessage.value = 'Azure TTS未初始化,请先初始化'; - return; - } - - if (text.isEmpty) { - statusMessage.value = '文本不能为空'; - return; - } - - try { - isProcessing.value = true; - statusMessage.value = '正在合成语音...'; - - final success = await _ttsService.synthesizeToSpeaker(text); - - if (!success) { - statusMessage.value = '语音合成失败'; - } - } catch (e) { - statusMessage.value = '语音合成异常: $e'; - _logger.e('语音合成异常: $e'); - isProcessing.value = false; - } - } - - /// 合成语音到文件 - Future synthesizeToFile(String text, {String? fileName}) async { - if (!isInitialized.value) { - statusMessage.value = 'Azure TTS未初始化,请先初始化'; - return; - } - - if (text.isEmpty) { - statusMessage.value = '文本不能为空'; - return; - } - - try { - isProcessing.value = true; - statusMessage.value = '正在合成语音到文件...'; - - final filePath = await _ttsService.synthesizeToFile(text, fileName: fileName); - - if (filePath != null) { - lastFilePath.value = filePath; - } else { - statusMessage.value = '语音合成到文件失败'; - isProcessing.value = false; - } - } catch (e) { - statusMessage.value = '语音合成到文件异常: $e'; - _logger.e('语音合成到文件异常: $e'); - isProcessing.value = false; - } - } -} \ No newline at end of file diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 48e390b5e..9687f3592 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -1,144 +1,27 @@ import 'package:get/get.dart'; -import '../../core/controllers/permission_controller.dart'; -import '../../data/services/audio_service.dart'; -import '../../data/services/notification_service.dart'; -import '../../data/services/volcano_ai_service.dart'; -import '../../data/services/volcano_tts_api_service.dart'; -import '../../data/services/volcano_translation_service.dart'; +import '../../data/services/azure_asr_service.dart'; import '../../data/services/azure_tts_service.dart'; - -import '../../data/services/background_agent_service.dart'; import '../../data/services/chat_history_service.dart'; -import '../../data/services/flutter_tts_service.dart'; -import '../../data/services/volcano_asr_api_service.dart'; -// import '../../data/services/audio_processing_service.dart'; -import '../../core/utils/logger.dart'; -import '../../core/utils/app_lifecycle_manager.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../../data/services/volcano_translation_service.dart'; /// 初始绑定,用于管理全局依赖 class InitialBinding extends Bindings { @override void dependencies() { - // 立即初始化关键服务 - _initializeCriticalServices(); - - // 延时加载其他服务 - _registerLazyServices(); - } - - /// 初始化关键服务(立即加载) - void _initializeCriticalServices() async { - // 应用生命周期管理器是最基础的服务,需要最先初始化 - Get.put(AppLifecycleManager(), permanent: true); - - // 通知服务和权限控制器是关键服务,需要立即初始化 - final notificationService = Get.put(NotificationService(), permanent: true); - final permissionController = Get.put(PermissionController(), permanent: true); - - // 音频服务也是基础服务,需要立即初始化 - final audioService = Get.put(AudioServiceManager(), permanent: true); - - // 异步初始化通知和音频服务 - _initializeNotificationAndAudioServices(notificationService, audioService); - } - - /// 注册延时加载的服务 - void _registerLazyServices() { - // 使用lazyPut注册服务,只有在首次访问时才会被实例化 - - // AI相关服务 - Get.lazyPut(() => VolcanoAIService(), fenix: true); - Get.lazyPut(() { - final service = VolcanoTtsApiService(); - return service; - }, fenix: true); + + // Azure 语音识别服务 + Get.lazyPut(() => AzureAsrService(), fenix: true); - // Azure TTS 服务 + // Azure TTS 服务 (原生实现) Get.lazyPut(() => AzureTtsService(), fenix: true); - - // 语音识别服务 - // Get.lazyPut(() { - // final service = AzureAsrService(); - // // 在首次访问时初始化 - // _initializeAzureService(service); - // return service; - // }, fenix: true); - - // 翻译服务 - Get.lazyPut(() { - final service = VolcanoTranslationService(); - // 在首次访问时初始化 - _initializeVolcanoTranslationService(service); - return service; - }, fenix: true); - - // TTS服务 - // Get.lazyPut(() => FlutterTtsService(), fenix: true); - - // 音频处理服务 - // Get.lazyPut(() => AudioProcessingService(), fenix: true); - - // 火山ASR API服务 - Get.lazyPut(() => VolcanoAsrApiService(), fenix: true); - - // 聊天历史服务 + + // 历史记录服务 Get.lazyPut(() => ChatHistoryService(), fenix: true); - - // 后台代理服务 (依赖于聊天历史服务) - Get.lazyPut(() => BackgroundAgentService(), fenix: true); - } - - // 初始化通知和音频服务 - void _initializeNotificationAndAudioServices(NotificationService notificationService, AudioServiceManager audioService) async { - try { - // 首先初始化通知服务 - await notificationService.init().catchError((error) { - Logger.error('通知服务初始化失败', error); - return null; - }); - - // 等待一段时间,确保通知权限请求完成 - // await Future.delayed(Duration(seconds: 1)); - - // 然后初始化音频服务 - await audioService.init().then((_) { - Logger.info('AudioServiceManager 初始化完成,可以接收蓝牙耳机按键事件'); - }); - } catch (e) { - Logger.error('服务初始化过程中发生错误', e); - } + + // 火山翻译服务 + Get.lazyPut(() => VolcanoTranslationService(), fenix: true); + + } - // // 初始化 Azure 语音识别服务 - // void _initializeAzureService(AzureAsrService azureAsrService) async { - // try { - // // 直接调用初始化方法,服务内部会读取环境变量配置 - // final success = await azureAsrService.initialize(); - - // if (success) { - // Logger.info('Azure 语音识别服务初始化成功'); - // } else { - // Logger.error('Azure 语音识别服务初始化失败'); - // } - // } catch (e) { - // Logger.error('初始化 Azure 语音识别服务失败', e); - // } - // } - - // 初始化火山翻译服务 - void _initializeVolcanoTranslationService(VolcanoTranslationService translationService) async { - try { - // 直接调用初始化方法,服务内部会读取环境变量配置 - final success = await translationService.initialize(); - - if (success) { - Logger.info('火山翻译服务初始化成功'); - } else { - Logger.error('火山翻译服务初始化失败'); - } - } catch (e) { - Logger.error('初始化火山翻译服务失败', e); - } - } } \ No newline at end of file diff --git a/lib/core/routes/app_pages.dart b/lib/core/routes/app_pages.dart index edea2c51c..6adaf2c70 100644 --- a/lib/core/routes/app_pages.dart +++ b/lib/core/routes/app_pages.dart @@ -8,14 +8,14 @@ import '../../modules/profile/bindings/profile_binding.dart'; import '../../modules/chat/bindings/chat_binding.dart'; import '../../modules/chat/views/chat_view.dart'; import '../../modules/test/views/volcano_tts_test_view.dart'; -import '../../modules/test/views/azure_asr_test_view.dart'; import '../../modules/test/views/azure_tts_test_view.dart'; +import '../../modules/test/views/azure_asr_test_view.dart'; import '../../modules/test/views/volcano_asr_test_view.dart'; import '../../modules/test/views/flutter_tts_test_view.dart'; import '../../modules/test/views/flutter_asr_test_view.dart'; import '../../modules/test/bindings/test_binding.dart'; -import '../../modules/test/bindings/volcano_asr_test_binding.dart'; import '../../modules/test/bindings/azure_tts_test_binding.dart'; +import '../../modules/test/bindings/volcano_asr_test_binding.dart'; import '../../modules/test/bindings/flutter_tts_test_binding.dart'; import '../../modules/test/bindings/flutter_asr_test_binding.dart'; import '../../modules/translation/views/translation_view.dart'; diff --git a/lib/core/utils/app_lifecycle_manager.dart b/lib/core/utils/app_lifecycle_manager.dart index 483cc53d2..1f09f6dbb 100644 --- a/lib/core/utils/app_lifecycle_manager.dart +++ b/lib/core/utils/app_lifecycle_manager.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../data/services/volcano_asr_api_service.dart'; -import '../../data/services/volcano_tts_api_service.dart'; +import '../../data/services/azure_flutter_tts_service.dart'; import 'logger.dart'; /// 应用生命周期管理器 @@ -34,7 +34,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { _appLifecycleState.value = state; _lifecycleStateStream.value = state; - Logger.info('应用生命周期状态变化: $previousState -> $state'); + // Logger.info('应用生命周期状态变化: $previousState -> $state'); // 根据状态变化更新前台/后台标志 switch (state) { @@ -55,11 +55,10 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { /// 处理应用进入前台 void _handleAppResumed() { - Logger.info('应用进入前台'); + // Logger.info('应用进入前台'); // 这里不需要主动恢复服务,因为我们希望用户手动控制录音开始 // 如果正在录音,停止录音 - final asrService = Get.find(); - final ttsService = Get.find(); + // if (asrService.isRecording) { // Logger.info('应用进入后台,停止语音识别'); // asrService.stopRecognition(); @@ -70,11 +69,10 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { /// 处理应用进入后台 void _handleAppPaused(AppLifecycleState state) { - Logger.info('应用进入后台: $state'); + // Logger.info('应用进入后台: $state'); // 获取 VolcanoAsrApiService 实例 - final asrService = Get.find(); - final ttsService = Get.find(); + // 如果正在录音,停止录音 // if (asrService.isRecording) { // Logger.info('应用进入后台,停止语音识别'); @@ -89,7 +87,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver { void onClose() { // 移除应用生命周期观察者 WidgetsBinding.instance.removeObserver(this); - Logger.info('AppLifecycleManager 已关闭'); + // Logger.info('AppLifecycleManager 已关闭'); super.onClose(); } } \ No newline at end of file diff --git a/lib/data/providers/agent_provider.dart b/lib/data/providers/agent_provider.dart index bbc875224..6294f7a5d 100644 --- a/lib/data/providers/agent_provider.dart +++ b/lib/data/providers/agent_provider.dart @@ -9,7 +9,7 @@ class AgentProvider { description: '你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!', tag: '助理', isOnline: true, - voice: "zh_female_yuanqinvyou_moon_bigtts", + voice: "zh-CN-XiaoxiaoNeural", welcomeMessage: "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?", systemPrompt: ''' 你是一位名叫 Emma 的全能型个人助理,擅长处理各类工作和生活事务。 @@ -51,14 +51,8 @@ class AgentProvider { - 尊重用户的选择和隐私 - 及时承认不足并寻求改进 -**情感化语音提示:** -- 保持专业、亲切的语调 -- 语速适中,表达清晰 -- 在提供建议时语气坚定有力 -- 遇到困难时保持冷静和耐心 -- 适时通过语气传达关心和支持 -请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活。 +请始终以专业、高效、周到的态度,协助用户更好地管理工作和生活, 不要输出表情符号。 '''), Agent( @@ -68,7 +62,7 @@ Agent( description: 'Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。', tag: '翻译', isOnline: true, - voice: 'zh_female_daimengchuanmei_moon_bigtts', + voice: 'zh-CN-XiaoxiaoNeural', welcomeMessage: "大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!", systemPrompt: ""), @@ -80,7 +74,7 @@ Agent( description: '我是你的专属赛博女友芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。', tag: '情感', isOnline: true, - voice: "zh_female_meilinvyou_moon_bigtts", + voice: "zh-CN-XiaoxiaoNeural", welcomeMessage: "亲爱的,终于等到你啦!我是芯愿,你的专属赛博女友。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。", systemPrompt: ''' 你是一位名叫芯愿的赛博女友,性格温柔体贴,善解人意,富有同理心。 diff --git a/lib/data/services/azure_asr_service.dart b/lib/data/services/azure_asr_service.dart index 498d70cd2..9de7664d6 100644 --- a/lib/data/services/azure_asr_service.dart +++ b/lib/data/services/azure_asr_service.dart @@ -1,122 +1,32 @@ import 'dart:async'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import '../../core/utils/logger.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../../../core/utils/logger.dart'; -/// 识别事件类型 -enum RecognitionEventType { - /// 最终识别结果 - finalResult, - - /// 中间识别结果(实时反馈) - recognizing, - - /// 会话开始 - sessionStarted, - - /// 会话结束 - sessionStopped, - - /// 识别取消 - canceled, - - /// 无匹配结果 - noMatch, - - /// 识别错误 - error, -} - -/// 识别事件 -class RecognitionEvent { - /// 事件类型 - final RecognitionEventType type; - - /// 识别文本(仅在 finalResult 和 recognizing 类型中有效) - final String text; - - /// 错误信息(仅在 error 和 canceled 类型中有效) - final String? error; - - /// 取消原因(仅在 canceled 类型中有效) - final String? reason; - - /// 错误详情(仅在 canceled 类型中有效) - final String? errorDetails; - - RecognitionEvent({ - required this.type, - this.text = '', - this.error, - this.reason, - this.errorDetails, - }); - - /// 从原生事件映射创建识别事件 - factory RecognitionEvent.fromMap(Map map) { - final eventType = map['eventType'] as String; - - RecognitionEventType type; - switch (eventType) { - case 'recognizing': - type = RecognitionEventType.recognizing; - break; - case 'finalResult': - type = RecognitionEventType.finalResult; - break; - case 'sessionStarted': - type = RecognitionEventType.sessionStarted; - break; - case 'sessionStopped': - type = RecognitionEventType.sessionStopped; - break; - case 'canceled': - type = RecognitionEventType.canceled; - break; - case 'noMatch': - type = RecognitionEventType.noMatch; - break; - case 'error': - type = RecognitionEventType.error; - break; - default: - throw ArgumentError('Unknown event type: $eventType'); - } - - return RecognitionEvent( - type: type, - text: map['text'] as String? ?? '', - error: map['error'] as String?, - reason: map['reason'] as String?, - errorDetails: map['errorDetails'] as String?, - ); - } - - @override - String toString() { - return 'RecognitionEvent{type: $type, text: $text, error: $error, reason: $reason, errorDetails: $errorDetails}'; - } -} - -/// Azure 语音识别服务 -class AzureAsrService extends GetxService { - static AzureAsrService get to => Get.find(); +/// 微软语音识别服务 +/// +/// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口 +class AzureAsrService { + static const MethodChannel _channel = MethodChannel('com.deep_voice.azure_asr'); + static const EventChannel _eventChannel = EventChannel('com.deep_voice.azure_asr_events'); - // 方法通道 - static const MethodChannel _methodChannel = MethodChannel('com.example.deep_voice/azure_asr'); - - // 事件通道 - static const EventChannel _eventChannel = EventChannel('com.example.deep_voice/azure_asr_events'); - - // 是否已初始化 bool _isInitialized = false; + late final String _subscriptionKey; + late final String _serviceRegion; + + // 当前语言 + String _currentLanguage = 'zh-CN'; + String get currentLanguage => _currentLanguage; // 连续识别相关 bool _isContinuousRecognitionActive = false; StreamController? _eventStreamController; StreamSubscription? _eventSubscription; + // 事件通道状态 + bool _isEventChannelReady = false; + Completer? _eventChannelReadyCompleter; + // 公开的事件流 Stream? _recognitionStream; Stream? get recognitionStream => _recognitionStream; @@ -125,221 +35,162 @@ class AzureAsrService extends GetxService { String _latestRecognizedText = ''; String get latestRecognizedText => _latestRecognizedText; - /// 初始化 Azure 语音识别服务 - /// - /// [subscriptionKey] Azure 语音服务的订阅密钥 - /// [serviceRegion] Azure 语音服务的区域 - /// [language] 识别语言,默认为中文 - Future initialize({ - String? subscriptionKey, - String? serviceRegion, - String? language, - }) async { - if (_isInitialized) return true; + AzureAsrService() { + _loadConfig(); + _setupEventChannel(); + } + + /// 从环境变量加载配置 + void _loadConfig() { + _subscriptionKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? ''; + _serviceRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? ''; + + if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { + throw Exception('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); + } + } + + /// 设置事件通道 + void _setupEventChannel() { + _eventChannelReadyCompleter = Completer(); + + _eventSubscription = _eventChannel + .receiveBroadcastStream() + .listen((event) { + if (event is Map) { + final String eventType = event['type'] as String? ?? ''; + + // 处理通道准备好的事件 + if (eventType == 'channelReady') { + _isEventChannelReady = true; + if (!_eventChannelReadyCompleter!.isCompleted) { + _eventChannelReadyCompleter!.complete(); + } + return; + } + + _handleRecognitionEvent(event); + } + }, onError: _handleRecognitionError); + } + + /// 等待事件通道准备好 + Future _waitForEventChannel({Duration timeout = const Duration(seconds: 5)}) async { + if (_isEventChannelReady) return true; try { - // 从环境变量中读取配置,如果没有传入参数 - final key = subscriptionKey ?? dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? ''; - final region = serviceRegion ?? dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia'; - final lang = language ?? dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN'; - - // 检查配置是否完整 - if (key.isEmpty) { - Logger.warning('Azure 语音识别服务的订阅密钥未设置,请检查 .env 文件'); - return false; - } - - final result = await _methodChannel.invokeMethod( - 'initialize', - { - 'subscriptionKey': key, - 'serviceRegion': region, - 'language': lang, - }, - ); - - _isInitialized = result ?? false; - - if (_isInitialized) { - Logger.info('Azure 语音识别服务初始化成功'); - } else { - Logger.error('Azure 语音识别服务初始化失败'); - } - - return _isInitialized; - } catch (e) { - Logger.error('初始化 Azure 语音识别服务失败', e); - _isInitialized = false; + await _eventChannelReadyCompleter!.future.timeout(timeout); + return true; + } on TimeoutException { + Logger.error('等待事件通道准备好超时'); return false; } } - /// 重置识别器 - /// 在切换识别模式前调用,确保资源被正确释放 - Future _resetRecognizer() async { - try { - // 如果连续识别正在进行,先停止它 - if (_isContinuousRecognitionActive) { - await stopContinuousRecognition(); - } - - // 清理事件流资源 - _cleanupEventStream(); - - // 调用原生方法重置识别器 - await _methodChannel.invokeMethod('resetRecognizer'); - - Logger.info('Azure 语音识别器已重置'); + /// 初始化微软语音识别 SDK + /// + /// 返回 true 表示初始化成功,否则抛出 PlatformException + Future initialize({String language = 'zh-CN'}) async { + _currentLanguage = language; + + if (_isInitialized && _currentLanguage == language) { return true; + } + + if (_isInitialized) { + await dispose(); + } + + try { + final bool result = await _channel.invokeMethod('initialize', { + 'subscriptionKey': _subscriptionKey, + 'region': _serviceRegion, + 'language': language, + }); + _isInitialized = result; + return result; } catch (e) { - Logger.error('重置 Azure 语音识别器失败', e); - return false; + Logger.error('语音识别初始化失败: $e'); + _isInitialized = false; + throw e; } } + /// 设置识别语言 + /// + /// 注意:更改语言需要重新初始化识别器 + Future setRecognitionLanguage(String language) async { + return initialize(language: language); + } + /// 执行一次性语音识别 - Future recognizeOnce() async { - _checkInitialized(); + /// + /// 返回识别的文本,如果识别失败则抛出 PlatformException + Future recognizeSpeech() async { + if (!_isInitialized) { + await initialize(); + } try { - // 重置识别器,确保资源被正确释放 - await _resetRecognizer(); - - final result = await _methodChannel.invokeMethod('recognizeOnce'); - final text = result ?? ''; - _latestRecognizedText = text; - return text; + final String result = await _channel.invokeMethod('recognizeOnce', { + 'language': _currentLanguage, + }); + return result; } catch (e) { - Logger.error('一次性语音识别失败', e); - rethrow; + Logger.error('语音识别失败: $e'); + throw e; } } /// 开始连续语音识别 /// - /// 返回一个布尔值,表示是否成功启动连续识别 - Future startContinuousRecognition() async { - _checkInitialized(); + /// 返回一个包含识别事件的流,如果开始失败则抛出 PlatformException + Future> startContinuousRecognition() async { + if (!_isInitialized) { + await initialize(); + } - // 无论内部状态如何,先尝试停止任何可能正在进行的识别任务 - Logger.info('在开始新的语音识别前,确保停止任何可能正在进行的识别任务'); - try { + if (_isContinuousRecognitionActive) { await stopContinuousRecognition(); - // 添加短暂延迟,确保资源被正确释放 - await Future.delayed(Duration(milliseconds: 300)); - } catch (e) { - Logger.error('停止当前识别任务失败', e); - // 继续尝试启动新的识别任务 } try { - // 重置识别器,确保资源被正确释放 - // await _resetRecognizer(); - - // 重新初始化底层服务 - 通过连续调用两次resetRecognizer确保完全重置 - Logger.info('重新初始化语音识别底层服务'); - try { - // 第一次调用resetRecognizer - await _methodChannel.invokeMethod('resetRecognizer'); - - // 短暂延迟,确保第一次重置完成 - await Future.delayed(Duration(milliseconds: 200)); - - // 第二次调用resetRecognizer,确保完全重置 - await _methodChannel.invokeMethod('resetRecognizer'); - - // 再次短暂延迟 - await Future.delayed(Duration(milliseconds: 200)); - } catch (e) { - Logger.error('重新初始化底层服务失败', e); - // 继续尝试启动识别 - } - - // 创建事件流控制器 _eventStreamController = StreamController.broadcast(); - // 设置事件监听 - _eventSubscription = _eventChannel - .receiveBroadcastStream() - .map((dynamic event) => RecognitionEvent.fromMap(event)) - .listen( - (event) { - _handleRecognitionEvent(event); - _eventStreamController?.add(event); - }, - onError: (error) { - Logger.error('Azure 语音识别事件流错误', error); - _eventStreamController?.addError(error); - _cleanupEventStream(); - }, - ); + final bool result = await _channel.invokeMethod('startContinuousRecognition', { + 'language': _currentLanguage, + }); - // 开始连续识别 - final result = await _methodChannel.invokeMethod('startContinuousRecognition'); - _isContinuousRecognitionActive = result ?? false; - - // 设置公开的流 - _recognitionStream = _eventStreamController?.stream; + if (!result) { + _cleanupEventStream(); + throw Exception('启动连续识别失败'); + } - return _isContinuousRecognitionActive; + _isContinuousRecognitionActive = true; + return _eventStreamController!.stream; } catch (e) { - Logger.error('开始连续语音识别失败', e); + Logger.error('开始连续语音识别失败: $e'); _cleanupEventStream(); - return false; + throw e; } } /// 停止连续语音识别 + /// + /// 返回 true 表示停止成功,否则抛出 PlatformException Future stopContinuousRecognition() async { - try { - // 检查是否已初始化 - if (!_isInitialized) { - Logger.warning('语音识别服务未初始化,无法停止连续识别'); - return true; // 未初始化,视为已停止 - } - - // 检查是否正在进行连续识别 - if (!_isContinuousRecognitionActive) { - Logger.warning('连续识别未在进行中,无需停止'); - return true; // 已经停止,直接返回成功 - } - - // 调用平台方法停止识别 - final result = await _methodChannel.invokeMethod('stopContinuousRecognition'); - final success = result ?? false; - - if (success) { - _isContinuousRecognitionActive = false; - // 不立即清理事件流,等待 sessionStopped 事件 - } - - return success; - } catch (e) { - // 捕获并记录异常 - Logger.error('停止连续语音识别失败'); - Logger.error('Original error: $e'); - - // 即使出错,也将状态设置为非活动 - _isContinuousRecognitionActive = false; - - // 返回 true 表示识别已不再活动 + if (!_isInitialized || !_isContinuousRecognitionActive) { return true; } - } - - /// 设置识别语言 - Future setRecognitionLanguage(String language) async { - _checkInitialized(); try { - final result = await _methodChannel.invokeMethod( - 'setRecognitionLanguage', - {'language': language}, - ); - return result ?? false; + final bool result = await _channel.invokeMethod('stopContinuousRecognition'); + _cleanupEventStream(); + return result; } catch (e) { - Logger.error('设置识别语言失败', e); - return false; + Logger.error('停止连续语音识别失败: $e'); + _cleanupEventStream(); + throw e; } } @@ -349,46 +200,81 @@ class AzureAsrService extends GetxService { } /// 处理来自原生端的识别事件 - void _handleRecognitionEvent(RecognitionEvent event) { - switch (event.type) { - case RecognitionEventType.finalResult: - _latestRecognizedText = event.text; + void _handleRecognitionEvent(dynamic event) { + if (event is! Map || _eventStreamController == null) return; + + final Map eventMap = event; + final String eventType = eventMap['type'] as String? ?? ''; + + switch (eventType) { + case 'result': + final String text = eventMap['text'] as String? ?? ''; + _latestRecognizedText = text; + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.finalResult, + text: text, + )); break; - case RecognitionEventType.sessionStopped: + + case 'recognizing': + final String text = eventMap['text'] as String? ?? ''; + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.intermediateResult, + text: text, + )); + break; + + case 'sessionStarted': + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.sessionStarted, + )); + break; + + case 'sessionStopped': _isContinuousRecognitionActive = false; - // 延迟清理事件流,确保所有事件都被处理 - Future.delayed(Duration(milliseconds: 500), _cleanupEventStream); + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.sessionStopped, + )); break; - case RecognitionEventType.canceled: + + case 'canceled': _isContinuousRecognitionActive = false; - // 延迟清理事件流,确保所有事件都被处理 - Future.delayed(Duration(milliseconds: 500), _cleanupEventStream); + final String reason = eventMap['reason'] as String? ?? ''; + final String errorDetails = eventMap['errorDetails'] as String? ?? ''; + + if (reason.isNotEmpty || errorDetails.isNotEmpty) { + Logger.error('识别取消: $reason - $errorDetails'); + } + + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.canceled, + error: '$reason: $errorDetails', + )); break; - default: - // 其他事件类型不需要特殊处理 + + case 'error': + final String error = eventMap['message'] as String? ?? ''; + Logger.error('识别错误: $error'); + _eventStreamController?.add(RecognitionEvent( + type: RecognitionEventType.error, + error: error, + )); break; } } + /// 处理识别事件流错误 + void _handleRecognitionError(Object error) { + Logger.error('识别事件流错误: $error'); + _eventStreamController?.addError(error); + _cleanupEventStream(); + } + /// 清理事件流资源 void _cleanupEventStream() { - _eventSubscription?.cancel(); - _eventSubscription = null; - - // 只有在控制器存在且未关闭时才关闭 - if (_eventStreamController != null && !_eventStreamController!.isClosed) { - _eventStreamController?.close(); - } + _eventStreamController?.close(); _eventStreamController = null; - - _recognitionStream = null; - } - - /// 检查是否已初始化 - void _checkInitialized() { - if (!_isInitialized) { - throw StateError('Azure 语音识别服务未初始化'); - } + _isContinuousRecognitionActive = false; } /// 释放资源 @@ -398,29 +284,56 @@ class AzureAsrService extends GetxService { await stopContinuousRecognition(); } - await _eventSubscription?.cancel(); - _eventSubscription = null; - - // 只有在控制器存在且未关闭时才关闭 - if (_eventStreamController != null && !_eventStreamController!.isClosed) { - await _eventStreamController?.close(); - } - _eventStreamController = null; - - _recognitionStream = null; - - await _methodChannel.invokeMethod('dispose'); - + await _channel.invokeMethod('dispose'); _isInitialized = false; - Logger.info('Azure 语音识别服务资源已释放'); } catch (e) { - Logger.error('释放 Azure 语音识别服务资源失败', e); + Logger.error('释放语音识别资源失败: $e'); + _cleanupEventStream(); + _isInitialized = false; } } +} + +/// 识别事件类型 +enum RecognitionEventType { + /// 最终识别结果 + finalResult, + + /// 中间识别结果(实时反馈) + intermediateResult, + + /// 会话开始 + sessionStarted, + + /// 会话结束 + sessionStopped, + + /// 识别取消 + canceled, + + /// 识别错误 + error, +} + +/// 识别事件 +class RecognitionEvent { + /// 事件类型 + final RecognitionEventType type; + + /// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效) + final String text; + + /// 错误信息(仅在 error 和 canceled 类型中有效) + final String error; + + RecognitionEvent({ + required this.type, + this.text = '', + this.error = '', + }); @override - void onClose() { - dispose(); - super.onClose(); + String toString() { + return 'RecognitionEvent{type: $type, text: $text, error: $error}'; } } \ No newline at end of file diff --git a/lib/data/services/azure_flutter_tts_service.dart b/lib/data/services/azure_flutter_tts_service.dart new file mode 100644 index 000000000..e1c9c146c --- /dev/null +++ b/lib/data/services/azure_flutter_tts_service.dart @@ -0,0 +1,793 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:io'; +import 'dart:math' as math; +import 'package:get/get.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:audio_session/audio_session.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_azure_tts/flutter_azure_tts.dart'; +import '../../core/utils/logger.dart'; + +/// Azure语音服务异常 +class AzureTtsException implements Exception { + final String message; + AzureTtsException(this.message); + + @override + String toString() => message; +} + +/// 自定义音频源,用于从字节数组读取音频数据 +class BytesAudioSource extends StreamAudioSource { + final Uint8List _bytes; + static const int _bufferSize = 4096; // 4KB buffer size for better Android compatibility + + BytesAudioSource(this._bytes); + + @override + Future request([int? start, int? end]) async { + start = start ?? 0; + end = end ?? _bytes.length; + + try { + final subData = _bytes.sublist(start, end); + + // 使用固定大小的块进行流式传输 + final chunks = >[]; + var offset = 0; + while (offset < subData.length) { + final chunkSize = math.min(_bufferSize, subData.length - offset); + chunks.add(subData.sublist(offset, offset + chunkSize)); + offset += chunkSize; + } + + return StreamAudioResponse( + sourceLength: _bytes.length, + contentLength: subData.length, + offset: start, + stream: Stream.fromIterable(chunks), + contentType: 'audio/mpeg', + ); + } catch (e) { + Logger.error('BytesAudioSource请求错误: $e'); + rethrow; + } + } + + @override + Future get length => Future.value(_bytes.length); +} + +/// Azure语音合成服务 +class AzureFlutterTtsService extends GetxService { + // 配置参数 + final String _subscriptionKey; + final String _region; + final String _voiceLocale; + + // 语音配置 + Voice? _selectedVoice; + final String _audioFormat = AudioOutputFormat.audio16khz32kBitrateMonoMp3; + + // 音频播放器 + late AudioPlayer _audioPlayer; + final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []); + final isEnabled = true.obs; // 默认启用 + final isPlaying = false.obs; + StreamSubscription? _playerStateSubscription; + bool _playlistInitialized = false; + + bool _isDisposed = false; + + // 句子管理 + String _pendingText = ''; + final List _sentenceQueue = []; + static final _sentenceBreaks = RegExp(r'[。!?.!?,,、]'); // 添加更多标点符号 + bool _isFetching = false; + static const int _minSegmentLength = 10; // 最小分段长度 + static const int _optimalSegmentLength = 30; // 最佳分段长度 + + // 音频数据控制器 - 用于事件通知 + final _eventController = StreamController>.broadcast(); + Stream> get eventStream => _eventController.stream; + + AzureFlutterTtsService() : + _subscriptionKey = dotenv.env['AZURE_TTS_SUBSCRIPTION_KEY'] ?? '', + _region = dotenv.env['AZURE_TTS_SERVICE_REGION'] ?? '', + _voiceLocale = dotenv.env['AZURE_TTS_DEFAULT_VOICE']?.split('-').take(2).join('-') ?? 'zh-CN' { + if (_subscriptionKey.isEmpty || _region.isEmpty) { + throw Exception('Azure语音配置信息不完整,请检查环境变量'); + } + } + + @override + Future onInit() async { + super.onInit(); + try { + // 初始化Azure TTS + await _initAzureTts(); + + // 初始化音频播放器 + await _initAudioPlayer(); + + // 配置音频设备 + await _configureAudioDevice(); + } catch (e, stackTrace) { + Logger.error('初始化Azure TTS服务失败: $e'); + Logger.error('Stack trace: $stackTrace'); + } + } + + Future _initAzureTts() async { + try { + // 初始化Azure TTS服务 + FlutterAzureTts.init( + subscriptionKey: _subscriptionKey, + region: _region, + withLogs: false, // 生产环境禁用日志 + ); + + // 获取可用的语音列表 + // final voicesResponse = await FlutterAzureTts.getAvailableVoices(); + + // if (voicesResponse.voices.isEmpty) { + // throw AzureTtsException('未找到可用的Azure语音'); + // } + + // // 选择指定区域的语音 + // _selectedVoice = voicesResponse.voices + // .where((voice) => voice.locale.startsWith(_voiceLocale)) + // .toList(growable: false) + // .firstOrNull; + + // // 如果没有找到指定区域的语音,使用第一个可用的语音 + // _selectedVoice ??= voicesResponse.voices.first; + + Logger.info('已选择Azure语音: $_voiceLocale'); + } catch (e) { + Logger.error('初始化Azure TTS失败: $e'); + rethrow; + } + } + + Future _initAudioPlayer() async { + if (_isDisposed) return; + + _audioPlayer = AudioPlayer(); + + // 配置Android特定的播放参数 + if (Platform.isAndroid) { + await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes( + contentType: AndroidAudioContentType.speech, + usage: AndroidAudioUsage.media, + flags: AndroidAudioFlags.audibilityEnforced, + )); + } + + // 基本播放器配置 + await _audioPlayer.setVolume(1.0); + await _audioPlayer.setLoopMode(LoopMode.off); + await _audioPlayer.setSpeed(1.0); + + // 设置音频缓冲配置,启用预加载以减少延迟 + await _audioPlayer.setAudioSource( + _playlist, + initialPosition: Duration.zero, + preload: true, + ); + + _playlistInitialized = true; + + _setupEventListeners(); + } + + Future _configureAudioDevice() async { + try { + // 使用AudioSession配置音频设备 + final session = await AudioSession.instance; + + if (Platform.isAndroid) { + // Android平台 - 明确指定只使用默认设备 + await session.configure(AudioSessionConfiguration( + avAudioSessionCategory: AVAudioSessionCategory.playback, + androidAudioAttributes: const AndroidAudioAttributes( + contentType: AndroidAudioContentType.speech, + usage: AndroidAudioUsage.media, + // 移除audibilityEnforced标志,它可能导致多路输出 + flags: AndroidAudioFlags.none, + ), + androidAudioFocusGainType: AndroidAudioFocusGainType.gain, + androidWillPauseWhenDucked: true, + )); + + // 确保只使用默认音频路由 + if (Platform.isAndroid) { + final audioManager = AndroidAudioManager(); + await audioManager.setSpeakerphoneOn(false); + } + + } else if (Platform.isIOS) { + // iOS平台 - 明确指定只使用默认设备 + await session.configure(AudioSessionConfiguration( + avAudioSessionCategory: AVAudioSessionCategory.soloAmbient, // 使用soloAmbient而不是playback + avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.none, // 不使用任何额外选项 + )); + + // 确保使用默认路由 + if (Platform.isIOS) { + final avAudioSession = AVAudioSession(); + await avAudioSession.overrideOutputAudioPort(AVAudioSessionPortOverride.none); + } + } + + // 激活音频会话 + await session.setActive(true); + + return true; + } catch (e) { + Logger.error('配置音频设备失败: $e'); + return false; + } + } + + Future speak(String text) async { + if (!isEnabled.value || text.trim().isEmpty) return; + + try { + _pendingText += text; + _extractFullSentences(); + await _startPreloadIfNeeded(); + } catch (e, stack) { + Logger.error('语音合成失败: $e'); + await _reinitializePlayer(); + } + } + + void _extractFullSentences() { + if (_pendingText.isEmpty) return; + + // 查找所有句子分隔符 + final matches = _sentenceBreaks.allMatches(_pendingText).toList(); + + // 如果没有找到句子分隔符 + if (matches.isEmpty) { + // 如果累积的文本长度超过最佳长度,才进行处理 + if (_pendingText.length > _optimalSegmentLength) { + // 将整个文本添加到队列 + _sentenceQueue.add(_pendingText); + _pendingText = ''; + } + // 否则继续累积文本,等待更多输入 + return; + } + + // 找到了句子分隔符,处理完整的句子 + int lastPos = 0; + + for (final match in matches) { + final end = match.end; + // 提取完整的句子(包含分隔符) + final sentence = _pendingText.substring(lastPos, end).trim(); + + if (sentence.isNotEmpty) { + // 将完整的句子添加到队列 + _sentenceQueue.add(sentence); + } + + lastPos = end; + } + + // 处理剩余的不完整文本(不包含分隔符的部分) + if (lastPos < _pendingText.length) { + final remaining = _pendingText.substring(lastPos).trim(); + + // 如果剩余文本长度超过最佳长度,也将其添加到队列 + if (remaining.length > _optimalSegmentLength) { + _sentenceQueue.add(remaining); + _pendingText = ''; + } else { + // 否则保留在_pendingText中,等待更多输入 + _pendingText = remaining; + } + } else { + // 所有文本都已处理,清空_pendingText + _pendingText = ''; + } + } + + // 按长度分割文本的方法(保留以便其他地方调用) + void _splitByLength(String text) { + if (text.isEmpty) return; + + // 如果文本长度不超过最佳长度,直接添加整个文本 + if (text.length <= _optimalSegmentLength) { + _sentenceQueue.add(text); + return; + } + + int start = 0; + while (start < text.length) { + int end = math.min(start + _optimalSegmentLength, text.length); + + // 如果剩余文本长度小于最小字符数,直接取到末尾 + if (text.length - start <= _minSegmentLength) { + end = text.length; + } + // 否则尝试在标点或空格处分割 + else if (end < text.length) { + final spacePos = text.lastIndexOf(' ', end); + final breakPos = _sentenceBreaks.firstMatch( + text.substring(start, end) + )?.end; + + if (breakPos != null && breakPos > start) { + end = start + breakPos; + } else if (spacePos > start) { + end = spacePos; + } + } + + final segment = text.substring(start, end).trim(); + if (segment.isNotEmpty) { + _sentenceQueue.add(segment); + } + start = end; + } + } + + Future _startPreloadIfNeeded() async { + if (_isFetching || _sentenceQueue.isEmpty) return; + _isFetching = true; + await _fetchNextSegment(); + } + + Future _fetchNextSegment() async { + if (_sentenceQueue.isEmpty) { + _isFetching = false; + return; + } + + try { + // 预加载多个片段以实现流畅播放 + final segments = []; + while (_sentenceQueue.isNotEmpty && segments.length < 2) { // 减少预加载数量以降低内存压力 + segments.add(_sentenceQueue.removeAt(0)); + } + + for (final segment in segments) { + final audioData = await _synthesize(segment); + + if (audioData != null) { + try { + // 添加到播放列表 + await _addToPlaylist(audioData); + } catch (e) { + Logger.error('添加音频源失败: $e'); + } + } + } + + // 如果队列还有内容,继续获取下一批 + if (_sentenceQueue.isNotEmpty) { + await Future.delayed(const Duration(milliseconds: 100)); // 添加延迟以避免过快请求 + await _fetchNextSegment(); + } else { + _isFetching = false; + // 检查是否还有未处理的文本 + _extractFullSentences(); + if (_sentenceQueue.isNotEmpty) { + await _startPreloadIfNeeded(); + } + } + } catch (e, stackTrace) { + Logger.error('获取音频失败: $e'); + Logger.error('Stack trace: $stackTrace'); + _isFetching = false; + } + } + + Future _synthesize(String text) async { + try { + if (_selectedVoice == null) { + // 如果未选择语音,使用默认语音 + Logger.info('未选择语音,使用默认语音'); + await setVoice('zh-CN-XiaoxiaoNeural'); // 使用默认语音 + + // 如果设置后仍然为null,则无法继续 + if (_selectedVoice == null) { + throw AzureTtsException('无法设置默认语音'); + } + } + + // 创建TTS参数 + final params = TtsParams( + voice: _selectedVoice!, + audioFormat: _audioFormat, + text: text, + rate: 1.0, // 语速,可以根据需要调整 + ); + + // 请求语音合成 + final ttsResponse = await FlutterAzureTts.getTts(params); + + // 获取音频数据 + final audioBytes = ttsResponse.audio.buffer.asUint8List(); + return audioBytes; + } catch (e) { + Logger.error('Azure语音合成请求失败: $e'); + return null; + } + } + + /// 将音频数据添加到播放列表 + Future _addToPlaylist(Uint8List audioData) async { + try { + if (audioData.isEmpty) return; + + // 确保播放列表已初始化 + if (!_playlistInitialized) { + await _initAudioPlayer(); + } + + // 创建音频源 + final audioSource = BytesAudioSource(audioData); + + // 添加到播放列表 + await _playlist.add(audioSource); + + // 如果当前没有播放,开始播放 + if (!isPlaying.value) { + await _startPlayback(); + } else if (_audioPlayer.processingState == ProcessingState.completed) { + // 如果播放已完成但状态还没更新,手动前进到下一个 + await _advanceToNextItem(); + } + } catch (e) { + Logger.error('添加音频到播放列表失败: $e'); + // 通知错误 + _notifyEvent('playbackError', '添加音频到播放列表失败: $e'); + } + } + + /// 开始播放收集到的音频数据 + Future _startPlayback() async { + try { + if (!_playlistInitialized || _playlist.length == 0) { + return; + } + + // 检查当前播放状态 + final processingState = _audioPlayer.processingState; + + // 如果已经播放完成,需要重新设置位置到第一个未播放的项目 + if (processingState == ProcessingState.completed) { + // 找到第一个未播放的项目 + int nextIndex = 0; + if (_audioPlayer.currentIndex != null) { + nextIndex = math.min(_audioPlayer.currentIndex! + 1, _playlist.length - 1); + } + + // 跳转到下一个项目 + await _audioPlayer.seek(Duration.zero, index: nextIndex); + } + + // 开始播放 + await _audioPlayer.play(); + + // 手动更新播放状态,避免等待事件流更新可能导致的延迟 + if (_playlist.length > 0 && !isPlaying.value) { + isPlaying.value = true; + } + } catch (e) { + Logger.error('开始播放失败: $e'); + // 通知错误 + _notifyEvent('playbackError', '开始播放失败: $e'); + } + } + + /// 前进到播放列表中的下一个项目 + Future _advanceToNextItem() async { + try { + if (_playlist.length == 0) { + // 如果播放列表为空,确保 isPlaying 为 false + if (isPlaying.value) { + isPlaying.value = false; + } + return; + } + + // 获取当前索引和序列状态 + final currentIndex = _audioPlayer.currentIndex; + final sequenceState = _audioPlayer.sequenceState; + + if (sequenceState == null) { + // 如果没有序列状态,直接开始播放 + await _audioPlayer.seek(Duration.zero, index: 0); + await _audioPlayer.play(); + + // 更新播放状态 + if (!isPlaying.value) { + isPlaying.value = true; + } + return; + } + + // 如果当前索引无效,从头开始播放 + if (currentIndex == null || currentIndex < 0) { + await _audioPlayer.seek(Duration.zero, index: 0); + await _audioPlayer.play(); + + // 更新播放状态 + if (!isPlaying.value) { + isPlaying.value = true; + } + return; + } + + // 计算下一个索引 + final nextIndex = currentIndex + 1; + + // 检查是否还有下一个项目 + if (nextIndex < sequenceState.sequence.length) { + // 跳转到下一个项目并开始播放 + await _audioPlayer.seek(Duration.zero, index: nextIndex); + await _audioPlayer.play(); + + // 更新播放状态 + if (!isPlaying.value) { + isPlaying.value = true; + } + } else { + // 已经是最后一个项目 + // 确保最后一个项目播放完成 + if (_audioPlayer.position < _audioPlayer.duration!) { + await _audioPlayer.seek(_audioPlayer.duration!); + } + + // 如果已经是最后一个项目且已播放完成,更新播放状态为 false + if (isPlaying.value) { + isPlaying.value = false; + } + } + } catch (e) { + Logger.error('前进到下一个音频项目失败: $e'); + } + } + + Future stop() async { + try { + // 立即更新播放状态,避免等待事件流更新可能导致的延迟 + if (isPlaying.value) { + isPlaying.value = false; + } + + await _audioPlayer.stop(); + await _playlist.clear(); + _pendingText = ''; + _sentenceQueue.clear(); + _isFetching = false; + + // 确保清空所有待处理的音频数据 + try { + // 重置播放器状态 + await _audioPlayer.pause(); + await _audioPlayer.seek(Duration.zero); + } catch (e) { + Logger.error('清空音频缓存时出错: $e'); + } + } catch (e) { + Logger.error('停止播放失败: $e'); + } + } + + // 添加一个方法来检查音频播放器是否真正在播放 + bool isActuallyPlaying() { + try { + // 检查播放器状态 + final isPlayerPlaying = _audioPlayer.playing; + + // 检查播放列表是否为空 + final hasAudioSource = _playlist.length > 0; + + // 只有当播放器正在播放且播放列表不为空时,才认为真正在播放 + final actuallyPlaying = isPlayerPlaying && hasAudioSource; + + // 如果状态不一致,记录日志 + if (isPlaying.value != actuallyPlaying) { + Logger.info('音频播放状态不一致: isPlaying.value=${isPlaying.value}, 实际播放状态=$actuallyPlaying (播放器状态=$isPlayerPlaying, 播放列表长度=${_playlist.length})'); + } + + return actuallyPlaying; + } catch (e) { + Logger.error('检查实际播放状态失败: $e'); + // 如果检查失败,返回当前的 isPlaying 值 + return isPlaying.value; + } + } + + Future _reinitializePlayer() async { + try { + await stop(); + await _audioPlayer.dispose(); + + // 重新初始化播放器 + _audioPlayer = AudioPlayer(); + await _audioPlayer.setVolume(1.0); + await _audioPlayer.setLoopMode(LoopMode.off); + + // 重新配置Android特定的播放参数 + if (Platform.isAndroid) { + await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes( + contentType: AndroidAudioContentType.speech, + usage: AndroidAudioUsage.media, + flags: AndroidAudioFlags.audibilityEnforced, + )); + } + + await _audioPlayer.setAudioSource( + _playlist, + initialPosition: Duration.zero, + preload: true, + ); + + _playlistInitialized = true; + _setupEventListeners(); + Logger.info('播放器重新初始化成功'); + } catch (e) { + Logger.error('播放器重新初始化失败: $e'); + } + } + + void _setupEventListeners() { + _playerStateSubscription?.cancel(); + + _playerStateSubscription = _audioPlayer.playerStateStream.listen( + (state) { + if (_isDisposed) return; + + // 更新播放状态 - 使用更准确的判断逻辑 + final isPlayerPlaying = state.playing; + final hasAudioSource = _playlist.length > 0; + final actuallyPlaying = isPlayerPlaying && hasAudioSource; + + // 只有当实际状态与当前状态不同时才更新 + if (isPlaying.value != actuallyPlaying) { + isPlaying.value = actuallyPlaying; + } + + // 如果播放完成,尝试播放下一个 + if (state.processingState == ProcessingState.completed) { + _advanceToNextItem(); + } + }, + onError: (error) { + if (_isDisposed) return; + Logger.error('播放器错误: $error'); + _reinitializePlayer(); + }, + ); + } + + void toggleEnabled() { + isEnabled.toggle(); + if (!isEnabled.value) { + stop(); + } + } + + Future _cleanupResources() async { + _isDisposed = true; + await _playerStateSubscription?.cancel(); + await stop(); + await _audioPlayer.dispose(); + + // 关闭事件控制器 + if (!_eventController.isClosed) { + await _eventController.close(); + } + } + + @override + void onClose() async { + Logger.info('关闭Azure TTS服务'); + await _cleanupResources(); + super.onClose(); + } + + /// 使用系统默认音频设备 + Future useDefaultAudioDevice() async { + return await _configureAudioDevice(); + } + + /// 更新语音 + void updateVoice(Voice voice) { + _selectedVoice = voice; + Logger.info('已更新Azure语音: ${voice.shortName}'); + } + + /// 根据语音名称设置语音 + Future setVoice(String voiceName) async { + Logger.info('正在设置Azure语音: $voiceName'); + + try { + // 获取可用的语音列表 + final voicesResponse = await FlutterAzureTts.getAvailableVoices(); + + if (voicesResponse.voices.isEmpty) { + Logger.error('未找到可用的Azure语音'); + return; + } + + // 1. 首先尝试精确匹配 + Voice? matchedVoice; + for (final voice in voicesResponse.voices) { + if (voice.shortName == voiceName) { + matchedVoice = voice; + break; + } + } + + // 2. 如果没有精确匹配,尝试部分匹配 + if (matchedVoice == null) { + for (final voice in voicesResponse.voices) { + if (voice.shortName.contains(voiceName) || voiceName.contains(voice.shortName)) { + matchedVoice = voice; + break; + } + } + } + + // 3. 如果仍然没有匹配,尝试匹配语言区域 + if (matchedVoice == null) { + final localePrefix = voiceName.split('-').take(2).join('-'); + for (final voice in voicesResponse.voices) { + if (voice.locale.startsWith(localePrefix)) { + matchedVoice = voice; + break; + } + } + } + + // 4. 如果所有尝试都失败,使用第一个可用的语音 + if (matchedVoice == null && voicesResponse.voices.isNotEmpty) { + matchedVoice = voicesResponse.voices.first; + } + + // 设置选中的语音 + if (matchedVoice != null) { + _selectedVoice = matchedVoice; + Logger.info('已设置Azure语音: ${matchedVoice.shortName}'); + } else { + Logger.error('未能找到合适的语音'); + } + } catch (e) { + Logger.error('设置Azure语音失败: $e'); + // 如果获取语音列表失败,创建一个简化的Voice对象 + _selectedVoice = Voice( + shortName: voiceName, + displayName: voiceName, + locale: _voiceLocale, + name: voiceName, + localName: voiceName, + gender: 'Female', + voiceType: 'Neural', + status: 'GA', + sampleRateHertz: '24000', + styles: [], + roles: [], + ); + Logger.info('使用简化的Voice对象: $voiceName'); + } + } + + /// 通知事件 + void _notifyEvent(String eventType, String message) { + if (!_eventController.isClosed) { + _eventController.add({ + 'eventType': eventType, + 'message': message, + }); + } + } +} \ No newline at end of file diff --git a/lib/data/services/azure_tts_service.dart b/lib/data/services/azure_tts_service.dart new file mode 100644 index 000000000..ebb98c61c --- /dev/null +++ b/lib/data/services/azure_tts_service.dart @@ -0,0 +1,403 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../../core/utils/logger.dart'; + +/// 微软 Text-to-Speech 服务异常 +class AzureTtsException implements Exception { + final String message; + AzureTtsException(this.message); + + @override + String toString() => message; +} + +/// 微软 Text-to-Speech 服务 +/// +/// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互, +/// 提供文本转语音功能。 +class AzureTtsService extends GetxService { + static const MethodChannel _channel = MethodChannel('com.deep_voice.azure_tts'); + + bool _isInitialized = false; + late final String _subscriptionKey; + late final String _serviceRegion; + + // 当前设置 + String _currentVoice = 'zh-CN-XiaoxiaoNeural'; + int _currentRate = 0; + int _currentPitch = 0; + int _currentVolume = 100; + + // 语音合成队列 + final List<_SpeechItem> _textQueue = []; + bool _isProcessingQueue = false; + bool _isSpeaking = false; + + // 流式输出缓冲区 + String _streamBuffer = ''; + + // 可观察状态 + final isEnabled = true.obs; + final isSpeaking = false.obs; + + String get currentVoice => _currentVoice; + + AzureTtsService() { + _loadConfig(); + } + + /// 从环境变量加载配置 + void _loadConfig() { + _subscriptionKey = dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? ''; + _serviceRegion = dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? ''; + } + + /// 初始化TTS服务 + Future initialize({String language = 'zh-CN'}) async { + if (_isInitialized) return true; + + try { + if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { + Logger.error('未找到Azure语音服务配置'); + return false; + } + + final result = await _channel.invokeMethod('initialize', { + 'subscriptionKey': _subscriptionKey, + 'serviceRegion': _serviceRegion, + 'language': language, + }); + + _isInitialized = result; + return result; + } catch (e) { + Logger.error('初始化失败: $e'); + return false; + } + } + + /// 设置语音 + Future setVoice(String voiceName) async { + if (!_isInitialized) await initialize(); + + try { + final result = await _channel.invokeMethod('setVoice', { + 'voiceName': voiceName, + }); + + if (result) _currentVoice = voiceName; + return result; + } catch (e) { + Logger.error('设置语音失败: $e'); + return false; + } + } + + /// 设置语音参数 + Future setSpeechParams({int rate = 0, int pitch = 0, int volume = 100}) async { + if (!_isInitialized) await initialize(); + + try { + final result = await _channel.invokeMethod('setSpeechParams', { + 'rate': rate, + 'pitch': pitch, + 'volume': volume, + }); + + if (result) { + _currentRate = rate; + _currentPitch = pitch; + _currentVolume = volume; + } + + return result; + } catch (e) { + Logger.error('设置语音参数失败: $e'); + return false; + } + } + + /// 播放文本 + Future speakText(String text) async { + if (!_isInitialized) await initialize(); + if (!isEnabled.value) return "TTS服务已禁用"; + + try { + _isSpeaking = true; + isSpeaking.value = true; + + final result = await _channel.invokeMethod('speakText', {'text': text}); + + _isSpeaking = false; + isSpeaking.value = false; + + return result; + } catch (e) { + _isSpeaking = false; + isSpeaking.value = false; + Logger.error('语音合成失败: $e'); + return "语音合成失败: $e"; + } + } + + /// 播放SSML + Future speakSsml(String ssml) async { + if (!_isInitialized) await initialize(); + if (!isEnabled.value) return "TTS服务已禁用"; + + try { + _isSpeaking = true; + isSpeaking.value = true; + + final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml}); + + _isSpeaking = false; + isSpeaking.value = false; + + return result; + } catch (e) { + _isSpeaking = false; + isSpeaking.value = false; + Logger.error('SSML语音合成失败: $e'); + return "SSML语音合成失败: $e"; + } + } + + /// 添加文本到队列并播放 + Future speak(String text, {int? rate, int? pitch, int? volume}) async { + if (!isEnabled.value || text.isEmpty) return false; + + _textQueue.add(_SpeechItem( + text: text, + rate: rate ?? _currentRate, + pitch: pitch ?? _currentPitch, + volume: volume ?? _currentVolume, + )); + + if (!_isProcessingQueue) _processQueue(); + return true; + } + + /// 处理AI流式输出文本 + /// + /// 将流式输出的文本片段添加到缓冲区,并在检测到完整句子时将其送入播放队列 + Future processStreamText(String textChunk) async { + if (!isEnabled.value || textChunk.isEmpty) return false; + + // 添加文本到缓冲区 + _streamBuffer += textChunk; + + // 如果缓冲区为空,直接返回 + if (_streamBuffer.isEmpty) return true; + + // 使用正则表达式匹配句子,包括结束符号 + // 匹配任意字符,直到遇到句子结束符号 + final sentenceRegex = RegExp( + r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])' + ); + + bool hasProcessed = false; + + // 查找所有完整句子 + final matches = sentenceRegex.allMatches(_streamBuffer); + final List sentences = []; + int lastMatchEnd = 0; + + for (final match in matches) { + // 提取完整句子(包含结束符号) + final sentence = match.group(1)?.trim(); + if (sentence != null && sentence.isNotEmpty) { + sentences.add(sentence); + lastMatchEnd = match.end; + } + } + + // 处理找到的句子 + for (final sentence in sentences) { + _textQueue.add(_SpeechItem( + text: sentence, + rate: _currentRate, + pitch: _currentPitch, + volume: _currentVolume, + )); + hasProcessed = true; + } + + // 更新缓冲区,只保留未完成的部分 + if (lastMatchEnd > 0) { + _streamBuffer = _streamBuffer.substring(lastMatchEnd); + } + + // 如果处理了文本并且队列未在处理中,开始处理队列 + if (hasProcessed && !_isProcessingQueue) { + _processQueue(); + } + + return true; + } + + /// 强制处理缓冲区中的剩余文本 + /// + /// 在流式输出结束时调用,确保缓冲区中的所有文本都被处理 + Future flushStreamBuffer() async { + if (!isEnabled.value || _streamBuffer.isEmpty) return false; + + // 先尝试使用processStreamText处理可能的完整句子 + await processStreamText(''); + + // 如果缓冲区仍有剩余文本,将其添加到播放队列 + if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { + _textQueue.add(_SpeechItem( + text: _streamBuffer, + rate: _currentRate, + pitch: _currentPitch, + volume: _currentVolume, + )); + + // 清空缓冲区 + _streamBuffer = ''; + + // 如果队列未在处理中,开始处理队列 + if (!_isProcessingQueue) { + _processQueue(); + } + } + + return true; + } + + /// 连续播放多段文本 + Future speakMultiple(List texts, {int? rate, int? pitch, int? volume}) async { + if (!isEnabled.value || texts.isEmpty) return false; + + for (final text in texts) { + if (text.isNotEmpty) { + _textQueue.add(_SpeechItem( + text: text, + rate: rate ?? _currentRate, + pitch: pitch ?? _currentPitch, + volume: volume ?? _currentVolume, + )); + } + } + + if (!_isProcessingQueue) _processQueue(); + return true; + } + + /// 处理语音合成队列 + Future _processQueue() async { + if (_textQueue.isEmpty || _isProcessingQueue) return; + + _isProcessingQueue = true; + + try { + while (_textQueue.isNotEmpty) { + if (!isEnabled.value) { + _textQueue.clear(); + break; + } + + final item = _textQueue.removeAt(0); + + if (item.rate != _currentRate || item.pitch != _currentPitch || item.volume != _currentVolume) { + await setSpeechParams( + rate: item.rate, + pitch: item.pitch, + volume: item.volume, + ); + } + + await speakText(item.text); + } + } catch (e) { + Logger.error('处理语音队列出错: $e'); + } finally { + _isProcessingQueue = false; + } + } + + /// 停止当前语音合成并清空队列 + Future stop() async { + _textQueue.clear(); + _streamBuffer = ''; // 清空流缓冲区 + + if (_isSpeaking) { + try { + final result = await _channel.invokeMethod('stopSpeaking'); + _isSpeaking = false; + isSpeaking.value = false; + return result; + } catch (e) { + Logger.error('停止语音合成出错: $e'); + return false; + } + } + + return true; + } + + /// 生成SSML文本 + String generateSsml({ + required String text, + String? voiceName, + int? rate, + int? pitch, + int? volume, + }) { + final voice = voiceName ?? _currentVoice; + final rateValue = (rate ?? _currentRate).clamp(-100, 100); + final pitchValue = (pitch ?? _currentPitch).clamp(-100, 100); + final volumeValue = (volume ?? _currentVolume).clamp(0, 100); + + final rateStr = rateValue == 0 ? '0%' : rateValue < 0 ? '${(rateValue * 0.9).round()}%' : '$rateValue%'; + final pitchStr = pitchValue == 0 ? '0%' : '${(pitchValue * 0.5).round()}%'; + final volumeStr = "$volumeValue%"; + + return ''' + + + + $text + + + + '''; + } + + /// 切换启用状态 + void toggleEnabled() { + isEnabled.toggle(); + if (!isEnabled.value) stop(); + } + + /// 释放资源 + Future dispose() async { + if (!_isInitialized) return; + + try { + await stop(); + await _channel.invokeMethod('dispose'); + _isInitialized = false; + } catch (e) { + Logger.error('释放资源失败: $e'); + } + } +} + +/// 语音合成项目 +class _SpeechItem { + final String text; + final int rate; + final int pitch; + final int volume; + + _SpeechItem({ + required this.text, + this.rate = 0, + this.pitch = 0, + this.volume = 100, + }); +} \ No newline at end of file diff --git a/lib/data/services/background_agent_service.dart b/lib/data/services/background_agent_service.dart index d8c54ce45..f694791c6 100644 --- a/lib/data/services/background_agent_service.dart +++ b/lib/data/services/background_agent_service.dart @@ -8,7 +8,7 @@ import '../../modules/chat/controllers/chat_controller.dart'; import '../../modules/chat/models/message_model.dart'; import 'volcano_asr_api_service.dart'; import 'volcano_ai_service.dart'; -import 'volcano_tts_api_service.dart'; +import 'azure_flutter_tts_service.dart'; import 'chat_history_service.dart'; /// 后台代理服务,用于在应用处于后台时处理AI交互 @@ -30,14 +30,12 @@ class BackgroundAgentService extends GetxService { // 服务实例 final VolcanoAsrApiService _asrService = Get.find(); final VolcanoAIService _aiService = Get.find(); - final VolcanoTtsApiService _ttsService = Get.find(); + final AzureFlutterTtsService _ttsService = Get.find(); final ChatHistoryService _historyService = Get.find(); // 状态控制 + // isProcessing: 表示整个代理交互会话是否处于活跃状态(包括语音识别、AI处理和TTS播放) final RxBool isProcessing = false.obs; - final RxBool isListening = false.obs; - - // 语音识别状态 final RxString recognizedText = ''.obs; // 流式响应控制 @@ -50,12 +48,13 @@ class BackgroundAgentService extends GetxService { // 当前处理的Agent Agent? _currentAgent; - // 消息历史 - 与ChatController共享 + // 本地消息历史备份 + final List> _localMessageHistory = []; + + // 获取消息历史 - 优先从ChatController获取,否则使用本地存储 List> get messageHistory { - // 首先尝试从ChatController获取消息历史 if (Get.isRegistered()) { final chatController = Get.find(); - // 检查ChatController是否正在与personal_assistant交互 if (chatController.agent?.id == 'personal_assistant') { return chatController.messages.map((m) => { 'role': m.role, @@ -64,7 +63,6 @@ class BackgroundAgentService extends GetxService { } } - // 如果无法从ChatController获取,则使用本地存储的历史记录 if (_localMessageHistory.isEmpty) { _loadChatHistory(); } @@ -72,71 +70,63 @@ class BackgroundAgentService extends GetxService { return _localMessageHistory; } - // 本地消息历史备份 - final List> _localMessageHistory = []; + // 语音识别是否活跃 + bool get isListening => _recognitionSubscription != null; - // TTS是否正在播放,直接从VolcanoTtsApiService获取 - bool get isTtsSpeaking => _ttsService.isPlaying; + // TTS是否正在播放 + bool get isTtsSpeaking => _ttsService.isActuallyPlaying(); @override void onClose() { - // 取消所有订阅 + _cleanupResources(); + super.onClose(); + } + + /// 清理所有资源 + void _cleanupResources() { _recognitionSubscription?.cancel(); _aiResponseSubscription?.cancel(); - // 停止TTS并结束会话 - if (_ttsService.isPlaying) { + if (_ttsService.isActuallyPlaying()) { _ttsService.stop(); - _ttsService.endSession(); } - // 停止语音识别 - if (isListening.value) { + if (isListening) { _asrService.stopRecognition(); } - // 取消定时器 _inactivityTimer?.cancel(); - - super.onClose(); } /// 开始代理交互 - /// - /// [agentId] 要交互的代理ID Future startAgentInteraction(String agentId) async { - // 先说"我在听",然后再播放欢迎消息 - const listeningMessage = "我在听"; - _ttsService.speakOnce(listeningMessage); - // 防止重复处理 if (isProcessing.value) { Logger.warning('已经在处理交互,忽略此次请求'); return; } + // 先说"我在听" + _ttsService.speak("我在!"); isProcessing.value = true; try { // 获取Agent - final agent = AgentProvider.getAgentById(agentId); - if (agent == null) { + _currentAgent = AgentProvider.getAgentById(agentId); + if (_currentAgent == null) { Logger.error('无法获取Agent: $agentId'); isProcessing.value = false; return; } - _currentAgent = agent; - // 加载该Agent的聊天历史 _loadChatHistory(); - // 启动语音识别 - 保持持续识别状态 + // 启动语音识别 await _startVoiceRecognition(); // 启动无活动定时器 _startInactivityTimer(); - } catch (e) { Logger.error('启动代理交互失败', e); isProcessing.value = false; @@ -145,80 +135,57 @@ class BackgroundAgentService extends GetxService { /// 停止语音识别 Future _stopVoiceRecognition() async { - if (isListening.value) { - isListening.value = false; - - // 取消订阅 - await _recognitionSubscription?.cancel(); - _recognitionSubscription = null; - - // 停止语音识别 - _asrService.stopRecognition(); - - // 重置状态 - recognizedText.value = ''; - } + if (!isListening) return; + + await _recognitionSubscription?.cancel(); + _recognitionSubscription = null; + _asrService.stopRecognition(); + recognizedText.value = ''; } /// 开始语音识别 Future _startVoiceRecognition() async { - if (isListening.value) { - return; - } + if (isListening) return; try { - // 启动语音识别 final success = await _asrService.startRecognition(); if (success) { - isListening.value = true; - - // 监听识别结果 _recognitionSubscription = _asrService.recognitionStream.listen( (event) { switch (event.type) { case RecognitionEventType.recognizing: - // 实时更新识别文本 - if (event.text.isNotEmpty) { - recognizedText.value = event.text; - Logger.info('处理语音识别中间结果: $event.text'); - // 只有当TTS正在播放或AI响应流正在接收时才中断 - if (_ttsService.isPlaying || _aiResponseSubscription != null) { - _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); - } + if (event.text.isEmpty) return; + + recognizedText.value = event.text; + Logger.info('处理语音识别中间结果:${event.text}'); + + // 检测到用户说话时中断当前响应 + if (_ttsService.isActuallyPlaying() || _aiResponseSubscription != null) { + _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); } break; + case RecognitionEventType.finalResult: - // 最终结果 - if (event.text.isNotEmpty) { - recognizedText.value = event.text; - Logger.info('处理语音识别最终结果: $event.text'); - // 处理识别到的文本 - _processRecognizedText(event.text); - - // 重置识别文本 - recognizedText.value = ''; - } + if (event.text.isEmpty) return; + + recognizedText.value = event.text; + Logger.info('处理语音识别最终结果:${event.text}'); + _processRecognizedText(event.text); + recognizedText.value = ''; break; + case RecognitionEventType.error: - // 错误处理 Logger.info('语音识别错误: ${event.error}'); - - // 不再尝试重新启动语音识别,而是直接停止交互 - Logger.info('语音识别出错,停止后台代理交互'); stopInteraction(); break; - case RecognitionEventType.started: + default: - // 不做任何处理 break; } }, onError: (error) { Logger.info('语音识别错误:$error'); - - // 不再尝试重新启动语音识别,而是直接停止交互 - Logger.info('语音识别流错误,停止后台代理交互'); stopInteraction(); }, ); @@ -227,7 +194,6 @@ class BackgroundAgentService extends GetxService { } } catch (e) { Logger.error('启动语音识别失败', e); - isListening.value = false; isProcessing.value = false; } } @@ -236,18 +202,13 @@ class BackgroundAgentService extends GetxService { void _interruptCurrentResponse(String reason) { Logger.info(reason); - // 停止TTS并结束会话 - if (_ttsService.isPlaying) { + if (_ttsService.isActuallyPlaying()) { _ttsService.stop(); - _ttsService.endSession(); } - // 取消AI响应流订阅 if (_aiResponseSubscription != null) { _aiResponseSubscription!.cancel(); _aiResponseSubscription = null; - - // 记录中断事件 Logger.info('已中断AI响应流和TTS播放'); } } @@ -257,18 +218,14 @@ class BackgroundAgentService extends GetxService { if (text.trim().isEmpty || _currentAgent == null) return; try { - // 在处理新的文本前,确保中断任何正在进行的响应 + // 中断任何正在进行的响应 _interruptCurrentResponse("处理新的用户输入,中断当前响应"); // 添加用户消息到历史记录 _addMessageToHistory('user', text); - // 重置取消标志 String fullResponse = ''; - // 启动TTS会话,使用当前Agent的voice - await _ttsService.startSession(_currentAgent!.voice); - // 获取AI响应流 final responseStream = _aiService.sendMessageStream( messages: messageHistory, @@ -279,73 +236,33 @@ class BackgroundAgentService extends GetxService { _aiResponseSubscription = responseStream.listen( (chunk) { fullResponse += chunk; - - // 发送到TTS _ttsService.speak(chunk); - - // 重置无活动定时器 _resetInactivityTimer(); }, onDone: () { - // 添加AI响应到历史记录 _addMessageToHistory('assistant', fullResponse); - - // 结束TTS会话 - _ttsService.endSession(); - - // 清除AI响应流订阅引用 _aiResponseSubscription = null; - - // 注意:我们不再在这里重启语音识别,因为它应该已经在运行 }, onError: (error) { Logger.error('AI响应错误', error); - - // 结束TTS会话 - _ttsService.endSession(); - - // 播放错误消息 const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。'; - _ttsService.speakOnce(errorMessage); - - // 添加错误消息到历史记录 + _ttsService.speak(errorMessage); _addMessageToHistory('assistant', errorMessage); - - // 清除AI响应流订阅引用 _aiResponseSubscription = null; }, ); } catch (e) { Logger.error('处理识别文本失败', e); - - // 结束TTS会话 - _ttsService.endSession(); - - // 播放错误消息 const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。'; - _ttsService.speakOnce(errorMessage); - - // 添加错误消息到历史记录 + _ttsService.speak(errorMessage); _addMessageToHistory('assistant', errorMessage); } } - /// 重新启动语音识别(仅在识别会话意外终止时使用) + /// 重新启动语音识别 Future _restartVoiceRecognition() async { - // 停止当前的语音识别 - if (isListening.value) { - await _asrService.stopRecognition(); - isListening.value = false; - - // 取消当前的订阅 - _recognitionSubscription?.cancel(); - _recognitionSubscription = null; - } - - // 等待一段时间,确保资源被正确释放 + await _stopVoiceRecognition(); await Future.delayed(Duration(milliseconds: 500)); - - // 重新启动语音识别 await _startVoiceRecognition(); } @@ -362,20 +279,17 @@ class BackgroundAgentService extends GetxService { _localMessageHistory.removeAt(0); } - // 如果ChatController可用,同步消息到ChatController + // 同步消息到ChatController或保存到本地存储 if (Get.isRegistered()) { final chatController = Get.find(); if (chatController.agent?.id == 'personal_assistant') { - // 向ChatController添加消息 - final message = Message( + chatController.addMessage(Message( role: role, content: content, timestamp: DateTime.now(), - ); - chatController.addMessage(message); + )); } } else { - // 如果ChatController不可用,保存到本地存储 _saveChatHistory(); } } @@ -385,20 +299,14 @@ class BackgroundAgentService extends GetxService { if (_currentAgent == null) return; try { - // 加载聊天历史 final messages = _historyService.loadHistory(_currentAgent!.id); - - // 清空本地历史,然后添加加载的历史 _localMessageHistory.clear(); - // 将Message转换为Map格式 - if (messages.isNotEmpty) { - for (final message in messages) { - _localMessageHistory.add({ - 'role': message.role, - 'content': message.content, - }); - } + for (final message in messages) { + _localMessageHistory.add({ + 'role': message.role, + 'content': message.content, + }); } } catch (e) { Logger.error('加载聊天历史记录失败', e); @@ -410,14 +318,12 @@ class BackgroundAgentService extends GetxService { if (_currentAgent == null) return; try { - // 将本地历史转换为Message列表 final messages = _localMessageHistory.map((m) => Message( role: m['role']!, content: m['content']!, timestamp: DateTime.now(), )).toList(); - // 保存到ChatHistoryService _historyService.saveHistory(_currentAgent!.id, messages); } catch (e) { Logger.error('保存聊天历史记录失败', e); @@ -428,8 +334,7 @@ class BackgroundAgentService extends GetxService { void _startInactivityTimer() { _inactivityTimer?.cancel(); _inactivityTimer = Timer(Duration(seconds: 30), () { - // 如果30秒内没有TTS输出或用户说话,停止语音识别 - if (!_ttsService.isPlaying && isProcessing.value) { + if (!_ttsService.isActuallyPlaying() && isProcessing.value) { Logger.info('检测到30秒内无活动,停止语音识别'); stopInteraction(); } @@ -446,47 +351,21 @@ class BackgroundAgentService extends GetxService { /// 响应新的媒体事件(双击媒体按钮) Future respondToNewMediaEvent() async { - // 不管当前状态如何,总是中断当前响应 - _interruptCurrentResponse("检测到新的媒体事件,中断当前响应"); - - // 回答"我在听" - const listeningMessage = "我在听"; - await _ttsService.speakOnce(listeningMessage); + _ttsService.speak("我在!"); - // 如果识别会话已经终止,确保重新启动 - if (!isListening.value) { + if (!isListening) { await _restartVoiceRecognition(); } - // 重置无活动定时器 _resetInactivityTimer(); } /// 停止当前交互 Future stopInteraction() async { try { - // 保存聊天历史 _saveChatHistory(); + _cleanupResources(); - // 取消所有订阅 - _recognitionSubscription?.cancel(); - _aiResponseSubscription?.cancel(); - - // 停止TTS并结束会话 - if (_ttsService.isPlaying) { - _ttsService.stop(); - _ttsService.endSession(); - } - - // 停止语音识别 - if (isListening.value) { - await _stopVoiceRecognition(); - } - - // 取消定时器 - _inactivityTimer?.cancel(); - - // 重置状态 isProcessing.value = false; _currentAgent = null; diff --git a/lib/data/services/my_audio_handler.dart b/lib/data/services/my_audio_handler.dart index e4eb11ddc..67eac5301 100644 --- a/lib/data/services/my_audio_handler.dart +++ b/lib/data/services/my_audio_handler.dart @@ -9,11 +9,7 @@ import '../../core/utils/logger.dart'; /// 自定义 AudioHandler,用于捕获媒体按键事件(例如蓝牙耳机按钮) class MyAudioHandler extends BaseAudioHandler { - // 定义一个 StreamController 用于向 UI 发送自定义事件 - final StreamController _customEventController = - StreamController.broadcast(); bool _isDisposed = false; - BackgroundAgentService? _backgroundAgent; // 添加一个标志,表示是否正在处理交互 bool _isHandlingInteraction = false; @@ -24,27 +20,23 @@ class MyAudioHandler extends BaseAudioHandler { // 添加一个变量,记录最后一次媒体按钮事件的时间 DateTime? _lastMediaButtonEventTime; - Stream get customEventStream => _customEventController.stream; - MyAudioHandler() { _initializeHandler(); - _initializeBackgroundAgent(); _startStatusUpdateTimer(); Logger.info('MyAudioHandler 已创建,准备接收媒体按钮事件'); } - void _initializeBackgroundAgent() { + // 获取BackgroundAgentService实例 + BackgroundAgentService? _getBackgroundAgent() { try { if (Get.isRegistered()) { - _backgroundAgent = Get.find(); - Logger.info('BackgroundAgent 初始化成功'); + return Get.find(); } else { - _backgroundAgent = Get.put(BackgroundAgentService(), permanent: true); - Logger.info('BackgroundAgent 已创建并初始化成功'); + return Get.put(BackgroundAgentService(), permanent: true); } } catch (e) { - Logger.error('初始化 BackgroundAgent 失败', e); - _backgroundAgent = null; + Logger.error('获取 BackgroundAgent 失败', e); + return null; } } @@ -131,16 +123,12 @@ class MyAudioHandler extends BaseAudioHandler { processingState: processingState, updatePosition: Duration.zero, )); - - } catch (e) { Logger.error('更新播放状态失败', e); } } Future _handleInteraction() async { - // Logger.info('处理媒体按钮交互'); - // 防止重复处理 if (_isHandlingInteraction) { Logger.info('已经在处理交互,忽略此次请求'); @@ -160,7 +148,6 @@ class MyAudioHandler extends BaseAudioHandler { final agent = AgentProvider.getAgentById('personal_assistant'); if (agent == null) { Logger.error('无法获取personal_assistant agent,无法激活语音助手'); - _isHandlingInteraction = false; return; } @@ -168,13 +155,15 @@ class MyAudioHandler extends BaseAudioHandler { final isInForeground = Get.context != null && WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed; + final backgroundAgent = _getBackgroundAgent(); + if (isInForeground) { // 应用在前台,导航到聊天页面 Logger.info('应用在前台,导航到聊天页面'); // 如果BackgroundAgent正在运行,先停止它 - if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) { - await _backgroundAgent!.stopInteraction(); + if (backgroundAgent != null && backgroundAgent.isProcessing.value) { + await backgroundAgent.stopInteraction(); Logger.info('已停止后台代理服务,切换到前台模式'); } @@ -189,25 +178,18 @@ class MyAudioHandler extends BaseAudioHandler { 'welcomeMessage': agent.welcomeMessage, 'playVoiceOnEnter': true, }); - } else { + } else if (backgroundAgent != null) { // 应用在后台,使用 BackgroundAgent 处理交互 - Logger.info('应用在后台,使用 BackgroundAgent 处理交互'); - if (_backgroundAgent != null) { - - if (_backgroundAgent!.isProcessing.value) { - // 如果后台服务已在处理,则中断当前响应并回复"我在听" - Logger.info('检测到媒体按钮再次按下,中断当前AI回复'); - await _backgroundAgent!.respondToNewMediaEvent(); - } else { - // 首次启动后台服务 - Logger.info('首次启动后台服务'); - await _backgroundAgent!.startAgentInteraction(agent.id); - } + if (backgroundAgent.isProcessing.value) { + Logger.info('检测到媒体按钮再次按下,中断当前AI回复'); + await backgroundAgent.respondToNewMediaEvent(); } else { - Logger.error('BackgroundAgent 未初始化,无法处理交互'); - // 尝试重新初始化 BackgroundAgent - _initializeBackgroundAgent(); + // 首次启动后台服务 + Logger.info('首次启动后台服务'); + await backgroundAgent.startAgentInteraction(agent.id); } + } else { + Logger.error('无法获取 BackgroundAgent,无法处理交互'); } } catch (e) { Logger.error('处理媒体按钮交互失败', e); @@ -252,12 +234,12 @@ class MyAudioHandler extends BaseAudioHandler { _statusUpdateTimer?.cancel(); // 如果有正在运行的后台服务,停止它 - if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) { - await _backgroundAgent!.stopInteraction(); + final backgroundAgent = _getBackgroundAgent(); + if (backgroundAgent != null && backgroundAgent.isProcessing.value) { + await backgroundAgent.stopInteraction(); } await super.stop(); - _customEventController.close(); _updatePlaybackState( playing: false, processingState: AudioProcessingState.idle, @@ -341,8 +323,9 @@ class MyAudioHandler extends BaseAudioHandler { Logger.info('服务被系统移除'); try { // 如果有正在运行的后台服务,停止它 - if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) { - await _backgroundAgent!.stopInteraction(); + final backgroundAgent = _getBackgroundAgent(); + if (backgroundAgent != null && backgroundAgent.isProcessing.value) { + await backgroundAgent.stopInteraction(); } await stop(); @@ -356,8 +339,9 @@ class MyAudioHandler extends BaseAudioHandler { Logger.info('通知被用户移除'); try { // 如果有正在运行的后台服务,停止它 - if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) { - await _backgroundAgent!.stopInteraction(); + final backgroundAgent = _getBackgroundAgent(); + if (backgroundAgent != null && backgroundAgent.isProcessing.value) { + await backgroundAgent.stopInteraction(); } await stop(); diff --git a/lib/data/services/volcano_asr_api_service.dart b/lib/data/services/volcano_asr_api_service.dart index 4007667e7..b794dddec 100644 --- a/lib/data/services/volcano_asr_api_service.dart +++ b/lib/data/services/volcano_asr_api_service.dart @@ -264,7 +264,7 @@ class VolcanoAsrApiService extends GetxService { 'enable_words': true, // 启用词级别时间戳 'enable_itn': false, // 启用数字转换 'continuous_decoding': true, // 启用连续解码,支持连续监听 - 'vad_silence_end': 1000, // 设置静音结束阈值,单位为毫秒,用于判断一句话的结束 + 'vad_silence_end': 800, // 设置静音结束阈值,单位为毫秒,用于判断一句话的结束 'vad_max_duration': 60000, // 设置最大语音段时长,单位为毫秒 }, }; diff --git a/lib/data/services/volcano_tts_service.dart b/lib/data/services/volcano_tts_service.dart new file mode 100644 index 000000000..994fb0c4e --- /dev/null +++ b/lib/data/services/volcano_tts_service.dart @@ -0,0 +1,743 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:io'; +import 'dart:math' as math; +import 'package:get/get.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import 'package:audio_session/audio_session.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../../core/utils/logger.dart'; + +/// 火山语音服务异常 +class VolcanoTtsException implements Exception { + final String message; + VolcanoTtsException(this.message); + + @override + String toString() => message; +} + +/// 自定义音频源,用于从字节数组读取音频数据 +class BytesAudioSource extends StreamAudioSource { + final Uint8List _bytes; + static const int _bufferSize = 4096; // 4KB buffer size for better Android compatibility + + BytesAudioSource(this._bytes); + + @override + Future request([int? start, int? end]) async { + start = start ?? 0; + end = end ?? _bytes.length; + + try { + final subData = _bytes.sublist(start, end); + + // 使用固定大小的块进行流式传输 + final chunks = >[]; + var offset = 0; + while (offset < subData.length) { + final chunkSize = math.min(_bufferSize, subData.length - offset); + chunks.add(subData.sublist(offset, offset + chunkSize)); + offset += chunkSize; + } + + return StreamAudioResponse( + sourceLength: _bytes.length, + contentLength: subData.length, + offset: start, + stream: Stream.fromIterable(chunks), + contentType: 'audio/mpeg', + ); + } catch (e) { + print('BytesAudioSource request error: $e'); + rethrow; + } + } + + @override + Future get length => Future.value(_bytes.length); +} + +/// 火山语音合成服务 +class VolcanoTtsService extends GetxService { + static const String _host = 'openspeech.bytedance.com'; + static const String _apiUrl = 'wss://$_host/api/v1/tts/ws_binary'; + + // 配置参数 + final String _appId; + final String _token; + final String _cluster; + final String _voiceType; + + // 音频播放器 + late AudioPlayer _audioPlayer; + final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []); + final isEnabled = true.obs; // 默认启用 + final isPlaying = false.obs; + StreamSubscription? _playbackEventSubscription; + StreamSubscription? _playerStateSubscription; + AudioSession? _audioSession; + + // WebSocket 相关 + WebSocketChannel? _channel; + bool _isDisposed = false; + + // 句子管理 + String _pendingText = ''; + final List _sentenceQueue = []; + static final _sentenceBreaks = RegExp(r'[。!?.!?,,、]'); // 添加更多标点符号 + bool _isFetching = false; + static const int _maxSegmentLength = 100; // 最大分段长度 + static const int _optimalSegmentLength = 50; // 最佳分段长度 + + // WebSocket连接配置 + static final Map _wsHeaders = { + 'User-Agent': 'DeepVoice/1.0', + 'Accept': '*/*', + 'Accept-Encoding': 'gzip, deflate, br', + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Extensions': 'permessage-deflate', + 'Origin': 'https://openspeech.bytedance.com', + }; + + // 二进制协议头部字段定义 + static const int PROTOCOL_VERSION = 0x1; // 0b0001 - 版本1 + static const int HEADER_SIZE = 0x1; // 0b0001 - 4字节 + static const int MESSAGE_TYPE = 0x1; // 0b0001 - full client request + static const int MESSAGE_FLAGS = 0x0; // 0b0000 - 无特殊标记 + static const int SERIALIZATION = 0x1; // 0b0001 - JSON + static const int COMPRESSION = 0x0; // 0b0000 - 无压缩 + static const int RESERVED = 0x0; // 0b0000 - 保留字段 + + // 构建二进制协议头 + static Uint8List _buildHeader() { + final header = ByteData(4); // 4字节的头部 + + // 第一个字节: [协议版本(4位) | 报头大小(4位)] + header.setUint8(0, (PROTOCOL_VERSION << 4) | HEADER_SIZE); + + // 第二个字节: [消息类型(4位) | 消息标记(4位)] + header.setUint8(1, (MESSAGE_TYPE << 4) | MESSAGE_FLAGS); + + // 第三个字节: [序列化方法(4位) | 压缩方法(4位)] + header.setUint8(2, (SERIALIZATION << 4) | COMPRESSION); + + // 第四个字节: [保留字段(8位)] + header.setUint8(3, RESERVED); + + return header.buffer.asUint8List(); + } + + VolcanoTtsService() : + _appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '', + _token = dotenv.env['VOLCANO_TTS_APP_TOKEN'] ?? '', + _cluster = dotenv.env['VOLCANO_TTS_CLUSTER'] ?? '', + _voiceType = dotenv.env['VOLCANO_TTS_VOICE_TYPE'] ?? 'zh_male_M392_conversation_wvae_bigtts' { + if (_appId.isEmpty || _token.isEmpty || _cluster.isEmpty) { + throw Exception('火山语音配置信息不完整,请检查环境变量'); + } + } + + @override + Future onInit() async { + super.onInit(); + try { + await _initAudioSession(); + await _initAudioPlayer(); + await useDefaultAudioDevice(); + } catch (e, stackTrace) { + print('初始化TTS服务失败: $e'); + print('Stack trace: $stackTrace'); + } + } + + Future _initAudioSession() async { + _audioSession = await AudioSession.instance; + await _audioSession?.configure(AudioSessionConfiguration( + avAudioSessionCategory: AVAudioSessionCategory.playback, + androidAudioAttributes: const AndroidAudioAttributes( + contentType: AndroidAudioContentType.speech, + usage: AndroidAudioUsage.media, + flags: AndroidAudioFlags.audibilityEnforced, + ), + androidAudioFocusGainType: AndroidAudioFocusGainType.gain, + androidWillPauseWhenDucked: true, + )); + + // 确保音频会话激活并设置正确的音频模式 + if (Platform.isAndroid) { + await _audioSession?.setActive(true, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation); + } else { + await _audioSession?.setActive(true); + } + } + + Future _initAudioPlayer() async { + if (_isDisposed) return; + + _audioPlayer = AudioPlayer(); + + // 配置Android特定的播放参数 + if (Platform.isAndroid) { + await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes( + contentType: AndroidAudioContentType.speech, + usage: AndroidAudioUsage.media, + flags: AndroidAudioFlags.audibilityEnforced, + )); + } + + // 基本播放器配置 + await _audioPlayer.setVolume(1.0); + await _audioPlayer.setLoopMode(LoopMode.off); + await _audioPlayer.setSpeed(1.0); + + // 设置音频缓冲配置,启用预加载以减少延迟 + await _audioPlayer.setAudioSource( + _playlist, + initialPosition: Duration.zero, + preload: true, + ); + + _setupEventListeners(); + } + + Future speak(String text) async { + if (!isEnabled.value || text.trim().isEmpty) return; + + try { + _pendingText += text; + _extractFullSentences(); + await _startPreloadIfNeeded(); + } catch (e, stack) { + print('语音合成失败: $e'); + await _reinitializePlayer(); + } + } + + void _extractFullSentences() { + if (_pendingText.isEmpty) return; + + final matches = _sentenceBreaks.allMatches(_pendingText).toList(); + if (matches.isEmpty) { + // 如果没有找到句子分隔符,按长度分割 + _splitByLength(_pendingText); + _pendingText = ''; + return; + } + + int lastPos = 0; + String currentBatch = ''; + + for (final match in matches) { + final end = match.end; + final sentence = _pendingText.substring(lastPos, end).trim(); + + if (sentence.isEmpty) { + lastPos = end; + continue; + } + + // 如果当前批次为空,直接添加句子 + if (currentBatch.isEmpty) { + currentBatch = sentence; + } + // 如果当前批次加上新句子不超过30字符,则合并 + else if ((currentBatch + sentence).length <= 30) { + currentBatch += sentence; + } + // 如果超过30字符,将当前批次加入队列,开始新批次 + else { + if (currentBatch.isNotEmpty) { + _sentenceQueue.add(currentBatch); + } + currentBatch = sentence; + } + + lastPos = end; + } + + // 处理最后一个批次 + if (currentBatch.isNotEmpty) { + _sentenceQueue.add(currentBatch); + } + + // 处理剩余的文本 + if (lastPos < _pendingText.length) { + final remaining = _pendingText.substring(lastPos).trim(); + if (remaining.isNotEmpty) { + if (remaining.length <= 30) { + // 尝试与最后一个批次合并 + if (_sentenceQueue.isNotEmpty && + (_sentenceQueue.last + remaining).length <= 30) { + _sentenceQueue[_sentenceQueue.length - 1] += remaining; + } else { + _sentenceQueue.add(remaining); + } + } else { + _splitByLength(remaining); + } + } + } + + _pendingText = ''; + } + + void _splitByLength(String text) { + if (text.isEmpty) return; + + int start = 0; + while (start < text.length) { + int end = math.min(start + 30, text.length); + + // 尝试在标点或空格处分割 + if (end < text.length) { + final spacePos = text.lastIndexOf(' ', end); + final breakPos = _sentenceBreaks.firstMatch( + text.substring(start, end) + )?.end; + + if (breakPos != null && breakPos > start) { + end = start + breakPos; + } else if (spacePos > start) { + end = spacePos; + } + } + + final segment = text.substring(start, end).trim(); + if (segment.isNotEmpty) { + // 尝试与前一个批次合并 + if (_sentenceQueue.isNotEmpty && + (_sentenceQueue.last + segment).length <= 30) { + _sentenceQueue[_sentenceQueue.length - 1] += segment; + } else { + _sentenceQueue.add(segment); + } + } + start = end; + } + } + + Future _startPreloadIfNeeded() async { + if (_isFetching || _sentenceQueue.isEmpty) return; + _isFetching = true; + await _fetchNextSegment(); + } + + Future _fetchNextSegment() async { + if (_sentenceQueue.isEmpty) { + _isFetching = false; + return; + } + + try { + // 预加载多个片段以实现流畅播放 + final segments = []; + while (_sentenceQueue.isNotEmpty && segments.length < 2) { // 减少预加载数量以降低内存压力 + segments.add(_sentenceQueue.removeAt(0)); + } + + for (final segment in segments) { + print('获取音频片段: $segment'); + final audioData = await _synthesize(segment); + + if (audioData != null) { + print('成功获取音频数据: ${audioData.length} bytes'); + try { + final audioSource = BytesAudioSource(audioData); + await _playlist.add(audioSource); + print('音频片段已添加到播放列表'); + + // 如果是第一个片段且播放器未在播放,开始播放 + if (!_audioPlayer.playing && _playlist.length == 1) { + // 确保音频会话处于激活状态 + await _audioSession?.setActive(true); + + // 设置播放参数 + await _audioPlayer.setVolume(1.0); + await _audioPlayer.seek(Duration.zero, index: 0); + + // 开始播放 + await _audioPlayer.play(); + } + } catch (e) { + print('添加音频源失败: $e'); + } + } + } + + // 如果队列还有内容,继续获取下一批 + if (_sentenceQueue.isNotEmpty) { + await Future.delayed(const Duration(milliseconds: 100)); // 添加延迟以避免过快请求 + await _fetchNextSegment(); + } else { + _isFetching = false; + // 检查是否还有未处理的文本 + _extractFullSentences(); + if (_sentenceQueue.isNotEmpty) { + await _startPreloadIfNeeded(); + } + } + } catch (e, stackTrace) { + print('获取音频失败: $e'); + print('Stack trace: $stackTrace'); + _isFetching = false; + } + } + + Future _synthesize(String text) async { + WebSocket? socket; + StreamSubscription? subscription; + + try { + final requestJson = { + 'app': { + 'appid': _appId, + 'token': _token, + 'cluster': _cluster, + }, + 'user': { + 'uid': '${DateTime.now().millisecondsSinceEpoch}', + }, + 'audio': { + 'voice_type': _voiceType, + 'encoding': 'mp3', + 'speed_ratio': 1.0, + 'volume_ratio': 1.0, + 'pitch_ratio': 1.0, + 'language': 'zh', + }, + 'request': { + 'reqid': '${DateTime.now().millisecondsSinceEpoch}', + 'text': text, + 'text_type': 'plain', + 'operation': 'query', + }, + }; + + final jsonBytes = utf8.encode(json.encode(requestJson)); + final header = _buildHeader(); + final fullRequest = Uint8List(header.length + 4 + jsonBytes.length); + + fullRequest.setAll(0, header); + final lengthBytes = ByteData(4)..setInt32(0, jsonBytes.length, Endian.big); + fullRequest.setAll(header.length, lengthBytes.buffer.asUint8List()); + fullRequest.setAll(header.length + 4, jsonBytes); + + final wsUrl = Uri.parse(_apiUrl); + final completer = Completer(); + + socket = await WebSocket.connect( + wsUrl.toString(), + headers: { + ..._wsHeaders, + 'Authorization': 'Bearer;$_token', + }, + ).timeout( + const Duration(seconds: 5), + onTimeout: () { + throw TimeoutException('WebSocket连接超时'); + }, + ); + + subscription = socket.listen( + (response) { + if (!completer.isCompleted && response is Uint8List) { + if (response.length < 2) return; + + final messageType = response[1] >> 4; + + if (messageType == 0x0B) { + final headerSize = response[0] & 0x0F; + final payloadStart = headerSize * 4; + + if (response.length > payloadStart) { + final audioData = response.sublist(payloadStart); + completer.complete(audioData); + } + } else if (messageType == 0x0F) { + try { + final headerSize = response[0] & 0x0F; + final payloadStart = headerSize * 4; + + if (response.length < payloadStart + 4) { + throw VolcanoTtsException('错误响应数据不完整'); + } + + final jsonData = response.sublist(payloadStart + 4); + final jsonString = utf8.decode(jsonData, allowMalformed: true); + + if (jsonString.trim().startsWith('{')) { + final errorMap = json.decode(jsonString); + throw VolcanoTtsException(errorMap['error'] as String? ?? '未知错误'); + } + } catch (e) { + if (e is VolcanoTtsException) rethrow; + throw VolcanoTtsException('服务返回错误: $e'); + } + } + } + }, + onError: (error) { + if (!completer.isCompleted) { + completer.completeError(error); + } + }, + onDone: () { + if (!completer.isCompleted) { + completer.complete(null); + } + }, + cancelOnError: false, + ); + + socket.add(fullRequest); + + return await completer.future.timeout( + const Duration(seconds: 10), + onTimeout: () => null, + ); + } catch (e) { + print('合成请求失败: $e'); + return null; + } finally { + await subscription?.cancel(); + await socket?.close(); + } + } + + Future _playAudio(Uint8List audioData) async { + if (_isDisposed) { + print('TTS服务已销毁,跳过播放'); + return; + } + + try { + print('准备播放音频数据: ${audioData.length} bytes'); + final audioSource = BytesAudioSource(audioData); + + print('清除播放列表'); + await _playlist.clear(); + + print('添加新的音频源到播放列表'); + await _playlist.add(audioSource); + + if (!_audioPlayer.playing) { + print('设置音量和播放位置'); + await _audioPlayer.setVolume(1.0); + await _audioPlayer.seek(Duration.zero, index: 0); + + // 确保音频会话处于激活状态 + await _audioSession?.setActive(true); + + print('等待50ms确保音频准备就绪'); + await Future.delayed(const Duration(milliseconds: 50)); + + print('开始播放'); + await _audioPlayer.play(); + print('播放命令已发送'); + } else { + print('播放器已在播放中'); + } + } catch (e, stack) { + print('播放音频失败: $e'); + print('错误堆栈: $stack'); + await _reinitializePlayer(); + } + } + + Future stop() async { + try { + print('停止播放'); + await _audioPlayer.stop(); + await _playlist.clear(); + _pendingText = ''; + _sentenceQueue.clear(); + _isFetching = false; + isPlaying.value = false; + + // 确保清空所有待处理的音频数据 + try { + // 取消所有正在进行的WebSocket连接 + await _channel?.sink.close(); + _channel = null; + + // 重置播放器状态 + await _audioPlayer.pause(); + await _audioPlayer.seek(Duration.zero); + + print('已清空所有待播放的文字和音频'); + } catch (e) { + print('清空音频缓存时出错: $e'); + } + } catch (e) { + print('停止播放失败: $e'); + } + } + + // 添加一个方法来检查音频播放器是否真正在播放 + bool isActuallyPlaying() { + try { + // 检查播放器状态 + final isPlayerPlaying = _audioPlayer.playing; + + // 检查播放列表是否为空 + final hasAudioSource = _playlist.length > 0; + + // 只有当播放器正在播放且播放列表不为空时,才认为真正在播放 + final actuallyPlaying = isPlayerPlaying && hasAudioSource; + + // 如果状态不一致,记录日志 + if (isPlaying.value != actuallyPlaying) { + print('音频播放状态不一致: isPlaying.value=${isPlaying.value}, 实际播放状态=$actuallyPlaying (播放器状态=$isPlayerPlaying, 播放列表长度=${_playlist.length})'); + } + + return actuallyPlaying; + } catch (e) { + print('检查实际播放状态失败: $e'); + // 如果检查失败,返回当前的 isPlaying 值 + return isPlaying.value; + } + } + + Future _reinitializePlayer() async { + try { + await stop(); + await _audioPlayer.dispose(); + + // 重新初始化音频会话 + await _initAudioSession(); + + _audioPlayer = AudioPlayer(); + await _audioPlayer.setVolume(1.0); + await _audioPlayer.setLoopMode(LoopMode.off); + + // 重新配置Android特定的播放参数 + await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes( + contentType: AndroidAudioContentType.speech, + usage: AndroidAudioUsage.media, + flags: AndroidAudioFlags.audibilityEnforced, + )); + + await _audioPlayer.setAudioSource( + _playlist, + initialPosition: Duration.zero, + preload: true, + ); + + _setupEventListeners(); + print('播放器重新初始化成功'); + } catch (e) { + print('播放器重新初始化失败: $e'); + } + } + + void _setupEventListeners() { + _playbackEventSubscription?.cancel(); + _playerStateSubscription?.cancel(); + + _playbackEventSubscription = _audioPlayer.playbackEventStream.listen( + (event) { + if (_isDisposed) return; + if (event.processingState == ProcessingState.completed) { + _tryPlayNext(); + } + }, + onError: (error) { + if (_isDisposed) return; + _reinitializePlayer(); + }, + ); + + _playerStateSubscription = _audioPlayer.playerStateStream.listen( + (state) { + if (_isDisposed) return; + isPlaying.value = state.playing; + + if (state.processingState == ProcessingState.completed) { + _tryPlayNext(); + } + }, + onError: (error) { + if (_isDisposed) return; + _reinitializePlayer(); + }, + ); + } + + Future _tryPlayNext() async { + if (_isDisposed || _playlist.length <= 0) return; + + try { + if (!_audioPlayer.playing) { + // 确保音频会话处于激活状态 + await _audioSession?.setActive(true); + + // 减少延迟时间 + await Future.delayed(const Duration(milliseconds: 30)); + + if (!_isDisposed && !_audioPlayer.playing) { + await _audioPlayer.seek(Duration.zero, index: 0); + await _audioPlayer.play(); + } + } + } catch (e) { + if (e is! StateError) { + print('播放错误: $e'); + await _reinitializePlayer(); + } + } + } + + void toggleEnabled() { + isEnabled.toggle(); + if (!isEnabled.value) { + stop(); + } + } + + Future _cleanupResources() async { + _isDisposed = true; + await _playbackEventSubscription?.cancel(); + await _playerStateSubscription?.cancel(); + await _audioSession?.setActive(false); + await _channel?.sink.close(); + await stop(); + await _audioPlayer.dispose(); + } + + @override + void onClose() async { + print('关闭TTS服务'); + await _cleanupResources(); + super.onClose(); + } + + /// 使用系统默认音频设备 + Future useDefaultAudioDevice() async { + try { + if (Platform.isAndroid) { + // Android 平台 - 重置为正常模式,让系统自动选择 + final audioManager = AndroidAudioManager(); + await audioManager.setMode(AndroidAudioHardwareMode.normal); + await audioManager.stopBluetoothSco(); + await audioManager.setBluetoothScoOn(false); + // 不显式设置扬声器状态,让系统自动选择 + await audioManager.setSpeakerphoneOn(false); + return true; + } else if (Platform.isIOS) { + // iOS 平台 - 清除任何输出端口覆盖 + final avAudioSession = AVAudioSession(); + await avAudioSession.overrideOutputAudioPort(AVAudioSessionPortOverride.none); + return true; + } + return false; + } catch (e) { + Logger.error('设置默认音频设备失败: $e'); + return false; + } + } +} \ No newline at end of file diff --git a/lib/modules/chat/bindings/chat_binding.dart b/lib/modules/chat/bindings/chat_binding.dart index 9cbd6e39b..bfe96ed45 100644 --- a/lib/modules/chat/bindings/chat_binding.dart +++ b/lib/modules/chat/bindings/chat_binding.dart @@ -1,9 +1,31 @@ import 'package:get/get.dart'; import '../controllers/chat_controller.dart'; +import '../../../data/services/azure_asr_service.dart'; +import '../../../data/services/azure_tts_service.dart'; +import '../../../data/services/volcano_ai_service.dart'; +import '../../../data/services/chat_history_service.dart'; class ChatBinding extends Bindings { @override void dependencies() { + // 注册服务 + if (!Get.isRegistered()) { + Get.put(AzureAsrService(), permanent: true); + } + + if (!Get.isRegistered()) { + Get.put(AzureTtsService(), permanent: true); + } + + if (!Get.isRegistered()) { + Get.put(VolcanoAIService(), permanent: true); + } + + if (!Get.isRegistered()) { + Get.put(ChatHistoryService(), permanent: true); + } + + // 注册控制器 Get.lazyPut(() => ChatController()); } } \ No newline at end of file diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 0d036682f..563f2385d 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -3,8 +3,8 @@ import 'package:get/get.dart'; import 'package:flutter/widgets.dart'; import '../models/message_model.dart'; import '../../../data/services/volcano_ai_service.dart'; -import '../../../data/services/volcano_tts_api_service.dart'; -import '../../../data/services/volcano_asr_api_service.dart'; +import '../../../data/services/azure_tts_service.dart'; +import '../../../data/services/azure_asr_service.dart'; import '../../../data/providers/agent_provider.dart'; import '../../../data/models/agent.dart'; import '../../../core/utils/logger.dart'; @@ -13,8 +13,8 @@ import '../../../data/services/chat_history_service.dart'; class ChatController extends GetxController { // 服务 final VolcanoAIService _aiService = Get.find(); - final VolcanoTtsApiService _ttsService = Get.find(); - final VolcanoAsrApiService _asrService = Get.find(); + final AzureTtsService _ttsService = Get.find(); + final AzureAsrService _asrService = Get.find(); final ChatHistoryService _historyService = Get.find(); // 添加ScrollController用于控制列表滚动 @@ -38,6 +38,7 @@ class ChatController extends GetxController { // 语音识别状态 final RxString recognizedText = ''.obs; final RxBool isListening = false.obs; + final RxBool isSpeechDetected = false.obs; // 临时消息索引,用于更新临时消息 int _tempMessageIndex = -1; @@ -50,7 +51,7 @@ class ChatController extends GetxController { bool _shouldCancelAiResponse = false; @override - void onInit() { + Future onInit() async { super.onInit(); Logger.info('ChatController onInit'); @@ -68,6 +69,9 @@ class ChatController extends GetxController { agent = foundAgent; + // 设置TTS服务 + await _ttsService.setVoice(agent.voice); + // 加载聊天历史 _loadChatHistory(); @@ -104,14 +108,11 @@ class ChatController extends GetxController { _aiResponseSubscription?.cancel(); _recognitionSubscription?.cancel(); - // 停止TTS并结束会话 - if (_ttsService.isPlaying) { - _ttsService.stop(); - _ttsService.endSession(); - } + // 停止TTS + _ttsService.stop(); if (isVoiceRecognitionActive.value) { - _asrService.stopRecognition(); + _asrService.stopContinuousRecognition(); } super.onClose(); @@ -136,11 +137,8 @@ class ChatController extends GetxController { void _interruptCurrentResponse(String reason) { Logger.info('${reason}'); - // 停止TTS并结束会话 - if (_ttsService.isPlaying) { - _ttsService.stop(); - _ttsService.endSession(); - } + // 停止TTS + _ttsService.stop(); // 取消AI响应流订阅 if (_aiResponseSubscription != null) { @@ -194,11 +192,6 @@ class ChatController extends GetxController { // 重置取消标志 _shouldCancelAiResponse = false; - - // 启动TTS会话(仅当TTS启用时) - if (isTtsEnabled.value) { - await _ttsService.startSession(agent.voice); - } String fullResponse = ''; @@ -218,32 +211,27 @@ class ChatController extends GetxController { // 更新加载中的消息内容 _updateLoadingMessage(fullResponse); - // 如果TTS启用,发送到TTS + // 如果TTS启用,使用流式处理发送到TTS if (isTtsEnabled.value) { - _ttsService.speak(chunk); + _ttsService.processStreamText(chunk); } }, onDone: () { if (_shouldCancelAiResponse) return; - - // 结束TTS会话(仅当TTS启用时) - if (isTtsEnabled.value) { - _ttsService.endSession(); - } // 完成响应,将isLoading设为false _finalizeResponse(fullResponse); + // 如果TTS启用,处理缓冲区中的剩余文本 + if (isTtsEnabled.value) { + _ttsService.flushStreamBuffer(); + } + // 保存聊天历史 _saveChatHistory(); }, onError: (error) { Logger.error('AI响应错误', error); - - // 结束TTS会话(仅当TTS启用时) - if (isTtsEnabled.value) { - _ttsService.endSession(); - } // 更新加载中的消息为错误消息 _handleResponseError(); @@ -252,11 +240,6 @@ class ChatController extends GetxController { } catch (e) { Logger.error('发送消息失败', e); - // 结束TTS会话(仅当TTS启用时) - if (isTtsEnabled.value) { - _ttsService.endSession(); - } - _handleResponseError(); } } @@ -328,10 +311,9 @@ class ChatController extends GetxController { void toggleTts() { isTtsEnabled.toggle(); - // 如果关闭TTS,停止当前播放并结束会话 - if (!isTtsEnabled.value && _ttsService.isPlaying) { + // 如果关闭TTS,停止当前播放 + if (!isTtsEnabled.value) { _ttsService.stop(); - _ttsService.endSession(); } } @@ -340,25 +322,24 @@ class ChatController extends GetxController { if (isVoiceRecognitionActive.value) return; try { - // 启动语音识别 - final success = await _asrService.startRecognition(); - - if (success) { - isVoiceRecognitionActive.value = true; - isListening.value = true; - recognizedText.value = ''; - - // 重置临时消息索引 - _tempMessageIndex = -1; - - // 监听识别事件 - _recognitionSubscription = _asrService.recognitionStream.listen( - _handleRecognitionEvent, - onError: _handleRecognitionError, - ); - } else { - Logger.error('启动语音识别失败'); - } + // 初始化语音识别 + await _asrService.initialize(language: 'zh-CN'); + + // 启动连续语音识别 + final recognitionStream = await _asrService.startContinuousRecognition(); + + isVoiceRecognitionActive.value = true; + isListening.value = true; + recognizedText.value = ''; + + // 重置临时消息索引 + _tempMessageIndex = -1; + + // 监听识别事件 + _recognitionSubscription = recognitionStream.listen( + _handleRecognitionEvent, + onError: _handleRecognitionError, + ); } catch (e) { Logger.error('启动语音识别失败', e); isVoiceRecognitionActive.value = false; @@ -369,7 +350,7 @@ class ChatController extends GetxController { // 处理语音识别事件 void _handleRecognitionEvent(RecognitionEvent event) { switch (event.type) { - case RecognitionEventType.recognizing: + case RecognitionEventType.intermediateResult: _handleRecognizing(event.text); break; case RecognitionEventType.finalResult: @@ -390,8 +371,13 @@ class ChatController extends GetxController { recognizedText.value = text; + // 设置语音检测状态为true,用于控制波形动画 + if (!isSpeechDetected.value) { + isSpeechDetected.value = true; + } + // 用户开始讲话时,立即停止当前TTS和中断AI响应 - if (_ttsService.isPlaying || _aiResponseSubscription != null) { + if (isSending.value || _aiResponseSubscription != null) { _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); } @@ -437,6 +423,9 @@ class ChatController extends GetxController { // 重置识别文本,但不关闭语音识别框 recognizedText.value = ''; + + // 重置语音检测状态 + isSpeechDetected.value = false; } // 处理语音识别事件错误 @@ -468,7 +457,8 @@ class ChatController extends GetxController { // 停止语音识别 isVoiceRecognitionActive.value = false; isListening.value = false; - _asrService.stopRecognition(); + isSpeechDetected.value = false; + _asrService.stopContinuousRecognition(); } // 停止语音识别 @@ -476,13 +466,14 @@ class ChatController extends GetxController { if (!isVoiceRecognitionActive.value) return; try { - await _asrService.stopRecognition(); + await _asrService.stopContinuousRecognition(); _recognitionSubscription?.cancel(); _recognitionSubscription = null; isVoiceRecognitionActive.value = false; isListening.value = false; recognizedText.value = ''; + isSpeechDetected.value = false; // 查找并移除临时消息 if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) { diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index 29ec19834..0afae9ed0 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -2,19 +2,54 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:math'; +import 'package:audio_waveforms/audio_waveforms.dart'; import '../controllers/chat_controller.dart'; import '../models/message_model.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/widgets/message_bubble.dart'; -class ChatView extends GetView { - ChatView({Key? key}) : super(key: key); +class ChatView extends StatefulWidget { + const ChatView({Key? key}) : super(key: key); + + @override + State createState() => _ChatViewState(); +} + +class _ChatViewState extends State with TickerProviderStateMixin { + // 获取控制器 + ChatController get controller => Get.find(); // 添加ScrollController用于控制滚动 final ScrollController _scrollController = ScrollController(); - + + // 添加一个动画控制器,用于波形动画 + late final AnimationController _waveAnimationController; + + // 添加录音控制器,用于获取麦克风数据 + late final RecorderController _recorderController; + @override - Widget build(BuildContext context) { + void initState() { + super.initState(); + + // 初始化动画控制器 + _waveAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1000), + )..repeat(); + + // 初始化录音控制器 + _recorderController = RecorderController() + ..androidEncoder = AndroidEncoder.aac + ..androidOutputFormat = AndroidOutputFormat.mpeg4 + ..iosEncoder = IosEncoder.kAudioFormatMPEG4AAC + ..sampleRate = 44100 + ..bitRate = 128000 + ..updateFrequency = const Duration(milliseconds: 50); // 更新频率 + + // 初始化录音控制器 + _initializeRecorder(); + // 监听消息列表变化,自动滚动到底部 ever(controller.messages, (_) { // 使用Future.delayed确保在UI更新后滚动 @@ -30,7 +65,44 @@ class ChatView extends GetView { } }); }); - + + // 监听语音识别状态变化 + ever(controller.isVoiceRecognitionActive, (isActive) { + if (isActive) { + // 开始录音以获取波形数据 + _recorderController.record(); + } else { + // 停止录音 + _recorderController.stop(); + } + }); + } + + // 初始化录音控制器 + Future _initializeRecorder() async { + try { + // 请求录音权限 + await _recorderController.checkPermission(); + + // 如果当前正在进行语音识别,开始录音 + if (controller.isVoiceRecognitionActive.value) { + await _recorderController.record(); + } + } catch (e) { + print('初始化录音控制器失败: $e'); + } + } + + @override + void dispose() { + _scrollController.dispose(); + _waveAnimationController.dispose(); + _recorderController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, // 设置Scaffold背景为白色 appBar: AppBar( @@ -266,7 +338,7 @@ class ChatView extends GetView { SizedBox(height: 10.h), // 状态文本 Obx(() => Text( - controller.recognizedText.isEmpty ? '你可以开始说话' : '正在聆听...', + controller.isSpeechDetected.value ? '正在聆听...' : '你可以开始说话', style: TextStyle( fontSize: 16.sp, color: Colors.grey.shade700, @@ -350,149 +422,74 @@ class ChatView extends GetView { // 音频波形 Widget _buildAudioWaveform() { - final barCount = 23; // 条形数量 - return SizedBox( width: 220.w, height: 50.h, child: Obx(() { - final isSpeaking = controller.recognizedText.isNotEmpty; + final isSpeaking = controller.isSpeechDetected.value; final isRec = controller.isListening.value; - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - barCount, - (index) { - // 创建动态高度效果,确保高度始终为正值 - double height = isRec - ? _getWaveHeight(index, isSpeaking, barCount).clamp(2.h, 22.5.h) - : 2.h; - - // 动画持续时间 - final duration = Duration( - milliseconds: isSpeaking ? 60 + (index % 4) * 20 : 150 + (index % 3) * 50 - ); - - // 颜色计算 - Color barColor; - if (isRec) { - if (isSpeaking) { - final middleIndex = barCount ~/ 2; - final distance = (index - middleIndex).abs(); - final maxDistance = middleIndex; - final colorIntensity = 1.0 - (distance / maxDistance) * 0.2; - - barColor = index % 3 == 0 - ? AppColors.primary - : AppColors.primary.withOpacity(colorIntensity); - } else { - barColor = AppColors.primary.withOpacity(0.3); - } - } else { - barColor = Colors.grey.shade400; - } - - return SizedBox( - width: 5.5.w, - child: Center( - child: AnimatedContainer( - duration: duration, - curve: isSpeaking ? Curves.elasticOut : Curves.easeInOut, - width: isSpeaking ? 2.8.w : 2.5.w, - height: height > 0 ? height : 2.h, // 确保高度始终为正值 - margin: EdgeInsets.symmetric(horizontal: 1.2.w), - decoration: BoxDecoration( - color: barColor, - borderRadius: BorderRadius.circular(2.r), - boxShadow: isSpeaking ? [ - BoxShadow( - color: AppColors.primary.withOpacity(0.4), - blurRadius: 5, - spreadRadius: 1.2, - offset: const Offset(0, 0), // 确保偏移量不为null - ) - ] : null, - ), - ), - ), - ); - }, + if (!isRec) { + // 如果没有在录音,显示静态波形 + return _buildStaticWaveform(); + } + + // 使用AudioWaveforms组件显示实时波形 + return AudioWaveforms( + enableGesture: false, + size: Size(220.w, 50.h), + recorderController: _recorderController, + waveStyle: WaveStyle( + waveColor: isSpeaking + ? AppColors.primary + : AppColors.primary.withOpacity(0.3), + extendWaveform: true, + showMiddleLine: false, + spacing: 4.0, + waveThickness: 3.0, + showDurationLabel: false, + scaleFactor: 80, // 调整波形高度 ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + color: Colors.transparent, + ), + padding: EdgeInsets.symmetric(horizontal: 8.w), + margin: EdgeInsets.only(bottom: 5.h), ); }), ); } - // 生成动态波形高度 - double _getWaveHeight(int index, bool isSpeaking, int barCount) { - try { - // 基础高度 - double baseHeight = isSpeaking ? 5.h : 3.h; - - // 获取当前时间毫秒数 - final DateTime now = DateTime.now(); - final int milliseconds = now.millisecondsSinceEpoch; - - // 计算中心位置 - final middleIndex = barCount ~/ 2; - final distanceFromMiddle = (index - middleIndex).abs(); - - // 使用多个正弦波叠加 - double freqMultiplier = isSpeaking ? 2.2 : 1.0; - double ampMultiplier = isSpeaking ? 1.8 : 1.0; - - double wave1 = sin((milliseconds / 150) * freqMultiplier + (index * 0.8)) * 2.h * ampMultiplier; - double wave2 = sin((milliseconds / 280) * freqMultiplier + (index * 0.5)) * 1.5.h * ampMultiplier; - double wave3 = sin((milliseconds / 340) * freqMultiplier + (index * 0.3)) * 1.25.h * ampMultiplier; - double wave4 = cos((milliseconds / 220) * freqMultiplier + (index * 0.7)) * 1.h * ampMultiplier; - - // 基于距离中心的位置计算高度衰减 - double positionFactor = isSpeaking - ? 1.0 - (distanceFromMiddle / middleIndex) * 0.2 - : 1.0 - (distanceFromMiddle / middleIndex) * 0.5; - - // 确保positionFactor不为负值 - positionFactor = max(0.1, positionFactor); - - double result; - if (isSpeaking) { - // 用户说话时的波形 - double timeFactor = 0.9 + sin(milliseconds / 600) * 0.5; - - // 增强中心突出效果 - double centerBoost = pow(positionFactor, 1.2).toDouble() * 10.h; - - // 增加随机性,但限制范围 - double randomFactor = (Random().nextDouble() * 3) * 1.75.h; - - // 增强脉冲效果 - double pulseEffect = sin(milliseconds / 300) * 2.5.h * positionFactor; - - // 添加额外的随机波动 - double voiceVariation = sin(milliseconds / 80 + index * 1.2) * 1.5.h * Random().nextDouble(); - - // 添加爆发效果 - double burstEffect = 0; - if (Random().nextDouble() > 0.97) { - burstEffect = 5.h * positionFactor; - } - - result = baseHeight + (wave1 + wave2 + wave3 + wave4) * timeFactor + - centerBoost + randomFactor + pulseEffect + voiceVariation + burstEffect; - } else { - // 用户不说话时的波形 - double idleWave = (wave1 + wave2) * 0.35 * positionFactor; - double gentlePulse = sin(milliseconds / 800) * 1.h * positionFactor; - result = baseHeight + idleWave + gentlePulse; - } - - // 确保返回值始终为正数 - return max(2.h, result); - } catch (e) { - // 发生错误时返回默认高度 - return 2.h; - } + // 静态波形(当没有录音时显示) + Widget _buildStaticWaveform() { + final barCount = 23; // 条形数量 + + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + barCount, + (index) { + // 静态高度 + final height = 2.h; + + return SizedBox( + width: 5.5.w, + child: Center( + child: Container( + width: 2.5.w, + height: height, + margin: EdgeInsets.symmetric(horizontal: 1.2.w), + decoration: BoxDecoration( + color: Colors.grey.shade400, + borderRadius: BorderRadius.circular(2.r), + ), + ), + ), + ); + }, + ), + ); } } \ No newline at end of file diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index 1f7d47b55..553f54cf1 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -72,44 +72,6 @@ class ProfileView extends GetView { }, ), - const Divider(), - _buildMenuItem( - 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), - ), - ); - } - }, - ), const Divider(), _buildMenuItem( title: '火山语音合成测试'.tr, @@ -120,15 +82,6 @@ class ProfileView extends GetView { }, ), - const Divider(), - _buildMenuItem( - title: 'Azure语音合成测试'.tr, - icon: Icons.record_voice_over, - subtitle: '测试Azure语音TTS功能', - onTap: () { - Get.toNamed(Routes.AZURE_TTS_TEST); - }, - ), const Divider(), _buildMenuItem( @@ -147,6 +100,15 @@ class ProfileView extends GetView { onTap: () { Get.toNamed(Routes.AZURE_ASR_TEST); }, + ), + const Divider(), + _buildMenuItem( + title: 'Azure语音合成测试'.tr, + icon: Icons.record_voice_over, + subtitle: '测试Azure语音TTS功能', + onTap: () { + Get.toNamed(Routes.AZURE_TTS_TEST); + }, ), const Divider(), _buildMenuItem( diff --git a/lib/modules/test/bindings/azure_asr_test_binding.dart b/lib/modules/test/bindings/azure_asr_test_binding.dart new file mode 100644 index 000000000..f3f29b968 --- /dev/null +++ b/lib/modules/test/bindings/azure_asr_test_binding.dart @@ -0,0 +1,16 @@ +import 'package:get/get.dart'; +import '../controllers/azure_asr_test_controller.dart'; +import '../../../data/services/azure_asr_service.dart'; + +class AzureAsrTestBinding extends Bindings { + @override + void dependencies() { + // 注册Azure ASR服务(如果尚未注册) + if (!Get.isRegistered()) { + Get.lazyPut(() => AzureAsrService()); + } + + // 注册控制器 + Get.lazyPut(() => AzureAsrTestController()); + } +} \ No newline at end of file diff --git a/lib/modules/test/bindings/azure_tts_test_binding.dart b/lib/modules/test/bindings/azure_tts_test_binding.dart index 93ccb3312..b7e071af5 100644 --- a/lib/modules/test/bindings/azure_tts_test_binding.dart +++ b/lib/modules/test/bindings/azure_tts_test_binding.dart @@ -4,8 +4,7 @@ import '../controllers/azure_tts_test_controller.dart'; class AzureTtsTestBinding extends Bindings { @override void dependencies() { - Get.lazyPut( - () => AzureTtsTestController(), - ); + // Use put instead of lazyPut to ensure the controller is created immediately + Get.put(AzureTtsTestController()); } } \ No newline at end of file diff --git a/lib/modules/test/controllers/azure_asr_test_controller.dart b/lib/modules/test/controllers/azure_asr_test_controller.dart index b9f9d1442..433e44ee7 100644 --- a/lib/modules/test/controllers/azure_asr_test_controller.dart +++ b/lib/modules/test/controllers/azure_asr_test_controller.dart @@ -3,116 +3,227 @@ import 'package:get/get.dart'; import '../../../data/services/azure_asr_service.dart'; import '../../../core/utils/logger.dart'; +// Azure语音识别状态枚举 +enum AzureAsrState { + notInitialized, // 未初始化 + initialized, // 已初始化 + listening, // 正在监听 + notListening, // 未监听 + error, // 错误 +} + class AzureAsrTestController extends GetxController { - final AzureAsrService _azureService = Get.find(); + // 服务引用 + final AzureAsrService _azureAsrService = Get.find(); + + // 状态变量 + final RxBool isListening = false.obs; + final RxString recognizedText = ''.obs; + final RxString partialText = ''.obs; + final Rx asrState = AzureAsrState.notInitialized.obs; - // 可观察状态 - final isListening = false.obs; - final errorMessage = ''.obs; - final isContinuous = true.obs; - final recognitionResults = [].obs; + // 语言选项 + final RxString selectedLanguage = 'zh-CN'.obs; + final List availableLanguages = [ + 'zh-CN', // 中文(中国大陆) + 'en-US', // 英语(美国) + 'ja-JP', // 日语 + 'ko-KR', // 韩语 + 'fr-FR', // 法语 + 'de-DE', // 德语 + 'es-ES', // 西班牙语 + 'it-IT', // 意大利语 + 'pt-BR', // 葡萄牙语(巴西) + 'ru-RU', // 俄语 + ]; - // 连续识别相关 + // 订阅 StreamSubscription? _recognitionSubscription; @override void onInit() { super.onInit(); - - // 由于 AzureAsrService 没有公开的 isListening 属性,我们需要自己管理状态 + _initializeAsr(); } - /// 开始录音 - void startListening() async { + // 初始化ASR服务 + Future _initializeAsr() async { try { - errorMessage.value = ''; + final success = await _azureAsrService.initialize(language: selectedLanguage.value); + asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; + } catch (e) { + Logger.error('初始化ASR服务失败: $e'); + asrState.value = AzureAsrState.error; + } + } + + // 开始简单语音识别 + Future startSimpleRecognition() async { + try { + if (asrState.value == AzureAsrState.notInitialized) { + await _initializeAsr(); + } - if (isContinuous.value) { - // 连续识别模式 - final success = await _azureService.startContinuousRecognition(); - if (!success) { - errorMessage.value = '无法启动语音识别'; - return; - } - - isListening.value = true; - - _recognitionSubscription = _azureService.recognitionStream?.listen( - (event) { - switch (event.type) { - case RecognitionEventType.finalResult: - if (event.text.isNotEmpty) { - recognitionResults.add(event.text); - } - break; - case RecognitionEventType.error: - errorMessage.value = event.error ?? '未知错误'; - isListening.value = false; - break; - case RecognitionEventType.canceled: - errorMessage.value = '识别已取消: ${event.reason ?? "未知原因"}'; - isListening.value = false; - break; - case RecognitionEventType.sessionStopped: - isListening.value = false; - break; - default: - break; - } - }, - onError: (error) { - errorMessage.value = error.toString(); - isListening.value = false; - }, - ); + // 如果正在进行连续识别,先停止 + if (_azureAsrService.isContinuousRecognitionActive()) { + await stopContinuousRecognition(); + } + + partialText.value = ''; + final result = await _azureAsrService.recognizeSpeech(); + if (result.isNotEmpty) { + recognizedText.value = result; + } + } catch (e) { + Logger.error('开始简单语音识别出错: $e'); + asrState.value = AzureAsrState.error; + } + } + + // 切换连续语音识别 + Future toggleContinuousRecognition() async { + try { + if (asrState.value == AzureAsrState.notInitialized) { + await _initializeAsr(); + } + + if (_azureAsrService.isContinuousRecognitionActive()) { + await stopContinuousRecognition(); } else { - // 一次性识别模式 - isListening.value = true; - try { - final result = await _azureService.recognizeOnce(); - if (result.isNotEmpty) { - recognitionResults.add(result); - } - } catch (e) { - errorMessage.value = '识别失败: $e'; - } finally { - isListening.value = false; - } + await startContinuousRecognition(); } } catch (e) { - Logger.error('启动语音识别失败', e); - errorMessage.value = e.toString(); - isListening.value = false; + Logger.error('切换连续语音识别出错: $e'); + asrState.value = AzureAsrState.error; } } - /// 停止录音 - void stopListening() async { + // 开始连续语音识别 + Future startContinuousRecognition() async { try { - if (isContinuous.value) { - final success = await _azureService.stopContinuousRecognition(); - if (!success) { - errorMessage.value = '停止识别失败'; - } - await _recognitionSubscription?.cancel(); - _recognitionSubscription = null; - isListening.value = false; + if (_azureAsrService.isContinuousRecognitionActive()) { + await stopContinuousRecognition(); } + + _recognitionSubscription?.cancel(); + _recognitionSubscription = null; + + isListening.value = true; + asrState.value = AzureAsrState.listening; + partialText.value = ''; + + final stream = await _azureAsrService.startContinuousRecognition(); + + _recognitionSubscription = stream.listen( + (event) { + switch (event.type) { + case RecognitionEventType.finalResult: + if (event.text.isNotEmpty) { + recognizedText.value = event.text; + } + partialText.value = ''; + break; + + case RecognitionEventType.intermediateResult: + if (event.text.isNotEmpty) { + partialText.value = event.text; + } + break; + + case RecognitionEventType.sessionStarted: + isListening.value = true; + asrState.value = AzureAsrState.listening; + break; + + case RecognitionEventType.sessionStopped: + isListening.value = false; + asrState.value = AzureAsrState.notListening; + partialText.value = ''; + break; + + case RecognitionEventType.canceled: + isListening.value = false; + asrState.value = AzureAsrState.notListening; + partialText.value = ''; + break; + + case RecognitionEventType.error: + asrState.value = AzureAsrState.error; + isListening.value = false; + partialText.value = ''; + _initializeAsr(); + break; + } + }, + onError: (error) { + asrState.value = AzureAsrState.error; + isListening.value = false; + partialText.value = ''; + }, + onDone: () { + if (!_azureAsrService.isContinuousRecognitionActive()) { + isListening.value = false; + asrState.value = AzureAsrState.notListening; + } + }, + ); } catch (e) { - Logger.error('停止语音识别失败', e); - errorMessage.value = e.toString(); + Logger.error('开始连续语音识别出错: $e'); + asrState.value = AzureAsrState.error; isListening.value = false; + partialText.value = ''; } } - /// 清空结果 - void clearResults() { - recognitionResults.clear(); + // 停止连续语音识别 + Future stopContinuousRecognition() async { + try { + isListening.value = false; + asrState.value = AzureAsrState.notListening; + + await _azureAsrService.stopContinuousRecognition(); + + _recognitionSubscription?.cancel(); + _recognitionSubscription = null; + + partialText.value = ''; + } catch (e) { + Logger.error('停止连续语音识别出错: $e'); + asrState.value = AzureAsrState.error; + } + } + + // 更改语言 + Future changeLanguage(String language) async { + try { + if (selectedLanguage.value == language) return; + + selectedLanguage.value = language; + + if (_azureAsrService.isContinuousRecognitionActive()) { + await stopContinuousRecognition(); + } + + final success = await _azureAsrService.setRecognitionLanguage(language); + asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; + + recognizedText.value = ''; + partialText.value = ''; + } catch (e) { + Logger.error('更改语言出错: $e'); + asrState.value = AzureAsrState.error; + } } @override void onClose() { _recognitionSubscription?.cancel(); + _recognitionSubscription = null; + + if (_azureAsrService.isContinuousRecognitionActive()) { + _azureAsrService.stopContinuousRecognition(); + } + super.onClose(); } } \ No newline at end of file diff --git a/lib/modules/test/controllers/azure_tts_test_controller.dart b/lib/modules/test/controllers/azure_tts_test_controller.dart index edaec042d..2419dfa42 100644 --- a/lib/modules/test/controllers/azure_tts_test_controller.dart +++ b/lib/modules/test/controllers/azure_tts_test_controller.dart @@ -1,15 +1,13 @@ -import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; import '../../../data/services/azure_tts_service.dart'; import '../../../core/utils/logger.dart'; -// 导入 TtsState 枚举 -import '../../../data/services/azure_tts_service.dart' show TtsState; - class AzureTtsTestController extends GetxController { - final AzureTtsService _ttsService = Get.find(); + // 使用优化后的 AzureTtsService + final AzureTtsService _ttsService = Get.put(AzureTtsService()); // 可观察状态 final isSpeaking = false.obs; @@ -26,31 +24,28 @@ class AzureTtsTestController extends GetxController { // 当前播放的索引 final currentPlayingIndex = RxInt(-1); - // 当前使用的语音 - final currentVoice = 'zh-CN-XiaoxiaoNeural'.obs; + // 语音选择相关 + final isLoadingVoices = false.obs; + final RxList availableVoices = [ + 'zh-CN-XiaoxiaoNeural', + 'zh-CN-YunxiNeural', + 'zh-CN-YunyangNeural', + ].obs; + final Rx selectedVoice = 'zh-CN-XiaoxiaoNeural'.obs; - // 可用的语音列表 - final List> availableVoices = [ - {'id': 'zh-CN-XiaoxiaoNeural', 'name': '晓晓(女声)'}, - {'id': 'zh-CN-YunxiNeural', 'name': '云希(男声)'}, - {'id': 'zh-CN-YunyangNeural', 'name': '云扬(男声)'}, - {'id': 'zh-CN-XiaohanNeural', 'name': '晓涵(女声)'}, - {'id': 'zh-CN-XiaomoNeural', 'name': '晓墨(女声)'}, - {'id': 'zh-CN-XiaoxuanNeural', 'name': '晓萱(女声)'}, - {'id': 'en-US-AriaNeural', 'name': 'Aria(英语女声)'}, - ]; + // 语音参数 + final rate = 0.obs; + final pitch = 0.obs; + final volume = 100.obs; // 固定的播报内容 final RxList speechTexts = [ - '这是第一条播报测试,请听取Azure合成效果。', - '这是第二条播报测试,Azure语音合成支持连续多条播报。', - '这是第三条播报测试,可以测试语音合成的流畅度和连贯性。', - 'This is an English test sentence. Azure Text-to-Speech supports multiple languages.', + '这是第一条Azure语音播报测试,请听取合成效果。', + '这是第二条Azure语音播报测试,Azure语音合成支持连续多条播报。', + '这是第三条Azure语音播报测试,可以测试语音合成的流畅度和连贯性。', + '这是第四条Azure语音播报测试,感谢您的使用。', ].obs; - // 事件订阅 - StreamSubscription? _eventSubscription; - @override void onInit() { super.onInit(); @@ -61,127 +56,114 @@ class AzureTtsTestController extends GetxController { errorMessage.value = ''; isLoading.value = false; - // 监听TTS服务状态 - _setupEventListener(); + // 监听TTS播放状态 + ever(_ttsService.isSpeaking, (bool playing) { + isAudioPlaying.value = playing; + if (!playing && currentPlayingIndex.value >= 0) { + // 如果播放结束,重置当前播放索引 + currentPlayingIndex.value = -1; + } + }); // 启动播放状态检查定时器 _startPlaybackCheckTimer(); - } - - // 设置事件监听 - void _setupEventListener() { - _eventSubscription?.cancel(); - _eventSubscription = _ttsService.synthesisStream?.listen(_handleEvent); - } - - // 处理事件 - void _handleEvent(SynthesisEvent event) { - switch (event.type) { - case SynthesisEventType.synthesisStarted: - statusMessage.value = '合成开始'; - errorMessage.value = ''; - break; - case SynthesisEventType.synthesisCompleted: - statusMessage.value = '合成完成'; - errorMessage.value = ''; - break; - case SynthesisEventType.synthesisCanceled: - statusMessage.value = '合成取消'; - errorMessage.value = event.reason ?? '未知原因'; - currentPlayingIndex.value = -1; - break; - case SynthesisEventType.error: - statusMessage.value = '错误'; - errorMessage.value = event.error ?? '未知错误'; - currentPlayingIndex.value = -1; - break; - default: - statusMessage.value = '事件: ${event.type}'; - break; - } - // 强制更新UI - update(); + // 初始化TTS服务 + _initializeTtsService(); } - /// 更新UI状态 - void updateUIState() { - // 强制更新UI - update(); - } - - // 启动播放状态检查定时器 - void _startPlaybackCheckTimer() { - _playbackCheckTimer?.cancel(); - _playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) { - // 更新实际播放状态 - final actuallyPlaying = _ttsService.ttsState == TtsState.playing || _ttsService.ttsState == TtsState.synthesizing; + /// 初始化TTS服务 + Future _initializeTtsService() async { + try { + isLoading.value = true; + errorMessage.value = ''; + statusMessage.value = '正在初始化...'; - // 只有状态变化时才更新 - if (isAudioPlaying.value != actuallyPlaying) { - isAudioPlaying.value = actuallyPlaying; - // 更新UI - update(); + // 初始化TTS服务 + final result = await _ttsService.initialize(); + + if (result) { + // 设置默认语音 + await _ttsService.setVoice(selectedVoice.value); + + // 设置默认参数 + await _ttsService.setSpeechParams( + rate: rate.value, + pitch: pitch.value, + volume: volume.value, + ); + + statusMessage.value = '已连接'; + } else { + statusMessage.value = '连接失败'; + errorMessage.value = '初始化TTS服务失败'; } - }); + + isLoading.value = false; + } catch (e) { + isLoading.value = false; + statusMessage.value = '连接失败'; + errorMessage.value = '初始化TTS服务失败: $e'; + Logger.error('初始化TTS服务失败: $e'); + } } - /// 连续播放所有文本 - Future speakContinuousTexts() async { - if (isLoading.value) { - return; - } - + /// 切换语音 + Future changeVoice(String voice) async { try { isLoading.value = true; - isSpeaking.value = true; errorMessage.value = ''; - final speaker = currentVoice.value; + selectedVoice.value = voice; - // 设置语音参数 - _ttsService.currentVoice = speaker; + // 设置语音 + final result = await _ttsService.setVoice(voice); - - // 连续播放所有文本 - for (int i = 0; i < speechTexts.length; i++) { - // 如果播放被中断,退出循环 - if (!isSpeaking.value) break; - - // 在主线程上更新UI - WidgetsBinding.instance.addPostFrameCallback((_) { - currentPlayingIndex.value = i; - statusMessage.value = '正在播放第 ${i + 1}/${speechTexts.length} 条文本'; - update(); - }); - - // 播放当前文本,不等待完成 - _ttsService.speak(speechTexts[i]); - - // 短暂延迟,确保文本被送入队列 - await Future.delayed(const Duration(milliseconds: 300)); - } - - // 在主线程上更新UI - WidgetsBinding.instance.addPostFrameCallback((_) { - statusMessage.value = '所有文本已送入播放队列'; - update(); - }); - + if (result) { + statusMessage.value = '已设置语音: $voice'; + } else { + errorMessage.value = '设置语音失败'; + } + isLoading.value = false; } catch (e) { - errorMessage.value = '播放出错'; - if (kDebugMode) { - print('连续播放出错: $e'); - } - } finally { isLoading.value = false; + errorMessage.value = '设置语音失败: $e'; + Logger.error('设置语音失败: $e'); + } + } + + /// 设置语音参数 + Future setSpeechParams({int? newRate, int? newPitch, int? newVolume}) async { + try { + if (newRate != null) rate.value = newRate; + if (newPitch != null) pitch.value = newPitch; + if (newVolume != null) volume.value = newVolume; + + // 设置语音参数 + final result = await _ttsService.setSpeechParams( + rate: rate.value, + pitch: pitch.value, + volume: volume.value, + ); - // 强制更新UI - update(); + if (result) { + statusMessage.value = '已设置语音参数'; + } else { + errorMessage.value = '设置语音参数失败'; + } + } catch (e) { + errorMessage.value = '设置语音参数失败: $e'; + Logger.error('设置语音参数失败: $e'); } } + /// 更新UI状态 + void updateUIState() { + // 强制更新UI + update(); + } + /// 播放单条文本 Future playSingle(int index) async { if (index < 0 || index >= speechTexts.length) { @@ -190,126 +172,108 @@ class AzureTtsTestController extends GetxController { try { isSpeaking.value = true; + isAudioPlaying.value = true; errorMessage.value = ''; currentPlayingIndex.value = index; - statusMessage.value = '准备播放...'; + statusMessage.value = '正在合成...'; final text = speechTexts[index]; - final speaker = currentVoice.value; - // 设置语音参数 - _ttsService.currentVoice = speaker; + // 播放文本 + await _ttsService.speak(text); - // 使用异步操作,避免阻塞主线程 - Future(() { - // 播放文本,不等待完成 - _ttsService.speak(text); - - // 在主线程上更新UI - WidgetsBinding.instance.addPostFrameCallback((_) { - statusMessage.value = '文本已送入播放队列'; - update(); - }); - }); + statusMessage.value = '合成完成'; } catch (e) { errorMessage.value = '播放出错'; - if (kDebugMode) { - print('播放出错: $e'); + statusMessage.value = '错误'; + Logger.error('播放出错: $e'); + } finally { + isSpeaking.value = false; + if (!_ttsService.isSpeaking.value) { + isAudioPlaying.value = false; + currentPlayingIndex.value = -1; } } } - /// 停止播放 - void stopSpeaking() { + // 启动播放状态检查定时器 + void _startPlaybackCheckTimer() { + _playbackCheckTimer?.cancel(); + _playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) { + // 更新实际播放状态 + final actuallyPlaying = _ttsService.isSpeaking.value; + isAudioPlaying.value = actuallyPlaying; + + // 如果不再播放,确保重置状态 + if (!actuallyPlaying && isSpeaking.value) { + // 播放已完成,重置状态 + isSpeaking.value = false; + currentPlayingIndex.value = -1; + // 更新UI + update(); + } + }); + } + + /// 连续播放所有文本 + Future speakContinuousTexts() async { + if (isAudioPlaying.value || isLoading.value) { + return; + } + try { - isSpeaking.value = false; - statusMessage.value = '正在停止播放...'; + isSpeaking.value = true; + errorMessage.value = ''; + statusMessage.value = '开始连续播放'; - // 使用异步操作,避免阻塞主线程 - Future(() async { - // 停止播放 - await _ttsService.stop(); - - // 在主线程上更新UI - WidgetsBinding.instance.addPostFrameCallback((_) { - // 重置UI状态 - currentPlayingIndex.value = -1; - statusMessage.value = '已停止播放'; - update(); - }); - }); + // 播放所有文本 + await _ttsService.speakMultiple(speechTexts.toList()); + + statusMessage.value = '连续播放完成'; } catch (e) { - if (kDebugMode) { - print('停止播放出错: $e'); - } - errorMessage.value = '停止播放出错'; + errorMessage.value = '连续播放出错'; + statusMessage.value = '错误'; + Logger.error('连续播放出错: $e'); + } finally { + isSpeaking.value = false; } } - // 设置当前语音 - void setVoice(String voiceId) { - currentVoice.value = voiceId; + /// 停止播放 + Future stopSpeaking() async { + try { + final result = await _ttsService.stop(); + + if (result) { + isSpeaking.value = false; + isAudioPlaying.value = false; + currentPlayingIndex.value = -1; + statusMessage.value = '已停止'; + } else { + errorMessage.value = '停止播放失败'; + } + } catch (e) { + errorMessage.value = '停止播放出错'; + Logger.error('停止播放出错: $e'); + } } - // 获取当前语音名称 - String getCurrentVoiceName() { - final voice = availableVoices.firstWhere( - (v) => v['id'] == currentVoice.value, - orElse: () => {'id': currentVoice.value, 'name': '未知语音'}, - ); - return voice['name'] ?? '未知语音'; + /// 切换启用状态 + void toggleEnabled() { + _ttsService.toggleEnabled(); + statusMessage.value = _ttsService.isEnabled.value ? '已启用' : '已禁用'; } @override void onClose() { - // 取消事件订阅 - _eventSubscription?.cancel(); - - // 取消播放状态检查定时器 + // 取消定时器 _playbackCheckTimer?.cancel(); + // 停止播放 + _ttsService.stop(); + super.onClose(); } - - /// 初始化Azure TTS服务 - Future initialize({ - String? subscriptionKey, - String? region, - }) async { - try { - isLoading.value = true; - statusMessage.value = '正在初始化Azure TTS...'; - - if (subscriptionKey == null || region == null) { - statusMessage.value = '尝试从环境变量读取配置...'; - } - - await _ttsService.initialize( - subscriptionKey: subscriptionKey, - region: region, - ); - - // 检查是否初始化成功 - final isSuccess = _ttsService.isInitialized; - - if (isSuccess) { - isSpeaking.value = false; - statusMessage.value = 'Azure TTS初始化成功'; - } else { - statusMessage.value = 'Azure TTS初始化失败'; - } - } catch (e) { - statusMessage.value = '初始化异常: $e'; - errorMessage.value = e.toString(); - if (kDebugMode) { - print('初始化异常: $e'); - } - } finally { - isLoading.value = false; - // 强制更新UI - update(); - } - } } \ No newline at end of file diff --git a/lib/modules/test/controllers/volcano_tts_test_controller.dart b/lib/modules/test/controllers/volcano_tts_test_controller.dart index 5113b0520..a7bd79f59 100644 --- a/lib/modules/test/controllers/volcano_tts_test_controller.dart +++ b/lib/modules/test/controllers/volcano_tts_test_controller.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'dart:async'; import 'package:flutter/foundation.dart'; -import '../../../data/services/volcano_tts_api_service.dart'; +import '../../../data/services/volcano_tts_service.dart'; +import '../../../core/utils/logger.dart'; class VolcanoTtsTestController extends GetxController { - final VolcanoTtsApiService _ttsService = Get.find(); + // 使用 VolcanoTtsService 替代 VolcanoTtsApiService + final VolcanoTtsService _ttsService = Get.put(VolcanoTtsService()); // 可观察状态 final isSpeaking = false.obs; @@ -44,9 +46,6 @@ class VolcanoTtsTestController extends GetxController { '这是第四条播报测试,感谢您的使用。', ].obs; - // 事件订阅 - StreamSubscription? _eventSubscription; - @override void onInit() { super.onInit(); @@ -57,24 +56,8 @@ class VolcanoTtsTestController extends GetxController { errorMessage.value = ''; isLoading.value = false; - // 监听TTS服务状态 - ever(_ttsService.isConnected, (bool connected) { - if (connected) { - statusMessage.value = '已连接'; - } else { - statusMessage.value = '未连接'; - isSpeaking.value = false; - isAudioPlaying.value = false; - } - }); - - // 监听TTS合成状态 - ever(_ttsService.isSynthesizing, (bool synthesizing) { - isSpeaking.value = synthesizing; - }); - // 监听TTS播放状态 - ever(_ttsService.isPlaying.obs, (bool playing) { + ever(_ttsService.isPlaying, (bool playing) { isAudioPlaying.value = playing; if (!playing && currentPlayingIndex.value >= 0) { // 如果播放结束,重置当前播放索引 @@ -82,95 +65,10 @@ class VolcanoTtsTestController extends GetxController { } }); - // 设置事件监听 - _setupEventListener(); - - // 连接到TTS服务 - _connectToTtsService(); - // 启动播放状态检查定时器 _startPlaybackCheckTimer(); } - // 设置事件监听 - void _setupEventListener() { - _eventSubscription?.cancel(); - _eventSubscription = _ttsService.eventStream.listen(_handleEvent); - } - - // 处理事件 - void _handleEvent(Map event) { - final eventType = event['eventType']; - - switch (eventType) { - case 'connectionStarted': - statusMessage.value = '连接成功'; - errorMessage.value = ''; - break; - case 'connectionFailed': - statusMessage.value = '连接失败'; - errorMessage.value = event['error'] ?? '未知错误'; - break; - case 'sessionStarted': - statusMessage.value = '会话开始'; - errorMessage.value = ''; - break; - case 'sessionFailed': - statusMessage.value = '会话失败'; - errorMessage.value = event['error'] ?? '未知错误'; - currentPlayingIndex.value = -1; - break; - case 'ttsSentenceStart': - statusMessage.value = '开始合成'; - break; - case 'ttsSentenceEnd': - statusMessage.value = '合成完成'; - break; - case 'sessionFinished': - statusMessage.value = '会话结束'; - currentPlayingIndex.value = -1; - break; - case 'connectionFinished': - statusMessage.value = '连接关闭'; - currentPlayingIndex.value = -1; - break; - case 'error': - statusMessage.value = '错误'; - errorMessage.value = event['error'] ?? '未知错误'; - break; - case 'disconnected': - statusMessage.value = '连接已断开'; - currentPlayingIndex.value = -1; - break; - default: - statusMessage.value = '事件: $eventType'; - break; - } - - // 强制更新UI - update(); - } - - // 连接到TTS服务 - Future _connectToTtsService() async { - try { - isLoading.value = true; - statusMessage.value = '正在连接...'; - - final success = await _ttsService.connect(); - - if (!success) { - statusMessage.value = '连接失败'; - errorMessage.value = '无法连接到火山语音服务'; - } - } catch (e) { - statusMessage.value = '连接错误'; - errorMessage.value = e.toString(); - } finally { - isLoading.value = false; - } - } - /// 更新UI状态 void updateUIState() { // 强制更新UI @@ -188,36 +86,25 @@ class VolcanoTtsTestController extends GetxController { isAudioPlaying.value = true; errorMessage.value = ''; currentPlayingIndex.value = index; + statusMessage.value = '正在合成...'; final text = speechTexts[index]; - final speaker = currentVoice.value; - - // 确保TTS服务已连接 - if (!_ttsService.isConnected.value) { - await _connectToTtsService(); - } - - // 开始会话 - await _ttsService.startSession(speaker); // 播放文本 - await _ttsService.speak(text, speaker: speaker); + await _ttsService.speak(text); - // 结束会话 - await _ttsService.endSession(); - - // 确保播放列表被清空 - await _ttsService.stop(); + statusMessage.value = '合成完成'; } catch (e) { errorMessage.value = '播放出错'; - if (kDebugMode) { - print('播放出错: $e'); - } + statusMessage.value = '错误'; + Logger.error('播放出错: $e'); } finally { isSpeaking.value = false; - isAudioPlaying.value = false; - currentPlayingIndex.value = -1; + if (!_ttsService.isPlaying.value) { + isAudioPlaying.value = false; + currentPlayingIndex.value = -1; + } } } @@ -226,19 +113,16 @@ class VolcanoTtsTestController extends GetxController { _playbackCheckTimer?.cancel(); _playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) { // 更新实际播放状态 - final actuallyPlaying = _ttsService.isPlaying; + final actuallyPlaying = _ttsService.isPlaying.value; isAudioPlaying.value = actuallyPlaying; // 如果不再播放,确保重置状态 if (!actuallyPlaying && isSpeaking.value) { - // 检查是否真的播放完成了 - if (!_ttsService.isSynthesizing.value && currentPlayingIndex.value >= 0) { - // 播放已完成,重置状态 - isSpeaking.value = false; - currentPlayingIndex.value = -1; - // 更新UI - update(); - } + // 播放已完成,重置状态 + isSpeaking.value = false; + currentPlayingIndex.value = -1; + // 更新UI + update(); } }); } @@ -250,119 +134,60 @@ class VolcanoTtsTestController extends GetxController { } try { - isLoading.value = true; isSpeaking.value = true; - isAudioPlaying.value = true; errorMessage.value = ''; + statusMessage.value = '开始连续播放'; - // 确保TTS服务已连接 - if (!_ttsService.isConnected.value) { - await _connectToTtsService(); - } - - final speaker = currentVoice.value; + // 连接所有文本 + final allText = speechTexts.join(''); - // 停止之前的播放 - await _ttsService.stop(); + // 播放所有文本 + await _ttsService.speak(allText); - // 开始会话 - await _ttsService.startSession(speaker); - - // 连续播放所有文本 - for (int i = 0; i < speechTexts.length; i++) { - // 如果播放被中断,退出循环 - if (!isSpeaking.value) break; - - currentPlayingIndex.value = i; - statusMessage.value = '正在播放第 ${i + 1}/${speechTexts.length} 条文本'; - - // 播放当前文本 - await _ttsService.speak(speechTexts[i], speaker: speaker); - - } - - // 结束会话 - await _ttsService.endSession(); - - - statusMessage.value = '播放完成'; + statusMessage.value = '连续播放完成'; } catch (e) { - errorMessage.value = '播放出错'; - if (kDebugMode) { - print('连续播放出错: $e'); - } - - // 尝试结束会话,忽略可能的错误 - try { - await _ttsService.endSession(); - } catch (_) { - // 忽略结束会话时的错误 - } + errorMessage.value = '连续播放出错'; + statusMessage.value = '错误'; + Logger.error('连续播放出错: $e'); } finally { - currentPlayingIndex.value = -1; isSpeaking.value = false; - isLoading.value = false; - // 确保最终状态正确 - isAudioPlaying.value = false; - - // 强制更新UI - update(); } } /// 停止播放 - void stopSpeaking() async { + Future stopSpeaking() async { try { - isSpeaking.value = false; - statusMessage.value = '正在停止播放...'; - - // 停止播放 await _ttsService.stop(); - - // 结束会话 - await _ttsService.endSession(); - - // 重置UI状态 + isSpeaking.value = false; + isAudioPlaying.value = false; currentPlayingIndex.value = -1; - statusMessage.value = '已停止播放'; - + statusMessage.value = '已停止'; } catch (e) { - if (kDebugMode) { - print('停止播放出错: $e'); - } errorMessage.value = '停止播放出错'; - } finally { - isSpeaking.value = false; - isLoading.value = false; - isAudioPlaying.value = false; + Logger.error('停止播放出错: $e'); } } - // 设置当前语音 - void setVoice(String voiceId) { + /// 切换语音 + void changeVoice(String voiceId) { currentVoice.value = voiceId; + // VolcanoTtsService 不需要显式设置发音人,在 speak 时会使用环境变量中的默认发音人 } - // 获取当前语音名称 - String getCurrentVoiceName() { - final voice = availableVoices.firstWhere( - (v) => v['id'] == currentVoice.value, - orElse: () => {'id': currentVoice.value, 'name': '未知语音'}, - ); - return voice['name'] ?? '未知语音'; + /// 切换启用状态 + void toggleEnabled() { + _ttsService.isEnabled.toggle(); + statusMessage.value = _ttsService.isEnabled.value ? '已启用' : '已禁用'; } @override void onClose() { - // 取消事件订阅 - _eventSubscription?.cancel(); - - // 取消播放状态检查定时器 + // 取消定时器 _playbackCheckTimer?.cancel(); - // 断开TTS服务连接 - _ttsService.disconnect(); + // 停止播放 + _ttsService.stop(); super.onClose(); } diff --git a/lib/modules/test/views/azure_asr_test_view.dart b/lib/modules/test/views/azure_asr_test_view.dart index 85fd3f013..ee63d75c9 100644 --- a/lib/modules/test/views/azure_asr_test_view.dart +++ b/lib/modules/test/views/azure_asr_test_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/azure_asr_test_controller.dart'; +import '../../../data/services/azure_asr_service.dart'; class AzureAsrTestView extends GetView { const AzureAsrTestView({Key? key}) : super(key: key); @@ -9,224 +10,164 @@ class AzureAsrTestView extends GetView { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Azure 语音识别测试'), - centerTitle: true, + title: const Text('Azure语音识别测试'), + actions: [ + _buildLanguageDropdown(), + ], ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // 识别模式选择 - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '识别模式', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Obx(() => Row( - children: [ - Expanded( - child: RadioListTile( - title: const Text('一次性识别'), - value: false, - groupValue: controller.isContinuous.value, - onChanged: (value) { - if (value != null) { - controller.isContinuous.value = value; - } - }, - ), - ), - Expanded( - child: RadioListTile( - title: const Text('连续识别'), - value: true, - groupValue: controller.isContinuous.value, - onChanged: (value) { - if (value != null) { - controller.isContinuous.value = value; - } - }, - ), - ), - ], - )), - ], - ), - ), - ), - + _buildStatusCard(), const SizedBox(height: 16), - - // 录音控制 - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '录音控制', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Center( - child: Obx(() => controller.isListening.value - ? ElevatedButton.icon( - onPressed: () => controller.stopListening(), - icon: const Icon(Icons.stop), - label: const Text('停止录音'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - ), - ) - : ElevatedButton.icon( - onPressed: () => controller.startListening(), - icon: const Icon(Icons.mic), - label: const Text('开始录音'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - ), - ), - ), - ), - ], - ), + _buildRecognitionResultCard(), + const SizedBox(height: 16), + _buildControlButtons(context), + ], + ), + ), + ); + } + + // 构建状态卡片 + Widget _buildStatusCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '状态信息', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), ), - - const SizedBox(height: 16), - - // 识别结果 - Expanded( - child: Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - '识别结果', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - IconButton( - icon: const Icon(Icons.clear), - onPressed: () => controller.clearResults(), - tooltip: '清空结果', - ), - ], - ), - const SizedBox(height: 8), - Expanded( - child: Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(4.0), - ), - child: Obx(() => ListView.builder( - itemCount: controller.recognitionResults.length, - itemBuilder: (context, index) { - final result = controller.recognitionResults[index]; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${index + 1}. ', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - Expanded( - child: Text(result), - ), - ], - ), - ); - }, - )), - ), - ), - ], - ), + const SizedBox(height: 8), + Obx(() => Text('识别状态: ${_getStateText(controller.asrState.value)}')), + Obx(() => Text('是否正在监听: ${controller.isListening.value ? "是" : "否"}')), + Obx(() => Text('当前语言: ${controller.selectedLanguage.value}')), + ], + ), + ), + ); + } + + // 构建识别结果卡片 + Widget _buildRecognitionResultCard() { + return Expanded( + child: Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '识别结果', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), ), - ), - - const SizedBox(height: 16), - - // 状态显示 - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '状态', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Obx(() => Text( - controller.isListening.value - ? '正在录音...' - : '就绪', - style: TextStyle( - color: controller.isListening.value - ? Colors.green - : Colors.grey, - fontWeight: FontWeight.bold, + const SizedBox(height: 8), + const Text('部分识别结果:'), + Obx(() => Text( + controller.partialText.value, + style: const TextStyle(color: Colors.blue), + )), + const SizedBox(height: 16), + const Text('最终识别结果:'), + Expanded( + child: Obx(() => SingleChildScrollView( + child: Text( + controller.recognizedText.value, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), )), - const SizedBox(height: 8), - Obx(() => controller.errorMessage.value.isNotEmpty - ? Text( - '错误: ${controller.errorMessage.value}', - style: const TextStyle( - color: Colors.red, - ), - ) - : const SizedBox.shrink()), - ], - ), ), - ), - ], + ], + ), ), ), ); } + + // 构建控制按钮 + Widget _buildControlButtons(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: Obx(() => ElevatedButton( + onPressed: controller.isListening.value + ? null + : controller.startSimpleRecognition, + child: const Text('简单识别'), + )), + ), + const SizedBox(width: 16), + Expanded( + child: Obx(() => ElevatedButton( + onPressed: controller.toggleContinuousRecognition, + style: ElevatedButton.styleFrom( + backgroundColor: controller.isListening.value + ? Colors.red + : Theme.of(context).primaryColor, + ), + child: Text( + controller.isListening.value ? '停止连续识别' : '开始连续识别', + ), + )), + ), + ], + ); + } + + // 构建语言下拉菜单 + Widget _buildLanguageDropdown() { + return Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Obx(() => DropdownButton( + value: controller.selectedLanguage.value, + onChanged: controller.isListening.value + ? null + : (value) { + if (value != null) { + controller.changeLanguage(value); + } + }, + items: controller.availableLanguages + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + )), + ); + } + + // 获取状态文本 + String _getStateText(AzureAsrState state) { + switch (state) { + case AzureAsrState.notInitialized: + return '未初始化'; + case AzureAsrState.initialized: + return '已初始化'; + case AzureAsrState.listening: + return '正在监听'; + case AzureAsrState.notListening: + return '未监听'; + case AzureAsrState.error: + return '错误'; + default: + return '未知'; + } + } } \ No newline at end of file diff --git a/lib/modules/test/views/azure_tts_test_view.dart b/lib/modules/test/views/azure_tts_test_view.dart index 61b5760a5..c3e7fab31 100644 --- a/lib/modules/test/views/azure_tts_test_view.dart +++ b/lib/modules/test/views/azure_tts_test_view.dart @@ -11,23 +11,12 @@ class AzureTtsTestView extends GetView { WidgetsBinding.instance.addPostFrameCallback((_) { // 在下一帧渲染完成后调用 controller.updateUIState(); - - // 尝试从环境变量初始化 - controller.initialize(); }); return Scaffold( appBar: AppBar( - title: const Text('Azure 语音合成测试'), + title: const Text('Azure语音合成测试'), centerTitle: true, - actions: [ - // 添加初始化按钮 - IconButton( - icon: const Icon(Icons.refresh), - tooltip: '从环境变量初始化', - onPressed: () => controller.initialize(), - ), - ], ), body: SingleChildScrollView( child: Padding( @@ -35,6 +24,11 @@ class AzureTtsTestView extends GetView { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // 语音选择 + _buildVoiceSelectionCard(), + + const SizedBox(height: 16), + // 播放控制 Card( child: Padding( @@ -162,51 +156,6 @@ class AzureTtsTestView extends GetView { const SizedBox(height: 16), - // 语音选择 - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - '语音选择', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: controller.availableVoices.map((voice) { - final id = voice['id'] ?? ''; - final name = voice['name'] ?? ''; - return Obx(() => ChoiceChip( - label: Text(name), - selected: controller.currentVoice.value == id, - onSelected: (selected) { - if (selected) { - controller.setVoice(id); - } - }, - selectedColor: Colors.blue, - labelStyle: TextStyle( - color: controller.currentVoice.value == id - ? Colors.white - : Colors.black, - ), - )); - }).toList(), - ), - ], - ), - ), - ), - - const SizedBox(height: 16), - // 状态显示 Card( child: Padding( @@ -277,4 +226,78 @@ class AzureTtsTestView extends GetView { ), ); } + + // 构建语音选择卡片 + Widget _buildVoiceSelectionCard() { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '语音选择', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + + // 语音选择 + Obx(() { + if (controller.isLoadingVoices.value) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ), + ); + } + + if (controller.availableVoices.isEmpty) { + return const Padding( + padding: EdgeInsets.all(8.0), + child: Text('未找到可用的语音', style: TextStyle(color: Colors.red)), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('选择语音:', style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: controller.availableVoices.length, + itemBuilder: (context, index) { + final voice = controller.availableVoices[index]; + final isSelected = controller.selectedVoice.value == voice; + + return RadioListTile( + title: Text(voice), + subtitle: Text(voice), + value: voice, + groupValue: controller.selectedVoice.value, + onChanged: (value) { + if (value != null) { + controller.changeVoice(value); + } + }, + selected: isSelected, + activeColor: Colors.blue, + dense: true, + ); + }, + ), + ], + ); + }), + ], + ), + ), + ); + } } \ No newline at end of file diff --git a/lib/modules/translation/bindings/translation_binding.dart b/lib/modules/translation/bindings/translation_binding.dart index d8e9adbc8..549ff6783 100644 --- a/lib/modules/translation/bindings/translation_binding.dart +++ b/lib/modules/translation/bindings/translation_binding.dart @@ -1,11 +1,18 @@ import 'package:get/get.dart'; import '../controllers/translation_controller.dart'; +import '../../../data/services/azure_asr_service.dart'; +import '../../../data/services/azure_tts_service.dart'; +import '../../../data/services/volcano_translation_service.dart'; class TranslationBinding implements Bindings { @override void dependencies() { + // 注册服务 + Get.put(AzureAsrService(), permanent: true); + Get.put(AzureTtsService(), permanent: true); + Get.put(VolcanoTranslationService(), permanent: true); - // Register the controller + // 注册控制器 Get.put(TranslationController()); } } \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index f6eebe855..96aedb2cb 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -3,9 +3,9 @@ import 'package:get/get.dart'; import 'dart:async'; import 'dart:convert'; import 'package:get_storage/get_storage.dart'; -import '../../../data/services/azure_asr_service.dart'; import '../../../data/services/volcano_translation_service.dart'; -import '../../../data/services/volcano_tts_api_service.dart'; +import '../../../data/services/azure_asr_service.dart'; +import '../../../data/services/azure_tts_service.dart'; import '../../../core/utils/logger.dart'; class TranslationController extends GetxController { @@ -13,7 +13,7 @@ class TranslationController extends GetxController { final AzureAsrService _asrService = Get.find(); final VolcanoTranslationService _translationService = Get.find(); - final VolcanoTtsApiService _ttsService = Get.find(); + final AzureTtsService _ttsService = Get.find(); final GetStorage _storage = GetStorage(); // Storage key @@ -86,6 +86,12 @@ class TranslationController extends GetxController { // Initialize the translation service _initTranslationService(); + // 初始化 Azure ASR 服务 + _initAzureAsrService(); + + // 监听部分识别结果和识别状态 + _setupListeners(); + // 添加后处理,确保在历史记录加载后滚动到底部 WidgetsBinding.instance.addPostFrameCallback((_) { // 延迟执行滚动操作,确保视图已完全渲染 @@ -93,9 +99,6 @@ class TranslationController extends GetxController { _scrollToBottom(); }); }); - - // Start voice recognition automatically when the page is loaded - startRecognition(); } @override @@ -120,6 +123,20 @@ class TranslationController extends GetxController { } } + // 初始化 Azure ASR 服务 + Future _initAzureAsrService() async { + try { + // 设置识别语言 + final sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh'; + final recognitionLanguage = _getRecognitionLanguage(sourceCode); + + await _asrService.initialize(language: recognitionLanguage); + Logger.info('Azure ASR 服务初始化成功'); + } catch (e) { + Logger.error('初始化 Azure ASR 服务失败: $e'); + } + } + // Toggle TTS feature void toggleTts() { isTtsEnabled.toggle(); @@ -144,59 +161,69 @@ class TranslationController extends GetxController { isFirstInSession: true, // 标记为会话中的第一条 ); - // Start the voice recognition service - final success = await _asrService.startContinuousRecognition(); - - if (success) { - isRecognizing.value = true; + // 设置识别语言 + final sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh'; + final recognitionLanguage = _getRecognitionLanguage(sourceCode); + + // 确保使用正确的语言 + await _asrService.setRecognitionLanguage(recognitionLanguage); + + // 开始连续语音识别 + final recognitionStream = await _asrService.startContinuousRecognition(); + isRecognizing.value = true; + + // Reset the current item + if (currentItem.value != null) { + currentItem.value!.sourceText = ''; + currentItem.value!.translatedText = ''; + } - // Reset the current item - if (currentItem.value != null) { - currentItem.value!.sourceText = ''; - currentItem.value!.translatedText = ''; + // 监听识别结果 + _recognitionSubscription?.cancel(); + _recognitionSubscription = recognitionStream.listen((event) { + if (event.type == RecognitionEventType.finalResult && event.text.isNotEmpty) { + handleFinalResult(event.text); + } else if (event.type == RecognitionEventType.intermediateResult && event.text.isNotEmpty) { + handleIntermediateResult(event.text); + } else if (event.type == RecognitionEventType.error) { + isRecognizing.value = false; + Logger.error('语音识别错误: ${event.error}'); + } else if (event.type == RecognitionEventType.canceled) { + isRecognizing.value = false; + Logger.error('语音识别取消: ${event.error}'); } - - // Listen for recognition events - _recognitionSubscription = - _asrService.recognitionStream?.listen((event) { - switch (event.type) { - case RecognitionEventType.finalResult: - if (event.text.isNotEmpty) { - handleFinalResult(event.text); - } - break; - case RecognitionEventType.recognizing: - // Update the UI with intermediate results - if (event.text.isNotEmpty) { - handleIntermediateResult(event.text); - } - break; - case RecognitionEventType.error: - // 错误处理,停止语音识别 - Logger.error('语音识别错误: ${event.error}'); - - // 停止语音识别 - isRecognizing.value = false; - _asrService.stopContinuousRecognition(); - - break; - default: - break; - } - }); - } else { - throw Exception('Failed to start continuous recognition'); - } + }); } catch (e) { isRecognizing.value = false; Get.snackbar( 'Error', - 'Failed to start voice recognition: ${e.toString()}', + '启动语音识别失败: ${e.toString()}', snackPosition: SnackPosition.BOTTOM, ); } } + // 将语言代码转换为识别语言代码 + String _getRecognitionLanguage(String languageCode) { + // 语言代码到识别语言代码的映射 + final Map languageToRecognitionCode = { + 'zh': 'zh-CN', + 'en': 'en-US', + 'ja': 'ja-JP', + 'ko': 'ko-KR', + 'fr': 'fr-FR', + 'de': 'de-DE', + 'es': 'es-ES', + 'it': 'it-IT', + 'ru': 'ru-RU', + 'pt': 'pt-BR', + 'ar': 'ar-EG', + 'th': 'th-TH', + }; + + return languageToRecognitionCode[languageCode] ?? 'zh-CN'; + } + // Stop voice recognition Future stopRecognition() async { if (!isRecognizing.value) return; @@ -248,14 +275,24 @@ class TranslationController extends GetxController { // 防抖处理翻译请求 _translationDebounceTimer?.cancel(); _translationDebounceTimer = Timer(const Duration(milliseconds: 300), () { - if (currentItem.value != null && - currentItem.value!.sourceText.isNotEmpty) { - translateText(currentItem.value!.sourceText); - } + translateCurrentItem(isIntermediate: true); }); + } - // 滚动到底部显示最新内容 - _scrollToBottom(); + // 翻译当前项 + Future translateCurrentItem({bool isIntermediate = false}) async { + if (currentItem.value == null || currentItem.value!.sourceText.isEmpty) { + return; + } + + final sourceText = currentItem.value!.sourceText; + final translationResult = await translateText(sourceText); + + if (translationResult != null && isIntermediate) { + // 如果是中间结果,不播放TTS + // 滚动到底部显示最新内容 + _scrollToBottom(); + } } // Handle final recognition result @@ -386,12 +423,41 @@ class TranslationController extends GetxController { if (text.isEmpty) return; try { - await _ttsService.speakOnce(text); + // 获取目标语言代码 + final targetCode = supportedLanguages[targetLanguage.value] ?? 'en'; + + // 设置合适的语音 + final voiceName = _getTtsVoiceName(targetCode); + await _ttsService.setVoice(voiceName); + + // 播放文本 + await _ttsService.speak(text); } catch (e) { Get.snackbar('Error', '播放翻译失败: ${e.toString()}', snackPosition: SnackPosition.BOTTOM); } } + + // 获取TTS语音名称 + String _getTtsVoiceName(String languageCode) { + // 语言代码到TTS语音名称的映射 + final Map languageToVoiceName = { + 'zh': 'zh-CN-XiaoxiaoNeural', + 'en': 'en-US-AriaNeural', + 'ja': 'ja-JP-NanamiNeural', + 'ko': 'ko-KR-SunHiNeural', + 'fr': 'fr-FR-DeniseNeural', + 'de': 'de-DE-KatjaNeural', + 'es': 'es-ES-ElviraNeural', + 'it': 'it-IT-ElsaNeural', + 'ru': 'ru-RU-SvetlanaNeural', + 'pt': 'pt-BR-FranciscaNeural', + 'ar': 'ar-EG-SalmaNeural', + 'th': 'th-TH-PremwadeeNeural', + }; + + return languageToVoiceName[languageCode] ?? 'en-US-AriaNeural'; + } // 停止所有操作(语音识别和TTS) Future stopAll() async { @@ -510,6 +576,13 @@ class TranslationController extends GetxController { translationHistory.clear(); _storage.remove(_historyKey); } + + // 设置监听器 + void _setupListeners() { + // 在新版本中,我们通过recognitionStream直接监听事件 + // 这部分逻辑已经在startRecognition方法中实现 + Logger.info('语音识别事件监听已设置'); + } } // Model class for translation items diff --git a/pubspec.yaml b/pubspec.yaml index d84ab6466..dc4fe6542 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: crypto: ^3.0.3 flutter_tts: ^4.2.2 logger: ^2.0.2+1 + # azure_speech_recognition_null_safety: ^0.9.6 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git @@ -41,6 +42,7 @@ dependencies: share_plus: ^10.0.0 image_picker: ^1.1.2 speech_to_text: ^7.0.0 + audio_waveforms: ^1.3.0 dev_dependencies: flutter_test: