|
|
@ -1,6 +1,7 @@ |
|
|
import 'dart:io'; |
|
|
import 'dart:io'; |
|
|
import 'dart:async'; |
|
|
import 'dart:async'; |
|
|
|
|
|
|
|
|
|
|
|
import 'package:dio/dio.dart' show CancelToken; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter/services.dart'; |
|
|
import 'package:flutter/services.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
@ -668,7 +669,12 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
_historyManager.initialize(); |
|
|
_historyManager.initialize(); |
|
|
|
|
|
|
|
|
// 应用模式逻辑(含ASR/TTS配置等) |
|
|
// 应用模式逻辑(含ASR/TTS配置等) |
|
|
changeTranslationMode(initMode); |
|
|
// ⚠️ 这是个 async 方法,里面才会按当前语言对去 initialize ASR。 |
|
|
|
|
|
// 不接住这个 Future 的话,用户进页面立刻点「开始」会抢在它前面: |
|
|
|
|
|
// AzureAsrService.startContinuousRecognition 那条 `if (!_isInitialized)` |
|
|
|
|
|
// 兜底分支**不带语言**,会落到默认的 ['zh-CN','en-US']—— |
|
|
|
|
|
// 选了别的语种就等于拿错的候选语言在做识别。startRecognition 会先 await 它。 |
|
|
|
|
|
_modeInitFuture = changeTranslationMode(initMode); |
|
|
|
|
|
|
|
|
// 监听设备上报的通话翻译开关 (0x16 / 0x17) |
|
|
// 监听设备上报的通话翻译开关 (0x16 / 0x17) |
|
|
_setupDeviceCallTranslationListener(initMode); |
|
|
_setupDeviceCallTranslationListener(initMode); |
|
|
@ -1155,10 +1161,15 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
await _initServicesFuture; |
|
|
await _initServicesFuture; |
|
|
_timerManager.startTimer(); |
|
|
_timerManager.startTimer(); |
|
|
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); |
|
|
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); |
|
|
// 通话翻译按 RecordingArchive 已经定好的 trans_call_ 前缀命名, |
|
|
// 文件名即语音记录助手里的标题,前缀按 RecordingArchive 的约定走: |
|
|
// 停止时才能归档进语音记录助手;其它模式暂不归档,命名保持原样。 |
|
|
// trans_call_* 通话翻译 |
|
|
final String filePath = currentMode.value == 'call' |
|
|
// trans_holder_* 同声翻译 |
|
|
? "${dir.path}/trans_call_$formattedTime.wav" |
|
|
// 其它模式(音视频/面对面)暂不归档,沿用本地化标题命名。 |
|
|
|
|
|
// ⚠️ 别把这里的前缀和下面 stopRecording 的归档条件改岔了—— |
|
|
|
|
|
// 命名对不上约定,归档进去的标题就成了「同声传译_2026...」这种, |
|
|
|
|
|
// 列表里跟别的来源混在一起认不出。 |
|
|
|
|
|
final String filePath = _shouldArchiveRecording |
|
|
|
|
|
? "${dir.path}/${_archivePrefix}_$formattedTime.wav" |
|
|
: "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"; |
|
|
: "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"; |
|
|
_currentRecordingPath = filePath; |
|
|
_currentRecordingPath = filePath; |
|
|
|
|
|
|
|
|
@ -1197,15 +1208,31 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
isCreateRecord.value = false; |
|
|
isCreateRecord.value = false; |
|
|
Logger.info('录音已停止,计时器已重置'); |
|
|
Logger.info('录音已停止,计时器已重置'); |
|
|
|
|
|
|
|
|
if (currentMode.value == 'call' && path != null && seconds > 0) { |
|
|
if (_shouldArchiveRecording && path != null && seconds > 0) { |
|
|
await RecordingArchive.archive( |
|
|
// ⚠️ 不 await:归档里有一次 echomeet_addrecord 网络请求(Dio 30s |
|
|
|
|
|
// connect + 30s receive),await 会把「点结束」一路堵到超时。 |
|
|
|
|
|
// 归档本身不依赖 controller(RecordingArchive 是静态的), |
|
|
|
|
|
// 页面退了也能跑完。 |
|
|
|
|
|
unawaited(RecordingArchive.archive( |
|
|
path: path, |
|
|
path: path, |
|
|
seconds: seconds, |
|
|
seconds: seconds, |
|
|
tag: 'TranslationController', |
|
|
tag: 'TranslationController', |
|
|
); |
|
|
)); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 这个模式的录音要不要进「语音记录助手」。 |
|
|
|
|
|
/// |
|
|
|
|
|
/// 通话翻译和同声翻译要;音视频翻译录的是系统声音、面对面翻译是逐句 |
|
|
|
|
|
/// 按轮次开停的(见 _startFaceToFaceRecognition),两者的归档语义还没定, |
|
|
|
|
|
/// 先不动。 |
|
|
|
|
|
bool get _shouldArchiveRecording => |
|
|
|
|
|
currentMode.value == 'call' || currentMode.value == 'simultaneous'; |
|
|
|
|
|
|
|
|
|
|
|
/// 归档文件名前缀,取值见 RecordingArchive 的约定。 |
|
|
|
|
|
String get _archivePrefix => |
|
|
|
|
|
currentMode.value == 'call' ? 'trans_call' : 'trans_holder'; |
|
|
|
|
|
|
|
|
/// 暂停录音 |
|
|
/// 暂停录音 |
|
|
Future<void> pauseRecording() async { |
|
|
Future<void> pauseRecording() async { |
|
|
_timerManager.pauseTimer(); |
|
|
_timerManager.pauseTimer(); |
|
|
@ -1253,6 +1280,14 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
if (!await _checkPermissions()) return; |
|
|
if (!await _checkPermissions()) return; |
|
|
if (isRecognizing.value) return; |
|
|
if (isRecognizing.value) return; |
|
|
|
|
|
|
|
|
|
|
|
// 等 onInit 里那次按当前语言对做的 ASR 初始化落地,别让它被兜底的 |
|
|
|
|
|
// 默认 zh-CN/en-US 初始化抢先(见 onInit 中 _modeInitFuture 的说明)。 |
|
|
|
|
|
try { |
|
|
|
|
|
await _modeInitFuture; |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
Logger.error('等待模式初始化失败(忽略): $e'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
await _initializeSession(); |
|
|
await _initializeSession(); |
|
|
|
|
|
|
|
|
@ -1651,6 +1686,18 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
_startAsrActiveTracking(); |
|
|
_startAsrActiveTracking(); |
|
|
isRecognizing.value = true; |
|
|
isRecognizing.value = true; |
|
|
|
|
|
|
|
|
|
|
|
// 同声翻译**默认开录音**:不录就没有文件,也就没有东西归档进「语音记录助手」, |
|
|
|
|
|
// 而用户的预期是translate完能在助手里找到这段音频。 |
|
|
|
|
|
// 录音按钮仍然在,用户本轮可以随时关掉;下一轮开始时回到默认开。 |
|
|
|
|
|
// |
|
|
|
|
|
// ⚠️ 只对同声翻译这么做,**通话翻译保持手动**:那录的是通话双方的声音, |
|
|
|
|
|
// 默认开启属于「未经告知录音」,合规风险完全不是一个量级。 |
|
|
|
|
|
// 音视频翻译录的是系统播放声,面对面是逐句按轮次开停,两者归档语义都还没定, |
|
|
|
|
|
// 一并保持原样。 |
|
|
|
|
|
if (currentMode.value == 'simultaneous' && !isRecording.value) { |
|
|
|
|
|
isRecording.value = true; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if (isRecording.value && isRecognizing.value) { |
|
|
if (isRecording.value && isRecognizing.value) { |
|
|
startRecording(); |
|
|
startRecording(); |
|
|
} |
|
|
} |
|
|
@ -1970,6 +2017,14 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
Future<void> stopRecognition() async { |
|
|
Future<void> stopRecognition() async { |
|
|
if (!isRecognizing.value) return; |
|
|
if (!isRecognizing.value) return; |
|
|
|
|
|
|
|
|
|
|
|
// ⚠️ 先翻状态再清理,别放到最后。 |
|
|
|
|
|
// 底下这串(停录音、关 ASR 的 WebSocket、归档)随时可能要好几秒,而 |
|
|
|
|
|
// 「开始/结束」按钮是 Obx 盯着 isRecognizing 的——放在最末尾翻,用户点完 |
|
|
|
|
|
// 「结束」会盯着一个毫无反应的按钮,以为没点上又点一次。 |
|
|
|
|
|
// 提前翻不影响防重入:开头那行 `if (!isRecognizing.value) return` 正是靠 |
|
|
|
|
|
// 它挡住重复调用,提前翻只会更严。 |
|
|
|
|
|
isRecognizing.value = false; |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
await stopRecording(); |
|
|
await stopRecording(); |
|
|
await _asrService.stopContinuousRecognition(); |
|
|
await _asrService.stopContinuousRecognition(); |
|
|
@ -1984,7 +2039,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
: sourceLanguageCode.value)) ?? |
|
|
: sourceLanguageCode.value)) ?? |
|
|
'未知'; |
|
|
'未知'; |
|
|
_recordAsrUsageStats(langName); |
|
|
_recordAsrUsageStats(langName); |
|
|
isRecognizing.value = false; |
|
|
|
|
|
currentSessionId = null; |
|
|
currentSessionId = null; |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
Logger.error('停止语音识别失败: ${e.toString()}'); |
|
|
Logger.error('停止语音识别失败: ${e.toString()}'); |
|
|
@ -2034,16 +2088,29 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
_historyManager.scrollToBottom(); |
|
|
_historyManager.scrollToBottom(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 防抖处理翻译请求 |
|
|
// 中间结果的翻译只是屏幕上的预览:它不播 TTS,也不写历史、不记统计。 |
|
|
|
|
|
// 原来是「每 300ms 无条件发一次」,一句 5 秒的话就是十几次真实的服务商调用; |
|
|
|
|
|
// 这些在途请求还会跟说完之后那一次 final 翻译抢同一条链路和同一份配额, |
|
|
|
|
|
// 弱网时直接表现为「话说完了译文还要等很久」。 |
|
|
|
|
|
// |
|
|
|
|
|
// 现在:间隔拉到 [_interimTranslateDebounce],且要求文本比上次真正发出去的 |
|
|
|
|
|
// 多出 [_interimMinGrowth] 个字才发(改写过的文本不受这条限制,照发)。 |
|
|
|
|
|
// 真正的取消发生在 [translateText] 里。 |
|
|
_translationDebounceTimer?.cancel(); |
|
|
_translationDebounceTimer?.cancel(); |
|
|
_translationDebounceTimer = Timer(const Duration(milliseconds: 300), () { |
|
|
_translationDebounceTimer = Timer(_interimTranslateDebounce, () { |
|
|
if (translationHistory.isNotEmpty && |
|
|
if (translationHistory.isEmpty) return; |
|
|
translationHistory.last.isIntermediate && |
|
|
final last = translationHistory.last; |
|
|
translationHistory.last.sourceText.isNotEmpty) { |
|
|
if (!last.isIntermediate || last.sourceText.isEmpty) return; |
|
|
translateText(translationHistory.last.sourceText, |
|
|
|
|
|
translationHistory.last.timestamp, |
|
|
// 只是在上次发出去的文本后面又多了一两个字:不值得再翻一次。 |
|
|
isFinal: false); |
|
|
// 文本被 ASR 改写过(不再以上次那段开头)则不受此限,照发。 |
|
|
} |
|
|
final notGrownEnough = last.sourceText.startsWith(_lastInterimSentSource) && |
|
|
|
|
|
last.sourceText.length - _lastInterimSentSource.length < |
|
|
|
|
|
_interimMinGrowth; |
|
|
|
|
|
if (notGrownEnough) return; |
|
|
|
|
|
|
|
|
|
|
|
_lastInterimSentSource = last.sourceText; |
|
|
|
|
|
translateText(last.sourceText, last.timestamp, isFinal: false); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -2105,10 +2172,30 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
isTranslating.value = true; |
|
|
isTranslating.value = true; |
|
|
String? translationResult; |
|
|
String? translationResult; |
|
|
|
|
|
|
|
|
|
|
|
// 中间结果(预览)可以被后来者取消;final 不行——它要写历史、要播报。 |
|
|
|
|
|
CancelToken? cancelToken; |
|
|
|
|
|
final int seq; |
|
|
|
|
|
if (isFinal) { |
|
|
|
|
|
// 说完了,屏幕上的预览已经没有意义。把在途的中间请求全部撤掉, |
|
|
|
|
|
// 免得它们晚一步回来,把 final 译文盖成半句。 |
|
|
|
|
|
_interimTranslateToken?.cancel('final result arrived'); |
|
|
|
|
|
_interimTranslateToken = null; |
|
|
|
|
|
_lastInterimSentSource = ''; |
|
|
|
|
|
seq = ++_interimTranslateSeq; |
|
|
|
|
|
} else { |
|
|
|
|
|
_interimTranslateToken?.cancel('superseded by newer interim'); |
|
|
|
|
|
cancelToken = _interimTranslateToken = CancelToken(); |
|
|
|
|
|
seq = ++_interimTranslateSeq; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
final languageCodes = _determineLanguageCodes(); |
|
|
final languageCodes = _determineLanguageCodes(); |
|
|
translationResult = await _performTranslation( |
|
|
translationResult = await _performTranslation(sourceText, |
|
|
sourceText, languageCodes['source']!, languageCodes['target']!); |
|
|
languageCodes['source']!, languageCodes['target']!, cancelToken); |
|
|
|
|
|
|
|
|
|
|
|
// 过期的中间结果直接丢:这期间已经有更新的中间请求、或者 final 发出去了, |
|
|
|
|
|
// 再写回去只会让译文闪回上一句的半截。 |
|
|
|
|
|
if (!isFinal && seq != _interimTranslateSeq) return null; |
|
|
|
|
|
|
|
|
if (translationResult != null) { |
|
|
if (translationResult != null) { |
|
|
await _updateTranslationHistory( |
|
|
await _updateTranslationHistory( |
|
|
@ -2117,7 +2204,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
} catch (e, st) { |
|
|
} catch (e, st) { |
|
|
Logger.e('Translation', '翻译失败: $e', e, st); |
|
|
Logger.e('Translation', '翻译失败: $e', e, st); |
|
|
} finally { |
|
|
} finally { |
|
|
isTranslating.value = false; |
|
|
// 只有最新那一发才有资格熄掉 loading,否则被取消的旧请求会把 |
|
|
|
|
|
// 正在跑的 final 提前标记成"翻译完了"。 |
|
|
|
|
|
if (seq == _interimTranslateSeq) isTranslating.value = false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return translationResult; |
|
|
return translationResult; |
|
|
@ -2184,12 +2273,13 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
/// [sourceCode] 源语言代码 |
|
|
/// [sourceCode] 源语言代码 |
|
|
/// [targetCode] 目标语言代码 |
|
|
/// [targetCode] 目标语言代码 |
|
|
/// 返回翻译结果 |
|
|
/// 返回翻译结果 |
|
|
Future<String?> _performTranslation( |
|
|
Future<String?> _performTranslation(String sourceText, String sourceCode, |
|
|
String sourceText, String sourceCode, String targetCode) async { |
|
|
String targetCode, CancelToken? cancelToken) async { |
|
|
return await _translationService.translateText( |
|
|
return await _translationService.translateText( |
|
|
text: sourceText, |
|
|
text: sourceText, |
|
|
sourceLanguageCode: sourceCode, |
|
|
sourceLanguageCode: sourceCode, |
|
|
targetLanguageCode: targetCode, |
|
|
targetLanguageCode: targetCode, |
|
|
|
|
|
cancelToken: cancelToken, |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -2236,6 +2326,14 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
translationHistory[index].targetLanguageCode = languageCodes['target']!; |
|
|
translationHistory[index].targetLanguageCode = languageCodes['target']!; |
|
|
translationHistory.refresh(); |
|
|
translationHistory.refresh(); |
|
|
|
|
|
|
|
|
|
|
|
// ⚠️ 播报必须排在最前面。 |
|
|
|
|
|
// 它是用户唯一「听得见」的一环,而下面的浮窗更新、写历史、记统计 |
|
|
|
|
|
// 全是 await 的平台通道 / 存储操作。原来 TTS 排在它们之后, |
|
|
|
|
|
// 等于白白把播报推迟了这一整串的耗时。 |
|
|
|
|
|
if (isFinal) { |
|
|
|
|
|
await _handleTtsPlayback(translationResult); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 更新悬浮窗内容 |
|
|
// 更新悬浮窗内容 |
|
|
if (isFloatingWindowEnabled.value) { |
|
|
if (isFloatingWindowEnabled.value) { |
|
|
await _updateFloatingWindowWithTranslation(translationHistory[index]); |
|
|
await _updateFloatingWindowWithTranslation(translationHistory[index]); |
|
|
@ -2254,10 +2352,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
_scrollToBottom(); |
|
|
_scrollToBottom(); |
|
|
|
|
|
|
|
|
if (isFinal) { |
|
|
|
|
|
await _handleTtsPlayback(translationResult); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 记录翻译统计 |
|
|
/// 记录翻译统计 |
|
|
@ -2637,6 +2731,49 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
await _reinitializeAsrService(restartRecognition: wasRecognizing); |
|
|
await _reinitializeAsrService(restartRecognition: wasRecognizing); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
/// 当前模式该给 ASR 传哪几种语言。 |
|
|
|
|
|
/// |
|
|
|
|
|
/// 传 1 种 = 固定语种识别;传 2 种 = 原生打开 Azure 的 **连续语种识别** |
|
|
|
|
|
/// (`LanguageIdMode=Continuous` + AutoDetectSourceLanguageConfig)。 |
|
|
|
|
|
/// 连续语种识别要先缓冲够音频、对候选语言逐个打分才敢出 final, |
|
|
|
|
|
/// 这段等待直接加在「说完到出译文」上,是面对面/同传最大的一块固定延迟。 |
|
|
|
|
|
/// |
|
|
|
|
|
/// 所以只有真正需要判断「现在是谁在说」的模式才开: |
|
|
|
|
|
/// - faceToFace:两个人轮流说,方向靠检测结果定,**必须**双语; |
|
|
|
|
|
/// - simultaneous / audioVideo:单向,方向固定,开了纯亏延迟; |
|
|
|
|
|
/// 固定语种时原生上报的 detectedLanguage 就是 supportedLanguages[0] |
|
|
|
|
|
/// (见 AzureAsrHelper 的 onResult),[_determineLanguageCodes] 据此 |
|
|
|
|
|
/// 算出 shouldSwap=true,方向仍然正确。 |
|
|
|
|
|
/// |
|
|
|
|
|
/// ⚠️ 代价:同传下用户若改说目标语种,不会再被自动识别成反向。 |
|
|
|
|
|
/// 同传本来就是单向场景,需要反向请切面对面。 |
|
|
|
|
|
List<String> _asrLanguagesForCurrentMode() { |
|
|
|
|
|
if (currentMode.value == 'faceToFace') { |
|
|
|
|
|
return [sourceLanguageCode.value, targetLanguageCode.value]; |
|
|
|
|
|
} |
|
|
|
|
|
return [sourceLanguageCode.value]; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// onInit 里那次 [changeTranslationMode] 的 Future,见 onInit 的说明。 |
|
|
|
|
|
Future<void>? _modeInitFuture; |
|
|
|
|
|
|
|
|
|
|
|
// ---- 中间结果(说话过程中的预览译文)翻译的节流与取消,见 handleIntermediateResult ---- |
|
|
|
|
|
|
|
|
|
|
|
/// 中间结果翻译的防抖间隔。中间译文只上屏、不播报,值得为它省下请求。 |
|
|
|
|
|
static const Duration _interimTranslateDebounce = Duration(milliseconds: 700); |
|
|
|
|
|
|
|
|
|
|
|
/// 相比上次真正发出去的文本,至少要多这么多个字才值得再翻一次。 |
|
|
|
|
|
static const int _interimMinGrowth = 4; |
|
|
|
|
|
|
|
|
|
|
|
/// 在途的中间结果翻译请求,发新的之前撤掉它。 |
|
|
|
|
|
CancelToken? _interimTranslateToken; |
|
|
|
|
|
|
|
|
|
|
|
/// 上一次真正发出去翻译的中间文本,用于增量门槛判断。 |
|
|
|
|
|
String _lastInterimSentSource = ''; |
|
|
|
|
|
|
|
|
|
|
|
/// 翻译请求序号:回来时不是最新的一发就丢弃,避免旧结果盖掉新译文。 |
|
|
|
|
|
int _interimTranslateSeq = 0; |
|
|
|
|
|
|
|
|
/// 重新初始化ASR服务 |
|
|
/// 重新初始化ASR服务 |
|
|
/// [restartRecognition] 初始化完成后是否自动重新开始识别 |
|
|
/// [restartRecognition] 初始化完成后是否自动重新开始识别 |
|
|
Future<void> _reinitializeAsrService({bool restartRecognition = false}) async { |
|
|
Future<void> _reinitializeAsrService({bool restartRecognition = false}) async { |
|
|
@ -2661,16 +2798,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { |
|
|
} |
|
|
} |
|
|
isRecognizing.value = false; |
|
|
isRecognizing.value = false; |
|
|
|
|
|
|
|
|
late final List<String> asrSupportedLanguages; |
|
|
final List<String> asrSupportedLanguages = _asrLanguagesForCurrentMode(); |
|
|
|
|
|
|
|
|
if (currentMode.value == 'audioVideo') { |
|
|
|
|
|
asrSupportedLanguages = [sourceLanguageCode.value]; |
|
|
|
|
|
} else { |
|
|
|
|
|
asrSupportedLanguages = [ |
|
|
|
|
|
sourceLanguageCode.value, |
|
|
|
|
|
targetLanguageCode.value |
|
|
|
|
|
]; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (currentMode.value == "call") { |
|
|
if (currentMode.value == "call") { |
|
|
await _initializeCallModeTranslationService(); |
|
|
await _initializeCallModeTranslationService(); |
|
|
|