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.
334 lines
9.7 KiB
334 lines
9.7 KiB
import 'dart:async';
|
|
import 'package:get/get.dart';
|
|
import '../../../data/services/volcano_asr_service.dart';
|
|
import '../../../data/services/volcano_tts_api_service.dart';
|
|
import '../../../core/utils/logger.dart';
|
|
import '../models/translation_message_model.dart';
|
|
|
|
class TranslationController extends GetxController {
|
|
// Observable states
|
|
final isListening = false.obs;
|
|
final isTranslating = false.obs;
|
|
final sourceLanguage = "Chinese".obs;
|
|
final targetLanguage = "English".obs;
|
|
final originalText = "".obs;
|
|
final translatedText = "".obs;
|
|
final isUserSpeaking = false.obs;
|
|
final messages = <TranslationMessage>[].obs;
|
|
|
|
// TTS启用状态
|
|
final isTtsEnabled = true.obs;
|
|
|
|
// Services
|
|
late final VolcanoAsrService _voiceService;
|
|
late final VolcanoTtsApiService _ttsService;
|
|
|
|
// Stream subscriptions
|
|
StreamSubscription? _recognitionSubscription;
|
|
|
|
// Timers
|
|
Timer? _silenceTimer;
|
|
Timer? _speakingCheckTimer;
|
|
|
|
// State tracking
|
|
bool _hasRecognizedSpeech = false;
|
|
bool _hasFinalResult = false;
|
|
DateTime? _lastSpeechTime;
|
|
Completer<String>? _recognitionCompleter;
|
|
|
|
// TTS事件订阅
|
|
StreamSubscription? _ttsEventSubscription;
|
|
bool _isTtsSpeaking = false;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_initializeServices();
|
|
_startSpeakingCheckTimer();
|
|
|
|
_ttsService = Get.find<VolcanoTtsApiService>();
|
|
|
|
// 设置TTS事件监听
|
|
_setupTtsEventListener();
|
|
}
|
|
|
|
Future<void> _initializeServices() async {
|
|
try {
|
|
_voiceService = Get.find<VolcanoAsrService>();
|
|
_ttsService = Get.find<VolcanoTtsApiService>();
|
|
|
|
await _voiceService.initialize();
|
|
await startListening();
|
|
} catch (e) {
|
|
Logger.error('Failed to initialize translation services', e);
|
|
Get.snackbar(
|
|
'Error',
|
|
'Failed to initialize translation services: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> startListening() async {
|
|
if (isListening.value) return;
|
|
|
|
try {
|
|
isListening.value = true;
|
|
originalText.value = '';
|
|
translatedText.value = '';
|
|
_hasRecognizedSpeech = false;
|
|
_hasFinalResult = false;
|
|
|
|
_recognitionCompleter = Completer<String>();
|
|
|
|
final success = await _voiceService.startContinuousRecognition();
|
|
if (!success) {
|
|
isListening.value = false;
|
|
throw Exception('Failed to start voice recognition');
|
|
}
|
|
|
|
_recognitionSubscription = _voiceService.recognitionStream?.listen(
|
|
(event) {
|
|
switch (event.type) {
|
|
case RecognitionEventType.recognizing:
|
|
if (event.text.isNotEmpty) {
|
|
originalText.value = event.text;
|
|
_hasRecognizedSpeech = true;
|
|
_lastSpeechTime = DateTime.now();
|
|
isUserSpeaking.value = true;
|
|
|
|
// Cancel previous silence timer
|
|
_silenceTimer?.cancel();
|
|
|
|
// Set new silence timer - if no new recognition results in 2 seconds,
|
|
// consider the user has stopped speaking
|
|
_silenceTimer = Timer(const Duration(seconds: 2), () {
|
|
if (_hasRecognizedSpeech && !_hasFinalResult) {
|
|
_processTranslation(event.text);
|
|
}
|
|
});
|
|
|
|
// If TTS is playing and user starts speaking, stop TTS
|
|
if (_isTtsSpeaking) {
|
|
_ttsService.endSession();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case RecognitionEventType.finalResult:
|
|
_hasFinalResult = true;
|
|
isUserSpeaking.value = false;
|
|
|
|
if (event.text.isNotEmpty) {
|
|
originalText.value = event.text;
|
|
_hasRecognizedSpeech = true;
|
|
_lastSpeechTime = DateTime.now();
|
|
|
|
_processTranslation(event.text);
|
|
} else if (_hasRecognizedSpeech) {
|
|
// If final result is empty but we had intermediate results
|
|
_processTranslation(originalText.value);
|
|
}
|
|
|
|
// Reset for next utterance
|
|
_hasRecognizedSpeech = false;
|
|
_hasFinalResult = false;
|
|
break;
|
|
|
|
case RecognitionEventType.error:
|
|
Logger.error('Voice recognition error', event.error);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
onError: (error) {
|
|
Logger.error('Voice recognition stream error', error);
|
|
isListening.value = false;
|
|
restartListening();
|
|
}
|
|
);
|
|
|
|
} catch (e) {
|
|
Logger.error('Failed to start listening', e);
|
|
isListening.value = false;
|
|
}
|
|
}
|
|
|
|
Future<void> _processTranslation(String text) async {
|
|
if (text.isEmpty) return;
|
|
|
|
try {
|
|
isTranslating.value = true;
|
|
|
|
// In a real implementation, you would call your translation service here
|
|
// For now, we'll simulate translation with a delay
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
|
|
|
// Simulate translation (in a real app, call your translation API here)
|
|
final translated = await _simulateTranslation(text);
|
|
translatedText.value = translated;
|
|
|
|
// Add to messages
|
|
messages.add(TranslationMessage(
|
|
original: text,
|
|
translated: translated,
|
|
timestamp: DateTime.now(),
|
|
isFromUser: true,
|
|
));
|
|
|
|
// Speak the translation if TTS is enabled
|
|
if (isTtsEnabled.value) {
|
|
await _synthesizeText(translated);
|
|
}
|
|
|
|
} catch (e) {
|
|
Logger.error('Translation error', e);
|
|
} finally {
|
|
isTranslating.value = false;
|
|
}
|
|
}
|
|
|
|
// This is a placeholder - in a real app, you would call your translation API
|
|
Future<String> _simulateTranslation(String text) async {
|
|
// This is just a simulation - replace with actual translation API call
|
|
if (sourceLanguage.value == "Chinese" && targetLanguage.value == "English") {
|
|
// Simulate Chinese to English translation
|
|
if (text.contains('测试')) return 'I am now starting to test the function of translating spoken Chinese into English.';
|
|
return 'Translated from Chinese: $text';
|
|
} else {
|
|
// Simulate English to Chinese translation
|
|
return '翻译自英语: $text';
|
|
}
|
|
}
|
|
|
|
void switchLanguages() {
|
|
final temp = sourceLanguage.value;
|
|
sourceLanguage.value = targetLanguage.value;
|
|
targetLanguage.value = temp;
|
|
|
|
// Clear current messages when switching languages
|
|
messages.clear();
|
|
}
|
|
|
|
Future<void> restartListening() async {
|
|
await stopListening();
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
await startListening();
|
|
}
|
|
|
|
Future<void> stopListening() async {
|
|
if (!isListening.value) return;
|
|
|
|
try {
|
|
await _recognitionSubscription?.cancel();
|
|
_recognitionSubscription = null;
|
|
|
|
_silenceTimer?.cancel();
|
|
_silenceTimer = null;
|
|
|
|
if (_voiceService.isContinuousRecognitionActive()) {
|
|
await _voiceService.stopRecognition();
|
|
}
|
|
|
|
isListening.value = false;
|
|
isUserSpeaking.value = false;
|
|
_hasRecognizedSpeech = false;
|
|
_hasFinalResult = false;
|
|
} catch (e) {
|
|
Logger.error('Failed to stop listening', e);
|
|
}
|
|
}
|
|
|
|
void _startSpeakingCheckTimer() {
|
|
_speakingCheckTimer?.cancel();
|
|
_speakingCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
|
|
_checkIfUserIsSpeaking();
|
|
});
|
|
}
|
|
|
|
void _checkIfUserIsSpeaking() {
|
|
if (_hasRecognizedSpeech && !_hasFinalResult && _lastSpeechTime != null) {
|
|
final timeSinceLastSpeech = DateTime.now().difference(_lastSpeechTime!);
|
|
if (timeSinceLastSpeech.inMilliseconds < 1000) {
|
|
isUserSpeaking.value = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
isUserSpeaking.value = false;
|
|
}
|
|
|
|
void _setupTtsEventListener() {
|
|
_ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent);
|
|
}
|
|
|
|
void _handleTtsEvent(Map<String, dynamic> event) {
|
|
final String eventType = event['type'] as String? ?? 'unknown';
|
|
|
|
switch (eventType) {
|
|
case 'ttsSentenceStart':
|
|
_isTtsSpeaking = true;
|
|
break;
|
|
case 'ttsSentenceEnd':
|
|
case 'sessionFinished':
|
|
case 'connectionFinished':
|
|
_isTtsSpeaking = false;
|
|
break;
|
|
case 'connectionFailed':
|
|
case 'sessionFailed':
|
|
_isTtsSpeaking = false;
|
|
print('TTS错误: ${event['message']}');
|
|
break;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
stopListening();
|
|
_speakingCheckTimer?.cancel();
|
|
|
|
// 取消TTS事件订阅
|
|
_ttsEventSubscription?.cancel();
|
|
|
|
// 结束TTS会话
|
|
_ttsService.endSession();
|
|
|
|
super.onClose();
|
|
}
|
|
|
|
// Add a method to synthesize text using the new TTS service
|
|
Future<void> _synthesizeText(String text) async {
|
|
if (text.isEmpty || !isTtsEnabled.value) return;
|
|
|
|
try {
|
|
// 如果当前有正在进行的TTS会话,先结束它
|
|
if (_isTtsSpeaking) {
|
|
await _ttsService.endSession();
|
|
}
|
|
|
|
// 确保连接已建立
|
|
if (!_ttsService.isConnected.value) {
|
|
final connected = await _ttsService.connect();
|
|
if (!connected) {
|
|
print('无法连接到TTS服务');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 开始合成
|
|
await _ttsService.synthesize(text, 'zh_female_1'); // 使用默认说话人
|
|
} catch (e) {
|
|
print('TTS播放出错: $e');
|
|
}
|
|
}
|
|
|
|
// Add a method to toggle TTS
|
|
void toggleTTS() {
|
|
isTtsEnabled.toggle();
|
|
if (!isTtsEnabled.value) {
|
|
_ttsService.endSession();
|
|
}
|
|
}
|
|
}
|