20 changed files with 2336 additions and 211 deletions
@ -0,0 +1,107 @@ |
|||||
|
import 'dart:async'; |
||||
|
import 'dart:typed_data'; |
||||
|
|
||||
|
/// 语音识别服务接口 |
||||
|
abstract class AstService { |
||||
|
/// 支持的语言 |
||||
|
List<String> get supportedLanguages; |
||||
|
|
||||
|
/// 初始化语音识别服务 |
||||
|
Future<bool> initialize({required List<String> supportedLanguages}); |
||||
|
|
||||
|
/// 开始录音 |
||||
|
Future<bool> enableRecord(String filePath); |
||||
|
|
||||
|
/// 停止录音 |
||||
|
Future<bool> stopContinuousTranslation(bool isSave); |
||||
|
|
||||
|
/// 开始录音 |
||||
|
Future<bool> path(String filePath); |
||||
|
} |
||||
|
|
||||
|
/// 识别事件类型 |
||||
|
// enum RecognitionEventType { |
||||
|
// /// 最终识别结果 |
||||
|
// finalResult, |
||||
|
|
||||
|
// /// 中间识别结果(实时反馈) |
||||
|
// intermediateResult, |
||||
|
|
||||
|
// /// 音频 |
||||
|
// onAudio, |
||||
|
|
||||
|
// /// 会话开始 |
||||
|
// sessionStarted, |
||||
|
|
||||
|
// /// 会话结束 |
||||
|
// sessionStopped, |
||||
|
|
||||
|
// /// 识别取消 |
||||
|
// canceled, |
||||
|
|
||||
|
// /// 识别错误 |
||||
|
// error, |
||||
|
// } |
||||
|
|
||||
|
/// 识别事件 |
||||
|
// class RecognitionEvent { |
||||
|
// /// 事件类型 |
||||
|
// final RecognitionEventType type; |
||||
|
|
||||
|
// /// 识别文本(仅在 finalResult 和 intermediateResult 类型中有效) |
||||
|
// final String text; |
||||
|
|
||||
|
// /// 检测到的语言 |
||||
|
// final String detectedLanguage; |
||||
|
|
||||
|
// /// 角色 |
||||
|
// final String role; |
||||
|
|
||||
|
// /// 原始音频 |
||||
|
// final Uint8List? audio; |
||||
|
|
||||
|
// /// 错误信息(仅在 error 和 canceled 类型中有效) |
||||
|
// final String error; |
||||
|
|
||||
|
// RecognitionEvent({ |
||||
|
// required this.type, |
||||
|
// this.text = '', |
||||
|
// this.detectedLanguage = '', |
||||
|
// this.role = '', |
||||
|
// this.audio, |
||||
|
// this.error = '', |
||||
|
// }); |
||||
|
|
||||
|
// /// 创建最终结果事件的快捷构造函数 |
||||
|
// factory RecognitionEvent.finalResult({ |
||||
|
// required String text, |
||||
|
// String detectedLanguage = '', |
||||
|
// }) { |
||||
|
// return RecognitionEvent( |
||||
|
// type: RecognitionEventType.finalResult, |
||||
|
// text: text, |
||||
|
// detectedLanguage: detectedLanguage, |
||||
|
// ); |
||||
|
// } |
||||
|
|
||||
|
// /// 创建错误事件的快捷构造函数 |
||||
|
// factory RecognitionEvent.error(String errorMessage) { |
||||
|
// return RecognitionEvent( |
||||
|
// type: RecognitionEventType.error, |
||||
|
// error: errorMessage, |
||||
|
// ); |
||||
|
// } |
||||
|
|
||||
|
// /// 检查是否为最终结果 |
||||
|
// bool get isFinalResult => type == RecognitionEventType.finalResult; |
||||
|
|
||||
|
// /// 检查是否为错误 |
||||
|
// bool get isError => |
||||
|
// type == RecognitionEventType.error || |
||||
|
// type == RecognitionEventType.canceled; |
||||
|
|
||||
|
// @override |
||||
|
// String toString() { |
||||
|
// return 'RecognitionEvent{type: $type, text: $text, detectedLanguage: $detectedLanguage, error: $error}'; |
||||
|
// } |
||||
|
// } |
||||
@ -0,0 +1,164 @@ |
|||||
|
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'; |
||||
|
|
||||
|
/// 音频源类型 |
||||
|
enum AudioSourceType { |
||||
|
microphone, // 使用设备麦克风 |
||||
|
external // 使用外部提供的音频数据 |
||||
|
} |
||||
|
|
||||
|
/// 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'); |
||||
|
// final GetStorage _storage = GetStorage(); |
||||
|
bool _isInitialized = false; |
||||
|
late final String _subscriptionKey; |
||||
|
late final String _serviceRegion; |
||||
|
late String _baseUrl; |
||||
|
final String _endpoint = '/'; // 修改为根路径 |
||||
|
late final String _accessKey; |
||||
|
late final String _secretKey; |
||||
|
late final String _region; |
||||
|
late final String _service; |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
AzureAstService() { |
||||
|
_loadConfig(); |
||||
|
} |
||||
|
|
||||
|
/// 从环境变量加载配置 |
||||
|
void _loadConfig() { |
||||
|
// final _env = _storage.read("ENV") as Map<String, String>; |
||||
|
_subscriptionKey = AppConfig.env('AZURE_SPEECH_KEY') ?? ''; |
||||
|
_serviceRegion = AppConfig.env('AZURE_SPEECH_REGION') ?? ''; |
||||
|
_accessKey = AppConfig.env('VOLCANO_TRANSLATION_ACCESS_KEY') ?? ''; |
||||
|
_secretKey = AppConfig.env('VOLCANO_TRANSLATION_SECRET_KEY') ?? ''; |
||||
|
_region = AppConfig.env('VOLCANO_TRANSLATION_REGION') ?? 'cn-north-1'; |
||||
|
_service = 'translate'; |
||||
|
_baseUrl = 'https://translate.volcengineapi.com'; |
||||
|
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { |
||||
|
throw Exception( |
||||
|
'未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@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> 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<bool> stopContinuousTranslation(bool isSave) async { |
||||
|
try { |
||||
|
final bool result = |
||||
|
await _channel.invokeMethod('stopContinuousTranslation', { |
||||
|
'isSave': isSave, |
||||
|
}); |
||||
|
|
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
Logger.error('停止录音: ${e.toString()}'); |
||||
|
rethrow; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@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; |
||||
|
// Future<bool> initialize({ |
||||
|
// required String subscriptionKey, |
||||
|
// required String region, |
||||
|
// required List<String> supportedLanguages, |
||||
|
// required String audioSourceType, |
||||
|
// required String translationAccessKey, |
||||
|
// required String translationSecretKey, |
||||
|
// String translationRegion = 'cn-north-1', |
||||
|
// }); |
||||
|
|
||||
|
final bool result = await _channel.invokeMethod('initialize', { |
||||
|
'subscriptionKey': _subscriptionKey, |
||||
|
'region': _serviceRegion, |
||||
|
'supportedLanguages': languages, |
||||
|
'audioSourceType': _audioSourceType.toString().split('.').last, |
||||
|
'translationAccessKey': _accessKey, |
||||
|
'translationSecretKey': _secretKey, |
||||
|
'translationRegion': _region, |
||||
|
}); |
||||
|
|
||||
|
_isInitialized = result; |
||||
|
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}'); |
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}'); |
||||
|
_isInitialized = false; |
||||
|
rethrow; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
Loading…
Reference in new issue