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.
566 lines
17 KiB
566 lines
17 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 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
|
|
final Rx<TranslationItem?> currentItem = Rx<TranslationItem?>(null);
|
|
|
|
// 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.value = 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 current item
|
|
if (currentItem.value != null) {
|
|
currentItem.value!.sourceText = '';
|
|
currentItem.value!.translatedText = '';
|
|
}
|
|
|
|
// 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:
|
|
// 错误处理,停止语音识别
|
|
Logger.error('语音识别错误: ${event.error}');
|
|
|
|
// 停止语音识别
|
|
isRecognizing.value = false;
|
|
_asrService.stopContinuousRecognition();
|
|
|
|
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;
|
|
|
|
// 重置当前会话ID
|
|
currentSessionId = null;
|
|
|
|
// 重置当前项
|
|
currentItem.value = null;
|
|
} catch (e) {
|
|
Get.snackbar('Error', '停止语音识别失败: ${e.toString()}',
|
|
snackPosition: SnackPosition.BOTTOM);
|
|
}
|
|
}
|
|
|
|
// Handle intermediate recognition result
|
|
void handleIntermediateResult(String text) {
|
|
if (text.isEmpty) return;
|
|
|
|
// 如果当前没有活跃的翻译项,创建一个新的
|
|
if (currentItem.value == null) {
|
|
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
|
|
|
|
currentItem.value = TranslationItem(
|
|
sourceText: text,
|
|
translatedText: '',
|
|
sourceLanguage: sourceLanguage.value,
|
|
targetLanguage: targetLanguage.value,
|
|
timestamp: DateTime.now(),
|
|
sessionId: currentSessionId!,
|
|
isFirstInSession: translationHistory.isEmpty ||
|
|
translationHistory.last.sessionId != currentSessionId,
|
|
);
|
|
currentItem.refresh();
|
|
|
|
} else {
|
|
// 更新当前翻译项
|
|
currentItem.value!.sourceText = text;
|
|
// 通知 GetX 更新
|
|
currentItem.refresh();
|
|
}
|
|
|
|
// 防抖处理翻译请求
|
|
_translationDebounceTimer?.cancel();
|
|
_translationDebounceTimer = Timer(const Duration(milliseconds: 300), () {
|
|
if (currentItem.value != null &&
|
|
currentItem.value!.sourceText.isNotEmpty) {
|
|
translateText(currentItem.value!.sourceText);
|
|
}
|
|
});
|
|
|
|
// 滚动到底部显示最新内容
|
|
_scrollToBottom();
|
|
}
|
|
|
|
// Handle final recognition result
|
|
void handleFinalResult(String text) async {
|
|
// 如果没有文本或当前项为空,直接返回
|
|
if (text.isEmpty || currentItem.value == null) return;
|
|
|
|
// 更新当前项的源文本
|
|
currentItem.value!.sourceText = text;
|
|
currentItem.refresh();
|
|
|
|
// 添加到历史记录
|
|
translationHistory.add(currentItem.value!);
|
|
saveTranslationHistory();
|
|
|
|
// 重置当前项
|
|
currentItem.value = null;
|
|
|
|
// 对最终结果进行一次翻译,确保翻译结果是最新的
|
|
final translationResult = await translateText(text);
|
|
if (translationResult != null) {
|
|
if (isTtsEnabled.value) {
|
|
await playTranslatedText(translationResult);
|
|
}
|
|
}
|
|
// 滚动到底部显示最新内容
|
|
_scrollToBottom();
|
|
}
|
|
|
|
// 检测文本语言
|
|
String detectLanguage(String text) {
|
|
if (text.isEmpty) return sourceLanguage.value;
|
|
|
|
// 检测各种语言特征
|
|
final bool containsChinese = RegExp(r'[\u4e00-\u9fa5]').hasMatch(text);
|
|
final bool containsLatin = RegExp(r'[a-zA-Z]').hasMatch(text);
|
|
final bool containsJapanese =
|
|
RegExp(r'[\u3040-\u309F\u30A0-\u30FF]').hasMatch(text);
|
|
final bool containsKorean =
|
|
RegExp(r'[\uAC00-\uD7AF\u1100-\u11FF]').hasMatch(text);
|
|
final bool containsCyrillic = RegExp(r'[\u0400-\u04FF]').hasMatch(text);
|
|
final bool containsArabic = RegExp(r'[\u0600-\u06FF]').hasMatch(text);
|
|
final bool containsThai = RegExp(r'[\u0E00-\u0E7F]').hasMatch(text);
|
|
|
|
// 获取当前设置的源语言代码
|
|
final String sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh';
|
|
|
|
// 根据文本特征判断语言
|
|
if (containsChinese && sourceCode != 'zh') {
|
|
return '中文 (Chinese)';
|
|
} else if (containsJapanese && sourceCode != 'ja') {
|
|
return '日语 (Japanese)';
|
|
} else if (containsKorean && sourceCode != 'ko') {
|
|
return '韩语 (Korean)';
|
|
} else if (containsCyrillic && sourceCode != 'ru') {
|
|
return '俄语 (Russian)';
|
|
} else if (containsArabic && sourceCode != 'ar') {
|
|
return '阿拉伯语 (Arabic)';
|
|
} else if (containsThai && sourceCode != 'th') {
|
|
return '泰语 (Thai)';
|
|
} else if (containsLatin && sourceCode == 'zh') {
|
|
return '英语 (English)';
|
|
}
|
|
|
|
// 默认返回当前设置的源语言
|
|
return sourceLanguage.value;
|
|
}
|
|
|
|
// Translate the recognized text
|
|
Future<String?> translateText(String sourceText) async {
|
|
if (sourceText.isEmpty) return null;
|
|
|
|
isTranslating.value = true;
|
|
String? translationResult;
|
|
|
|
try {
|
|
// 检测源文本语言
|
|
final detectedLanguage = detectLanguage(sourceText);
|
|
final bool shouldSwapLanguages = detectedLanguage != sourceLanguage.value;
|
|
|
|
// 调用翻译服务
|
|
final result = await _translationService.translateText(
|
|
text: sourceText,
|
|
sourceLanguage:
|
|
shouldSwapLanguages ? detectedLanguage : sourceLanguage.value,
|
|
targetLanguage:
|
|
shouldSwapLanguages ? sourceLanguage.value : targetLanguage.value,
|
|
);
|
|
|
|
if (result != null) {
|
|
translationResult = result;
|
|
|
|
// 更新翻译结果
|
|
if (currentItem.value != null &&
|
|
currentItem.value!.sourceText.contains(sourceText)) {
|
|
// 直接更新并刷新
|
|
currentItem.value!.translatedText = result;
|
|
currentItem.refresh();
|
|
} else {
|
|
// 在历史记录中查找匹配的项
|
|
final index = translationHistory.indexWhere(
|
|
(historyItem) => historyItem.sourceText == sourceText);
|
|
|
|
if (index != -1) {
|
|
// 直接更新历史记录项
|
|
translationHistory[index].translatedText = result;
|
|
// 通知 GetX 更新
|
|
translationHistory.refresh();
|
|
|
|
saveTranslationHistory();
|
|
}
|
|
// 滚动到底部显示最新内容
|
|
_scrollToBottom();
|
|
}
|
|
}
|
|
return translationResult;
|
|
} catch (e) {
|
|
Get.snackbar('Error', '翻译失败: ${e.toString()}',
|
|
snackPosition: SnackPosition.BOTTOM);
|
|
return null;
|
|
} finally {
|
|
isTranslating.value = false;
|
|
}
|
|
}
|
|
|
|
// 播放翻译文本
|
|
Future<void> playTranslatedText(String text) async {
|
|
if (text.isEmpty) return;
|
|
|
|
try {
|
|
await _ttsService.speakSingle(text);
|
|
} catch (e) {
|
|
Get.snackbar('Error', '播放翻译失败: ${e.toString()}',
|
|
snackPosition: SnackPosition.BOTTOM);
|
|
}
|
|
}
|
|
|
|
// 停止所有操作(语音识别和TTS)
|
|
Future<void> stopAll() async {
|
|
try {
|
|
// 停止语音识别
|
|
if (isRecognizing.value) {
|
|
await stopRecognition();
|
|
}
|
|
|
|
// 停止TTS播放
|
|
await _ttsService.stop();
|
|
} catch (e) {
|
|
Get.snackbar('Error', '停止操作失败: ${e.toString()}',
|
|
snackPosition: SnackPosition.BOTTOM);
|
|
}
|
|
}
|
|
|
|
// 播放特定翻译项
|
|
Future<void> playTranslationItem(TranslationItem item) async {
|
|
if (item.translatedText.isEmpty) return;
|
|
await playTranslatedText(item.translatedText);
|
|
}
|
|
|
|
// 播放文本
|
|
Future<void> playText(String text) async {
|
|
if (text.isEmpty) return;
|
|
await playTranslatedText(text);
|
|
}
|
|
|
|
// 滚动到列表底部
|
|
void _scrollToBottom() {
|
|
if (!scrollController.hasClients) {
|
|
Future.delayed(const Duration(milliseconds: 100), _scrollToBottom);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
scrollController.animateTo(
|
|
scrollController.position.maxScrollExtent,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeOut,
|
|
);
|
|
} catch (e) {
|
|
// 如果滚动失败,尝试延迟后再次滚动
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
if (scrollController.hasClients) {
|
|
scrollController.jumpTo(scrollController.position.maxScrollExtent);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 切换源语言和目标语言
|
|
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) {
|
|
try {
|
|
final List<dynamic> historyList = jsonDecode(historyJson);
|
|
translationHistory.value =
|
|
historyList.map((item) => TranslationItem.fromJson(item)).toList();
|
|
} catch (e) {
|
|
// 如果解析失败,清除历史记录
|
|
_storage.remove(_historyKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
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) {
|
|
DateTime timestamp;
|
|
try {
|
|
timestamp = DateTime.parse(json['timestamp']);
|
|
} catch (e) {
|
|
timestamp = DateTime.now();
|
|
}
|
|
|
|
return TranslationItem(
|
|
sourceText: json['sourceText'] ?? '',
|
|
translatedText: json['translatedText'] ?? '',
|
|
sourceLanguage: json['sourceLanguage'] ?? '中文 (Chinese)',
|
|
targetLanguage: json['targetLanguage'] ?? '英语 (English)',
|
|
timestamp: 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,
|
|
};
|
|
}
|
|
}
|
|
|