import 'dart:async'; import 'dart:typed_data'; import '../../../data/models/appconfig.dart'; import 'package:flutter/services.dart'; import 'package:get_storage/get_storage.dart'; import '../../../core/utils/logger.dart'; import 'package:get/get.dart'; import '../asr_service.dart'; /// Azure 语音识别服务 /// /// 该服务提供了通过平台通道与原生 Microsoft Speech SDK 交互的接口 class AzureAsrService extends GetxService implements AsrService { static final AzureAsrService to = Get.put(AzureAsrService()); static const MethodChannel _channel = MethodChannel('azure_speech/asr'); static const EventChannel _eventChannel = EventChannel('azure_speech/asr_events'); // 音频数据事件通道 static const EventChannel _audioDataEventChannel = EventChannel('azure_speech/audio_data_events'); // final GetStorage _storage = GetStorage(); bool _isInitialized = false; late final String _subscriptionKey; late final String _serviceRegion; late final String _xunfeiAppId; late final String _xunfeiAccessKeyId; late final String _xunfeiAccessKeySecret; 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; String _latestAsrProvider = ''; String get latestAsrProvider => _latestAsrProvider; // 音频数据事件订阅 StreamSubscription? _audioDataEventSubscription; // 音频数据流控制器 StreamController? _audioDataStreamController; // 当前音频源类型 AudioSourceType _audioSourceType = AudioSourceType.microphone; // 防抖时间戳与间隔(实例级) DateTime? _lastStartAt; DateTime? _lastStopAt; static const Duration _debounce = Duration(milliseconds: 600); AzureAsrService() { _loadConfig(); } /// 从环境变量加载配置 void _loadConfig() { // final _env = _storage.read("ENV") as Map; _subscriptionKey = AppConfig.env('AZURE_SPEECH_KEY') ?? ''; _serviceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? ''; _xunfeiAppId = AppConfig.env('XUNFEI_ASR_APP_ID') ?? ''; _xunfeiAccessKeyId = AppConfig.env('XUNFEI_ASR_ACCESS_KEY_ID') ?? ''; _xunfeiAccessKeySecret = AppConfig.env('XUNFEI_ASR_ACCESS_KEY_SECRET') ?? ''; if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { throw Exception( '未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); } } /// 设置事件通道 void _setupEventChannel() { _eventSubscription?.cancel(); _eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) { if (event is Map) { _handleRecognitionEvent(event); } }, onError: _handleRecognitionError); } /// 设置音频数据事件通道 void _setupAudioDataEventChannel() { _audioDataEventSubscription?.cancel(); _audioDataEventSubscription = _audioDataEventChannel.receiveBroadcastStream().listen((event) { if (event is Map) { _handleAudioDataEvent(event); } }, onError: (error) { Logger.error('音频数据事件流错误: ${error.toString()}'); }); } @override Future initialize({ List? supportedLanguages, bool useExternalAudio = false, bool useEchoCancellation = false, }) async { try { final List languages = supportedLanguages ?? _defaultSupportedLanguages; //底层会初始化前释放 // // 检查是否需要重新初始化 // if (_isInitialized) { // await dispose(); // } // 设置音频源类型 _audioSourceType = useExternalAudio ? AudioSourceType.external : AudioSourceType.microphone; final bool result = await _channel.invokeMethod('initialize', { 'subscriptionKey': _subscriptionKey, 'region': _serviceRegion, 'supportedLanguages': languages, 'audioSourceType': _audioSourceType.toString().split('.').last, 'useEchoCancellation': useEchoCancellation, 'xunfeiAppId': _xunfeiAppId, 'xunfeiAccessKeyId': _xunfeiAccessKeyId, 'xunfeiAccessKeySecret': _xunfeiAccessKeySecret, }); _setupEventChannel(); _isInitialized = result; Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); return result; } catch (e) { Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}'); _isInitialized = false; rethrow; } } @override void onClose() async { // 页面或服务被删除时,确保释放底层资源 await dispose(); super.onClose(); } @override Future recognizeOnce() async { if (!_isInitialized) { await initialize(); } try { // 调用同步的recognizeOnce方法 final result = await _channel.invokeMethod('recognizeOnce'); if (result is Map) { // 处理新格式返回 final String text = result['text'] as String? ?? ''; final String detectedLanguage = result['detectedLanguage'] as String? ?? ''; // 更新最新识别结果 _latestRecognizedText = text; _latestDetectedLanguage = detectedLanguage; return RecognitionEvent.finalResult( text: text, detectedLanguage: detectedLanguage, ); } else if (result is String) { // 兼容旧版本 _latestRecognizedText = result; return RecognitionEvent.finalResult(text: result); } throw Exception('无效的识别结果格式'); } catch (e) { Logger.error('语音识别失败: ${e.toString()}'); return RecognitionEvent.error(e.toString()); } } @override /// 开始连续语音识别(带防抖) /// /// 行为说明: /// - 在 600ms 的冷却时间内再次调用会被防抖拦截并返回 false; /// - 首次或冷却期外调用会继续启动连续识别; /// - 若当前已处于连续识别中,会先调用停止后再启动。 Future startContinuousRecognition( bool audioSourceType, { bool isRemoveFirstPunctuation = true, String mode = "normal", bool skipAsrForCallMode = false, }) async { // 防抖判断:短时间内重复调用直接拦截 final now = DateTime.now(); if (_lastStartAt != null && now.difference(_lastStartAt!) < _debounce) { return false; } _lastStartAt = now; if (!_isInitialized) { await initialize(); } if (_isContinuousRecognitionActive) { await stopContinuousRecognition(); } try { // 开始连续识别 final bool result = await _channel.invokeMethod('startContinuousRecognition', { 'audioSourceType': audioSourceType, 'isRemoveFirstPunctuation': isRemoveFirstPunctuation, 'mode': mode, 'skipAsrForCallMode': skipAsrForCallMode, }); if (!result) { throw Exception('启动连续识别失败'); } _isContinuousRecognitionActive = true; return result; } catch (e) { Logger.error('开始连续语音识别失败: ${e.toString()}'); rethrow; } } @override /// 停止连续语音识别(带防抖) /// /// 行为说明: /// - 在 600ms 的冷却时间内再次调用会被防抖拦截并返回 false; /// - 若当前不在识别中,直接返回 true; /// - 调用成功后立即将内部状态置为 false(同时事件通道回调仍会再次校正)。 Future stopContinuousRecognition() async { // 移除防抖判断,确保停止操作总是被执行 // 这与 AgentService 的逻辑保持一致,防止“松开还在录”的问题 _lastStopAt = DateTime.now(); if (!_isInitialized || !_isContinuousRecognitionActive) { return true; } try { final bool result = await _channel.invokeMethod('stopContinuousRecognition'); if (result) { _isContinuousRecognitionActive = false; } return result; } catch (e) { Logger.error('停止连续语音识别失败: ${e.toString()}'); rethrow; } } @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 bool isContinuousRecognitionActive() { return _isContinuousRecognitionActive; } /// 处理音频数据事件 void _handleAudioDataEvent(dynamic event) { if (event is! Map) return; final Map eventMap = event; final String eventType = eventMap['type'] as String? ?? ''; switch (eventType) { case 'audioData': final Uint8List data = eventMap['data'] as Uint8List? ?? Uint8List(0); final double timestamp = (eventMap['timestamp'] as num?)?.toDouble() ?? 0.0; final int size = eventMap['size'] as int? ?? 0; print('接收到音频数据: 大小=${size}字节, 时间戳=${timestamp}'); Logger.debug('接收到音频数据: 大小=${size}字节, 时间戳=${timestamp}'); // 发送到音频数据流 _audioDataStreamController?.add(data); break; } } /// 获取音频数据流 Stream? getAudioDataStream() { print("getAudioDataStream"); _audioDataStreamController ??= StreamController.broadcast(); return _audioDataStreamController?.stream; } /// 处理来自原生端的识别事件 void _handleRecognitionEvent(dynamic event) { if (event is! Map) { print('🔴[ASR-DBG] 非 Map 事件被丢弃: ${event.runtimeType}'); return; } if (_eventStreamController == null) { print('🔴[ASR-DBG] _eventStreamController 为 null,事件被丢弃: $event'); return; } final Map eventMap = event; final String eventType = eventMap['type'] as String? ?? ''; final String provider = eventMap['provider'] as String? ?? ''; if (eventType != 'nativeLog' && eventType != 'audioData') { print( '🔴[ASR-DBG] 原生事件抵达 azure_asr_service: type=$eventType, provider=$provider, text=${eventMap['text']}'); } if (provider.isNotEmpty) { if (provider != _latestAsrProvider) { _latestAsrProvider = provider; Logger.info('ASR provider: $_latestAsrProvider'); } } else { if (eventType == 'sessionStarted') { Logger.warning('ASR provider missing in event'); } } switch (eventType) { case 'nativeLog': final String level = eventMap['level'] as String? ?? 'INFO'; final String message = eventMap['message'] as String? ?? ''; switch (level) { case 'ERROR': Logger.e('NativeASR', message); break; case 'WARNING': Logger.w('NativeASR', message); break; case 'DEBUG': Logger.d('NativeASR', message); break; default: Logger.i('NativeASR', message); } return; case 'result': final String text = eventMap['text'] as String? ?? ''; final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; _latestRecognizedText = text; _latestDetectedLanguage = detectedLanguage; Logger.d('ASR', '[$_latestAsrProvider] result: lang=$detectedLanguage, text=${text}'); _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.finalResult, text: text, detectedLanguage: detectedLanguage, provider: _latestAsrProvider, )); break; case 'result1': final String text = eventMap['text'] as String? ?? ''; final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; _latestRecognizedText = text; _latestDetectedLanguage = detectedLanguage; Logger.d('ASR', '[$_latestAsrProvider] result1: lang=$detectedLanguage, text=${text}'); _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.finalResult1, text: text, detectedLanguage: detectedLanguage, provider: _latestAsrProvider, )); break; case 'recognizing': final String text = eventMap['text'] as String? ?? ''; final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.intermediateResult, text: text, detectedLanguage: detectedLanguage, provider: _latestAsrProvider, )); break; case 'sessionStarted': Logger.i('ASR', '[$_latestAsrProvider] sessionStarted'); _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.sessionStarted, provider: _latestAsrProvider, )); break; case 'sessionStopped': _isContinuousRecognitionActive = false; Logger.i('ASR', '[$_latestAsrProvider] sessionStopped'); _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.sessionStopped, provider: _latestAsrProvider, )); break; case 'canceled': _isContinuousRecognitionActive = false; final String reason = eventMap['reason'] as String? ?? ''; final String errorDetails = eventMap['errorDetails'] as String? ?? ''; Logger.e('ASR', '[$_latestAsrProvider] canceled: $reason - $errorDetails'); _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.canceled, error: '$reason: $errorDetails', provider: _latestAsrProvider, )); break; case 'error': final String error = eventMap['message'] as String? ?? ''; Logger.e('ASR', '[$_latestAsrProvider] error: $error'); _eventStreamController?.add(RecognitionEvent( type: RecognitionEventType.error, error: error, provider: _latestAsrProvider, )); break; } } /// 处理识别事件流错误 void _handleRecognitionError(Object error) { Logger.error('识别事件流错误: ${error.toString()}'); _eventStreamController?.addError(error); _cleanupEventStream(); } /// 清理事件流资源 void _cleanupEventStream() { _eventStreamController?.close(); _eventStreamController = null; _isContinuousRecognitionActive = false; } @override Future dispose() async { try { if (_isContinuousRecognitionActive) { await stopContinuousRecognition(); } // 取消事件订阅 await _eventSubscription?.cancel(); _eventSubscription = null; // 取消音频数据事件订阅 await _audioDataEventSubscription?.cancel(); _audioDataEventSubscription = null; // 关闭音频数据流 await _audioDataStreamController?.close(); _audioDataStreamController = null; // 通知原生端释放资源 await _channel.invokeMethod('dispose'); _isInitialized = false; Logger.info('Azure 语音识别资源已释放'); } catch (e) { Logger.error('释放语音识别资源失败: ${e.toString()}'); _cleanupEventStream(); _isInitialized = false; } } @override Future pushAudioData(Uint8List data) async { if (!_isInitialized) { await initialize(useExternalAudio: true); } if (_audioSourceType != AudioSourceType.external) { Logger.error('当前非外部音频模式,不能推送音频数据'); return false; } try { final bool result = await _channel.invokeMethod('pushAudioData', { 'audioData': data // Flutter 会自动将 Uint8List 转换为 ByteBuffer }); if (!result) { throw Exception('推送音频数据失败'); } return true; } catch (e) { Logger.error('推送音频数据失败: ${e.toString()}'); return false; } } @override Future enableRecord( AudioSourceType audioSourceType, String filePath, bool acceptAudioData, ) async { try { final bool result = await _channel.invokeMethod('enableRecord', { 'audioSourceType': audioSourceType.index, 'filePath': filePath, 'acceptAudioData': acceptAudioData, // 新增参数 }); if (acceptAudioData) { _setupAudioDataEventChannel(); } return result; } catch (e) { Logger.error('开始录音: ${e.toString()}'); rethrow; } } @override Future stopRecord(bool isSave) async { try { final bool result = await _channel.invokeMethod('stopRecord', { 'isSave': isSave, }); return result; } catch (e) { Logger.error('停止录音: ${e.toString()}'); rethrow; } } @override Future pauseRecord() async { try { final bool result = await _channel.invokeMethod('pauseRecord'); return result; } catch (e) { Logger.error('暂停录音: ${e.toString()}'); rethrow; } } /// 设置音频配置 /// /// [sampleRate] 采样率,默认16000 /// [channels] 声道数,默认1(单声道) Future setAudioConfig({ int sampleRate = 16000, int channels = 1, }) async { try { final bool result = await _channel.invokeMethod('setAudioConfig', { 'sampleRate': sampleRate, 'channels': channels, }); Logger.info( '音频配置设置${result ? '成功' : '失败'}: 采样率=$sampleRate, 声道数=$channels'); return result; } catch (e) { Logger.error('设置音频配置失败: ${e.toString()}'); rethrow; } } @override Future moveFile(String sourcePath, String destPath) async { // TODO: implement moveFile try { final bool result = await _channel.invokeMethod('moveFile', { 'sourcePath': sourcePath, 'destPath': destPath, }); return result; } catch (e) { Logger.error('开始录音: ${e.toString()}'); rethrow; } } @override Future renameFile(String filePath, String newName) async { // TODO: implement renameFile try { final bool result = await _channel.invokeMethod('renameFile', { 'filePath': filePath, 'newName': newName, }); return result; } catch (e) { Logger.error('开始录音: ${e.toString()}'); rethrow; } } @override Future disableBluetoothAudio() async { // TODO: implement disableBluetoothAudio try { final bool result = await _channel.invokeMethod('disableBluetoothAudio'); return; } catch (e) { Logger.error('开始录音: ${e.toString()}'); rethrow; } } @override Future restoreOriginalAudioState() async { // TODO: implement restoreOriginalAudioState try { final bool result = await _channel.invokeMethod('restoreOriginalAudioState'); return; } catch (e) { Logger.error('开始录音: ${e.toString()}'); rethrow; } } @override Future resumeRecord() async { try { final bool result = await _channel.invokeMethod('resumeRecord'); return result; } catch (e) { Logger.error('继续录音: ${e.toString()}'); rethrow; } } }