You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

467 lines
13 KiB

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';
/// 音频源类型
enum AudioSourceType {
microphone, // 使用设备麦克风
external // 使用外部提供的音频数据
}
/// 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');
// final GetStorage _storage = GetStorage();
bool _isInitialized = false;
late final String _subscriptionKey;
late final String _serviceRegion;
final List<String> _defaultSupportedLanguages = ['zh-CN', 'en-US'];
@override
List<String> get supportedLanguages => _defaultSupportedLanguages;
// 连续识别相关
bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController;
StreamSubscription? _eventSubscription;
// 最新的识别结果
String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText;
// 最新检测到的语言
String _latestDetectedLanguage = '';
String get latestDetectedLanguage => _latestDetectedLanguage;
// 当前音频源类型
AudioSourceType _audioSourceType = AudioSourceType.microphone;
AzureAsrService() {
_loadConfig();
}
/// 从环境变量加载配置
void _loadConfig() {
// final _env = _storage.read("ENV") as Map<String, String>;
_subscriptionKey = AppConfig.env('AZURE_SPEECH_KEY') ?? '';
_serviceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? '';
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);
}
@override
Future<bool> initialize({
List<String>? supportedLanguages,
bool useExternalAudio = false,
bool useEchoCancellation = false,
}) async {
try {
final List<String> 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,
});
_setupEventChannel();
_isInitialized = result;
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}');
return result;
} catch (e) {
Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}');
_isInitialized = false;
rethrow;
}
}
@override
Future<RecognitionEvent> 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
Future<Stream<RecognitionEvent>> startContinuousRecognition(
bool audioSourceType) async {
if (!_isInitialized) {
await initialize();
}
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
try {
_eventStreamController = StreamController<RecognitionEvent>.broadcast();
// 开始连续识别
final bool result =
await _channel.invokeMethod('startContinuousRecognition', {
'audioSourceType': audioSourceType,
});
if (!result) {
_cleanupEventStream();
throw Exception('启动连续识别失败');
}
_isContinuousRecognitionActive = true;
return _eventStreamController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: ${e.toString()}');
_cleanupEventStream();
rethrow;
}
}
@override
Future<bool> stopContinuousRecognition() async {
if (!_isInitialized || !_isContinuousRecognitionActive) {
return true;
}
try {
final bool result =
await _channel.invokeMethod('stopContinuousRecognition');
_cleanupEventStream();
return result;
} catch (e) {
Logger.error('停止连续语音识别失败: ${e.toString()}');
_cleanupEventStream();
rethrow;
}
}
@override
bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive;
}
/// 处理来自原生端的识别事件
void _handleRecognitionEvent(dynamic event) {
if (event is! Map || _eventStreamController == null) return;
final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试
switch (eventType) {
case 'result':
final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage;
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.finalResult,
text: text,
detectedLanguage: detectedLanguage,
));
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,
));
break;
case 'sessionStarted':
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStarted,
));
break;
case 'sessionStopped':
_isContinuousRecognitionActive = false;
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.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(RecognitionEvent(
type: RecognitionEventType.canceled,
error: '$reason: $errorDetails',
));
break;
case 'error':
final String error = eventMap['message'] as String? ?? '';
Logger.error('识别错误: ${error.toString()}');
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.error,
error: error,
));
break;
}
}
/// 处理识别事件流错误
void _handleRecognitionError(Object error) {
Logger.error('识别事件流错误: ${error.toString()}');
_eventStreamController?.addError(error);
_cleanupEventStream();
}
/// 清理事件流资源
void _cleanupEventStream() {
_eventStreamController?.close();
_eventStreamController = null;
_isContinuousRecognitionActive = false;
}
@override
Future<void> dispose() async {
try {
if (_isContinuousRecognitionActive) {
await stopContinuousRecognition();
}
// 取消事件订阅
await _eventSubscription?.cancel();
_eventSubscription = null;
// 通知原生端释放资源
await _channel.invokeMethod('dispose');
_isInitialized = false;
Logger.info('Azure 语音识别资源已释放');
} catch (e) {
Logger.error('释放语音识别资源失败: ${e.toString()}');
_cleanupEventStream();
_isInitialized = false;
}
}
@override
Future<bool> 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<bool> 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<bool> 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<bool> pauseRecord() async {
try {
final bool result = await _channel.invokeMethod('pauseRecord');
return result;
} catch (e) {
Logger.error('停止录音: ${e.toString()}');
rethrow;
}
}
@override
Future<bool> 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<bool> 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<void> setPause() async {
// TODO: implement setPause
try {
final bool result = await _channel.invokeMethod('setPause');
return;
} catch (e) {
Logger.error('开始录音: ${e.toString()}');
rethrow;
}
}
@override
Future<void> setContinue() async {
// TODO: implement setcontinue
try {
final bool result = await _channel.invokeMethod('setcontinue');
return;
} catch (e) {
Logger.error('开始录音: ${e.toString()}');
rethrow;
}
}
@override
Future<void> disableBluetoothAudio() async {
// TODO: implement disableBluetoothAudio
try {
final bool result = await _channel.invokeMethod('disableBluetoothAudio');
return;
} catch (e) {
Logger.error('开始录音: ${e.toString()}');
rethrow;
}
}
@override
Future<void> restoreOriginalAudioState() async {
// TODO: implement restoreOriginalAudioState
try {
final bool result =
await _channel.invokeMethod('restoreOriginalAudioState');
return;
} catch (e) {
Logger.error('开始录音: ${e.toString()}');
rethrow;
}
}
}