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.

563 lines
18 KiB

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'dart:async';
import 'dart:convert';
import 'package:get_storage/get_storage.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/volcano_translation_service.dart';
import '../../../data/services/volcano_tts_api_service.dart';
// import '../../../core/utils/logger.dart';
class TranslationController extends GetxController {
// Services
final AzureAsrService _asrService = Get.find<AzureAsrService>();
final VolcanoTranslationService _translationService = Get.find<VolcanoTranslationService>();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
final GetStorage _storage = GetStorage();
// Storage key
static const String _historyKey = 'translation_history';
static const int _maxHistoryItems = 100; // 最多保存100条历史记录
// State variables
final isRecognizing = false.obs;
final isTranslating = false.obs;
final isTtsEnabled = true.obs;
final recognizedText = ''.obs;
final translatedText = ''.obs;
final sourceLanguage = '中文 (Chinese)'.obs;
final targetLanguage = '英语 (English)'.obs;
final translationHistory = <TranslationItem>[].obs;
// 支持的语言列表 - 改为Map结构,键为显示名称,值为语言代码
final Map<String, String> supportedLanguages = {
'中文 (Chinese)': 'zh',
'英语 (English)': 'en',
'日语 (Japanese)': 'ja',
'韩语 (Korean)': 'ko',
'法语 (French)': 'fr',
'德语 (German)': 'de',
'西班牙语 (Spanish)': 'es',
'意大利语 (Italian)': 'it',
'俄语 (Russian)': 'ru',
'泰语 (Thai)': 'th',
'葡萄牙语 (Portuguese)': 'pt',
'阿拉伯语 (Arabic)': 'ar',
'越南语 (Vietnamese)': 'vi',
'印尼语 (Indonesian)': 'id',
'印地语 (Hindi)': 'hi',
'土耳其语 (Turkish)': 'tr',
'波兰语 (Polish)': 'pl',
'荷兰语 (Dutch)': 'nl',
'瑞典语 (Swedish)': 'sv',
'捷克语 (Czech)': 'cs',
'希腊语 (Greek)': 'el',
'罗马尼亚语 (Romanian)': 'ro',
'匈牙利语 (Hungarian)': 'hu',
'乌克兰语 (Ukrainian)': 'uk',
'保加利亚语 (Bulgarian)': 'bg',
'丹麦语 (Danish)': 'da',
'芬兰语 (Finnish)': 'fi',
'挪威语 (Norwegian)': 'no',
'马来语 (Malay)': 'ms',
};
// Current translation item
TranslationItem? currentItem;
// Subscription for recognition stream
StreamSubscription? _recognitionSubscription;
// Scroll controller for ListView
final ScrollController scrollController = ScrollController();
// Timer for debouncing translation requests
Timer? _translationDebounceTimer;
// 当前会话ID
String? currentSessionId;
@override
void onInit() {
super.onInit();
// 加载历史记录
loadTranslationHistory();
// Initialize the translation service
_initTranslationService();
// 添加后处理,确保在历史记录加载后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
// 延迟执行滚动操作,确保视图已完全渲染
Future.delayed(const Duration(milliseconds: 300), () {
_scrollToBottom();
});
});
// Start voice recognition automatically when the page is loaded
startRecognition();
}
@override
void onClose() {
stopRecognition();
_recognitionSubscription?.cancel();
_translationDebounceTimer?.cancel();
scrollController.dispose();
super.onClose();
}
// Initialize the translation service
Future<void> _initTranslationService() async {
try {
await _translationService.initialize();
} catch (e) {
Get.snackbar(
'Error',
'Failed to initialize translation service: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
}
}
// Toggle TTS feature
void toggleTts() {
isTtsEnabled.toggle();
}
// Start voice recognition
Future<void> startRecognition() async {
if (isRecognizing.value) return;
try {
// 生成新的会话ID
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
// Initialize a new translation item
currentItem = TranslationItem(
sourceText: '',
translatedText: '',
sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value,
timestamp: DateTime.now(),
sessionId: currentSessionId!,
isFirstInSession: true, // 标记为会话中的第一条
);
// Start the voice recognition service
final success = await _asrService.startContinuousRecognition();
if (success) {
isRecognizing.value = true;
// Reset the recognized text
recognizedText.value = '';
translatedText.value = '';
// Listen for recognition events
_recognitionSubscription = _asrService.recognitionStream?.listen((event) {
switch (event.type) {
case RecognitionEventType.finalResult:
if (event.text.isNotEmpty) {
handleFinalResult(event.text);
}
break;
case RecognitionEventType.recognizing:
// Update the UI with intermediate results
if (event.text.isNotEmpty) {
handleIntermediateResult(event.text);
}
break;
case RecognitionEventType.error:
Get.snackbar(
'Error',
'Recognition error: ${event.error}',
snackPosition: SnackPosition.BOTTOM,
);
break;
default:
break;
}
});
} else {
throw Exception('Failed to start continuous recognition');
}
} catch (e) {
isRecognizing.value = false;
Get.snackbar(
'Error',
'Failed to start voice recognition: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
}
}
// Stop voice recognition
Future<void> stopRecognition() async {
if (!isRecognizing.value) return;
try {
await _asrService.stopContinuousRecognition();
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
isRecognizing.value = false;
// 添加当前项到历史(如果有内容)
finalizeCurrentItem();
// 重置当前会话ID
currentSessionId = null;
// 重置当前显示的文本
recognizedText.value = '';
translatedText.value = '';
} catch (e) {
Get.snackbar(
'Error',
'Failed to stop voice recognition: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
}
}
// Handle intermediate recognition result
void handleIntermediateResult(String text) {
if (text.isEmpty) return;
// 如果当前没有活跃的翻译项,创建一个新的
if (currentItem == null) {
// 如果没有当前会话ID,创建一个新的
if (currentSessionId == null) {
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
}
currentItem = TranslationItem(
sourceText: text,
translatedText: '',
sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value,
timestamp: DateTime.now(),
sessionId: currentSessionId!,
isFirstInSession: translationHistory.isEmpty ||
translationHistory.last.sessionId != currentSessionId,
);
} else {
// 更新当前翻译项
currentItem!.sourceText = text;
}
// 实时更新识别文本
recognizedText.value = text;
// Debounce translation requests to avoid too many API calls
_translationDebounceTimer?.cancel();
_translationDebounceTimer = Timer(const Duration(milliseconds: 300), () {
translateText(text, isFinal: false);
});
// Scroll to the bottom to show the latest content
_scrollToBottom();
}
// Handle final recognition result
void handleFinalResult(String text) {
if (text.isEmpty) return;
// 更新当前翻译项(如果不存在则创建一个)
if (currentItem == null) {
// 如果没有当前会话ID,创建一个新的
if (currentSessionId == null) {
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
}
currentItem = TranslationItem(
sourceText: text,
translatedText: '',
sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value,
timestamp: DateTime.now(),
sessionId: currentSessionId!,
isFirstInSession: translationHistory.isEmpty ||
translationHistory.last.sessionId != currentSessionId,
);
} else {
currentItem!.sourceText = text;
}
// 更新识别文本
recognizedText.value = text;
// 翻译最终结果 (添加到历史的逻辑已移至translateText方法)
translateText(text, isFinal: true).then((_) {
// 作为安全措施,确保当前项被添加到历史中
// 这是为了处理translateText中可能因为条件不满足而没有添加到历史的情况
finalizeCurrentItem();
});
// Scroll to the bottom to show the latest content
_scrollToBottom();
}
// Finalize the current translation item and add it to history
void finalizeCurrentItem() {
if (currentItem != null &&
currentItem!.sourceText.isNotEmpty &&
currentItem!.translatedText.isNotEmpty &&
!translationHistory.contains(currentItem)) {
translationHistory.add(currentItem!);
saveTranslationHistory();
currentItem = null;
}
}
// Handle voice recognition result (legacy method, kept for compatibility)
void handleVoiceResult(String text) {
handleFinalResult(text);
}
// Translate the recognized text
Future<void> translateText(String text, {bool isFinal = true}) async {
if (text.isEmpty) return;
isTranslating.value = true;
// 保存当前的翻译项引用,确保异步返回时更新正确的项
final TranslationItem? itemToUpdate = currentItem;
try {
// 获取源语言和目标语言的代码
final String sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh';
// 尝试检测输入文本的语言(这里简化处理,实际项目可能需要更复杂的语言检测)
bool shouldSwapLanguages = false;
// 简单的语言检测逻辑 - 检查是否包含中文字符
final bool containsChinese = RegExp(r'[\u4e00-\u9fa5]').hasMatch(text);
final bool containsLatin = RegExp(r'[a-zA-Z]').hasMatch(text);
// 如果源语言是中文但文本不包含中文字符,且包含拉丁字符,可能需要交换语言
if (sourceCode == 'zh' && !containsChinese && containsLatin) {
shouldSwapLanguages = true;
}
// 如果源语言不是中文但文本包含中文字符,可能需要交换语言
else if (sourceCode != 'zh' && containsChinese) {
shouldSwapLanguages = true;
}
// 调用翻译服务
final result = await _translationService.translateText(
text: text,
sourceLanguage: shouldSwapLanguages ? targetLanguage.value : sourceLanguage.value,
targetLanguage: shouldSwapLanguages ? sourceLanguage.value : targetLanguage.value,
);
if (result != null) {
// 实时更新翻译文本
translatedText.value = result;
// 确保更新的是发起翻译请求时的翻译项,而不是当前可能已经变化的翻译项
if (itemToUpdate != null && itemToUpdate.sourceText == text) {
itemToUpdate.translatedText = result;
// 如果是最终结果,并且当前项仍然是发起请求时的项,则添加到历史
if (isFinal && itemToUpdate == currentItem) {
// 检查是否已经存在相同会话ID的项
bool isFirstInSession = true;
for (var item in translationHistory) {
if (item.sessionId == itemToUpdate.sessionId) {
isFirstInSession = false;
break;
}
}
// 创建一个新的TranslationItem,包含正确的isFirstInSession值
final updatedItem = TranslationItem(
sourceText: itemToUpdate.sourceText,
translatedText: itemToUpdate.translatedText,
sourceLanguage: itemToUpdate.sourceLanguage,
targetLanguage: itemToUpdate.targetLanguage,
timestamp: itemToUpdate.timestamp,
sessionId: itemToUpdate.sessionId,
isFirstInSession: isFirstInSession,
);
translationHistory.add(updatedItem);
saveTranslationHistory();
currentItem = null; // 重要:将当前项设为null,表示已完成
// 重置当前显示的文本,避免与历史项重复
recognizedText.value = '';
translatedText.value = '';
}
// Play TTS if enabled and this is a final result
if (isTtsEnabled.value && isFinal) {
playTranslatedText(result);
}
}
}
} catch (e) {
Get.snackbar(
'Error',
'Translation failed: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
} finally {
isTranslating.value = false;
}
}
// Play the translated text using TTS
Future<void> playTranslatedText(String text) async {
if (text.isEmpty) return;
try {
await _ttsService.speakSingle(text);
} catch (e) {
Get.snackbar(
'Error',
'Failed to play translation: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
}
}
// Play a specific translation item
Future<void> playTranslationItem(TranslationItem item) async {
await playTranslatedText(item.translatedText);
}
// Scroll to the bottom of the list
void _scrollToBottom() {
if (scrollController.hasClients) {
try {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} catch (e) {
// 如果滚动失败,尝试延迟后再次滚动
Future.delayed(const Duration(milliseconds: 200), () {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
}
} else {
// 如果scrollController还没有客户端,延迟后再尝试
Future.delayed(const Duration(milliseconds: 200), _scrollToBottom);
}
}
// 切换源语言和目标语言
void swapLanguages() {
final temp = sourceLanguage.value;
sourceLanguage.value = targetLanguage.value;
targetLanguage.value = temp;
// 如果正在识别,需要停止并重新开始
if (isRecognizing.value) {
stopRecognition();
startRecognition();
}
}
// 设置源语言
void setSourceLanguage(String language) {
if (sourceLanguage.value != language) {
sourceLanguage.value = language;
// 如果正在识别,需要停止并重新开始
if (isRecognizing.value) {
stopRecognition();
startRecognition();
}
}
}
// 设置目标语言
void setTargetLanguage(String language) {
if (targetLanguage.value != language) {
targetLanguage.value = language;
}
}
// Load translation history from storage
void loadTranslationHistory() {
final historyJson = _storage.read(_historyKey);
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
translationHistory.value = historyList.map((item) => TranslationItem.fromJson(item)).toList();
// 确保在历史记录加载后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
}
// Save translation history to storage
void saveTranslationHistory() {
// 限制历史记录数量,保留最新的记录
if (translationHistory.length > _maxHistoryItems) {
translationHistory.removeRange(0, translationHistory.length - _maxHistoryItems);
}
final List<Map<String, dynamic>> historyJson =
translationHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(historyJson));
}
// Clear translation history
void clearTranslationHistory() {
translationHistory.clear();
_storage.remove(_historyKey);
}
}
// Model class for translation items
class TranslationItem {
String sourceText;
String translatedText;
final String sourceLanguage;
final String targetLanguage;
final DateTime timestamp;
final String sessionId;
final bool isFirstInSession;
TranslationItem({
required this.sourceText,
required this.translatedText,
required this.sourceLanguage,
required this.targetLanguage,
required this.timestamp,
required this.sessionId,
this.isFirstInSession = false,
});
factory TranslationItem.fromJson(Map<String, dynamic> json) {
return TranslationItem(
sourceText: json['sourceText'],
translatedText: json['translatedText'],
sourceLanguage: json['sourceLanguage'],
targetLanguage: json['targetLanguage'],
timestamp: DateTime.parse(json['timestamp']),
sessionId: json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
isFirstInSession: json['isFirstInSession'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'sourceText': sourceText,
'translatedText': translatedText,
'sourceLanguage': sourceLanguage,
'targetLanguage': targetLanguage,
'timestamp': timestamp.toIso8601String(),
'sessionId': sessionId,
'isFirstInSession': isFirstInSession,
};
}
}