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.
456 lines
13 KiB
456 lines
13 KiB
import 'dart:async';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
import '../../../core/utils/logger.dart';
|
|
import 'package:get/get.dart';
|
|
/// 微软语音识别服务
|
|
///
|
|
/// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口
|
|
class AzureAsrService {
|
|
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');
|
|
|
|
bool _isInitialized = false;
|
|
late final String _subscriptionKey;
|
|
late final String _serviceRegion;
|
|
|
|
// 当前语言
|
|
String _currentLanguage = 'zh-CN';
|
|
String get currentLanguage => _currentLanguage;
|
|
|
|
// 自动语言检测相关
|
|
bool _isAutoDetectLanguageEnabled = false;
|
|
bool get isAutoDetectLanguageEnabled => _isAutoDetectLanguageEnabled;
|
|
List<String> _supportedLanguages = ['zh-CN', 'en-US'];
|
|
List<String> get supportedLanguages => _supportedLanguages;
|
|
|
|
// 连续识别相关
|
|
bool _isContinuousRecognitionActive = false;
|
|
StreamController<RecognitionEvent>? _eventStreamController;
|
|
StreamSubscription? _eventSubscription;
|
|
|
|
// 事件通道状态
|
|
bool _isEventChannelReady = false;
|
|
Completer<void>? _eventChannelReadyCompleter;
|
|
|
|
// 公开的事件流
|
|
Stream<RecognitionEvent>? _recognitionStream;
|
|
Stream<RecognitionEvent>? get recognitionStream => _recognitionStream;
|
|
|
|
// 最新的识别结果
|
|
String _latestRecognizedText = '';
|
|
String get latestRecognizedText => _latestRecognizedText;
|
|
|
|
// 最新检测到的语言
|
|
String _latestDetectedLanguage = '';
|
|
String get latestDetectedLanguage => _latestDetectedLanguage;
|
|
|
|
AzureAsrService() {
|
|
_loadConfig();
|
|
_setupEventChannel();
|
|
}
|
|
|
|
/// 从环境变量加载配置
|
|
void _loadConfig() {
|
|
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
|
|
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
|
|
|
|
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
|
|
throw Exception('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
|
|
}
|
|
}
|
|
|
|
/// 设置事件通道
|
|
void _setupEventChannel() {
|
|
_eventChannelReadyCompleter = Completer<void>();
|
|
|
|
_eventSubscription = _eventChannel
|
|
.receiveBroadcastStream()
|
|
.listen((event) {
|
|
if (event is Map) {
|
|
final String eventType = event['type'] as String? ?? '';
|
|
|
|
// 处理通道准备好的事件
|
|
if (eventType == 'channelReady') {
|
|
_isEventChannelReady = true;
|
|
if (!_eventChannelReadyCompleter!.isCompleted) {
|
|
_eventChannelReadyCompleter!.complete();
|
|
}
|
|
return;
|
|
}
|
|
|
|
_handleRecognitionEvent(event);
|
|
}
|
|
}, onError: _handleRecognitionError);
|
|
}
|
|
|
|
/// 等待事件通道准备好
|
|
Future<bool> _waitForEventChannel({Duration timeout = const Duration(seconds: 5)}) async {
|
|
if (_isEventChannelReady) return true;
|
|
|
|
try {
|
|
await _eventChannelReadyCompleter!.future.timeout(timeout);
|
|
return true;
|
|
} on TimeoutException {
|
|
Logger.error('等待事件通道准备好超时');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 初始化微软语音识别 SDK
|
|
///
|
|
/// 返回 true 表示初始化成功,否则抛出 PlatformException
|
|
Future<bool> initialize({
|
|
String language = 'zh-CN',
|
|
bool autoDetectLanguage = false,
|
|
List<String>? supportedLanguages,
|
|
}) async {
|
|
_currentLanguage = language;
|
|
_isAutoDetectLanguageEnabled = autoDetectLanguage;
|
|
|
|
if (supportedLanguages != null && supportedLanguages.isNotEmpty) {
|
|
_supportedLanguages = supportedLanguages;
|
|
}
|
|
|
|
if (_isInitialized &&
|
|
_currentLanguage == language &&
|
|
_isAutoDetectLanguageEnabled == autoDetectLanguage) {
|
|
return true;
|
|
}
|
|
|
|
if (_isInitialized) {
|
|
await dispose();
|
|
}
|
|
|
|
try {
|
|
final bool result = await _channel.invokeMethod('initialize', {
|
|
'subscriptionKey': _subscriptionKey,
|
|
'region': _serviceRegion,
|
|
'language': language,
|
|
'autoDetectLanguage': autoDetectLanguage,
|
|
'supportedLanguages': _supportedLanguages,
|
|
});
|
|
_isInitialized = result;
|
|
return result;
|
|
} catch (e) {
|
|
Logger.error('语音识别初始化失败: $e');
|
|
_isInitialized = false;
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/// 设置识别语言
|
|
///
|
|
/// 注意:更改语言需要重新初始化识别器
|
|
Future<bool> setRecognitionLanguage(String language) async {
|
|
return initialize(language: language);
|
|
}
|
|
|
|
/// 执行一次性语音识别
|
|
///
|
|
/// 返回识别的文本和检测到的语言,如果识别失败则抛出 PlatformException
|
|
Future<RecognitionResult> recognizeSpeech() async {
|
|
if (!_isInitialized) {
|
|
await initialize();
|
|
}
|
|
|
|
try {
|
|
final result = await _channel.invokeMethod('recognizeOnce', {
|
|
'language': _currentLanguage,
|
|
});
|
|
|
|
if (result is Map) {
|
|
final String text = result['text'] as String? ?? '';
|
|
final String detectedLanguage = result['detectedLanguage'] as String? ?? '';
|
|
_latestRecognizedText = text;
|
|
_latestDetectedLanguage = detectedLanguage;
|
|
return RecognitionResult(text: text, detectedLanguage: detectedLanguage);
|
|
} else if (result is String) {
|
|
// 兼容旧版本
|
|
_latestRecognizedText = result;
|
|
return RecognitionResult(text: result);
|
|
}
|
|
|
|
throw Exception('无效的识别结果格式');
|
|
} catch (e) {
|
|
Logger.error('语音识别失败: $e');
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/// 开始连续语音识别
|
|
///
|
|
/// 返回一个包含识别事件的流,如果开始失败则抛出 PlatformException
|
|
Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
|
|
if (!_isInitialized) {
|
|
await initialize();
|
|
}
|
|
|
|
if (_isContinuousRecognitionActive) {
|
|
await stopContinuousRecognition();
|
|
}
|
|
|
|
try {
|
|
_eventStreamController = StreamController<RecognitionEvent>.broadcast();
|
|
|
|
final bool result = await _channel.invokeMethod('startContinuousRecognition');
|
|
|
|
if (!result) {
|
|
_cleanupEventStream();
|
|
throw Exception('启动连续识别失败');
|
|
}
|
|
|
|
_isContinuousRecognitionActive = true;
|
|
return _eventStreamController!.stream;
|
|
} catch (e) {
|
|
Logger.error('开始连续语音识别失败: $e');
|
|
_cleanupEventStream();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/// 停止连续语音识别
|
|
///
|
|
/// 返回 true 表示停止成功,否则抛出 PlatformException
|
|
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');
|
|
_cleanupEventStream();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/// 检查连续识别是否活跃
|
|
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');
|
|
}
|
|
|
|
_eventStreamController?.add(RecognitionEvent(
|
|
type: RecognitionEventType.canceled,
|
|
error: '$reason: $errorDetails',
|
|
));
|
|
break;
|
|
|
|
case 'error':
|
|
final String error = eventMap['message'] as String? ?? '';
|
|
Logger.error('识别错误: $error');
|
|
_eventStreamController?.add(RecognitionEvent(
|
|
type: RecognitionEventType.error,
|
|
error: error,
|
|
));
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// 处理识别事件流错误
|
|
void _handleRecognitionError(Object error) {
|
|
Logger.error('识别事件流错误: $error');
|
|
_eventStreamController?.addError(error);
|
|
_cleanupEventStream();
|
|
}
|
|
|
|
/// 清理事件流资源
|
|
void _cleanupEventStream() {
|
|
_eventStreamController?.close();
|
|
_eventStreamController = null;
|
|
_isContinuousRecognitionActive = false;
|
|
}
|
|
|
|
/// 释放资源
|
|
Future<void> dispose() async {
|
|
try {
|
|
if (_isContinuousRecognitionActive) {
|
|
await stopContinuousRecognition();
|
|
}
|
|
|
|
await _channel.invokeMethod('dispose');
|
|
_isInitialized = false;
|
|
} catch (e) {
|
|
Logger.error('释放语音识别资源失败: $e');
|
|
_cleanupEventStream();
|
|
_isInitialized = false;
|
|
}
|
|
}
|
|
|
|
/// 启用或禁用自动语言检测
|
|
Future<bool> enableAutoDetectLanguage(bool enable, {List<String>? languages}) async {
|
|
if (!_isInitialized) {
|
|
await initialize(autoDetectLanguage: enable, supportedLanguages: languages);
|
|
return _isAutoDetectLanguageEnabled;
|
|
}
|
|
|
|
try {
|
|
final bool result = await _channel.invokeMethod('enableAutoDetectLanguage', {
|
|
'enable': enable,
|
|
'languages': languages,
|
|
});
|
|
|
|
if (result) {
|
|
_isAutoDetectLanguageEnabled = enable;
|
|
if (languages != null && languages.isNotEmpty) {
|
|
_supportedLanguages = languages;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
} catch (e) {
|
|
Logger.error('设置自动语言检测失败: $e');
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/// 设置支持的语言列表(用于自动语言检测)
|
|
Future<bool> setSupportedLanguages(List<String> languages) async {
|
|
if (languages.isEmpty) {
|
|
throw ArgumentError('语言列表不能为空');
|
|
}
|
|
|
|
if (!_isInitialized) {
|
|
await initialize(supportedLanguages: languages);
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
final bool result = await _channel.invokeMethod('setSupportedLanguages', {
|
|
'languages': languages,
|
|
});
|
|
|
|
if (result) {
|
|
_supportedLanguages = languages;
|
|
}
|
|
|
|
return result;
|
|
} catch (e) {
|
|
Logger.error('设置支持的语言列表失败: $e');
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 识别事件类型
|
|
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}';
|
|
}
|
|
}
|