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.
340 lines
10 KiB
340 lines
10 KiB
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'dart:async';
|
|
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>();
|
|
|
|
// 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;
|
|
|
|
// 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;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
|
|
// Initialize the translation service
|
|
_initTranslationService();
|
|
|
|
// 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 {
|
|
// Initialize a new translation item
|
|
currentItem = TranslationItem(
|
|
sourceText: '',
|
|
translatedText: '',
|
|
sourceLanguage: sourceLanguage.value,
|
|
targetLanguage: targetLanguage.value,
|
|
timestamp: DateTime.now(),
|
|
);
|
|
|
|
// 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();
|
|
|
|
// 重置当前显示的文本
|
|
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) {
|
|
currentItem = TranslationItem(
|
|
sourceText: text,
|
|
translatedText: '',
|
|
sourceLanguage: sourceLanguage.value,
|
|
targetLanguage: targetLanguage.value,
|
|
timestamp: DateTime.now(),
|
|
);
|
|
} 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) {
|
|
currentItem = TranslationItem(
|
|
sourceText: text,
|
|
translatedText: '',
|
|
sourceLanguage: sourceLanguage.value,
|
|
targetLanguage: targetLanguage.value,
|
|
timestamp: DateTime.now(),
|
|
);
|
|
} 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!);
|
|
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 result = await _translationService.translateText(
|
|
text: text,
|
|
sourceLanguage: sourceLanguage.value,
|
|
targetLanguage: targetLanguage.value,
|
|
);
|
|
|
|
if (result != null) {
|
|
// 实时更新翻译文本
|
|
translatedText.value = result;
|
|
|
|
// 确保更新的是发起翻译请求时的翻译项,而不是当前可能已经变化的翻译项
|
|
if (itemToUpdate != null && itemToUpdate.sourceText == text) {
|
|
itemToUpdate.translatedText = result;
|
|
|
|
// 如果是最终结果,并且当前项仍然是发起请求时的项,则添加到历史
|
|
if (isFinal && itemToUpdate == currentItem) {
|
|
translationHistory.add(itemToUpdate);
|
|
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) {
|
|
Future.delayed(const Duration(milliseconds: 100), () {
|
|
scrollController.animateTo(
|
|
scrollController.position.maxScrollExtent,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeOut,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Model class for translation items
|
|
class TranslationItem {
|
|
String sourceText;
|
|
String translatedText;
|
|
final String sourceLanguage;
|
|
final String targetLanguage;
|
|
final DateTime timestamp;
|
|
|
|
TranslationItem({
|
|
required this.sourceText,
|
|
required this.translatedText,
|
|
required this.sourceLanguage,
|
|
required this.targetLanguage,
|
|
required this.timestamp,
|
|
});
|
|
}
|