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.
1675 lines
58 KiB
1675 lines
58 KiB
import 'dart:io';
|
|
|
|
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';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import '../../../data/services/ast_service.dart';
|
|
import '../../../data/services/bluetooth_manager.dart';
|
|
import '../../../data/services/music_manager.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 '../../../data/services/asr_service.dart';
|
|
import '../../../data/services/translation_usage_service.dart';
|
|
import '../../../data/services/ble_manager.dart';
|
|
|
|
// 切换音频输出设备的枚举
|
|
enum AudioOutputDevice {
|
|
defaultDevice, // 默认(如有耳机优先耳机,否则扬声器)
|
|
headphones, // 强制耳机
|
|
speaker, // 强制扬声器
|
|
}
|
|
|
|
class TranslationController extends GetxController {
|
|
static const String TAG = 'TranslationController';
|
|
|
|
// 服务
|
|
final BluetoothManager _bluetoothManager = Get.find<BluetoothManager>();
|
|
final BleManager _bleManager = Get.find<BleManager>();
|
|
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 bleManager = Get.find<BleManager>();
|
|
//音乐播放管理
|
|
final _musiceManager = Get.find<MusicManager>();
|
|
// 统计服务
|
|
late final TranslationUsageService _usageService;
|
|
|
|
// ASR统计相关 - 精确时长统计
|
|
DateTime? _asrSessionStartTime; // ASR会话开始时间
|
|
DateTime? _asrActiveStartTime; // ASR活跃开始时间(用户实际开始语音识别的时间)
|
|
Duration _asrActiveDuration = Duration.zero; // ASR累计活跃时长
|
|
bool _isAsrActive = false; // ASR是否处于活跃状态
|
|
|
|
// 音频输入源
|
|
bool _audioSourceType = false;
|
|
//是否开启播放
|
|
bool isPlayback = false;
|
|
//录音计时器
|
|
final RxString recordDurationText = '00:00'.obs;
|
|
//记录计时器
|
|
Timer? _recordTimer;
|
|
//秒
|
|
int _seconds = 0;
|
|
//暂停状态记录计时器是否暂停
|
|
bool _isTimerPaused = false;
|
|
// 存储相关配置
|
|
static const String _historyKey = 'translation_history';
|
|
static const String _sourceLanguageKey = 'translation_source_language';
|
|
static const String _targetLanguageKey = 'translation_target_language';
|
|
|
|
// 【重要改进】区分存储限制和显示限制
|
|
// 存储限制:大幅增加到1000条,充分利用本地存储能力,几乎无限制存储
|
|
static const int _maxStorageItems = 1000;
|
|
// 主页面显示限制:只显示最近20条,保证界面性能和用户体验
|
|
static const int _maxDisplayItems = 20;
|
|
|
|
// 分页加载相关状态
|
|
final isLoadingMore = false.obs; // 是否正在加载更多历史记录
|
|
final hasMoreHistory = true.obs; // 是否还有更多历史记录
|
|
final currentPage = 1.obs; // 当前页码
|
|
final pageSize = 20; // 每页记录数量
|
|
final isLoadingMoreError = false.obs; // 加载更多时是否出错
|
|
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 currentModeTitle = 'simultaneousTranslation'.obs;
|
|
final currentMode = 'simultaneous'.obs;
|
|
// 翻译历史和当前项
|
|
|
|
String? fTFTranslationResult = '';
|
|
final translationHistory = <TranslationItem>[].obs;
|
|
String? currentSessionId;
|
|
final appDocDir = getApplicationDocumentsDirectory();
|
|
var appDir;
|
|
var dir;
|
|
|
|
// 最新的语音识别文本
|
|
final latestRecognitionText = ''.obs;
|
|
|
|
// 检测到的语言代码
|
|
final detectedLanguageCode1 = ''.obs;
|
|
//int num = 0;
|
|
Future<void> changeTranslationMode(String mode) async {
|
|
currentMode.value = mode;
|
|
currentModeTitle.value = _getModeTitle(mode);
|
|
stopAll();
|
|
}
|
|
|
|
String _getModeTitle(String mode) {
|
|
switch (mode) {
|
|
case 'audioVideo':
|
|
return 'audioVideoTranslation';
|
|
case 'call':
|
|
return 'callTranslation';
|
|
case 'faceToFace':
|
|
return 'faceToFaceTranslation';
|
|
|
|
default:
|
|
return 'simultaneousTranslation';
|
|
}
|
|
}
|
|
|
|
/// 暂停计时器
|
|
void pauseRecordTimer() {
|
|
if (_recordTimer != null && !_isTimerPaused) {
|
|
_recordTimer?.cancel();
|
|
_recordTimer = null;
|
|
_isTimerPaused = true;
|
|
Logger.info('录音计时器已暂停');
|
|
}
|
|
}
|
|
|
|
/// 恢复计时器
|
|
void resumeRecordTimer() {
|
|
if (_isTimerPaused) {
|
|
_recordTimer = Timer.periodic(Duration(seconds: 1), (_) {
|
|
_seconds++;
|
|
final minutes = (_seconds ~/ 60).toString().padLeft(2, '0');
|
|
final seconds = (_seconds % 60).toString().padLeft(2, '0');
|
|
recordDurationText.value = '$minutes:$seconds';
|
|
});
|
|
_isTimerPaused = false;
|
|
Logger.info('录音计时器已恢复');
|
|
}
|
|
}
|
|
|
|
/// 重置计时器
|
|
void resetRecordTimer() {
|
|
_recordTimer?.cancel();
|
|
_recordTimer = null;
|
|
_seconds = 0;
|
|
_isTimerPaused = false;
|
|
recordDurationText.value = '00:00';
|
|
Logger.info('录音计时器已重置');
|
|
}
|
|
|
|
Future<void> setActiveSpeaker(int id) async {
|
|
activeSpeaker.value = id;
|
|
await _asrService.startContinuousRecognition(_audioSourceType);
|
|
// 监听识别结果
|
|
if (activeSpeaker.value != 0) {
|
|
isPlayback = false;
|
|
isRecognizing.value = true;
|
|
if (isRecording.value && isRecognizing.value) {
|
|
if (lasyIsRecording.value != isRecording.value) {
|
|
lasyIsRecording.value = isRecording.value;
|
|
await startRecording();
|
|
} else {
|
|
await _asrService.resumeRecord();
|
|
// 恢复计时器
|
|
resumeRecordTimer();
|
|
}
|
|
}
|
|
|
|
// 开始ASR活跃时长跟踪(面对面模式)
|
|
_startAsrActiveTracking();
|
|
|
|
lastActiveSpeaker.value = id;
|
|
} else {
|
|
// 停止ASR活跃时长跟踪(面对面模式)
|
|
_stopAsrActiveTracking();
|
|
if (isRecording.value && isRecognizing.value) {
|
|
// 暂停计时器
|
|
pauseRecordTimer();
|
|
await _asrService.pauseRecord();
|
|
}
|
|
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<void> disableBluetoothAudio() async =>
|
|
await _asrService.disableBluetoothAudio();
|
|
//恢复原始音频状态
|
|
Future<void> restoreOriginalAudioState() async =>
|
|
await _asrService.restoreOriginalAudioState();
|
|
|
|
// 兼容旧版视图的计算属性
|
|
Rx<TranslationItem?> get currentItem {
|
|
if (translationHistory.isNotEmpty &&
|
|
translationHistory.last.isIntermediate) {
|
|
return Rx<TranslationItem?>(translationHistory.last);
|
|
}
|
|
return Rx<TranslationItem?>(null);
|
|
}
|
|
|
|
// UI相关
|
|
final ScrollController scrollController = ScrollController();
|
|
|
|
// 其他
|
|
StreamSubscription? _recognitionSubscription;
|
|
StreamSubscription? _voiceInteractionSubscription;
|
|
Timer? _translationDebounceTimer;
|
|
// 获取支持的语言列表
|
|
Map<String, String> get supportedLanguages =>
|
|
_languageManager.getChineseNameToAsrCodeMap();
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
|
|
//_setupEventListeners();
|
|
// 预先检查权限,避免在用户交互时阻塞
|
|
_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;
|
|
}
|
|
|
|
// 加载历史记录
|
|
loadTranslationHistory();
|
|
|
|
// 初始化服务
|
|
_initServices();
|
|
// 初始化语音翻译服务
|
|
_initializeCallModeTranslationService();
|
|
// 🔑 reverse模式下不需要手动滚动,ListView会自然显示最新内容
|
|
// 参考Agent模块的实现模式
|
|
}
|
|
|
|
/// 预先检查权限
|
|
Future<void> _preCheckPermissions() async {
|
|
try {
|
|
await requestRecordPermission();
|
|
} catch (e) {
|
|
Logger.e("Permission", "预检查权限失败: $e");
|
|
}
|
|
}
|
|
|
|
// 初始化统计服务
|
|
void _initUsageService() {
|
|
try {
|
|
// 确保统计服务已注册
|
|
if (!Get.isRegistered<TranslationUsageService>()) {
|
|
Get.put(TranslationUsageService(), permanent: true);
|
|
}
|
|
_usageService = Get.find<TranslationUsageService>();
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 保存当前语言设置
|
|
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 {
|
|
// final BuildContext context = Get.overlayContext!;
|
|
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('初始化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; // 先 await 获取目录
|
|
dir = Directory("${appDir.path}/TranslatAudio");
|
|
if (!await dir.exists()) {
|
|
await dir.create(recursive: true); // Add recursive creation
|
|
}
|
|
Logger.info('所有服务初始化完成');
|
|
} catch (e) {
|
|
Logger.error('初始化服务失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
// 初始化通话模式的语音翻译服务
|
|
Future<void> _initializeCallModeTranslationService() async {
|
|
try {
|
|
Logger.info('开始初始化通话模式语音翻译服务');
|
|
// 为通话模式配置特殊的ASR设置
|
|
final List<String> callModeLanguages = [
|
|
sourceLanguageCode.value,
|
|
targetLanguageCode.value
|
|
];
|
|
// Future<bool> initialize({
|
|
// required String subscriptionKey,
|
|
// required String region,
|
|
// required List<String> supportedLanguages,
|
|
// required String audioSourceType,
|
|
// required String translationAccessKey,
|
|
// required String translationSecretKey,
|
|
// String translationRegion = 'cn-north-1',
|
|
// });
|
|
// 重新初始化ASR服务以支持通话音频源
|
|
await _astService.initialize(supportedLanguages: callModeLanguages);
|
|
|
|
// 配置实时翻译参数
|
|
await _configureCallModeTranslation();
|
|
|
|
Logger.info('通话模式语音翻译服务初始化完成');
|
|
return;
|
|
} catch (e) {
|
|
Logger.error('通话模式语音翻译服务初始化失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
// 配置通话模式的翻译参数
|
|
Future<void> _configureCallModeTranslation() async {
|
|
try {
|
|
// 设置通话模式的特殊配置
|
|
// 1. 更短的识别超时时间,适应通话场景
|
|
// 2. 更高的识别敏感度
|
|
// 3. 噪声抑制优化
|
|
|
|
// 这里可以调用ASR服务的特殊配置方法
|
|
// await _asrService.configureForCallMode(
|
|
// endSilenceTimeout: 200, // 更短的静音超时
|
|
// noiseReduction: true, // 启用噪声抑制
|
|
// echoCancellation: true, // 启用回声消除
|
|
// );
|
|
|
|
Logger.info('通话模式翻译参数配置完成');
|
|
} catch (e) {
|
|
Logger.error('通话模式翻译参数配置失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
// // 处理通话模式的翻译结果
|
|
// Future<void> _handleCallModeTranslation(String sourceText) async {
|
|
// try {
|
|
// // 在通话模式下,翻译结果可能需要特殊处理
|
|
// // 例如:发送到蓝牙设备、显示在特定UI等
|
|
|
|
// final translationResult = await _translationService.translateText(
|
|
// text: sourceText,
|
|
// sourceLanguageCode: sourceLanguageCode.value,
|
|
// targetLanguageCode: targetLanguageCode.value,
|
|
// );
|
|
|
|
// if (translationResult != null && translationResult.isNotEmpty) {
|
|
// // 通话模式下的特殊处理
|
|
// await _processCallModeTranslationResult(sourceText, translationResult);
|
|
// }
|
|
// } catch (e) {
|
|
// Logger.error('通话模式翻译处理失败: ${e.toString()}');
|
|
// }
|
|
// }
|
|
|
|
// // 处理通话模式的翻译结果
|
|
// Future<void> _processCallModeTranslationResult(String sourceText, String translatedText) async {
|
|
// try {
|
|
// // 1. 更新UI显示
|
|
// final newItem = TranslationItem(
|
|
// sourceText: sourceText,
|
|
// translatedText: translatedText,
|
|
// sourceLanguageCode: sourceLanguageCode.value,
|
|
// targetLanguageCode: targetLanguageCode.value,
|
|
// timestamp: DateTime.now(),
|
|
// sessionId: currentSessionId ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
|
// isFirstInSession: translationHistory.isEmpty,
|
|
// isIntermediate: false,
|
|
// );
|
|
|
|
// translationHistory.add(newItem);
|
|
// translationHistory.refresh();
|
|
// _scrollToBottom();
|
|
// saveTranslationHistory();
|
|
|
|
// // 2. 通话模式下可能需要将翻译结果发送到蓝牙设备
|
|
// // 或者通过其他方式传输给通话对方
|
|
// await _sendTranslationToCallParty(translatedText);
|
|
|
|
// // 3. 记录统计信息
|
|
// _recordCallModeUsageStats(sourceText, translatedText);
|
|
|
|
// Logger.info('通话模式翻译结果处理完成: $sourceText -> $translatedText');
|
|
// } catch (e) {
|
|
// Logger.error('通话模式翻译结果处理失败: ${e.toString()}');
|
|
// }
|
|
// }
|
|
|
|
// // 将翻译结果发送给通话对方
|
|
// Future<void> _sendTranslationToCallParty(String translatedText) async {
|
|
// try {
|
|
// // 这里可以实现将翻译结果发送给通话对方的逻辑
|
|
// // 例如:通过蓝牙、网络等方式
|
|
|
|
// // 示例:通过蓝牙发送
|
|
// await bleManager.sendTranslationResult(translatedText);
|
|
|
|
// Logger.info('翻译结果已发送给通话对方: $translatedText');
|
|
// } catch (e) {
|
|
// Logger.error('发送翻译结果失败: ${e.toString()}');
|
|
// }
|
|
// }
|
|
|
|
// // 记录通话模式的使用统计
|
|
// void _recordCallModeUsageStats(String sourceText, String translatedText) {
|
|
// try {
|
|
// _usageService.recordTranslationApiCall(
|
|
// sourceText: sourceText,
|
|
// targetText: translatedText,
|
|
// sourceLanguage: _languageManager.getChineseNameByAsrCode(sourceLanguageCode.value) ?? '未知',
|
|
// targetLanguage: _languageManager.getChineseNameByAsrCode(targetLanguageCode.value) ?? '未知',
|
|
// mode: 'call', // 明确标记为通话模式
|
|
// );
|
|
|
|
// Logger.info('通话模式使用统计已记录');
|
|
// } catch (e) {
|
|
// Logger.error('记录通话模式统计失败: ${e.toString()}');
|
|
// }
|
|
// }
|
|
|
|
@override
|
|
void onClose() {
|
|
stopAll();
|
|
_recognitionSubscription?.cancel();
|
|
_recognitionSubscription = null;
|
|
_voiceInteractionSubscription?.cancel();
|
|
_translationDebounceTimer?.cancel();
|
|
scrollController.dispose();
|
|
restoreOtherServices();
|
|
_asrService.dispose();
|
|
_astService.dispose();
|
|
super.onClose();
|
|
}
|
|
|
|
// 切换TTS功能
|
|
void toggleTts() => isTtsEnabled.toggle();
|
|
|
|
// 切换录音功能
|
|
Future<void> toggleisRecord() async {
|
|
isRecording.toggle();
|
|
if (isRecording.value && isRecognizing.value) {
|
|
startRecording();
|
|
} else if (!isRecording.value) {
|
|
stopRecording();
|
|
} else {}
|
|
}
|
|
|
|
Future<void> startRecording() async {
|
|
//isRecording.value = true;
|
|
_seconds = 0;
|
|
recordDurationText.value = '00:00';
|
|
_recordTimer = Timer.periodic(Duration(seconds: 1), (_) {
|
|
_seconds++;
|
|
final minutes = (_seconds ~/ 60).toString().padLeft(2, '0');
|
|
final seconds = (_seconds % 60).toString().padLeft(2, '0');
|
|
recordDurationText.value = '$minutes:$seconds';
|
|
});
|
|
|
|
// 启动实际录音逻辑
|
|
// ...
|
|
final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
|
|
await _asrService.enableRecord(
|
|
"${dir.path}/${currentModeTitle.value.tr}_$formattedTime.wav");
|
|
// await _astService.enableRecord(
|
|
// "${dir.path}/${currentModeTitle.value.tr}_${formattedTime}_mic.wav");
|
|
return;
|
|
}
|
|
|
|
Future<void> stopRecording() async {
|
|
_recordTimer?.cancel();
|
|
_recordTimer = null;
|
|
isRecording.value = false;
|
|
_isTimerPaused = false;
|
|
recordDurationText.value = '00:00';
|
|
_seconds = 0;
|
|
|
|
_asrService.stopRecord(true);
|
|
lasyIsRecording.value = false;
|
|
Logger.info('录音已停止,计时器已重置');
|
|
}
|
|
|
|
// 开始语音识别
|
|
Future<void> startRecognition() async {
|
|
Logger.info('开始语音识别');
|
|
// 等待权限请求完成并获取结果
|
|
final bool record = await requestRecordPermission();
|
|
|
|
// 如果权限被拒绝,直接返回
|
|
if (!record) {
|
|
Logger.d("Permission", "录音权限被拒绝,无法开始录音");
|
|
return;
|
|
}
|
|
|
|
if (isRecognizing.value) return;
|
|
|
|
try {
|
|
// 每次开始识别前检查 ASR 服务是否已配置为当前语言对
|
|
final List<String> currentLanguages = [
|
|
sourceLanguageCode.value,
|
|
targetLanguageCode.value
|
|
];
|
|
final bool needsReinit =
|
|
!_asrService.supportedLanguages.toSet().containsAll(currentLanguages);
|
|
|
|
if (needsReinit) {
|
|
Logger.info('识别开始前发现语言变更,重新初始化ASR服务');
|
|
await _reinitializeAsrService();
|
|
}
|
|
|
|
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
|
|
|
|
// 开始新的翻译会话统计
|
|
try {
|
|
_usageService.startNewSession();
|
|
_asrSessionStartTime = DateTime.now(); // 记录ASR会话开始时间
|
|
_asrActiveDuration = Duration.zero; // 重置ASR活跃时长
|
|
_isAsrActive = false; // 重置ASR活跃状态
|
|
} catch (e) {
|
|
Logger.error('开始新会话统计失败: $e');
|
|
}
|
|
|
|
// 开始连续语音识别
|
|
if (currentMode.value == "audioVideo") {
|
|
bleManager.openDecoder();
|
|
// 发送ble音乐或者通话远端声音
|
|
_audioSourceType = true;
|
|
isTtsEnabled.value = false; // 禁用TTS以避免干扰
|
|
Logger.info('发送ble音乐或者通话远端声音');
|
|
} else if (currentMode.value == "call") {
|
|
// 发送ble系统mic和dac(音乐或者通话远端)声音
|
|
bleManager.openA2DPDecoder();
|
|
_audioSourceType = true;
|
|
isTtsEnabled.value = false;
|
|
await _astService.startContinuousTranslation();
|
|
// await _astService.path("${dir.path}/8_mic.wav");
|
|
Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音');
|
|
} else {
|
|
// 开始连续语音识别
|
|
_audioSourceType = false;
|
|
isTtsEnabled.value = true;
|
|
}
|
|
|
|
if (_bluetoothManager.currentDeviceRx.value != null) {
|
|
restoreOriginalAudioState();
|
|
} else {
|
|
disableBluetoothAudio();
|
|
}
|
|
await _asrService.startContinuousRecognition(_audioSourceType);
|
|
// 开始ASR活跃时长计时
|
|
_startAsrActiveTracking();
|
|
isRecognizing.value = true;
|
|
if (isRecording.value && isRecognizing.value) {
|
|
startRecording();
|
|
}
|
|
} catch (e) {
|
|
isRecognizing.value = false;
|
|
Logger.error('启动语音识别失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
// 处理识别事件
|
|
void _handleRecognitionEvent(RecognitionEvent event) {
|
|
if (event.type == RecognitionEventType.finalResult &&
|
|
event.text.isNotEmpty) {
|
|
detectedLanguageCode = event.detectedLanguage;
|
|
print("处理识别事件:${event.text}");
|
|
handleFinalResult(event.text);
|
|
} else if (event.type == RecognitionEventType.intermediateResult &&
|
|
event.text.isNotEmpty) {
|
|
if (event.detectedLanguage.isNotEmpty) {
|
|
detectedLanguageCode = event.detectedLanguage;
|
|
}
|
|
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) {
|
|
isRecognizing.value = false;
|
|
Logger.error('语音识别错误: ${event.error}');
|
|
} else if (event.type == RecognitionEventType.canceled) {
|
|
isRecognizing.value = false;
|
|
Logger.error('语音识别取消: ${event.error}');
|
|
}
|
|
}
|
|
|
|
// 停止语音识别
|
|
Future<void> stopRecognition() async {
|
|
if (!isRecognizing.value) return;
|
|
|
|
try {
|
|
await stopRecording();
|
|
await _asrService.stopContinuousRecognition();
|
|
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()}');
|
|
}
|
|
}
|
|
|
|
// 处理中间识别结果
|
|
void handleIntermediateResult(String text) {
|
|
if (text.isEmpty) return;
|
|
|
|
if (translationHistory.isEmpty || !translationHistory.last.isIntermediate) {
|
|
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
|
|
|
|
final newItem = TranslationItem(
|
|
sourceText: text,
|
|
translatedText: '',
|
|
sourceLanguageCode: sourceLanguageCode.value,
|
|
targetLanguageCode: targetLanguageCode.value,
|
|
timestamp: DateTime.now(),
|
|
sessionId: currentSessionId!,
|
|
isFirstInSession: translationHistory.isEmpty ||
|
|
translationHistory.last.sessionId != currentSessionId,
|
|
isIntermediate: true,
|
|
);
|
|
// 添加新项后滚动到底部
|
|
if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) {
|
|
_asrService.stopContinuousRecognition();
|
|
} else {
|
|
translationHistory.add(newItem);
|
|
}
|
|
_scrollToBottom();
|
|
} else {
|
|
// 更新当前项
|
|
|
|
translationHistory.last.sourceText = text;
|
|
translationHistory.refresh();
|
|
// 更新后滚动到底部
|
|
_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);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 处理最终识别结果
|
|
Future<void> 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: '',
|
|
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();
|
|
} //else {
|
|
translationHistory.add(newItem);
|
|
translationHistory.refresh();
|
|
_scrollToBottom();
|
|
saveTranslationHistory();
|
|
// 使用新项目的时间戳进行翻译
|
|
translateText(text, newItem.timestamp, isFinal: true);
|
|
// }
|
|
}
|
|
}
|
|
|
|
// 翻译文本
|
|
Future<String?> translateText(String sourceText, DateTime timestamp,
|
|
{bool isFinal = false}) async {
|
|
if (sourceText.isEmpty) return null;
|
|
|
|
isTranslating.value = true;
|
|
String? translationResult;
|
|
|
|
try {
|
|
String detectedSourceLanguageCode;
|
|
String detectedTargetLanguageCode;
|
|
if ((currentMode.value == 'faceToFace' && activeSpeaker.value == 1) ||
|
|
(currentMode.value == 'faceToFace' &&
|
|
activeSpeaker.value == 0 &&
|
|
lastActiveSpeaker.value == 1)) {
|
|
detectedLanguageCode = sourceLanguageCode.value;
|
|
detectedSourceLanguageCode = sourceLanguageCode.value;
|
|
detectedTargetLanguageCode = targetLanguageCode.value;
|
|
} else if ((currentMode.value == 'faceToFace' &&
|
|
activeSpeaker.value == 2) ||
|
|
(currentMode.value == 'faceToFace' &&
|
|
activeSpeaker.value == 0 &&
|
|
lastActiveSpeaker.value == 2)) {
|
|
// 根据检测到的语言确定源语言和目标语言
|
|
detectedLanguageCode = targetLanguageCode.value;
|
|
detectedSourceLanguageCode = targetLanguageCode.value;
|
|
detectedTargetLanguageCode = sourceLanguageCode.value;
|
|
} else {
|
|
// 根据检测到的语言确定源语言和目标语言
|
|
final bool shouldSwap =
|
|
detectedLanguageCode == sourceLanguageCode.value;
|
|
detectedSourceLanguageCode =
|
|
shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value;
|
|
detectedTargetLanguageCode =
|
|
shouldSwap ? targetLanguageCode.value : sourceLanguageCode.value;
|
|
}
|
|
// 调用翻译服务
|
|
translationResult = await _translationService.translateText(
|
|
text: sourceText,
|
|
sourceLanguageCode: detectedSourceLanguageCode,
|
|
targetLanguageCode: detectedTargetLanguageCode,
|
|
);
|
|
|
|
/// }
|
|
if (translationResult != null) {
|
|
// 查找匹配的项目 - 优先使用timestamp查找
|
|
int index = -1;
|
|
|
|
index = translationHistory
|
|
.indexWhere((item) => item.timestamp.isAtSameMomentAs(timestamp));
|
|
if (index != -1) {
|
|
// 更新历史项
|
|
translationHistory[index].translatedText = translationResult;
|
|
translationHistory[index].sourceLanguageCode =
|
|
detectedSourceLanguageCode;
|
|
translationHistory[index].targetLanguageCode =
|
|
detectedTargetLanguageCode;
|
|
translationHistory.refresh();
|
|
|
|
// 【改进】只有在最终结果时才保存,避免中间态时的频繁保存
|
|
if (isFinal) {
|
|
saveTranslationHistory();
|
|
|
|
// 记录翻译API调用统计 - 优化版(精确计费参数)
|
|
try {
|
|
final sourceLanguageName = _languageManager
|
|
.getChineseNameByAsrCode(detectedSourceLanguageCode) ??
|
|
'未知';
|
|
final targetLanguageName = _languageManager
|
|
.getChineseNameByAsrCode(detectedTargetLanguageCode) ??
|
|
'未知';
|
|
|
|
// 使用精确的计费参数:只统计源文本字符数(翻译API的实际计费基础)
|
|
_usageService.recordTranslationApiCall(
|
|
sourceText: sourceText, // 只有源文本用于翻译API计费
|
|
targetText: translationResult, // 目标文本用于TTS计费
|
|
sourceLanguage: sourceLanguageName,
|
|
targetLanguage: targetLanguageName,
|
|
mode: currentMode.value,
|
|
);
|
|
|
|
Logger.info(
|
|
'翻译统计: 源文本${sourceText.length}字符, 模式${currentMode.value}, 语言对$sourceLanguageName→$targetLanguageName');
|
|
|
|
// 记录ASR统计(在翻译完成时统计ASR使用情况)
|
|
_recordAsrUsageStats(sourceLanguageName);
|
|
} catch (e) {
|
|
Logger.error('记录翻译统计失败: $e');
|
|
}
|
|
}
|
|
|
|
// 更新后滚动到底部
|
|
_scrollToBottom();
|
|
// 播放TTS - 只在最终结果时播放
|
|
if (isFinal) {
|
|
if (isTtsEnabled.value && currentMode.value != 'faceToFace') {
|
|
await playTranslatedText(translationResult);
|
|
} else {
|
|
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 = '';
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Logger.info('未找到匹配的历史项 $sourceText ${timestamp.toIso8601String()}');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Logger.error('翻译失败: ${e.toString()}');
|
|
} finally {
|
|
isTranslating.value = false;
|
|
}
|
|
|
|
return translationResult;
|
|
}
|
|
|
|
// 播放翻译文本
|
|
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(text);
|
|
final endTime = DateTime.now();
|
|
|
|
// 记录TTS API调用统计 - 优化版(基于文本字符数计费)
|
|
try {
|
|
final durationSeconds = endTime.difference(startTime).inSeconds;
|
|
final languageName = _languageManager
|
|
.getChineseNameByAsrCode(detectedTargetLanguageCode) ??
|
|
'未知';
|
|
|
|
// TTS的主要计费依据是文本字符数,播放时长仅用于分析
|
|
_usageService.recordTtsApiCall(
|
|
text: text, // 文本字符数是TTS计费的主要依据
|
|
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 {
|
|
if (currentMode.value == "audioVideo" || currentMode.value == "call") {
|
|
bleManager.closeCodec();
|
|
// 发送ble音乐或者通话远端声音
|
|
Logger.info('发送ble音乐或者通话远端声音');
|
|
}
|
|
if (isRecognizing.value) {
|
|
await stopRecognition();
|
|
}
|
|
// 确保停止ASR活跃时长跟踪
|
|
_stopAsrActiveTracking();
|
|
_translationDebounceTimer?.cancel();
|
|
await _ttsService.stop();
|
|
} catch (e) {
|
|
Logger.error('停止操作失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
// 播放特定翻译项
|
|
Future<void> playTranslationItem(TranslationItem item) async {
|
|
if (item.translatedText.isEmpty) return;
|
|
|
|
try {
|
|
final voiceName =
|
|
_languageManager.getTtsVoiceNameByAsrCode(item.targetLanguageCode) ??
|
|
'en-US-AriaNeural';
|
|
await _ttsService.setVoice(voiceName);
|
|
await _ttsService.speakOnce(item.translatedText);
|
|
} catch (e) {
|
|
Logger.error('播放翻译项失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
// 播放文本
|
|
Future<void> playText(String text, {String? languageCode}) async {
|
|
if (text.isEmpty) return;
|
|
|
|
try {
|
|
final voiceName = languageCode != null
|
|
? _languageManager.getTtsVoiceNameByAsrCode(languageCode) ??
|
|
'en-US-AriaNeural'
|
|
: _languageManager
|
|
.getTtsVoiceNameByAsrCode(targetLanguageCode.value) ??
|
|
'en-US-AriaNeural';
|
|
|
|
await _ttsService.setVoice(voiceName);
|
|
await _ttsService.speakOnce(text);
|
|
} catch (e) {
|
|
Logger.error('播放文本失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
// 滚动到底部(适配reverse模式)
|
|
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) {
|
|
Logger.error('滚动到顶部失败(reverse模式): ${e.toString()}');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 设置滚动监听器(简化版,不再需要实际监听)
|
|
void setupScrollListener() {
|
|
// 简化后的空方法,保留接口兼容性
|
|
}
|
|
|
|
// 底部控制栏高度
|
|
double _bottomBarHeight = 0.0;
|
|
|
|
// 设置底部控制栏高度
|
|
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();
|
|
|
|
// 更新ASR支持的语言并重新初始化
|
|
_reinitializeAsrService();
|
|
}
|
|
|
|
// 设置源语言
|
|
void setSourceLanguage(String language) {
|
|
if (sourceLanguage.value != language) {
|
|
stopAll();
|
|
|
|
sourceLanguage.value = language;
|
|
|
|
final asrCode = _languageManager.getAsrCodeByChineseName(language);
|
|
if (asrCode != null) {
|
|
sourceLanguageCode.value = asrCode;
|
|
}
|
|
|
|
// 保存语言设置
|
|
_saveLanguageSettings();
|
|
|
|
// 更新ASR支持的语言并重新初始化
|
|
_reinitializeAsrService();
|
|
}
|
|
}
|
|
|
|
// 设置目标语言
|
|
void setTargetLanguage(String language) {
|
|
if (targetLanguage.value != language) {
|
|
stopAll();
|
|
|
|
targetLanguage.value = language;
|
|
|
|
final asrCode = _languageManager.getAsrCodeByChineseName(language);
|
|
if (asrCode != null) {
|
|
targetLanguageCode.value = asrCode;
|
|
}
|
|
|
|
// 保存语言设置
|
|
_saveLanguageSettings();
|
|
|
|
// 更新ASR支持的语言并重新初始化
|
|
_reinitializeAsrService();
|
|
}
|
|
}
|
|
|
|
// 重新初始化ASR服务以支持新的语言
|
|
Future<void> _reinitializeAsrService() async {
|
|
try {
|
|
// 停止当前识别
|
|
await stopRecognition();
|
|
|
|
// 更新ASR支持的语言
|
|
final List<String> asrSupportedLanguages = [
|
|
sourceLanguageCode.value,
|
|
targetLanguageCode.value
|
|
];
|
|
|
|
Logger.info('重新初始化ASR服务,支持语言: $asrSupportedLanguages');
|
|
|
|
// 重新初始化ASR服务
|
|
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服务重新初始化成功');
|
|
} catch (e) {
|
|
Logger.error('重新初始化ASR服务失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
// 加载翻译历史
|
|
void loadTranslationHistory() {
|
|
final historyJson = _storage.read(_historyKey);
|
|
if (historyJson != null) {
|
|
try {
|
|
final List<dynamic> historyList = jsonDecode(historyJson);
|
|
final List<TranslationItem> allHistory =
|
|
historyList.map((item) => TranslationItem.fromJson(item)).toList();
|
|
|
|
// 【注意】存储中的记录都是完成态记录(isIntermediate = false)
|
|
// 因为保存时已经过滤掉了中间态记录,确保数据质量
|
|
|
|
// 初始化分页状态
|
|
currentPage.value = 1;
|
|
isLoadingMore.value = false;
|
|
isLoadingMoreError.value = false;
|
|
|
|
// 主页面只显示最近的20条完成态记录,保证界面性能
|
|
if (allHistory.length > _maxDisplayItems) {
|
|
translationHistory.value =
|
|
allHistory.sublist(allHistory.length - _maxDisplayItems);
|
|
// 检查是否还有更多历史记录
|
|
hasMoreHistory.value = true;
|
|
} else {
|
|
translationHistory.value = allHistory;
|
|
hasMoreHistory.value = false;
|
|
}
|
|
|
|
// 设置滚动监听器
|
|
_setupScrollListener();
|
|
|
|
// 🔑 reverse模式下不需要滚动到底部,ListView会自然显示最新记录
|
|
// 参考Agent模块:使用reverse:true时不需要手动滚动到底部
|
|
} catch (e) {
|
|
_storage.remove(_historyKey);
|
|
Logger.error('加载翻译历史失败: ${e.toString()}');
|
|
}
|
|
} else {
|
|
// 没有历史数据时也要初始化分页状态
|
|
hasMoreHistory.value = false;
|
|
currentPage.value = 1;
|
|
isLoadingMore.value = false;
|
|
isLoadingMoreError.value = false;
|
|
}
|
|
}
|
|
|
|
// 保存翻译历史 - 回归原始简单实现
|
|
// 【修复】完全回归原始保存逻辑,只保存当前内存中的完成态记录
|
|
void saveTranslationHistory() {
|
|
try {
|
|
// 只保存当前内存中的完成态记录,过滤掉中间态记录
|
|
final List<TranslationItem> finalRecords =
|
|
translationHistory.where((item) => !item.isIntermediate).toList();
|
|
|
|
// 应用存储限制,保留最新的1000条完成态记录
|
|
List<TranslationItem> historyToSave = finalRecords;
|
|
if (finalRecords.length > _maxStorageItems) {
|
|
historyToSave =
|
|
finalRecords.sublist(finalRecords.length - _maxStorageItems);
|
|
}
|
|
|
|
// 保存到本地存储
|
|
final List<Map<String, dynamic>> historyJson =
|
|
historyToSave.map((item) => item.toJson()).toList();
|
|
_storage.write(_historyKey, jsonEncode(historyJson));
|
|
|
|
Logger.info('翻译历史保存成功: 存储${historyToSave.length}条记录');
|
|
} catch (e) {
|
|
Logger.error('保存翻译历史失败: ${e.toString()}');
|
|
}
|
|
}
|
|
|
|
// 设置滚动监听器(适配reverse模式)
|
|
void _setupScrollListener() {
|
|
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();
|
|
}
|
|
}
|
|
});
|
|
|
|
_scrollListenerSetup = true;
|
|
}
|
|
|
|
// 加载更多历史记录(分页加载)
|
|
Future<void> loadMoreHistory() async {
|
|
// 防止重复加载
|
|
if (isLoadingMore.value || !hasMoreHistory.value) {
|
|
Logger.d(TAG,
|
|
'跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreHistory=${hasMoreHistory.value}');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
isLoadingMore.value = true;
|
|
isLoadingMoreError.value = false;
|
|
|
|
final nextPage = currentPage.value + 1;
|
|
Logger.i(TAG, '正在加载第 $nextPage 页翻译历史...');
|
|
|
|
// 从存储中获取所有历史记录
|
|
final historyJson = _storage.read(_historyKey);
|
|
if (historyJson != null) {
|
|
final List<dynamic> historyList = jsonDecode(historyJson);
|
|
final List<TranslationItem> allHistory =
|
|
historyList.map((item) => TranslationItem.fromJson(item)).toList();
|
|
|
|
// 计算分页参数
|
|
final totalItems = allHistory.length;
|
|
final totalPages = (totalItems / pageSize).ceil();
|
|
|
|
// 修复分页索引计算:从最新记录向前分页
|
|
// 第1页:最新20条 (index: totalItems-20 到 totalItems)
|
|
// 第2页:接下来20条 (index: totalItems-40 到 totalItems-20)
|
|
// 第3页:再接下来20条 (index: totalItems-60 到 totalItems-40)
|
|
final endIndex = totalItems - ((nextPage - 1) * pageSize);
|
|
final startIndex = totalItems - (nextPage * pageSize);
|
|
|
|
Logger.d(TAG,
|
|
'分页计算: 总记录=$totalItems, 总页数=$totalPages, 当前页=$nextPage, startIndex=$startIndex, endIndex=$endIndex');
|
|
|
|
if (endIndex > 0) {
|
|
// 获取当前页的数据,确保索引不越界
|
|
final actualStartIndex = startIndex < 0 ? 0 : startIndex;
|
|
final actualEndIndex = endIndex > totalItems ? totalItems : endIndex;
|
|
|
|
final pageItems =
|
|
allHistory.sublist(actualStartIndex, actualEndIndex);
|
|
Logger.d(TAG,
|
|
'实际索引: start=$actualStartIndex, end=$actualEndIndex, 获取${pageItems.length}条记录');
|
|
|
|
if (pageItems.isNotEmpty) {
|
|
// 🔑 简化延迟,参考Agent模块(300ms vs 1000ms)
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
|
|
|
// 🔑 reverse模式下:将新记录插入到现有记录列表的开头
|
|
// 由于reverse显示,用户看到的顺序不会改变,无需复杂的位置保持
|
|
final updatedHistory = [...pageItems, ...translationHistory];
|
|
translationHistory.assignAll(updatedHistory);
|
|
|
|
// 更新分页状态
|
|
currentPage.value = nextPage;
|
|
// 修复hasMoreHistory判断:当startIndex <= 0时表示已经到达最早的记录
|
|
hasMoreHistory.value = startIndex > 0;
|
|
|
|
Logger.i(TAG,
|
|
'成功加载 ${pageItems.length} 条历史记录,当前页: $nextPage/$totalPages, 还有更多: ${hasMoreHistory.value}');
|
|
} else {
|
|
// 没有更多记录
|
|
hasMoreHistory.value = false;
|
|
Logger.i(TAG, '没有更多翻译历史记录');
|
|
}
|
|
} else {
|
|
// 索引超出范围,没有更多记录
|
|
hasMoreHistory.value = false;
|
|
Logger.i(TAG, '已到达历史记录开头');
|
|
}
|
|
} else {
|
|
// 没有存储数据
|
|
hasMoreHistory.value = false;
|
|
Logger.i(TAG, '存储中没有更多历史记录');
|
|
}
|
|
} catch (e) {
|
|
Logger.e(TAG, '加载更多历史记录失败: $e');
|
|
isLoadingMoreError.value = true;
|
|
} finally {
|
|
isLoadingMore.value = false;
|
|
}
|
|
}
|
|
|
|
// 获取所有完成态历史记录(供历史页面使用)
|
|
// 【修复】简化实现,合并存储和内存中的完成态记录
|
|
List<TranslationItem> getAllHistory() {
|
|
try {
|
|
final List<TranslationItem> result = <TranslationItem>[];
|
|
final Set<String> existingIds = <String>{};
|
|
|
|
// 添加存储的历史记录(存储中的记录都是完成态)
|
|
final historyJson = _storage.read(_historyKey);
|
|
if (historyJson != null) {
|
|
final List<dynamic> historyList = jsonDecode(historyJson);
|
|
final List<TranslationItem> storedHistory =
|
|
historyList.map((item) => TranslationItem.fromJson(item)).toList();
|
|
|
|
for (final item in storedHistory) {
|
|
final id =
|
|
'${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}';
|
|
if (!existingIds.contains(id)) {
|
|
existingIds.add(id);
|
|
result.add(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 添加当前内存中的完成态记录(排除中间态记录)
|
|
for (final item in translationHistory) {
|
|
if (!item.isIntermediate) {
|
|
final id =
|
|
'${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}';
|
|
if (!existingIds.contains(id)) {
|
|
existingIds.add(id);
|
|
result.add(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 按时间排序
|
|
result.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
|
return result;
|
|
} catch (e) {
|
|
Logger.error('获取完整历史记录失败: ${e.toString()}');
|
|
// 发生错误时,返回当前内存中的完成态记录
|
|
return translationHistory.where((item) => !item.isIntermediate).toList();
|
|
}
|
|
}
|
|
|
|
// 清除翻译历史
|
|
void clearTranslationHistory() {
|
|
translationHistory.clear();
|
|
_storage.remove(_historyKey);
|
|
}
|
|
|
|
// 删除指定会话的所有翻译记录
|
|
void deleteSessionHistory(String sessionId) {
|
|
try {
|
|
// 1. 从内存中删除该会话的所有记录
|
|
final originalCount = translationHistory.length;
|
|
translationHistory.removeWhere((item) => item.sessionId == sessionId);
|
|
final removedFromMemory = originalCount - translationHistory.length;
|
|
|
|
// 2. 从本地存储中删除该会话的记录
|
|
final historyJson = _storage.read(_historyKey);
|
|
int removedFromStorage = 0;
|
|
|
|
if (historyJson != null) {
|
|
final List<dynamic> historyList = jsonDecode(historyJson);
|
|
final List<TranslationItem> storedHistory =
|
|
historyList.map((item) => TranslationItem.fromJson(item)).toList();
|
|
|
|
final originalStoredCount = storedHistory.length;
|
|
storedHistory.removeWhere((item) => item.sessionId == sessionId);
|
|
removedFromStorage = originalStoredCount - storedHistory.length;
|
|
|
|
// 重新保存更新后的存储数据
|
|
final List<Map<String, dynamic>> updatedHistoryJson =
|
|
storedHistory.map((item) => item.toJson()).toList();
|
|
_storage.write(_historyKey, jsonEncode(updatedHistoryJson));
|
|
}
|
|
|
|
// 3. 刷新UI显示
|
|
translationHistory.refresh();
|
|
|
|
Logger.info(
|
|
'删除会话历史成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条');
|
|
} catch (e) {
|
|
Logger.error('删除会话历史失败: ${e.toString()}');
|
|
rethrow; // 重新抛出异常,让调用方处理
|
|
}
|
|
}
|
|
|
|
// 删除指定的单条翻译记录
|
|
void deleteTranslationItem(String sessionId, DateTime timestamp) {
|
|
try {
|
|
// 1. 从内存中删除指定的翻译记录
|
|
final originalCount = translationHistory.length;
|
|
translationHistory.removeWhere((item) =>
|
|
item.sessionId == sessionId &&
|
|
item.timestamp.millisecondsSinceEpoch ==
|
|
timestamp.millisecondsSinceEpoch);
|
|
final removedFromMemory = originalCount - translationHistory.length;
|
|
|
|
// 2. 从本地存储中删除指定的翻译记录
|
|
final historyJson = _storage.read(_historyKey);
|
|
int removedFromStorage = 0;
|
|
|
|
if (historyJson != null) {
|
|
final List<dynamic> historyList = jsonDecode(historyJson);
|
|
final List<TranslationItem> storedHistory =
|
|
historyList.map((item) => TranslationItem.fromJson(item)).toList();
|
|
|
|
final originalStoredCount = storedHistory.length;
|
|
storedHistory.removeWhere((item) =>
|
|
item.sessionId == sessionId &&
|
|
item.timestamp.millisecondsSinceEpoch ==
|
|
timestamp.millisecondsSinceEpoch);
|
|
removedFromStorage = originalStoredCount - storedHistory.length;
|
|
|
|
// 重新保存更新后的存储数据
|
|
final List<Map<String, dynamic>> updatedHistoryJson =
|
|
storedHistory.map((item) => item.toJson()).toList();
|
|
_storage.write(_historyKey, jsonEncode(updatedHistoryJson));
|
|
}
|
|
|
|
// 3. 刷新UI显示
|
|
translationHistory.refresh();
|
|
|
|
Logger.info(
|
|
'删除单条翻译记录成功: sessionId=$sessionId, timestamp=${timestamp.toIso8601String()}, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条');
|
|
} catch (e) {
|
|
Logger.error('删除单条翻译记录失败: ${e.toString()}');
|
|
rethrow; // 重新抛出异常,让调用方处理
|
|
}
|
|
}
|
|
|
|
// 滚动到底部 (公共方法,用于向后兼容)
|
|
void scrollToBottom({bool animate = true}) {
|
|
_scrollToBottom(animate: animate);
|
|
}
|
|
|
|
/// 开始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使用统计 - 基于精确时长跟踪
|
|
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');
|
|
} catch (e) {
|
|
Logger.error('记录ASR统计失败: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 翻译项模型
|
|
class TranslationItem {
|
|
String sourceText;
|
|
String translatedText;
|
|
String sourceLanguageCode;
|
|
String targetLanguageCode;
|
|
final DateTime timestamp;
|
|
String sessionId;
|
|
final bool isFirstInSession;
|
|
bool isIntermediate; // 是否为中间结果
|
|
|
|
TranslationItem({
|
|
required this.sourceText,
|
|
required this.translatedText,
|
|
required this.sourceLanguageCode,
|
|
required this.targetLanguageCode,
|
|
required this.timestamp,
|
|
required this.sessionId,
|
|
this.isFirstInSession = false,
|
|
this.isIntermediate = false,
|
|
});
|
|
|
|
factory TranslationItem.fromJson(Map<String, dynamic> json) {
|
|
DateTime timestamp;
|
|
try {
|
|
timestamp = DateTime.parse(json['timestamp']);
|
|
} catch (e) {
|
|
timestamp = DateTime.now();
|
|
}
|
|
|
|
return TranslationItem(
|
|
sourceText: json['sourceText'] ?? '',
|
|
translatedText: json['translatedText'] ?? '',
|
|
sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN',
|
|
targetLanguageCode: json['targetLanguageCode'] ?? 'en-US',
|
|
timestamp: timestamp,
|
|
sessionId:
|
|
json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
|
isFirstInSession: json['isFirstInSession'] ?? false,
|
|
isIntermediate: false, // 从存储加载的都是最终结果
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'sourceText': sourceText,
|
|
'translatedText': translatedText,
|
|
'sourceLanguageCode': sourceLanguageCode,
|
|
'targetLanguageCode': targetLanguageCode,
|
|
'timestamp': timestamp.toIso8601String(),
|
|
'sessionId': sessionId,
|
|
'isFirstInSession': isFirstInSession,
|
|
// 不保存isIntermediate,因为存储的都是最终结果
|
|
};
|
|
}
|
|
|
|
// 获取源语言的中文名称
|
|
String get sourceLanguageName {
|
|
final languageManager = Get.find<LanguageManager>();
|
|
return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ??
|
|
'未知语言';
|
|
}
|
|
|
|
// 获取目标语言的中文名称
|
|
String get targetLanguageName {
|
|
final languageManager = Get.find<LanguageManager>();
|
|
return languageManager.getChineseNameByAsrCode(targetLanguageCode) ??
|
|
'未知语言';
|
|
}
|
|
}
|
|
|