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(); } /// 设置事件通道 void _setupEventChannel() { _eventSubscription?.cancel(); _eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) { if (event is Map) { _handleRecognitionEvent(event); } }, onError: _handleRecognitionError); } /// 处理来自原生端的识别事件(AST事件) /// 将原生侧 AST 事件映射为统一的 ASTEvent,提供给业务层使用。 void _handleRecognitionEvent(dynamic event) { if (event is! Map || _eventStreamController == null) return; final Map eventMap = event; final String eventType = eventMap['type'] as String? ?? ''; switch (eventType) { case 'recognized': print("Ast处理识别完成事件:${eventMap}"); 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? ?? ''; _latestRecognizedText = text; _latestDetectedLanguage = detectedLanguage; _eventStreamController?.add(ASTEvent( type: ASTEventType.finalResult, serviceId: serviceId, utteranceId: utteranceId, text: text, detectedLanguage: detectedLanguage, )); break; case 'recognizing': print("Ast处理识别中事件:${eventMap}"); 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? ?? ''; _eventStreamController?.add(ASTEvent( type: ASTEventType.intermediateResult, serviceId: serviceId, utteranceId: utteranceId, text: text, detectedLanguage: detectedLanguage, )); break; case 'translatedInterim': print("Ast处理翻译中事件:${eventMap}"); 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? ?? ''; _eventStreamController?.add(ASTEvent( type: ASTEventType.translationInterim, serviceId: serviceId, utteranceId: utteranceId, text: text, detectedLanguage: detectedLanguage, )); break; case 'translated': print("Ast处理翻译结果事件:${eventMap}"); 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? ?? ''; _eventStreamController?.add(ASTEvent( type: ASTEventType.translationResult, serviceId: serviceId, utteranceId: utteranceId, text: text, detectedLanguage: detectedLanguage, )); break; case 'sessionStarted': _eventStreamController?.add(ASTEvent( type: ASTEventType.sessionStarted, )); break; case 'sessionStopped': _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? ?? ''; if (reason.isNotEmpty || errorDetails.isNotEmpty) { Logger.error('识别取消: $reason - ${errorDetails.toString()}'); } _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('识别错误: ${error.toString()}'); _eventStreamController?.add(ASTEvent( type: ASTEventType.error, error: error, )); 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) { await initialize(); } try { _eventStreamController = StreamController.broadcast(); // 开始连续识别 final bool result = await _channel.invokeMethod('recognizeCallback'); if (!result) { _cleanupEventStream(); } return _eventStreamController!.stream; } catch (e) { Logger.error('开始连续语音识别失败: ${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 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 { await _eventSubscription?.cancel(); _eventSubscription = null; await _eventStreamController?.close(); _eventStreamController = null; await _channel.invokeMethod('dispose'); _isInitialized = false; Logger.info('Azure AST 资源已释放'); } 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; // 底层会初始化前释放 if (_isInitialized) { await dispose(); } 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; _setupEventChannel(); print('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); return result; } catch (e) { Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}'); _isInitialized = false; rethrow; } } @override void setLowVolumeThreshold(int threshold) { try { _channel.invokeMethod('setLowVolumeThreshold', { 'threshold': threshold, }); } catch (e) { Logger.error('设置低音量阈值失败: ${e.toString()}'); rethrow; } } }