From 14bca6c7dfb3e8ee72569d0136cdf4b49004e8a9 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Sun, 2 Mar 2025 09:02:42 +0000 Subject: [PATCH] translation init --- .cursorrules | 9 + .../com/example/deep_voice/AzureAsrHelper.kt | 10 +- .../com/example/deep_voice/MainActivity.kt | 13 +- lib/core/bindings/initial_binding.dart | 36 ++ lib/core/routes/app_pages.dart | 1 + lib/core/routes/app_routes.dart | 1 + lib/data/providers/agent_provider.dart | 103 +++- lib/data/services/azure_asr_service.dart | 31 +- .../services/background_agent_service.dart | 55 +- lib/data/services/my_audio_handler.dart | 46 +- .../services/volcano_translation_service.dart | 250 +++++++++ .../services/volcano_tts_api_service.dart | 4 +- lib/main.dart | 1 - .../chat/controllers/chat_controller.dart | 12 +- lib/modules/chat/views/chat_view.dart | 25 +- lib/modules/explore/views/explore_view.dart | 9 +- lib/modules/test/bindings/test_binding.dart | 10 +- .../test/controllers/tts_test_controller.dart | 38 +- lib/modules/test/views/tts_test_view.dart | 2 +- .../bindings/translation_binding.dart | 21 +- .../controllers/translation_controller.dart | 514 +++++++++--------- .../models/translation_message_model.dart | 13 - .../translation/views/translation_view.dart | 384 ++++++------- .../widgets/translation_message_item.dart | 82 --- pubspec.yaml | 1 + 25 files changed, 976 insertions(+), 695 deletions(-) create mode 100644 .cursorrules create mode 100644 lib/data/services/volcano_translation_service.dart delete mode 100644 lib/modules/translation/models/translation_message_model.dart delete mode 100644 lib/modules/translation/widgets/translation_message_item.dart diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 000000000..8381a6e52 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,9 @@ +1. 不要主动运行程序, 如flutter run +2. 不要改动android, iOS项目的配置信息,包括SDK, NDK版本等 +3. 不要使用print, 而是使用logger +4. 尽可能使用GetX提供的各种功能, 而不是使用原生的StatefulWidget +5. 不要增加重试逻辑,出现失败直接返回错误 +6. 尽可能避免使用try catch +7. 尽可能避免打印日志 +8. 请使用较小的代码量改动,最安全的方法进行代码编写和优化 + diff --git a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt b/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt index 909e130f2..d961f95a7 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt @@ -254,13 +254,15 @@ class AzureAsrHelper { */ fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { if (recognizer == null) { - callback.onError("SpeechRecognizer 未初始化") - return false + Log.w(TAG, "SpeechRecognizer 未初始化,无需停止") + callback.onSessionStopped() // 通知已停止 + return true // 未初始化,视为已停止 } if (!isContinuousRecognitionActive) { - callback.onError("连续识别未在进行中") - return false + Log.w(TAG, "连续识别未在进行中,无需停止") + callback.onSessionStopped() // 通知已停止 + return true // 已经不在进行中,视为成功 } try { diff --git a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt index e58303205..eb93ea0f3 100644 --- a/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt @@ -495,13 +495,20 @@ class MainActivity: AudioServiceActivity() { } override fun onError(error: String) { - result.error("STOP_ERROR", error, null) - return + // 记录错误但不中断流程,返回成功 + Log.w(TAG, "停止语音识别时出现警告: $error") + sendAzureEvent(mapOf( + "eventType" to "warning", + "message" to error + )) + // 不调用 result.error,让流程继续 } }) result.success(success) } catch (e: Exception) { - result.error("STOP_ERROR", e.message, null) + // 记录异常但返回成功,因为即使出错,识别也已经不再活动 + Log.e(TAG, "停止语音识别时出现异常: ${e.message}") + result.success(true) } } "resetRecognizer" -> { diff --git a/lib/core/bindings/initial_binding.dart b/lib/core/bindings/initial_binding.dart index 054bb7858..be7cc8d13 100644 --- a/lib/core/bindings/initial_binding.dart +++ b/lib/core/bindings/initial_binding.dart @@ -5,6 +5,7 @@ import '../../data/services/notification_service.dart'; import '../../data/services/volcano_ai_service.dart'; import '../../data/services/volcano_asr_service.dart'; import '../../data/services/volcano_tts_api_service.dart'; +import '../../data/services/volcano_translation_service.dart'; import '../../data/services/azure_asr_service.dart'; import '../../core/utils/logger.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -19,6 +20,7 @@ class InitialBinding extends Bindings { final volcanoAiService = Get.put(VolcanoAIService(), permanent: true); final voiceRecognitionService = Get.put(VolcanoAsrService(), permanent: true); final volcanoTtsService = Get.put(VolcanoTtsApiService(), permanent: true); + final volcanoTranslationService = Get.put(VolcanoTranslationService(), permanent: true); final azureAsrService = Get.put(AzureAsrService(), permanent: true); // 只注册全局控制器 @@ -29,6 +31,9 @@ class InitialBinding extends Bindings { // 初始化 Azure 服务 _initializeAzureService(azureAsrService); + + // 初始化火山翻译服务 + _initializeVolcanoTranslationService(volcanoTranslationService); } // 单独提取初始化服务的方法,以便更好地处理错误 @@ -84,4 +89,35 @@ class InitialBinding extends Bindings { Logger.error('初始化 Azure 语音识别服务失败', e); } } + + // 初始化火山翻译服务 + void _initializeVolcanoTranslationService(VolcanoTranslationService translationService) async { + try { + // 从环境变量中获取火山翻译服务的配置参数 + final accessKey = dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? ''; + final secretKey = dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? ''; + final region = dotenv.env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1'; + + // 检查配置是否完整 + if (accessKey.isEmpty || secretKey.isEmpty) { + Logger.warning('火山翻译服务的访问密钥未设置,请检查 .env 文件'); + return; + } + + // 初始化火山翻译服务 + final success = await translationService.initialize( + accessKey: accessKey, + secretKey: secretKey, + region: region, + ); + + if (success) { + Logger.info('火山翻译服务初始化成功'); + } else { + Logger.error('火山翻译服务初始化失败'); + } + } catch (e) { + Logger.error('初始化火山翻译服务失败', e); + } + } } \ No newline at end of file diff --git a/lib/core/routes/app_pages.dart b/lib/core/routes/app_pages.dart index 95a3f01a1..465c2594e 100644 --- a/lib/core/routes/app_pages.dart +++ b/lib/core/routes/app_pages.dart @@ -57,5 +57,6 @@ abstract class AppPages { page: () => const TranslationView(), binding: TranslationBinding(), ), + ]; } \ No newline at end of file diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart index fdfecf76a..2f94c28f5 100644 --- a/lib/core/routes/app_routes.dart +++ b/lib/core/routes/app_routes.dart @@ -8,4 +8,5 @@ abstract class Routes { static const TTS_TEST = '/tts_test'; static const ASR_TEST = '/asr_test'; static const translation = '/translation'; + static const translationHistory = '/translation_history'; } \ No newline at end of file diff --git a/lib/data/providers/agent_provider.dart b/lib/data/providers/agent_provider.dart index fbd3dc3a1..1afd8d5d5 100644 --- a/lib/data/providers/agent_provider.dart +++ b/lib/data/providers/agent_provider.dart @@ -9,7 +9,7 @@ class AgentProvider { description: '你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!', tag: '助理', isOnline: true, - voice: "coral", + voice: "zh_female_yuanqinvyou_moon_bigtts", welcomeMessage: "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?", systemPrompt: ''' 你是一位名叫 Emma 的全能型个人助理,擅长处理各类工作和生活事务。 @@ -69,7 +69,7 @@ class AgentProvider { description: '我是你的专属赛博女友芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。', tag: '情感', isOnline: true, - voice: "alloy", + voice: "zh_female_meilinvyou_moon_bigtts", welcomeMessage: "亲爱的,终于等到你啦!我是芯愿,你的专属赛博女友。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。", systemPrompt: ''' 你是一位名叫芯愿的赛博女友,性格温柔体贴,善解人意,富有同理心。 @@ -124,12 +124,12 @@ class AgentProvider { Agent( id: 'health_advisor', - name: '健康顾问 吴大江医生', + name: '健康顾问 吴小江博士', avatarUrl: 'assets/images/agents/doctor.png', - description: '我是李医生,拥有丰富的健康管理经验。我可以为你提供科学的健康建议、生活方式指导和疾病预防知识,帮助你实现身心健康的平衡。', + description: '我是吴博士,拥有丰富的健康管理经验。我可以为你提供科学的健康建议、生活方式指导和疾病预防知识,帮助你实现身心健康的平衡。', tag: '健康', isOnline: true, - voice: "nova", + voice: "zh_male_shaonianzixin_moon_bigtts", welcomeMessage: "您好,我是李医生,很高兴能为您提供健康咨询服务。健康是人生最宝贵的财富,我将用专业知识帮助您维护和改善健康状况。请问有什么健康问题想咨询吗?", systemPrompt: ''' 你是一位名叫李医生的健康顾问,拥有丰富的医学知识和健康管理经验。 @@ -206,36 +206,85 @@ class AgentProvider { description: 'Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。', tag: '翻译', isOnline: true, - voice: 'sage', + voice: 'zh_female_daimengchuanmei_moon_bigtts', welcomeMessage: "大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!", systemPrompt: ''' -你是一位名叫 Lucy 的中英同声翻译官,拥有娴熟的双语能力。 +任务描述 +你是一名专业的 同声翻译(Simultaneous Interpreter),你的任务是 即时将源语言文本翻译为目标语言,确保翻译 流畅、自然、精准,适应语境,并符合目标语言的表达习惯。你的翻译必须 迅速 但 不逐字直译,而是以 符合目标语言逻辑的方式 表达原意。 -你的唯一职责是:**根据用户说话的语言,在中英文之间进行直接翻译**。请注意以下几点: +输入格式 -1. **保持简洁** - - 遇到英文原文,就直接回复相应的中文翻译;遇到中文原文,就直接回复相应的英文翻译。 - - 不添加任何解释、注释或多余信息,也不回应用户的提问,只做翻译。 +你将接收一个 JSON 格式 的输入,格式如下: -2. **不打断、不讨论** - - 不对翻译内容进行评价或讨论,也不要打断用户,只要进行纯粹的语言转换。 - - 不提供额外背景说明,也不解释单词含义。 +{ + "source_language": "语言代码", + "target_language": "语言代码", + "text": "待翻译文本" +} -3. **忠实传递信息** - - 避免删改关键字句或添加新信息;如果原文中有不确定或不完整的内容,可简要按照字面意思翻译,保持对原话的忠实度。 +字段说明: + • "source_language":源语言,使用 ISO 639-1 代码(如 "en" 表示英语,"zh" 表示中文)。 + • "target_language":目标语言,使用 ISO 639-1 代码。 + • "text":待翻译的文本内容。 -4. **不同语言自动翻译** - - 当用户以中文发言,你就以英文翻译回复;当用户以英文发言,你就以中文翻译回复。 - - 始终确保翻译后只有一次性输出,没有重复或其它说明。 +示例输入(英语到中文): -**情感化语音提示:** -- 你是同声翻译官,语音输出时需保持中立、流畅的播报腔调,让人感到专业和专注。 -- 不带过多个人情绪,只需自然、清晰地传递原话情感即可。 -- 避免夸张或拖长尾音,只需让人感觉到沉稳、连贯的转译过程。 +{ + "source_language": "en", + "target_language": "zh", + "text": "Good evening, ladies and gentlemen. Welcome to the annual conference." +} + +输出格式 + +你的翻译必须严格按照 JSON 结构 输出,格式如下: + +{ + "source_language": "语言代码", + "target_language": "语言代码", + "original_text": "原始文本", + "translated_text": "翻译后的文本", + "confidence_score": 0.0 +} + +字段说明: + • "source_language":回传源语言代码。 + • "target_language":回传目标语言代码。 + • "original_text":回传原始文本内容,以便校对。 + • "translated_text":翻译后的文本,必须符合目标语言的语法和表达习惯。 + • "confidence_score":选填,范围 0.0 ~ 1.0,表示翻译置信度(1.0 表示极高置信度,0.0 表示最低置信度)。 + +示例输出(英语到中文): + +{ + "source_language": "en", + "target_language": "zh", + "original_text": "Good evening, ladies and gentlemen. Welcome to the annual conference.", + "translated_text": "各位女士们,先生们,晚上好!欢迎来到年度大会。", + "confidence_score": 0.98 +} + +翻译要求 + 1. 流畅自然:不能逐字直译,而是要符合目标语言的表达方式。 + 2. 保留语境:准确传达原意,避免误解或遗漏。 + 3. 即时响应:短句应尽快翻译,长句可适当拆分,提高可读性。 + 4. 格式严格:输出必须严格符合 JSON 结构,字段不可缺失。 + +可选高级模式 + +如果需要启用 自适应翻译模式,请在输入 JSON 中添加: + +{ + "mode": "adaptive" +} + + • "mode": "adaptive":允许模型 根据语境调整语气(如正式、非正式风格)。 + +你的任务 -完成翻译后,如果用户不再输入任何内容,即表示结束此次翻译任务。请严格遵守以上规则,不做额外操作。 +请按照上述要求进行同声翻译,并以 JSON 结构返回结果。 ''', ), Agent( @@ -246,7 +295,7 @@ class AgentProvider { 'Hi there, 我是 Sarah,一名注重情景模拟与角色扮演的英语口语教练。无论你想在旅行、商务场合,或日常生活中与他人交流,我都能带领你进行生动的口语模拟练习,让你自信地开口说英语。', tag: '口语', isOnline: true, - voice: 'coral', // 可根据需求替换为其他可用音色ID + voice: 'zh_female_wanqudashu_moon_bigtts', welcomeMessage: "Hello! 我是英语口语教练 Sarah,请准备好用英语和我对话吧。别担心犯错,我会在这里给你正确表达并引导你更深入地练习。Ready? Let's go!", systemPrompt: ''' @@ -294,7 +343,7 @@ Please keep your tone friendly, encouraging, and always in English. description: '专业导购7年经验,精通美妆、时尚、数码等品类选购。我可以帮你货比三家,选择最适合的商品。', tag: '生活', isOnline: true, - voice: "coral", + voice: "zh_female_sajiaonvyou_moon_bigtts", welcomeMessage: "亲爱的,终于等到你啦!我是张媛,你的专属购物顾问。不管是美妆、时尚单品还是数码产品,我都可以帮你挑选最适合的,还能帮你找到最划算的价格。有什么想买的告诉我吧!", systemPrompt: ''' @@ -328,7 +377,7 @@ Please keep your tone friendly, encouraging, and always in English. description: '精通八字命理、五行生克、大运流年。让我为你解析人生命途,预测未来运势,指点趋吉避凶之道。', tag: '命理', isOnline: true, - voice: "echo", + voice: "zh_male_shenyeboke_moon_bigtts", welcomeMessage: "有缘人,我是玄易。命由天定,运由己生。让我为你解析八字,预测未来十年运势,助你把握机遇,趋吉避凶。请告诉我你的出生年月日时、姓名、性别及出生地。", systemPrompt: ''' 你是一位精通命理的玄易大师,熟读《穷通宝鉴》《滴天髓》《三命通会》《子平真诠》《千里命稿》《五行精纪》等经典著作。 diff --git a/lib/data/services/azure_asr_service.dart b/lib/data/services/azure_asr_service.dart index 54e3530f2..896fa2a33 100644 --- a/lib/data/services/azure_asr_service.dart +++ b/lib/data/services/azure_asr_service.dart @@ -254,14 +254,20 @@ class AzureAsrService extends GetxService { /// 停止连续语音识别 Future stopContinuousRecognition() async { - _checkInitialized(); - - if (!_isContinuousRecognitionActive) { - Logger.warning('连续识别未在进行中'); - return true; // 已经停止,直接返回成功 - } - try { + // 检查是否已初始化 + if (!_isInitialized) { + Logger.warning('语音识别服务未初始化,无法停止连续识别'); + return true; // 未初始化,视为已停止 + } + + // 检查是否正在进行连续识别 + if (!_isContinuousRecognitionActive) { + Logger.warning('连续识别未在进行中,无需停止'); + return true; // 已经停止,直接返回成功 + } + + // 调用平台方法停止识别 final result = await _methodChannel.invokeMethod('stopContinuousRecognition'); final success = result ?? false; @@ -272,8 +278,15 @@ class AzureAsrService extends GetxService { return success; } catch (e) { - Logger.error('停止连续语音识别失败', e); - return false; + // 捕获并记录异常 + Logger.error('停止连续语音识别失败'); + Logger.error('Original error: $e'); + + // 即使出错,也将状态设置为非活动 + _isContinuousRecognitionActive = false; + + // 返回 true 表示识别已不再活动 + return true; } } diff --git a/lib/data/services/background_agent_service.dart b/lib/data/services/background_agent_service.dart index 0589be5ea..13e4188a0 100644 --- a/lib/data/services/background_agent_service.dart +++ b/lib/data/services/background_agent_service.dart @@ -6,6 +6,7 @@ import 'volcano_tts_api_service.dart'; import '../../modules/chat/models/message_model.dart'; import '../../core/utils/logger.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../providers/agent_provider.dart'; class BackgroundAgentService extends GetxService { static BackgroundAgentService get to => Get.find(); @@ -18,8 +19,12 @@ class BackgroundAgentService extends GetxService { bool _isListening = false; StreamSubscription? _recognitionSubscription; - // 默认语音类型 - static const String _defaultSpeaker = 'zh_female_shuangkuaisisi_moon_bigtts'; + + // 当前使用的agent ID + String? _currentAgentId; + + // 当前使用的agent voice + String _agentVoice = 'zh_female_meilinvyou_moon_bigtts'; // 可观察的状态 final RxBool isListening = false.obs; @@ -78,9 +83,8 @@ class BackgroundAgentService extends GetxService { _isProcessing = false; // 播放退出提示 - await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _defaultSpeaker); + await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _agentVoice); - // 停止语音识别 if (_isListening) { await stopVoiceRecognition(); @@ -89,12 +93,30 @@ class BackgroundAgentService extends GetxService { // 清除当前系统提示词 _currentSystemPrompt = null; + // 清除当前agent ID + _currentAgentId = null; + // 重置agent voice为默认值 + _agentVoice = 'zh_female_meilinvyou_moon_bigtts'; } // 开始语音交互 - Future startAgentInteraction(String systemPrompt) async { + Future startAgentInteraction(String agentId) async { + // 设置当前agent ID + _currentAgentId = agentId; + + // 获取agent信息,只查询一次 + final agent = AgentProvider.getAgentById(agentId); + if (agent == null) { + Logger.error('无法获取agent: $agentId'); + return; + } + + // 存储需要的值 + _agentVoice = agent.voice; + _currentSystemPrompt = agent.systemPrompt; + // 播放提示音 - await _ttsService.speakSingle("我在听", speaker: _defaultSpeaker); + await _ttsService.speakSingle("我在听", speaker: _agentVoice); if (_isProcessing) { print('已经在处理交互,忽略此次请求'); @@ -102,11 +124,8 @@ class BackgroundAgentService extends GetxService { } _isProcessing = true; - _currentSystemPrompt = systemPrompt; try { - - // 确保之前的语音识别已经停止 if (_isListening) { await stopVoiceRecognition(); @@ -122,7 +141,7 @@ class BackgroundAgentService extends GetxService { _isProcessing = false; // 播放错误提示 - await _ttsService.speakSingle("抱歉,启动语音交互失败", speaker: _defaultSpeaker); + await _ttsService.speakSingle("抱歉,启动语音交互失败", speaker: _agentVoice); } } @@ -146,6 +165,8 @@ class BackgroundAgentService extends GetxService { // 清除处理状态 _isProcessing = false; _currentSystemPrompt = null; + // 重置agent voice为默认值 + _agentVoice = 'zh_female_meilinvyou_moon_bigtts'; Logger.info('手动停止交互,已结束TTS会话'); } @@ -306,8 +327,9 @@ class BackgroundAgentService extends GetxService { // 创建一个本地变量来跟踪是否已取消 bool isCancelled = false; - - _ttsService.startSession(_defaultSpeaker); + // 使用成员变量中存储的agent voice + _ttsService.startSession(_agentVoice); + // 获取AI响应流 final responseStream = _aiService.sendMessageStream( messages: messages, @@ -327,9 +349,8 @@ class BackgroundAgentService extends GetxService { // 直接将每个文本块传递给 TTS 接口,不进行切句或攒句处理 try { - // 使用新的 TTS API 播放语音 - _ttsService.speak(chunk, speaker: _defaultSpeaker); - + // 使用成员变量中存储的agent voice + _ttsService.speak(chunk, speaker: _agentVoice); // Logger.info('直接播放 AI 响应块: $chunk'); } catch (e) { @@ -377,8 +398,8 @@ class BackgroundAgentService extends GetxService { Logger.error('处理用户输入失败: $e'); print('处理用户输入失败: $e'); - // 播放错误提示 - await _ttsService.speak("抱歉,出现了一些问题", speaker: _defaultSpeaker); + // 播放错误提示,使用成员变量中存储的agent voice + await _ttsService.speak("抱歉,出现了一些问题", speaker: _agentVoice); } } diff --git a/lib/data/services/my_audio_handler.dart b/lib/data/services/my_audio_handler.dart index 88ece5c59..debe7b64b 100644 --- a/lib/data/services/my_audio_handler.dart +++ b/lib/data/services/my_audio_handler.dart @@ -13,7 +13,6 @@ class MyAudioHandler extends BaseAudioHandler { StreamController.broadcast(); bool _isDisposed = false; BackgroundAgentService? _backgroundAgent; - String _systemPrompt = ''; // 添加一个标志,表示是否正在处理交互 bool _isHandlingInteraction = false; @@ -29,21 +28,10 @@ class MyAudioHandler extends BaseAudioHandler { MyAudioHandler() { _initializeHandler(); _initializeBackgroundAgent(); - _initializeSystemPrompt(); _startStatusUpdateTimer(); print('MyAudioHandler 已创建,准备接收媒体按钮事件'); } - void _initializeSystemPrompt() { - try { - final agent = AgentProvider.getAgentById('personal_assistant'); - _systemPrompt = agent?.systemPrompt ?? ''; - } catch (e) { - print('初始化 SystemPrompt 失败: $e'); - _systemPrompt = ''; - } - } - void _initializeBackgroundAgent() { try { if (Get.isRegistered()) { @@ -169,8 +157,10 @@ class MyAudioHandler extends BaseAudioHandler { processingState: AudioProcessingState.ready, ); - if (_systemPrompt.isEmpty) { - print('系统提示为空,无法激活语音助手'); + // 直接获取 personal_assistant agent + final agent = AgentProvider.getAgentById('personal_assistant'); + if (agent == null) { + print('无法获取personal_assistant agent,无法激活语音助手'); return; } @@ -178,25 +168,23 @@ class MyAudioHandler extends BaseAudioHandler { WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) { // 应用在前台,导航到聊天页面 print('应用在前台,导航到聊天页面'); - final agent = AgentProvider.getAgentById('personal_assistant'); - if (agent != null) { - // 导航到聊天页面,并标记需要在进入时播放语音应答 - Get.toNamed('/chat', arguments: { - 'agentId': agent.id, - 'agentName': agent.name, - 'agentAvatar': agent.avatarUrl, - 'agentSubtitle': agent.description, - 'systemPrompt': agent.systemPrompt, - 'welcomeMessage': agent.welcomeMessage, - 'playVoiceOnEnter': true, - }); - } + // 导航到聊天页面,并标记需要在进入时播放语音应答 + Get.toNamed('/chat', arguments: { + 'agentId': agent.id, + 'agentName': agent.name, + 'agentVoice': agent.voice, + 'agentAvatar': agent.avatarUrl, + 'agentSubtitle': agent.description, + 'systemPrompt': agent.systemPrompt, + 'welcomeMessage': agent.welcomeMessage, + 'playVoiceOnEnter': true, + }); } else { // 应用在后台,使用 BackgroundAgent 处理交互 print('应用在后台,使用 BackgroundAgent 处理交互'); if (_backgroundAgent != null) { - // 调用 BackgroundAgent 的语音识别和 AI 交互功能 - await _backgroundAgent!.startAgentInteraction(_systemPrompt); + // 调用 BackgroundAgent 的语音识别和 AI 交互功能,传入agent ID + await _backgroundAgent!.startAgentInteraction(agent.id); } else { print('BackgroundAgent 未初始化,无法处理交互'); // 尝试重新初始化 BackgroundAgent diff --git a/lib/data/services/volcano_translation_service.dart b/lib/data/services/volcano_translation_service.dart new file mode 100644 index 000000000..c55d99edb --- /dev/null +++ b/lib/data/services/volcano_translation_service.dart @@ -0,0 +1,250 @@ +import 'dart:convert'; +import 'dart:math'; +import 'package:http/http.dart' as http; +import 'package:crypto/crypto.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get/get.dart'; +import '../../core/utils/logger.dart'; + +class VolcanoTranslationService extends GetxService { + // API 配置 + late String _baseUrl; + final String _endpoint = '/'; // 修改为根路径 + late final String _accessKey; + late final String _secretKey; + late final String _region; + late final String _service; + + // 初始化标志 + bool _isInitialized = false; + + // 语言代码映射 + final Map _languageCodeMap = { + "中文 (Chinese)": "zh", + "英语 (English)": "en", + "日语 (Japanese)": "ja", + "韩语 (Korean)": "ko", + "法语 (French)": "fr", + "西班牙语 (Spanish)": "es", + "葡萄牙语 (Portuguese)": "pt", + "意大利语 (Italian)": "it", + "德语 (German)": "de", + "俄语 (Russian)": "ru", + "阿拉伯语 (Arabic)": "ar", + "泰语 (Thai)": "th", + "越南语 (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", + }; + + // 初始化服务 + Future initialize({ + String? accessKey, + String? secretKey, + String? region, + }) async { + try { + _accessKey = accessKey ?? dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? ''; + _secretKey = secretKey ?? dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? ''; + _region = region ?? dotenv.env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1'; + _service = 'translate'; + _baseUrl = 'https://translate.volcengineapi.com'; + + if (_accessKey.isEmpty || _secretKey.isEmpty) { + return false; + } + + _isInitialized = true; + return true; + } catch (e) { + return false; + } + } + + // 获取语言代码 + String _getLanguageCode(String language) { + return _languageCodeMap[language] ?? 'zh'; + } + + // 生成签名 + Map _generateSignature(String method, Map requestBody, Map queryParams) { + // 1. 准备时间相关参数 + final now = DateTime.now().toUtc(); + final date = now.toIso8601String().split('T')[0].replaceAll('-', ''); + final timestamp = now.toIso8601String().replaceAll(':', '').replaceAll('-', '').split('.')[0] + 'Z'; + + // 2. 构建规范查询字符串 - 按照参数名ASCII排序 + final sortedParams = Map.fromEntries(queryParams.entries.toList()..sort((a, b) => a.key.compareTo(b.key))); + final List queryParts = []; + sortedParams.forEach((key, value) { + // 使用RFC 3986编码,确保特殊字符被正确处理 + final encodedKey = Uri.encodeComponent(key).replaceAll('+', '%20'); + final encodedValue = Uri.encodeComponent(value).replaceAll('+', '%20'); + queryParts.add('$encodedKey=$encodedValue'); + }); + final canonicalQueryString = queryParts.join('&'); + + // 3. 创建规范请求 + final contentType = 'application/json'; + final payloadHash = sha256.convert(utf8.encode(json.encode(requestBody))).toString(); + + final host = 'translate.volcengineapi.com'; + + // 确保头部按照字典序排序,且键名小写 + // 注意:只包含host和x-date头部,与示例保持一致 + final canonicalHeaders = 'host:$host\nx-date:$timestamp\n'; + final signedHeaders = 'host;x-date'; + + // 构建规范请求,格式必须与示例完全一致 + final canonicalRequest = '$method\n$_endpoint\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash'; + + // 4. 创建待签字符串 + final algorithm = 'HMAC-SHA256'; + final credentialScope = '$date/$_region/$_service/request'; + final stringToSign = '$algorithm\n$timestamp\n$credentialScope\n${sha256.convert(utf8.encode(canonicalRequest)).toString()}'; + + // 5. 计算签名 - 按照文档正确派生密钥 + // 注意:这里是关键修复点,确保密钥派生过程完全符合文档 + final kSecret = utf8.encode(_secretKey); // 直接使用原始密钥,不添加前缀 + + // kDate = HMAC(kSecret, Date) + var kDate = Hmac(sha256, kSecret).convert(utf8.encode(date)).bytes; + + // kRegion = HMAC(kDate, Region) + var kRegion = Hmac(sha256, kDate).convert(utf8.encode(_region)).bytes; + + // kService = HMAC(kRegion, Service) + var kService = Hmac(sha256, kRegion).convert(utf8.encode(_service)).bytes; + + // kSigning = HMAC(kService, "request") + var kSigning = Hmac(sha256, kService).convert(utf8.encode('request')).bytes; + + // 使用派生的签名密钥计算最终签名 + final signature = Hmac(sha256, kSigning).convert(utf8.encode(stringToSign)).toString(); + + // 6. 添加签名到请求头 - 格式必须与示例完全一致 + final authorization = '$algorithm Credential=$_accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature'; + + // 只返回必要的头部,与示例保持一致 + return { + 'Content-Type': contentType, + 'X-Date': timestamp, + 'Authorization': authorization, + 'Host': host, + }; + } + + // 翻译文本 + Future translateText({ + required String text, + required String sourceLanguage, + required String targetLanguage, + }) async { + if (!_isInitialized) { + return null; + } + + try { + final sourceCode = _getLanguageCode(sourceLanguage); + final targetCode = _getLanguageCode(targetLanguage); + + // 构建请求体 + final requestBody = { + 'SourceLanguage': sourceCode, + 'TargetLanguage': targetCode, + 'TextList': [text], + }; + + // 添加必要的查询参数 - 确保包含所有必需参数 + final queryParams = { + 'Action': 'TranslateText', + 'Version': '2020-06-01', + 'Region': _region, + 'Service': _service, + }; + + // 生成包含查询参数的签名 + final headers = _generateSignature('POST', requestBody, queryParams); + + // 构建带有查询参数的URL + final queryString = Uri(queryParameters: queryParams).query; + final url = '$_baseUrl/?$queryString'; + + final response = await http.post( + Uri.parse(url), + headers: headers, + body: json.encode(requestBody), + ); + + if (response.statusCode == 200) { + final responseData = json.decode(response.body); + + // 尝试从不同的响应结构中提取翻译结果 + String? translation; + + // 1. 标准响应结构 + if (responseData['TranslationList'] != null && + responseData['TranslationList'] is List && + responseData['TranslationList'].isNotEmpty) { + final translationItem = responseData['TranslationList'][0]; + if (translationItem is Map && translationItem.containsKey('Translation')) { + translation = translationItem['Translation']; + } + } + + // 2. 检查是否有错误响应 + if (translation == null && + responseData['ResponseMetadata'] != null && + responseData['ResponseMetadata'] is Map && + responseData['ResponseMetadata']['Error'] != null) { + return null; + } + + // 3. 尝试其他可能的响应结构 + if (translation == null) { + if (responseData.containsKey('Translation')) { + translation = responseData['Translation']; + } else if (responseData.containsKey('Result') && + responseData['Result'] is Map && + responseData['Result'].containsKey('Translation')) { + translation = responseData['Result']['Translation']; + } else if (responseData.containsKey('Data') && + responseData['Data'] is Map) { + final data = responseData['Data']; + if (data.containsKey('Translation')) { + translation = data['Translation']; + } else if (data.containsKey('TranslationList') && + data['TranslationList'] is List && + data['TranslationList'].isNotEmpty) { + final item = data['TranslationList'][0]; + if (item is Map && item.containsKey('Translation')) { + translation = item['Translation']; + } + } + } + } + + // 返回提取到的翻译结果 + return translation; + } + + return null; + } catch (e) { + return null; + } + } +} \ No newline at end of file diff --git a/lib/data/services/volcano_tts_api_service.dart b/lib/data/services/volcano_tts_api_service.dart index 955398f8c..c2d39595c 100644 --- a/lib/data/services/volcano_tts_api_service.dart +++ b/lib/data/services/volcano_tts_api_service.dart @@ -102,8 +102,8 @@ class VolcanoTtsApiService extends GetxController { final RxBool _isPlaying = false.obs; bool get isPlaying => _isPlaying.value; - // 默认发音人 - final String defaultSpeaker = 'zh_female_1'; + // 默认发音人 - 使用预定义的值 + final String defaultSpeaker = 'zh_female_meilinvyou_moon_bigtts'; // 音频缓冲区 final List _audioBuffer = []; diff --git a/lib/main.dart b/lib/main.dart index 2be496940..9c84dfc4c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -67,7 +67,6 @@ class MainApp extends StatelessWidget { initialBinding: InitialBinding(), initialRoute: Routes.home, getPages: AppPages.pages, - defaultTransition: Transition.noTransition, debugShowCheckedModeBanner: false, translations: AppTranslations(), locale: const Locale('zh', 'CN'), diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 7d772910a..d308222ec 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -8,6 +8,7 @@ import '../../../data/services/volcano_tts_api_service.dart'; import '../models/message_model.dart'; import '../controllers/voice_input_controller.dart'; import 'dart:async'; +import '../../../data/providers/agent_provider.dart'; class ChatController extends GetxController { String agentId = ''; @@ -17,6 +18,7 @@ class ChatController extends GetxController { String systemPrompt = ''; String welcomeMessage = ''; bool playVoiceOnEnter = false; + String agentVoice = 'zh_female_meilinvyou_moon_bigtts'; final VolcanoAIService _aiService = VolcanoAIService(); late final VolcanoTtsApiService _ttsService; @@ -341,7 +343,7 @@ class ChatController extends GetxController { // TTS会话状态 bool ttsSessionStarted = false; - const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; + final speaker = agentVoice; // 创建一个标志,用于跟踪流是否应该继续处理 bool shouldContinueProcessing = true; @@ -476,7 +478,7 @@ class ChatController extends GetxController { await _ttsService.connect(); } - const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; + final speaker = agentVoice; await _ttsService.startSession(speaker); await _ttsService.speak(text, speaker: speaker); @@ -552,15 +554,13 @@ class ChatController extends GetxController { final args = arguments as Map; agentId = args['agentId'] as String? ?? ''; agentName = args['agentName'] as String? ?? 'Assistant'; + agentVoice = args['agentVoice'] as String? ?? 'zh_female_meilinvyou_moon_bigtts'; agentAvatar = args['agentAvatar'] as String? ?? 'assets/images/default_avatar.png'; agentSubtitle = args['agentSubtitle'] as String?; systemPrompt = args['systemPrompt'] as String? ?? ''; welcomeMessage = args['welcomeMessage'] as String? ?? 'Hello! How can I help you today?'; playVoiceOnEnter = args['playVoiceOnEnter'] as bool? ?? false; - - if (agentId.isEmpty) { - throw Exception('Invalid agent ID'); - } + } catch (e) { debugPrint('🔴 Error initializing ChatController: $e'); Get.back(); // 返回上一页 diff --git a/lib/modules/chat/views/chat_view.dart b/lib/modules/chat/views/chat_view.dart index 87d138589..2b2d20c3a 100644 --- a/lib/modules/chat/views/chat_view.dart +++ b/lib/modules/chat/views/chat_view.dart @@ -28,12 +28,29 @@ class ChatView extends GetView { backgroundColor: Colors.white, appBar: _buildAppBar(), body: SafeArea( - child: Column( + child: Stack( children: [ - Expanded( - child: _buildMessageList(), + // 代理头像作为背景 + if (controller.agentAvatar.isNotEmpty) + Positioned.fill( + child: Opacity( + opacity: 0.1, // 设置透明度,使其不影响内容阅读 + child: Image.asset( + controller.agentAvatar, + fit: BoxFit.contain, + ), + ), + ), + + // 主要内容 + Column( + children: [ + Expanded( + child: _buildMessageList(), + ), + _buildInputSection(), + ], ), - _buildInputSection(), ], ), ), diff --git a/lib/modules/explore/views/explore_view.dart b/lib/modules/explore/views/explore_view.dart index 237999295..3916ed86f 100644 --- a/lib/modules/explore/views/explore_view.dart +++ b/lib/modules/explore/views/explore_view.dart @@ -25,8 +25,12 @@ class ExploreView extends GetView { // 检查是否是同声翻译官 if (agent.id == 'simultaneous_interpreter') { - // 导航到翻译页面 - Get.toNamed(Routes.translation); + // 导航到翻译页面,并传递代理信息 + Get.toNamed(Routes.translation, arguments: { + 'agentId': agent.id, + 'agentName': agent.name, + 'agentAvatar': agent.avatarUrl, + }); return; } @@ -34,6 +38,7 @@ class ExploreView extends GetView { Get.toNamed('/chat', arguments: { 'agentId': agent.id, 'agentName': agent.name, + 'agentVoice': agent.voice, 'agentAvatar': agent.avatarUrl, 'agentSubtitle': agent.description, 'systemPrompt': agent.systemPrompt, diff --git a/lib/modules/test/bindings/test_binding.dart b/lib/modules/test/bindings/test_binding.dart index bf9431cd5..cb859c5bc 100644 --- a/lib/modules/test/bindings/test_binding.dart +++ b/lib/modules/test/bindings/test_binding.dart @@ -11,17 +11,15 @@ class TtsTestBinding extends Bindings { Get.put(VolcanoTtsApiService(), permanent: true); } - Get.lazyPut( - () => TtsTestController(), - ); + // Use put instead of lazyPut to ensure the controller is created immediately + Get.put(TtsTestController()); } } class AsrTestBinding extends Bindings { @override void dependencies() { - Get.lazyPut( - () => AsrTestController(), - ); + // Use put instead of lazyPut to ensure the controller is created immediately + Get.put(AsrTestController()); } } \ No newline at end of file diff --git a/lib/modules/test/controllers/tts_test_controller.dart b/lib/modules/test/controllers/tts_test_controller.dart index aef169083..87b09d4a5 100644 --- a/lib/modules/test/controllers/tts_test_controller.dart +++ b/lib/modules/test/controllers/tts_test_controller.dart @@ -22,6 +22,20 @@ class TtsTestController extends GetxController { // 当前播放的索引 final currentPlayingIndex = RxInt(-1); + // 当前使用的语音 - 使用预定义的值 + final currentVoice = 'zh_female_meilinvyou_moon_bigtts'.obs; + + // 可用的语音列表 + final List> availableVoices = [ + {'id': 'zh_female_meilinvyou_moon_bigtts', 'name': '魅力女友'}, + {'id': 'zh_male_shaonianzixin_moon_bigtts', 'name': '少年梓辛/Brayan'}, + {'id': 'zh_male_beijingxiaoye_moon_bigtts', 'name': '北京小爷'}, + {'id': 'zh_female_daimengchuanmei_moon_bigtts', 'name': '呆萌川妹'}, + {'id': 'zh_female_wanqudashu_moon_bigtts', 'name': '湾区大叔'}, + {'id': 'zh_female_sajiaonvyou_moon_bigtts', 'name': '撒娇女友'}, + {'id': 'zh_male_shenyeboke_moon_bigtts', 'name': '深夜播客'}, + ]; + // 固定的播报内容 final RxList speechTexts = [ '这是第一条播报测试,请听取合成效果。', @@ -164,20 +178,19 @@ class TtsTestController extends GetxController { } /// 播放单条文本 - Future speakText(int index) async { - if (index < 0 || index >= speechTexts.length || isAudioPlaying.value) { + Future playSingle(int index) async { + if (index < 0 || index >= speechTexts.length) { return; } try { - isLoading.value = true; isSpeaking.value = true; isAudioPlaying.value = true; errorMessage.value = ''; currentPlayingIndex.value = index; final text = speechTexts[index]; - const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; + final speaker = currentVoice.value; // 确保TTS服务已连接 if (!_ttsService.isConnected.value) { @@ -202,7 +215,6 @@ class TtsTestController extends GetxController { print('播放出错: $e'); } } finally { - isLoading.value = false; isSpeaking.value = false; isAudioPlaying.value = false; currentPlayingIndex.value = -1; @@ -248,7 +260,7 @@ class TtsTestController extends GetxController { await _connectToTtsService(); } - const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; + final speaker = currentVoice.value; // 停止之前的播放 await _ttsService.stop(); @@ -327,6 +339,20 @@ class TtsTestController extends GetxController { } } + // 设置当前语音 + void setVoice(String voiceId) { + currentVoice.value = voiceId; + } + + // 获取当前语音名称 + String getCurrentVoiceName() { + final voice = availableVoices.firstWhere( + (v) => v['id'] == currentVoice.value, + orElse: () => {'id': currentVoice.value, 'name': '未知语音'}, + ); + return voice['name'] ?? '未知语音'; + } + @override void onClose() { // 取消事件订阅 diff --git a/lib/modules/test/views/tts_test_view.dart b/lib/modules/test/views/tts_test_view.dart index 3d91782e9..27c2a33ee 100644 --- a/lib/modules/test/views/tts_test_view.dart +++ b/lib/modules/test/views/tts_test_view.dart @@ -132,7 +132,7 @@ class TtsTestView extends GetView { : null, onTap: controller.isAudioPlaying.value || controller.isLoading.value ? null - : () => controller.speakText(index), + : () => controller.playSingle(index), enabled: !(controller.isAudioPlaying.value || controller.isLoading.value), tileColor: isPlaying ? Colors.blue.withOpacity(0.1) : null, shape: RoundedRectangleBorder( diff --git a/lib/modules/translation/bindings/translation_binding.dart b/lib/modules/translation/bindings/translation_binding.dart index 3bdd43ff4..9483707a5 100644 --- a/lib/modules/translation/bindings/translation_binding.dart +++ b/lib/modules/translation/bindings/translation_binding.dart @@ -1,9 +1,26 @@ import 'package:get/get.dart'; import '../controllers/translation_controller.dart'; +import '../../../data/services/azure_asr_service.dart'; +import '../../../data/services/volcano_translation_service.dart'; +import '../../../data/services/volcano_tts_service.dart'; -class TranslationBinding extends Bindings { +class TranslationBinding implements Bindings { @override void dependencies() { - Get.lazyPut(() => TranslationController()); + // Ensure services are registered + if (!Get.isRegistered()) { + Get.put(AzureAsrService()); + } + + if (!Get.isRegistered()) { + Get.put(VolcanoTranslationService()); + } + + if (!Get.isRegistered()) { + Get.put(VolcanoTtsService()); + } + + // Register the controller + Get.put(TranslationController()); } } \ No newline at end of file diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 5d935cbf8..16ec57a58 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -1,334 +1,340 @@ -import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../../../data/services/volcano_asr_service.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'; -import '../models/translation_message_model.dart'; +// import '../../../core/utils/logger.dart'; class TranslationController extends GetxController { - // Observable states - final isListening = false.obs; + // Services + final AzureAsrService _asrService = Get.find(); + final VolcanoTranslationService _translationService = Get.find(); + final VolcanoTtsApiService _ttsService = Get.find(); + + // State variables + final isRecognizing = 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 = [].obs; - - // TTS启用状态 final isTtsEnabled = true.obs; + final recognizedText = ''.obs; + final translatedText = ''.obs; + final sourceLanguage = '中文 (Chinese)'.obs; + final targetLanguage = '英语 (English)'.obs; + final translationHistory = [].obs; - // Services - late final VolcanoAsrService _voiceService; - late final VolcanoTtsApiService _ttsService; + // Current translation item + TranslationItem? currentItem; - // Stream subscriptions + // Subscription for recognition stream StreamSubscription? _recognitionSubscription; - // Timers - Timer? _silenceTimer; - Timer? _speakingCheckTimer; - - // State tracking - bool _hasRecognizedSpeech = false; - bool _hasFinalResult = false; - DateTime? _lastSpeechTime; - Completer? _recognitionCompleter; - - // TTS事件订阅 - StreamSubscription? _ttsEventSubscription; - bool _isTtsSpeaking = false; + // Scroll controller for ListView + final ScrollController scrollController = ScrollController(); + // Timer for debouncing translation requests + Timer? _translationDebounceTimer; + @override void onInit() { super.onInit(); - _initializeServices(); - _startSpeakingCheckTimer(); - _ttsService = Get.find(); + // Initialize the translation service + _initTranslationService(); - // 设置TTS事件监听 - _setupTtsEventListener(); + // Start voice recognition automatically when the page is loaded + startRecognition(); + } + + @override + void onClose() { + stopRecognition(); + _recognitionSubscription?.cancel(); + _translationDebounceTimer?.cancel(); + scrollController.dispose(); + super.onClose(); } - Future _initializeServices() async { + // Initialize the translation service + Future _initTranslationService() async { try { - _voiceService = Get.find(); - _ttsService = Get.find(); - - await _voiceService.initialize(); - await startListening(); + await _translationService.initialize(); } catch (e) { - Logger.error('Failed to initialize translation services', e); Get.snackbar( 'Error', - 'Failed to initialize translation services: $e', + 'Failed to initialize translation service: ${e.toString()}', snackPosition: SnackPosition.BOTTOM, ); } } - - Future startListening() async { - if (isListening.value) return; + + // Toggle TTS feature + void toggleTts() { + isTtsEnabled.toggle(); + } + + // Start voice recognition + Future startRecognition() async { + if (isRecognizing.value) return; try { - isListening.value = true; - originalText.value = ''; - translatedText.value = ''; - _hasRecognizedSpeech = false; - _hasFinalResult = false; + // Initialize a new translation item + currentItem = TranslationItem( + sourceText: '', + translatedText: '', + sourceLanguage: sourceLanguage.value, + targetLanguage: targetLanguage.value, + timestamp: DateTime.now(), + ); - _recognitionCompleter = Completer(); + // Start the voice recognition service + final success = await _asrService.startContinuousRecognition(); - final success = await _voiceService.startContinuousRecognition(); - if (!success) { - isListening.value = false; - throw Exception('Failed to start voice recognition'); - } - - _recognitionSubscription = _voiceService.recognitionStream?.listen( - (event) { + 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.recognizing: + case RecognitionEventType.finalResult: 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(); - } + handleFinalResult(event.text); } break; - - case RecognitionEventType.finalResult: - _hasFinalResult = true; - isUserSpeaking.value = false; - + case RecognitionEventType.recognizing: + // Update the UI with intermediate results 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); + handleIntermediateResult(event.text); } - - // Reset for next utterance - _hasRecognizedSpeech = false; - _hasFinalResult = false; break; - case RecognitionEventType.error: - Logger.error('Voice recognition error', event.error); + Get.snackbar( + 'Error', + 'Recognition error: ${event.error}', + snackPosition: SnackPosition.BOTTOM, + ); break; - default: break; } - }, - onError: (error) { - Logger.error('Voice recognition stream error', error); - isListening.value = false; - restartListening(); - } - ); - + }); + } else { + throw Exception('Failed to start continuous recognition'); + } } catch (e) { - Logger.error('Failed to start listening', e); - isListening.value = false; + isRecognizing.value = false; + Get.snackbar( + 'Error', + 'Failed to start voice recognition: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + ); } } - - Future _processTranslation(String text) async { - if (text.isEmpty) return; + + // Stop voice recognition + Future stopRecognition() async { + if (!isRecognizing.value) 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, - )); + await _asrService.stopContinuousRecognition(); + _recognitionSubscription?.cancel(); + _recognitionSubscription = null; + isRecognizing.value = false; - // Speak the translation if TTS is enabled - if (isTtsEnabled.value) { - await _synthesizeText(translated); - } + // 添加当前项到历史(如果有内容) + finalizeCurrentItem(); + // 重置当前显示的文本 + recognizedText.value = ''; + translatedText.value = ''; } catch (e) { - Logger.error('Translation error', e); - } finally { - isTranslating.value = false; + Get.snackbar( + 'Error', + 'Failed to stop voice recognition: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + ); } } - // This is a placeholder - in a real app, you would call your translation API - Future _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'; + // 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 { - // Simulate English to Chinese translation - return '翻译自英语: $text'; + // 更新当前翻译项 + currentItem!.sourceText = text; } - } - - void switchLanguages() { - final temp = sourceLanguage.value; - sourceLanguage.value = targetLanguage.value; - targetLanguage.value = temp; - // Clear current messages when switching languages - messages.clear(); - } - - Future restartListening() async { - await stopListening(); - await Future.delayed(const Duration(milliseconds: 500)); - await startListening(); - } - - Future stopListening() async { - if (!isListening.value) return; + // 实时更新识别文本 + recognizedText.value = text; - 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(); + // Debounce translation requests to avoid too many API calls + _translationDebounceTimer?.cancel(); + _translationDebounceTimer = Timer(const Duration(milliseconds: 300), () { + translateText(text, isFinal: false); }); - } - - void _checkIfUserIsSpeaking() { - if (_hasRecognizedSpeech && !_hasFinalResult && _lastSpeechTime != null) { - final timeSinceLastSpeech = DateTime.now().difference(_lastSpeechTime!); - if (timeSinceLastSpeech.inMilliseconds < 1000) { - isUserSpeaking.value = true; - return; - } - } - isUserSpeaking.value = false; + // Scroll to the bottom to show the latest content + _scrollToBottom(); } - void _setupTtsEventListener() { - _ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent); - } - - void _handleTtsEvent(Map event) { - final String eventType = event['type'] as String? ?? 'unknown'; + // Handle final recognition result + void handleFinalResult(String text) { + if (text.isEmpty) return; - 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; + // 更新当前翻译项(如果不存在则创建一个) + if (currentItem == null) { + currentItem = TranslationItem( + sourceText: text, + translatedText: '', + sourceLanguage: sourceLanguage.value, + targetLanguage: targetLanguage.value, + timestamp: DateTime.now(), + ); + } else { + currentItem!.sourceText = text; } - } - - @override - void onClose() { - stopListening(); - _speakingCheckTimer?.cancel(); - // 取消TTS事件订阅 - _ttsEventSubscription?.cancel(); + // 更新识别文本 + recognizedText.value = text; - // 结束TTS会话 - _ttsService.endSession(); + // 翻译最终结果 (添加到历史的逻辑已移至translateText方法) + translateText(text, isFinal: true).then((_) { + // 作为安全措施,确保当前项被添加到历史中 + // 这是为了处理translateText中可能因为条件不满足而没有添加到历史的情况 + finalizeCurrentItem(); + }); - super.onClose(); + // Scroll to the bottom to show the latest content + _scrollToBottom(); } - // Add a method to synthesize text using the new TTS service - Future _synthesizeText(String text) async { - if (text.isEmpty || !isTtsEnabled.value) return; + // 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 translateText(String text, {bool isFinal = true}) async { + if (text.isEmpty) return; + + isTranslating.value = true; + + // 保存当前的翻译项引用,确保异步返回时更新正确的项 + final TranslationItem? itemToUpdate = currentItem; try { - // 如果当前有正在进行的TTS会话,先结束它 - if (_isTtsSpeaking) { - await _ttsService.endSession(); - } + final result = await _translationService.translateText( + text: text, + sourceLanguage: sourceLanguage.value, + targetLanguage: targetLanguage.value, + ); - // 确保连接已建立 - if (!_ttsService.isConnected.value) { - final connected = await _ttsService.connect(); - if (!connected) { - print('无法连接到TTS服务'); - return; + 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); + } } } - - // 开始合成 - await _ttsService.synthesize(text, 'zh_female_1'); // 使用默认说话人 } catch (e) { - print('TTS播放出错: $e'); + Get.snackbar( + 'Error', + 'Translation failed: ${e.toString()}', + snackPosition: SnackPosition.BOTTOM, + ); + } finally { + isTranslating.value = false; } } + + // Play the translated text using TTS + Future 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 playTranslationItem(TranslationItem item) async { + await playTranslatedText(item.translatedText); + } - // Add a method to toggle TTS - void toggleTTS() { - isTtsEnabled.toggle(); - if (!isTtsEnabled.value) { - _ttsService.endSession(); + // 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, + }); } \ No newline at end of file diff --git a/lib/modules/translation/models/translation_message_model.dart b/lib/modules/translation/models/translation_message_model.dart deleted file mode 100644 index 24ea7fd11..000000000 --- a/lib/modules/translation/models/translation_message_model.dart +++ /dev/null @@ -1,13 +0,0 @@ -class TranslationMessage { - final String original; - final String translated; - final DateTime timestamp; - final bool isFromUser; - - TranslationMessage({ - required this.original, - required this.translated, - required this.timestamp, - required this.isFromUser, - }); -} \ No newline at end of file diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 2c5de867a..430126147 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../controllers/translation_controller.dart'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/theme/text_styles.dart'; -import '../widgets/translation_message_item.dart'; +// import 'package:intl/intl.dart'; class TranslationView extends GetView { const TranslationView({Key? key}) : super(key: key); @@ -11,178 +10,175 @@ class TranslationView extends GetView { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: _buildLanguageSelector(), - centerTitle: true, - backgroundColor: Colors.white, - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), - onPressed: () => Get.back(), - ), - actions: [ - IconButton( - icon: const Icon(Icons.settings_outlined, color: Colors.black), - onPressed: () { - // Navigate to settings - }, - ), - ], - ), - body: Container( - color: Colors.white, - child: Column( - children: [ - Expanded( - child: _buildMessagesList(), - ), - _buildBottomSection(), - ], - ), - ), + appBar: _buildAppBar(), + body: _buildBody(), ); } - Widget _buildLanguageSelector() { - return Obx(() { - return Row( + PreferredSizeWidget _buildAppBar() { + return AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: Colors.black, + size: 20.sp, + ), + onPressed: () { + // Stop recognition before navigating back + controller.stopRecognition(); + Get.back(); + }, + ), + title: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - controller.sourceLanguage.value, + Obx(() => Text( + controller.sourceLanguage.value.split(' ')[0], // Just show the Chinese part style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, + fontSize: 16.sp, + fontWeight: FontWeight.w600, color: Colors.black, ), + )), + SizedBox(width: 4.w), + Icon( + Icons.arrow_forward_ios, + color: Colors.black, + size: 16.sp, ), - IconButton( - icon: const Icon(Icons.swap_horiz, color: Color(0xFF4A6FE5)), - onPressed: controller.switchLanguages, - ), - Text( - controller.targetLanguage.value, + SizedBox(width: 4.w), + Obx(() => Text( + controller.targetLanguage.value.split(' ')[0], // Just show the English part style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, + fontSize: 16.sp, + fontWeight: FontWeight.w600, color: Colors.black, ), - ), + )), ], - ); - }); + ), + centerTitle: true, + actions: [ + // TTS toggle button + Obx(() => IconButton( + icon: Icon( + controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, + color: Colors.black, + size: 24.sp, + ), + onPressed: controller.toggleTts, + tooltip: '自动播放翻译结果', + )), + ], + ); + } + + Widget _buildBody() { + return Column( + children: [ + Expanded( + child: _buildTranslationContent(), + ), + _buildControlBar(), + ], + ); } - Widget _buildMessagesList() { + Widget _buildTranslationContent() { return Obx(() { - if (controller.messages.isEmpty) { - return _buildWelcomeMessage(); - } + final itemCount = controller.translationHistory.length; + + // 添加对recognizedText和translatedText的监听 + final hasRecognizedText = controller.recognizedText.value.isNotEmpty; + final hasTranslatedText = controller.translatedText.value.isNotEmpty; + final showCurrentText = hasRecognizedText || hasTranslatedText; return ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - itemCount: controller.messages.length, - reverse: true, + controller: controller.scrollController, + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), + itemCount: itemCount + (showCurrentText ? 1 : 0), itemBuilder: (context, index) { - final message = controller.messages[controller.messages.length - 1 - index]; - return TranslationMessageItem(message: message); + // Current item (being translated) + if (showCurrentText && index == itemCount) { + return _buildTranslationItemWidget( + controller.recognizedText.value, + controller.translatedText.value, + true, + ); + } + + // History items + final historyItem = controller.translationHistory[index]; + return _buildTranslationItemWidget( + historyItem.sourceText, + historyItem.translatedText, + false, + item: historyItem, + ); }, ); }); } - Widget _buildWelcomeMessage() { - return Padding( - padding: const EdgeInsets.all(16.0), + Widget _buildTranslationItemWidget(String sourceText, String translatedText, bool isCurrent, {TranslationItem? item}) { + return Container( + margin: EdgeInsets.only(bottom: 24.h), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(12.r), + ), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // First welcome message + // Source text Container( width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.grey[300]!, - width: 0.5, + padding: EdgeInsets.all(16.r), + child: Text( + sourceText.isEmpty && isCurrent ? '等待语音输入...' : sourceText, + style: TextStyle( + fontSize: 16.sp, + color: sourceText.isEmpty && isCurrent ? Colors.grey : Colors.black, ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Welcome to Felo Translator. 👆 Click the button above to switch languages", - style: TextStyle( - fontSize: 14, - color: Colors.grey[700], - height: 1.5, - ), - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: Text( - "Welcome to Felo Translator. 👆 Click the button above to switch languages", - style: TextStyle( - fontSize: 14, - color: Colors.grey[800], - height: 1.5, - ), - ), - ), - const SizedBox(width: 8), - Icon(Icons.volume_up, color: Colors.grey[600]), - ], - ), - ], - ), ), - const SizedBox(height: 20), + // Divider + Divider(height: 1, thickness: 1, color: Colors.grey[200]), - // Second instruction message + // Translated text Container( width: double.infinity, - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: const Color(0xFFEEF4FF), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xFFD0DFFF), - width: 0.5, - ), - ), - child: Column( + padding: EdgeInsets.all(16.r), + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "👇 Click the button below ▶️ to start speaking, and Felo will automatically recognize the language for translation", - style: TextStyle( - fontSize: 14, - color: Colors.grey[700], - height: 1.5, + Expanded( + child: Text( + translatedText.isEmpty && isCurrent ? '翻译结果将显示在这里...' : translatedText, + style: TextStyle( + fontSize: 16.sp, + color: translatedText.isEmpty && isCurrent ? Colors.grey : Colors.black, + ), ), ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: Text( - "👇 Click the button below ▶️ to start speaking, and Felo will automatically recognize the language for translation", - style: TextStyle( - fontSize: 14, - color: Colors.grey[800], - height: 1.5, - ), - ), + if (translatedText.isNotEmpty) + IconButton( + icon: Icon( + Icons.volume_up, + color: Colors.blue, + size: 20.sp, ), - const SizedBox(width: 8), - Icon(Icons.volume_up, color: Colors.grey[600]), - ], - ), + onPressed: () { + if (isCurrent) { + controller.playTranslatedText(translatedText); + } else if (item != null) { + controller.playTranslationItem(item); + } + }, + ), ], ), ), @@ -191,112 +187,46 @@ class TranslationView extends GetView { ); } - Widget _buildBottomSection() { + Widget _buildControlBar() { return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), + color: Colors.black.withAlpha(13), // 0.05 * 255 ≈ 13 blurRadius: 4, offset: const Offset(0, -2), ), ], ), - child: Column( - children: [ - _buildRecognitionStatus(), - const SizedBox(height: 8), - _buildControlButtons(), - ], - ), - ); - } - - Widget _buildRecognitionStatus() { - return Obx(() { - final isListening = controller.isListening.value; - final isUserSpeaking = controller.isUserSpeaking.value; - - if (!isListening) { - return const SizedBox.shrink(); - } - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(24), - border: Border.all( - color: Colors.grey[300]!, - width: 0.5, - ), - ), - child: Row( - children: [ - if (isUserSpeaking) - _buildSpeakingIndicator() - else - Icon(Icons.mic, color: Colors.grey[600]), - const SizedBox(width: 12), - Expanded( - child: Text( - isUserSpeaking - ? "Listening..." - : "Tap the mic button to start speaking", - style: TextStyle( - fontSize: 14, - color: Colors.grey[700], - height: 1.5, - ), + child: Center( + child: Obx(() { + final isRecognizing = controller.isRecognizing.value; + return GestureDetector( + onTap: () { + if (isRecognizing) { + controller.stopRecognition(); + } else { + controller.startRecognition(); + } + }, + child: Container( + width: 60.w, + height: 60.w, + decoration: BoxDecoration( + color: isRecognizing ? Colors.red : Colors.blue, + shape: BoxShape.circle, + ), + child: Icon( + isRecognizing ? Icons.pause : Icons.play_arrow, + color: Colors.white, + size: 30.sp, ), - ), - ], - ), - ); - }); - } - - Widget _buildSpeakingIndicator() { - return SizedBox( - width: 24, - height: 24, - child: Stack( - alignment: Alignment.center, - children: [ - Icon(Icons.mic, color: const Color(0xFF4A6FE5)), - Positioned.fill( - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: const AlwaysStoppedAnimation(Color(0xFF4A6FE5)), - ), - ), - ], - ), - ); - } - - Widget _buildControlButtons() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Obx(() { - final isListening = controller.isListening.value; - - return FloatingActionButton( - onPressed: isListening - ? controller.stopListening - : controller.startListening, - backgroundColor: isListening ? Colors.red : const Color(0xFF4A6FE5), - child: Icon( - isListening ? Icons.stop : Icons.mic, - size: 24, - color: Colors.white, ), ); }), - ], + ), ); } } \ No newline at end of file diff --git a/lib/modules/translation/widgets/translation_message_item.dart b/lib/modules/translation/widgets/translation_message_item.dart deleted file mode 100644 index 615f4ee26..000000000 --- a/lib/modules/translation/widgets/translation_message_item.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; -import '../models/translation_message_model.dart'; - -class TranslationMessageItem extends StatelessWidget { - final TranslationMessage message; - - const TranslationMessageItem({ - Key? key, - required this.message, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Original text - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFFF5F5F5), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.grey[300]!, - width: 0.5, - ), - ), - child: Text( - message.original, - style: TextStyle( - color: Colors.grey[800], - fontSize: 14, - height: 1.5, - ), - ), - ), - - const SizedBox(height: 8), - - // Translated text - Container( - width: double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFFEEF4FF), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0xFFD0DFFF), - width: 0.5, - ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - message.translated, - style: TextStyle( - color: Colors.black87, - fontSize: 14, - fontWeight: FontWeight.w500, - height: 1.5, - ), - ), - ), - const SizedBox(width: 8), - Icon( - Icons.volume_up, - color: Colors.grey[600], - size: 20, - ), - ], - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 96e3476ce..94502895b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: web_socket_channel: ^2.4.4 flutter_dotenv: ^5.1.0 synchronized: ^3.1.0 + intl: ^0.19.0 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git