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.
1762 lines
56 KiB
1762 lines
56 KiB
import 'dart:io';
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:get_storage/get_storage.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:native_plugin/native_plugin.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:device_info_plus/device_info_plus.dart';
|
|
import 'package:pip/pip.dart';
|
|
import '../../../data/services/ast_service.dart';
|
|
import '../../../data/services/bluetooth_manager.dart';
|
|
import '../../../data/services/music_manager.dart';
|
|
import '../../../data/services/translation_usage_service.dart';
|
|
import '../../../data/services/volcano_translation_service.dart';
|
|
import '../../../data/services/tts_service.dart';
|
|
import '../../../data/services/language_manager.dart';
|
|
import '../../../core/utils/logger.dart';
|
|
import '../../../core/utils/permission_util.dart';
|
|
import '../../../core/services/floating_window_manager.dart';
|
|
|
|
import '../../../data/services/asr_service.dart';
|
|
import '../../../data/services/usage_stats_service.dart';
|
|
import '../../../data/services/ble_manager.dart';
|
|
import 'record_timer_controller.dart';
|
|
import 'translation_history_controller.dart';
|
|
import '../models/translation_models.dart';
|
|
import '../views/dialogs/translation_mode_selector.dart';
|
|
import '../../../data/models/user_Info.dart';
|
|
import '../../common/widgets/recharge_dialog.dart';
|
|
|
|
/// 音频输出设备枚举
|
|
enum AudioOutputDevice {
|
|
defaultDevice, // 默认(如有耳机优先耳机,否则扬声器)
|
|
headphones, // 强制耳机
|
|
speaker, // 强制扬声器
|
|
}
|
|
|
|
/// 翻译主控制器 - 负责管理语音识别、翻译和TTS功能(主控制器)
|
|
class TranslationController extends GetxController with WidgetsBindingObserver {
|
|
static const String TAG = 'TranslationController';
|
|
|
|
// ==================== 服务依赖 ====================
|
|
final AsrService _asrService = Get.find<AsrService>();
|
|
final VolcanoTranslationService _translationService =
|
|
Get.find<VolcanoTranslationService>();
|
|
final TtsService _ttsService = Get.find<TtsService>();
|
|
final AstService _astService = Get.find<AstService>();
|
|
final LanguageManager _languageManager = Get.find<LanguageManager>();
|
|
final GetStorage _storage = GetStorage();
|
|
final BluetoothManager bluetoothManager = Get.find<BluetoothManager>();
|
|
final BleManager bleManager = Get.find<BleManager>();
|
|
final _musiceManager = Get.find<MusicManager>();
|
|
late final UsageStatsService _usageService;
|
|
// 安卓悬浮窗管理器
|
|
late final FloatingWindowManager _floatingWindowManager;
|
|
// 苹果悬浮窗管理器
|
|
late final Pip _pip;
|
|
late final NativePlugin _nativePlugin;
|
|
int _pipContentView = 0;
|
|
|
|
// ==================== 悬浮窗相关 ====================
|
|
final isFloatingWindowEnabled = false.obs; // 悬浮窗状态
|
|
|
|
// ==================== ASR统计相关 ====================
|
|
DateTime? _asrSessionStartTime; // ASR会话开始时间
|
|
DateTime? _asrActiveStartTime; // ASR活跃开始时间(用户实际开始语音识别的时间)
|
|
Duration _asrActiveDuration = Duration.zero; // ASR累计活跃时长
|
|
bool _isAsrActive = false; // ASR是否处于活跃状态
|
|
|
|
// ==================== 配置相关 ====================
|
|
bool _audioSourceType = false; // 音频输入源类型
|
|
bool isPlayback = false; // 是否开启播放
|
|
bool isContinuePlaying = false; // 是否继续播放
|
|
|
|
// 存储相关配置
|
|
static const String _sourceLanguageKey = 'translation_source_language';
|
|
static const String _targetLanguageKey = 'translation_target_language';
|
|
// 添加悬浮按钮位置存储键
|
|
static const String _floatingButtonPositionKey = 'floating_button_position';
|
|
|
|
// 分页加载相关状态
|
|
final pageSize = 20; // 每页记录数量
|
|
bool _scrollListenerSetup = false; // 滚动监听器设置标志
|
|
|
|
// ==================== 状态变量 ====================
|
|
RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2
|
|
RxInt lastActiveSpeaker = 0.obs; // 上一次状态
|
|
final isRecognizing = false.obs;
|
|
final isPreparing = false.obs;
|
|
final isTranslating = false.obs;
|
|
final isTtsEnabled = true.obs;
|
|
final isRecording = false.obs;
|
|
final isCreateRecord = false.obs;
|
|
final hasRecordPermission = false.obs;
|
|
|
|
// 添加悬浮按钮位置状态
|
|
final floatingButtonPosition = Rx<Offset>(Offset(2.0, 100.0)); // 默认位置
|
|
|
|
// ==================== 语言相关 ====================
|
|
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? currentSessionId;
|
|
final appDocDir = getApplicationDocumentsDirectory();
|
|
var appDir;
|
|
var dir;
|
|
|
|
// ==================== 管理器 ====================
|
|
late final RecordTimerController _timerManager;
|
|
late final TranslationHistoryManager _historyManager;
|
|
|
|
// ==================== 访问器 ====================
|
|
RxString get recordDurationText => _timerManager.recordDurationText;
|
|
RxList<TranslationItem> 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<String, String> get supportedLanguages =>
|
|
_languageManager.getChineseNameToAsrCodeMap();
|
|
|
|
// ==================== 流订阅 ====================
|
|
StreamSubscription? _recognitionSubscription;
|
|
StreamSubscription? _voiceInteractionSubscription;
|
|
Timer? _translationDebounceTimer;
|
|
|
|
// 播放防重复机制
|
|
String? _lastPlayedItemKey; // 最后播放的项目标识
|
|
int _lastPlayTime = 0; // 最后播放时间
|
|
static const int _playDebounceInterval = 500; // 防抖间隔:500毫秒
|
|
|
|
// 底部控制栏高度
|
|
double _bottomBarHeight = 0.0;
|
|
|
|
/// 初始化安卓悬浮窗管理器
|
|
void _initFloatingWindowManager() {
|
|
try {
|
|
if (!Get.isRegistered<FloatingWindowManager>()) {
|
|
Get.put(FloatingWindowManager(), permanent: true);
|
|
}
|
|
_floatingWindowManager = Get.find<FloatingWindowManager>();
|
|
Logger.info('悬浮窗管理器初始化完成');
|
|
} catch (e) {
|
|
Logger.error('悬浮窗管理器初始化失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 初始化苹果悬浮窗管理器
|
|
Future<void> initPlatformState() async {
|
|
_pip = Pip();
|
|
_nativePlugin = NativePlugin();
|
|
try {
|
|
isFloatingWindowEnabled.value = await _pip.isActived();
|
|
await _pip.registerStateChangedObserver(
|
|
PipStateChangedObserver(
|
|
onPipStateChanged: (state, error) {
|
|
isFloatingWindowEnabled.value = state == PipState.pipStateStarted;
|
|
|
|
if (state == PipState.pipStateFailed) {
|
|
_pip.dispose();
|
|
}
|
|
},
|
|
),
|
|
);
|
|
} on PlatformException {
|
|
isFloatingWindowEnabled.value = false;
|
|
}
|
|
}
|
|
|
|
/// 切换悬浮窗功能
|
|
Future<void> toggleFloatingWindow() async {
|
|
if (isFloatingWindowEnabled.value) {
|
|
if (Platform.isAndroid) {
|
|
await _disableFloatingWindow();
|
|
} else if (Platform.isIOS) {
|
|
await _pip.dispose();
|
|
}
|
|
} else {
|
|
if (Platform.isAndroid) {
|
|
await _enableFloatingWindow();
|
|
} else if (Platform.isIOS) {
|
|
await _startPip();
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _startPip() async {
|
|
if (_pipContentView == 0) {
|
|
_pipContentView = await _nativePlugin.createPipContentView();
|
|
}
|
|
final options = PipOptions(
|
|
autoEnterEnabled: true,
|
|
aspectRatioX: 375,
|
|
aspectRatioY: 400,
|
|
sourceRectHintLeft: 0,
|
|
sourceRectHintTop: 0,
|
|
sourceRectHintRight: 0,
|
|
sourceRectHintBottom: 0,
|
|
seamlessResizeEnabled: true,
|
|
useExternalStateMonitor: true,
|
|
externalStateMonitorInterval: 100,
|
|
contentView: _pipContentView,
|
|
preferredContentWidth: 375,
|
|
preferredContentHeight: 400,
|
|
controlStyle: 2,
|
|
);
|
|
|
|
await _pip.setup(options);
|
|
await _pip.start();
|
|
}
|
|
|
|
/// 启用悬浮窗功能
|
|
Future<void> _enableFloatingWindow() async {
|
|
// 检查是否在支持悬浮窗的模式
|
|
if (!_isFloatingWindowSupportedMode()) {
|
|
// Get.snackbar(
|
|
// 'unsupportedMode'.tr,
|
|
// '悬浮窗仅支持通话和音视频翻译模式',
|
|
// snackPosition: SnackPosition.TOP,
|
|
// );
|
|
return;
|
|
}
|
|
|
|
final bool success = await _floatingWindowManager.enableFloatingWindow();
|
|
if (success) {
|
|
isFloatingWindowEnabled.value = true;
|
|
await _floatingWindowManager.showFloatingWindow();
|
|
|
|
// 如果当前有翻译内容,立即更新到悬浮窗
|
|
if (translationHistory.isNotEmpty) {
|
|
final latestItem = translationHistory.last;
|
|
await _updateFloatingWindowWithTranslation(latestItem);
|
|
}
|
|
|
|
// Get.snackbar(
|
|
// '悬浮窗已启用',
|
|
// '翻译结果将在悬浮窗中显示',
|
|
// snackPosition: SnackPosition.TOP,
|
|
// );
|
|
}
|
|
}
|
|
|
|
/// 禁用悬浮窗功能
|
|
Future<void> _disableFloatingWindow() async {
|
|
await _floatingWindowManager.disableFloatingWindow();
|
|
isFloatingWindowEnabled.value = false;
|
|
|
|
// Get.snackbar(
|
|
// '悬浮窗已禁用',
|
|
// '翻译结果将在应用内显示',
|
|
// snackPosition: SnackPosition.TOP,
|
|
// );
|
|
}
|
|
|
|
/// 检查当前模式是否支持悬浮窗
|
|
bool _isFloatingWindowSupportedMode() {
|
|
return currentMode.value == 'call' || currentMode.value == 'audioVideo';
|
|
}
|
|
|
|
/// 更新悬浮窗翻译内容
|
|
Future<void> _updateFloatingWindowWithTranslation(
|
|
TranslationItem item) async {
|
|
if (!isFloatingWindowEnabled.value) return;
|
|
|
|
final sourceLanguageName =
|
|
_languageManager.getChineseNameByAsrCode(item.sourceLanguageCode) ??
|
|
'unknown';
|
|
final targetLanguageName =
|
|
_languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ??
|
|
'unknown';
|
|
|
|
if (Platform.isAndroid) {
|
|
final data = FloatingWindowData(
|
|
sourceText: item.sourceText,
|
|
translatedText: item.translatedText,
|
|
sourceLanguage: sourceLanguageName,
|
|
targetLanguage: targetLanguageName,
|
|
timestamp: item.timestamp,
|
|
isIntermediate: item.isIntermediate,
|
|
);
|
|
await _floatingWindowManager.updateFloatingWindowContent(data);
|
|
} else if (Platform.isIOS) {
|
|
await _nativePlugin.updatePipContentView({
|
|
'viewId': _pipContentView,
|
|
'cn': item.sourceText,
|
|
'en': item.translatedText,
|
|
'lang': '$sourceLanguageName -> $targetLanguageName',
|
|
});
|
|
}
|
|
}
|
|
|
|
/// 更改翻译模式
|
|
/// [mode] 翻译模式:simultaneous(同声传译)、audioVideo(音视频)、call(通话)、faceToFace(面对面)
|
|
Future<void> changeTranslationMode(String mode) async {
|
|
currentMode.value = mode;
|
|
currentModeTitle.value = _getModeTitle(mode);
|
|
_historyManager.setMode(mode);
|
|
loadTranslationHistory();
|
|
//stopAll();
|
|
|
|
// 检查悬浮窗支持情况
|
|
if (isFloatingWindowEnabled.value && !_isFloatingWindowSupportedMode()) {
|
|
// 当前模式不支持悬浮窗,自动禁用
|
|
await _disableFloatingWindow();
|
|
// Get.snackbar(
|
|
// '模式已切换',
|
|
// '悬浮窗仅支持通话和音视频翻译模式',
|
|
// snackPosition: SnackPosition.TOP,
|
|
// );
|
|
} else if (!isFloatingWindowEnabled.value &&
|
|
_isFloatingWindowSupportedMode()) {
|
|
// 切换到支持悬浮窗的模式,提示用户可以启用
|
|
// Get.snackbar(
|
|
// '模式已切换',
|
|
// '可以启用悬浮窗显示翻译结果',
|
|
// snackPosition: SnackPosition.TOP,
|
|
// duration: const Duration(seconds: 4),
|
|
// );
|
|
}
|
|
|
|
if (mode == 'call') {
|
|
await _initializeCallModeTranslationService();
|
|
} else {
|
|
_reinitializeAsrService();
|
|
}
|
|
}
|
|
|
|
/// 根据模式获取标题
|
|
/// [mode] 翻译模式
|
|
/// 返回对应的标题字符串
|
|
String _getModeTitle(String mode) {
|
|
switch (mode) {
|
|
case 'audioVideo':
|
|
return 'audioVideoTranslation';
|
|
case 'call':
|
|
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<void> setActiveSpeaker(int id) async {
|
|
await _handleFaceToFaceHoldToTalk(id);
|
|
}
|
|
|
|
/// 处理面对面翻译的按住说话逻辑
|
|
/// [id] 说话者ID
|
|
Future<void> _handleFaceToFaceHoldToTalk(int id) async {
|
|
if (id == 0) {
|
|
// 松开按钮,停止当前激活的说话者
|
|
if (activeSpeaker.value != 0) {
|
|
lastActiveSpeaker.value = activeSpeaker.value; // 记录上一次激活的说话者
|
|
activeSpeaker.value = id;
|
|
await _stopFaceToFaceRecognition();
|
|
}
|
|
} else {
|
|
// 按下按钮,只有在没有其他按钮激活时才能激活
|
|
if (activeSpeaker.value == 0) {
|
|
activeSpeaker.value = id;
|
|
await _startFaceToFaceRecognition(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 开始面对面翻译的语音识别
|
|
/// [speakerId] 说话者ID
|
|
Future<void> _startFaceToFaceRecognition(int speakerId) async {
|
|
try {
|
|
if (User.isLoggedIn() && User.instance.tradeintegral <= 0) {
|
|
showRechargeDialog('translation');
|
|
activeSpeaker.value = 0;
|
|
return;
|
|
}
|
|
if (_asrSessionStartTime == null) {
|
|
await _initializeSession();
|
|
}
|
|
await _asrService.startContinuousRecognition(
|
|
_audioSourceType,
|
|
isRemoveFirstPunctuation: false,
|
|
); //开启识别
|
|
_startAsrActiveTracking(); //开启识别活动跟踪
|
|
if (isRecording.value) {
|
|
if (isCreateRecord.value) {
|
|
//已经创建文件
|
|
await continueRecording();
|
|
} else {
|
|
await startRecording();
|
|
}
|
|
}
|
|
Logger.info('面对面翻译:激活说话者 $speakerId');
|
|
} catch (e) {
|
|
Logger.error('启动面对面翻译失败: ${e.toString()}');
|
|
activeSpeaker.value = 0;
|
|
}
|
|
}
|
|
|
|
/// 停止面对面翻译的语音识别
|
|
Future<void> _stopFaceToFaceRecognition() async {
|
|
try {
|
|
if (isRecording.value) {
|
|
await pauseRecording(); //暂停
|
|
}
|
|
if (faceToFaceRecognitionText.value.isNotEmpty) {
|
|
handleFinalResult(faceToFaceRecognitionText.value);
|
|
faceToFaceRecognitionText.value = '';
|
|
}
|
|
await _asrService.stopContinuousRecognition();
|
|
_stopAsrActiveTracking(); //停止识别活动跟踪
|
|
_recordAsrUsageStats(sourceLanguage.value);
|
|
isRecognizing.value = false;
|
|
currentSessionId = null;
|
|
|
|
Logger.info('面对面翻译:停止语音识别');
|
|
} catch (e) {
|
|
Logger.error('停止面对面翻译失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
/// 获取当前翻译项(兼容旧版视图)
|
|
Rx<TranslationItem?> get currentItem {
|
|
if (translationHistory.isNotEmpty &&
|
|
translationHistory.last.isIntermediate) {
|
|
return Rx<TranslationItem?>(translationHistory.last);
|
|
}
|
|
return Rx<TranslationItem?>(null);
|
|
}
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
// 初始化管理器
|
|
_timerManager = RecordTimerController();
|
|
_historyManager = TranslationHistoryManager();
|
|
|
|
if (Platform.isAndroid) {
|
|
// 初始化安卓悬浮窗管理器
|
|
_initFloatingWindowManager();
|
|
} else if (Platform.isIOS) {
|
|
// 初始化苹果悬浮窗管理器
|
|
initPlatformState();
|
|
}
|
|
|
|
// 预先检查权限,避免在用户交互时阻塞
|
|
_preCheckPermissions();
|
|
|
|
// 初始化统计服务
|
|
_initUsageService();
|
|
|
|
// 加载上次保存的语言设置
|
|
_loadSavedLanguages();
|
|
|
|
// 初始化语言代码
|
|
final sourceAsrCode =
|
|
_languageManager.getAsrCodeByChineseName(sourceLanguage.value);
|
|
if (sourceAsrCode != null) {
|
|
sourceLanguageCode.value = sourceAsrCode;
|
|
}
|
|
|
|
final targetAsrCode =
|
|
_languageManager.getAsrCodeByChineseName(targetLanguage.value);
|
|
if (targetAsrCode != null) {
|
|
targetLanguageCode.value = targetAsrCode;
|
|
}
|
|
|
|
// 初始化翻译模式(用于历史隔离的初始加载)
|
|
final initMode = Get.arguments?['translationId'] ?? 'simultaneous';
|
|
_historyManager.setMode(initMode);
|
|
|
|
// 按初始模式加载历史
|
|
loadTranslationHistory();
|
|
|
|
// 初始化服务
|
|
_initServices();
|
|
|
|
// 初始化历史管理器
|
|
_historyManager.initialize();
|
|
|
|
// 应用模式逻辑(含ASR/TTS配置等)
|
|
changeTranslationMode(initMode);
|
|
|
|
// 监听应用生命周期:当应用切到后台/失焦时兜底重置 activeSpeaker
|
|
WidgetsBinding.instance.addObserver(this);
|
|
}
|
|
|
|
// @override
|
|
// void onReady() {
|
|
// super.onReady();
|
|
// // 初始化翻译模式,根据跳转参数
|
|
// final mode = Get.arguments?['translationId'] ?? 'simultaneous';
|
|
// changeTranslationMode(mode);
|
|
// }
|
|
|
|
/// 预先检查权限
|
|
Future<void> _preCheckPermissions() async {
|
|
try {
|
|
await requestRecordPermission();
|
|
} catch (e) {
|
|
Logger.e("Permission", "预检查权限失败: $e");
|
|
}
|
|
}
|
|
|
|
/// 初始化用量统计服务
|
|
void _initUsageService() {
|
|
try {
|
|
// 确保统计服务已注册
|
|
if (!Get.isRegistered<UsageStatsService>()) {
|
|
Get.put(UsageStatsService(), permanent: true);
|
|
}
|
|
_usageService = Get.find<UsageStatsService>();
|
|
Logger.info('使用量统计服务初始化完成');
|
|
} catch (e) {
|
|
Logger.error('使用量统计服务初始化失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 加载保存的语言设置
|
|
void _loadSavedLanguages() {
|
|
final savedSourceLanguage = _storage.read(_sourceLanguageKey);
|
|
final savedTargetLanguage = _storage.read(_targetLanguageKey);
|
|
|
|
if (savedSourceLanguage != null &&
|
|
supportedLanguages.containsKey(savedSourceLanguage)) {
|
|
sourceLanguage.value = savedSourceLanguage;
|
|
}
|
|
|
|
if (savedTargetLanguage != null &&
|
|
supportedLanguages.containsKey(savedTargetLanguage)) {
|
|
targetLanguage.value = savedTargetLanguage;
|
|
}
|
|
|
|
// 加载悬浮按钮位置
|
|
_loadFloatingButtonPosition();
|
|
}
|
|
|
|
/// 加载悬浮按钮位置
|
|
void _loadFloatingButtonPosition() {
|
|
final savedPosition = _storage.read(_floatingButtonPositionKey);
|
|
if (savedPosition != null && savedPosition is Map) {
|
|
final dx = savedPosition['dx']?.toDouble() ?? 2.0;
|
|
final dy = savedPosition['dy']?.toDouble() ?? 100.0;
|
|
floatingButtonPosition.value = Offset(dx, dy);
|
|
}
|
|
}
|
|
|
|
/// 保存悬浮按钮位置
|
|
void saveFloatingButtonPosition(Offset position) {
|
|
floatingButtonPosition.value = position;
|
|
_storage.write(_floatingButtonPositionKey, {
|
|
'dx': position.dx,
|
|
'dy': position.dy,
|
|
});
|
|
}
|
|
|
|
/// 保存当前语言设置
|
|
void _saveLanguageSettings() {
|
|
_storage.write(_sourceLanguageKey, sourceLanguage.value);
|
|
_storage.write(_targetLanguageKey, targetLanguage.value);
|
|
}
|
|
|
|
/// 请求录音权限
|
|
/// 返回是否授权成功
|
|
Future<bool> requestRecordPermission() async {
|
|
try {
|
|
hasRecordPermission.value =
|
|
await PermissionUtil.instance.requestPermission(
|
|
permissionType: Permission.microphone,
|
|
permissionName: 'microphonePermission'.tr,
|
|
explanationText: 'microphonePermissionExplanation'.tr,
|
|
permanentDenialText: 'microphonePermissionPermanentDenial'.tr,
|
|
);
|
|
|
|
if (hasRecordPermission.value) {
|
|
Logger.d("Permission", "用户授予了录音权限");
|
|
return true;
|
|
} else {
|
|
Logger.d("Permission", "用户未授予录音权限");
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
Logger.e("Permission", "请求录音权限时出错: $e");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 请求存储权限
|
|
/// 返回是否授权成功
|
|
Future<bool> _requestStoragePermission() async {
|
|
// 只在Android 10及以下版本请求存储权限
|
|
if (Platform.isAndroid) {
|
|
try {
|
|
final deviceInfo = DeviceInfoPlugin();
|
|
final androidInfo = await deviceInfo.androidInfo;
|
|
final sdkVersion = androidInfo.version.sdkInt;
|
|
|
|
// Android 11+ (API 30+) 不需要存储权限
|
|
if (sdkVersion >= 30) {
|
|
Logger.d("Permission", "Android 11+版本,无需请求存储权限");
|
|
return true; // 直接返回true,表示可以使用存储
|
|
}
|
|
} catch (e) {
|
|
Logger.e("Permission", "获取Android版本信息失败: $e");
|
|
// 如果获取版本失败,为了兼容性还是请求权限
|
|
}
|
|
}
|
|
|
|
try {
|
|
final bool granted = await PermissionUtil.instance.requestPermission(
|
|
permissionType: Permission.storage,
|
|
permissionName: 'storagePermission'.tr,
|
|
explanationText: 'storagePermissionExplanation'.tr,
|
|
permanentDenialText: 'storagePermissionPermanentDenial'.tr,
|
|
);
|
|
|
|
if (granted) {
|
|
Logger.d("Permission", "用户授予了存储权限");
|
|
return true;
|
|
} else {
|
|
Logger.d("Permission", "用户未授予存储权限");
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
Logger.e("Permission", "请求存储权限时出错: $e");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 暂停其他服务(如音乐播放)
|
|
Future<void> suspendOtherServices() async {
|
|
try {
|
|
var status = await _musiceManager.getStatus();
|
|
var isPlaying = status['isPlaying'] ?? false;
|
|
|
|
if (isPlaying == true) {
|
|
isContinuePlaying = true;
|
|
await _musiceManager.pause();
|
|
} else {
|
|
isContinuePlaying = false;
|
|
}
|
|
} catch (e) {
|
|
Logger.error('暂停其他服务失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
/// 恢复其他服务(如音乐播放)
|
|
Future<void> restoreOtherServices() async {
|
|
try {
|
|
if (isContinuePlaying) await _musiceManager.resume();
|
|
} catch (e) {
|
|
Logger.error('恢复其他服务失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
/// 初始化所有服务
|
|
Future<void> _initServices() async {
|
|
try {
|
|
await _translationService.initialize();
|
|
|
|
// 初始化 ASR 服务时,明确指定需要支持的语言
|
|
// final List<String> asrSupportedLanguages = [
|
|
// sourceLanguageCode.value,
|
|
// targetLanguageCode.value
|
|
// ];
|
|
//Logger.info('2初始化ASR服务,支持语言: $asrSupportedLanguages');
|
|
|
|
//await _asrService.initialize(supportedLanguages: asrSupportedLanguages);
|
|
|
|
// 获取识别事件流
|
|
// var recognitionStream = await _asrService.recognizeCallback();
|
|
// _recognitionSubscription?.cancel();
|
|
// _recognitionSubscription =
|
|
// recognitionStream.listen(_handleRecognitionEvent);
|
|
|
|
final bool storage = await _requestStoragePermission();
|
|
if (!storage) {
|
|
Logger.d("Permission", "未授予存储权限,无法提供录音功能");
|
|
return;
|
|
}
|
|
|
|
appDir = await appDocDir;
|
|
dir = Directory("${appDir.path}/TranslatAudio");
|
|
if (!await dir.exists()) {
|
|
await dir.create(recursive: true);
|
|
}
|
|
Logger.info('所有服务初始化完成');
|
|
} catch (e) {
|
|
Logger.error('初始化服务失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
/// 初始化通话模式的语音翻译服务
|
|
Future<void> _initializeCallModeTranslationService() async {
|
|
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 List<String> callModeLanguages = [
|
|
sourceLanguageCode.value,
|
|
targetLanguageCode.value
|
|
];
|
|
Logger.info('1初始化AST服务,支持语言: $callModeLanguages'); //ast=asr+translate
|
|
await _astService.initialize(supportedLanguages: callModeLanguages);
|
|
Logger.info('通话模式语音翻译服务初始化完成');
|
|
} catch (e) {
|
|
Logger.error('通话模式语音翻译服务初始化失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
stopAll();
|
|
_recognitionSubscription?.cancel();
|
|
_recognitionSubscription = null;
|
|
_voiceInteractionSubscription?.cancel();
|
|
_translationDebounceTimer?.cancel();
|
|
restoreOtherServices();
|
|
// 移除对服务的 dispose,避免频繁销毁/重建造成卡顿
|
|
// _asrService.dispose();
|
|
// _astService.dispose();
|
|
|
|
if (Platform.isAndroid) {
|
|
_disableFloatingWindow();
|
|
} else if (Platform.isIOS) {
|
|
_pip.dispose();
|
|
if (_pipContentView != 0) {
|
|
_nativePlugin.disposePipContentView(_pipContentView);
|
|
}
|
|
}
|
|
|
|
// 释放管理器资源
|
|
_timerManager.dispose();
|
|
_historyManager.dispose();
|
|
|
|
// 取消应用生命周期监听
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
|
|
super.onClose();
|
|
}
|
|
|
|
/// 应用生命周期变化回调
|
|
/// 当应用进入后台、失去焦点或分离时,兜底将 activeSpeaker 重置为 0
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (state == AppLifecycleState.inactive ||
|
|
state == AppLifecycleState.paused ||
|
|
state == AppLifecycleState.detached) {
|
|
if (activeSpeaker.value != 0) {
|
|
Future.microtask(() {
|
|
setActiveSpeaker(0);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 切换TTS功能开关
|
|
void toggleTts() => isTtsEnabled.toggle();
|
|
|
|
/// 切换录音功能开关
|
|
Future<void> toggleisRecord() async {
|
|
isRecording.toggle();
|
|
if (isRecording.value &&
|
|
isRecognizing.value &&
|
|
currentMode.value != 'faceToFace') {
|
|
startRecording();
|
|
} else if (!isRecording.value) {
|
|
stopRecording();
|
|
}
|
|
}
|
|
|
|
/// 开始录音
|
|
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");
|
|
} else {
|
|
await _asrService.enableRecord(
|
|
false, "${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav");
|
|
}
|
|
|
|
// if (currentMode.value == 'call') {
|
|
// await _astService.enableRecord(
|
|
// "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav");
|
|
// }
|
|
isCreateRecord.value = true;
|
|
}
|
|
|
|
/// 停止录音
|
|
Future<void> stopRecording() async {
|
|
_timerManager.stopTimer();
|
|
_asrService.stopRecord(true);
|
|
isCreateRecord.value = false;
|
|
Logger.info('录音已停止,计时器已重置');
|
|
}
|
|
|
|
/// 暂停录音
|
|
Future<void> pauseRecording() async {
|
|
_timerManager.pauseTimer();
|
|
_asrService.pauseRecord();
|
|
|
|
Logger.info(' 录音已暂停');
|
|
}
|
|
|
|
/// 继续录音
|
|
Future<void> continueRecording() async {
|
|
_timerManager.resumeTimer();
|
|
_asrService.resumeRecord();
|
|
|
|
Logger.info(' 录音已继续');
|
|
}
|
|
|
|
/// 开始语音识别
|
|
Future<void> startRecognition() async {
|
|
Logger.info('开始语音识别');
|
|
if (User.isLoggedIn() && User.instance.tradeintegral <= 0) {
|
|
showRechargeDialog('translation');
|
|
return;
|
|
}
|
|
|
|
if (!await _checkPermissions()) return;
|
|
if (isRecognizing.value) return;
|
|
|
|
try {
|
|
await _initializeSession();
|
|
await _configureAudioMode();
|
|
if (currentMode.value != 'faceToFace') {
|
|
if ((currentMode.value == 'audioVideo' ||
|
|
currentMode.value == 'call') &&
|
|
!bleManager.isCodecActive) {
|
|
return;
|
|
}
|
|
await _startAsrService();
|
|
await _finalizeRecognitionStart();
|
|
}
|
|
} catch (e) {
|
|
isPreparing.value = false;
|
|
isRecognizing.value = false;
|
|
Logger.error('启动语音识别失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
/// 检查权限
|
|
Future<bool> _checkPermissions() async {
|
|
final bool record = await requestRecordPermission();
|
|
if (!record) {
|
|
Logger.d("Permission", "录音权限被拒绝,无法开始录音");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// 初始化会话
|
|
Future<void> _initializeSession() async {
|
|
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
|
|
|
|
try {
|
|
_usageService.startNewSession();
|
|
_asrSessionStartTime = DateTime.now();
|
|
_asrActiveDuration = Duration.zero;
|
|
_isAsrActive = false;
|
|
} catch (e) {
|
|
Logger.error('开始新会话统计失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 配置音频模式
|
|
Future<void> _configureAudioMode() async {
|
|
switch (currentMode.value) {
|
|
case "audioVideo":
|
|
await _configureAudioVideoMode();
|
|
break;
|
|
case "call":
|
|
await _configureCallMode();
|
|
break;
|
|
default:
|
|
await _configureDefaultMode();
|
|
}
|
|
}
|
|
|
|
/// 配置音视频模式
|
|
Future<void> _configureAudioVideoMode() async {
|
|
isPreparing.value = true;
|
|
await bleManager.openDecoder();
|
|
int attempts = 0;
|
|
while (!bleManager.isCodecActive && attempts < 10) {
|
|
await Future.delayed(const Duration(milliseconds: 150));
|
|
attempts++;
|
|
}
|
|
isPreparing.value = false;
|
|
if (!bleManager.isCodecActive) {
|
|
Get.snackbar('tip'.tr, 'operationFailedPleaseRetry'.tr);
|
|
throw Exception('lxm--BLE编解码打开失败');
|
|
}
|
|
_audioSourceType = true;
|
|
isTtsEnabled.value = false;
|
|
Logger.info('发送ble音乐或者通话远端声音');
|
|
}
|
|
|
|
/// 配置通话模式
|
|
Future<void> _configureCallMode() async {
|
|
final List<String> callModeLanguages = [
|
|
sourceLanguageCode.value,
|
|
targetLanguageCode.value
|
|
];
|
|
Logger.info('1初始化AST服务,支持语言: $callModeLanguages'); //ast=asr+translate
|
|
await _astService.initialize(supportedLanguages: callModeLanguages);
|
|
isPreparing.value = true;
|
|
await bleManager.openA2DPDecoder();
|
|
int attempts = 0;
|
|
while (!bleManager.isCodecActive && attempts < 10) {
|
|
await Future.delayed(const Duration(milliseconds: 150));
|
|
attempts++;
|
|
}
|
|
isPreparing.value = false;
|
|
if (!bleManager.isCodecActive) {
|
|
Get.snackbar('tip'.tr, 'operationFailedPleaseRetry'.tr);
|
|
throw Exception('lxm--BLE编解码打开失败');
|
|
}
|
|
_audioSourceType = true;
|
|
isTtsEnabled.value = false;
|
|
Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音');
|
|
}
|
|
|
|
/// 配置默认模式
|
|
Future<void> _configureDefaultMode() async {
|
|
_audioSourceType = false;
|
|
isTtsEnabled.value = true;
|
|
}
|
|
|
|
/// 启动语音识别
|
|
Future<void> _startAsrService() async {
|
|
await _asrService.startContinuousRecognition(_audioSourceType);
|
|
}
|
|
|
|
/// 完成识别启动
|
|
Future<void> _finalizeRecognitionStart() async {
|
|
_startAsrActiveTracking();
|
|
isRecognizing.value = true;
|
|
|
|
if (isRecording.value && isRecognizing.value) {
|
|
startRecording();
|
|
}
|
|
}
|
|
|
|
/// 处理语音识别事件
|
|
/// [event] 识别事件
|
|
void _handleRecognitionEvent(RecognitionEvent event) {
|
|
// Logger.info("处理识别事件:${event}");
|
|
switch (event.type) {
|
|
case RecognitionEventType.finalResult:
|
|
if (event.text.isEmpty) break;
|
|
// 通话模式下:目标语言即为识别语言,无需判断
|
|
detectedLanguageCode = currentMode.value == 'call'
|
|
? targetLanguageCode.value
|
|
: event.detectedLanguage;
|
|
Logger.info("处理识别完成:${event.text}");
|
|
if (currentMode.value != 'faceToFace') {
|
|
handleFinalResult(event.text);
|
|
} else {
|
|
faceToFaceRecognitionText.value += event.text;
|
|
Logger.info('lxm-面对面翻译识别文本更新: ${faceToFaceRecognitionText.value}');
|
|
}
|
|
if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) {
|
|
handleFinalResult(faceToFaceRecognitionText.value);
|
|
faceToFaceRecognitionText.value = '';
|
|
}
|
|
break;
|
|
|
|
case RecognitionEventType.finalResult1:
|
|
// 通话模式下:己方(麦克风)识别完成事件(对应ast服务)
|
|
if (event.text.isEmpty) break;
|
|
//通话模式下:己方语言即为源语言,无需判断
|
|
detectedLanguageCode = sourceLanguageCode.value;
|
|
Logger.info("lxm---处理己方识别完成:${event.text}");
|
|
handleFinalResult(event.text);
|
|
break;
|
|
|
|
case RecognitionEventType.intermediateResult:
|
|
if (event.text.isEmpty) break;
|
|
// 通话模式下:目标语言即为识别语言,无需判断
|
|
detectedLanguageCode = currentMode.value == 'call'
|
|
? targetLanguageCode.value
|
|
: event.detectedLanguage;
|
|
Logger.info("处理识别中事件:${event.text}");
|
|
handleIntermediateResult(event.text);
|
|
break;
|
|
|
|
case RecognitionEventType.error:
|
|
Get.snackbar('error'.tr, event.error);
|
|
isRecognizing.value = false;
|
|
Logger.error('语音识别错误: ${event.error}');
|
|
break;
|
|
|
|
case RecognitionEventType.canceled:
|
|
isRecognizing.value = false;
|
|
Logger.error('语音识别取消: ${event.error}');
|
|
break;
|
|
|
|
case RecognitionEventType.sessionStarted:
|
|
// 可按需处理会话开始事件
|
|
break;
|
|
|
|
case RecognitionEventType.sessionStopped:
|
|
// 可按需处理会话结束事件
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// 停止语音识别
|
|
Future<void> stopRecognition() async {
|
|
if (!isRecognizing.value) return;
|
|
|
|
try {
|
|
await stopRecording();
|
|
await _asrService.stopContinuousRecognition();
|
|
if (currentMode.value == "call") {
|
|
await _astService.stopContinuousTranslation();
|
|
}
|
|
_stopAsrActiveTracking();
|
|
final langName = _languageManager.getChineseNameByAsrCode(
|
|
(detectedLanguageCode.isNotEmpty
|
|
? detectedLanguageCode
|
|
: sourceLanguageCode.value)) ??
|
|
'未知';
|
|
_recordAsrUsageStats(langName);
|
|
isRecognizing.value = false;
|
|
currentSessionId = null;
|
|
} catch (e) {
|
|
Logger.error('停止语音识别失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
/// 处理中间识别结果
|
|
/// [text] 识别的文本
|
|
void handleIntermediateResult(String text) {
|
|
if (text.isEmpty) return;
|
|
Logger.info(
|
|
"handleIntermediateResult:${translationHistory.isEmpty}${translationHistory.isNotEmpty ? translationHistory.last.isIntermediate : 'N/A'}");
|
|
if (translationHistory.isEmpty ||
|
|
(translationHistory.isNotEmpty &&
|
|
!translationHistory.last.isIntermediate)) {
|
|
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
|
|
|
|
// 关键调整:使用与最终结果一致的语言方向,避免左右“跳变”
|
|
final languageCodes = _determineLanguageCodes();
|
|
|
|
final newItem = TranslationItem(
|
|
sourceText: text,
|
|
translatedText: '',
|
|
sourceLanguageCode: languageCodes['source']!,
|
|
targetLanguageCode: languageCodes['target']!,
|
|
timestamp: DateTime.now(),
|
|
sessionId: currentSessionId!,
|
|
isFirstInSession: translationHistory.isEmpty ||
|
|
translationHistory.last.sessionId != currentSessionId,
|
|
isIntermediate: true,
|
|
);
|
|
|
|
if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) {
|
|
_asrService.stopContinuousRecognition();
|
|
} else {
|
|
_historyManager.addTranslationItem(newItem);
|
|
}
|
|
} else {
|
|
// 更新当前项
|
|
if (currentMode.value != 'faceToFace') {
|
|
translationHistory.last.sourceText = text;
|
|
} else {
|
|
translationHistory.last.sourceText =
|
|
faceToFaceRecognitionText.value + text;
|
|
}
|
|
translationHistory.refresh();
|
|
_historyManager.scrollToBottom();
|
|
}
|
|
|
|
// 防抖处理翻译请求
|
|
_translationDebounceTimer?.cancel();
|
|
_translationDebounceTimer = Timer(const Duration(milliseconds: 300), () {
|
|
if (translationHistory.isNotEmpty &&
|
|
translationHistory.last.isIntermediate &&
|
|
translationHistory.last.sourceText.isNotEmpty) {
|
|
translateText(translationHistory.last.sourceText,
|
|
translationHistory.last.timestamp,
|
|
isFinal: false);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 处理最终识别结果
|
|
/// [text] 识别的文本
|
|
Future<void> handleFinalResult(String text) async {
|
|
if (text.isEmpty) return;
|
|
|
|
// 更新当前项
|
|
if (translationHistory.isNotEmpty &&
|
|
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: '',
|
|
sourceLanguageCode: sourceLanguageCode.value,
|
|
targetLanguageCode: targetLanguageCode.value,
|
|
timestamp: DateTime.now(),
|
|
sessionId: currentSessionId ??
|
|
DateTime.now().millisecondsSinceEpoch.toString(),
|
|
isFirstInSession: translationHistory.isEmpty ||
|
|
translationHistory.last.sessionId != currentSessionId,
|
|
isIntermediate: false,
|
|
);
|
|
|
|
if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) {
|
|
_asrService.stopContinuousRecognition();
|
|
}
|
|
|
|
translationHistory.add(newItem);
|
|
translationHistory.refresh();
|
|
_scrollToBottom();
|
|
saveTranslationHistory();
|
|
translateText(text, newItem.timestamp, isFinal: true);
|
|
}
|
|
}
|
|
|
|
/// 翻译文本
|
|
/// [sourceText] 源文本
|
|
/// [timestamp] 时间戳
|
|
/// [isFinal] 是否为最终结果
|
|
/// 返回翻译结果
|
|
Future<String?> translateText(String sourceText, DateTime timestamp,
|
|
{bool isFinal = false}) async {
|
|
if (sourceText.isEmpty) return null;
|
|
|
|
isTranslating.value = true;
|
|
String? translationResult;
|
|
|
|
try {
|
|
final languageCodes = _determineLanguageCodes();
|
|
translationResult = await _performTranslation(
|
|
sourceText, languageCodes['source']!, languageCodes['target']!);
|
|
|
|
if (translationResult != null) {
|
|
await _updateTranslationHistory(
|
|
sourceText, translationResult, timestamp, languageCodes, isFinal);
|
|
}
|
|
} catch (e) {
|
|
Logger.error('翻译失败: ${e.toString()}');
|
|
} finally {
|
|
isTranslating.value = false;
|
|
}
|
|
|
|
return translationResult;
|
|
}
|
|
|
|
/// 确定语言代码
|
|
/// 返回源语言和目标语言代码的映射
|
|
Map<String, String> _determineLanguageCodes() {
|
|
String detectedSourceLanguageCode;
|
|
String detectedTargetLanguageCode;
|
|
|
|
if (_isFaceToFaceSpeaker1()) {
|
|
detectedLanguageCode = sourceLanguageCode.value;
|
|
detectedSourceLanguageCode = sourceLanguageCode.value;
|
|
detectedTargetLanguageCode = targetLanguageCode.value;
|
|
} else if (_isFaceToFaceSpeaker2()) {
|
|
detectedLanguageCode = targetLanguageCode.value;
|
|
detectedSourceLanguageCode = targetLanguageCode.value;
|
|
detectedTargetLanguageCode = sourceLanguageCode.value;
|
|
} else {
|
|
final shouldSwap = detectedLanguageCode == sourceLanguageCode.value;
|
|
detectedSourceLanguageCode =
|
|
shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value;
|
|
detectedTargetLanguageCode =
|
|
shouldSwap ? targetLanguageCode.value : sourceLanguageCode.value;
|
|
}
|
|
|
|
return {
|
|
'source': detectedSourceLanguageCode,
|
|
'target': detectedTargetLanguageCode,
|
|
};
|
|
}
|
|
|
|
/// 判断是否为面对面说话者1
|
|
bool _isFaceToFaceSpeaker1() {
|
|
return (currentMode.value == 'faceToFace' && activeSpeaker.value == 1) ||
|
|
(currentMode.value == 'faceToFace' &&
|
|
activeSpeaker.value == 0 &&
|
|
lastActiveSpeaker.value == 1);
|
|
}
|
|
|
|
/// 判断是否为面对面说话者2
|
|
bool _isFaceToFaceSpeaker2() {
|
|
return (currentMode.value == 'faceToFace' && activeSpeaker.value == 2) ||
|
|
(currentMode.value == 'faceToFace' &&
|
|
activeSpeaker.value == 0 &&
|
|
lastActiveSpeaker.value == 2);
|
|
}
|
|
|
|
/// 执行翻译
|
|
/// [sourceText] 源文本
|
|
/// [sourceCode] 源语言代码
|
|
/// [targetCode] 目标语言代码
|
|
/// 返回翻译结果
|
|
Future<String?> _performTranslation(
|
|
String sourceText, String sourceCode, String targetCode) async {
|
|
return await _translationService.translateText(
|
|
text: sourceText,
|
|
sourceLanguageCode: sourceCode,
|
|
targetLanguageCode: targetCode,
|
|
);
|
|
}
|
|
|
|
/// 更新翻译历史
|
|
/// [sourceText] 源文本
|
|
/// [translationResult] 翻译结果
|
|
/// [timestamp] 时间戳
|
|
/// [languageCodes] 语言代码映射
|
|
/// [isFinal] 是否为最终结果
|
|
Future<void> _updateTranslationHistory(
|
|
String sourceText,
|
|
String translationResult,
|
|
DateTime timestamp,
|
|
Map<String, String> languageCodes,
|
|
bool isFinal,
|
|
) async {
|
|
final index = translationHistory
|
|
.indexWhere((item) => item.timestamp.isAtSameMomentAs(timestamp));
|
|
|
|
if (index != -1) {
|
|
await _updateExistingHistoryItem(
|
|
index, translationResult, languageCodes, sourceText, isFinal);
|
|
} else {
|
|
Logger.info('未找到匹配的历史项 $sourceText ${timestamp.toIso8601String()}');
|
|
}
|
|
}
|
|
|
|
/// 更新现有历史项
|
|
/// [index] 历史项索引
|
|
/// [translationResult] 翻译结果
|
|
/// [languageCodes] 语言代码映射
|
|
/// [sourceText] 源文本
|
|
/// [isFinal] 是否为最终结果
|
|
Future<void> _updateExistingHistoryItem(
|
|
int index,
|
|
String translationResult,
|
|
Map<String, String> languageCodes,
|
|
String sourceText,
|
|
bool isFinal,
|
|
) async {
|
|
// 更新历史项
|
|
translationHistory[index].translatedText = translationResult;
|
|
translationHistory[index].sourceLanguageCode = languageCodes['source']!;
|
|
translationHistory[index].targetLanguageCode = languageCodes['target']!;
|
|
translationHistory.refresh();
|
|
|
|
// 更新悬浮窗内容
|
|
if (isFloatingWindowEnabled.value) {
|
|
await _updateFloatingWindowWithTranslation(translationHistory[index]);
|
|
}
|
|
|
|
// 添加TranslationItem日志打印
|
|
if (isFinal) {
|
|
final item = translationHistory[index];
|
|
Logger.info('lxm-翻译完成 - TranslationItem详情: '
|
|
'源文本: ${item.sourceText}, '
|
|
'翻译文本: ${item.translatedText}, '
|
|
'源语言: ${item.sourceLanguageCode}, '
|
|
'目标语言: ${item.targetLanguageCode}, '
|
|
'时间戳: ${item.timestamp.toIso8601String()}, '
|
|
'会话ID: ${item.sessionId}, '
|
|
'是否会话首项: ${item.isFirstInSession}, '
|
|
'是否中间结果: ${item.isIntermediate}');
|
|
}
|
|
|
|
if (isFinal) {
|
|
saveTranslationHistory();
|
|
await _recordTranslationStats(
|
|
sourceText, translationResult, languageCodes);
|
|
}
|
|
|
|
_scrollToBottom();
|
|
|
|
if (isFinal) {
|
|
await _handleTtsPlayback(translationResult);
|
|
}
|
|
}
|
|
|
|
/// 记录翻译统计
|
|
/// [sourceText] 源文本
|
|
/// [translationResult] 翻译结果
|
|
/// [languageCodes] 语言代码映射
|
|
Future<void> _recordTranslationStats(
|
|
String sourceText,
|
|
String translationResult,
|
|
Map<String, String> languageCodes,
|
|
) async {
|
|
try {
|
|
final sourceLanguageName =
|
|
_languageManager.getChineseNameByAsrCode(languageCodes['source']!) ??
|
|
'未知';
|
|
final targetLanguageName =
|
|
_languageManager.getChineseNameByAsrCode(languageCodes['target']!) ??
|
|
'未知';
|
|
|
|
_usageService.recordTranslationApiCall(
|
|
sourceText: sourceText,
|
|
targetText: translationResult,
|
|
sourceLanguage: sourceLanguageName,
|
|
targetLanguage: targetLanguageName,
|
|
mode: currentMode.value,
|
|
);
|
|
|
|
Logger.info(
|
|
'翻译统计: 源文本${sourceText.length}字符, 模式${currentMode.value}, 语言对$sourceLanguageName→$targetLanguageName');
|
|
} catch (e) {
|
|
Logger.error('记录翻译统计失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 处理TTS播放
|
|
/// [translationResult] 翻译结果
|
|
Future<void> _handleTtsPlayback(String translationResult) async {
|
|
if (isTtsEnabled.value) {
|
|
await playTranslatedText(translationResult);
|
|
}
|
|
}
|
|
|
|
// /// 处理面对面TTS
|
|
// /// [translationResult] 翻译结果
|
|
// Future<void> _handleFaceToFaceTts(String translationResult) async {
|
|
// fTFTranslationResult = (fTFTranslationResult ?? '') + translationResult;
|
|
// print("translationResult=$translationResult");
|
|
|
|
// if (activeSpeaker.value == 0 && !isPlayback) {
|
|
// isPlayback = true;
|
|
// await playTranslatedText(fTFTranslationResult!);
|
|
// fTFTranslationResult = '';
|
|
// }
|
|
// }
|
|
|
|
/// 播放翻译文本
|
|
/// [text] 要播放的文本
|
|
Future<void> playTranslatedText(String text) async {
|
|
if (text.isEmpty) return;
|
|
|
|
try {
|
|
final bool shouldSwap = detectedLanguageCode == targetLanguageCode.value;
|
|
final detectedTargetLanguageCode =
|
|
shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value;
|
|
final voiceName = _languageManager
|
|
.getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ??
|
|
'en-US-AriaNeural';
|
|
Logger.info("voiceName=$voiceName,text=$text");
|
|
|
|
final startTime = DateTime.now();
|
|
await _ttsService.setVoice(voiceName);
|
|
await _ttsService.speakOnce(currsessionid, text);
|
|
final endTime = DateTime.now();
|
|
|
|
// 记录TTS API调用统计
|
|
try {
|
|
final durationSeconds = endTime.difference(startTime).inSeconds;
|
|
final languageName = _languageManager
|
|
.getChineseNameByAsrCode(detectedTargetLanguageCode) ??
|
|
'未知';
|
|
|
|
_usageService.recordTtsApiCall(
|
|
text: text,
|
|
durationSeconds: durationSeconds,
|
|
language: languageName,
|
|
);
|
|
|
|
Logger.info(
|
|
'TTS统计: 文本${text.length}字符, 播放${durationSeconds}秒, 语言$languageName');
|
|
} catch (e) {
|
|
Logger.error('记录TTS统计失败: $e');
|
|
}
|
|
} catch (e) {
|
|
Logger.error('播放翻译失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
/// 停止所有操作
|
|
Future<void> stopAll() async {
|
|
try {
|
|
isPreparing.value = false;
|
|
if (currentMode.value == "audioVideo" || currentMode.value == "call") {
|
|
bleManager.closeCodec();
|
|
Logger.info('关闭ble编解码器');
|
|
}
|
|
if (isRecognizing.value) {
|
|
await stopRecognition();
|
|
}
|
|
|
|
_stopAsrActiveTracking();
|
|
_translationDebounceTimer?.cancel();
|
|
await _ttsService.stop();
|
|
} catch (e) {
|
|
Logger.error('停止操作失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
/// 播放特定翻译项
|
|
/// [item] 翻译项
|
|
Future<void> playTranslationItem(TranslationItem item) async {
|
|
if (item.translatedText.isEmpty) return;
|
|
|
|
// 防重复点击:如果短时间内重复点击同一项,忽略后续点击
|
|
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
|
final itemKey =
|
|
'${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}';
|
|
|
|
// 检查是否是重复点击同一项
|
|
if (_lastPlayedItemKey == itemKey &&
|
|
currentTime - _lastPlayTime < _playDebounceInterval) {
|
|
Logger.info('防重复播放:忽略短时间内的重复点击');
|
|
return;
|
|
}
|
|
|
|
// 如果正在播放,先停止当前播放
|
|
if (_ttsService.isSpeaking) {
|
|
await _ttsService.stop();
|
|
// 短暂延迟确保停止完成
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
}
|
|
|
|
try {
|
|
// 更新播放记录
|
|
_lastPlayedItemKey = itemKey;
|
|
_lastPlayTime = currentTime;
|
|
|
|
final voiceName =
|
|
_languageManager.getTtsVoiceNameByAsrCode(item.targetLanguageCode) ??
|
|
'en-US-AriaNeural';
|
|
await _ttsService.setVoice(voiceName);
|
|
await _ttsService.speakOnce(currsessionid, item.translatedText);
|
|
} catch (e) {
|
|
Logger.error('播放翻译项失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
/// 播放文本
|
|
/// [text] 要播放的文本
|
|
/// [languageCode] 语言代码(可选)
|
|
Future<void> playText(String text, {String? languageCode}) async {
|
|
if (text.isEmpty) return;
|
|
|
|
// 防重复点击:基于文本内容和语言代码生成唯一标识
|
|
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
|
final textKey = '${text}_${languageCode ?? targetLanguageCode.value}';
|
|
|
|
// 检查是否是重复点击同一文本
|
|
if (_lastPlayedItemKey == textKey &&
|
|
currentTime - _lastPlayTime < _playDebounceInterval) {
|
|
Logger.info('防重复播放:忽略短时间内的重复文本播放请求');
|
|
return;
|
|
}
|
|
|
|
// 如果正在播放,先停止当前播放
|
|
if (_ttsService.isSpeaking) {
|
|
await _ttsService.stop();
|
|
// 短暂延迟确保停止完成
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
}
|
|
|
|
try {
|
|
// 更新播放记录
|
|
_lastPlayedItemKey = textKey;
|
|
_lastPlayTime = currentTime;
|
|
|
|
final voiceName = languageCode != null
|
|
? _languageManager.getTtsVoiceNameByAsrCode(languageCode) ??
|
|
'en-US-AriaNeural'
|
|
: _languageManager
|
|
.getTtsVoiceNameByAsrCode(targetLanguageCode.value) ??
|
|
'en-US-AriaNeural';
|
|
|
|
await _ttsService.setVoice(voiceName);
|
|
await _ttsService.speakOnce(currsessionid, text);
|
|
} catch (e) {
|
|
Logger.error('播放文本失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
/// 滚动到底部(适配reverse模式)
|
|
/// [animate] 是否使用动画
|
|
void _scrollToBottom({bool animate = true}) {
|
|
if (scrollController.hasClients &&
|
|
(translationHistory.isNotEmpty || currentSessionId != null)) {
|
|
try {
|
|
const targetPosition = 0.0; // reverse模式下最新消息在顶部
|
|
|
|
if (animate) {
|
|
scrollController.animateTo(
|
|
targetPosition,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeOut,
|
|
);
|
|
} else {
|
|
scrollController.jumpTo(targetPosition);
|
|
}
|
|
} catch (e) {
|
|
Logger.error('滚动到顶部失败(reverse模式): ${e.toString()}');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 设置滚动监听器(简化版,保留接口兼容性)
|
|
void setupScrollListener() {
|
|
// 简化后的空方法,保留接口兼容性
|
|
}
|
|
|
|
/// 设置底部控制栏高度
|
|
/// [height] 高度值
|
|
void setBottomBarHeight(double height) {
|
|
_bottomBarHeight = height;
|
|
}
|
|
|
|
/// 获取底部控制栏高度
|
|
double get bottomBarHeight => _bottomBarHeight;
|
|
|
|
/// 切换源语言和目标语言
|
|
void swapLanguages() {
|
|
stopAll();
|
|
|
|
final tempName = sourceLanguage.value;
|
|
sourceLanguage.value = targetLanguage.value;
|
|
targetLanguage.value = tempName;
|
|
|
|
final tempCode = sourceLanguageCode.value;
|
|
sourceLanguageCode.value = targetLanguageCode.value;
|
|
targetLanguageCode.value = tempCode;
|
|
|
|
_saveLanguageSettings();
|
|
_reinitializeAsrService();
|
|
}
|
|
|
|
/// 设置源语言
|
|
/// [language] 语言名称
|
|
void setSourceLanguage(String language) {
|
|
if (sourceLanguage.value != language) {
|
|
stopAll();
|
|
|
|
sourceLanguage.value = language;
|
|
|
|
final asrCode = _languageManager.getAsrCodeByChineseName(language);
|
|
if (asrCode != null) {
|
|
sourceLanguageCode.value = asrCode;
|
|
}
|
|
|
|
_saveLanguageSettings();
|
|
_reinitializeAsrService();
|
|
}
|
|
}
|
|
|
|
/// 设置目标语言
|
|
/// [language] 语言名称
|
|
void setTargetLanguage(String language) {
|
|
if (targetLanguage.value != language) {
|
|
stopAll();
|
|
|
|
targetLanguage.value = language;
|
|
|
|
final asrCode = _languageManager.getAsrCodeByChineseName(language);
|
|
if (asrCode != null) {
|
|
targetLanguageCode.value = asrCode;
|
|
}
|
|
|
|
_saveLanguageSettings();
|
|
_reinitializeAsrService();
|
|
}
|
|
}
|
|
|
|
/// 重新初始化ASR服务
|
|
Future<void> _reinitializeAsrService() async {
|
|
try {
|
|
//1.统一先停止旧识别
|
|
await stopRecognition();
|
|
|
|
late final List<String> asrSupportedLanguages;
|
|
|
|
if (currentMode.value == 'audioVideo') {
|
|
asrSupportedLanguages = [sourceLanguageCode.value];
|
|
} else {
|
|
asrSupportedLanguages = [
|
|
sourceLanguageCode.value,
|
|
targetLanguageCode.value
|
|
];
|
|
}
|
|
|
|
if (currentMode.value == "call") {
|
|
_initializeCallModeTranslationService();
|
|
} else {
|
|
Logger.info('重新初始化ASR服务,支持语言: $asrSupportedLanguages');
|
|
|
|
await _asrService.initialize(supportedLanguages: asrSupportedLanguages);
|
|
|
|
var recognitionStream = await _asrService.recognizeCallback();
|
|
_recognitionSubscription?.cancel();
|
|
_recognitionSubscription =
|
|
recognitionStream.listen(_handleRecognitionEvent);
|
|
}
|
|
Logger.info('ASR服务重新初始化成功');
|
|
} catch (e) {
|
|
Logger.error('重新初始化ASR服务失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
// ==================== 历史记录相关方法 ====================
|
|
/// 加载翻译历史
|
|
void loadTranslationHistory() => _historyManager.loadTranslationHistory();
|
|
|
|
/// 保存翻译历史
|
|
void saveTranslationHistory() => _historyManager.saveTranslationHistory();
|
|
|
|
/// 设置滚动监听器(适配reverse模式)
|
|
void _setupScrollListener() {
|
|
if (_scrollListenerSetup) return;
|
|
|
|
scrollController.addListener(() {
|
|
if (scrollController.position.pixels >=
|
|
scrollController.position.maxScrollExtent - 100) {
|
|
if (hasMoreHistory.value && !isLoadingMore.value) {
|
|
Logger.d(TAG, '触发加载更多翻译历史记录(reverse模式)');
|
|
loadMoreHistory();
|
|
}
|
|
}
|
|
});
|
|
|
|
_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<TranslationItem> getAllHistory() => _historyManager.getAllHistory();
|
|
|
|
/// 加载更多历史记录
|
|
Future<void> loadMoreHistory() => _historyManager.loadMoreHistory();
|
|
|
|
/// 滚动到底部
|
|
/// [animate] 是否使用动画
|
|
void scrollToBottom({bool animate = true}) =>
|
|
_historyManager.scrollToBottom(animate: animate);
|
|
|
|
// ==================== ASR活跃时长跟踪 ====================
|
|
/// 开始ASR活跃时长跟踪
|
|
void _startAsrActiveTracking() {
|
|
if (!_isAsrActive) {
|
|
_asrActiveStartTime = DateTime.now();
|
|
_isAsrActive = true;
|
|
Logger.info('开始ASR活跃时长跟踪');
|
|
}
|
|
}
|
|
|
|
/// 停止ASR活跃时长跟踪
|
|
void _stopAsrActiveTracking() {
|
|
if (_isAsrActive && _asrActiveStartTime != null) {
|
|
final activeDuration = DateTime.now().difference(_asrActiveStartTime!);
|
|
_asrActiveDuration += activeDuration;
|
|
_isAsrActive = false;
|
|
_asrActiveStartTime = null;
|
|
Logger.info('停止ASR活跃时长跟踪,本次活跃时长: ${activeDuration.inSeconds}秒');
|
|
}
|
|
}
|
|
|
|
/// 记录ASR使用统计 - 基于精确时长跟踪
|
|
/// [language] 语言名称
|
|
void _recordAsrUsageStats(String language) {
|
|
try {
|
|
if (_asrSessionStartTime == null) return;
|
|
|
|
_stopAsrActiveTracking();
|
|
|
|
final now = DateTime.now();
|
|
final totalDurationSeconds =
|
|
now.difference(_asrSessionStartTime!).inSeconds;
|
|
final effectiveDurationSeconds = _asrActiveDuration.inSeconds;
|
|
|
|
_usageService.recordAsrApiCall(
|
|
effectiveDurationSeconds: effectiveDurationSeconds,
|
|
totalDurationSeconds: totalDurationSeconds,
|
|
language: language,
|
|
mode: currentMode.value,
|
|
audioFormat: 'pcm',
|
|
sampleRate: 16000,
|
|
);
|
|
|
|
Logger.info(
|
|
'ASR会话统计: 总时长${totalDurationSeconds}秒, 实际活跃时长${effectiveDurationSeconds}秒, 语言$language');
|
|
_asrSessionStartTime = null;
|
|
_asrActiveDuration = Duration.zero;
|
|
_isAsrActive = false;
|
|
_asrActiveStartTime = null;
|
|
} catch (e) {
|
|
Logger.error('记录ASR统计失败: $e');
|
|
}
|
|
}
|
|
}
|
|
|