import 'dart:async'; import '../../../data/models/appconfig.dart'; import 'package:flutter/services.dart'; import '../../../core/utils/logger.dart'; import 'package:get/get.dart'; import '../ast_service.dart'; /// Azure 语音识别服务 /// /// 该服务提供了通过平台通道与原生 Microsoft Speech SDK 交互的接口 class AzureAstService extends GetxService implements AstService { static final AzureAstService to = Get.put(AzureAstService()); static const MethodChannel _channel = MethodChannel('azure_speech/ast'); static const EventChannel _eventChannel = EventChannel('azure_speech/ast_events'); bool _isInitialized = false; late final String _subscriptionKey; late final String _serviceRegion; late final String _azureTranslationKey; late final String _azureTranslationRegion; late final String _volcanoTranslationAccessKey; late final String _volcanoTranslationSecretKey; late final String _volcanoTranslationRegion; late final String _xunfeiAppId; late final String _xunfeiAccessKeyId; late final String _xunfeiAccessKeySecret; late final String _iflytekHost; /// 豆包语音识别服务相关配置 late final String _doubaoAppKey; late final String _doubaoAccessKey; late final String _doubaoResourceId; /// 阿里巴巴语音识别服务相关配置 late final String _alibabaAppKey; late final String _alibabaAppId; late final String _alibabaAppURL; final List _defaultSupportedLanguages = ['zh-CN', 'en-US']; @override List get supportedLanguages => _defaultSupportedLanguages; //连续识别相关 bool _isContinuousRecognitionActive = false; StreamController? _eventStreamController; StreamSubscription? _eventSubscription; // 最新的识别结果 String _latestRecognizedText = ''; String get latestRecognizedText => _latestRecognizedText; // 最新检测到的语言 String _latestDetectedLanguage = ''; String get latestDetectedLanguage => _latestDetectedLanguage; AzureAstService() { _loadConfig(); } @override void onClose() async { // 页面或服务被删除时,确保释放底层资源 await dispose(); super.onClose(); } /// 设置事件通道(只在首次调用时订阅 EventChannel,后续复用) void _setupEventChannel() { // 确保 _eventStreamController 已就绪,避免丢失早期事件 _eventStreamController ??= StreamController.broadcast(); // EventChannel.receiveBroadcastStream() 只能活跃一次,复用已有订阅 if (_eventSubscription != null) { Logger.info('[STS] _setupEventChannel: 复用已有EventSubscription, streamController=${_eventStreamController.hashCode}'); return; } Logger.info('[STS] _setupEventChannel: 首次订阅EventChannel, streamController=${_eventStreamController.hashCode}'); _eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) { if (event is Map) { _handleRecognitionEvent(event); } }, onError: _handleRecognitionError); } /// 处理来自原生端的识别事件(AST事件) void _handleRecognitionEvent(dynamic event) { if (event is! Map) return; if (_eventStreamController == null) { Logger.error('[STS] _handleRecognitionEvent: _eventStreamController 为 null,事件被丢弃: ${event['type']}'); return; } if (_eventStreamController!.isClosed) { Logger.error('[STS] _handleRecognitionEvent: _eventStreamController 已关闭,事件被丢弃: ${event['type']}'); return; } final Map eventMap = event; final String eventType = eventMap['type'] as String? ?? ''; switch (eventType) { case 'recognized': final String serviceId = eventMap['serviceId'] as String? ?? ''; final String utteranceId = eventMap['utteranceId'] as String? ?? ''; final String text = eventMap['text'] as String? ?? ''; final String detectedLanguage = eventMap['language'] as String? ?? ''; Logger.info('[STS] 识别完成 [$serviceId]: text=$text, uttId=$utteranceId'); _latestRecognizedText = text; _latestDetectedLanguage = detectedLanguage; _eventStreamController?.add(ASTEvent( type: ASTEventType.finalResult, serviceId: serviceId, utteranceId: utteranceId, text: text, detectedLanguage: detectedLanguage, )); break; case 'recognizing': final String serviceId = eventMap['serviceId'] as String? ?? ''; final String utteranceId = eventMap['utteranceId'] as String? ?? ''; final String text = eventMap['text'] as String? ?? ''; final String detectedLanguage = eventMap['language'] as String? ?? ''; Logger.d('AzureAST', '[STS] 识别中 [$serviceId]: text=$text'); _eventStreamController?.add(ASTEvent( type: ASTEventType.intermediateResult, serviceId: serviceId, utteranceId: utteranceId, text: text, detectedLanguage: detectedLanguage, )); break; case 'translatedInterim': final String serviceId = eventMap['serviceId'] as String? ?? ''; final String utteranceId = eventMap['utteranceId'] as String? ?? ''; final String text = eventMap['translatedText'] as String? ?? ''; final String detectedLanguage = eventMap['targetLanguage'] as String? ?? ''; Logger.d('AzureAST', '[STS] 翻译中 [$serviceId]: text=$text'); _eventStreamController?.add(ASTEvent( type: ASTEventType.translationInterim, serviceId: serviceId, utteranceId: utteranceId, text: text, detectedLanguage: detectedLanguage, )); break; case 'translated': final String serviceId = eventMap['serviceId'] as String? ?? ''; final String utteranceId = eventMap['utteranceId'] as String? ?? ''; final String text = eventMap['translatedText'] as String? ?? ''; final String detectedLanguage = eventMap['targetLanguage'] as String? ?? ''; Logger.info('[STS] 翻译完成 [$serviceId]: text=$text, uttId=$utteranceId'); _eventStreamController?.add(ASTEvent( type: ASTEventType.translationResult, serviceId: serviceId, utteranceId: utteranceId, text: text, detectedLanguage: detectedLanguage, )); break; case 'sessionStarted': Logger.info('[STS] 会话开始'); _eventStreamController?.add(ASTEvent( type: ASTEventType.sessionStarted, )); break; case 'sessionStopped': Logger.info('[STS] 会话停止'); _isContinuousRecognitionActive = false; _eventStreamController?.add(ASTEvent( type: ASTEventType.sessionStopped, )); break; case 'canceled': _isContinuousRecognitionActive = false; final String reason = eventMap['reason'] as String? ?? ''; final String errorDetails = eventMap['errorDetails'] as String? ?? ''; Logger.error('[STS] 识别取消: $reason - $errorDetails'); _eventStreamController?.add(ASTEvent( type: ASTEventType.canceled, error: '$reason: $errorDetails', )); break; case 'error': String error = eventMap['message'] as String? ?? ''; if (error.isEmpty) { error = eventMap['error'] as String? ?? ''; } final String code = eventMap['code']?.toString() ?? ''; if (code.isNotEmpty) { error = '$error (Code: $code)'; } Logger.error('[STS] 识别错误: $error'); _eventStreamController?.add(ASTEvent( type: ASTEventType.error, error: error, )); break; case 'serviceInitialized': Logger.info('[STS] 服务初始化: ${eventMap['serviceId']} ${eventMap['direction']}'); break; default: Logger.info('[STS] 未处理事件: $eventType'); break; } } /// 处理识别事件流错误 void _handleRecognitionError(Object error) { Logger.error('识别事件流错误: ${error.toString()}'); _eventStreamController?.addError(error); _cleanupEventStream(); } /// 清理事件流资源 void _cleanupEventStream() { _eventStreamController?.close(); _eventStreamController = null; _isContinuousRecognitionActive = false; } /// 从环境变量加载配置 void _loadConfig() { _subscriptionKey = AppConfig.env('AZURE_SPEECH_KEY') ?? ''; _serviceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? ''; _azureTranslationKey = AppConfig.env('AZURE_TRANSLATION_KEY') ?? ''; _azureTranslationRegion = AppConfig.env('AZURE_TRANSLATION_REGION') ?? ''; _volcanoTranslationAccessKey = AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY') ?? ''; _volcanoTranslationSecretKey = AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY') ?? ''; _volcanoTranslationRegion = AppConfig.env('VOLCANO_TRANSLATION_REGION') ?? 'cn-north-1'; _xunfeiAppId = AppConfig.env('XUNFEI_ASR_APP_ID') ?? ''; _xunfeiAccessKeyId = AppConfig.env('XUNFEI_ASR_ACCESS_KEY_ID') ?? ''; _xunfeiAccessKeySecret = AppConfig.env('XUNFEI_ASR_ACCESS_KEY_SECRET') ?? ''; _iflytekHost = AppConfig.env('IFLYTEK_ASR_HOST') ?? ''; _doubaoAppKey = AppConfig.env('VOLC_OPENSPEECH_APP_ID') ?? ''; _doubaoAccessKey = AppConfig.env('VOLC_OPENSPEECH_ACCESS_TOKEN') ?? ''; _doubaoResourceId = AppConfig.env('VOLC_OPENSPEECH_TRANSLATION_BIGMODEL') ?? ''; _alibabaAppKey = AppConfig.env('ALIBABA_OPENSPEECH_APP_KEY') ?? ''; _alibabaAppId = AppConfig.env('ALIBABA_OPENSPEECH_APP_ID') ?? ''; _alibabaAppURL = AppConfig.env('ALIBABA_OPENSPEECH_APP_URL') ?? ''; if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { throw Exception( '未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); } } @override Future> recognizeCallback() async { if (!_isInitialized) { Logger.info('[STS] recognizeCallback: 未初始化,先执行initialize'); await initialize(); } try { _eventStreamController ??= StreamController.broadcast(); Logger.info('[STS] recognizeCallback: streamController=${_eventStreamController.hashCode}, hasListener=${_eventStreamController!.hasListener}'); // 开始连续识别 final bool result = await _channel.invokeMethod('recognizeCallback'); Logger.info('[STS] recognizeCallback: invokeMethod 结果=$result'); if (!result) { _cleanupEventStream(); } return _eventStreamController!.stream; } catch (e) { Logger.error('[STS] recognizeCallback 失败: ${e.toString()}'); rethrow; } } @override Future enableRecord(String filePath) async { try { final bool result = await _channel.invokeMethod('enableRecord', { 'filePath': filePath, }); return result; } catch (e) { Logger.error('开始录音: ${e.toString()}'); rethrow; } } @override Future pauseRecord() async { try { final bool result = await _channel.invokeMethod('pauseRecord'); return result; } on MissingPluginException { Logger.warning('[STS] pauseRecord: 原生未实现'); return false; } catch (e) { Logger.error('暂停录音失败: ${e.toString()}'); return false; } } @override Future resumeRecord() async { try { final bool result = await _channel.invokeMethod('resumeRecord'); return result; } on MissingPluginException { Logger.warning('[STS] resumeRecord: 原生未实现'); return false; } catch (e) { Logger.error('继续录音失败: ${e.toString()}'); return false; } } @override Future stopRecord(bool isSave) async { try { final bool result = await _channel.invokeMethod('stopRecord', { 'isSave': isSave, }); return result; } on MissingPluginException { Logger.warning('[STS] stopRecord: 原生未实现'); return false; } catch (e) { Logger.error('停止录音失败: ${e.toString()}'); return false; } } @override Future path(String filePath) async { try { final bool result = await _channel.invokeMethod('path', { 'filePath': filePath, }); return result; } catch (e) { Logger.error('开始录音: ${e.toString()}'); rethrow; } } @override Future startContinuousTranslation() async { try { final bool result = await _channel.invokeMethod('startContinuousTranslation'); return result; } catch (e) { Logger.error('停止录音: ${e.toString()}'); rethrow; } } @override Future stopContinuousTranslation() async { try { final bool result = await _channel.invokeMethod('stopContinuousTranslation'); return result; } catch (e) { Logger.error('停止录音: ${e.toString()}'); rethrow; } } @override Future dispose() async { try { Logger.info('[STS] dispose: 开始释放, streamController=${_eventStreamController?.hashCode}, eventSub=${_eventSubscription?.hashCode}'); // 不取消 _eventSubscription(EventChannel 只能订阅一次,保持复用) // 只关闭 _eventStreamController,下次 initialize 时会重新创建 await _eventStreamController?.close(); _eventStreamController = null; await _channel.invokeMethod('dispose'); _isInitialized = false; Logger.info('[STS] dispose: 完成, streamController=null, eventSub保留'); } catch (e) { Logger.error('Azure AST 资源释放失败: ${e.toString()}'); _isInitialized = false; } } @override Future initialize({ List? supportedLanguages, String provider = 'azure', }) async { try { final List languages = supportedLanguages ?? _defaultSupportedLanguages; Logger.info('[STS] initialize: provider=$provider, isInitialized=$_isInitialized'); // 底层会初始化前释放 if (_isInitialized) { await dispose(); } // 先建立事件通道,确保原生端发事件时 astEventSink 已就绪 _setupEventChannel(); Logger.info('[STS] initialize: eventChannel已建立, streamController=${_eventStreamController?.hashCode}'); final bool result = await _channel.invokeMethod('initialize', { 'provider': provider, 'subscriptionKey': _subscriptionKey, 'region': _serviceRegion, 'supportedLanguages': languages, 'volcanoTranslationAccessKey': _volcanoTranslationAccessKey, 'volcanoTranslationSecretKey': _volcanoTranslationSecretKey, 'volcanoTranslationRegion': _volcanoTranslationRegion, 'azureTranslationKey': _azureTranslationKey, 'azureTranslationServiceRegion': _azureTranslationRegion, 'xfyunAppId': _xunfeiAppId, 'xfyunAccessKeyId': _xunfeiAccessKeyId, 'xfyunAccessKeySecret': _xunfeiAccessKeySecret, 'iflytekHost': _iflytekHost, 'appKey': _doubaoAppKey, 'accessKey': _doubaoAccessKey, 'resourceId': _doubaoResourceId, 'alibabaAppKey': _alibabaAppKey, 'alibabaAppId': _alibabaAppId, 'alibabaAppURL': _alibabaAppURL, }); _isInitialized = result; print('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); return result; } catch (e) { Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}'); _isInitialized = false; rethrow; } } @override Future setBroadcastPeerTranslate(bool enabled) async { try { await _channel.invokeMethod('setBroadcastPeerTranslate', { 'enabled': enabled, }); Logger.info('[STS] setBroadcastPeerTranslate=$enabled'); } on MissingPluginException { // 原生未实现:忽略(默认原生层会按自己的默认行为处理) } catch (e) { Logger.error('setBroadcastPeerTranslate 失败: $e'); } } @override void setLowVolumeThreshold(int threshold) { try { _channel.invokeMethod('setLowVolumeThreshold', { 'threshold': threshold, }); } catch (e) { Logger.error('设置低音量阈值失败: ${e.toString()}'); rethrow; } } }