|
|
|
@ -834,18 +834,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
|
try { |
|
|
|
Logger.info('开始初始化通话模式语音翻译服务'); |
|
|
|
|
|
|
|
// 初始化 ASR 服务时,明确指定需要支持的语言 |
|
|
|
final List<String> asrSupportedLanguages = [targetLanguageCode.value]; |
|
|
|
Logger.info('1初始化ASR服务,支持语言: $asrSupportedLanguages'); |
|
|
|
|
|
|
|
await _asrService.initialize(supportedLanguages: asrSupportedLanguages); |
|
|
|
|
|
|
|
// 获取识别事件流 |
|
|
|
var recognitionStream = await _asrService.recognizeCallback(); |
|
|
|
_recognitionSubscription?.cancel(); |
|
|
|
_recognitionSubscription = |
|
|
|
recognitionStream.listen(_handleRecognitionEvent); |
|
|
|
|
|
|
|
final sourceShort = _languageManager.getShortCodeByAsrCode(sourceLanguageCode.value) ?? 'zh'; |
|
|
|
final targetShort = _languageManager.getShortCodeByAsrCode(targetLanguageCode.value) ?? 'en'; |
|
|
|
final bestProvider = _languageManager.findBestMatchingProvider(sourceShort, targetShort); |
|
|
|
@ -870,10 +858,15 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
|
|
|
|
|
await _astService.initialize(supportedLanguages: callModeLanguages, provider: astProvider); |
|
|
|
|
|
|
|
// 获取识别事件流 |
|
|
|
// 获取识别事件流(端到端服务已包含 ASR+翻译+TTS,不需要单独启动 ASR) |
|
|
|
var astStream = await _astService.recognizeCallback(); |
|
|
|
_astEventSubscription?.cancel(); |
|
|
|
_astEventSubscription = astStream.listen(_handleAstEvent); |
|
|
|
_astEventSubscription = astStream.listen(_handleAstEvent, onError: (e) { |
|
|
|
Logger.error('[STS] astStream 错误: $e'); |
|
|
|
}, onDone: () { |
|
|
|
Logger.info('[STS] astStream 已关闭'); |
|
|
|
}); |
|
|
|
Logger.info('[STS] 已订阅 astStream, subscription=${_astEventSubscription.hashCode}'); |
|
|
|
|
|
|
|
Logger.info('通话模式语音翻译服务初始化完成'); |
|
|
|
} catch (e) { |
|
|
|
@ -948,13 +941,19 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
|
Future<void> startRecording() async { |
|
|
|
_timerManager.startTimer(); |
|
|
|
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); |
|
|
|
if (currentMode.value == 'call' || currentMode.value == 'audioVideo') { |
|
|
|
await _asrService.enableRecord( |
|
|
|
true, "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); |
|
|
|
AudioSourceType audioSourceType; |
|
|
|
if (currentMode.value == 'call') { |
|
|
|
audioSourceType = AudioSourceType.external; |
|
|
|
} else if (currentMode.value == 'audioVideo') { |
|
|
|
audioSourceType = AudioSourceType.systemAudio; |
|
|
|
} else { |
|
|
|
await _asrService.enableRecord( |
|
|
|
false, "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); |
|
|
|
audioSourceType = AudioSourceType.microphone; |
|
|
|
} |
|
|
|
await _asrService.enableRecord( |
|
|
|
audioSourceType, |
|
|
|
"${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav", |
|
|
|
false, |
|
|
|
); |
|
|
|
|
|
|
|
// if (currentMode.value == 'call') { |
|
|
|
// await _astService.enableRecord( |
|
|
|
@ -1087,28 +1086,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
|
} |
|
|
|
|
|
|
|
/// 配置通话模式 |
|
|
|
/// 配置通话模式(只做 BLE 配置,AST 已在 _initializeCallModeTranslationService 中初始化) |
|
|
|
Future<void> _configureCallMode() async { |
|
|
|
final sourceShort = _languageManager.getShortCodeByAsrCode(sourceLanguageCode.value) ?? 'zh'; |
|
|
|
final targetShort = _languageManager.getShortCodeByAsrCode(targetLanguageCode.value) ?? 'en'; |
|
|
|
final bestProvider = _languageManager.findBestMatchingProvider(sourceShort, targetShort); |
|
|
|
final astProvider = bestProvider?.provider ?? 'azure'; |
|
|
|
|
|
|
|
final transCode0 = bestProvider?.sourceSpec['translationCode'] ?? sourceLanguageCode.value; |
|
|
|
final transCode1 = bestProvider?.targetSpec['translationCode'] ?? targetLanguageCode.value; |
|
|
|
final ttsVoice0 = bestProvider?.sourceSpec['ttsCode'] ?? |
|
|
|
(_languageManager.getTtsVoiceNameByAsrCode(sourceLanguageCode.value) ?? 'zh-CN-XiaoxiaoNeural'); |
|
|
|
final ttsVoice1 = bestProvider?.targetSpec['ttsCode'] ?? |
|
|
|
(_languageManager.getTtsVoiceNameByAsrCode(targetLanguageCode.value) ?? 'en-US-AriaNeural'); |
|
|
|
final List<String> callModeLanguages = [ |
|
|
|
sourceLanguageCode.value, |
|
|
|
targetLanguageCode.value, |
|
|
|
transCode0, |
|
|
|
transCode1, |
|
|
|
ttsVoice0, |
|
|
|
ttsVoice1, |
|
|
|
]; |
|
|
|
Logger.info('1初始化AST服务,支持语言: $callModeLanguages, provider: $astProvider'); |
|
|
|
await _astService.initialize(supportedLanguages: callModeLanguages, provider: astProvider); |
|
|
|
isPreparing.value = true; |
|
|
|
await bleManager.openA2DPDecoder(); |
|
|
|
int attempts = 0; |
|
|
|
@ -1232,71 +1211,208 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
|
} |
|
|
|
|
|
|
|
/// 处理 AST(语音识别+翻译一体)事件 |
|
|
|
/// 按 serviceId 区分双路(A=己方, B=对方),用 utteranceId 匹配同一句话的事件 |
|
|
|
void _handleAstEvent(ASTEvent event) { |
|
|
|
Logger.info('[STS] _handleAstEvent 收到: type=${event.type}, serviceId=${event.serviceId}, text=${event.text}'); |
|
|
|
// 根据 serviceId 确定语言方向 |
|
|
|
final String srcCode = event.serviceId == "A" |
|
|
|
? sourceLanguageCode.value |
|
|
|
: targetLanguageCode.value; |
|
|
|
final String tgtCode = event.serviceId == "A" |
|
|
|
? targetLanguageCode.value |
|
|
|
: sourceLanguageCode.value; |
|
|
|
|
|
|
|
switch (event.type) { |
|
|
|
case ASTEventType.intermediateResult: |
|
|
|
if (event.text.isEmpty) break; |
|
|
|
Logger.info('AST 识别中 [${event.serviceId}]: ${event.text}'); |
|
|
|
handleIntermediateResult(event.text); |
|
|
|
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); |
|
|
|
|
|
|
|
// 按 utteranceId 查找已有项 |
|
|
|
TranslationItem? target; |
|
|
|
int targetIndex = -1; |
|
|
|
if (event.utteranceId.isNotEmpty) { |
|
|
|
for (int i = translationHistory.length - 1; i >= 0; i--) { |
|
|
|
if (translationHistory[i].isIntermediate && |
|
|
|
translationHistory[i].utteranceId == event.utteranceId) { |
|
|
|
target = translationHistory[i]; |
|
|
|
targetIndex = i; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (target != null) { |
|
|
|
target.sourceText = event.text; |
|
|
|
target.sourceLanguageCode = srcCode; |
|
|
|
target.targetLanguageCode = tgtCode; |
|
|
|
target.serviceId = event.serviceId; |
|
|
|
translationHistory.refresh(); |
|
|
|
} else { |
|
|
|
_historyManager.addTranslationItem(TranslationItem( |
|
|
|
sourceText: event.text, |
|
|
|
translatedText: '', |
|
|
|
sourceLanguageCode: srcCode, |
|
|
|
targetLanguageCode: tgtCode, |
|
|
|
timestamp: DateTime.now(), |
|
|
|
sessionId: currentSessionId!, |
|
|
|
isFirstInSession: translationHistory.isEmpty || |
|
|
|
translationHistory.last.sessionId != currentSessionId, |
|
|
|
isIntermediate: true, |
|
|
|
utteranceId: event.utteranceId, |
|
|
|
serviceId: event.serviceId, |
|
|
|
)); |
|
|
|
} |
|
|
|
break; |
|
|
|
|
|
|
|
case ASTEventType.finalResult: |
|
|
|
if (event.text.isEmpty) break; |
|
|
|
Logger.info('AST 识别完成 [${event.serviceId}]: ${event.text}'); |
|
|
|
// AST 已内置翻译,直接更新源文本,不再调用 translateText |
|
|
|
if (translationHistory.isNotEmpty && |
|
|
|
translationHistory.last.isIntermediate) { |
|
|
|
translationHistory.last.sourceText = event.text; |
|
|
|
translationHistory.last.isIntermediate = false; |
|
|
|
translationHistory.refresh(); |
|
|
|
_scrollToBottom(); |
|
|
|
saveTranslationHistory(); |
|
|
|
} else { |
|
|
|
final newItem = TranslationItem( |
|
|
|
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); |
|
|
|
|
|
|
|
TranslationItem? target; |
|
|
|
if (event.utteranceId.isNotEmpty) { |
|
|
|
for (int i = translationHistory.length - 1; i >= 0; i--) { |
|
|
|
if (translationHistory[i].isIntermediate && |
|
|
|
translationHistory[i].utteranceId == event.utteranceId) { |
|
|
|
target = translationHistory[i]; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (target == null) { |
|
|
|
target = TranslationItem( |
|
|
|
sourceText: event.text, |
|
|
|
translatedText: '', |
|
|
|
sourceLanguageCode: sourceLanguageCode.value, |
|
|
|
targetLanguageCode: targetLanguageCode.value, |
|
|
|
sourceLanguageCode: srcCode, |
|
|
|
targetLanguageCode: tgtCode, |
|
|
|
timestamp: DateTime.now(), |
|
|
|
sessionId: currentSessionId ?? |
|
|
|
DateTime.now().millisecondsSinceEpoch.toString(), |
|
|
|
sessionId: currentSessionId!, |
|
|
|
isFirstInSession: translationHistory.isEmpty || |
|
|
|
translationHistory.last.sessionId != currentSessionId, |
|
|
|
isIntermediate: false, |
|
|
|
isIntermediate: true, |
|
|
|
utteranceId: event.utteranceId, |
|
|
|
serviceId: event.serviceId, |
|
|
|
isFinalResultReceived: true, |
|
|
|
); |
|
|
|
translationHistory.add(newItem); |
|
|
|
_historyManager.addTranslationItem(target); |
|
|
|
} else { |
|
|
|
target.sourceText = event.text; |
|
|
|
target.sourceLanguageCode = srcCode; |
|
|
|
target.targetLanguageCode = tgtCode; |
|
|
|
target.serviceId = event.serviceId; |
|
|
|
target.isFinalResultReceived = true; |
|
|
|
} |
|
|
|
|
|
|
|
// 识别+翻译都到齐才标记完成 |
|
|
|
if (target.isFinalResultReceived && target.isTranslationResultReceived) { |
|
|
|
target.isIntermediate = false; |
|
|
|
translationHistory.refresh(); |
|
|
|
_scrollToBottom(); |
|
|
|
saveTranslationHistory(); |
|
|
|
} else { |
|
|
|
translationHistory.refresh(); |
|
|
|
} |
|
|
|
break; |
|
|
|
|
|
|
|
case ASTEventType.translationInterim: |
|
|
|
if (event.text.isEmpty) break; |
|
|
|
Logger.info('AST 翻译中 [${event.serviceId}]: ${event.text}'); |
|
|
|
if (translationHistory.isNotEmpty) { |
|
|
|
translationHistory.last.translatedText = event.text; |
|
|
|
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); |
|
|
|
|
|
|
|
TranslationItem? target; |
|
|
|
if (event.utteranceId.isNotEmpty) { |
|
|
|
for (int i = translationHistory.length - 1; i >= 0; i--) { |
|
|
|
if (translationHistory[i].isIntermediate && |
|
|
|
translationHistory[i].utteranceId == event.utteranceId) { |
|
|
|
target = translationHistory[i]; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (target != null) { |
|
|
|
target.translatedText = event.text; |
|
|
|
target.sourceLanguageCode = srcCode; |
|
|
|
target.targetLanguageCode = tgtCode; |
|
|
|
translationHistory.refresh(); |
|
|
|
} else { |
|
|
|
_historyManager.addTranslationItem(TranslationItem( |
|
|
|
sourceText: '', |
|
|
|
translatedText: event.text, |
|
|
|
sourceLanguageCode: srcCode, |
|
|
|
targetLanguageCode: tgtCode, |
|
|
|
timestamp: DateTime.now(), |
|
|
|
sessionId: currentSessionId!, |
|
|
|
isFirstInSession: translationHistory.isEmpty || |
|
|
|
translationHistory.last.sessionId != currentSessionId, |
|
|
|
isIntermediate: true, |
|
|
|
utteranceId: event.utteranceId, |
|
|
|
serviceId: event.serviceId, |
|
|
|
)); |
|
|
|
} |
|
|
|
break; |
|
|
|
|
|
|
|
case ASTEventType.translationResult: |
|
|
|
if (event.text.isEmpty) break; |
|
|
|
Logger.info('AST 翻译完成 [${event.serviceId}]: ${event.text}'); |
|
|
|
if (translationHistory.isNotEmpty) { |
|
|
|
translationHistory.last.translatedText = event.text; |
|
|
|
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); |
|
|
|
|
|
|
|
TranslationItem? target; |
|
|
|
if (event.utteranceId.isNotEmpty) { |
|
|
|
for (int i = translationHistory.length - 1; i >= 0; i--) { |
|
|
|
if (translationHistory[i].isIntermediate && |
|
|
|
translationHistory[i].utteranceId == event.utteranceId) { |
|
|
|
target = translationHistory[i]; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (target == null) { |
|
|
|
target = TranslationItem( |
|
|
|
sourceText: '', |
|
|
|
translatedText: event.text, |
|
|
|
sourceLanguageCode: srcCode, |
|
|
|
targetLanguageCode: tgtCode, |
|
|
|
timestamp: DateTime.now(), |
|
|
|
sessionId: currentSessionId!, |
|
|
|
isFirstInSession: translationHistory.isEmpty || |
|
|
|
translationHistory.last.sessionId != currentSessionId, |
|
|
|
isIntermediate: true, |
|
|
|
utteranceId: event.utteranceId, |
|
|
|
serviceId: event.serviceId, |
|
|
|
isTranslationResultReceived: true, |
|
|
|
); |
|
|
|
translationHistory.add(target); |
|
|
|
translationHistory.refresh(); |
|
|
|
_scrollToBottom(); |
|
|
|
} else { |
|
|
|
target.translatedText = event.text; |
|
|
|
target.sourceLanguageCode = srcCode; |
|
|
|
target.targetLanguageCode = tgtCode; |
|
|
|
target.isTranslationResultReceived = true; |
|
|
|
} |
|
|
|
|
|
|
|
if (target.isFinalResultReceived && target.isTranslationResultReceived) { |
|
|
|
target.isIntermediate = false; |
|
|
|
translationHistory.refresh(); |
|
|
|
_scrollToBottom(); |
|
|
|
saveTranslationHistory(); |
|
|
|
} else { |
|
|
|
translationHistory.refresh(); |
|
|
|
} |
|
|
|
break; |
|
|
|
|
|
|
|
case ASTEventType.error: |
|
|
|
Logger.error('AST 错误: ${event.error}'); |
|
|
|
isRecognizing.value = false; |
|
|
|
break; |
|
|
|
|
|
|
|
case ASTEventType.canceled: |
|
|
|
Logger.error('AST 取消: ${event.error}'); |
|
|
|
isRecognizing.value = false; |
|
|
|
break; |
|
|
|
|
|
|
|
default: |
|
|
|
@ -1898,7 +2014,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
|
} |
|
|
|
|
|
|
|
if (currentMode.value == "call") { |
|
|
|
_initializeCallModeTranslationService(); |
|
|
|
await _initializeCallModeTranslationService(); |
|
|
|
} else { |
|
|
|
Logger.info('重新初始化ASR服务,支持语言: $asrSupportedLanguages'); |
|
|
|
|
|
|
|
|