From e07319274b77523f6f9228fd81a36d294b54aa78 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Tue, 18 Mar 2025 09:59:57 +0000 Subject: [PATCH 1/2] add flutter speech --- lib/core/bindings/initial_binding.dart | 13 +- lib/core/themes/theme_service.dart | 24 +- lib/core/utils/app_lifecycle_manager.dart | 2 - lib/data/services/asr_service.dart | 105 ++ .../services/audio_processing_service.dart | 110 -- lib/data/services/audio_service.dart | 156 --- .../services/azure_flutter_tts_service.dart | 793 ------------- lib/data/services/azure_tts_service.dart | 491 -------- .../services/background_agent_service.dart | 377 ------- lib/data/services/flutter_asr_service.dart | 142 --- lib/data/services/flutter_tts_service.dart | 367 +----- lib/data/services/my_audio_handler.dart | 352 ------ lib/data/services/notification_service.dart | 224 ---- lib/data/services/speech_factory.dart | 113 ++ .../{ => speech_impl}/azure_asr_service.dart | 123 +- .../speech_impl/azure_tts_service.dart | 570 ++++++++++ .../speech_impl/flutter_asr_service.dart | 439 ++++++++ .../speech_impl/flutter_tts_service.dart | 484 ++++++++ .../speech_impl/volcano_asr_api_service.dart | 807 +++++++++++++ .../volcano_tts_api_service.dart | 2 +- lib/data/services/tts_service.dart | 83 ++ .../services/voice_interaction_service.dart | 6 +- .../services/volcano_asr_api_service.dart | 1003 ----------------- lib/data/services/volcano_tts_service.dart | 743 ------------ lib/modules/chat/bindings/chat_binding.dart | 22 +- .../chat/controllers/chat_controller.dart | 17 +- lib/modules/chat/views/chat_view.dart | 2 +- lib/modules/profile/views/profile_view.dart | 1 - .../controllers/settings_controller.dart | 39 +- lib/modules/settings/views/settings_view.dart | 483 ++++++-- .../test/bindings/azure_asr_test_binding.dart | 16 - .../test/bindings/azure_tts_test_binding.dart | 10 - .../bindings/flutter_asr_test_binding.dart | 16 - .../bindings/flutter_tts_test_binding.dart | 10 - lib/modules/test/bindings/test_binding.dart | 25 - .../bindings/volcano_asr_test_binding.dart | 10 - .../azure_asr_test_controller.dart | 298 ----- .../azure_tts_test_controller.dart | 279 ----- .../flutter_asr_test_controller.dart | 154 --- .../flutter_tts_test_controller.dart | 163 --- .../volcano_asr_test_controller.dart | 218 ---- .../volcano_tts_test_controller.dart | 194 ---- .../test/views/azure_asr_test_view.dart | 173 --- .../test/views/azure_tts_test_view.dart | 303 ----- .../test/views/flutter_asr_test_view.dart | 476 -------- .../test/views/flutter_tts_test_view.dart | 334 ------ .../test/views/volcano_asr_test_view.dart | 234 ---- .../test/views/volcano_tts_test_view.dart | 224 ---- .../bindings/translation_binding.dart | 10 +- .../controllers/translation_controller.dart | 14 +- lib/routes/app_pages.dart | 53 +- pubspec.yaml | 1 + 52 files changed, 3087 insertions(+), 8221 deletions(-) create mode 100644 lib/data/services/asr_service.dart delete mode 100644 lib/data/services/audio_processing_service.dart delete mode 100644 lib/data/services/audio_service.dart delete mode 100644 lib/data/services/azure_flutter_tts_service.dart delete mode 100644 lib/data/services/azure_tts_service.dart delete mode 100644 lib/data/services/background_agent_service.dart delete mode 100644 lib/data/services/flutter_asr_service.dart delete mode 100644 lib/data/services/my_audio_handler.dart delete mode 100644 lib/data/services/notification_service.dart create mode 100644 lib/data/services/speech_factory.dart rename lib/data/services/{ => speech_impl}/azure_asr_service.dart (78%) create mode 100644 lib/data/services/speech_impl/azure_tts_service.dart create mode 100644 lib/data/services/speech_impl/flutter_asr_service.dart create mode 100644 lib/data/services/speech_impl/flutter_tts_service.dart create mode 100644 lib/data/services/speech_impl/volcano_asr_api_service.dart rename lib/data/services/{ => speech_impl}/volcano_tts_api_service.dart (99%) create mode 100644 lib/data/services/tts_service.dart delete mode 100644 lib/data/services/volcano_asr_api_service.dart delete mode 100644 lib/data/services/volcano_tts_service.dart delete mode 100644 lib/modules/test/bindings/azure_asr_test_binding.dart delete mode 100644 lib/modules/test/bindings/azure_tts_test_binding.dart delete mode 100644 lib/modules/test/bindings/flutter_asr_test_binding.dart delete mode 100644 lib/modules/test/bindings/flutter_tts_test_binding.dart delete mode 100644 lib/modules/test/bindings/test_binding.dart delete mode 100644 lib/modules/test/bindings/volcano_asr_test_binding.dart delete mode 100644 lib/modules/test/controllers/azure_asr_test_controller.dart delete mode 100644 lib/modules/test/controllers/azure_tts_test_controller.dart delete mode 100644 lib/modules/test/controllers/flutter_asr_test_controller.dart delete mode 100644 lib/modules/test/controllers/flutter_tts_test_controller.dart delete mode 100644 lib/modules/test/controllers/volcano_asr_test_controller.dart delete mode 100644 lib/modules/test/controllers/volcano_tts_test_controller.dart delete mode 100644 lib/modules/test/views/azure_asr_test_view.dart delete mode 100644 lib/modules/test/views/azure_tts_test_view.dart delete mode 100644 lib/modules/test/views/flutter_asr_test_view.dart delete mode 100644 lib/modules/test/views/flutter_tts_test_view.dart delete mode 100644 lib/modules/test/views/volcano_asr_test_view.dart delete mode 100644 lib/modules/test/views/volcano_tts_test_view.dart diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index e6ec09174..1dc794dfd 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -1,6 +1,5 @@ import 'package:get/get.dart'; -import '../../data/services/azure_asr_service.dart'; -import '../../data/services/azure_tts_service.dart'; +import '../../data/services/speech_factory.dart'; import '../../data/services/chat_history_service.dart'; import '../../data/services/volcano_translation_service.dart'; import '../../data/services/voice_interaction_service.dart'; @@ -18,11 +17,9 @@ class InitialBinding extends Bindings { // 语言管理器(需要最先初始化) Get.put(LanguageManager(), permanent: true); - // Azure 语音识别服务 - Get.lazyPut(() => AzureAsrService(), fenix: true); - - // Azure TTS 服务 (原生实现) - Get.lazyPut(() => AzureTtsService(), fenix: true); + //初始化语音工厂 + Get.put(SpeechFactory(), permanent: true); + Get.find().initialize(initialType: SpeechServiceType.azure); // 历史记录服务 Get.lazyPut(() => ChatHistoryService(), fenix: true); @@ -38,5 +35,7 @@ class InitialBinding extends Bindings { // 经典蓝牙服务 Get.lazyPut(() => ClassicBluetoothService(), fenix: true); + + } } \ No newline at end of file diff --git a/lib/core/themes/theme_service.dart b/lib/core/themes/theme_service.dart index f604ff2bf..fd438f9d8 100644 --- a/lib/core/themes/theme_service.dart +++ b/lib/core/themes/theme_service.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import '../../core/utils/logger.dart'; class ThemeService { final _box = GetStorage(); @@ -9,20 +10,29 @@ class ThemeService { /// 获取主题模式 ThemeMode getThemeMode() { final isDarkMode = isSavedDarkMode(); - print('获取保存的主题: ${isDarkMode ? "暗黑" : "浅色"}'); + Logger.info('获取保存的主题: ${isDarkMode ? "暗黑" : "浅色"}'); return isDarkMode ? ThemeMode.dark : ThemeMode.light; } /// 是否是暗黑模式 bool isSavedDarkMode() { - final value = _box.read(_key); - print('从存储读取主题设置: $value'); - return value ?? false; + try { + final value = _box.read(_key); + Logger.info('从存储读取主题设置: $value'); + return value ?? false; + } catch (e) { + Logger.error('读取主题设置失败: $e'); + return false; + } } /// 保存主题模式到本地存储 - void saveThemeMode(bool isDarkMode) { - print('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}'); - _box.write(_key, isDarkMode); + Future saveThemeMode(bool isDarkMode) async { + try { + Logger.info('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}'); + await _box.write(_key, isDarkMode); + } catch (e) { + Logger.error('保存主题设置失败: $e'); + } } } \ No newline at end of file diff --git a/lib/core/utils/app_lifecycle_manager.dart b/lib/core/utils/app_lifecycle_manager.dart index 1f09f6dbb..f1ea2b0b8 100644 --- a/lib/core/utils/app_lifecycle_manager.dart +++ b/lib/core/utils/app_lifecycle_manager.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../../data/services/volcano_asr_api_service.dart'; -import '../../data/services/azure_flutter_tts_service.dart'; import 'logger.dart'; /// 应用生命周期管理器 diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart new file mode 100644 index 000000000..8e18e3433 --- /dev/null +++ b/lib/data/services/asr_service.dart @@ -0,0 +1,105 @@ +import 'dart:async'; + +/// 语音识别服务接口 +abstract class AsrService { + /// 支持的语言 + List get supportedLanguages; + + /// 初始化语音识别服务 + Future initialize({ + List? supportedLanguages, + }); + + /// 执行一次性语音识别 + /// + /// 返回识别的最终结果 + Future recognizeOnce(); + + /// 开始连续语音识别 + /// + /// 返回一个包含识别事件的流 + Future> startContinuousRecognition(); + + /// 停止连续语音识别 + Future stopContinuousRecognition(); + + /// 检查连续识别是否活跃 + bool isContinuousRecognitionActive(); + + /// 释放资源 + Future dispose(); +} + +/// 识别事件类型 +enum RecognitionEventType { + /// 最终识别结果 + finalResult, + + /// 中间识别结果(实时反馈) + intermediateResult, + + /// 会话开始 + sessionStarted, + + /// 会话结束 + sessionStopped, + + /// 识别取消 + canceled, + + /// 识别错误 + error, +} + +/// 识别事件 +class RecognitionEvent { + /// 事件类型 + final RecognitionEventType type; + + /// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效) + final String text; + + /// 检测到的语言 + final String detectedLanguage; + + /// 错误信息(仅在 error 和 canceled 类型中有效) + final String error; + + RecognitionEvent({ + required this.type, + this.text = '', + this.detectedLanguage = '', + this.error = '', + }); + + /// 创建最终结果事件的快捷构造函数 + factory RecognitionEvent.finalResult({ + required String text, + String detectedLanguage = '', + }) { + return RecognitionEvent( + type: RecognitionEventType.finalResult, + text: text, + detectedLanguage: detectedLanguage, + ); + } + + /// 创建错误事件的快捷构造函数 + factory RecognitionEvent.error(String errorMessage) { + return RecognitionEvent( + type: RecognitionEventType.error, + error: errorMessage, + ); + } + + /// 检查是否为最终结果 + bool get isFinalResult => type == RecognitionEventType.finalResult; + + /// 检查是否为错误 + bool get isError => type == RecognitionEventType.error || type == RecognitionEventType.canceled; + + @override + String toString() { + return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}'; + } +} \ No newline at end of file diff --git a/lib/data/services/audio_processing_service.dart b/lib/data/services/audio_processing_service.dart deleted file mode 100644 index 640f1f854..000000000 --- a/lib/data/services/audio_processing_service.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'dart:async'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import '../../core/utils/logger.dart'; - -/// 音频处理服务 -/// -/// 提供回音消除、降噪和语音活动检测等功能 -class AudioProcessingService extends GetxService { - static const MethodChannel _channel = MethodChannel('com.deep_voice.audio_processing'); - - // 状态变量 - final _isEchoCancellationEnabled = false.obs; - final _isNoiseSuppressionEnabled = false.obs; - final _isVoiceActivityDetectionEnabled = false.obs; - - // 获取可观察状态 - RxBool get isEchoCancellationEnabled => _isEchoCancellationEnabled; - RxBool get isNoiseSuppressionEnabled => _isNoiseSuppressionEnabled; - RxBool get isVoiceActivityDetectionEnabled => _isVoiceActivityDetectionEnabled; - - AudioProcessingService() { - _initPlatformState(); - } - - /// 初始化平台状态 - Future _initPlatformState() async { - try { - final bool isEchoCancellationAvailable = await _channel.invokeMethod('isEchoCancellationAvailable') ?? false; - final bool isNoiseSuppressionAvailable = await _channel.invokeMethod('isNoiseSuppressionAvailable') ?? false; - - Logger.info('回音消除可用: $isEchoCancellationAvailable'); - Logger.info('噪声抑制可用: $isNoiseSuppressionAvailable'); - } catch (e) { - Logger.error('初始化音频处理服务失败: $e'); - } - } - - /// 为指定的音频会话ID启用回音消除 - Future enableEchoCancellation(int audioSessionId) async { - try { - final bool result = await _channel.invokeMethod('enableEchoCancellation', { - 'audioSessionId': audioSessionId, - }) ?? false; - - _isEchoCancellationEnabled.value = result; - Logger.info('启用回音消除${result ? "成功" : "失败"}'); - return result; - } catch (e) { - Logger.error('启用回音消除失败: $e'); - return false; - } - } - - /// 禁用回音消除 - Future disableEchoCancellation() async { - try { - final bool result = await _channel.invokeMethod('disableEchoCancellation') ?? false; - - _isEchoCancellationEnabled.value = !result; - Logger.info('禁用回音消除${result ? "成功" : "失败"}'); - return result; - } catch (e) { - Logger.error('禁用回音消除失败: $e'); - return false; - } - } - - /// 为指定的音频会话ID启用噪声抑制 - Future enableNoiseSuppression(int audioSessionId) async { - try { - final bool result = await _channel.invokeMethod('enableNoiseSuppression', { - 'audioSessionId': audioSessionId, - }) ?? false; - - _isNoiseSuppressionEnabled.value = result; - Logger.info('启用噪声抑制${result ? "成功" : "失败"}'); - return result; - } catch (e) { - Logger.error('启用噪声抑制失败: $e'); - return false; - } - } - - /// 禁用噪声抑制 - Future disableNoiseSuppression() async { - try { - final bool result = await _channel.invokeMethod('disableNoiseSuppression') ?? false; - - _isNoiseSuppressionEnabled.value = !result; - Logger.info('禁用噪声抑制${result ? "成功" : "失败"}'); - return result; - } catch (e) { - Logger.error('禁用噪声抑制失败: $e'); - return false; - } - } - - /// 获取当前录音的音频会话ID - Future getAudioSessionId() async { - try { - final int? sessionId = await _channel.invokeMethod('getAudioSessionId'); - Logger.info('获取音频会话ID: $sessionId'); - return sessionId; - } catch (e) { - Logger.error('获取音频会话ID失败: $e'); - return null; - } - } -} \ No newline at end of file diff --git a/lib/data/services/audio_service.dart b/lib/data/services/audio_service.dart deleted file mode 100644 index a9ad58a4c..000000000 --- a/lib/data/services/audio_service.dart +++ /dev/null @@ -1,156 +0,0 @@ -import 'dart:async'; -import 'package:audio_service/audio_service.dart'; -import 'package:get/get.dart'; -import 'package:flutter/material.dart'; -import 'package:synchronized/synchronized.dart'; -import 'my_audio_handler.dart'; - -class AudioServiceManager extends GetxService { - static AudioServiceManager get to => Get.find(); - MyAudioHandler? _audioHandler; - bool _isInitialized = false; - final _lock = Lock(); - - // 添加一个标志,表示是否已经尝试过初始化 - bool _hasAttemptedInit = false; - - // 添加一个计时器,用于定期检查服务状态 - Timer? _serviceCheckTimer; - - Future init() async { - print('初始化 AudioServiceManager'); - - if (_isInitialized) { - print('AudioServiceManager 已经初始化'); - return this; - } - - // 如果已经尝试过初始化但失败了,不再重试 - if (_hasAttemptedInit) { - print('AudioServiceManager 之前初始化失败,尝试重新初始化'); - // 重置标志,允许重新初始化 - _hasAttemptedInit = false; - } - - _hasAttemptedInit = true; - - try { - // 使用锁确保初始化的原子性 - await _lock.synchronized(() async { - if (_isInitialized) return; - - // 使用 AudioService.init 初始化音频服务 - _audioHandler = await AudioService.init( - builder: () => MyAudioHandler(), - config: AudioServiceConfig( - androidNotificationChannelId: 'com.example.deep_voice.channel.audio', - androidNotificationChannelName: 'Deep Voice Audio Service', - // 使用应用图标作为通知图标,确保这个图标存在 - androidNotificationIcon: 'mipmap/ic_launcher', - // 修复配置冲突:当androidStopForegroundOnPause为true时,androidNotificationOngoing才能为true - androidNotificationOngoing: false, - // 设置为true,允许在暂停时停止前台服务,避免与androidNotificationOngoing冲突 - androidStopForegroundOnPause: true, - androidShowNotificationBadge: true, - notificationColor: Colors.blue, - fastForwardInterval: const Duration(seconds: 10), - rewindInterval: const Duration(seconds: 10), - preloadArtwork: false, - ), - ); - - // 注册自定义音频处理器 - await Get.put(_audioHandler!, permanent: true); - - _isInitialized = true; - print('AudioServiceManager 初始化成功'); - - // 启动定期检查服务状态的计时器 - _startServiceCheckTimer(); - }); - - return this; - } catch (e) { - print('AudioServiceManager初始化失败: $e'); - _isInitialized = false; // 确保失败时重置状态 - - // 延迟5秒后尝试重新初始化 - Future.delayed(Duration(seconds: 5), () { - print('尝试重新初始化 AudioServiceManager'); - _hasAttemptedInit = false; - init(); - }); - - return this; // 即使失败也返回实例,避免空指针异常 - } - } - - // 启动定期检查服务状态的计时器 - void _startServiceCheckTimer() { - _serviceCheckTimer?.cancel(); - _serviceCheckTimer = Timer.periodic(Duration(minutes: 5), (timer) { - _checkAndRestartServiceIfNeeded(); - }); - } - - // 检查服务状态,如果需要则重启服务 - Future _checkAndRestartServiceIfNeeded() async { - if (!_isInitialized || _audioHandler == null) { - print('服务检查:AudioServiceManager 未初始化,尝试重新初始化'); - _hasAttemptedInit = false; - await init(); - return; - } - print('服务检查:AudioServiceManager 已初始化,服务状态: $_isInitialized'); - } - - // 获取音频处理器 - MyAudioHandler? get audioHandler => _audioHandler; - - // 发送媒体按钮事件 - Future sendMediaButtonEvent() async { - if (_audioHandler != null) { - try { - print('发送媒体按钮事件'); - await _audioHandler!.customAction('media_button'); - } catch (e) { - print('发送媒体按钮事件失败: $e'); - - // 如果发送失败,尝试重新初始化服务 - _isInitialized = false; - _hasAttemptedInit = false; - await init(); - } - } else { - print('AudioHandler 未初始化,无法发送媒体按钮事件'); - // 尝试重新初始化 - _isInitialized = false; - _hasAttemptedInit = false; - await init(); - } - } - - // 手动触发媒体按钮事件(用于测试) - Future triggerMediaButtonEvent() async { - print('手动触发媒体按钮事件'); - await sendMediaButtonEvent(); - } - - @override - void onClose() async { - _isInitialized = false; - _serviceCheckTimer?.cancel(); - - // 停止现有的音频处理器 - if (_audioHandler != null) { - try { - await _audioHandler!.stop(); - } catch (e) { - print('停止 AudioHandler 失败: $e'); - } - _audioHandler = null; - } - - super.onClose(); - } -} \ 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 deleted file mode 100644 index e1c9c146c..000000000 --- a/lib/data/services/azure_flutter_tts_service.dart +++ /dev/null @@ -1,793 +0,0 @@ -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 deleted file mode 100644 index 4cd000f22..000000000 --- a/lib/data/services/azure_tts_service.dart +++ /dev/null @@ -1,491 +0,0 @@ -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; -} - -/// 音频输出设备类型 -enum AudioOutputType { - /// 扬声器 - speaker, - - /// 听筒 - earpiece, - - /// 自动选择(有耳机用耳机,没有则用听筒) - auto -} - -/// 微软 Text-to-Speech 服务 -/// -/// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互, -/// 提供文本转语音功能。 -class AzureTtsService extends GetxService { - static final AzureTtsService to = Get.put(AzureTtsService()); - 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; - AudioOutputType _currentAudioOutputType = AudioOutputType.auto; - - // 语音合成队列 - final List<_SpeechItem> _textQueue = []; - bool _isProcessingQueue = false; - bool _isSpeaking = false; - - // 流式输出缓冲区 - String _streamBuffer = ''; - - // 可观察状态 - final isEnabled = true.obs; - final isSpeaking = false.obs; - final audioOutputType = AudioOutputType.auto.obs; - - String get currentVoice => _currentVoice; - AudioOutputType get currentAudioOutputType => _currentAudioOutputType; - - AzureTtsService() { - _loadConfig(); - } - - /// 从环境变量加载配置 - void _loadConfig() { - _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; - _serviceRegion = dotenv.env['AZURE_SPEECH_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, - 'region': _serviceRegion, - 'language': language, - }); - - _isInitialized = result; - - // 确保设置默认的音频输出设备类型 - if (result) { - // 不需要显式调用 setAudioOutputType,因为原生层已经设置了默认值 - // 只需要确保 Dart 层的状态与原生层一致 - _currentAudioOutputType = AudioOutputType.auto; - audioOutputType.value = AudioOutputType.auto; - } - - return result; - } catch (e) { - Logger.error('初始化失败: $e'); - return false; - } - } - - /// 设置语音 - Future setVoice(String voiceName) async { - if (!_isInitialized) await initialize(); - if (voiceName == _currentVoice) return true; - - try { - final result = await _channel.invokeMethod('setVoice', { - 'voiceName': voiceName, - }); - - if (result) _currentVoice = voiceName; - return result; - } catch (e) { - Logger.error('设置语音失败: $e'); - return false; - } - } - - /// 设置音频输出设备类型 - Future setAudioOutputType(AudioOutputType outputType) async { - if (!_isInitialized) await initialize(); - - try { - final String outputTypeStr = outputType.toString().split('.').last; - final result = await _channel.invokeMethod('setAudioOutputType', { - 'outputType': outputTypeStr, - }); - - if (result) { - _currentAudioOutputType = outputType; - audioOutputType.value = outputType; - } - - 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, AudioOutputType? outputType}) async { - if (!isEnabled.value || text.isEmpty) return false; - - // 如果指定了新的音频输出设备类型,先设置 - if (outputType != null && outputType != _currentAudioOutputType) { - await setAudioOutputType(outputType); - } - - _textQueue.add(_SpeechItem( - text: text, - rate: rate ?? _currentRate, - pitch: pitch ?? _currentPitch, - volume: volume ?? _currentVolume, - )); - - if (!_isProcessingQueue) _processQueue(); - return true; - } - - /// 处理AI流式输出文本 - /// - /// 将流式输出的文本片段添加到缓冲区,并在检测到完整句子时将其送入播放队列 - Future processStreamText(String textChunk, {AudioOutputType? outputType}) async { - if (!isEnabled.value || textChunk.isEmpty) return false; - - // 如果指定了新的音频输出设备类型,先设置 - if (outputType != null && outputType != _currentAudioOutputType) { - await setAudioOutputType(outputType); - } - - // 添加文本到缓冲区 - _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({AudioOutputType? outputType}) async { - if (!isEnabled.value || _streamBuffer.isEmpty) return false; - - // 如果指定了新的音频输出设备类型,先设置 - if (outputType != null && outputType != _currentAudioOutputType) { - await setAudioOutputType(outputType); - } - - // 先尝试使用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, AudioOutputType? outputType}) async { - if (!isEnabled.value || texts.isEmpty) return false; - - // 如果指定了新的音频输出设备类型,先设置 - if (outputType != null && outputType != _currentAudioOutputType) { - await setAudioOutputType(outputType); - } - - 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 toggleAudioOutputType() async { - AudioOutputType newType; - - switch (_currentAudioOutputType) { - case AudioOutputType.speaker: - newType = AudioOutputType.earpiece; - break; - case AudioOutputType.earpiece: - newType = AudioOutputType.auto; - break; - case AudioOutputType.auto: - newType = AudioOutputType.speaker; - break; - } - - return await setAudioOutputType(newType); - } - - /// 释放资源 - 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 deleted file mode 100644 index f694791c6..000000000 --- a/lib/data/services/background_agent_service.dart +++ /dev/null @@ -1,377 +0,0 @@ -import 'dart:async'; -import 'package:get/get.dart'; -import 'package:flutter/widgets.dart'; -import '../../core/utils/logger.dart'; -import '../providers/agent_provider.dart'; -import '../models/agent.dart'; -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 'azure_flutter_tts_service.dart'; -import 'chat_history_service.dart'; - -/// 后台代理服务,用于在应用处于后台时处理AI交互 -class BackgroundAgentService extends GetxService { - // 单例实例 - static BackgroundAgentService? _instance; - - // 单例工厂方法 - factory BackgroundAgentService() { - _instance ??= BackgroundAgentService._internal(); - return _instance!; - } - - // 私有构造函数 - BackgroundAgentService._internal() { - Logger.info('BackgroundAgentService 已创建'); - } - - // 服务实例 - final VolcanoAsrApiService _asrService = Get.find(); - final VolcanoAIService _aiService = Get.find(); - final AzureFlutterTtsService _ttsService = Get.find(); - final ChatHistoryService _historyService = Get.find(); - - // 状态控制 - // isProcessing: 表示整个代理交互会话是否处于活跃状态(包括语音识别、AI处理和TTS播放) - final RxBool isProcessing = false.obs; - final RxString recognizedText = ''.obs; - - // 流式响应控制 - StreamSubscription? _recognitionSubscription; - StreamSubscription? _aiResponseSubscription; - - // 定时器,用于检测无活动超时 - Timer? _inactivityTimer; - - // 当前处理的Agent - Agent? _currentAgent; - - // 本地消息历史备份 - final List> _localMessageHistory = []; - - // 获取消息历史 - 优先从ChatController获取,否则使用本地存储 - List> get messageHistory { - if (Get.isRegistered()) { - final chatController = Get.find(); - if (chatController.agent?.id == 'personal_assistant') { - return chatController.messages.map((m) => { - 'role': m.role, - 'content': m.content, - }).toList(); - } - } - - if (_localMessageHistory.isEmpty) { - _loadChatHistory(); - } - - return _localMessageHistory; - } - - // 语音识别是否活跃 - bool get isListening => _recognitionSubscription != null; - - // TTS是否正在播放 - bool get isTtsSpeaking => _ttsService.isActuallyPlaying(); - - @override - void onClose() { - _cleanupResources(); - super.onClose(); - } - - /// 清理所有资源 - void _cleanupResources() { - _recognitionSubscription?.cancel(); - _aiResponseSubscription?.cancel(); - - if (_ttsService.isActuallyPlaying()) { - _ttsService.stop(); - } - - if (isListening) { - _asrService.stopRecognition(); - } - - _inactivityTimer?.cancel(); - } - - /// 开始代理交互 - Future startAgentInteraction(String agentId) async { - // 防止重复处理 - if (isProcessing.value) { - Logger.warning('已经在处理交互,忽略此次请求'); - return; - } - - // 先说"我在听" - _ttsService.speak("我在!"); - isProcessing.value = true; - - try { - // 获取Agent - _currentAgent = AgentProvider.getAgentById(agentId); - if (_currentAgent == null) { - Logger.error('无法获取Agent: $agentId'); - isProcessing.value = false; - return; - } - - // 加载该Agent的聊天历史 - _loadChatHistory(); - - // 启动语音识别 - await _startVoiceRecognition(); - - // 启动无活动定时器 - _startInactivityTimer(); - } catch (e) { - Logger.error('启动代理交互失败', e); - isProcessing.value = false; - } - } - - /// 停止语音识别 - Future _stopVoiceRecognition() async { - if (!isListening) return; - - await _recognitionSubscription?.cancel(); - _recognitionSubscription = null; - _asrService.stopRecognition(); - recognizedText.value = ''; - } - - /// 开始语音识别 - Future _startVoiceRecognition() async { - if (isListening) return; - - try { - final success = await _asrService.startRecognition(); - - if (success) { - _recognitionSubscription = _asrService.recognitionStream.listen( - (event) { - switch (event.type) { - case RecognitionEventType.recognizing: - 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.isEmpty) return; - - recognizedText.value = event.text; - Logger.info('处理语音识别最终结果:${event.text}'); - _processRecognizedText(event.text); - recognizedText.value = ''; - break; - - case RecognitionEventType.error: - Logger.info('语音识别错误: ${event.error}'); - stopInteraction(); - break; - - default: - break; - } - }, - onError: (error) { - Logger.info('语音识别错误:$error'); - stopInteraction(); - }, - ); - } else { - Logger.error('启动语音识别失败'); - } - } catch (e) { - Logger.error('启动语音识别失败', e); - isProcessing.value = false; - } - } - - /// 中断当前响应 - void _interruptCurrentResponse(String reason) { - Logger.info(reason); - - if (_ttsService.isActuallyPlaying()) { - _ttsService.stop(); - } - - if (_aiResponseSubscription != null) { - _aiResponseSubscription!.cancel(); - _aiResponseSubscription = null; - Logger.info('已中断AI响应流和TTS播放'); - } - } - - /// 处理识别到的文本 - Future _processRecognizedText(String text) async { - if (text.trim().isEmpty || _currentAgent == null) return; - - try { - // 中断任何正在进行的响应 - _interruptCurrentResponse("处理新的用户输入,中断当前响应"); - - // 添加用户消息到历史记录 - _addMessageToHistory('user', text); - - String fullResponse = ''; - - // 获取AI响应流 - final responseStream = _aiService.sendMessageStream( - messages: messageHistory, - systemPrompt: _currentAgent!.systemPrompt, - ); - - // 订阅响应流 - _aiResponseSubscription = responseStream.listen( - (chunk) { - fullResponse += chunk; - _ttsService.speak(chunk); - _resetInactivityTimer(); - }, - onDone: () { - _addMessageToHistory('assistant', fullResponse); - _aiResponseSubscription = null; - }, - onError: (error) { - Logger.error('AI响应错误', error); - const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。'; - _ttsService.speak(errorMessage); - _addMessageToHistory('assistant', errorMessage); - _aiResponseSubscription = null; - }, - ); - } catch (e) { - Logger.error('处理识别文本失败', e); - const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。'; - _ttsService.speak(errorMessage); - _addMessageToHistory('assistant', errorMessage); - } - } - - /// 重新启动语音识别 - Future _restartVoiceRecognition() async { - await _stopVoiceRecognition(); - await Future.delayed(Duration(milliseconds: 500)); - await _startVoiceRecognition(); - } - - /// 添加消息到历史记录 - void _addMessageToHistory(String role, String content) { - // 添加到本地历史记录 - _localMessageHistory.add({ - 'role': role, - 'content': content, - }); - - // 保持本地历史记录不超过10条消息 - if (_localMessageHistory.length > 10) { - _localMessageHistory.removeAt(0); - } - - // 同步消息到ChatController或保存到本地存储 - if (Get.isRegistered()) { - final chatController = Get.find(); - if (chatController.agent?.id == 'personal_assistant') { - chatController.addMessage(Message( - role: role, - content: content, - timestamp: DateTime.now(), - )); - } - } else { - _saveChatHistory(); - } - } - - // 加载聊天历史 - void _loadChatHistory() { - if (_currentAgent == null) return; - - try { - final messages = _historyService.loadHistory(_currentAgent!.id); - _localMessageHistory.clear(); - - for (final message in messages) { - _localMessageHistory.add({ - 'role': message.role, - 'content': message.content, - }); - } - } catch (e) { - Logger.error('加载聊天历史记录失败', e); - } - } - - // 保存聊天历史 - void _saveChatHistory() { - if (_currentAgent == null) return; - - try { - final messages = _localMessageHistory.map((m) => Message( - role: m['role']!, - content: m['content']!, - timestamp: DateTime.now(), - )).toList(); - - _historyService.saveHistory(_currentAgent!.id, messages); - } catch (e) { - Logger.error('保存聊天历史记录失败', e); - } - } - - /// 启动无活动定时器 - void _startInactivityTimer() { - _inactivityTimer?.cancel(); - _inactivityTimer = Timer(Duration(seconds: 30), () { - if (!_ttsService.isActuallyPlaying() && isProcessing.value) { - Logger.info('检测到30秒内无活动,停止语音识别'); - stopInteraction(); - } - }); - } - - /// 重置无活动定时器 - void _resetInactivityTimer() { - if (_inactivityTimer != null) { - _inactivityTimer!.cancel(); - _startInactivityTimer(); - } - } - - /// 响应新的媒体事件(双击媒体按钮) - Future respondToNewMediaEvent() async { - _ttsService.speak("我在!"); - - if (!isListening) { - await _restartVoiceRecognition(); - } - - _resetInactivityTimer(); - } - - /// 停止当前交互 - Future stopInteraction() async { - try { - _saveChatHistory(); - _cleanupResources(); - - isProcessing.value = false; - _currentAgent = null; - - Logger.info('已停止后台代理交互'); - } catch (e) { - Logger.error('停止交互失败', e); - } - } -} \ No newline at end of file diff --git a/lib/data/services/flutter_asr_service.dart b/lib/data/services/flutter_asr_service.dart deleted file mode 100644 index b44984ef7..000000000 --- a/lib/data/services/flutter_asr_service.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:get/get.dart'; -// 移除 speech_to_text 依赖 -// import 'package:speech_to_text/speech_to_text.dart'; -// import 'package:speech_to_text/speech_recognition_result.dart'; -import '../../core/utils/logger.dart'; - -/// ASR状态枚举 -enum AsrState { - notInitialized, // 未初始化 - initialized, // 已初始化 - listening, // 正在监听 - notListening, // 未监听 - error // 错误状态 -} - -/// 监听模式枚举(与 speech_to_text 兼容) -enum ListenMode { - deviceDefault, - dictation, - search, - confirmation, -} - -/// 本地名称类(与 speech_to_text 兼容) -class LocaleName { - final String name; - final String localeId; - - LocaleName(this.name, this.localeId); -} - -/// Flutter ASR服务,负责本地语音转文本功能 -/// 注意:此实现为空实现,不再依赖 speech_to_text 插件 -class FlutterAsrService extends GetxService { - // 当前ASR状态 - final Rx _asrState = AsrState.notInitialized.obs; - AsrState get asrState => _asrState.value; - - // 可用的语音识别语言 - final RxList _locales = [].obs; - List get locales => _locales; - - // 当前选择的语言 - final RxString _currentLocale = ''.obs; - String get currentLocale => _currentLocale.value; - - // 识别结果 - final RxString _lastWords = ''.obs; - String get lastWords => _lastWords.value; - - // 识别准确度 - final RxDouble _confidence = 0.0.obs; - double get confidence => _confidence.value; - - // 是否可用 - final RxBool _isAvailable = false.obs; - bool get isAvailable => _isAvailable.value; - - // 是否正在监听 - bool _isListening = false; - bool get isListening => _isListening; - - // 识别结果流 - final _recognitionResultsController = StreamController.broadcast(); - Stream get recognitionResults => _recognitionResultsController.stream; - - @override - void onInit() { - super.onInit(); - _initSpeech(); - } - - /// 初始化语音识别 - Future _initSpeech() async { - try { - // 空实现,不再使用 speech_to_text - _isAvailable.value = false; - _asrState.value = AsrState.error; - - // 添加一个默认语言 - _locales.add(LocaleName('中文 (简体)', 'zh-CN')); - _currentLocale.value = 'zh-CN'; - - Logger.info('FlutterAsrService 已初始化(空实现)'); - } catch (e) { - _asrState.value = AsrState.error; - _isAvailable.value = false; - Logger.error('语音识别初始化失败: $e'); - } - } - - /// 开始监听 - /// - /// [listenFor] 监听时长,默认30秒 - /// [pauseFor] 暂停时长,默认3秒,超过此时长无语音输入会自动停止 - /// [partialResults] 是否返回部分结果,默认true - /// [onDevice] 是否使用设备本地识别,默认false - /// [listenMode] 监听模式,默认设备默认模式 - Future startListening({ - Duration listenFor = const Duration(seconds: 30), - Duration pauseFor = const Duration(seconds: 3), - bool partialResults = true, - bool onDevice = false, - ListenMode listenMode = ListenMode.deviceDefault, - }) async { - // 空实现,不再使用 speech_to_text - Logger.error('FlutterAsrService 为空实现,不支持语音识别'); - return false; - } - - /// 停止监听 - Future stopListening() async { - // 空实现,不再使用 speech_to_text - _isListening = false; - _asrState.value = AsrState.notListening; - } - - /// 取消监听 - Future cancelListening() async { - // 空实现,不再使用 speech_to_text - _isListening = false; - _lastWords.value = ''; - _confidence.value = 0.0; - _asrState.value = AsrState.notListening; - } - - /// 设置当前语言 - void setLocale(String localeId) { - if (_currentLocale.value != localeId) { - _currentLocale.value = localeId; - Logger.info('设置语音识别语言: $localeId'); - } - } - - @override - void onClose() { - _recognitionResultsController.close(); - super.onClose(); - } -} \ No newline at end of file diff --git a/lib/data/services/flutter_tts_service.dart b/lib/data/services/flutter_tts_service.dart index 453218ebb..61de220ee 100644 --- a/lib/data/services/flutter_tts_service.dart +++ b/lib/data/services/flutter_tts_service.dart @@ -1,364 +1,3 @@ -import 'dart:io'; -import 'package:flutter_tts/flutter_tts.dart'; -import 'package:get/get.dart'; -import '../../core/utils/logger.dart'; - -/// Flutter TTS服务,负责本地文本转语音功能 -class FlutterTtsService extends GetxService { - late FlutterTts _flutterTts; - - // 当前TTS状态 - TtsState _ttsState = TtsState.stopped; - TtsState get ttsState => _ttsState; - - // 可用的语音 - List> _voices = []; - List> get voices => _voices; - - // 当前选择的语音 - String _currentVoice = ''; - String get currentVoice => _currentVoice; - set currentVoice(String voice) => _currentVoice = voice; - - // 语速 (0.1-2.0) - double _rate = 0.5; - double get rate => _rate; - set rate(double value) => _rate = value; - - // 音量 (0.0-1.0) - double _volume = 1.0; - double get volume => _volume; - set volume(double value) => _volume = value; - - // 音调 (0.5-2.0) - double _pitch = 1.0; - double get pitch => _pitch; - set pitch(double value) => _pitch = value; - - // 文本队列 - 仅包含待播放的文本 - List _textQueue = []; - List get textQueue => _textQueue; - - // 当前正在播放的文本 - String _currentText = ''; - String get currentText => _currentText; - - // 是否正在处理队列 - bool _isProcessingQueue = false; - - @override - void onInit() { - super.onInit(); - _initTts(); - } - - /// 初始化TTS引擎 - Future _initTts() async { - _flutterTts = FlutterTts(); - - // 设置完成回调 - _flutterTts.setCompletionHandler(() { - Logger.info('TTS播放完成: "$_currentText"'); - - // 清空当前播放文本 - _currentText = ''; - - // 处理队列中的下一条文本 - _processNextInQueue(); - - // 只有当队列为空且不再处理队列时才设置为stopped状态 - if (_textQueue.isEmpty && !_isProcessingQueue) { - _ttsState = TtsState.stopped; - Logger.info('TTS队列播放完成,设置为stopped状态'); - } - }); - - // 设置错误回调 - _flutterTts.setErrorHandler((error) { - Logger.error('Flutter TTS错误: $error'); - - // 清空当前播放文本 - _currentText = ''; - - // 尝试播放下一条文本 - _processNextInQueue(); - - // 只有当队列为空且不再处理队列时才设置为stopped状态 - if (_textQueue.isEmpty && !_isProcessingQueue) { - _ttsState = TtsState.stopped; - Logger.info('TTS队列播放出错且为空,设置为stopped状态'); - } - }); - - // 设置开始回调 - _flutterTts.setStartHandler(() { - _ttsState = TtsState.playing; - }); - - // 设置取消回调 - _flutterTts.setCancelHandler(() { - // 清空当前播放文本 - _currentText = ''; - _isProcessingQueue = false; - _ttsState = TtsState.stopped; - Logger.info('TTS播放被取消,设置为stopped状态'); - }); - - // 设置暂停回调 - _flutterTts.setPauseHandler(() { - _ttsState = TtsState.paused; - }); - - // 设置继续回调 - _flutterTts.setContinueHandler(() { - _ttsState = TtsState.playing; - }); - - // 加载可用语音 - await _loadVoices(); - - Logger.info('Flutter TTS服务已初始化'); - } - - /// 加载可用的语音 - Future _loadVoices() async { - try { - if (Platform.isAndroid || Platform.isIOS) { - final voices = await _flutterTts.getVoices; - if (voices != null) { - _voices = []; - for (var voice in voices) { - if (voice is Map) { - final Map voiceMap = {}; - voice.forEach((key, value) { - if (key is String && value is String) { - voiceMap[key] = value; - } else if (key is String) { - voiceMap[key] = value.toString(); - } - }); - if (voiceMap.containsKey('name')) { - _voices.add(voiceMap); - } - } - } - - // 如果有可用语音,设置第一个为默认 - if (_voices.isNotEmpty) { - _currentVoice = _voices[0]['name'] ?? ''; - await _flutterTts.setVoice({"name": _currentVoice}); - } - } - } - } catch (e) { - Logger.error('加载TTS语音失败', e); - } - } - - /// 设置语音 - Future setVoice(String voiceName) async { - try { - _currentVoice = voiceName; - await _flutterTts.setVoice({"name": voiceName}); - } catch (e) { - Logger.error('设置TTS语音失败', e); - } - } - - /// 设置语速 - Future setRate(double rate) async { - try { - _rate = rate; - await _flutterTts.setSpeechRate(rate); - } catch (e) { - Logger.error('设置TTS语速失败', e); - } - } - - /// 设置音量 - Future setVolume(double volume) async { - try { - _volume = volume; - await _flutterTts.setVolume(volume); - } catch (e) { - Logger.error('设置TTS音量失败', e); - } - } - - /// 设置音调 - Future setPitch(double pitch) async { - try { - _pitch = pitch; - await _flutterTts.setPitch(pitch); - } catch (e) { - Logger.error('设置TTS音调失败', e); - } - } - - /// 设置语言 - Future setLanguage(String language) async { - try { - await _flutterTts.setLanguage(language); - } catch (e) { - Logger.error('设置TTS语言失败', e); - } - } - - /// 添加文本到队列并开始处理队列(如果尚未开始) - Future speak(String text) async { - if (text.isEmpty) return; - - // 添加到队列 - _addToQueue(text); - - // 开始处理队列(如果尚未开始) - _startQueueProcessingIfNeeded(); - } - - /// 添加多条文本到队列并开始处理队列(如果尚未开始) - Future speakTexts(List texts) async { - if (texts.isEmpty) return; - - // 添加所有非空文本到队列 - int addedCount = 0; - for (final text in texts) { - if (text.isNotEmpty) { - _textQueue.add(text); - addedCount++; - } - } - - if (addedCount > 0) { - Logger.info('已添加$addedCount条文本到TTS队列'); - - // 开始处理队列(如果尚未开始) - _startQueueProcessingIfNeeded(); - } - } - - /// 添加文本到队列 - void _addToQueue(String text) { - if (text.isEmpty) return; - - _textQueue.add(text); - Logger.info('已添加文本到TTS队列: "${text.length > 20 ? "${text.substring(0, 20)}..." : text}"'); - } - - /// 开始处理队列(如果尚未开始) - void _startQueueProcessingIfNeeded() { - // 如果队列为空,不需要处理 - if (_textQueue.isEmpty) return; - - // 如果已经在处理队列或正在播放,不需要重新开始 - if (_isProcessingQueue || _ttsState == TtsState.playing) return; - - // 开始处理队列 - _isProcessingQueue = true; - _processNextInQueue(); - } - - /// 处理队列中的下一条文本 - void _processNextInQueue() { - // 如果队列为空,结束处理但不立即设置stopped状态 - if (_textQueue.isEmpty) { - _currentText = ''; - _isProcessingQueue = false; - - // 只有当当前没有文本在播放时才设置为stopped状态 - if (_currentText.isEmpty) { - _ttsState = TtsState.stopped; - Logger.info('TTS队列为空,设置为stopped状态'); - } - return; - } - - // 获取队列中的第一条文本并播放 - _speakNextText(); - } - - /// 播放队列中的下一条文本 - Future _speakNextText() async { - if (_textQueue.isEmpty) { - _isProcessingQueue = false; - - // 只有当当前没有文本在播放时才设置为stopped状态 - if (_currentText.isEmpty) { - _ttsState = TtsState.stopped; - Logger.info('TTS队列播放完成,设置为stopped状态'); - } - return; - } - - // 获取队列中的第一条文本 - final text = _textQueue.removeAt(0); - _currentText = text; - - try { - // 设置TTS参数 - await _flutterTts.setSpeechRate(_rate); - await _flutterTts.setVolume(_volume); - await _flutterTts.setPitch(_pitch); - - if (_currentVoice.isNotEmpty) { - await _flutterTts.setVoice({"name": _currentVoice}); - } - - // 播放文本 - await _flutterTts.speak(text); - Logger.info('正在播放文本,队列中还有${_textQueue.length}条待播放'); - } catch (e) { - Logger.error('TTS播放文本失败', e); - // 清空当前文本 - _currentText = ''; - // 尝试播放下一个 - _processNextInQueue(); - } - } - - /// 停止播放并清空队列 - Future stop() async { - try { - await _flutterTts.stop(); - - // 重置队列处理状态 - _textQueue.clear(); - _currentText = ''; - _isProcessingQueue = false; - _ttsState = TtsState.stopped; - - Logger.info('已停止TTS播放并清空队列'); - } catch (e) { - Logger.error('停止TTS播放失败', e); - } - } - - /// 暂停播放 - Future pause() async { - try { - await _flutterTts.pause(); - _ttsState = TtsState.paused; - } catch (e) { - Logger.error('暂停TTS播放失败', e); - } - } - - /// 清空队列并停止播放 - Future clearQueue() async { - _textQueue.clear(); - await stop(); - Logger.info('已清空TTS队列'); - } - - @override - void onClose() { - _flutterTts.stop(); - super.onClose(); - } -} - -/// TTS状态枚举 -enum TtsState { - playing, - stopped, - paused, -} \ No newline at end of file +// 重新导出FlutterTtsService类 +// 这个文件作为兼容层,将speech_impl/flutter_tts_service.dart中的服务导出 +export 'speech_impl/flutter_tts_service.dart'; \ No newline at end of file diff --git a/lib/data/services/my_audio_handler.dart b/lib/data/services/my_audio_handler.dart deleted file mode 100644 index 67eac5301..000000000 --- a/lib/data/services/my_audio_handler.dart +++ /dev/null @@ -1,352 +0,0 @@ -import 'dart:async'; -import 'package:audio_service/audio_service.dart'; -import 'package:get/get.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'background_agent_service.dart'; -import '../../data/providers/agent_provider.dart'; -import '../../core/utils/logger.dart'; - -/// 自定义 AudioHandler,用于捕获媒体按键事件(例如蓝牙耳机按钮) -class MyAudioHandler extends BaseAudioHandler { - bool _isDisposed = false; - - // 添加一个标志,表示是否正在处理交互 - bool _isHandlingInteraction = false; - - // 添加一个计时器,用于定期更新播放状态 - Timer? _statusUpdateTimer; - - // 添加一个变量,记录最后一次媒体按钮事件的时间 - DateTime? _lastMediaButtonEventTime; - - MyAudioHandler() { - _initializeHandler(); - _startStatusUpdateTimer(); - Logger.info('MyAudioHandler 已创建,准备接收媒体按钮事件'); - } - - // 获取BackgroundAgentService实例 - BackgroundAgentService? _getBackgroundAgent() { - try { - if (Get.isRegistered()) { - return Get.find(); - } else { - return Get.put(BackgroundAgentService(), permanent: true); - } - } catch (e) { - Logger.error('获取 BackgroundAgent 失败', e); - return null; - } - } - - void _initializeHandler() { - try { - // 设置初始播放状态 - playbackState.add(PlaybackState( - controls: [ - MediaControl.play, - MediaControl.pause, - MediaControl.stop, - MediaControl.skipToPrevious, - MediaControl.skipToNext, - ], - systemActions: { - MediaAction.play, - MediaAction.pause, - MediaAction.stop, - MediaAction.skipToPrevious, - MediaAction.skipToNext, - MediaAction.seek, - }, - androidCompactActionIndices: const [0, 1, 2], - processingState: AudioProcessingState.idle, - playing: false, - )); - - // 设置媒体元数据 - final item = MediaItem( - id: 'deep_voice_audio', - title: 'Deep Voice Audio', - album: 'Deep Voice', - artist: 'Deep Voice', - genre: 'Speech', - duration: const Duration(seconds: 1), - artUri: null, // 移除图片URI - playable: true, - displayTitle: 'Deep Voice Audio', - displaySubtitle: 'Voice Assistant', - displayDescription: 'TTS Audio', - extras: const {'source': 'deep_voice'}, - ); - - mediaItem.add(item); - Logger.info('AudioHandler 媒体项已设置'); - - // 立即更新播放状态,确保通知显示 - _updatePlaybackState( - playing: true, - processingState: AudioProcessingState.ready, - ); - } catch (e) { - Logger.error('初始化 AudioHandler 失败', e); - } - } - - // 启动定期更新播放状态的计时器 - void _startStatusUpdateTimer() { - _statusUpdateTimer?.cancel(); - _statusUpdateTimer = Timer.periodic(Duration(minutes: 5), (timer) { - try { - // 定期更新播放状态,确保服务保持活跃 - _updatePlaybackState( - playing: true, - processingState: AudioProcessingState.ready, - ); - Logger.info('定期更新播放状态成功'); - } catch (e) { - Logger.error('定期更新播放状态失败', e); - } - }); - } - - // 更新播放状态 - void _updatePlaybackState({ - required bool playing, - required AudioProcessingState processingState, - }) { - if (_isDisposed) return; - - try { - playbackState.add(playbackState.value.copyWith( - playing: playing, - processingState: processingState, - updatePosition: Duration.zero, - )); - } catch (e) { - Logger.error('更新播放状态失败', e); - } - } - - Future _handleInteraction() async { - // 防止重复处理 - if (_isHandlingInteraction) { - Logger.info('已经在处理交互,忽略此次请求'); - return; - } - - _isHandlingInteraction = true; - - try { - // 更新播放状态,确保通知显示 - _updatePlaybackState( - playing: true, - processingState: AudioProcessingState.ready, - ); - - // 直接获取 personal_assistant agent - final agent = AgentProvider.getAgentById('personal_assistant'); - if (agent == null) { - Logger.error('无法获取personal_assistant agent,无法激活语音助手'); - return; - } - - // 检查app是否在前台 - 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(); - Logger.info('已停止后台代理服务,切换到前台模式'); - } - - // 导航到聊天页面,并标记需要在进入时播放语音应答 - Get.toNamed('/chat', arguments: { - 'agentId': agent.id, - 'agentName': agent.name, - 'agentVoice': agent.voice, - 'agentAvatar': agent.avatarUrl, - 'agentSubtitle': agent.description, - 'systemPrompt': agent.systemPrompt, - 'welcomeMessage': agent.welcomeMessage, - 'playVoiceOnEnter': true, - }); - } else if (backgroundAgent != null) { - // 应用在后台,使用 BackgroundAgent 处理交互 - if (backgroundAgent.isProcessing.value) { - Logger.info('检测到媒体按钮再次按下,中断当前AI回复'); - await backgroundAgent.respondToNewMediaEvent(); - } else { - // 首次启动后台服务 - Logger.info('首次启动后台服务'); - await backgroundAgent.startAgentInteraction(agent.id); - } - } else { - Logger.error('无法获取 BackgroundAgent,无法处理交互'); - } - } catch (e) { - Logger.error('处理媒体按钮交互失败', e); - } finally { - _isHandlingInteraction = false; - } - } - - @override - Future play() async { - Logger.info('收到播放命令'); - try { - _updatePlaybackState( - playing: true, - processingState: AudioProcessingState.ready, - ); - await _handleInteraction(); - } catch (e) { - Logger.error('处理播放命令失败', e); - } - } - - @override - Future pause() async { - Logger.info('收到暂停命令'); - try { - _updatePlaybackState( - playing: false, - processingState: AudioProcessingState.ready, - ); - await _handleInteraction(); - } catch (e) { - Logger.error('处理暂停命令失败', e); - } - } - - @override - Future stop() async { - Logger.info('停止 AudioHandler'); - _isDisposed = true; - try { - _statusUpdateTimer?.cancel(); - - // 如果有正在运行的后台服务,停止它 - final backgroundAgent = _getBackgroundAgent(); - if (backgroundAgent != null && backgroundAgent.isProcessing.value) { - await backgroundAgent.stopInteraction(); - } - - await super.stop(); - _updatePlaybackState( - playing: false, - processingState: AudioProcessingState.idle, - ); - } catch (e) { - Logger.error('停止 AudioHandler 时发生错误', e); - } - } - - @override - Future skipToPrevious() async { - Logger.info('收到上一曲命令'); - try { - _updatePlaybackState( - playing: true, - processingState: AudioProcessingState.ready, - ); - await _handleInteraction(); - } catch (e) { - Logger.error('处理上一曲命令失败', e); - } - } - - @override - Future skipToNext() async { - Logger.info('收到下一曲命令'); - try { - _updatePlaybackState( - playing: true, - processingState: AudioProcessingState.ready, - ); - await _handleInteraction(); - } catch (e) { - Logger.error('处理下一曲命令失败', e); - } - } - - @override - Future seek(Duration position) async { - Logger.info('收到跳转命令: $position'); - } - - @override - Future customAction(String name, - [Map? extras]) async { - Logger.info('收到自定义命令: $name'); - try { - if (name == 'media_button') { - Logger.info('收到媒体按钮事件'); - - // 检查是否是重复触发(防止短时间内多次触发) - final now = DateTime.now(); - if (_lastMediaButtonEventTime != null) { - final difference = now.difference(_lastMediaButtonEventTime!); - if (difference.inMilliseconds < 500) { - Logger.info('媒体按钮事件触发过于频繁,忽略此次事件'); - return; - } - } - - // 更新最后一次事件时间 - _lastMediaButtonEventTime = now; - - _updatePlaybackState( - playing: true, - processingState: AudioProcessingState.ready, - ); - await _handleInteraction(); - } else if (name == 'ping') { - // 用于检查服务是否响应的测试命令 - Logger.info('收到 ping 命令,服务正常运行'); - return 'pong'; - } - } catch (e) { - Logger.error('处理自定义命令失败', e); - } - } - - @override - Future onTaskRemoved() async { - Logger.info('服务被系统移除'); - try { - // 如果有正在运行的后台服务,停止它 - final backgroundAgent = _getBackgroundAgent(); - if (backgroundAgent != null && backgroundAgent.isProcessing.value) { - await backgroundAgent.stopInteraction(); - } - - await stop(); - } catch (e) { - Logger.error('处理服务移除失败', e); - } - } - - @override - Future onNotificationDeleted() async { - Logger.info('通知被用户移除'); - try { - // 如果有正在运行的后台服务,停止它 - final backgroundAgent = _getBackgroundAgent(); - if (backgroundAgent != null && backgroundAgent.isProcessing.value) { - await backgroundAgent.stopInteraction(); - } - - await stop(); - } catch (e) { - Logger.error('处理通知移除失败', e); - } - } -} diff --git a/lib/data/services/notification_service.dart b/lib/data/services/notification_service.dart deleted file mode 100644 index f73201175..000000000 --- a/lib/data/services/notification_service.dart +++ /dev/null @@ -1,224 +0,0 @@ -import 'dart:io'; -import 'package:get/get.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:permission_handler/permission_handler.dart'; -import '../providers/agent_provider.dart'; -import 'package:flutter/foundation.dart'; - -class NotificationService extends GetxService { - static NotificationService get to => Get.find(); - - FlutterLocalNotificationsPlugin? _flutterLocalNotificationsPlugin; - bool _notificationActive = false; - bool _isRequestingPermission = false; - - String get _defaultAgentName { - final agent = AgentProvider.getAgentById('personal_assistant'); - return agent?.name ?? 'Deep Voice'; - } - - Future init() async { - try { - if (Platform.isIOS || Platform.isAndroid || Platform.isMacOS) { - _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); - - // 使用应用图标作为通知图标 - const initializationSettingsAndroid = AndroidInitializationSettings('mipmap/ic_launcher'); - - final initializationSettingsDarwin = DarwinInitializationSettings( - requestSoundPermission: false, - requestBadgePermission: false, - requestAlertPermission: false, - ); - - final initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsDarwin, - macOS: initializationSettingsDarwin, - ); - - await _flutterLocalNotificationsPlugin?.initialize( - initializationSettings, - onDidReceiveNotificationResponse: (details) { - if (Get.currentRoute != '/chat') { - Get.toNamed('/chat'); - } - }, - ); - } - - // 创建通知渠道 - 使用高重要性确保通知在后台可见 - if (Platform.isAndroid) { - await _flutterLocalNotificationsPlugin?.resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel( - const AndroidNotificationChannel( - 'foreground_service', - 'Foreground Service', - description: 'Used for the foreground service notification', - importance: Importance.high, // 提高重要性 - playSound: false, - enableVibration: false, - showBadge: true, - ), - ); - } - - // 请求权限,但避免重复请求 - await _requestPermissionIfNeeded(); - - // 在初始化完成后显示前台服务通知 - if (Platform.isAndroid) { - await showForegroundNotification(); - } - - // Add macOS-specific code for status bar icon - if (Platform.isMacOS) { - await showMacOSStatusBarIcon(); - } - } catch (e) { - print('通知服务初始化失败: $e'); - } - } - - Future showForegroundNotification() async { - if (_notificationActive) return; - - try { - final androidDetails = AndroidNotificationDetails( - 'foreground_service', - 'Foreground Service', - channelDescription: 'Used for the foreground service notification', - importance: Importance.high, // 提高重要性 - priority: Priority.high, // 提高优先级 - ongoing: true, - autoCancel: false, - showWhen: false, - enableVibration: false, - playSound: false, - icon: 'mipmap/ic_launcher', // 使用应用图标 - visibility: NotificationVisibility.public, - category: AndroidNotificationCategory.service, - actions: [ - const AndroidNotificationAction( - 'chat', - '开始对话', - showsUserInterface: true, - ), - ], - ); - - const iosDetails = DarwinNotificationDetails( - presentAlert: false, - presentBadge: false, - presentSound: false, - interruptionLevel: InterruptionLevel.passive, - ); - - final notificationDetails = NotificationDetails( - android: androidDetails, - iOS: iosDetails, - ); - - await _flutterLocalNotificationsPlugin?.show( - 0, - _defaultAgentName, - '点击开始对话', - notificationDetails, - ); - - _notificationActive = true; - } catch (e) { - print('显示前台服务通知失败: $e'); - } - } - - Future cancelNotification() async { - if (!_notificationActive) return; - try { - await _flutterLocalNotificationsPlugin?.cancel(0); - _notificationActive = false; - } catch (e) { - print('取消通知失败: $e'); - } - } - - Future _requestPermissionIfNeeded() async { - // 避免重复请求权限 - if (_isRequestingPermission) { - print('已有权限请求正在进行中,跳过此次请求'); - return; - } - - _isRequestingPermission = true; - - try { - if (Platform.isMacOS) { - // On macOS, we don't need to request notification permissions through permission_handler - // Instead, we'll use the flutter_local_notifications plugin directly - final bool? granted = await _flutterLocalNotificationsPlugin - ?.resolvePlatformSpecificImplementation() - ?.requestPermissions( - alert: true, - badge: true, - sound: true, - ); - if (granted != true) { - debugPrint('MacOS Notifications: Permission request was denied'); - } - } else if (Platform.isAndroid || Platform.isIOS) { - // 检查权限状态,避免不必要的请求 - final status = await Permission.notification.status; - if (status != PermissionStatus.granted) { - await Permission.notification.request(); - } - } - } catch (e) { - print('请求通知权限失败: $e'); - } finally { - _isRequestingPermission = false; - } - } - - Future showNotification({ - required String title, - required String body, - }) async { - if (_flutterLocalNotificationsPlugin == null) return; - - try { - const androidDetails = AndroidNotificationDetails( - 'chat_channel', - '聊天消息', - channelDescription: '接收新的聊天消息', - importance: Importance.high, - priority: Priority.high, - icon: 'mipmap/ic_launcher', // 使用应用图标 - ); - - const iosDetails = DarwinNotificationDetails( - presentAlert: true, - presentBadge: true, - presentSound: true, - ); - - const notificationDetails = NotificationDetails( - android: androidDetails, - iOS: iosDetails, - ); - - await _flutterLocalNotificationsPlugin?.show( - 0, - title, - body, - notificationDetails, - ); - } catch (e) { - print('显示通知失败: $e'); - } - } - - Future showMacOSStatusBarIcon() async { - // Implementation using platform channel to communicate with native macOS code - } -} \ No newline at end of file diff --git a/lib/data/services/speech_factory.dart b/lib/data/services/speech_factory.dart new file mode 100644 index 000000000..c3b659686 --- /dev/null +++ b/lib/data/services/speech_factory.dart @@ -0,0 +1,113 @@ +import 'package:get/get.dart'; +import 'asr_service.dart'; +import 'tts_service.dart'; +import 'speech_impl/azure_asr_service.dart'; +import 'speech_impl/azure_tts_service.dart'; +import 'speech_impl/flutter_asr_service.dart'; +import 'speech_impl/flutter_tts_service.dart'; +import '../../core/utils/logger.dart'; + +/// 语音服务类型 +enum SpeechServiceType { + /// Azure 语音服务 + azure, + + /// Flutter 本地语音服务 + flutter, +} + +/// 语音服务工厂类 +/// +/// 用于根据配置信息获取不同的TTS和ASR实现 +class SpeechFactory extends GetxService { + static SpeechFactory get to => Get.find(); + + + /// 当前使用的语音服务类型 + SpeechServiceType _currentServiceType = SpeechServiceType.azure; + + /// 获取当前使用的语音服务类型 + SpeechServiceType get currentServiceType => _currentServiceType; + + /// 设置语音服务类型 + void setServiceType(SpeechServiceType type) { + if (_currentServiceType != type) { + _currentServiceType = type; + Logger.info('语音服务类型已切换为: ${type.toString().split('.').last}'); + + // 重新注册服务 + _registerServices(); + } + } + + /// 注册语音服务 + void _registerServices() { + // 注销现有服务 + if (Get.isRegistered()) { + Get.delete(force: true); + } + + if (Get.isRegistered()) { + Get.delete(force: true); + } + + // 根据类型注册新服务 + switch (_currentServiceType) { + case SpeechServiceType.azure: + Get.lazyPut(() => AzureAsrService()); + Get.lazyPut(() => AzureTtsService()); + break; + + case SpeechServiceType.flutter: + Get.lazyPut(() => FlutterAsrService()); + Get.lazyPut(() => FlutterTtsService()); + break; + } + + final asrService = Get.find(); + final ttsService = Get.find(); + asrService.initialize(); + ttsService.initialize(); + } + + /// 初始化语音服务 + Future initialize({SpeechServiceType? initialType}) async { + try { + // 如果指定了初始类型,则设置 + if (initialType != null) { + _currentServiceType = initialType; + } + + // 注册服务 + _registerServices(); + + // // 初始化服务 + // final asrService = Get.find(); + // final ttsService = Get.find(); + + // await asrService.initialize(); + // await ttsService.initialize(); + + Logger.info('语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}'); + } catch (e) { + Logger.error('语音服务初始化失败: $e'); + rethrow; + } + } + + /// 获取ASR服务 + AsrService getAsrService() { + if (!Get.isRegistered()) { + _registerServices(); + } + return Get.find(); + } + + /// 获取TTS服务 + TtsService getTtsService() { + if (!Get.isRegistered()) { + _registerServices(); + } + return Get.find(); + } +} \ No newline at end of file diff --git a/lib/data/services/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart similarity index 78% rename from lib/data/services/azure_asr_service.dart rename to lib/data/services/speech_impl/azure_asr_service.dart index e3ddb78d7..95f01bddd 100644 --- a/lib/data/services/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -3,10 +3,12 @@ import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import '../../../core/utils/logger.dart'; import 'package:get/get.dart'; -/// 微软语音识别服务 +import '../asr_service.dart'; + +/// Azure 语音识别服务 /// /// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口 -class AzureAsrService { +class AzureAsrService extends GetxService implements AsrService { static final AzureAsrService to = Get.put(AzureAsrService()); static const MethodChannel _channel = MethodChannel('com.deep_voice.azure_asr'); static const EventChannel _eventChannel = EventChannel('com.deep_voice.azure_asr_events'); @@ -15,8 +17,8 @@ class AzureAsrService { late final String _subscriptionKey; late final String _serviceRegion; - - List _supportedLanguages = ['zh-CN', 'en-US']; + final List _supportedLanguages = ['zh-CN']; + @override List get supportedLanguages => _supportedLanguages; // 连续识别相关 @@ -28,10 +30,6 @@ class AzureAsrService { bool _isEventChannelReady = false; Completer? _eventChannelReadyCompleter; - // 公开的事件流 - Stream? _recognitionStream; - Stream? get recognitionStream => _recognitionStream; - // 最新的识别结果 String _latestRecognizedText = ''; String get latestRecognizedText => _latestRecognizedText; @@ -92,16 +90,14 @@ class AzureAsrService { } } - /// 初始化微软语音识别 SDK - /// - /// 返回 true 表示初始化成功,否则抛出 PlatformException + @override Future initialize({ List? supportedLanguages, }) async { try { - if (supportedLanguages != null && supportedLanguages.isNotEmpty) { - _supportedLanguages = supportedLanguages; + _supportedLanguages.clear(); + _supportedLanguages.addAll(supportedLanguages); } // 检查是否需要重新初始化 @@ -125,11 +121,8 @@ class AzureAsrService { } } - - /// 执行一次性语音识别 - /// - /// 返回识别的文本和检测到的语言,如果识别失败则抛出 PlatformException - Future recognizeSpeech() async { + @override + Future recognizeOnce() async { if (!_isInitialized) { await initialize(); } @@ -142,23 +135,25 @@ class AzureAsrService { final String detectedLanguage = result['detectedLanguage'] as String? ?? ''; _latestRecognizedText = text; _latestDetectedLanguage = detectedLanguage; - return RecognitionResult(text: text, detectedLanguage: detectedLanguage); + + return RecognitionEvent.finalResult( + text: text, + detectedLanguage: detectedLanguage, + ); } else if (result is String) { // 兼容旧版本 _latestRecognizedText = result; - return RecognitionResult(text: result); + return RecognitionEvent.finalResult(text: result); } throw Exception('无效的识别结果格式'); } catch (e) { Logger.error('语音识别失败: $e'); - rethrow; + return RecognitionEvent.error(e.toString()); } } - /// 开始连续语音识别 - /// - /// 返回一个包含识别事件的流,如果开始失败则抛出 PlatformException + @override Future> startContinuousRecognition() async { if (!_isInitialized) { await initialize(); @@ -193,9 +188,7 @@ class AzureAsrService { } } - /// 停止连续语音识别 - /// - /// 返回 true 表示停止成功,否则抛出 PlatformException + @override Future stopContinuousRecognition() async { if (!_isInitialized || !_isContinuousRecognitionActive) { return true; @@ -212,7 +205,7 @@ class AzureAsrService { } } - /// 检查连续识别是否活跃 + @override bool isContinuousRecognitionActive() { return _isContinuousRecognitionActive; } @@ -300,13 +293,17 @@ class AzureAsrService { _isContinuousRecognitionActive = false; } - /// 释放资源 + @override Future dispose() async { try { if (_isContinuousRecognitionActive) { await stopContinuousRecognition(); } + // 取消事件订阅 + await _eventSubscription?.cancel(); + _eventSubscription = null; + // 通知原生端释放资源 await _channel.invokeMethod('dispose'); _isInitialized = false; @@ -318,72 +315,4 @@ class AzureAsrService { _isInitialized = false; } } - -} - -/// 识别事件类型 -enum RecognitionEventType { - /// 最终识别结果 - finalResult, - - /// 中间识别结果(实时反馈) - intermediateResult, - - /// 会话开始 - sessionStarted, - - /// 会话结束 - sessionStopped, - - /// 识别取消 - canceled, - - /// 识别错误 - error, -} - -/// 识别事件 -class RecognitionEvent { - /// 事件类型 - final RecognitionEventType type; - - /// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效) - final String text; - - /// 检测到的语言(仅在启用自动语言检测时有效) - final String detectedLanguage; - - /// 错误信息(仅在 error 和 canceled 类型中有效) - final String error; - - RecognitionEvent({ - required this.type, - this.text = '', - this.detectedLanguage = '', - this.error = '', - }); - - @override - String toString() { - return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}'; - } -} - -/// 识别结果 -class RecognitionResult { - /// 识别文本 - final String text; - - /// 检测到的语言(仅在启用自动语言检测时有效) - final String detectedLanguage; - - RecognitionResult({ - required this.text, - this.detectedLanguage = '', - }); - - @override - String toString() { - return 'RecognitionResult{text: $text, detectedLanguage: $detectedLanguage}'; - } } \ No newline at end of file diff --git a/lib/data/services/speech_impl/azure_tts_service.dart b/lib/data/services/speech_impl/azure_tts_service.dart new file mode 100644 index 000000000..73c182cf8 --- /dev/null +++ b/lib/data/services/speech_impl/azure_tts_service.dart @@ -0,0 +1,570 @@ +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'; +import '../tts_service.dart'; + +/// 音频输出设备类型 +enum AudioOutputType { + /// 扬声器 + speaker, + + /// 听筒 + earpiece, + + /// 自动选择(有耳机用耳机,没有则用听筒) + auto +} + +/// 微软 Text-to-Speech 服务 +/// +/// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互, +/// 提供文本转语音功能。 +class AzureTtsService extends GetxService implements TtsService { + static final AzureTtsService to = Get.put(AzureTtsService()); + 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; + AudioOutputType _currentAudioOutputType = AudioOutputType.auto; + + // 支持的语音列表 + final List _supportedVoices = [ + 'zh-CN-XiaoxiaoNeural', + 'zh-CN-YunxiNeural', + 'zh-CN-YunjianNeural', + 'en-US-JennyNeural', + 'en-US-GuyNeural' + ]; + + // 语音合成队列 + final List<_SpeechItem> _textQueue = []; + bool _isProcessingQueue = false; + + // 事件流控制器 + final StreamController _eventController = StreamController.broadcast(); + @override + Stream get onEvent => _eventController.stream; + + // 可观察状态 + final isEnabled = true.obs; + final _isSpeaking = false.obs; + final audioOutputType = AudioOutputType.auto.obs; + + // 流式文本缓冲区 + String _streamBuffer = ''; + + @override + String get currentVoice => _currentVoice; + + @override + bool get isSpeaking => _isSpeaking.value; + + @override + List get supportedVoices => _supportedVoices; + + AudioOutputType get currentAudioOutputType => _currentAudioOutputType; + + AzureTtsService() { + _loadConfig(); + } + + /// 从环境变量加载配置 + void _loadConfig() { + _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; + _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; + } + + @override + Future initialize({ + List? supportedLanguages, + }) async { + if (_isInitialized) return true; + + try { + if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { + Logger.error('未找到Azure语音服务配置'); + return false; + } + + // 始终优先使用中文作为默认语言 + final String defaultLanguage = 'zh-CN'; + + // 更新支持的语音列表(如果提供) + if (supportedLanguages != null && supportedLanguages.isNotEmpty) { + // 如果支持中文,优先使用中文,否则使用提供的第一种语言 + String language = supportedLanguages.contains(defaultLanguage) + ? defaultLanguage + : supportedLanguages.first; + + final result = await _channel.invokeMethod('initialize', { + 'subscriptionKey': _subscriptionKey, + 'region': _serviceRegion, + 'language': language, + }); + + _isInitialized = result; + + // 确保设置默认的音频输出设备类型 + if (result) { + // 不需要显式调用 setAudioOutputType,因为原生层已经设置了默认值 + // 只需要确保 Dart 层的状态与原生层一致 + _currentAudioOutputType = AudioOutputType.auto; + audioOutputType.value = AudioOutputType.auto; + } + + return result; + } else { + // 使用默认语言初始化 + final result = await _channel.invokeMethod('initialize', { + 'subscriptionKey': _subscriptionKey, + 'region': _serviceRegion, + 'language': 'zh-CN', + }); + + _isInitialized = result; + + // 确保设置默认的音频输出设备类型 + if (result) { + _currentAudioOutputType = AudioOutputType.auto; + audioOutputType.value = AudioOutputType.auto; + } + + return result; + } + } catch (e) { + Logger.error('初始化失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('TTS初始化失败: $e')); + + return false; + } + } + + @override + Future setVoice(String voiceName) async { + if (!_isInitialized) await initialize(); + if (voiceName == _currentVoice) return true; + + try { + final result = await _channel.invokeMethod('setVoice', { + 'voiceName': voiceName, + }); + + if (result) _currentVoice = voiceName; + return result; + } catch (e) { + Logger.error('设置语音失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('设置语音失败: $e')); + + return false; + } + } + + /// 设置音频输出设备类型 + Future setAudioOutputType(AudioOutputType outputType) async { + if (!_isInitialized) await initialize(); + + try { + final String outputTypeStr = outputType.toString().split('.').last; + final result = await _channel.invokeMethod('setAudioOutputType', { + 'outputType': outputTypeStr, + }); + + if (result) { + _currentAudioOutputType = outputType; + audioOutputType.value = outputType; + } + + return result; + } catch (e) { + Logger.error('设置音频输出设备失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.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'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('设置语音参数失败: $e')); + + return false; + } + } + + @override + Future speakOnce(String text) async { + if (!_isInitialized) await initialize(); + if (!isEnabled.value || text.isEmpty) return false; + + try { + _isSpeaking.value = true; + + // 发送开始事件 + _eventController.add(TtsEvent(type: TtsEventType.started)); + + final result = await _channel.invokeMethod('speakText', {'text': text}); + + _isSpeaking.value = false; + + // 发送完成事件 + _eventController.add(TtsEvent(type: TtsEventType.completed)); + + return result == "OK"; + } catch (e) { + _isSpeaking.value = false; + Logger.error('语音合成失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('语音合成失败: $e')); + + return false; + } + } + + /// 播放SSML + Future speakSsmlOnce(String ssml) async { + if (!_isInitialized) await initialize(); + if (!isEnabled.value || ssml.isEmpty) return false; + + try { + _isSpeaking.value = true; + + // 发送开始事件 + _eventController.add(TtsEvent(type: TtsEventType.started)); + + final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml}); + + _isSpeaking.value = false; + + // 发送完成事件 + _eventController.add(TtsEvent(type: TtsEventType.completed)); + + return result == "OK"; + } catch (e) { + _isSpeaking.value = false; + Logger.error('SSML语音合成失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('SSML语音合成失败: $e')); + + return false; + } + } + + @override + Future speakStream(String text) async { + if (!isEnabled.value || text.isEmpty) return false; + + try { + // 添加文本到缓冲区 + _streamBuffer += text; + + // 如果缓冲区为空,直接返回 + 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; + } catch (e) { + Logger.error('处理流式文本失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('处理流式文本失败: $e')); + + return false; + } + } + + @override + Future flushStream() async { + if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true; + + try { + // 处理缓冲区中可能的完整句子 + await speakStream(''); + + // 如果缓冲区仍有剩余文本,将其添加到播放队列 + if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { + _textQueue.add(_SpeechItem( + text: _streamBuffer, + rate: _currentRate, + pitch: _currentPitch, + volume: _currentVolume, + )); + + // 清空缓冲区 + _streamBuffer = ''; + + // 如果队列未在处理中,开始处理队列 + if (!_isProcessingQueue) { + _processQueue(); + } + } + + Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条'); + + // 等待队列处理完成 + while (_isProcessingQueue || _isSpeaking.value) { + await Future.delayed(const Duration(milliseconds: 100)); + } + + Logger.info('TTS队列播放完成'); + return true; + } catch (e) { + Logger.error('刷新TTS流失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('刷新TTS流失败: $e')); + + return false; + } + } + + /// 处理语音合成队列 + 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, + ); + } + + // 使用speakOnce播放当前项 + await speakOnce(item.text); + } + } catch (e) { + Logger.error('处理语音队列出错: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('处理语音队列出错: $e')); + } finally { + _isProcessingQueue = false; + } + } + + @override + Future stop() async { + try { + _textQueue.clear(); + + if (_isSpeaking.value) { + final result = await _channel.invokeMethod('stopSpeaking'); + _isSpeaking.value = false; + + // 发送取消事件 + _eventController.add(TtsEvent(type: TtsEventType.canceled)); + + return result; + } + + return true; + } catch (e) { + Logger.error('停止语音合成出错: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('停止语音合成出错: $e')); + + return false; + } + } + + /// 生成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 toggleAudioOutputType() async { + AudioOutputType newType; + + switch (_currentAudioOutputType) { + case AudioOutputType.speaker: + newType = AudioOutputType.earpiece; + break; + case AudioOutputType.earpiece: + newType = AudioOutputType.auto; + break; + case AudioOutputType.auto: + newType = AudioOutputType.speaker; + break; + } + + return await setAudioOutputType(newType); + } + + @override + Future dispose() async { + if (!_isInitialized) return; + + try { + await stop(); + await _channel.invokeMethod('dispose'); + + // 关闭事件流控制器 + await _eventController.close(); + + _isInitialized = false; + } catch (e) { + Logger.error('释放资源失败: $e'); + } + } + + /// 处理AI返回的文本流,将其转换为语音 + /// + /// [textStream] 文本流 + /// 返回一个Future,当流处理完成时完成 + Future processAiTextStream(Stream textStream) async { + if (!isEnabled.value) { + Logger.error('TTS服务未启用'); + return false; + } + + try { + await for (final text in textStream) { + if (text.isEmpty) continue; + await speakStream(text); + } + + // 处理完流后,刷新缓冲区中的剩余文本 + await flushStream(); + return true; + } catch (e) { + Logger.error('处理文本流时出错: $e'); + return false; + } + } +} + +/// 语音合成项目 +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/speech_impl/flutter_asr_service.dart b/lib/data/services/speech_impl/flutter_asr_service.dart new file mode 100644 index 000000000..0ae32a508 --- /dev/null +++ b/lib/data/services/speech_impl/flutter_asr_service.dart @@ -0,0 +1,439 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:speech_to_text/speech_to_text.dart' as stt; +import 'package:speech_to_text/speech_recognition_result.dart'; +import 'package:speech_to_text/speech_recognition_error.dart'; +import '../../../core/utils/logger.dart'; +import '../asr_service.dart'; + +/// ASR状态枚举 +enum AsrState { + /// 未初始化 + notInitialized, + + /// 已初始化但未开始监听 + initialized, + + /// 正在监听 + listening, + + /// 正在处理识别结果 + processing, + + /// 识别完成 + done, + + /// 发生错误 + error, +} + +/// Flutter ASR服务,负责本地语音转文本功能 +/// 使用speech_to_text插件实现语音识别功能 +class FlutterAsrService extends GetxService implements AsrService { + static final FlutterAsrService to = Get.put(FlutterAsrService()); + + // speech_to_text 实例 + final stt.SpeechToText _speech = stt.SpeechToText(); + + // 可用的语音识别语言 + final List _supportedLanguages = []; + @override + List get supportedLanguages => _supportedLanguages; + + // 当前选择的语言 + final RxString _currentLocale = ''.obs; + String get currentLocale => _currentLocale.value; + + // 是否可用 + final RxBool _isAvailable = false.obs; + bool get isAvailable => _isAvailable.value; + + // 是否正在监听 + bool _isListening = false; + + // 当前ASR状态 + final Rx _asrState = AsrState.notInitialized.obs; + AsrState get asrState => _asrState.value; + + // 连续识别模式的结果流控制器 + StreamController? _continuousRecognitionController; + + FlutterAsrService() { + _initSpeech(); + } + + /// 初始化语音识别 + Future _initSpeech() async { + try { + _asrState.value = AsrState.notInitialized; + + // 初始化语音识别,使用较短的最终超时时间,提高响应速度 + _isAvailable.value = await _speech.initialize( + onStatus: _onSpeechStatus, + onError: _onSpeechError, + debugLogging: false, + finalTimeout: const Duration(milliseconds: 800), // 更短的最终超时时间,加速结果返回 + ); + + if (_isAvailable.value) { + // 获取支持的语言列表 + final systemLocales = await _speech.locales(); + + // 清空并重新填充支持的语言列表 + _supportedLanguages.clear(); + for (var locale in systemLocales) { + if (!_supportedLanguages.contains(locale.localeId)) { + _supportedLanguages.add(locale.localeId); + } + } + + // 设置默认语言(优先使用系统语言或中文) + var systemLocale = await _speech.systemLocale(); + if (systemLocale != null) { + _currentLocale.value = systemLocale.localeId; + } else if (_supportedLanguages.contains('zh-CN')) { + _currentLocale.value = 'zh-CN'; + } else if (_supportedLanguages.isNotEmpty) { + _currentLocale.value = _supportedLanguages.first; + } + + _asrState.value = AsrState.initialized; + Logger.info('语音识别初始化成功,支持 ${_supportedLanguages.length} 种语言'); + } else { + _asrState.value = AsrState.error; + Logger.error('语音识别不可用'); + } + } catch (e) { + _isAvailable.value = false; + _asrState.value = AsrState.error; + Logger.error('语音识别初始化失败: $e'); + } + } + + /// 处理语音识别状态变化 + void _onSpeechStatus(String status) { + Logger.info('语音识别状态: $status'); + + switch (status) { + case 'listening': + _asrState.value = AsrState.listening; + _isListening = true; + break; + case 'notListening': + if (_asrState.value == AsrState.listening) { + _asrState.value = AsrState.processing; + } + _isListening = false; + break; + case 'done': + _asrState.value = AsrState.done; + _isListening = false; + + // 在连续识别模式下,当一次识别完成后立即开始下一次识别 + if (_continuousRecognitionController != null && !_continuousRecognitionController!.isClosed) { + _restartContinuousRecognition(); + } + break; + } + } + + /// 处理语音识别错误 + void _onSpeechError(SpeechRecognitionError error) { + Logger.error('语音识别错误: ${error.errorMsg} (永久性错误: ${error.permanent})'); + _asrState.value = AsrState.error; + + // 向连续识别控制器发送错误事件(如果存在) + _continuousRecognitionController?.add( + RecognitionEvent.error(error.errorMsg) + ); + + // 在连续识别模式下,尝试从错误中恢复(非永久性错误) + if (!error.permanent && _continuousRecognitionController != null && !_continuousRecognitionController!.isClosed) { + _restartContinuousRecognition(); + } + } + + /// 启动语音识别的统一方法 + Future _startListening({ + required Function(SpeechRecognitionResult) onResult, + Duration listenFor = const Duration(seconds: 30), + Duration pauseFor = const Duration(seconds: 3), + bool autoPunctuation = true, + }) async { + try { + // 如果当前正在监听,先停止 + if (_speech.isListening) { + await _speech.stop(); + // 短暂延迟确保资源释放 + await Future.delayed(const Duration(milliseconds: 300)); + } + + // 启动语音识别 + return await _speech.listen( + onResult: onResult, + localeId: _currentLocale.value, + listenFor: listenFor, + pauseFor: pauseFor, + listenOptions: stt.SpeechListenOptions( + partialResults: true, + autoPunctuation: autoPunctuation, + ), + ) ?? false; + } catch (e) { + Logger.error('启动语音识别失败: $e'); + return false; + } + } + + /// 重新启动连续识别 + Future _restartContinuousRecognition() async { + try { + // 确保控制器有效 + if (_continuousRecognitionController == null || _continuousRecognitionController!.isClosed) { + Logger.info('连续识别控制器已关闭,跳过重启'); + return; + } + + // 短暂延迟后再重新启动 + await Future.delayed(const Duration(milliseconds: 300)); + + // 使用统一方法启动识别 + final success = await _startListening( + onResult: _onSpeechResult, + ); + + if (success) { + Logger.info('连续识别重启成功'); + } else { + Logger.error('连续识别重启失败'); + _continuousRecognitionController?.add( + RecognitionEvent.error('重启语音识别失败') + ); + } + } catch (e) { + Logger.error('重启连续识别失败: $e'); + } + } + + /// 处理语音识别结果 + void _onSpeechResult(SpeechRecognitionResult result) { + // 如果是连续识别,发送到连续识别控制器 + if (_continuousRecognitionController != null) { + if (result.finalResult) { + Logger.info('收到最终识别结果: "${result.recognizedWords}"'); + _continuousRecognitionController!.add( + RecognitionEvent.finalResult( + text: result.recognizedWords, + detectedLanguage: currentLocale, + ) + ); + } else { + _continuousRecognitionController!.add( + RecognitionEvent( + type: RecognitionEventType.intermediateResult, + text: result.recognizedWords, + detectedLanguage: currentLocale, + ) + ); + } + } + } + + /// 设置当前语言 + void setLocale(String localeId) { + if (_supportedLanguages.contains(localeId)) { + _currentLocale.value = localeId; + Logger.info('设置语音识别语言: $localeId'); + } else { + Logger.error('不支持的语言: $localeId'); + } + } + + @override + Future initialize({ + List? supportedLanguages, + }) async { + try { + if (supportedLanguages != null && supportedLanguages.isNotEmpty) { + _supportedLanguages.clear(); + _supportedLanguages.addAll(supportedLanguages); + } + + // 初始化语音识别 + await _initSpeech(); + return _isAvailable.value; + } catch (e) { + Logger.error('语音识别初始化失败: $e'); + return false; + } + } + + @override + Future recognizeOnce() async { + try { + if (!_isAvailable.value) { + return RecognitionEvent.error('语音识别不可用'); + } + + if (_isListening) { + await stopContinuousRecognition(); + } + + // 创建Completer来等待最终结果 + final completer = Completer(); + + // 开始监听 + _asrState.value = AsrState.listening; + + // 定义结果处理函数 + void resultListener(SpeechRecognitionResult result) { + if (result.finalResult) { + // 只有在最终结果时才完成completer + completer.complete(RecognitionEvent.finalResult( + text: result.recognizedWords, + detectedLanguage: currentLocale, + )); + } + } + + // 使用统一方法启动识别,单次识别使用更短的时间参数 + final success = await _startListening( + onResult: resultListener, + listenFor: const Duration(seconds: 15), + pauseFor: const Duration(seconds: 2), + ); + + if (!success) { + return RecognitionEvent.error('启动语音识别失败'); + } + + // 设置较短的超时时间 + return await completer.future.timeout( + const Duration(seconds: 20), // 略长于listenFor以确保获取到结果 + onTimeout: () { + _speech.stop(); + return RecognitionEvent.error('语音识别超时'); + }, + ); + } catch (e) { + Logger.error('一次性语音识别失败: $e'); + return RecognitionEvent.error('$e'); + } + } + + @override + Future> startContinuousRecognition() async { + try { + if (!_isAvailable.value) { + final controller = StreamController.broadcast(); + controller.add(RecognitionEvent.error('语音识别不可用')); + controller.close(); + return controller.stream; + } + + // 如果已经在监听,先停止 + if (_isListening) { + await stopContinuousRecognition(); + } + + // 创建新的流控制器 + _continuousRecognitionController = StreamController.broadcast(); + + // 开始会话 + _continuousRecognitionController!.add( + RecognitionEvent(type: RecognitionEventType.sessionStarted) + ); + + // 开始连续监听 + _asrState.value = AsrState.listening; + + // 使用统一方法启动识别,连续识别使用较长的参数 + final success = await _startListening( + onResult: _onSpeechResult, + ); + + if (!success) { + // 添加错误事件,但保留流 + _continuousRecognitionController!.add( + RecognitionEvent.error('启动连续语音识别失败') + ); + + // 返回已有的流控制器 + return _continuousRecognitionController!.stream; + } + + _isListening = true; + return _continuousRecognitionController!.stream; + } catch (e) { + Logger.error('启动连续语音识别失败: $e'); + + // 如果已经创建了控制器,使用它 + if (_continuousRecognitionController != null) { + _continuousRecognitionController!.add(RecognitionEvent.error('$e')); + return _continuousRecognitionController!.stream; + } else { + // 否则创建新的控制器 + final controller = StreamController.broadcast(); + controller.add(RecognitionEvent.error('$e')); + return controller.stream; + } + } + } + + @override + Future stopContinuousRecognition() async { + try { + // 无论是否正在监听,都尝试停止 + if (_speech.isListening) { + await _speech.stop(); + } + + _isListening = false; + + // 发送会话结束事件 + _continuousRecognitionController?.add( + RecognitionEvent(type: RecognitionEventType.sessionStopped) + ); + + // 关闭流控制器 + await _continuousRecognitionController?.close(); + _continuousRecognitionController = null; + + _asrState.value = AsrState.done; + return true; + } catch (e) { + Logger.error('停止连续语音识别失败: $e'); + + // 强制重置状态 + _isListening = false; + _asrState.value = AsrState.error; + + return false; + } + } + + @override + bool isContinuousRecognitionActive() { + return _isListening; + } + + @override + Future dispose() async { + try { + // 确保停止所有识别活动 + if (_speech.isListening) { + await _speech.stop(); + } + + // 关闭控制器 + await _continuousRecognitionController?.close(); + _continuousRecognitionController = null; + + _isListening = false; + _asrState.value = AsrState.notInitialized; + } catch (e) { + Logger.error('释放语音识别资源失败: $e'); + } + } +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/flutter_tts_service.dart b/lib/data/services/speech_impl/flutter_tts_service.dart new file mode 100644 index 000000000..810f82787 --- /dev/null +++ b/lib/data/services/speech_impl/flutter_tts_service.dart @@ -0,0 +1,484 @@ +import 'dart:async'; +import 'dart:collection'; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:flutter_tts/flutter_tts.dart'; +import '../../../core/utils/logger.dart'; +import '../tts_service.dart'; + +/// Flutter TTS服务,负责基本的文本转语音功能 +/// +/// 使用flutter_tts插件实现TtsService接口 +class FlutterTtsService extends GetxService implements TtsService { + static final FlutterTtsService to = Get.put(FlutterTtsService()); + + // flutter_tts实例 + late final FlutterTts _flutterTts; + + // 支持的语音列表 + final List _supportedVoices = []; + List get supportedVoices => _supportedVoices; + + // 当前语音 + final RxString _currentVoice = ''.obs; + @override + String get currentVoice => _currentVoice.value; + + // 播放状态 + final RxBool _isSpeaking = false.obs; + @override + bool get isSpeaking => _isSpeaking.value; + + // 文本队列 + final Queue<_SpeechItem> _textQueue = Queue<_SpeechItem>(); + bool _isProcessingQueue = false; + + // 事件流控制器 + final StreamController _eventController = StreamController.broadcast(); + @override + Stream get onEvent => _eventController.stream; + + // 语言 + String _language = 'zh-CN'; + + // 流式文本缓冲区 + String _streamBuffer = ''; + + // 当前参数设置 + double _volume = 1.0; + double _rate = 0.5; + double _pitch = 1.0; + + FlutterTtsService() { + _flutterTts = FlutterTts(); + _initTts(); + } + + /// 初始化TTS + Future _initTts() async { + try { + // 设置完成回调 + _flutterTts.setCompletionHandler(() { + _isSpeaking.value = false; + _eventController.add(TtsEvent(type: TtsEventType.completed)); + }); + + // 设置开始回调 + _flutterTts.setStartHandler(() { + _isSpeaking.value = true; + _eventController.add(TtsEvent(type: TtsEventType.started)); + }); + + // 设置错误回调 + _flutterTts.setErrorHandler((msg) { + _isSpeaking.value = false; + _eventController.add(TtsEvent.error(msg)); + Logger.error('TTS错误: $msg'); + }); + + // 设置取消回调 + _flutterTts.setCancelHandler(() { + _isSpeaking.value = false; + _eventController.add(TtsEvent(type: TtsEventType.canceled)); + }); + + // 设置默认语言 + await _flutterTts.setLanguage(_language); + + // 设置音量 + await _flutterTts.setVolume(_volume); + + // 设置语速 + await _flutterTts.setSpeechRate(_rate); + + // 设置音调 + await _flutterTts.setPitch(_pitch); + + Logger.info('FlutterTtsService 已初始化'); + } catch (e) { + Logger.error('TTS初始化失败: $e'); + } + } + + @override + Future initialize({ + List? supportedLanguages, + }) async { + try { + // 获取可用的语音 + await _loadVoices(); + + // 更新支持的语言列表(如果提供) + if (supportedLanguages != null && supportedLanguages.isNotEmpty) { + _language = supportedLanguages.first; + await _flutterTts.setLanguage(_language); + } + + Logger.info('FlutterTtsService 初始化成功'); + Logger.info('支持的语音: ${_supportedVoices.join(", ")}'); + return true; + } catch (e) { + Logger.error('TTS初始化失败: $e'); + return false; + } + } + + /// 加载可用的语音 + Future _loadVoices() async { + try { + _supportedVoices.clear(); + + // 获取可用的语音 + final voices = await _flutterTts.getVoices; + + if (voices != null && voices is List) { + for (final voice in voices) { + if (voice is Map && voice.containsKey('name')) { + _supportedVoices.add(voice['name'].toString()); + } + } + } + + // 如果没有找到语音,添加默认语音 + if (_supportedVoices.isEmpty) { + _supportedVoices.add('default'); + } + + // 设置默认语音 + if (_currentVoice.value.isEmpty && _supportedVoices.isNotEmpty) { + _currentVoice.value = _supportedVoices.first; + } + } catch (e) { + Logger.error('加载TTS语音失败: $e'); + + // 添加默认语音 + if (_supportedVoices.isEmpty) { + _supportedVoices.add('default'); + _currentVoice.value = 'default'; + } + } + } + + @override + Future setVoice(String voiceName) async { + try { + // 检查语音是否存在 + if (!_supportedVoices.contains(voiceName) && voiceName != 'default') { + Logger.warning('语音不存在: $voiceName,使用默认语音'); + voiceName = _supportedVoices.isNotEmpty ? _supportedVoices.first : 'default'; + } + + // 设置语音 + await _flutterTts.setVoice({"name": voiceName}); + _currentVoice.value = voiceName; + + Logger.info('设置TTS语音: $voiceName'); + return true; + } catch (e) { + Logger.error('设置TTS语音失败: $e'); + return false; + } + } + + /// 设置语音参数 + Future setSpeechParams({double? rate, double? pitch, double? volume}) async { + try { + if (volume != null) { + await _flutterTts.setVolume(volume); + _volume = volume; + } + + if (rate != null) { + await _flutterTts.setSpeechRate(rate); + _rate = rate; + } + + if (pitch != null) { + await _flutterTts.setPitch(pitch); + _pitch = pitch; + } + + return true; + } catch (e) { + Logger.error('设置语音参数失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error('设置语音参数失败: $e')); + + return false; + } + } + + @override + Future speakOnce(String text) async { + if (text.isEmpty) return false; + + try { + // 如果正在播放,先停止 + if (_isSpeaking.value) { + await stop(); + } + + // 播放文本 + await _flutterTts.speak(text); + + return true; + } catch (e) { + // 发送错误事件 + _eventController.add(TtsEvent.error(e.toString())); + + Logger.error('TTS播放失败: $e'); + return false; + } + } + + @override + Future speakStream(String text) async { + if (text.isEmpty) return false; + + try { + // 添加文本到缓冲区 + _streamBuffer += text; + + // 如果缓冲区为空,直接返回 + 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: _rate, + pitch: _pitch, + volume: _volume, + )); + hasProcessed = true; + } + + // 更新缓冲区,只保留未完成的部分 + if (lastMatchEnd > 0) { + _streamBuffer = _streamBuffer.substring(lastMatchEnd); + } + + // 如果处理了文本并且队列未在处理中,开始处理队列 + if (hasProcessed && !_isProcessingQueue) { + _processQueue(); + } + + return true; + } catch (e) { + Logger.error('处理流式文本失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error(e.toString())); + + return false; + } + } + + @override + Future flushStream() async { + if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true; + + try { + // 处理缓冲区中可能的完整句子 + await speakStream(''); + + // 如果缓冲区仍有剩余文本,将其添加到播放队列 + if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { + _textQueue.add(_SpeechItem( + text: _streamBuffer, + rate: _rate, + pitch: _pitch, + volume: _volume, + )); + + // 清空缓冲区 + _streamBuffer = ''; + + // 如果队列未在处理中,开始处理队列 + if (!_isProcessingQueue) { + _processQueue(); + } + } + + Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条'); + + // 等待队列处理完成 + while (_isProcessingQueue || _isSpeaking.value) { + await Future.delayed(const Duration(milliseconds: 100)); + } + + Logger.info('TTS队列播放完成'); + return true; + } catch (e) { + Logger.error('等待TTS队列播放完成失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error(e.toString())); + + return false; + } + } + + /// 处理队列中的文本 + Future _processQueue() async { + if (_isProcessingQueue) return; + + _isProcessingQueue = true; + + try { + while (_textQueue.isNotEmpty) { + if (!_isSpeaking.value) { + final item = _textQueue.removeFirst(); + + // 如果参数有变化,更新参数 + if (item.rate != _rate || item.pitch != _pitch || item.volume != _volume) { + await setSpeechParams( + rate: item.rate, + pitch: item.pitch, + volume: item.volume, + ); + } + + // 播放文本 + await _flutterTts.speak(item.text); + + // 等待开始播放 + int attempts = 0; + while (!_isSpeaking.value && attempts < 10) { + await Future.delayed(const Duration(milliseconds: 50)); + attempts++; + } + + // 等待播放完成 + while (_isSpeaking.value) { + await Future.delayed(const Duration(milliseconds: 100)); + } + } else { + // 等待一小段时间再检查队列 + await Future.delayed(const Duration(milliseconds: 100)); + } + } + } catch (e) { + Logger.error('TTS处理队列失败: $e'); + + // 发送错误事件 + _eventController.add(TtsEvent.error(e.toString())); + } finally { + _isProcessingQueue = false; + } + } + + @override + Future stop() async { + try { + Logger.info('尝试停止TTS播放...'); + + // 清空队列 + _textQueue.clear(); + _streamBuffer = ''; + + // 如果正在播放,停止播放 + if (_isSpeaking.value) { + // 先重置状态标志,避免处理队列循环继续等待 + _isProcessingQueue = false; + + // 执行停止 + final result = await _flutterTts.stop(); + Logger.info('TTS停止结果: $result'); + + // 短暂延迟等待状态更新 + await Future.delayed(const Duration(milliseconds: 100)); + + // 强制更新状态 + _isSpeaking.value = false; + + // 发送取消事件 + _eventController.add(TtsEvent(type: TtsEventType.canceled)); + } + + Logger.info('TTS停止播放完成'); + return true; + } catch (e) { + Logger.error('TTS停止播放失败: $e'); + + // 即使出错也尝试强制重置状态 + _isSpeaking.value = false; + _isProcessingQueue = false; + + return false; + } + } + + @override + Future dispose() async { + try { + await stop(); + _textQueue.clear(); + + // 关闭事件流控制器 + await _eventController.close(); + + Logger.info('TTS资源已释放'); + } catch (e) { + Logger.error('释放TTS资源失败: $e'); + } + } + + /// 处理AI返回的文本流,将其转换为语音 + /// + /// [textStream] 文本流 + /// 返回一个Future,当流处理完成时完成 + Future processAiTextStream(Stream textStream) async { + try { + await for (final text in textStream) { + if (text.isEmpty) continue; + await speakStream(text); + } + + // 处理完流后,刷新缓冲区中的剩余文本 + await flushStream(); + return true; + } catch (e) { + Logger.error('处理文本流时出错: $e'); + return false; + } + } +} + +/// 语音合成项目 +class _SpeechItem { + final String text; + final double rate; + final double pitch; + final double volume; + + _SpeechItem({ + required this.text, + this.rate = 0.5, + this.pitch = 1.0, + this.volume = 1.0, + }); +} \ No newline at end of file diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart new file mode 100644 index 000000000..7a2d6f457 --- /dev/null +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -0,0 +1,807 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:crypto/crypto.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get/get.dart'; +import 'package:uuid/uuid.dart'; +import 'package:web_socket_channel/io.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; +import '../../../core/utils/logger.dart'; +import 'package:record/record.dart'; +import '../asr_service.dart'; + +/// Volcano语音识别服务 +/// +/// 负责与火山引擎实时语音识别服务的通信 +class VolcanoAsrApiService implements AsrService { + static final VolcanoAsrApiService to = Get.find(); + + // API配置 + late final String _appKey; + late final String _secretKey; + late final String _serviceId; + late final String _hostUrl; + final String _path = '/ws/v1/asr'; + + // 请求相关参数 + final String _algorithm = 'hmac-sha1'; + final Uuid _uuid = const Uuid(); + + // 录音配置 + final int _sampleRate = 16000; + final int _bitsPerSample = 16; + final int _channels = 1; + final String _encoding = 'pcm'; + + // 连接相关 + WebSocketChannel? _channel; + Completer? _connectionCompleter; + StreamSubscription? _connectionSubscription; + Timer? _heartbeatTimer; + + // 语音录音 + final Record? _recorder = null; // TODO: 实现正确的Record实例化 + Timer? _audioTimer; + bool _isRecording = false; + + // 状态控制 + bool _isInitialized = false; + bool _isConnecting = false; + bool _isContinuousRecognitionActive = false; + + // 识别结果控制 + bool _isRecognitionEndRequested = false; + String _currentSessionId = ''; + final List _supportedLanguages = ['zh-CN', 'en-US']; + String _selectedLanguage = 'zh-CN'; + + // 结果流 + StreamController? _recognitionController; + + // 事件计数器(用于调试) + int _sendPacketCount = 0; + int _receivePacketCount = 0; + + // AsrService接口实现 + @override + List get supportedLanguages => _supportedLanguages; + + @override + Future initialize({ + List? supportedLanguages, + }) async { + try { + // 如果已经初始化过,先释放资源 + if (_isInitialized) { + await dispose(); + } + + _isInitialized = false; + + // 从环境变量加载配置 + _loadConfig(); + + // 设置支持的语言 + if (supportedLanguages != null && supportedLanguages.isNotEmpty) { + _supportedLanguages.clear(); + _supportedLanguages.addAll(supportedLanguages); + } + + // 初始化录音机 + await _initRecorder(); + + _isInitialized = true; + Logger.info('火山语音API已初始化'); + return true; + } catch (e) { + Logger.error('初始化火山语音API失败: $e'); + return false; + } + } + + @override + Future recognizeOnce() async { + try { + if (!_isInitialized) { + await initialize(); + } + + final resultCompleter = Completer(); + + // 开始连续识别流程 + final stream = await startContinuousRecognition(); + + // 监听结果 + StreamSubscription? subscription; + subscription = stream.listen((event) { + if (event.type == RecognitionEventType.finalResult) { + // 收到最终结果,完成单次识别 + subscription?.cancel(); + resultCompleter.complete(event); + stopContinuousRecognition(); + } else if (event.type == RecognitionEventType.error) { + // 发生错误,返回错误 + subscription?.cancel(); + resultCompleter.completeError(event.error); + stopContinuousRecognition(); + } + }, onError: (e) { + subscription?.cancel(); + resultCompleter.completeError(e.toString()); + stopContinuousRecognition(); + }, onDone: () { + if (!resultCompleter.isCompleted) { + resultCompleter.complete(RecognitionEvent.error('未收到识别结果')); + } + }); + + // 设置超时 + Timer(const Duration(seconds: 30), () { + if (!resultCompleter.isCompleted) { + subscription?.cancel(); + resultCompleter.complete(RecognitionEvent.error('识别超时')); + stopContinuousRecognition(); + } + }); + + return resultCompleter.future; + } catch (e) { + Logger.error('单次语音识别失败: $e'); + return RecognitionEvent.error(e.toString()); + } + } + + @override + Future> startContinuousRecognition() async { + if (!_isInitialized) { + await initialize(); + } + + if (_isContinuousRecognitionActive) { + await stopContinuousRecognition(); + } + + try { + // 创建结果控制器 + _recognitionController = StreamController.broadcast(); + + // 连接WebSocket + await _connectWebSocket(); + + // 开始录音 + await _startRecording(); + + _isContinuousRecognitionActive = true; + _isRecognitionEndRequested = false; + + Logger.info('连续语音识别已开始'); + + return _recognitionController!.stream; + } catch (e) { + Logger.error('开始连续语音识别失败: $e'); + await _cleanupResources(); + + // 返回一个包含错误的流 + final errorController = StreamController.broadcast(); + errorController.add(RecognitionEvent.error(e.toString())); + errorController.close(); + + return errorController.stream; + } + } + + @override + Future stopContinuousRecognition() async { + if (!_isContinuousRecognitionActive) { + return true; + } + + try { + _isRecognitionEndRequested = true; + + // 先停止录音,以确保不会再发送新的音频数据 + await _stopRecording(); + + // 发送结束指令 + await _sendEndSignal(); + + // 等待服务器处理完所有已发送的数据 + await Future.delayed(const Duration(milliseconds: 500)); + + // 清理资源 + await _cleanupResources(); + + _isContinuousRecognitionActive = false; + Logger.info('连续语音识别已停止'); + + return true; + } catch (e) { + Logger.error('停止连续语音识别失败: $e'); + + // 确保资源被清理 + await _cleanupResources(); + _isContinuousRecognitionActive = false; + + return false; + } + } + + @override + bool isContinuousRecognitionActive() { + return _isContinuousRecognitionActive; + } + + @override + Future dispose() async { + try { + if (_isContinuousRecognitionActive) { + await stopContinuousRecognition(); + } + + // 确保资源被清理 + await _cleanupResources(); + + _isInitialized = false; + Logger.info('火山语音API资源已释放'); + } catch (e) { + Logger.error('释放火山语音API资源失败: $e'); + _isContinuousRecognitionActive = false; + _isInitialized = false; + } + } + + /// 从环境变量加载配置 + void _loadConfig() { + _appKey = dotenv.env['VOLCANO_APP_KEY'] ?? ''; + _secretKey = dotenv.env['VOLCANO_SECRET_KEY'] ?? ''; + _serviceId = dotenv.env['VOLCANO_SERVICE_ID'] ?? ''; + _hostUrl = dotenv.env['VOLCANO_HOST_URL'] ?? 'ws-api-asr.volces.com'; + + if (_appKey.isEmpty || _secretKey.isEmpty || _serviceId.isEmpty) { + throw Exception('未找到火山语音API配置,请在.env文件中设置VOLCANO_APP_KEY, VOLCANO_SECRET_KEY和VOLCANO_SERVICE_ID'); + } + } + + /// 初始化录音机 + Future _initRecorder() async { + try { + // TODO: 正确实现录音机初始化 + // _recorder = Record(); + + // 检查和请求权限 + // final hasPermission = await _recorder!.hasPermission(); + // if (!hasPermission) { + // throw Exception('没有录音权限'); + // } + + Logger.info('录音机初始化成功'); + } catch (e) { + // _recorder = null; + Logger.error('初始化录音机失败: $e'); + rethrow; + } + } + + /// 连接WebSocket + Future _connectWebSocket() async { + if (_isConnecting) { + // 已经有一个连接进程在进行中 + if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { + return _connectionCompleter!.future; + } + } + + _connectionCompleter = Completer(); + _isConnecting = true; + + try { + // 关闭现有连接 + await _closeWebSocket(); + + // 生成新的会话ID + _currentSessionId = _uuid.v4(); + + // 构建URL和请求头 + final (url, headers) = _buildConnectionParams(); + + // 创建WebSocket连接 + if (kIsWeb) { + _channel = WebSocketChannel.connect(Uri.parse(url)); + } else { + _channel = IOWebSocketChannel.connect( + Uri.parse(url), + headers: headers, + ); + } + + // 处理WebSocket消息 + _connectionSubscription = _channel!.stream.listen( + _handleWebSocketMessage, + onError: _handleWebSocketError, + onDone: _handleWebSocketDone, + ); + + // 开始心跳 + _startHeartbeat(); + + // 发送开始识别指令 + await _sendStartCommand(); + + _isConnecting = false; + _connectionCompleter!.complete(); + + Logger.info('WebSocket连接成功'); + } catch (e) { + _isConnecting = false; + if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { + _connectionCompleter!.completeError(e); + } + + Logger.error('连接WebSocket失败: $e'); + rethrow; + } + + return _connectionCompleter!.future; + } + + /// 构建连接参数 + (String, Map?) _buildConnectionParams() { + final timestamp = (DateTime.now().millisecondsSinceEpoch / 1000).floor(); + final nonce = _generateNonce(); + + // 构建表单(用于签名和查询参数) + final form = { + 'algorithm': _algorithm, + 'app_key': _appKey, + 'nonce': nonce, + 'service_id': _serviceId, + 'session_id': _currentSessionId, + 'timestamp': timestamp.toString(), + }; + + // 计算签名 + final signature = _calculateSignature(form); + + // 添加签名到表单 + form['signature'] = signature; + + // 构建URL查询参数 + final queryString = form.entries + .map((e) => '${e.key}=${Uri.encodeComponent(e.value)}') + .join('&'); + + // 构建完整URL + final url = 'wss://$_hostUrl$_path?$queryString'; + + // 构建请求头 + final headers = { + 'User-Agent': 'Deep Voice App/1.0.0', + }; + + return (url, kIsWeb ? null : headers); + } + + /// 生成随机数 + String _generateNonce() { + final random = Random(); + final randoms = List.generate(16, (_) => random.nextInt(256)); + return base64Url.encode(randoms).substring(0, 16); + } + + /// 计算签名 + String _calculateSignature(Map form) { + // 按键排序 + final sortedKeys = form.keys.toList()..sort(); + + // 构建待签名字符串 + final signString = sortedKeys.map((key) => '$key=${form[key]}').join(''); + + // 使用HMAC-SHA1进行签名 + final key = utf8.encode(_secretKey); + final bytes = utf8.encode(signString); + final hmacSha1 = Hmac(sha1, key); + final digest = hmacSha1.convert(bytes); + + // 返回Base64编码的签名 + return base64.encode(digest.bytes); + } + + /// 开始心跳定时 + void _startHeartbeat() { + _heartbeatTimer?.cancel(); + _heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (_) { + _sendHeartbeat(); + }); + } + + /// 发送心跳包 + void _sendHeartbeat() { + if (_channel != null && _isContinuousRecognitionActive) { + try { + final heartbeatMsg = { + 'type': 'heartbeat', + 'sequence': _sendPacketCount++, + }; + + _channel!.sink.add(jsonEncode(heartbeatMsg)); + } catch (e) { + Logger.error('发送心跳包失败: $e'); + } + } + } + + /// 发送开始识别指令 + Future _sendStartCommand() async { + if (_channel == null) return; + + try { + final startCommand = { + 'type': 'start', + 'sequence': _sendPacketCount++, + 'asr_params': { + 'sample_rate': _sampleRate, + 'bits_per_sample': _bitsPerSample, + 'channel': _channels, + 'encoding': _encoding, + 'audio_format': 'raw', + 'lang': _selectedLanguage, + 'vad_enable': true, + 'vad_pause_time': 500, + 'max_seconds': 60, + }, + }; + + _channel!.sink.add(jsonEncode(startCommand)); + Logger.info('已发送开始识别指令'); + } catch (e) { + Logger.error('发送开始识别指令失败: $e'); + rethrow; + } + } + + /// 开始录音 + Future _startRecording() async { + // TODO: 实现正确的录音开始逻辑 + /* + if (_recorder == null) { + await _initRecorder(); + } + + if (_isRecording) { + await _stopRecording(); + } + + try { + // 开始录音 + await _recorder!.start( + encoder: AudioEncoder.pcm16bits, + samplingRate: _sampleRate, + numChannels: _channels, + ); + + _isRecording = true; + + // 设置定时器定期获取音频数据 + _audioTimer = Timer.periodic(const Duration(milliseconds: 200), (_) async { + await _captureAndSendAudio(); + }); + + Logger.info('录音已开始'); + } catch (e) { + _isRecording = false; + Logger.error('开始录音失败: $e'); + rethrow; + } + */ + + // 临时实现,防止错误 + _isRecording = true; + Logger.info('录音已开始(模拟)'); + } + + /// 捕获并发送音频数据 + Future _captureAndSendAudio() async { + // TODO: 实现正确的音频捕获逻辑 + /* + if (!_isRecording || _recorder == null || _channel == null) { + return; + } + + try { + Uint8List? audioData; + + if (Platform.isIOS) { + // iOS 使用文件缓冲区 + final path = await _recorder!.stop(); + if (path == null) return; + + // 读取文件内容 + final file = File(path); + if (await file.exists()) { + audioData = await file.readAsBytes(); + + // 重新开始录音 + await _recorder!.start( + encoder: AudioEncoder.pcm16bits, + samplingRate: _sampleRate, + numChannels: _channels, + ); + } + } else { + // Android 使用stream获取PCM数据 + // 还未实现 + } + + // 如果有音频数据,发送给服务器 + if (audioData != null && audioData.isNotEmpty) { + await _sendAudioData(audioData); + } + } catch (e) { + Logger.error('捕获和发送音频失败: $e'); + } + */ + } + + /// 发送音频数据 + Future _sendAudioData(Uint8List data) async { + if (_channel == null || data.isEmpty) return; + + try { + final audioMessage = { + 'type': 'audio', + 'sequence': _sendPacketCount++, + 'audio': base64.encode(data), + }; + + _channel!.sink.add(jsonEncode(audioMessage)); + } catch (e) { + Logger.error('发送音频数据失败: $e'); + } + } + + /// 发送结束信号 + Future _sendEndSignal() async { + if (_channel == null) return; + + try { + final endCommand = { + 'type': 'end', + 'sequence': _sendPacketCount++, + }; + + _channel!.sink.add(jsonEncode(endCommand)); + Logger.info('已发送结束指令'); + } catch (e) { + Logger.error('发送结束指令失败: $e'); + } + } + + /// 停止录音 + Future _stopRecording() async { + _audioTimer?.cancel(); + _audioTimer = null; + + // TODO: 实现正确的录音停止逻辑 + /* + if (_isRecording && _recorder != null) { + try { + await _recorder!.stop(); + _isRecording = false; + Logger.info('录音已停止'); + } catch (e) { + Logger.error('停止录音失败: $e'); + } + } + */ + + // 临时实现,防止错误 + _isRecording = false; + Logger.info('录音已停止(模拟)'); + } + + /// 处理WebSocket消息 + void _handleWebSocketMessage(dynamic message) { + try { + _receivePacketCount++; + + if (message is String) { + final Map data = jsonDecode(message); + final String type = data['type'] as String? ?? ''; + + switch (type) { + case 'result': + _handleResultMessage(data); + break; + + case 'start_result': + _handleStartResultMessage(data); + break; + + case 'end_result': + _handleEndResultMessage(data); + break; + + case 'error': + _handleErrorMessage(data); + break; + + case 'heartbeat_result': + // 心跳响应,不需要特殊处理 + break; + + default: + Logger.info('收到未知类型的消息: $type'); + } + } + } catch (e) { + Logger.error('处理WebSocket消息失败: $e'); + + if (_recognitionController != null && !_recognitionController!.isClosed) { + _recognitionController!.addError('处理消息失败: $e'); + } + } + } + + /// 处理结果消息 + void _handleResultMessage(Map data) { + try { + final Map? result = data['result'] as Map?; + if (result == null) return; + + final bool isEnd = result['is_end'] as bool? ?? false; + final String text = result['text'] as String? ?? ''; + + if (_recognitionController != null && !_recognitionController!.isClosed) { + if (isEnd) { + // 最终结果 + _recognitionController!.add(RecognitionEvent( + type: RecognitionEventType.finalResult, + text: text, + )); + } else { + // 中间结果 + _recognitionController!.add(RecognitionEvent( + type: RecognitionEventType.intermediateResult, + text: text, + )); + } + } + } catch (e) { + Logger.error('处理结果消息失败: $e'); + } + } + + /// 处理开始结果消息 + void _handleStartResultMessage(Map data) { + final int code = data['code'] as int? ?? -1; + + if (code == 0) { + // 开始成功 + if (_recognitionController != null && !_recognitionController!.isClosed) { + _recognitionController!.add(RecognitionEvent( + type: RecognitionEventType.sessionStarted, + )); + } + } else { + // 开始失败 + final String message = data['message'] as String? ?? '未知错误'; + + Logger.error('语音识别开始失败: [$code] $message'); + + if (_recognitionController != null && !_recognitionController!.isClosed) { + _recognitionController!.add(RecognitionEvent.error('开始失败: [$code] $message')); + } + } + } + + /// 处理结束结果消息 + void _handleEndResultMessage(Map data) { + final int code = data['code'] as int? ?? -1; + + if (code == 0) { + // 结束成功 + Logger.info('语音识别成功结束'); + } else { + // 结束失败 + final String message = data['message'] as String? ?? '未知错误'; + Logger.error('语音识别结束失败: [$code] $message'); + } + + // 无论成功还是失败,通知会话已结束 + if (_recognitionController != null && !_recognitionController!.isClosed) { + _recognitionController!.add(RecognitionEvent( + type: RecognitionEventType.sessionStopped, + )); + } + + // 清理资源 + _cleanupResources(); + } + + /// 处理错误消息 + void _handleErrorMessage(Map data) { + final int code = data['code'] as int? ?? -1; + final String message = data['message'] as String? ?? '未知错误'; + + Logger.error('收到错误消息: [$code] $message'); + + if (_recognitionController != null && !_recognitionController!.isClosed) { + _recognitionController!.add(RecognitionEvent.error('[$code] $message')); + } + } + + /// 处理WebSocket错误 + void _handleWebSocketError(dynamic error) { + Logger.error('WebSocket错误: $error'); + + if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { + _connectionCompleter!.completeError(error); + } + + if (_recognitionController != null && !_recognitionController!.isClosed) { + _recognitionController!.addError(error); + } + + // 清理资源 + _cleanupResources(); + } + + /// 处理WebSocket连接关闭 + void _handleWebSocketDone() { + Logger.info('WebSocket连接已关闭'); + + if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) { + _connectionCompleter!.completeError('WebSocket连接已关闭'); + } + + // 通知会话已结束 + if (_recognitionController != null && !_recognitionController!.isClosed && !_isRecognitionEndRequested) { + _recognitionController!.add(RecognitionEvent( + type: RecognitionEventType.sessionStopped, + )); + } + + // 清理资源 + _cleanupResources(); + } + + /// 关闭WebSocket连接 + Future _closeWebSocket() async { + _connectionSubscription?.cancel(); + _connectionSubscription = null; + + _heartbeatTimer?.cancel(); + _heartbeatTimer = null; + + if (_channel != null) { + try { + _channel!.sink.close(); + _channel = null; + } catch (e) { + Logger.error('关闭WebSocket连接失败: $e'); + } + } + } + + /// 清理所有资源 + Future _cleanupResources() async { + // 停止录音 + await _stopRecording(); + + // 关闭WebSocket + await _closeWebSocket(); + + // 关闭结果控制器 + if (_recognitionController != null && !_recognitionController!.isClosed) { + await _recognitionController!.close(); + _recognitionController = null; + } + + _isContinuousRecognitionActive = false; + _isRecognitionEndRequested = false; + } +} \ No newline at end of file diff --git a/lib/data/services/volcano_tts_api_service.dart b/lib/data/services/speech_impl/volcano_tts_api_service.dart similarity index 99% rename from lib/data/services/volcano_tts_api_service.dart rename to lib/data/services/speech_impl/volcano_tts_api_service.dart index 043a3ab5c..e01e253ed 100644 --- a/lib/data/services/volcano_tts_api_service.dart +++ b/lib/data/services/speech_impl/volcano_tts_api_service.dart @@ -11,7 +11,7 @@ import 'package:web_socket_channel/io.dart'; import 'package:uuid/uuid.dart'; import 'package:just_audio/just_audio.dart'; import 'package:path_provider/path_provider.dart'; -import '../../core/utils/logger.dart'; +import '../../../core/utils/logger.dart'; /// 火山语音合成双向流式API服务 /// diff --git a/lib/data/services/tts_service.dart b/lib/data/services/tts_service.dart new file mode 100644 index 000000000..62d938b56 --- /dev/null +++ b/lib/data/services/tts_service.dart @@ -0,0 +1,83 @@ +import 'dart:async'; + +/// 语音合成服务接口 +abstract class TtsService { + + /// 当前选择的语音 + String get currentVoice; + + /// 当前是否正在播放 + bool get isSpeaking; + + /// TTS事件流,用于监听语音合成事件 + Stream get onEvent; + + /// 初始化语音合成服务 + Future initialize({ + List? supportedLanguages, + }); + + /// 设置语音 + Future setVoice(String voiceName); + + + /// 单次播放文本,完成后返回 + Future speakOnce(String text); + + /// 将文本加入播放队列,支持连续播放多段文本 + Future speakStream(String text); + + /// 播放队列中剩余文本 + Future flushStream(); + + /// 停止播放 + Future stop(); + + /// 释放资源 + Future dispose(); +} + +/// TTS事件类型 +enum TtsEventType { + /// 开始播放 + started, + + /// 播放完成 + completed, + + /// 播放取消 + canceled, + + /// 播放错误 + error, +} + +/// TTS事件 +class TtsEvent { + /// 事件类型 + final TtsEventType type; + + /// 错误信息(仅在 error 类型中有效) + final String error; + + TtsEvent({ + required this.type, + this.error = '', + }); + + /// 创建错误事件的快捷构造函数 + factory TtsEvent.error(String errorMessage) { + return TtsEvent( + type: TtsEventType.error, + error: errorMessage, + ); + } + + /// 检查是否为错误 + bool get isError => type == TtsEventType.error; + + @override + String toString() { + return 'TtsEvent{type: $type, error: $error}'; + } +} \ No newline at end of file diff --git a/lib/data/services/voice_interaction_service.dart b/lib/data/services/voice_interaction_service.dart index 13fc08c66..88dbd0d1d 100644 --- a/lib/data/services/voice_interaction_service.dart +++ b/lib/data/services/voice_interaction_service.dart @@ -5,8 +5,10 @@ import '../../core/utils/logger.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import '../../modules/chat/models/message_model.dart'; import 'chat_history_service.dart'; -import '../../data/services/azure_asr_service.dart'; -import '../../data/services/azure_tts_service.dart'; +import 'speech_impl/azure_asr_service.dart'; +import 'speech_impl/azure_tts_service.dart'; +import 'asr_service.dart'; +import 'tts_service.dart'; /// 语音交互服务 /// diff --git a/lib/data/services/volcano_asr_api_service.dart b/lib/data/services/volcano_asr_api_service.dart deleted file mode 100644 index b794dddec..000000000 --- a/lib/data/services/volcano_asr_api_service.dart +++ /dev/null @@ -1,1003 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math'; -import 'dart:typed_data'; -import 'package:crypto/crypto.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:get/get.dart'; -import 'package:uuid/uuid.dart'; -import 'package:web_socket_channel/io.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; -import '../../core/utils/logger.dart'; -import 'package:record/record.dart'; - -/// 识别事件类型 -enum RecognitionEventType { - started, - recognizing, - finalResult, - error, -} - -/// 识别事件 -class RecognitionEvent { - final RecognitionEventType type; - final String text; - final String? error; - - RecognitionEvent({ - required this.type, - this.text = '', - this.error, - }); -} - -/// 火山语音识别API服务 -/// -/// 该服务提供了通过WebSocket与火山语音大模型流式识别API交互的接口 -class VolcanoAsrApiService extends GetxService { - // 协议常量 - static const int _protocolVersion = 0x01; - static const int _defaultHeaderSize = 0x01; - - // 消息类型 - static const int _fullClientRequest = 0x01; - static const int _audioOnlyRequest = 0x02; - static const int _fullServerResponse = 0x09; - static const int _serverAck = 0x0B; - static const int _serverErrorResponse = 0x0F; - - // 消息类型特定标志 - static const int _noSequence = 0x00; - static const int _posSequence = 0x01; - static const int _negSequence = 0x02; - static const int _negWithSequence = 0x03; - - // 消息序列化方法 - static const int _noSerialization = 0x00; - static const int _json = 0x01; - - // 消息压缩方法 - static const int _noCompression = 0x00; - static const int _gzip = 0x01; - - // 配置参数 - late final String _appId; - late final String _token; - late final String _resourceId; - late final String _apiUrl; - - // WebSocket连接 - WebSocketChannel? _webSocketChannel; - - // 状态变量 - final _isListening = false.obs; - final _isConnected = false.obs; - final _errorMessage = ''.obs; - final _recognitionResults = [].obs; - - // 序列号 - int _sequence = 0; - - // 识别结果流控制器 - final _recognitionStreamController = StreamController.broadcast(); - - // 最新的识别结果 - final _latestRecognizedText = ''.obs; - String get latestRecognizedText => _latestRecognizedText.value; - - // 获取可观察状态 - RxBool get isListening => _isListening; - bool get isConnected => _isConnected.value; - String get errorMessage => _errorMessage.value; - List get recognitionResults => _recognitionResults; - - // 识别结果流 - Stream get recognitionStream => _recognitionStreamController.stream; - - // 音频录制相关 - final _audioRecorder = AudioRecorder(); - final _isRecording = false.obs; - bool get isRecording => _isRecording.value; - - // 音频数据缓冲区 - final List _audioBuffer = []; - StreamSubscription? _audioStreamSubscription; - - // 音频发送控制 - DateTime _lastAudioSendTime = DateTime.now(); - static const Duration _minAudioSendInterval = Duration(milliseconds: 100); - static const int _minAudioBufferSize = 3200; // 约200ms的16kHz 16bit音频 - - // 音频录制配置 - static const int _defaultSampleRate = 16000; - static const int _defaultBitsPerSample = 16; - static const int _defaultChannels = 1; - - VolcanoAsrApiService() { - // 从环境变量获取配置 - _appId = dotenv.env['VOLCANO_ASR_APP_ID'] ?? ''; - _token = dotenv.env['VOLCANO_ASR_APP_TOKEN'] ?? ''; - _resourceId = dotenv.env['VOLCANO_ASR_RESOURCE_ID'] ?? 'volc.bigasr.sauc.duration'; - _apiUrl = dotenv.env['VOLCANO_ASR_API_URL'] ?? 'wss://openspeech.bytedance.com/api/v3/sauc/bigmodel'; - - Logger.info('火山语音识别API配置: APP_ID=${_appId.isNotEmpty ? "已设置" : "未设置"}, APP_TOKEN=${_token.isNotEmpty ? "已设置" : "未设置"}'); - Logger.info('资源ID: $_resourceId'); - Logger.info('API URL: $_apiUrl'); - - if (_appId.isEmpty || _token.isEmpty) { - _errorMessage.value = '火山语音识别API配置不完整,请检查环境变量'; - Logger.error(_errorMessage.value); - } - } - - @override - void onInit() { - super.onInit(); - } - - /// 连接到WebSocket服务器 - Future connect({ - required int sampleRate, - required int bitsPerSample, - required int channels, - String format = 'pcm', - String codec = 'raw', - String modelName = 'bigmodel', - bool enablePunc = true, - }) async { - if (_isConnected.value) { - Logger.warning('已经连接到WebSocket服务器'); - return true; - } - - if (_appId.isEmpty || _token.isEmpty) { - _errorMessage.value = '火山语音识别API配置不完整,请检查环境变量'; - Logger.error(_errorMessage.value); - return false; - } - Logger.info('开始连接到ASR WebSocket服务器'); - try { - _errorMessage.value = ''; - _recognitionResults.clear(); - _sequence = 0; - - // 构建请求头 - final Map headers = { - 'X-Api-App-Key': _appId, - 'X-Api-Access-Key': _token, - 'X-Api-Resource-Id': _resourceId, - 'X-Api-Connect-Id': const Uuid().v4(), - }; - - Logger.info('连接到WebSocket服务器: $_apiUrl'); - - // 创建WebSocket连接 - if (kIsWeb) { - // Web平台不支持在连接时添加headers,需要使用其他方式 - // 可以考虑将认证信息添加到URL中 - final Uri uri = Uri.parse(_apiUrl).replace( - queryParameters: { - 'app_key': _appId, - 'access_key': _token, - 'resource_id': _resourceId, - 'connect_id': const Uuid().v4(), - }, - ); - Logger.info('Web平台连接URL: ${uri.toString()}'); - _webSocketChannel = WebSocketChannel.connect(uri); - } else { - // 非Web平台使用IOWebSocketChannel - _webSocketChannel = IOWebSocketChannel.connect( - Uri.parse(_apiUrl), - headers: headers, - pingInterval: const Duration(seconds: 10), // 添加ping间隔,保持连接活跃 - ); - } - - // 监听WebSocket消息 - _webSocketChannel!.stream.listen( - _handleWebSocketMessage, - onError: _handleWebSocketError, - onDone: _handleWebSocketDone, - ); - - // 发送全客户端请求 - await _sendFullClientRequest( - sampleRate: sampleRate, - bitsPerSample: bitsPerSample, - channels: channels, - format: format, - codec: codec, - modelName: modelName, - enablePunc: enablePunc, - ); - - _isConnected.value = true; - _isListening.value = true; - - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.started, - text: '', - )); - - Logger.info('ASR WebSocket连接成功建立'); - - return true; - } catch (e) { - _errorMessage.value = '连接WebSocket服务器失败: $e'; - Logger.error('连接WebSocket服务器失败: $e'); - _cleanupWebSocket(); - return false; - } - } - - /// 发送全客户端请求 - Future _sendFullClientRequest({ - required int sampleRate, - required int bitsPerSample, - required int channels, - required String format, - required String codec, - required String modelName, - required bool enablePunc, - }) async { - // 构建payload - final Map payload = { - 'user': { - 'uid': 'user_${DateTime.now().millisecondsSinceEpoch}', - }, - 'audio': { - 'format': format, - 'sample_rate': sampleRate, - 'bits': bitsPerSample, - 'channel': channels, - 'codec': codec, - }, - 'request': { - 'model_name': modelName, - 'enable_punc': enablePunc, - 'show_utterances': true, // 启用分句信息,用于区分中间结果和最终结果 - 'result_type': 'all', // 返回所有分句结果 - 'enable_words': true, // 启用词级别时间戳 - 'enable_itn': false, // 启用数字转换 - 'continuous_decoding': true, // 启用连续解码,支持连续监听 - 'vad_silence_end': 800, // 设置静音结束阈值,单位为毫秒,用于判断一句话的结束 - 'vad_max_duration': 60000, // 设置最大语音段时长,单位为毫秒 - }, - }; - - // 序列化并压缩payload - final String payloadStr = jsonEncode(payload); - // Logger.info('发送全客户端请求: $payloadStr'); - final List payloadBytes = await _gzipCompress(utf8.encode(payloadStr)); - - // 增加序列号 - _sequence = 1; - - // 构建消息 - final List message = _buildMessage( - messageType: _fullClientRequest, - messageTypeSpecificFlags: _posSequence, - serializationMethod: _json, - compressionType: _gzip, - sequence: _sequence, - payload: payloadBytes, - ); - - // 发送消息 - _webSocketChannel!.sink.add(Uint8List.fromList(message)); - } - - /// 发送音频数据 - Future sendAudioData(List audioData, {bool isLast = false}) async { - if (!_isConnected.value || _webSocketChannel == null) { - return false; - } - - try { - // 如果是最后一个包,直接发送 - if (isLast) { - // 先发送缓冲区中的数据 - if (_audioBuffer.isNotEmpty) { - await _sendAudioDataInternal(_audioBuffer, false); - _audioBuffer.clear(); - } - - // 发送最后一个包 - return await _sendAudioDataInternal([0, 0, 0, 0, 0, 0, 0, 0], true); - } - - // 将新的音频数据添加到缓冲区 - _audioBuffer.addAll(audioData); - - // 检查是否应该发送缓冲区中的数据 - final now = DateTime.now(); - final timeSinceLastSend = now.difference(_lastAudioSendTime); - - if (_audioBuffer.length >= _minAudioBufferSize || timeSinceLastSend >= _minAudioSendInterval) { - // 创建缓冲区的副本并清空缓冲区 - final bufferToSend = List.from(_audioBuffer); - _audioBuffer.clear(); - - // 更新最后发送时间 - _lastAudioSendTime = now; - - // 发送缓冲区中的数据 - return await _sendAudioDataInternal(bufferToSend, false); - } - - return true; - } catch (e) { - Logger.error('发送音频数据失败: $e'); - return false; - } - } - - /// 内部方法:实际发送音频数据 - Future _sendAudioDataInternal(List audioData, bool isLast) async { - if (!_isConnected.value || _webSocketChannel == null) { - return false; - } - - try { - // 增加序列号 - _sequence++; - - // 如果是最后一个音频包,使用负序列号 - final int sequence = isLast ? -_sequence : _sequence; - final int messageTypeSpecificFlags = isLast ? _negWithSequence : _posSequence; - - // 压缩音频数据 - List compressedAudio; - if (audioData.isEmpty && isLast) { - // 对于最后一个空包,使用一个有效的GZIP数据 - compressedAudio = [31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - } else { - compressedAudio = await _gzipCompress(audioData); - } - - // 构建消息 - final List message = _buildMessage( - messageType: _audioOnlyRequest, - messageTypeSpecificFlags: messageTypeSpecificFlags, - serializationMethod: _noSerialization, - compressionType: _gzip, - sequence: sequence, - payload: compressedAudio, - ); - - // 发送消息 - _webSocketChannel!.sink.add(Uint8List.fromList(message)); - - - return true; - } catch (e) { - Logger.error('发送音频数据失败: $e'); - return false; - } - } - - /// 构建消息 - List _buildMessage({ - required int messageType, - required int messageTypeSpecificFlags, - required int serializationMethod, - required int compressionType, - required int sequence, - required List payload, - }) { - // 构建头部 - final List header = [ - (_protocolVersion << 4) | _defaultHeaderSize, - (messageType << 4) | messageTypeSpecificFlags, - (serializationMethod << 4) | compressionType, - 0, // 保留字段 - ]; - - // 序列号 - final List sequenceBytes = _intToBytes(sequence); - - // payload大小 - final List payloadSizeBytes = _intToBytes(payload.length); - - // 组装消息 - final List message = [ - ...header, - ...sequenceBytes, - ...payloadSizeBytes, - ...payload, - ]; - - return message; - } - - /// 处理WebSocket消息 - void _handleWebSocketMessage(dynamic message) { - if (message is! List) { - // 尝试解析非二进制消息作为JSON错误 - try { - if (message is String) { - final Map errorJson = jsonDecode(message); - if (errorJson.containsKey('error')) { - final String errorMessage = errorJson['error']; - Logger.warning('收到WebSocket错误消息: $errorMessage'); - - _errorMessage.value = '服务器错误: $errorMessage'; - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.error, - error: _errorMessage.value, - )); - - // 如果是严重错误,关闭连接 - if (errorMessage.contains('decode ws request failed') || - errorMessage.contains('unable to ungzip payload')) { - _isListening.value = false; - _isConnected.value = false; - _cleanupWebSocket(); - } - - return; - } - } - } catch (e) { - // 解析失败,使用原始消息 - Logger.warning('收到非二进制WebSocket消息: $message'); - } - return; - } - - try { - final List data = message; - - // 确保消息长度足够 - if (data.length < 12) { - Logger.warning('WebSocket消息长度不足: ${data.length}字节'); - return; - } - - // 解析头部 - final int protocolVersion = (data[0] >> 4) & 0x0F; - final int headerSize = data[0] & 0x0F; - final int messageType = (data[1] >> 4) & 0x0F; - final int messageTypeSpecificFlags = data[1] & 0x0F; - final int serializationMethod = (data[2] >> 4) & 0x0F; - final int compressionType = data[2] & 0x0F; - - // 解析序列号 - final List sequenceBytes = data.sublist(4, 8); - final int sequence = _bytesToInt(sequenceBytes); - - // 解析payload大小 - final List payloadSizeBytes = data.sublist(8, 12); - final int payloadSize = _bytesToInt(payloadSizeBytes); - - // 确保payload长度正确 - if (data.length < 12 + payloadSize) { - Logger.warning('WebSocket消息payload长度不足: 预期${payloadSize}字节,实际${data.length - 12}字节'); - return; - } - - // 解析payload - final List payload = data.sublist(12, 12 + payloadSize); - - // 检查是否是最后一个包(负序列号) - final bool isLastPackage = sequence < 0; - - // 处理不同类型的消息 - if (messageType == _fullServerResponse) { - _handleFullServerResponse(payload, compressionType, isLastPackage); - } else if (messageType == _serverAck) { - _handleServerAck(payload); - } else if (messageType == _serverErrorResponse) { - _handleServerErrorResponse(sequence, payload); - } - - // 如果是最后一个包,记录日志但不断开连接 - if (isLastPackage) { - Logger.info('收到最后一个包,序列号: $sequence,继续监听'); - } - } catch (e) { - Logger.error('处理WebSocket消息失败: $e'); - - _errorMessage.value = '处理WebSocket消息失败: $e'; - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.error, - error: _errorMessage.value, - )); - } - } - - /// 处理全服务器响应 - Future _handleFullServerResponse(List payload, int compressionType, bool isLastPackage) async { - try { - String payloadStr; - - if (compressionType == _gzip) { - final List decompressed = await _gzipDecompress(payload); - payloadStr = utf8.decode(decompressed); - } else { - payloadStr = utf8.decode(payload); - } - - // 解析JSON - final Map response = jsonDecode(payloadStr); - - - - // 处理识别结果 - if (response.containsKey('result')) { - // 处理分句信息 - if (response['result'] is Map && - response['result'].containsKey('utterances') && - response['result']['utterances'] is List && - response['result']['utterances'].isNotEmpty) { - - // 遍历所有分句 - for (final utterance in response['result']['utterances']) { - final String text = utterance['text'] ?? ''; - final bool isDefinite = utterance['definite'] ?? false; - - if (text.isNotEmpty) { - if (isDefinite) { - // 当definite为true时,表示这是一个完整的句子,作为最终结果处理 - if (!_recognitionResults.contains(text)) { - _recognitionResults.add(text); - _latestRecognizedText.value = text; - - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.finalResult, - text: text, - )); - - // Logger.info('添加最终识别结果: $text'); - - // 重要:在这里不要断开连接,而是继续监听下一句话 - // 但可以清空当前的中间结果状态,准备接收新的语音输入 - _latestRecognizedText.value = ''; - - // 关键修改:向服务器发送一个短暂的静音包,触发服务器端的分段 - // 这有助于确保下一次识别从新的内容开始 - if (_isConnected.value && _webSocketChannel != null && _isRecording.value) { - // 发送一个极短的静音包,不会中断录音,但会触发服务器重置状态 - sendAudioData([0, 0, 0, 0, 0, 0, 0, 0], isLast: false); - } - } - } else { - // 当definite为false时,表示这是一个中间结果 - // 只有当文本与最新的不同时才发送,避免重复 - if (text != _latestRecognizedText.value) { - _latestRecognizedText.value = text; - - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.recognizing, - text: text, - )); - - // Logger.info('添加中间识别结果: $text'); - } - } - } - } - } else if (response['result'] is Map && response['result'].containsKey('text')) { - // 如果没有分句信息但有完整文本,则根据isLastPackage判断是否作为最终结果 - final String fullText = response['result']['text'] ?? ''; - - if (fullText.isNotEmpty && fullText != _latestRecognizedText.value) { - _latestRecognizedText.value = fullText; - - // 如果是最后一个包,则作为最终结果处理 - if (isLastPackage) { - if (!_recognitionResults.contains(fullText)) { - _recognitionResults.add(fullText); - - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.finalResult, - text: fullText, - )); - - Logger.info('添加最终识别结果(最后一个包): $fullText'); - - // 重要:清空当前的中间结果状态,准备接收新的语音输入 - _latestRecognizedText.value = ''; - - // 关键修改:向服务器发送一个短暂的静音包,触发服务器端的分段 - if (_isConnected.value && _webSocketChannel != null && _isRecording.value) { - // 发送一个极短的静音包,不会中断录音,但会触发服务器重置状态 - sendAudioData([0, 0, 0, 0, 0, 0, 0, 0], isLast: false); - } - } - } else { - // 否则作为中间结果处理 - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.recognizing, - text: fullText, - )); - - Logger.info('添加中间识别结果(无分句): $fullText'); - } - } - } - } - - // 如果是最后一个包,记录日志 - if (isLastPackage) { - Logger.info('收到最后一个包,继续监听'); - } - } catch (e) { - Logger.error('处理全服务器响应失败: $e'); - } - } - - /// 处理服务器确认 - void _handleServerAck(List payload) { - try { - final String payloadStr = utf8.decode(payload); - Logger.info('收到服务器确认: $payloadStr'); - } catch (e) { - Logger.error('处理服务器确认失败: $e'); - } - } - - /// 处理服务器错误响应 - void _handleServerErrorResponse(int errorCode, List payload) { - try { - final String errorMessage = utf8.decode(payload); - Logger.error('服务器错误: 错误码=$errorCode, 错误信息=$errorMessage'); - - _errorMessage.value = '服务器错误: $errorMessage (错误码: $errorCode)'; - - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.error, - error: _errorMessage.value, - )); - - // 对于任何服务器错误,主动关闭连接 - _isListening.value = false; - _isConnected.value = false; - _cleanupWebSocket(); - - // 如果正在录音,也停止录音 - if (_isRecording.value) { - stopRecognition(); - } - } catch (e) { - Logger.error('处理服务器错误响应失败: $e'); - } - } - - /// 处理最后一个包 - void _handleLastPackage() { - - // 确保最后的文本被作为最终结果发送 - if (_latestRecognizedText.value.isNotEmpty && - !_recognitionResults.contains(_latestRecognizedText.value)) { - _recognitionResults.add(_latestRecognizedText.value); - - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.finalResult, - text: _latestRecognizedText.value, - )); - - Logger.info('添加最终识别结果(最后一个包): ${_latestRecognizedText.value}'); - } - - _isListening.value = false; - _cleanupWebSocket(); - } - - /// 处理WebSocket错误 - void _handleWebSocketError(dynamic error) { - // 避免重复记录相同的错误 - if (_errorMessage.value.contains(error.toString())) { - return; - } - - // 简化错误消息处理 - final String errorStr = error.toString().toLowerCase(); - String errorMessage; - - if (errorStr.contains('not upgraded to websocket') || errorStr.contains('status code: 400')) { - errorMessage = '认证失败,请检查API密钥'; - } else if (errorStr.contains('connection refused') || errorStr.contains('failed host lookup')) { - errorMessage = '无法连接到服务器,请检查网络'; - } else if (errorStr.contains('timeout')) { - errorMessage = '连接超时'; - } else { - errorMessage = '连接错误: $error'; - } - - _errorMessage.value = errorMessage; - Logger.error('WebSocket错误: $errorMessage'); - - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.error, - error: errorMessage, - )); - - // 清理资源 - _isListening.value = false; - _isConnected.value = false; - _cleanupWebSocket(); - } - - /// 处理WebSocket关闭 - void _handleWebSocketDone() { - Logger.info('ASR WebSocket连接已关闭'); - - _isListening.value = false; - _isConnected.value = false; - _cleanupWebSocket(); - } - - /// 清理WebSocket连接 - void _cleanupWebSocket() { - if (_webSocketChannel == null) { - return; - } - - try { - // 先关闭sink,然后等待stream自然关闭 - _webSocketChannel?.sink.close(WebSocketStatus.normalClosure, '客户端主动关闭连接'); - } catch (e) { - Logger.error('关闭WebSocket连接失败: $e'); - } finally { - _webSocketChannel = null; - } - } - - /// 检查麦克风权限 - Future checkMicrophonePermission() async { - try { - final hasPermission = await _audioRecorder.hasPermission(); - return hasPermission; - } catch (e) { - Logger.error('检查麦克风权限失败: $e'); - return false; - } - } - - /// 开始录音并识别 - Future startRecognition({ - int sampleRate = _defaultSampleRate, - int bitsPerSample = _defaultBitsPerSample, - int channels = _defaultChannels, - String format = 'pcm', - String codec = 'raw', - String modelName = 'bigmodel', - bool enablePunc = true, - }) async { - try { - // 如果已经在录音,先停止 - if (_isRecording.value) { - await stopRecognition(); - // 添加短暂延迟,确保之前的会话完全关闭 - await Future.delayed(const Duration(milliseconds: 500)); - } - - // 检查麦克风权限 - final hasPermission = await checkMicrophonePermission(); - if (!hasPermission) { - _errorMessage.value = '没有麦克风权限'; - Logger.error(_errorMessage.value); - return false; - } - - // 清空识别结果 - _recognitionResults.clear(); - _latestRecognizedText.value = ''; - - // 清空音频缓冲区 - _audioBuffer.clear(); - _lastAudioSendTime = DateTime.now(); - - // 连接到ASR服务 - final connected = await connect( - sampleRate: sampleRate, - bitsPerSample: bitsPerSample, - channels: channels, - format: format, - codec: codec, - modelName: modelName, - enablePunc: enablePunc, - ); - - if (!connected) { - return false; - } - - // 配置录音 - final config = RecordConfig( - encoder: AudioEncoder.pcm16bits, - sampleRate: sampleRate, - numChannels: channels, - bitRate: bitsPerSample * sampleRate * channels, - ); - - Logger.info('开始录音,采样率: $sampleRate Hz, 位深: $bitsPerSample bits, 通道数: $channels'); - - // 开始录音流 - final stream = await _audioRecorder.startStream(config); - - _isRecording.value = true; - - // 订阅音频流 - _audioStreamSubscription = stream.listen( - (data) { - if (_isConnected.value && _isRecording.value && _webSocketChannel != null) { - sendAudioData(data.toList()); - } - }, - onError: (error) { - Logger.error('音频流错误: $error'); - _errorMessage.value = '音频流错误: $error'; - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.error, - error: _errorMessage.value, - )); - }, - onDone: () { - Logger.info('音频流结束'); - if (_isRecording.value) { - stopRecognition(); - } - }, - cancelOnError: false, - ); - - return true; - } catch (e) { - _errorMessage.value = '开始录音失败: $e'; - Logger.error(_errorMessage.value); - - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.error, - error: _errorMessage.value, - )); - - return false; - } - } - - /// 停止录音并完成识别 - Future stopRecognition() async { - if (!_isRecording.value) { - return true; - } - - try { - _isRecording.value = false; - - // 取消音频流订阅 - await _audioStreamSubscription?.cancel(); - _audioStreamSubscription = null; - - // 停止录音 - await _audioRecorder.stop(); - - // 发送最后一个音频包,标记为结束 - if (_isConnected.value && _webSocketChannel != null) { - try { - // 发送最后一个包并等待处理 - await sendAudioData([0, 0, 0, 0, 0, 0, 0, 0], isLast: true); - await Future.delayed(const Duration(milliseconds: 300)); - } catch (e) { - Logger.error('发送最后一个音频包失败: $e'); - } - - // 清空当前状态 - _latestRecognizedText.value = ''; - } - - // 断开连接 - await disconnect(); - - return true; - } catch (e) { - Logger.error('停止录音失败: $e'); - await disconnect(); - return false; - } - } - - /// 断开连接 - Future disconnect() async { - Logger.info('断开ASR连接'); - // 避免递归调用 - if (_isConnected.value && _isRecording.value) { - _isRecording.value = false; - - // 取消音频流订阅 - await _audioStreamSubscription?.cancel(); - _audioStreamSubscription = null; - - // 停止录音 - try { - await _audioRecorder.stop(); - } catch (e) { - Logger.error('停止录音失败: $e'); - } - } - - _cleanupWebSocket(); - _isConnected.value = false; - _isListening.value = false; - - // 确保在断开连接时不会有未处理的事件 - if (!_recognitionStreamController.isClosed && _recognitionStreamController.hasListener) { - try { - // 发送一个最终事件,表示连接已断开 - _recognitionStreamController.add(RecognitionEvent( - type: RecognitionEventType.error, - error: '连接已断开', - )); - } catch (e) { - Logger.error('发送断开连接事件失败: $e'); - } - } - } - - /// 整数转字节数组 - List _intToBytes(int value) { - return [ - (value >> 24) & 0xFF, - (value >> 16) & 0xFF, - (value >> 8) & 0xFF, - value & 0xFF, - ]; - } - - /// 字节数组转整数 - int _bytesToInt(List bytes) { - if (bytes.length != 4) { - throw ArgumentError('字节数组长度必须为4'); - } - - return ((bytes[0] & 0xFF) << 24) | - ((bytes[1] & 0xFF) << 16) | - ((bytes[2] & 0xFF) << 8) | - (bytes[3] & 0xFF); - } - - /// GZIP压缩 - Future> _gzipCompress(List data) async { - if (data.isEmpty) { - // 返回一个有效的空GZIP数据流 - return [31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - } - - try { - return gzip.encode(data); - } catch (e) { - Logger.error('GZIP压缩失败: $e'); - // 返回原始数据,但这可能导致协议错误 - throw Exception('GZIP压缩失败: $e'); - } - } - - /// GZIP解压缩 - Future> _gzipDecompress(List data) async { - if (data.isEmpty) { - return []; - } - - try { - return gzip.decode(data); - } catch (e) { - Logger.error('GZIP解压缩失败: $e,数据长度: ${data.length}'); - // 无法解压缩,返回原始数据 - throw Exception('GZIP解压缩失败: $e'); - } - } - - @override - void onClose() { - stopRecognition(); - disconnect(); - _audioRecorder.dispose(); - _recognitionStreamController.close(); - super.onClose(); - } -} \ No newline at end of file diff --git a/lib/data/services/volcano_tts_service.dart b/lib/data/services/volcano_tts_service.dart deleted file mode 100644 index 994fb0c4e..000000000 --- a/lib/data/services/volcano_tts_service.dart +++ /dev/null @@ -1,743 +0,0 @@ -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 bfe96ed45..5618dccaa 100644 --- a/lib/modules/chat/bindings/chat_binding.dart +++ b/lib/modules/chat/bindings/chat_binding.dart @@ -1,29 +1,15 @@ 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/speech_impl/azure_asr_service.dart'; +import '../../../data/services/speech_impl/azure_tts_service.dart'; +import '../../../data/services/asr_service.dart'; +import '../../../data/services/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()); diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index fd4dc4772..152e8d8f5 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -3,19 +3,18 @@ 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/azure_tts_service.dart'; -import '../../../data/services/azure_asr_service.dart'; +import '../../../data/services/tts_service.dart'; import '../../../data/providers/agent_provider.dart'; import '../../../data/models/agent.dart'; import '../../../core/utils/logger.dart'; import '../../../data/services/chat_history_service.dart'; import '../../../data/services/voice_interaction_service.dart'; - +import '../../../data/services/asr_service.dart'; class ChatController extends GetxController { // 服务 final VolcanoAIService _aiService = Get.find(); - final AzureTtsService _ttsService = Get.find(); - final AzureAsrService _asrService = Get.find(); + late TtsService _ttsService; + late AsrService _asrService; final ChatHistoryService _historyService = Get.find(); final VoiceInteractionService _voiceInteractionService = Get.find(); @@ -58,6 +57,7 @@ class ChatController extends GetxController { super.onInit(); Logger.info('ChatController onInit'); + // 从路由参数获取Agent信息 final Map args = Get.arguments ?? {}; final String agentId = args['agentId'] ?? 'personal_assistant'; @@ -72,6 +72,9 @@ class ChatController extends GetxController { agent = foundAgent; + _ttsService = Get.find(); + _asrService = Get.find(); + // 设置TTS服务 await _ttsService.setVoice(agent.voice); @@ -308,7 +311,7 @@ class ChatController extends GetxController { // 如果TTS启用,使用流式处理发送到TTS if (isTtsEnabled.value) { - _ttsService.processStreamText(chunk); + _ttsService.speakStream(chunk); } }, onDone: () { @@ -319,7 +322,7 @@ class ChatController extends GetxController { // 如果TTS启用,处理缓冲区中的剩余文本 if (isTtsEnabled.value) { - _ttsService.flushStreamBuffer(); + _ttsService.flushStream(); } // 保存聊天历史 diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index e575c5c56..4abeb6d55 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -489,7 +489,7 @@ class ChatView extends GetView { ), child: SafeArea( child: SizedBox( - height: 120.h, + height: 80.h, child: Padding( padding: EdgeInsets.symmetric(horizontal: 20.w), child: Row( diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index 175bb2ce8..84e8a25fa 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -3,7 +3,6 @@ import 'package:get/get.dart'; import '../controllers/profile_controller.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../core/widgets/common_bottom_nav.dart'; -import '../../../data/services/audio_service.dart'; import '../../../routes/app_routes.dart'; class ProfileView extends GetView { diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 3b806ecf2..1ab68e829 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import '../../../core/themes/theme_service.dart'; +import '../../../data/services/speech_factory.dart'; class SettingsController extends GetxController { // 主题服务 @@ -14,6 +15,13 @@ class SettingsController extends GetxController { final RxString targetLanguage = '英文'.obs; final List availableLanguages = ['中文', '英文', '日文', '韩文', '法文', '德文', '西班牙文', '俄文']; + // 语音服务类型 + final Rx speechServiceType = SpeechServiceType.azure.obs; + final List> availableSpeechServices = [ + {'type': SpeechServiceType.azure, 'name': 'Azure 语音服务', 'description': '微软云端语音服务,支持高质量语音合成和识别'}, + {'type': SpeechServiceType.flutter, 'name': 'Flutter 本地语音服务', 'description': '设备本地语音服务,无需联网即可使用'}, + ]; + @override void onInit() { super.onInit(); @@ -23,10 +31,23 @@ class SettingsController extends GetxController { // 设置当前主题状态 isDarkMode.value = savedDarkMode; + + // 获取当前语音服务类型 + _loadCurrentSpeechServiceType(); + } + + // 加载当前语音服务类型 + void _loadCurrentSpeechServiceType() { + try { + final factory = Get.find(); + speechServiceType.value = factory.currentServiceType; + } catch (e) { + // 如果找不到SpeechFactory实例,使用默认值 + } } // 切换主题 - void toggleTheme() { + Future toggleTheme() async { // 切换暗黑模式状态 isDarkMode.value = !isDarkMode.value; @@ -34,10 +55,18 @@ class SettingsController extends GetxController { Get.changeThemeMode(isDarkMode.value ? ThemeMode.dark : ThemeMode.light); // 保存用户选择的主题设置 - 确保立即写入存储 - _themeService.saveThemeMode(isDarkMode.value); - - // 打印日志,确认主题已保存 - print('主题已切换,当前模式: ${isDarkMode.value ? "暗黑" : "浅色"}'); + await _themeService.saveThemeMode(isDarkMode.value); + } + + // 设置语音服务类型 + void setSpeechServiceType(SpeechServiceType type) { + try { + final factory = Get.find(); + factory.setServiceType(type); + speechServiceType.value = type; + } catch (e) { + // 处理错误 + } } void setSourceLanguage(String language) { diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 683076e9c..d66e6e9c9 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -113,6 +113,7 @@ class SettingsView extends GetView { fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black87, ), + overflow: TextOverflow.ellipsis, ), SizedBox(height: 4.h), Row( @@ -275,6 +276,11 @@ class SettingsView extends GetView { ), Divider(height: 1, color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]), + // 语音服务选择 + _buildSpeechServiceSetting(isDarkMode), + + Divider(height: 1, color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200]), + // 隐私设置 _buildSimpleNavigationSetting( title: '隐私设置', @@ -498,46 +504,52 @@ class SettingsView extends GetView { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Container( - width: 32.w, - height: 32.w, - decoration: BoxDecoration( - color: iconBgColor, - borderRadius: BorderRadius.circular(16.r), - ), - child: Icon( - icon, - size: 16.w, - color: iconColor, - ), - ), - SizedBox(width: 12.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, - ), + Expanded( + child: Row( + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: iconBgColor, + borderRadius: BorderRadius.circular(16.r), ), - if (subtitle != null) - SizedBox(height: 2.h), - if (subtitle != null) - Text( - subtitle, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, + child: Icon( + icon, + size: 16.w, + color: iconColor, + ), + ), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + overflow: TextOverflow.ellipsis, ), - ), - ], - ), - ], + if (subtitle != null) + SizedBox(height: 2.h), + if (subtitle != null) + Text( + subtitle, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), ), Switch( value: value, @@ -568,54 +580,64 @@ class SettingsView extends GetView { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Container( - width: 32.w, - height: 32.w, - decoration: BoxDecoration( - color: iconBgColor, - borderRadius: BorderRadius.circular(16.r), - ), - child: Icon( - icon, - size: 16.w, - color: iconColor, - ), - ), - SizedBox(width: 12.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: isDarkMode ? Colors.white : Colors.black87, - ), + Expanded( + child: Row( + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: iconBgColor, + borderRadius: BorderRadius.circular(16.r), ), - if (subtitle != null) - SizedBox(height: 2.h), - if (subtitle != null) - Text( - subtitle, - style: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.black54, + child: Icon( + icon, + size: 16.w, + color: iconColor, + ), + ), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + overflow: TextOverflow.ellipsis, ), - ), - ], - ), - ], + if (subtitle != null) + SizedBox(height: 2.h), + if (subtitle != null) + Text( + subtitle, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), ), Row( + mainAxisSize: MainAxisSize.min, children: [ - Text( - value, - style: valueStyle ?? TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.white : Colors.black87, + Flexible( + child: Text( + value, + style: valueStyle ?? TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.white : Colors.black87, + ), + overflow: TextOverflow.ellipsis, ), ), SizedBox(width: 8.w), @@ -659,46 +681,52 @@ class SettingsView extends GetView { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Container( - width: 32.w, - height: 32.w, - decoration: BoxDecoration( - color: iconBgColor, - borderRadius: BorderRadius.circular(16.r), - ), - child: Icon( - icon, - size: 16.w, - color: iconColor, - ), - ), - SizedBox(width: 12.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.w500, - color: titleColor ?? (isDarkMode ? Colors.white : Colors.black87), - ), + Expanded( + child: Row( + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: iconBgColor, + borderRadius: BorderRadius.circular(16.r), ), - if (subtitle != null) - SizedBox(height: 2.h), - if (subtitle != null) - Text( - subtitle, - style: TextStyle( - fontSize: 12.sp, - color: subtitleColor ?? (isDarkMode ? Colors.white70 : Colors.black54), + child: Icon( + icon, + size: 16.w, + color: iconColor, + ), + ), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: titleColor ?? (isDarkMode ? Colors.white : Colors.black87), + ), + overflow: TextOverflow.ellipsis, ), - ), - ], - ), - ], + if (subtitle != null) + SizedBox(height: 2.h), + if (subtitle != null) + Text( + subtitle, + style: TextStyle( + fontSize: 12.sp, + color: subtitleColor ?? (isDarkMode ? Colors.white70 : Colors.black54), + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), ), Container( width: 32.w, @@ -718,4 +746,221 @@ class SettingsView extends GetView { ), ); } + + // 语音服务设置项 + Widget _buildSpeechServiceSetting(bool isDarkMode) { + return InkWell( + onTap: () { + _showSpeechServiceSelectionDialog(isDarkMode); + }, + child: Padding( + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode ? Colors.teal[900]!.withOpacity(0.3) : Colors.teal[100]!, + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + Icons.record_voice_over, + size: 16.w, + color: isDarkMode ? Colors.teal[300]! : Colors.teal[600]!, + ), + ), + SizedBox(width: 12.w), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '语音服务', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + SizedBox(height: 2.h), + Text( + '选择语音识别和合成服务', + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Obx(() { + final serviceType = controller.speechServiceType.value; + final serviceName = controller.availableSpeechServices + .firstWhere((service) => service['type'] == serviceType)['name']; + + return Flexible( + child: Text( + serviceName, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[600], + ), + overflow: TextOverflow.ellipsis, + ), + ); + }), + SizedBox(width: 8.w), + Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[100], + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + Icons.chevron_right, + size: 16.w, + color: isDarkMode ? Colors.white70 : Colors.grey[500], + ), + ), + ], + ), + ], + ), + ), + ); + } + + // 显示语音服务选择对话框 + void _showSpeechServiceSelectionDialog(bool isDarkMode) { + Get.dialog( + Dialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), + ), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '选择语音服务', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + SizedBox(height: 16.h), + ...controller.availableSpeechServices.map((service) { + return Obx(() { + final isSelected = controller.speechServiceType.value == service['type']; + + return InkWell( + onTap: () { + controller.setSpeechServiceType(service['type']); + Get.back(); + }, + child: Container( + margin: EdgeInsets.only(bottom: 12.h), + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: isSelected + ? (isDarkMode ? Colors.teal[900]!.withOpacity(0.3) : Colors.teal[50]!) + : (isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[100]!), + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: isSelected + ? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!) + : Colors.transparent, + width: 1, + ), + ), + child: Row( + children: [ + Container( + width: 24.w, + height: 24.w, + decoration: BoxDecoration( + color: isSelected + ? (isDarkMode ? Colors.teal[300] : Colors.teal[600]) + : (isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[300]), + shape: BoxShape.circle, + ), + child: isSelected + ? Icon( + Icons.check, + size: 16.w, + color: Colors.white, + ) + : null, + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + service['name'], + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 4.h), + Text( + service['description'], + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ], + ), + ), + ], + ), + ), + ); + }); + }).toList(), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + '取消', + style: TextStyle( + color: isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } } \ No newline at end of file diff --git a/lib/modules/test/bindings/azure_asr_test_binding.dart b/lib/modules/test/bindings/azure_asr_test_binding.dart deleted file mode 100644 index f3f29b968..000000000 --- a/lib/modules/test/bindings/azure_asr_test_binding.dart +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index b7e071af5..000000000 --- a/lib/modules/test/bindings/azure_tts_test_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/azure_tts_test_controller.dart'; - -class AzureTtsTestBinding extends Bindings { - @override - void dependencies() { - // 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/bindings/flutter_asr_test_binding.dart b/lib/modules/test/bindings/flutter_asr_test_binding.dart deleted file mode 100644 index 19bbdb0ea..000000000 --- a/lib/modules/test/bindings/flutter_asr_test_binding.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/flutter_asr_test_controller.dart'; -import '../../../data/services/flutter_asr_service.dart'; - -class FlutterAsrTestBinding extends Bindings { - @override - void dependencies() { - // 注册ASR服务(如果尚未注册) - if (!Get.isRegistered()) { - Get.lazyPut(() => FlutterAsrService()); - } - - // 注册控制器 - Get.lazyPut(() => FlutterAsrTestController()); - } -} \ No newline at end of file diff --git a/lib/modules/test/bindings/flutter_tts_test_binding.dart b/lib/modules/test/bindings/flutter_tts_test_binding.dart deleted file mode 100644 index eddb211bb..000000000 --- a/lib/modules/test/bindings/flutter_tts_test_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/flutter_tts_test_controller.dart'; - -/// Flutter TTS测试绑定 -class FlutterTtsTestBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut(() => FlutterTtsTestController()); - } -} \ No newline at end of file diff --git a/lib/modules/test/bindings/test_binding.dart b/lib/modules/test/bindings/test_binding.dart deleted file mode 100644 index 9f369a763..000000000 --- a/lib/modules/test/bindings/test_binding.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/volcano_tts_test_controller.dart'; -import '../controllers/azure_asr_test_controller.dart'; -import '../../../data/services/volcano_tts_api_service.dart'; - -class VolcanoTtsTestBinding extends Bindings { - @override - void dependencies() { - // Make sure the VolcanoTtsApiService is registered - if (!Get.isRegistered()) { - Get.put(VolcanoTtsApiService(), permanent: true); - } - - // Use put instead of lazyPut to ensure the controller is created immediately - Get.put(VolcanoTtsTestController()); - } -} - -class AzureAsrTestBinding extends Bindings { - @override - void dependencies() { - // Use put instead of lazyPut to ensure the controller is created immediately - Get.put(AzureAsrTestController()); - } -} \ No newline at end of file diff --git a/lib/modules/test/bindings/volcano_asr_test_binding.dart b/lib/modules/test/bindings/volcano_asr_test_binding.dart deleted file mode 100644 index 5882e694d..000000000 --- a/lib/modules/test/bindings/volcano_asr_test_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/volcano_asr_test_controller.dart'; - -class VolcanoAsrTestBinding extends Bindings { - @override - void dependencies() { - // Use put instead of lazyPut to ensure the controller is created immediately - Get.put(VolcanoAsrTestController()); - } -} \ 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 deleted file mode 100644 index ba60adcb1..000000000 --- a/lib/modules/test/controllers/azure_asr_test_controller.dart +++ /dev/null @@ -1,298 +0,0 @@ -import 'dart:async'; -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 _azureAsrService = Get.find(); - - // 状态变量 - final RxBool isListening = false.obs; - final RxString recognizedText = ''.obs; - final RxString partialText = ''.obs; - final RxString detectedLanguage = ''.obs; - final Rx asrState = AzureAsrState.notInitialized.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', // 俄语 - ]; - - // 自动语言检测 - final RxBool isAutoDetectEnabled = false.obs; - final RxList selectedSupportedLanguages = ['zh-CN', 'en-US'].obs; - - // 订阅 - StreamSubscription? _recognitionSubscription; - - @override - void onInit() { - super.onInit(); - _initializeAsr(); - } - - // 初始化ASR服务 - Future _initializeAsr() async { - try { - // 根据选择的支持语言初始化服务 - final success = await _azureAsrService.initialize( - supportedLanguages: selectedSupportedLanguages, - ); - 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 (_azureAsrService.isContinuousRecognitionActive()) { - await stopContinuousRecognition(); - } - - partialText.value = ''; - final result = await _azureAsrService.recognizeSpeech(); - if (result.text.isNotEmpty) { - recognizedText.value = result.text; - detectedLanguage.value = result.detectedLanguage; - } - } 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 { - await startContinuousRecognition(); - } - } catch (e) { - Logger.error('切换连续语音识别出错: $e'); - asrState.value = AzureAsrState.error; - } - } - - // 开始连续语音识别 - Future startContinuousRecognition() async { - try { - 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; - detectedLanguage.value = event.detectedLanguage; - } - partialText.value = ''; - break; - - case RecognitionEventType.intermediateResult: - if (event.text.isNotEmpty) { - partialText.value = event.text; - if (event.detectedLanguage.isNotEmpty) { - detectedLanguage.value = event.detectedLanguage; - } - } - 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'); - asrState.value = AzureAsrState.error; - isListening.value = false; - partialText.value = ''; - } - } - - // 停止连续语音识别 - 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(); - } - - // 确保新语言在选中支持的语言列表中 - if (!selectedSupportedLanguages.contains(language)) { - // 添加新语言到支持列表 - final newLanguages = [...selectedSupportedLanguages, language]; - selectedSupportedLanguages.value = newLanguages; - - // 重新初始化ASR服务 - await _initializeAsr(); - } - - recognizedText.value = ''; - partialText.value = ''; - detectedLanguage.value = ''; - asrState.value = AzureAsrState.initialized; - } catch (e) { - Logger.error('更改语言出错: $e'); - asrState.value = AzureAsrState.error; - } - } - - // 切换自动语言检测 - Future toggleAutoDetectLanguage() async { - try { - isAutoDetectEnabled.value = !isAutoDetectEnabled.value; - - // 停止当前识别 - if (_azureAsrService.isContinuousRecognitionActive()) { - await stopContinuousRecognition(); - } - - // 重新初始化ASR服务 - 自动语言检测现在通过选择多种语言实现 - await _initializeAsr(); - - recognizedText.value = ''; - partialText.value = ''; - detectedLanguage.value = ''; - } catch (e) { - Logger.error('切换自动语言检测出错: $e'); - asrState.value = AzureAsrState.error; - } - } - - // 更新支持的语言列表 - Future updateSupportedLanguages(List languages) async { - try { - if (languages.isEmpty) return; - - selectedSupportedLanguages.value = languages; - - // 停止当前识别 - if (_azureAsrService.isContinuousRecognitionActive()) { - await stopContinuousRecognition(); - } - - // 重新初始化ASR服务,使用新的语言列表 - final success = await _azureAsrService.initialize( - supportedLanguages: languages, - ); - - asrState.value = success ? AzureAsrState.initialized : AzureAsrState.error; - } 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 deleted file mode 100644 index 2419dfa42..000000000 --- a/lib/modules/test/controllers/azure_tts_test_controller.dart +++ /dev/null @@ -1,279 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'dart:async'; -import 'package:flutter/foundation.dart'; -import '../../../data/services/azure_tts_service.dart'; -import '../../../core/utils/logger.dart'; - -class AzureTtsTestController extends GetxController { - // 使用优化后的 AzureTtsService - final AzureTtsService _ttsService = Get.put(AzureTtsService()); - - // 可观察状态 - final isSpeaking = false.obs; - final errorMessage = ''.obs; - final isLoading = false.obs; - final statusMessage = '未连接'.obs; - - // 添加实际音频播放状态 - final isAudioPlaying = false.obs; - - // 播放状态检查定时器 - Timer? _playbackCheckTimer; - - // 当前播放的索引 - final currentPlayingIndex = RxInt(-1); - - // 语音选择相关 - 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 rate = 0.obs; - final pitch = 0.obs; - final volume = 100.obs; - - // 固定的播报内容 - final RxList speechTexts = [ - '这是第一条Azure语音播报测试,请听取合成效果。', - '这是第二条Azure语音播报测试,Azure语音合成支持连续多条播报。', - '这是第三条Azure语音播报测试,可以测试语音合成的流畅度和连贯性。', - '这是第四条Azure语音播报测试,感谢您的使用。', - ].obs; - - @override - void onInit() { - super.onInit(); - - // 初始化UI状态 - isSpeaking.value = false; - isAudioPlaying.value = false; - errorMessage.value = ''; - isLoading.value = false; - - // 监听TTS播放状态 - ever(_ttsService.isSpeaking, (bool playing) { - isAudioPlaying.value = playing; - if (!playing && currentPlayingIndex.value >= 0) { - // 如果播放结束,重置当前播放索引 - currentPlayingIndex.value = -1; - } - }); - - // 启动播放状态检查定时器 - _startPlaybackCheckTimer(); - - // 初始化TTS服务 - _initializeTtsService(); - } - - /// 初始化TTS服务 - Future _initializeTtsService() async { - try { - isLoading.value = true; - errorMessage.value = ''; - statusMessage.value = '正在初始化...'; - - // 初始化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 changeVoice(String voice) async { - try { - isLoading.value = true; - errorMessage.value = ''; - - selectedVoice.value = voice; - - // 设置语音 - final result = await _ttsService.setVoice(voice); - - if (result) { - statusMessage.value = '已设置语音: $voice'; - } else { - errorMessage.value = '设置语音失败'; - } - - isLoading.value = false; - } catch (e) { - 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, - ); - - 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) { - return; - } - - try { - isSpeaking.value = true; - isAudioPlaying.value = true; - errorMessage.value = ''; - currentPlayingIndex.value = index; - statusMessage.value = '正在合成...'; - - final text = speechTexts[index]; - - // 播放文本 - await _ttsService.speak(text); - - statusMessage.value = '合成完成'; - - } catch (e) { - errorMessage.value = '播放出错'; - statusMessage.value = '错误'; - Logger.error('播放出错: $e'); - } finally { - isSpeaking.value = false; - if (!_ttsService.isSpeaking.value) { - isAudioPlaying.value = false; - currentPlayingIndex.value = -1; - } - } - } - - // 启动播放状态检查定时器 - 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 = true; - errorMessage.value = ''; - statusMessage.value = '开始连续播放'; - - // 播放所有文本 - await _ttsService.speakMultiple(speechTexts.toList()); - - statusMessage.value = '连续播放完成'; - - } catch (e) { - errorMessage.value = '连续播放出错'; - statusMessage.value = '错误'; - Logger.error('连续播放出错: $e'); - } finally { - isSpeaking.value = false; - } - } - - /// 停止播放 - 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'); - } - } - - /// 切换启用状态 - void toggleEnabled() { - _ttsService.toggleEnabled(); - statusMessage.value = _ttsService.isEnabled.value ? '已启用' : '已禁用'; - } - - @override - void onClose() { - // 取消定时器 - _playbackCheckTimer?.cancel(); - - // 停止播放 - _ttsService.stop(); - - super.onClose(); - } -} \ No newline at end of file diff --git a/lib/modules/test/controllers/flutter_asr_test_controller.dart b/lib/modules/test/controllers/flutter_asr_test_controller.dart deleted file mode 100644 index 71b0bc733..000000000 --- a/lib/modules/test/controllers/flutter_asr_test_controller.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'dart:async'; -import 'package:get/get.dart'; -import '../../../data/services/flutter_asr_service.dart'; -import '../../../core/utils/logger.dart'; - -/// Flutter ASR测试控制器,演示如何使用FlutterAsrService -class FlutterAsrTestController extends GetxController { - // 获取ASR服务实例 - final FlutterAsrService _asrService = Get.find(); - - // 当前ASR状态 - 使用Rx包装以便UI可以响应变化 - final Rx _asrState = AsrState.notInitialized.obs; - AsrState get asrState => _asrState.value; - - // 当前识别的文本 - 使用Rx包装以便UI可以响应变化 - final RxString _recognizedText = ''.obs; - String get recognizedText => _recognizedText.value; - - // 识别准确度 - 使用Rx包装以便UI可以响应变化 - final RxDouble _confidence = 0.0.obs; - double get confidence => _confidence.value; - - // 是否正在监听 - 计算属性 - bool get isListening => asrState == AsrState.listening; - - // 是否可用 - 使用Rx包装以便UI可以响应变化 - final RxBool _isAvailable = false.obs; - bool get isAvailable => _isAvailable.value; - - // 可用的语言列表 - 使用Rx包装以便UI可以响应变化 - final RxList _locales = [].obs; - List get locales => _locales; - - // 当前选择的语言 - 使用Rx包装以便UI可以响应变化 - final RxString _currentLocale = ''.obs; - String get currentLocale => _currentLocale.value; - - // 历史记录 - 使用Rx包装以便UI可以响应变化 - final RxList _history = [].obs; - List get history => _history; - - // 连续识别模式 - 使用Rx包装以便UI可以响应变化 - final RxBool _continuousMode = false.obs; - bool get continuousMode => _continuousMode.value; - set continuousMode(bool value) => _continuousMode.value = value; - - // 定时器,用于定期更新状态 - late Timer _updateTimer; - - // 识别结果订阅 - StreamSubscription? _resultSubscription; - - @override - void onInit() { - super.onInit(); - - // 初始化状态 - _updateState(); - - // 设置定时器,每300毫秒更新一次状态,确保UI能够及时反映服务状态变化 - _updateTimer = Timer.periodic(const Duration(milliseconds: 300), (_) { - _updateState(); - - // 如果启用了连续模式,且当前不在监听状态,且ASR已初始化,则自动重新开始监听 - if (_continuousMode.value && - _asrState.value != AsrState.listening && - _asrState.value != AsrState.notInitialized && - _asrState.value != AsrState.error && - _isAvailable.value) { - _restartListening(); - } - }); - - // 订阅识别结果流 - _resultSubscription = _asrService.recognitionResults.listen(_onFinalResult); - } - - /// 更新控制器状态,从服务获取最新状态 - void _updateState() { - _asrState.value = _asrService.asrState; - _recognizedText.value = _asrService.lastWords; - _confidence.value = _asrService.confidence; - _isAvailable.value = _asrService.isAvailable; - _locales.assignAll(_asrService.locales); - _currentLocale.value = _asrService.currentLocale; - } - - /// 开始语音识别 - Future startListening() async { - return await _asrService.startListening(); - } - - /// 停止语音识别 - Future stopListening() async { - await _asrService.stopListening(); - } - - /// 取消语音识别 - Future cancelListening() async { - await _asrService.cancelListening(); - } - - /// 切换连续识别模式 - void toggleContinuousMode() { - _continuousMode.value = !_continuousMode.value; - Logger.info('连续识别模式: ${_continuousMode.value ? '开启' : '关闭'}'); - - // 如果开启连续模式且当前未在监听,则开始监听 - if (_continuousMode.value && !isListening && _isAvailable.value) { - startListening(); - } - } - - /// 重新开始监听(用于连续模式) - Future _restartListening() async { - // 添加小延迟,避免过于频繁的重启 - await Future.delayed(const Duration(milliseconds: 300)); - if (_continuousMode.value && !isListening && _isAvailable.value) { - Logger.info('连续模式: 重新开始监听'); - await startListening(); - } - } - - /// 设置语言 - void setLocale(String localeId) { - _asrService.setLocale(localeId); - _currentLocale.value = localeId; - } - - /// 处理最终识别结果 - void _onFinalResult(String result) { - if (result.isNotEmpty) { - // 添加到历史记录 - _history.insert(0, result); - - // 限制历史记录数量 - if (_history.length > 10) { - _history.removeLast(); - } - } - } - - /// 清空历史记录 - void clearHistory() { - _history.clear(); - } - - @override - void onClose() { - _updateTimer.cancel(); - _resultSubscription?.cancel(); - super.onClose(); - } -} \ No newline at end of file diff --git a/lib/modules/test/controllers/flutter_tts_test_controller.dart b/lib/modules/test/controllers/flutter_tts_test_controller.dart deleted file mode 100644 index f3441c683..000000000 --- a/lib/modules/test/controllers/flutter_tts_test_controller.dart +++ /dev/null @@ -1,163 +0,0 @@ -import 'package:get/get.dart'; -import '../../../data/services/flutter_tts_service.dart'; -import '../../../core/utils/logger.dart'; -import 'dart:async'; - -/// Flutter TTS测试控制器,演示如何使用FlutterTtsService -class FlutterTtsTestController extends GetxController { - // 获取TTS服务实例 - final FlutterTtsService _ttsService = Get.find(); - - // 当前TTS状态 - 使用Rx包装以便UI可以响应变化 - final Rx _ttsState = TtsState.stopped.obs; - TtsState get ttsState => _ttsState.value; - - // 当前正在播放的文本 - 使用Rx包装以便UI可以响应变化 - final RxString _currentText = ''.obs; - String get currentText => _currentText.value; - - // 是否正在播放 - 计算属性 - bool get isPlaying => ttsState == TtsState.playing; - - // 队列中的文本 - 使用Rx包装以便UI可以响应变化 - final RxList _textQueue = [].obs; - List get textQueue => _textQueue; - - // 可用的语音列表 - 使用Rx包装以便UI可以响应变化 - final RxList> _voices = >[].obs; - List> get voices => _voices; - - // 当前选择的语音 - 使用Rx包装以便UI可以响应变化 - final RxString _currentVoice = ''.obs; - String get currentVoice => _currentVoice.value; - - // 定时器,用于定期更新状态 - late Timer _updateTimer; - - @override - void onInit() { - super.onInit(); - - // 初始化状态 - _updateState(); - - // 设置定时器,每300毫秒更新一次状态,确保UI能够及时反映服务状态变化 - _updateTimer = Timer.periodic(const Duration(milliseconds: 300), (_) { - _updateState(); - }); - } - - /// 更新控制器状态,从服务获取最新状态 - void _updateState() { - // 更新TTS状态 - final serviceState = _ttsService.ttsState; - if (_ttsState.value != serviceState) { - _ttsState.value = serviceState; - } - - // 更新当前播放文本 - final serviceText = _ttsService.currentText; - if (_currentText.value != serviceText) { - _currentText.value = serviceText; - } - - // 更新队列 - final serviceQueue = _ttsService.textQueue; - if (!listEquals(_textQueue, serviceQueue)) { - _textQueue.value = List.from(serviceQueue); - } - - // 更新可用语音列表 - final serviceVoices = _ttsService.voices; - if (!listEquals(_voices, serviceVoices)) { - _voices.value = List.from(serviceVoices); - } - - // 更新当前选择的语音 - final serviceCurrentVoice = _ttsService.currentVoice; - if (_currentVoice.value != serviceCurrentVoice) { - _currentVoice.value = serviceCurrentVoice; - } - } - - /// 比较两个列表是否相等 - bool listEquals(List a, List b) { - if (a.length != b.length) return false; - for (int i = 0; i < a.length; i++) { - if (a[i] != b[i]) return false; - } - return true; - } - - /// 添加文本到队列并开始播放(如果尚未开始) - Future speak(String text) async { - if (text.isEmpty) { - Logger.warning('文本为空,无法播放'); - return; - } - - await _ttsService.speak(text); - _updateState(); - } - - /// 添加多条文本到队列并开始播放(如果尚未开始) - Future speakTexts(List texts) async { - if (texts.isEmpty) { - Logger.warning('文本列表为空,无法播放'); - return; - } - - await _ttsService.speakTexts(texts); - _updateState(); - } - - /// 停止播放并清空队列 - Future stop() async { - await _ttsService.stop(); - _updateState(); - } - - /// 暂停播放 - Future pause() async { - await _ttsService.pause(); - _updateState(); - } - - /// 设置语音 - Future setVoice(String voiceName) async { - await _ttsService.setVoice(voiceName); - _updateState(); - } - - /// 设置语言 - Future setLanguage(String language) async { - await _ttsService.setLanguage(language); - } - - /// 设置语速 - Future setRate(double rate) async { - await _ttsService.setRate(rate); - } - - /// 设置音量 - Future setVolume(double volume) async { - await _ttsService.setVolume(volume); - } - - /// 设置音调 - Future setPitch(double pitch) async { - await _ttsService.setPitch(pitch); - } - - /// 清空队列并停止播放 - Future clearQueue() async { - await _ttsService.clearQueue(); - _updateState(); - } - - @override - void onClose() { - _updateTimer.cancel(); - super.onClose(); - } -} \ No newline at end of file diff --git a/lib/modules/test/controllers/volcano_asr_test_controller.dart b/lib/modules/test/controllers/volcano_asr_test_controller.dart deleted file mode 100644 index 34ac7ade3..000000000 --- a/lib/modules/test/controllers/volcano_asr_test_controller.dart +++ /dev/null @@ -1,218 +0,0 @@ -import 'dart:async'; -import 'package:get/get.dart'; -import '../../../data/services/volcano_asr_api_service.dart'; -import '../../../core/utils/logger.dart'; - -class VolcanoAsrTestController extends GetxController { - // Observable variables - final isListening = false.obs; - final errorMessage = ''.obs; - final recognitionResults = [].obs; - final isContinuous = false.obs; - final connectionStatus = 'Disconnected'.obs; - - // 当前识别中的文本 - final currentRecognizingText = ''.obs; - - // 当前识别的索引 - int _currentRecognitionIndex = -1; - - // Services - late final VolcanoAsrApiService _asrService; - - // Stream subscriptions - StreamSubscription? _recognitionSubscription; - - @override - void onInit() { - super.onInit(); - - // Initialize the ASR service - _asrService = Get.find(); - - // Subscribe to recognition stream - _recognitionSubscription = _asrService.recognitionStream.listen((event) { - switch (event.type) { - case RecognitionEventType.started: - // 开始新的识别 - if (isContinuous.value) { - // 如果是连续模式,检查是否需要创建新条目 - if (_currentRecognitionIndex == -1 || recognitionResults.isEmpty) { - // 如果没有当前识别索引或结果列表为空,添加新条目 - recognitionResults.add(''); - _currentRecognitionIndex = recognitionResults.length - 1; - } - } else { - // 非连续模式,清空结果并添加新条目 - recognitionResults.clear(); - recognitionResults.add(''); - _currentRecognitionIndex = 0; - } - currentRecognizingText.value = ''; - break; - - case RecognitionEventType.recognizing: - // 实时识别结果 - if (event.text.isNotEmpty) { - currentRecognizingText.value = event.text; - - // 确保有一个当前条目可以更新 - if (_currentRecognitionIndex == -1 || _currentRecognitionIndex >= recognitionResults.length) { - recognitionResults.add(''); - _currentRecognitionIndex = recognitionResults.length - 1; - } - - // 更新当前条目 - recognitionResults[_currentRecognitionIndex] = event.text; - } - break; - - case RecognitionEventType.finalResult: - // 最终识别结果 - if (event.text.isNotEmpty) { - // 确保有一个当前条目可以更新 - if (_currentRecognitionIndex == -1 || _currentRecognitionIndex >= recognitionResults.length) { - recognitionResults.add(event.text); - _currentRecognitionIndex = recognitionResults.length - 1; - } else { - // 更新当前条目为最终结果 - recognitionResults[_currentRecognitionIndex] = event.text; - } - - // 收到最终结果后,重置索引,为新句子做准备 - _currentRecognitionIndex = -1; - - // 清空当前识别文本 - currentRecognizingText.value = ''; - } - break; - - case RecognitionEventType.error: - // 错误处理 - if (event.error != null) { - errorMessage.value = event.error!; - - // 如果有错误且正在识别,停止识别 - if (isListening.value) { - isListening.value = false; - connectionStatus.value = 'Disconnected'; - - // 停止语音识别,不尝试重启 - _asrService.stopRecognition(); - - } - } - break; - - // 注意:处理其他事件类型 - default: - // 其他事件类型不做特殊处理 - break; - } - }); - - // Update connection status based on service state - ever(_asrService.isListening, (bool listening) { - isListening.value = listening; - connectionStatus.value = listening ? 'Connected' : 'Disconnected'; - }); - - // Update error message from service - if (_asrService.errorMessage.isNotEmpty) { - errorMessage.value = _asrService.errorMessage; - } - } - - @override - void onClose() { - // Cancel all subscriptions - _recognitionSubscription?.cancel(); - - // Disconnect from the ASR service if still connected - if (_asrService.isConnected) { - stopListening(); - } - - super.onClose(); - } - - // Start listening for speech - Future startListening() async { - try { - // Clear previous error message - errorMessage.value = ''; - - // 如果是新的识别会话 - if (!isListening.value) { - currentRecognizingText.value = ''; - - // 如果不是连续识别模式,则清空结果列表 - if (!isContinuous.value) { - recognitionResults.clear(); - _currentRecognitionIndex = -1; - } - } - - // 使用麦克风录音功能 - final success = await _asrService.startRecognition( - sampleRate: 16000, - bitsPerSample: 16, - channels: 1, - format: 'pcm', - codec: 'raw', - enablePunc: true, - ); - - if (success) { - isListening.value = true; - connectionStatus.value = 'Connected'; - } else { - errorMessage.value = _asrService.errorMessage; - Logger.error('Failed to start recognition: ${_asrService.errorMessage}'); - } - } catch (e) { - errorMessage.value = e.toString(); - Logger.error('Error starting recognition: $e'); - } - } - - // 重新开始识别(用于连续识别模式) - Future _restartRecognition() async { - // 短暂延迟,确保上一次识别完全结束 - await Future.delayed(const Duration(milliseconds: 500)); - - // 如果仍处于监听状态且是连续识别模式,则开始新的识别 - if (isListening.value && isContinuous.value) { - startListening(); - } - } - - // Stop listening for speech - void stopListening() { - try { - connectionStatus.value = 'Disconnecting...'; - - // 停止麦克风录音 - _asrService.stopRecognition().then((_) { - isListening.value = false; - connectionStatus.value = 'Disconnected'; - - // 重置当前识别索引 - _currentRecognitionIndex = -1; - }).catchError((e) { - errorMessage.value = e.toString(); - Logger.error('Error stopping recognition: $e'); - }); - } catch (e) { - errorMessage.value = e.toString(); - Logger.error('Error stopping recognition: $e'); - } - } - - // Clear recognition results - void clearResults() { - recognitionResults.clear(); - currentRecognizingText.value = ''; - _currentRecognitionIndex = -1; - } -} \ 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 deleted file mode 100644 index a7bd79f59..000000000 --- a/lib/modules/test/controllers/volcano_tts_test_controller.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'dart:async'; -import 'package:flutter/foundation.dart'; -import '../../../data/services/volcano_tts_service.dart'; -import '../../../core/utils/logger.dart'; - -class VolcanoTtsTestController extends GetxController { - // 使用 VolcanoTtsService 替代 VolcanoTtsApiService - final VolcanoTtsService _ttsService = Get.put(VolcanoTtsService()); - - // 可观察状态 - final isSpeaking = false.obs; - final errorMessage = ''.obs; - final isLoading = false.obs; - final statusMessage = '未连接'.obs; - - // 添加实际音频播放状态 - final isAudioPlaying = false.obs; - - // 播放状态检查定时器 - Timer? _playbackCheckTimer; - - // 当前播放的索引 - final currentPlayingIndex = RxInt(-1); - - // 当前使用的语音 - 使用预定义的值 - final currentVoice = 'zh_female_meilinvyou_moon_bigtts'.obs; - - // 可用的语音列表 - final List> availableVoices = [ - {'id': 'zh_female_meilinvyou_moon_bigtts', 'name': '魅力女友'}, - {'id': 'zh_male_shaonianzixin_moon_bigtts', 'name': '少年梓辛/Brayan'}, - {'id': 'zh_male_beijingxiaoye_moon_bigtts', 'name': '北京小爷'}, - {'id': 'zh_female_daimengchuanmei_moon_bigtts', 'name': '呆萌川妹'}, - {'id': 'zh_female_wanqudashu_moon_bigtts', 'name': '湾区大叔'}, - {'id': 'zh_female_sajiaonvyou_moon_bigtts', 'name': '撒娇女友'}, - {'id': 'zh_male_shenyeboke_moon_bigtts', 'name': '深夜播客'}, - ]; - - // 固定的播报内容 - final RxList speechTexts = [ - '这是第一条播报测试,请听取合成效果。', - '这是第二条播报测试,火山语音合成支持连续多条播报。', - '这是第三条播报测试,可以测试语音合成的流畅度和连贯性。', - '这是第四条播报测试,感谢您的使用。', - ].obs; - - @override - void onInit() { - super.onInit(); - - // 初始化UI状态 - isSpeaking.value = false; - isAudioPlaying.value = false; - errorMessage.value = ''; - isLoading.value = false; - - // 监听TTS播放状态 - ever(_ttsService.isPlaying, (bool playing) { - isAudioPlaying.value = playing; - if (!playing && currentPlayingIndex.value >= 0) { - // 如果播放结束,重置当前播放索引 - currentPlayingIndex.value = -1; - } - }); - - // 启动播放状态检查定时器 - _startPlaybackCheckTimer(); - } - - /// 更新UI状态 - void updateUIState() { - // 强制更新UI - update(); - } - - /// 播放单条文本 - Future playSingle(int index) async { - if (index < 0 || index >= speechTexts.length) { - return; - } - - try { - isSpeaking.value = true; - isAudioPlaying.value = true; - errorMessage.value = ''; - currentPlayingIndex.value = index; - statusMessage.value = '正在合成...'; - - final text = speechTexts[index]; - - // 播放文本 - await _ttsService.speak(text); - - statusMessage.value = '合成完成'; - - } catch (e) { - errorMessage.value = '播放出错'; - statusMessage.value = '错误'; - Logger.error('播放出错: $e'); - } finally { - isSpeaking.value = false; - if (!_ttsService.isPlaying.value) { - isAudioPlaying.value = false; - currentPlayingIndex.value = -1; - } - } - } - - // 启动播放状态检查定时器 - void _startPlaybackCheckTimer() { - _playbackCheckTimer?.cancel(); - _playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) { - // 更新实际播放状态 - final actuallyPlaying = _ttsService.isPlaying.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 = true; - errorMessage.value = ''; - statusMessage.value = '开始连续播放'; - - // 连接所有文本 - final allText = speechTexts.join(''); - - // 播放所有文本 - await _ttsService.speak(allText); - - statusMessage.value = '连续播放完成'; - - } catch (e) { - errorMessage.value = '连续播放出错'; - statusMessage.value = '错误'; - Logger.error('连续播放出错: $e'); - } finally { - isSpeaking.value = false; - } - } - - /// 停止播放 - Future stopSpeaking() async { - try { - await _ttsService.stop(); - isSpeaking.value = false; - isAudioPlaying.value = false; - currentPlayingIndex.value = -1; - statusMessage.value = '已停止'; - } catch (e) { - errorMessage.value = '停止播放出错'; - Logger.error('停止播放出错: $e'); - } - } - - /// 切换语音 - void changeVoice(String voiceId) { - currentVoice.value = voiceId; - // VolcanoTtsService 不需要显式设置发音人,在 speak 时会使用环境变量中的默认发音人 - } - - /// 切换启用状态 - void toggleEnabled() { - _ttsService.isEnabled.toggle(); - statusMessage.value = _ttsService.isEnabled.value ? '已启用' : '已禁用'; - } - - @override - void onClose() { - // 取消定时器 - _playbackCheckTimer?.cancel(); - - // 停止播放 - _ttsService.stop(); - - super.onClose(); - } -} \ No newline at end of file diff --git a/lib/modules/test/views/azure_asr_test_view.dart b/lib/modules/test/views/azure_asr_test_view.dart deleted file mode 100644 index ee63d75c9..000000000 --- a/lib/modules/test/views/azure_asr_test_view.dart +++ /dev/null @@ -1,173 +0,0 @@ -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); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Azure语音识别测试'), - actions: [ - _buildLanguageDropdown(), - ], - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildStatusCard(), - const SizedBox(height: 16), - _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: 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: 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, - ), - ), - )), - ), - ], - ), - ), - ), - ); - } - - // 构建控制按钮 - 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 deleted file mode 100644 index c3e7fab31..000000000 --- a/lib/modules/test/views/azure_tts_test_view.dart +++ /dev/null @@ -1,303 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../controllers/azure_tts_test_controller.dart'; - -class AzureTtsTestView extends GetView { - const AzureTtsTestView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - // 强制更新控制器状态 - WidgetsBinding.instance.addPostFrameCallback((_) { - // 在下一帧渲染完成后调用 - controller.updateUIState(); - }); - - return Scaffold( - appBar: AppBar( - title: const Text('Azure语音合成测试'), - centerTitle: true, - ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 语音选择 - _buildVoiceSelectionCard(), - - 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), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Obx(() { - return ElevatedButton.icon( - onPressed: controller.isAudioPlaying.value || controller.isLoading.value - ? null - : () => controller.speakContinuousTexts(), - icon: controller.isAudioPlaying.value - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2) - ) - : const Icon(Icons.playlist_play), - label: Text(controller.isAudioPlaying.value ? '播放中...' : '全部播放'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - ); - }), - Obx(() => ElevatedButton.icon( - onPressed: controller.isAudioPlaying.value - ? () => controller.stopSpeaking() - : null, - icon: const Icon(Icons.stop), - label: const Text('停止'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - )), - ], - ), - ], - ), - ), - ), - - 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), - // 生成可点击的播报内容 - Obx(() => ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: controller.speechTexts.length, - separatorBuilder: (context, index) => const Divider(height: 1), - itemBuilder: (context, index) { - final isPlaying = controller.isAudioPlaying.value && - controller.currentPlayingIndex.value == index; - - return ListTile( - title: Text( - controller.speechTexts[index], - style: TextStyle( - fontWeight: isPlaying ? FontWeight.bold : FontWeight.normal, - color: isPlaying ? Colors.blue : null, - ), - ), - leading: CircleAvatar( - backgroundColor: isPlaying ? Colors.blue : Colors.grey.shade200, - radius: 18, - child: Icon( - isPlaying ? Icons.volume_up : Icons.play_arrow, - color: isPlaying ? Colors.white : Colors.black, - size: 20, - ), - ), - trailing: isPlaying - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : null, - onTap: controller.isAudioPlaying.value || controller.isLoading.value - ? null - : () => controller.playSingle(index), - enabled: !(controller.isAudioPlaying.value || controller.isLoading.value), - tileColor: isPlaying ? Colors.blue.withOpacity(0.1) : null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: isPlaying - ? const BorderSide(color: Colors.blue, width: 1) - : BorderSide.none, - ), - ); - }, - )), - ], - ), - ), - ), - - 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.statusMessage.value, - style: TextStyle( - color: controller.isLoading.value - ? Colors.orange - : controller.isAudioPlaying.value - ? Colors.green - : Colors.grey, - fontWeight: FontWeight.bold, - ), - )), - const SizedBox(height: 8), - Obx(() => controller.errorMessage.value.isNotEmpty - ? Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.red.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.error_outline, color: Colors.red, size: 16), - SizedBox(width: 8), - Text( - '错误', - style: TextStyle( - color: Colors.red, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - SizedBox(height: 4), - Text( - controller.errorMessage.value, - style: TextStyle(color: Colors.red.shade800), - ), - ], - ), - ) - : const SizedBox.shrink()), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } - - // 构建语音选择卡片 - 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/test/views/flutter_asr_test_view.dart b/lib/modules/test/views/flutter_asr_test_view.dart deleted file mode 100644 index e06a5bbc0..000000000 --- a/lib/modules/test/views/flutter_asr_test_view.dart +++ /dev/null @@ -1,476 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../controllers/flutter_asr_test_controller.dart'; -import '../../../data/services/flutter_asr_service.dart'; - -/// Flutter ASR测试视图,演示如何使用FlutterAsrService -class FlutterAsrTestView extends GetView { - const FlutterAsrTestView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Flutter ASR Test'), - centerTitle: true, - ), - body: SingleChildScrollView( - child: 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( - 'Speech Recognition Controls', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - // 添加连续识别模式开关 - Obx(() { - final isContinuous = controller.continuousMode; - return SwitchListTile( - title: const Text('连续识别模式'), - subtitle: Text( - isContinuous - ? '已开启:识别将自动连续进行' - : '已关闭:识别完成后需手动重新开始', - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - ), - ), - value: isContinuous, - onChanged: controller.isAvailable - ? (value) => controller.toggleContinuousMode() - : null, - activeColor: Colors.blue, - dense: true, - ); - }), - const Divider(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Obx(() { - final isListening = controller.isListening; - return ElevatedButton.icon( - onPressed: isListening || !controller.isAvailable - ? null - : () => controller.startListening(), - icon: isListening - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.mic), - label: Text(isListening ? 'Listening...' : 'Start Listening'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - ); - }), - Obx(() { - final isActive = controller.isListening; - return ElevatedButton.icon( - onPressed: isActive - ? () => controller.stopListening() - : null, - icon: const Icon(Icons.stop), - label: const Text('Stop'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - ); - }), - ], - ), - ], - ), - ), - ), - - const SizedBox(height: 16), - - // 语言选择卡片 - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Language Selection', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - - Obx(() { - final locales = controller.locales; - final currentLocale = controller.currentLocale; - - if (locales.isEmpty) { - return const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - 'No languages available. Please check ASR settings on your device.', - style: TextStyle( - color: Colors.red, - fontStyle: FontStyle.italic, - ), - ), - ); - } - - return DropdownButtonFormField( - decoration: const InputDecoration( - labelText: 'Select Language', - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - value: currentLocale.isNotEmpty ? currentLocale : null, - items: locales.map((locale) { - final name = locale.name ?? 'Unknown'; - final id = locale.localeId ?? ''; - return DropdownMenuItem( - value: id, - child: Text('$name (${id.isNotEmpty ? id : 'Unknown'})'), - ); - }).toList(), - onChanged: controller.isListening - ? null - : (value) { - if (value != null) { - controller.setLocale(value); - } - }, - isExpanded: true, - hint: const Text('Select a language'), - ); - }), - ], - ), - ), - ), - - const SizedBox(height: 16), - - // 当前识别结果卡片 - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Current Recognition', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - - Obx(() { - final text = controller.recognizedText; - final confidence = controller.confidence; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade300), - ), - width: double.infinity, - child: Text( - text.isEmpty ? 'Speak to see recognition results here' : text, - style: TextStyle( - fontSize: 16, - fontStyle: text.isEmpty ? FontStyle.italic : FontStyle.normal, - color: text.isEmpty ? Colors.grey : Colors.black, - ), - ), - ), - - if (confidence > 0) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - 'Confidence: ${(confidence * 100).toStringAsFixed(1)}%', - style: TextStyle( - color: _getConfidenceColor(confidence), - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ); - }), - ], - ), - ), - ), - - const SizedBox(height: 16), - - // 历史记录卡片 - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Recognition History', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - Obx(() { - final hasHistory = controller.history.isNotEmpty; - return TextButton.icon( - onPressed: hasHistory ? () => controller.clearHistory() : null, - icon: const Icon(Icons.clear_all, size: 18), - label: const Text('Clear'), - style: TextButton.styleFrom( - foregroundColor: Colors.red, - padding: const EdgeInsets.symmetric(horizontal: 8), - ), - ); - }), - ], - ), - const SizedBox(height: 8), - - Obx(() { - final history = controller.history; - - if (history.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: Center( - child: Text( - 'No recognition history yet', - style: TextStyle( - fontStyle: FontStyle.italic, - color: Colors.grey, - ), - ), - ), - ); - } - - return ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: history.length, - separatorBuilder: (context, index) => const Divider(height: 1), - itemBuilder: (context, index) { - final text = history[index]; - - return ListTile( - title: Text(text), - leading: CircleAvatar( - backgroundColor: Colors.blue.shade100, - radius: 18, - child: Text( - '${index + 1}', - style: const TextStyle( - color: Colors.blue, - fontWeight: FontWeight.bold, - ), - ), - ), - dense: true, - ); - }, - ); - }), - ], - ), - ), - ), - - const SizedBox(height: 16), - - // 状态显示卡片 - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Status', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Obx(() { - final state = controller.asrState; - final stateText = _getAsrStateText(state); - final stateColor = _getAsrStateColor(state); - final isContinuous = controller.continuousMode; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: stateColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: stateColor.withOpacity(0.3)), - ), - child: Row( - children: [ - Icon( - _getAsrStateIcon(state), - color: stateColor, - ), - const SizedBox(width: 8), - Text( - 'Current Status: $stateText', - style: TextStyle( - color: stateColor, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - - if (isContinuous) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.blue.withOpacity(0.3)), - ), - child: Row( - children: const [ - Icon( - Icons.repeat, - color: Colors.blue, - size: 16, - ), - SizedBox(width: 8), - Text( - '连续识别模式已开启', - style: TextStyle( - color: Colors.blue, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ], - ), - ), - ), - ], - ); - }), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } - - // 获取ASR状态文本 - String _getAsrStateText(AsrState state) { - switch (state) { - case AsrState.notInitialized: - return 'Not Initialized'; - case AsrState.initialized: - return 'Initialized'; - case AsrState.listening: - return 'Listening'; - case AsrState.notListening: - return 'Not Listening'; - case AsrState.error: - return 'Error'; - default: - return 'Unknown'; - } - } - - // 获取ASR状态颜色 - Color _getAsrStateColor(AsrState state) { - switch (state) { - case AsrState.notInitialized: - return Colors.grey; - case AsrState.initialized: - return Colors.blue; - case AsrState.listening: - return Colors.green; - case AsrState.notListening: - return Colors.orange; - case AsrState.error: - return Colors.red; - default: - return Colors.grey; - } - } - - // 获取ASR状态图标 - IconData _getAsrStateIcon(AsrState state) { - switch (state) { - case AsrState.notInitialized: - return Icons.hourglass_empty; - case AsrState.initialized: - return Icons.check_circle; - case AsrState.listening: - return Icons.mic; - case AsrState.notListening: - return Icons.mic_off; - case AsrState.error: - return Icons.error; - default: - return Icons.help_outline; - } - } - - // 获取置信度颜色 - Color _getConfidenceColor(double confidence) { - if (confidence >= 0.8) { - return Colors.green; - } else if (confidence >= 0.6) { - return Colors.orange; - } else { - return Colors.red; - } - } -} \ No newline at end of file diff --git a/lib/modules/test/views/flutter_tts_test_view.dart b/lib/modules/test/views/flutter_tts_test_view.dart deleted file mode 100644 index 8a0001633..000000000 --- a/lib/modules/test/views/flutter_tts_test_view.dart +++ /dev/null @@ -1,334 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../controllers/flutter_tts_test_controller.dart'; -import '../../../data/services/flutter_tts_service.dart'; - -/// Flutter TTS测试视图,演示如何使用FlutterTtsService -class FlutterTtsTestView extends GetView { - const FlutterTtsTestView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Flutter TTS Test'), - centerTitle: true, - ), - body: SingleChildScrollView( - child: 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( - 'Playback Controls', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Obx(() { - final isPlaying = controller.isPlaying; - return ElevatedButton.icon( - onPressed: isPlaying - ? null - : () => _playAllTexts(), - icon: isPlaying - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.playlist_play), - label: Text(isPlaying ? 'Playing...' : 'Play All'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - ); - }), - Obx(() { - final isActive = controller.ttsState != TtsState.stopped; - return ElevatedButton.icon( - onPressed: isActive - ? () => controller.stop() - : null, - icon: const Icon(Icons.stop), - label: const Text('Stop'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - ); - }), - ], - ), - ], - ), - ), - ), - - const SizedBox(height: 16), - - // 语音选择卡片 - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Voice Selection', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - - Obx(() { - final voices = controller.voices; - final currentVoice = controller.currentVoice; - - if (voices.isEmpty) { - return const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - 'No voices available. Please check TTS settings on your device.', - style: TextStyle( - color: Colors.red, - fontStyle: FontStyle.italic, - ), - ), - ); - } - - return DropdownButtonFormField( - decoration: const InputDecoration( - labelText: 'Select Voice', - border: OutlineInputBorder(), - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - ), - value: currentVoice.isNotEmpty ? currentVoice : null, - items: voices.map((voice) { - final name = voice['name'] ?? 'Unknown'; - final locale = voice['locale'] ?? ''; - return DropdownMenuItem( - value: name, - child: Text('$name ${locale.isNotEmpty ? '($locale)' : ''}'), - ); - }).toList(), - onChanged: controller.isPlaying - ? null - : (value) { - if (value != null) { - controller.setVoice(value); - } - }, - isExpanded: true, - hint: const Text('Select a voice'), - ); - }), - ], - ), - ), - ), - - const SizedBox(height: 16), - - // 播报内容列表卡片 - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Sample Texts', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - - // 预设的播报内容 - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: _getSampleTexts().length, - separatorBuilder: (context, index) => const Divider(height: 1), - itemBuilder: (context, index) { - final text = _getSampleTexts()[index]; - - return Obx(() { - final isPlaying = controller.isPlaying; - final isCurrentPlaying = controller.isPlaying && - controller.currentText == text; - - return ListTile( - title: Text( - text, - style: TextStyle( - fontWeight: isCurrentPlaying ? FontWeight.bold : FontWeight.normal, - color: isCurrentPlaying ? Colors.blue : null, - ), - ), - leading: CircleAvatar( - backgroundColor: isCurrentPlaying - ? Colors.blue - : Colors.blue.shade100, - radius: 18, - child: Icon( - isCurrentPlaying ? Icons.volume_up : Icons.play_arrow, - color: isCurrentPlaying ? Colors.white : Colors.blue, - size: 20, - ), - ), - onTap: isPlaying - ? null - : () => controller.speak(text), - enabled: !isPlaying, - tileColor: isCurrentPlaying ? Colors.blue.withOpacity(0.1) : null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: isCurrentPlaying - ? const BorderSide(color: Colors.blue, width: 1) - : BorderSide.none, - ), - ); - }); - }, - ), - ], - ), - ), - ), - - const SizedBox(height: 16), - - // 状态显示卡片 - Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Status', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Obx(() { - final state = controller.ttsState; - final stateText = _getTtsStateText(state); - final stateColor = _getTtsStateColor(state); - - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: stateColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: stateColor.withOpacity(0.3)), - ), - child: Row( - children: [ - Icon( - _getTtsStateIcon(state), - color: stateColor, - ), - const SizedBox(width: 8), - Text( - 'Current Status: $stateText', - style: TextStyle( - color: stateColor, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - }), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } - - // 连续播放所有文本 - void _playAllTexts() { - // 使用控制器的连续播放功能播放所有文本 - controller.speakTexts(_getSampleTexts()); - } - - // 获取TTS状态文本 - String _getTtsStateText(TtsState state) { - switch (state) { - case TtsState.playing: - return 'Playing'; - case TtsState.paused: - return 'Paused'; - case TtsState.stopped: - return 'Stopped'; - default: - return 'Unknown'; - } - } - - // 获取TTS状态颜色 - Color _getTtsStateColor(TtsState state) { - switch (state) { - case TtsState.playing: - return Colors.green; - case TtsState.paused: - return Colors.orange; - case TtsState.stopped: - return Colors.grey; - default: - return Colors.grey; - } - } - - // 获取TTS状态图标 - IconData _getTtsStateIcon(TtsState state) { - switch (state) { - case TtsState.playing: - return Icons.volume_up; - case TtsState.paused: - return Icons.pause; - case TtsState.stopped: - return Icons.stop; - default: - return Icons.help_outline; - } - } - - // 获取示例文本 - List _getSampleTexts() { - return [ - 'This is a Flutter TTS test. Please listen to the synthesis effect.', - '这是Flutter TTS测试,请听取合成效果。', - 'Flutter TTS supports various voices and parameter adjustments.', - 'Flutter TTS支持多种语音和参数调节,可以根据需要进行设置。', - ]; - } -} \ No newline at end of file diff --git a/lib/modules/test/views/volcano_asr_test_view.dart b/lib/modules/test/views/volcano_asr_test_view.dart deleted file mode 100644 index ecc56da9f..000000000 --- a/lib/modules/test/views/volcano_asr_test_view.dart +++ /dev/null @@ -1,234 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../controllers/volcano_asr_test_controller.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class VolcanoAsrTestView extends GetView { - const VolcanoAsrTestView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('火山语音识别测试'), - centerTitle: true, - ), - body: Padding( - padding: EdgeInsets.all(16.0.r), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 识别模式选择 - Card( - child: Padding( - padding: EdgeInsets.all(16.0.r), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '识别模式', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 8.h), - 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; - } - }, - ), - ), - ], - )), - ], - ), - ), - ), - - SizedBox(height: 16.h), - - // 录音控制 - Card( - child: Padding( - padding: EdgeInsets.all(16.0.r), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '录音控制', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 16.h), - 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: EdgeInsets.symmetric( - horizontal: 24.w, - vertical: 12.h, - ), - ), - ) - : ElevatedButton.icon( - onPressed: () => controller.startListening(), - icon: const Icon(Icons.mic), - label: const Text('开始录音'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - padding: EdgeInsets.symmetric( - horizontal: 24.w, - vertical: 12.h, - ), - ), - ), - ), - ), - ], - ), - ), - ), - - SizedBox(height: 16.h), - - // 识别结果 - Expanded( - child: Card( - child: Padding( - padding: EdgeInsets.all(16.0.r), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '识别结果', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - ), - ), - IconButton( - icon: const Icon(Icons.clear), - onPressed: () => controller.clearResults(), - tooltip: '清空结果', - ), - ], - ), - SizedBox(height: 8.h), - Expanded( - child: Container( - padding: EdgeInsets.all(8.0.r), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(4.0.r), - ), - child: Obx(() => ListView.builder( - itemCount: controller.recognitionResults.length, - itemBuilder: (context, index) { - final result = controller.recognitionResults[index]; - return Padding( - padding: EdgeInsets.symmetric(vertical: 4.0.h), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${index + 1}. ', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - Expanded( - child: Text(result), - ), - ], - ), - ); - }, - )), - ), - ), - ], - ), - ), - ), - ), - - SizedBox(height: 16.h), - - // 状态显示 - Card( - child: Padding( - padding: EdgeInsets.all(16.0.r), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '状态', - style: TextStyle( - fontSize: 16.sp, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 8.h), - Obx(() => Text( - controller.connectionStatus.value, - style: TextStyle( - color: controller.connectionStatus.value == 'Connected' - ? Colors.green - : (controller.connectionStatus.value == 'Disconnecting...' - ? Colors.orange - : Colors.grey), - fontWeight: FontWeight.bold, - ), - )), - SizedBox(height: 8.h), - Obx(() => controller.errorMessage.value.isNotEmpty - ? Text( - '错误: ${controller.errorMessage.value}', - style: TextStyle( - color: Colors.red, - fontSize: 14.sp, - ), - ) - : const SizedBox.shrink()), - ], - ), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/modules/test/views/volcano_tts_test_view.dart b/lib/modules/test/views/volcano_tts_test_view.dart deleted file mode 100644 index 5bae14c57..000000000 --- a/lib/modules/test/views/volcano_tts_test_view.dart +++ /dev/null @@ -1,224 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import '../controllers/volcano_tts_test_controller.dart'; - -class VolcanoTtsTestView extends GetView { - const VolcanoTtsTestView({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - // 强制更新控制器状态 - WidgetsBinding.instance.addPostFrameCallback((_) { - // 在下一帧渲染完成后调用 - controller.updateUIState(); - }); - - return Scaffold( - appBar: AppBar( - title: const Text('火山语音合成测试'), - centerTitle: true, - ), - body: SingleChildScrollView( - child: 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: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Obx(() { - return ElevatedButton.icon( - onPressed: controller.isAudioPlaying.value || controller.isLoading.value - ? null - : () => controller.speakContinuousTexts(), - icon: controller.isAudioPlaying.value - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2) - ) - : const Icon(Icons.playlist_play), - label: Text(controller.isAudioPlaying.value ? '播放中...' : '全部播放'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - ); - }), - Obx(() => ElevatedButton.icon( - onPressed: controller.isAudioPlaying.value - ? () => controller.stopSpeaking() - : null, - icon: const Icon(Icons.stop), - label: const Text('停止'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - )), - ], - ), - ], - ), - ), - ), - - 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), - // 生成5条可点击的播报内容 - Obx(() => ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: controller.speechTexts.length, - separatorBuilder: (context, index) => const Divider(height: 1), - itemBuilder: (context, index) { - final isPlaying = controller.isAudioPlaying.value && - controller.currentPlayingIndex.value == index; - - return ListTile( - title: Text( - controller.speechTexts[index], - style: TextStyle( - fontWeight: isPlaying ? FontWeight.bold : FontWeight.normal, - color: isPlaying ? Colors.blue : null, - ), - ), - leading: CircleAvatar( - backgroundColor: isPlaying ? Colors.blue : Colors.grey.shade200, - radius: 18, - child: Icon( - isPlaying ? Icons.volume_up : Icons.play_arrow, - color: isPlaying ? Colors.white : Colors.black, - size: 20, - ), - ), - trailing: isPlaying - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : null, - onTap: controller.isAudioPlaying.value || controller.isLoading.value - ? null - : () => controller.playSingle(index), - enabled: !(controller.isAudioPlaying.value || controller.isLoading.value), - tileColor: isPlaying ? Colors.blue.withOpacity(0.1) : null, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: isPlaying - ? const BorderSide(color: Colors.blue, width: 1) - : BorderSide.none, - ), - ); - }, - )), - ], - ), - ), - ), - - 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.statusMessage.value, - style: TextStyle( - color: controller.isLoading.value - ? Colors.orange - : controller.isAudioPlaying.value - ? Colors.green - : Colors.grey, - fontWeight: FontWeight.bold, - ), - )), - const SizedBox(height: 8), - Obx(() => controller.errorMessage.value.isNotEmpty - ? Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.red.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.error_outline, color: Colors.red, size: 16), - SizedBox(width: 8), - Text( - '错误', - style: TextStyle( - color: Colors.red, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - SizedBox(height: 4), - Text( - controller.errorMessage.value, - style: TextStyle(color: Colors.red.shade800), - ), - ], - ), - ) - : const SizedBox.shrink()), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} \ 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 caa514c28..a31ed7a5b 100644 --- a/lib/modules/translation/bindings/translation_binding.dart +++ b/lib/modules/translation/bindings/translation_binding.dart @@ -1,7 +1,9 @@ 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/speech_impl/azure_asr_service.dart'; +import '../../../data/services/speech_impl/azure_tts_service.dart'; +import '../../../data/services/asr_service.dart'; +import '../../../data/services/tts_service.dart'; import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/language_manager.dart'; @@ -9,8 +11,8 @@ class TranslationBinding implements Bindings { @override void dependencies() { Get.put(LanguageManager(), permanent: true); - Get.put(AzureAsrService(), permanent: true); - Get.put(AzureTtsService(), permanent: true); + Get.lazyPut(() => AzureAsrService(), fenix: true); + Get.lazyPut(() => AzureTtsService(), fenix: true); Get.put(VolcanoTranslationService(), permanent: true); Get.put(TranslationController()); } diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 0fd6d268b..fd93019bf 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -4,16 +4,16 @@ import 'dart:async'; import 'dart:convert'; import 'package:get_storage/get_storage.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/tts_service.dart'; import '../../../data/services/language_manager.dart'; import '../../../core/utils/logger.dart'; import '../../../data/services/voice_interaction_service.dart'; +import '../../../data/services/asr_service.dart'; class TranslationController extends GetxController { // 服务 - final AzureAsrService _asrService = Get.find(); + final AsrService _asrService = Get.find(); final VolcanoTranslationService _translationService = Get.find(); - final AzureTtsService _ttsService = Get.find(); + final TtsService _ttsService = Get.find(); final LanguageManager _languageManager = Get.find(); final GetStorage _storage = GetStorage(); final VoiceInteractionService _voiceInteractionService = Get.find(); @@ -377,7 +377,7 @@ class TranslationController extends GetxController { final voiceName = _languageManager.getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ?? 'en-US-AriaNeural'; await _ttsService.setVoice(voiceName); - await _ttsService.speak(text); + await _ttsService.speakOnce(text); } catch (e) { Logger.error('播放翻译失败: $e'); } @@ -404,7 +404,7 @@ class TranslationController extends GetxController { try { final voiceName = _languageManager.getTtsVoiceNameByAsrCode(item.targetLanguageCode) ?? 'en-US-AriaNeural'; await _ttsService.setVoice(voiceName); - await _ttsService.speak(item.translatedText); + await _ttsService.speakOnce(item.translatedText); } catch (e) { Logger.error('播放翻译项失败: $e'); } @@ -420,7 +420,7 @@ class TranslationController extends GetxController { : _languageManager.getTtsVoiceNameByAsrCode(targetLanguageCode.value) ?? 'en-US-AriaNeural'; await _ttsService.setVoice(voiceName); - await _ttsService.speak(text); + await _ttsService.speakOnce(text); } catch (e) { Logger.error('播放文本失败: $e'); } diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 2441f966d..7c3388e5e 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -7,17 +7,7 @@ import '../modules/profile/views/profile_view.dart'; 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_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/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'; import '../modules/translation/bindings/translation_binding.dart'; import '../modules/translation/views/translation_history_view.dart'; @@ -74,46 +64,7 @@ abstract class AppPages { page: () => const ProfileView(), binding: ProfileBinding(), ), - GetPage( - name: Routes.speechDemo, - page: () => const VolcanoAsrTestView(), - binding: VolcanoAsrTestBinding(), - ), - GetPage( - name: Routes.TTS_TEST, - page: () => VolcanoTtsTestView(), - binding: VolcanoTtsTestBinding(), - ), - GetPage( - name: Routes.ASR_TEST, - page: () => const AzureAsrTestView(), - binding: AzureAsrTestBinding(), - ), - GetPage( - name: Routes.AZURE_ASR_TEST, - page: () => const AzureAsrTestView(), - binding: AzureAsrTestBinding(), - ), - GetPage( - name: Routes.AZURE_TTS_TEST, - page: () => const AzureTtsTestView(), - binding: AzureTtsTestBinding(), - ), - GetPage( - name: Routes.VOLCANO_ASR_TEST, - page: () => const VolcanoAsrTestView(), - binding: VolcanoAsrTestBinding(), - ), - GetPage( - name: Routes.FLUTTER_TTS_TEST, - page: () => const FlutterTtsTestView(), - binding: FlutterTtsTestBinding(), - ), - GetPage( - name: Routes.FLUTTER_ASR_TEST, - page: () => const FlutterAsrTestView(), - binding: FlutterAsrTestBinding(), - ), + GetPage( name: Routes.translation, page: () => const TranslationView(), diff --git a/pubspec.yaml b/pubspec.yaml index f4e29d2e3..245cb88bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: font_awesome_flutter: ^10.8.0 lottie: ^3.3.1 app_settings: 5.2.0 + speech_to_text: ^7.0.0 dev_dependencies: flutter_test: From 6951c34163881714cf0a995d19263355357c89e8 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Tue, 18 Mar 2025 10:02:39 +0000 Subject: [PATCH 2/2] add --- lib/data/services/speech_factory.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/data/services/speech_factory.dart b/lib/data/services/speech_factory.dart index c3b659686..a6f1e4d50 100644 --- a/lib/data/services/speech_factory.dart +++ b/lib/data/services/speech_factory.dart @@ -63,11 +63,6 @@ class SpeechFactory extends GetxService { Get.lazyPut(() => FlutterTtsService()); break; } - - final asrService = Get.find(); - final ttsService = Get.find(); - asrService.initialize(); - ttsService.initialize(); } /// 初始化语音服务