diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index fb258a9d8..b4f8ca27f 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -4,7 +4,6 @@ import '../../../core/utils/permission_util.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'dart:async'; -import 'dart:convert'; import 'package:get_storage/get_storage.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; @@ -24,44 +23,40 @@ import 'record_timer_controller.dart'; import 'translation_history_controller.dart'; import '../models/translation_models.dart'; -// 切换音频输出设备的枚举 +/// 音频输出设备枚举 enum AudioOutputDevice { defaultDevice, // 默认(如有耳机优先耳机,否则扬声器) headphones, // 强制耳机 speaker, // 强制扬声器 } +/// 翻译控制器 - 负责管理语音识别、翻译和TTS功能 class TranslationController extends GetxController with WidgetsBindingObserver { static const String TAG = 'TranslationController'; - // 服务 - + // ==================== 服务依赖 ==================== final AsrService _asrService = Get.find(); final VolcanoTranslationService _translationService = Get.find(); final TtsService _ttsService = Get.find(); - final AstService _astService = Get.find(); final LanguageManager _languageManager = Get.find(); final GetStorage _storage = GetStorage(); - // 蓝牙服务 final BluetoothManager bluetoothManager = Get.find(); final BleManager bleManager = Get.find(); - //音乐播放管理 final _musiceManager = Get.find(); - // 统计服务 late final TranslationUsageService _usageService; - // ASR统计相关 - 精确时长统计 + // ==================== ASR统计相关 ==================== DateTime? _asrSessionStartTime; // ASR会话开始时间 DateTime? _asrActiveStartTime; // ASR活跃开始时间(用户实际开始语音识别的时间) Duration _asrActiveDuration = Duration.zero; // ASR累计活跃时长 bool _isAsrActive = false; // ASR是否处于活跃状态 - // 音频输入源 - bool _audioSourceType = false; -//是否开启播放 - bool isPlayback = false; + // ==================== 配置相关 ==================== + bool _audioSourceType = false; // 音频输入源类型 + bool isPlayback = false; // 是否开启播放 + bool isContinuePlaying = false; // 是否继续播放 // 存储相关配置 static const String _sourceLanguageKey = 'translation_source_language'; @@ -71,59 +66,60 @@ class TranslationController extends GetxController with WidgetsBindingObserver { final pageSize = 20; // 每页记录数量 bool _scrollListenerSetup = false; // 滚动监听器设置标志 + // ==================== 状态变量 ==================== RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2 RxInt lastActiveSpeaker = 0.obs; // 上一次状态 - bool isContinuePlaying = false; // 是否继续播放 - // 状态变量 final isRecognizing = false.obs; final isTranslating = false.obs; final isTtsEnabled = true.obs; final isRecording = false.obs; final lasyIsRecording = false.obs; final hasRecordPermission = false.obs; - // 语言相关 + + // ==================== 语言相关 ==================== final sourceLanguage = '中文(简体)'.obs; final targetLanguage = '英语'.obs; final sourceLanguageCode = 'zh-CN'.obs; final targetLanguageCode = 'en-US'.obs; var detectedLanguageCode = ''; - + final detectedLanguageCode1 = ''.obs; + final latestRecognitionText = ''.obs; + final faceToFaceRecognitionText = ''.obs; + // ==================== 翻译相关 ==================== var currsessionid = ''; final currentModeTitle = 'simultaneousTranslation'.obs; final currentMode = 'simultaneous'.obs; - // 翻译历史和当前项 - - String? fTFTranslationResult = ''; + //String? fTFTranslationResult = ''; String? currentSessionId; final appDocDir = getApplicationDocumentsDirectory(); var appDir; var dir; - // 使用新的管理器 + // ==================== 管理器 ==================== late final RecordTimerController _timerManager; late final TranslationHistoryManager _historyManager; - // 提供访问器 + + // ==================== 访问器 ==================== RxString get recordDurationText => _timerManager.recordDurationText; RxList get translationHistory => _historyManager.translationHistory; ScrollController get scrollController => _historyManager.scrollController; - // 添加历史管理器状态访问器 RxBool get hasMoreHistory => _historyManager.hasMoreHistory; RxBool get isLoadingMore => _historyManager.isLoadingMore; RxBool get isLoadingMoreError => _historyManager.isLoadingMoreError; - // 其他 + Map get supportedLanguages => + _languageManager.getChineseNameToAsrCodeMap(); + + // ==================== 流订阅 ==================== StreamSubscription? _recognitionSubscription; StreamSubscription? _voiceInteractionSubscription; Timer? _translationDebounceTimer; - // 获取支持的语言列表 - Map get supportedLanguages => - _languageManager.getChineseNameToAsrCodeMap(); -// 最新的语音识别文本 - final latestRecognitionText = ''.obs; -// 检测到的语言代码 - final detectedLanguageCode1 = ''.obs; - //int num = 0; + // 底部控制栏高度 + double _bottomBarHeight = 0.0; + + /// 更改翻译模式 + /// [mode] 翻译模式:simultaneous(同声传译)、audioVideo(音视频)、call(通话)、faceToFace(面对面) Future changeTranslationMode(String mode) async { currentMode.value = mode; currentModeTitle.value = _getModeTitle(mode); @@ -131,11 +127,12 @@ class TranslationController extends GetxController with WidgetsBindingObserver { if (mode == 'call') { await _initializeCallModeTranslationService(); - } else if (mode == 'faceToFace') { - await _ttsService.setAudioOutputDevice(1); } } + /// 根据模式获取标题 + /// [mode] 翻译模式 + /// 返回对应的标题字符串 String _getModeTitle(String mode) { switch (mode) { case 'audioVideo': @@ -144,84 +141,75 @@ class TranslationController extends GetxController with WidgetsBindingObserver { return 'callTranslation'; case 'faceToFace': return 'faceToFaceTranslation'; - default: return 'simultaneousTranslation'; } } - // 更新计时器相关方法 + // ==================== 计时器相关方法 ==================== + /// 暂停录音计时器 void pauseRecordTimer() => _timerManager.pauseTimer(); + + /// 恢复录音计时器 void resumeRecordTimer() => _timerManager.resumeTimer(); + + /// 重置录音计时器 void resetRecordTimer() => _timerManager.resetTimer(); + /// 设置激活的说话者(支持按住说话模式) + /// [id] 说话者ID:0=无激活, 1=按钮1, 2=按钮2 Future setActiveSpeaker(int id) async { - activeSpeaker.value = id; - await _asrService.startContinuousRecognition(_audioSourceType); - - if (activeSpeaker.value != 0) { - await _handleActiveSpeakerOn(id); - } else { - await _handleActiveSpeakerOff(); - } + await _handleFaceToFaceHoldToTalk(id); } - // 拆分出的辅助方法 - Future _handleActiveSpeakerOn(int id) async { - isPlayback = false; - isRecognizing.value = true; - - if (isRecording.value && isRecognizing.value) { - await _handleRecordingStart(); - } - - _startAsrActiveTracking(); - lastActiveSpeaker.value = id; - } - - Future _handleRecordingStart() async { - if (lasyIsRecording.value != isRecording.value) { - lasyIsRecording.value = isRecording.value; - await startRecording(); + /// 处理面对面翻译的按住说话逻辑 + /// [id] 说话者ID + Future _handleFaceToFaceHoldToTalk(int id) async { + if (id == 0) { + // 松开按钮,停止当前激活的说话者 + if (activeSpeaker.value != 0) { + lastActiveSpeaker.value = activeSpeaker.value; // 记录上一次激活的说话者 + activeSpeaker.value = id; + await _stopFaceToFaceRecognition(); + } } else { - await _asrService.resumeRecord(); - resumeRecordTimer(); + // 按下按钮,只有在没有其他按钮激活时才能激活 + if (activeSpeaker.value == 0) { + activeSpeaker.value = id; + await _startFaceToFaceRecognition(id); + } } } - Future _handleActiveSpeakerOff() async { - _stopAsrActiveTracking(); - - if (isRecording.value && isRecognizing.value) { - pauseRecordTimer(); - await _asrService.pauseRecord(); + /// 开始面对面翻译的语音识别 + /// [speakerId] 说话者ID + Future _startFaceToFaceRecognition(int speakerId) async { + try { + await _asrService.startContinuousRecognition(_audioSourceType); + await continueRecording(); + Logger.info('面对面翻译:激活说话者 $speakerId'); + } catch (e) { + Logger.error('启动面对面翻译失败: ${e.toString()}'); + activeSpeaker.value = 0; } - - await _handleTranslationPlayback(); } - Future _handleTranslationPlayback() async { - if (fTFTranslationResult != '' && !isPlayback) { - isPlayback = true; - // await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); - // await Future.delayed(const Duration(milliseconds: 1300)); - await playTranslatedText(fTFTranslationResult!); - fTFTranslationResult = ''; - } else if ((translationHistory.isEmpty || - !translationHistory.last.isIntermediate) && - currentMode.value == 'faceToFace') { - _asrService.stopContinuousRecognition(); + /// 停止面对面翻译的语音识别 + Future _stopFaceToFaceRecognition() async { + try { + await pauseRecording(); //暂停 + await _asrService.stopContinuousRecognition(); + _stopAsrActiveTracking(); + isRecognizing.value = false; + currentSessionId = null; + + Logger.info('面对面翻译:停止语音识别'); + } catch (e) { + Logger.error('停止面对面翻译失败: ${e.toString()}'); } } - // 禁用蓝牙音频 - Future disableBluetoothAudio() async => - await _asrService.disableBluetoothAudio(); - //恢复原始音频状态 - Future restoreOriginalAudioState() async => - await _asrService.restoreOriginalAudioState(); - - // 兼容旧版视图的计算属性 + /// 获取当前翻译项(兼容旧版视图) Rx get currentItem { if (translationHistory.isNotEmpty && translationHistory.last.isIntermediate) { @@ -237,7 +225,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver { _timerManager = RecordTimerController(); _historyManager = TranslationHistoryManager(); - //_setupEventListeners(); // 预先检查权限,避免在用户交互时阻塞 _preCheckPermissions(); @@ -282,7 +269,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 初始化统计服务 + /// 初始化统计服务 void _initUsageService() { try { // 确保统计服务已注册 @@ -296,7 +283,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 加载保存的语言设置 + /// 加载保存的语言设置 void _loadSavedLanguages() { final savedSourceLanguage = _storage.read(_sourceLanguageKey); final savedTargetLanguage = _storage.read(_targetLanguageKey); @@ -312,12 +299,14 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 保存当前语言设置 + /// 保存当前语言设置 void _saveLanguageSettings() { _storage.write(_sourceLanguageKey, sourceLanguage.value); _storage.write(_targetLanguageKey, targetLanguage.value); } + /// 请求录音权限 + /// 返回是否授权成功 Future requestRecordPermission() async { try { hasRecordPermission.value = @@ -341,9 +330,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 请求存储权限 + /// 请求存储权限 + /// 返回是否授权成功 Future _requestStoragePermission() async { - // final BuildContext context = Get.overlayContext!; try { final bool granted = await PermissionUtil.instance.requestPermission( permissionType: Permission.storage, @@ -365,7 +354,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 暂停其他服务 + /// 暂停其他服务(如音乐播放) Future suspendOtherServices() async { try { var status = await _musiceManager.getStatus(); @@ -373,25 +362,25 @@ class TranslationController extends GetxController with WidgetsBindingObserver { if (isPlaying == true) { isContinuePlaying = true; - await _musiceManager.pause(); // 暂停音乐 + await _musiceManager.pause(); } else { isContinuePlaying = false; } } catch (e) { - Logger.error('初始化服务失败: ${e.toString()}'); + Logger.error('暂停其他服务失败: ${e.toString()}'); } } - // 恢复其他服务 + /// 恢复其他服务(如音乐播放) Future restoreOtherServices() async { try { - if (isContinuePlaying) await _musiceManager.resume(); // 暂停音乐 + if (isContinuePlaying) await _musiceManager.resume(); } catch (e) { - Logger.error('初始化服务失败: ${e.toString()}'); + Logger.error('恢复其他服务失败: ${e.toString()}'); } } - // 初始化服务 + /// 初始化所有服务 Future _initServices() async { try { await _translationService.initialize(); @@ -403,12 +392,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver { ]; Logger.info('初始化ASR服务,支持语言: $asrSupportedLanguages'); - await _asrService.initialize( - supportedLanguages: asrSupportedLanguages, - ); - // 获取识别事件流 - 这启动了异步识别过程 + await _asrService.initialize(supportedLanguages: asrSupportedLanguages); + + // 获取识别事件流 var recognitionStream = await _asrService.recognizeCallback(); - // 监听识别结果 _recognitionSubscription?.cancel(); _recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent); @@ -418,10 +405,11 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Logger.d("Permission", "未授予存储权限,无法提供录音功能"); return; } - appDir = await appDocDir; // 先 await 获取目录 + + appDir = await appDocDir; dir = Directory("${appDir.path}/TranslatAudio"); if (!await dir.exists()) { - await dir.create(recursive: true); // Add recursive creation + await dir.create(recursive: true); } Logger.info('所有服务初始化完成'); } catch (e) { @@ -429,21 +417,17 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 初始化通话模式的语音翻译服务 + /// 初始化通话模式的语音翻译服务 Future _initializeCallModeTranslationService() async { try { Logger.info('开始初始化通话模式语音翻译服务'); - // 为通话模式配置特殊的ASR设置 final List callModeLanguages = [ sourceLanguageCode.value, targetLanguageCode.value ]; - // 重新初始化ASR服务以支持通话音频源 await _astService.initialize(supportedLanguages: callModeLanguages); - Logger.info('通话模式语音翻译服务初始化完成'); - return; } catch (e) { Logger.error('通话模式语音翻译服务初始化失败: ${e.toString()}'); } @@ -459,6 +443,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { restoreOtherServices(); _asrService.dispose(); _astService.dispose(); + // 释放管理器资源 _timerManager.dispose(); _historyManager.dispose(); @@ -470,15 +455,13 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } /// 应用生命周期变化回调 - /// 当应用进入后台、失去焦点或分离(inactive/paused/detached)时, - /// 兜底将 activeSpeaker 重置为 0,等价于“松手”,避免 onPointerUp 未触发导致的卡住问题。 + /// 当应用进入后台、失去焦点或分离时,兜底将 activeSpeaker 重置为 0 @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.inactive || state == AppLifecycleState.paused || state == AppLifecycleState.detached) { if (activeSpeaker.value != 0) { - // 使用 microtask 避免阻塞当前生命周期回调 Future.microtask(() { setActiveSpeaker(0); }); @@ -486,25 +469,23 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 切换TTS功能 + /// 切换TTS功能开关 void toggleTts() => isTtsEnabled.toggle(); - // 切换录音功能 + /// 切换录音功能开关 Future toggleisRecord() async { isRecording.toggle(); if (isRecording.value && isRecognizing.value) { startRecording(); } else if (!isRecording.value) { stopRecording(); - } else {} + } } + /// 开始录音 Future startRecording() async { - //isRecording.value = true; _timerManager.startTimer(); - // 启动实际录音逻辑 - // ... final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); await _asrService.enableRecord( "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav"); @@ -512,18 +493,33 @@ class TranslationController extends GetxController with WidgetsBindingObserver { await _astService.enableRecord( "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav"); } - return; } + /// 停止录音 Future stopRecording() async { - // 停止计时器 _timerManager.stopTimer(); _asrService.stopRecord(true); lasyIsRecording.value = false; Logger.info('录音已停止,计时器已重置'); } - // 开始语音识别 + /// 暂停录音 + Future pauseRecording() async { + _timerManager.pauseTimer(); + _asrService.pauseRecord(); + lasyIsRecording.value = false; + Logger.info(' 录音已暂停'); + } + + /// 继续录音 + Future continueRecording() async { + _timerManager.resumeTimer(); + _asrService.resumeRecord(); + lasyIsRecording.value = true; + Logger.info(' 录音已继续'); + } + + /// 开始语音识别 Future startRecognition() async { Logger.info('开始语音识别'); @@ -541,7 +537,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 拆分出的辅助方法 + /// 检查权限 Future _checkPermissions() async { final bool record = await requestRecordPermission(); if (!record) { @@ -551,6 +547,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { return true; } + /// 初始化会话 Future _initializeSession() async { currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); @@ -564,6 +561,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } + /// 配置音频模式 Future _configureAudioMode() async { switch (currentMode.value) { case "audioVideo": @@ -577,6 +575,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } + /// 配置音视频模式 Future _configureAudioVideoMode() async { bleManager.openDecoder(); _audioSourceType = true; @@ -584,29 +583,28 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Logger.info('发送ble音乐或者通话远端声音'); } + /// 配置通话模式 Future _configureCallMode() async { bleManager.openA2DPDecoder(); _audioSourceType = true; isTtsEnabled.value = false; - Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); } + /// 配置默认模式 Future _configureDefaultMode() async { _audioSourceType = false; isTtsEnabled.value = true; } + /// 启动语音识别 Future _startAsrService() async { - // if (bluetoothManager.currentDeviceRx.value != null) { - // restoreOriginalAudioState(); - // } else { - // disableBluetoothAudio(); - // } - - await _asrService.startContinuousRecognition(_audioSourceType); + if (currentMode.value == 'faceToFace') { + await _asrService.startContinuousRecognition(_audioSourceType); + } } + /// 完成识别启动 Future _finalizeRecognitionStart() async { _startAsrActiveTracking(); isRecognizing.value = true; @@ -616,28 +614,39 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 处理识别事件 + /// 处理语音识别事件 + /// [event] 识别事件 void _handleRecognitionEvent(RecognitionEvent event) { if (event.type == RecognitionEventType.finalResult && event.text.isNotEmpty) { detectedLanguageCode = event.detectedLanguage; - print("处理识别事件:${event.text}"); - handleFinalResult(event.text); + print("处理识别完成:${event.text}"); + if (currentMode.value != 'faceToFace') { + handleFinalResult(event.text); + } else { + faceToFaceRecognitionText.value += event.text; + } + if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { + handleFinalResult(faceToFaceRecognitionText.value); + faceToFaceRecognitionText.value = ''; + } } else if (event.type == RecognitionEventType.intermediateResult && event.text.isNotEmpty) { if (event.detectedLanguage.isNotEmpty) { detectedLanguageCode = event.detectedLanguage; } - print("处理识别事件:${event.text}"); + print("处理识别中事件:${event.text}"); handleIntermediateResult(event.text); - } else if (event.type == RecognitionEventType.finalResult1 && - event.text.isNotEmpty) { - if (event.detectedLanguage.isNotEmpty) { - detectedLanguageCode1.value = event.detectedLanguage; - } - latestRecognitionText.value = event.text; - print("处理麦:${event.text}"); - } else if (event.type == RecognitionEventType.error) { + } + //else if (event.type == RecognitionEventType.finalResult1 && + // event.text.isNotEmpty) { + // if (event.detectedLanguage.isNotEmpty) { + // detectedLanguageCode1.value = event.detectedLanguage; + // } + // latestRecognitionText.value = event.text; + // print("处理麦:${event.text}"); + // } + else if (event.type == RecognitionEventType.error) { Get.snackbar('error'.tr, event.error); isRecognizing.value = false; Logger.error('语音识别错误: ${event.error}'); @@ -647,7 +656,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 停止语音识别 + /// 停止语音识别 Future stopRecognition() async { if (!isRecognizing.value) return; @@ -657,39 +666,20 @@ class TranslationController extends GetxController with WidgetsBindingObserver { if (currentMode.value == "call") { await _astService.stopContinuousTranslation(); } - // 停止ASR活跃时长计时 _stopAsrActiveTracking(); - isRecognizing.value = false; - - // 如果当前有中间结果,将其作为最终结果处理 - if (translationHistory.isNotEmpty && - translationHistory.last.isIntermediate && - translationHistory.last.sourceText.isNotEmpty) { - // 标记为最终结果 - translationHistory.last.isIntermediate = false; - translationHistory.refresh(); - - // 如果还没有翻译结果,尝试翻译 - if (translationHistory.last.translatedText.isEmpty) { - await translateText(translationHistory.last.sourceText, - translationHistory.last.timestamp, - isFinal: true); - } - - saveTranslationHistory(); - } - currentSessionId = null; } catch (e) { Logger.error('停止语音识别失败: ${e.toString()}'); } } - // 处理中间识别结果 + /// 处理中间识别结果 + /// [text] 识别的文本 void handleIntermediateResult(String text) { if (text.isEmpty) return; - + print( + "handleIntermediateResult:${translationHistory.isEmpty}${translationHistory.last.isIntermediate}"); if (translationHistory.isEmpty || !translationHistory.last.isIntermediate) { currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); @@ -712,7 +702,12 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } else { // 更新当前项 - translationHistory.last.sourceText = text; + if (currentMode.value != 'faceToFace') { + translationHistory.last.sourceText = text; + } else { + translationHistory.last.sourceText = + faceToFaceRecognitionText.value + text; + } translationHistory.refresh(); _historyManager.scrollToBottom(); } @@ -730,27 +725,26 @@ class TranslationController extends GetxController with WidgetsBindingObserver { }); } - // 处理最终识别结果 + /// 处理最终识别结果 + /// [text] 识别的文本 Future handleFinalResult(String text) async { if (text.isEmpty || translationHistory.isEmpty) return; - // ASR识别到最终结果,表示用户完成了一次有效的语音输入 - // 更新当前项 if (translationHistory.last.isIntermediate) { translationHistory.last.sourceText = text; translationHistory.last.isIntermediate = false; translationHistory.refresh(); - // 更新后滚动到底部 _scrollToBottom(); saveTranslationHistory(); +//如果识别中,且是面对面翻译,则停止识别 if (activeSpeaker.value == 0 && currentMode.value == 'faceToFace') { _asrService.stopContinuousRecognition(); } - // 使用项目的时间戳进行翻译 + translateText(text, translationHistory.last.timestamp, isFinal: true); } else { - // 如果最后一项不是中间结果,创建新项 + // 创建新项 final newItem = TranslationItem( sourceText: text, translatedText: '', @@ -763,20 +757,24 @@ class TranslationController extends GetxController with WidgetsBindingObserver { translationHistory.last.sessionId != currentSessionId, isIntermediate: false, ); + if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { _asrService.stopContinuousRecognition(); - } //else { + } + translationHistory.add(newItem); translationHistory.refresh(); _scrollToBottom(); saveTranslationHistory(); - // 使用新项目的时间戳进行翻译 translateText(text, newItem.timestamp, isFinal: true); - // } } } - // 翻译文本 + /// 翻译文本 + /// [sourceText] 源文本 + /// [timestamp] 时间戳 + /// [isFinal] 是否为最终结果 + /// 返回翻译结果 Future translateText(String sourceText, DateTime timestamp, {bool isFinal = false}) async { if (sourceText.isEmpty) return null; @@ -802,7 +800,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver { return translationResult; } - // 拆分出的辅助方法 + /// 确定语言代码 + /// 返回源语言和目标语言代码的映射 Map _determineLanguageCodes() { String detectedSourceLanguageCode; String detectedTargetLanguageCode; @@ -829,6 +828,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { }; } + /// 判断是否为面对面说话者1 bool _isFaceToFaceSpeaker1() { return (currentMode.value == 'faceToFace' && activeSpeaker.value == 1) || (currentMode.value == 'faceToFace' && @@ -836,6 +836,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver { lastActiveSpeaker.value == 1); } + /// 判断是否为面对面说话者2 bool _isFaceToFaceSpeaker2() { return (currentMode.value == 'faceToFace' && activeSpeaker.value == 2) || (currentMode.value == 'faceToFace' && @@ -843,6 +844,11 @@ class TranslationController extends GetxController with WidgetsBindingObserver { lastActiveSpeaker.value == 2); } + /// 执行翻译 + /// [sourceText] 源文本 + /// [sourceCode] 源语言代码 + /// [targetCode] 目标语言代码 + /// 返回翻译结果 Future _performTranslation( String sourceText, String sourceCode, String targetCode) async { return await _translationService.translateText( @@ -852,6 +858,12 @@ class TranslationController extends GetxController with WidgetsBindingObserver { ); } + /// 更新翻译历史 + /// [sourceText] 源文本 + /// [translationResult] 翻译结果 + /// [timestamp] 时间戳 + /// [languageCodes] 语言代码映射 + /// [isFinal] 是否为最终结果 Future _updateTranslationHistory( String sourceText, String translationResult, @@ -870,6 +882,12 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } + /// 更新现有历史项 + /// [index] 历史项索引 + /// [translationResult] 翻译结果 + /// [languageCodes] 语言代码映射 + /// [sourceText] 源文本 + /// [isFinal] 是否为最终结果 Future _updateExistingHistoryItem( int index, String translationResult, @@ -896,6 +914,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } + /// 记录翻译统计 + /// [sourceText] 源文本 + /// [translationResult] 翻译结果 + /// [languageCodes] 语言代码映射 Future _recordTranslationStats( String sourceText, String translationResult, @@ -925,28 +947,31 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } + /// 处理TTS播放 + /// [translationResult] 翻译结果 Future _handleTtsPlayback(String translationResult) async { - if (isTtsEnabled.value && currentMode.value != 'faceToFace') { - await playTranslatedText(translationResult); - } else { - await _handleFaceToFaceTts(translationResult); - } - } - - Future _handleFaceToFaceTts(String translationResult) async { - fTFTranslationResult = (fTFTranslationResult ?? '') + translationResult; - print("translationResult=$translationResult"); - - if (activeSpeaker.value == 0 && !isPlayback) { - isPlayback = true; - // await _ttsService.setAudioOutputDevice(lastActiveSpeaker.value); - // await Future.delayed(const Duration(milliseconds: 1300)); - await playTranslatedText(fTFTranslationResult!); - fTFTranslationResult = ''; - } + //if (isTtsEnabled.value && currentMode.value != 'faceToFace') { + await playTranslatedText(translationResult); + // } else { + // await _handleFaceToFaceTts(translationResult); + // } } - // 播放翻译文本 + // /// 处理面对面TTS + // /// [translationResult] 翻译结果 + // Future _handleFaceToFaceTts(String translationResult) async { + // fTFTranslationResult = (fTFTranslationResult ?? '') + translationResult; + // print("translationResult=$translationResult"); + + // if (activeSpeaker.value == 0 && !isPlayback) { + // isPlayback = true; + // await playTranslatedText(fTFTranslationResult!); + // fTFTranslationResult = ''; + // } + // } + + /// 播放翻译文本 + /// [text] 要播放的文本 Future playTranslatedText(String text) async { if (text.isEmpty) return; @@ -964,17 +989,16 @@ class TranslationController extends GetxController with WidgetsBindingObserver { await _ttsService.speakOnce(currsessionid, text); final endTime = DateTime.now(); - // 记录TTS API调用统计 - 优化版(基于文本字符数计费) + // 记录TTS API调用统计 try { final durationSeconds = endTime.difference(startTime).inSeconds; final languageName = _languageManager .getChineseNameByAsrCode(detectedTargetLanguageCode) ?? '未知'; - // TTS的主要计费依据是文本字符数,播放时长仅用于分析 _usageService.recordTtsApiCall( - text: text, // 文本字符数是TTS计费的主要依据 - durationSeconds: durationSeconds, // 播放时长用于用户体验分析 + text: text, + durationSeconds: durationSeconds, language: languageName, ); @@ -988,18 +1012,17 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 停止所有操作 + /// 停止所有操作 Future stopAll() async { try { if (currentMode.value == "audioVideo" || currentMode.value == "call") { bleManager.closeCodec(); - // 发送ble音乐或者通话远端声音 - Logger.info('发送ble音乐或者通话远端声音'); + Logger.info('关闭ble编解码器'); } if (isRecognizing.value) { await stopRecognition(); } - // 确保停止ASR活跃时长跟踪 + _stopAsrActiveTracking(); _translationDebounceTimer?.cancel(); await _ttsService.stop(); @@ -1008,7 +1031,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 播放特定翻译项 + /// 播放特定翻译项 + /// [item] 翻译项 Future playTranslationItem(TranslationItem item) async { if (item.translatedText.isEmpty) return; @@ -1023,7 +1047,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 播放文本 + /// 播放文本 + /// [text] 要播放的文本 + /// [languageCode] 语言代码(可选) Future playText(String text, {String? languageCode}) async { if (text.isEmpty) return; @@ -1042,24 +1068,21 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 滚动到底部(适配reverse模式) + /// 滚动到底部(适配reverse模式) + /// [animate] 是否使用动画 void _scrollToBottom({bool animate = true}) { if (scrollController.hasClients && (translationHistory.isNotEmpty || currentSessionId != null)) { try { - // 🔑 reverse模式下:滚动到顶部(position = 0)来显示最新消息 - // 正常模式下:滚动到底部(maxScrollExtent)来显示最新消息 const targetPosition = 0.0; // reverse模式下最新消息在顶部 if (animate) { - // 有动画的滚动,用于新消息添加时 scrollController.animateTo( targetPosition, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } else { - // 无动画的跳转,用于初始化时 scrollController.jumpTo(targetPosition); } } catch (e) { @@ -1068,23 +1091,21 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - // 设置滚动监听器(简化版,不再需要实际监听) + /// 设置滚动监听器(简化版,保留接口兼容性) void setupScrollListener() { // 简化后的空方法,保留接口兼容性 } - // 底部控制栏高度 - double _bottomBarHeight = 0.0; - - // 设置底部控制栏高度 + /// 设置底部控制栏高度 + /// [height] 高度值 void setBottomBarHeight(double height) { _bottomBarHeight = height; } - // 获取底部控制栏高度 + /// 获取底部控制栏高度 double get bottomBarHeight => _bottomBarHeight; - // 切换源语言和目标语言 + /// 切换源语言和目标语言 void swapLanguages() { stopAll(); @@ -1096,14 +1117,12 @@ class TranslationController extends GetxController with WidgetsBindingObserver { sourceLanguageCode.value = targetLanguageCode.value; targetLanguageCode.value = tempCode; - // 保存语言设置 _saveLanguageSettings(); - - // 更新ASR支持的语言并重新初始化 _reinitializeAsrService(); } - // 设置源语言 + /// 设置源语言 + /// [language] 语言名称 void setSourceLanguage(String language) { if (sourceLanguage.value != language) { stopAll(); @@ -1115,15 +1134,13 @@ class TranslationController extends GetxController with WidgetsBindingObserver { sourceLanguageCode.value = asrCode; } - // 保存语言设置 _saveLanguageSettings(); - - // 更新ASR支持的语言并重新初始化 _reinitializeAsrService(); } } - // 设置目标语言 + /// 设置目标语言 + /// [language] 语言名称 void setTargetLanguage(String language) { if (targetLanguage.value != language) { stopAll(); @@ -1135,21 +1152,16 @@ class TranslationController extends GetxController with WidgetsBindingObserver { targetLanguageCode.value = asrCode; } - // 保存语言设置 _saveLanguageSettings(); - - // 更新ASR支持的语言并重新初始化 _reinitializeAsrService(); } } - // 重新初始化ASR服务以支持新的语言 + /// 重新初始化ASR服务以支持新的语言 Future _reinitializeAsrService() async { try { - // 停止当前识别 await stopRecognition(); - // 更新ASR支持的语言 final List asrSupportedLanguages = [ sourceLanguageCode.value, targetLanguageCode.value @@ -1157,21 +1169,14 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Logger.info('重新初始化ASR服务,支持语言: $asrSupportedLanguages'); - // 重新初始化ASR服务 - await _asrService.initialize( - supportedLanguages: asrSupportedLanguages, - ); + await _asrService.initialize(supportedLanguages: asrSupportedLanguages); - // 获取识别事件流 - 这启动了异步识别过程 var recognitionStream = await _asrService.recognizeCallback(); - // 监听识别结果 _recognitionSubscription?.cancel(); _recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent); - //判断是否进入通话 if (currentMode.value == "call") { - // 重新初始化ASR服务以支持通话音频源 _initializeCallModeTranslationService(); } Logger.info('ASR服务重新初始化成功'); @@ -1180,21 +1185,20 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } + // ==================== 历史记录相关方法 ==================== + /// 加载翻译历史 void loadTranslationHistory() => _historyManager.loadTranslationHistory(); - // 更新历史记录相关方法 + /// 保存翻译历史 void saveTranslationHistory() => _historyManager.saveTranslationHistory(); - // 设置滚动监听器(适配reverse模式) + /// 设置滚动监听器(适配reverse模式) void _setupScrollListener() { - if (_scrollListenerSetup) return; // 防止重复设置 + if (_scrollListenerSetup) return; scrollController.addListener(() { - // 🔑 reverse模式下:检查是否滚动到底部(实际是历史记录的顶部) - // 参考Agent模块的实现 if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100) { - // 当距离底部100像素时触发加载更多历史记录 if (hasMoreHistory.value && !isLoadingMore.value) { Logger.d(TAG, '触发加载更多翻译历史记录(reverse模式)'); loadMoreHistory(); @@ -1205,18 +1209,33 @@ class TranslationController extends GetxController with WidgetsBindingObserver { _scrollListenerSetup = true; } + /// 清空翻译历史 void clearTranslationHistory() => _historyManager.clearTranslationHistory(); + /// 删除会话历史 + /// [sessionId] 会话ID void deleteSessionHistory(String sessionId) => _historyManager.deleteSessionHistory(sessionId); + /// 删除翻译项 + /// [sessionId] 会话ID + /// [timestamp] 时间戳 void deleteTranslationItem(String sessionId, DateTime timestamp) => _historyManager.deleteTranslationItem(sessionId, timestamp); + + /// 获取所有历史记录 + /// 返回所有翻译项列表 List getAllHistory() => _historyManager.getAllHistory(); + + /// 加载更多历史记录 Future loadMoreHistory() => _historyManager.loadMoreHistory(); + + /// 滚动到底部 + /// [animate] 是否使用动画 void scrollToBottom({bool animate = true}) => _historyManager.scrollToBottom(animate: animate); + // ==================== ASR活跃时长跟踪 ==================== /// 开始ASR活跃时长跟踪 void _startAsrActiveTracking() { if (!_isAsrActive) { @@ -1238,11 +1257,11 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } /// 记录ASR使用统计 - 基于精确时长跟踪 + /// [language] 语言名称 void _recordAsrUsageStats(String language) { try { if (_asrSessionStartTime == null) return; - // 确保停止当前的活跃时长跟踪 _stopAsrActiveTracking(); final now = DateTime.now(); diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 6796213ba..fed9c1558 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:ui'; import 'dart:io'; @@ -538,57 +537,6 @@ class TranslationView extends GetView { ); } -// // 按住说话按钮组件 -// Widget _buildHoldToTalkButton({ -// required bool isDarkMode, -// required int speakerId, -// required int activeSpeaker, -// required IconData icon, -// required String label, -// }) { -// bool isActive = activeSpeaker == speakerId; - -// return Listener( -// onPointerDown: (_) { -// if (activeSpeaker == 0) { -// // 没有激活的按钮时才能激活 - -// if (speakerId == 1) { -// //耳机收音 -// controller.restoreOriginalAudioState(); - -// //controller.setActiveSpeaker(speakerId); -// } else { -// controller.disableBluetoothAudio(); -// } -// controller.setActiveSpeaker(speakerId); -// //controller.startRecognition(); -// } -// }, -// onPointerUp: (_) { -// if (isActive) { -// //controller.stopRecognition(); -// controller.setActiveSpeaker(0); -// } -// }, -// child: Container( -// width: 60.w, -// height: 60.w, -// decoration: BoxDecoration( -// color: isActive -// ? (isDarkMode ? Colors.green[700] : Colors.green) // 激活状态颜色 -// : (isDarkMode ? AppColors.primary : Colors.blue), -// shape: BoxShape.circle, -// ), -// child: Icon( -// Icons.mic, // 麦克风图标 -// color: isActive ? Colors.white : Colors.white.withOpacity(0.7), -// size: 30.sp, -// ), -// ), -// ); -// } - // 按住说话按钮组件 Widget _buildHoldToTalkButton({ required bool isDarkMode, @@ -599,22 +547,25 @@ class TranslationView extends GetView { }) { bool isActive = activeSpeaker == speakerId; bool isDisabled = activeSpeaker != 0 && activeSpeaker != speakerId; - Timer? _holdTimer; // 增加延迟触发的计时器 + return Listener( onPointerDown: (_) async { - // 同步检查权限状态,不使用 await + // 检查权限 if (!controller.hasRecordPermission.value) { Get.snackbar('权限提示', '需要麦克风权限才能使用录音功能'); return; } - controller.setActiveSpeaker(speakerId); - }, - onPointerUp: (_) { - controller.setActiveSpeaker(0); + // 只有在没有其他按钮激活时才能激活当前按钮 + if (activeSpeaker == 0) { + await controller.setActiveSpeaker(speakerId); + } }, - onPointerCancel: (_) { - controller.setActiveSpeaker(0); + onPointerUp: (_) async { + // 松开按钮时,如果当前按钮是激活状态,则停止 + if (isActive) { + await controller.setActiveSpeaker(0); + } }, child: AnimatedContainer( duration: const Duration(milliseconds: 200), @@ -643,7 +594,7 @@ class TranslationView extends GetView { Stack( alignment: Alignment.center, children: [ - // 设备类型图标(叠加在麦克风上) + // 设备类型图标 Icon( icon, color: isActive