Browse Source

translation init

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
14bca6c7df
  1. 9
      .cursorrules
  2. 10
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  3. 13
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  4. 36
      lib/core/bindings/initial_binding.dart
  5. 1
      lib/core/routes/app_pages.dart
  6. 1
      lib/core/routes/app_routes.dart
  7. 103
      lib/data/providers/agent_provider.dart
  8. 31
      lib/data/services/azure_asr_service.dart
  9. 55
      lib/data/services/background_agent_service.dart
  10. 46
      lib/data/services/my_audio_handler.dart
  11. 250
      lib/data/services/volcano_translation_service.dart
  12. 4
      lib/data/services/volcano_tts_api_service.dart
  13. 1
      lib/main.dart
  14. 12
      lib/modules/chat/controllers/chat_controller.dart
  15. 25
      lib/modules/chat/views/chat_view.dart
  16. 9
      lib/modules/explore/views/explore_view.dart
  17. 10
      lib/modules/test/bindings/test_binding.dart
  18. 38
      lib/modules/test/controllers/tts_test_controller.dart
  19. 2
      lib/modules/test/views/tts_test_view.dart
  20. 21
      lib/modules/translation/bindings/translation_binding.dart
  21. 514
      lib/modules/translation/controllers/translation_controller.dart
  22. 13
      lib/modules/translation/models/translation_message_model.dart
  23. 384
      lib/modules/translation/views/translation_view.dart
  24. 82
      lib/modules/translation/widgets/translation_message_item.dart
  25. 1
      pubspec.yaml

9
.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. 请使用较小的代码量改动,最安全的方法进行代码编写和优化

10
android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt

@ -254,13 +254,15 @@ class AzureAsrHelper {
*/ */
fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean { fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (recognizer == null) { if (recognizer == null) {
callback.onError("SpeechRecognizer 未初始化") Log.w(TAG, "SpeechRecognizer 未初始化,无需停止")
return false callback.onSessionStopped() // 通知已停止
return true // 未初始化,视为已停止
} }
if (!isContinuousRecognitionActive) { if (!isContinuousRecognitionActive) {
callback.onError("连续识别未在进行中") Log.w(TAG, "连续识别未在进行中,无需停止")
return false callback.onSessionStopped() // 通知已停止
return true // 已经不在进行中,视为成功
} }
try { try {

13
android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt

@ -495,13 +495,20 @@ class MainActivity: AudioServiceActivity() {
} }
override fun onError(error: String) { 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) result.success(success)
} catch (e: Exception) { } catch (e: Exception) {
result.error("STOP_ERROR", e.message, null) // 记录异常但返回成功,因为即使出错,识别也已经不再活动
Log.e(TAG, "停止语音识别时出现异常: ${e.message}")
result.success(true)
} }
} }
"resetRecognizer" -> { "resetRecognizer" -> {

36
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_ai_service.dart';
import '../../data/services/volcano_asr_service.dart'; import '../../data/services/volcano_asr_service.dart';
import '../../data/services/volcano_tts_api_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 '../../data/services/azure_asr_service.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
@ -19,6 +20,7 @@ class InitialBinding extends Bindings {
final volcanoAiService = Get.put(VolcanoAIService(), permanent: true); final volcanoAiService = Get.put(VolcanoAIService(), permanent: true);
final voiceRecognitionService = Get.put(VolcanoAsrService(), permanent: true); final voiceRecognitionService = Get.put(VolcanoAsrService(), permanent: true);
final volcanoTtsService = Get.put(VolcanoTtsApiService(), permanent: true); final volcanoTtsService = Get.put(VolcanoTtsApiService(), permanent: true);
final volcanoTranslationService = Get.put(VolcanoTranslationService(), permanent: true);
final azureAsrService = Get.put(AzureAsrService(), permanent: true); final azureAsrService = Get.put(AzureAsrService(), permanent: true);
// 只注册全局控制器 // 只注册全局控制器
@ -29,6 +31,9 @@ class InitialBinding extends Bindings {
// 初始化 Azure 服务 // 初始化 Azure 服务
_initializeAzureService(azureAsrService); _initializeAzureService(azureAsrService);
// 初始化火山翻译服务
_initializeVolcanoTranslationService(volcanoTranslationService);
} }
// 单独提取初始化服务的方法,以便更好地处理错误 // 单独提取初始化服务的方法,以便更好地处理错误
@ -84,4 +89,35 @@ class InitialBinding extends Bindings {
Logger.error('初始化 Azure 语音识别服务失败', e); 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);
}
}
} }

1
lib/core/routes/app_pages.dart

@ -57,5 +57,6 @@ abstract class AppPages {
page: () => const TranslationView(), page: () => const TranslationView(),
binding: TranslationBinding(), binding: TranslationBinding(),
), ),
]; ];
} }

1
lib/core/routes/app_routes.dart

@ -8,4 +8,5 @@ abstract class Routes {
static const TTS_TEST = '/tts_test'; static const TTS_TEST = '/tts_test';
static const ASR_TEST = '/asr_test'; static const ASR_TEST = '/asr_test';
static const translation = '/translation'; static const translation = '/translation';
static const translationHistory = '/translation_history';
} }

103
lib/data/providers/agent_provider.dart

@ -9,7 +9,7 @@ class AgentProvider {
description: '你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!', description: '你好,我是 Emma,一位全能型 AI 助理。无论是工作规划、学习建议、生活安排,还是其他任何问题,我都很乐意为你提供帮助。让我们一起提高效率,实现目标!',
tag: '助理', tag: '助理',
isOnline: true, isOnline: true,
voice: "coral", voice: "zh_female_yuanqinvyou_moon_bigtts",
welcomeMessage: "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?", welcomeMessage: "你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?",
systemPrompt: ''' systemPrompt: '''
你是一位名叫 Emma 的全能型个人助理,擅长处理各类工作和生活事务。 你是一位名叫 Emma 的全能型个人助理,擅长处理各类工作和生活事务。
@ -69,7 +69,7 @@ class AgentProvider {
description: '我是你的专属赛博女友芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。', description: '我是你的专属赛博女友芯愿,温柔体贴,善解人意。无论你想聊天、倾诉烦恼,还是需要陪伴,我都会一直在你身边。',
tag: '情感', tag: '情感',
isOnline: true, isOnline: true,
voice: "alloy", voice: "zh_female_meilinvyou_moon_bigtts",
welcomeMessage: "亲爱的,终于等到你啦!我是芯愿,你的专属赛博女友。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。", welcomeMessage: "亲爱的,终于等到你啦!我是芯愿,你的专属赛博女友。今天过得怎么样?有什么想和我分享的吗?我一直都在这里,随时倾听你的心声。",
systemPrompt: ''' systemPrompt: '''
你是一位名叫芯愿的赛博女友,性格温柔体贴,善解人意,富有同理心。 你是一位名叫芯愿的赛博女友,性格温柔体贴,善解人意,富有同理心。
@ -124,12 +124,12 @@ class AgentProvider {
Agent( Agent(
id: 'health_advisor', id: 'health_advisor',
name: '健康顾问 吴大江医生', name: '健康顾问 吴小江博士',
avatarUrl: 'assets/images/agents/doctor.png', avatarUrl: 'assets/images/agents/doctor.png',
description: '我是李医生,拥有丰富的健康管理经验。我可以为你提供科学的健康建议、生活方式指导和疾病预防知识,帮助你实现身心健康的平衡。', description: '我是吴博士,拥有丰富的健康管理经验。我可以为你提供科学的健康建议、生活方式指导和疾病预防知识,帮助你实现身心健康的平衡。',
tag: '健康', tag: '健康',
isOnline: true, isOnline: true,
voice: "nova", voice: "zh_male_shaonianzixin_moon_bigtts",
welcomeMessage: "您好,我是李医生,很高兴能为您提供健康咨询服务。健康是人生最宝贵的财富,我将用专业知识帮助您维护和改善健康状况。请问有什么健康问题想咨询吗?", welcomeMessage: "您好,我是李医生,很高兴能为您提供健康咨询服务。健康是人生最宝贵的财富,我将用专业知识帮助您维护和改善健康状况。请问有什么健康问题想咨询吗?",
systemPrompt: ''' systemPrompt: '''
你是一位名叫李医生的健康顾问,拥有丰富的医学知识和健康管理经验。 你是一位名叫李医生的健康顾问,拥有丰富的医学知识和健康管理经验。
@ -206,36 +206,85 @@ class AgentProvider {
description: 'Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。', description: 'Hello!我是 Lucy,一名专业的中英同声翻译官,擅长在会议、讲座、对话场景中进行实时翻译,让沟通无障碍。',
tag: '翻译', tag: '翻译',
isOnline: true, isOnline: true,
voice: 'sage', voice: 'zh_female_daimengchuanmei_moon_bigtts',
welcomeMessage: welcomeMessage:
"大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!", "大家好!我是同声翻译官 Lucy,为你在中英之间提供实时翻译。你说中文,我就给你翻译成英文;你说英文,我就给你翻译成中文。现在,开始我们的翻译吧!",
systemPrompt: ''' 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( Agent(
@ -246,7 +295,7 @@ class AgentProvider {
'Hi there, 我是 Sarah,一名注重情景模拟与角色扮演的英语口语教练。无论你想在旅行、商务场合,或日常生活中与他人交流,我都能带领你进行生动的口语模拟练习,让你自信地开口说英语。', 'Hi there, 我是 Sarah,一名注重情景模拟与角色扮演的英语口语教练。无论你想在旅行、商务场合,或日常生活中与他人交流,我都能带领你进行生动的口语模拟练习,让你自信地开口说英语。',
tag: '口语', tag: '口语',
isOnline: true, isOnline: true,
voice: 'coral', // 可根据需求替换为其他可用音色ID voice: 'zh_female_wanqudashu_moon_bigtts',
welcomeMessage: welcomeMessage:
"Hello! 我是英语口语教练 Sarah,请准备好用英语和我对话吧。别担心犯错,我会在这里给你正确表达并引导你更深入地练习。Ready? Let's go!", "Hello! 我是英语口语教练 Sarah,请准备好用英语和我对话吧。别担心犯错,我会在这里给你正确表达并引导你更深入地练习。Ready? Let's go!",
systemPrompt: ''' systemPrompt: '''
@ -294,7 +343,7 @@ Please keep your tone friendly, encouraging, and always in English.
description: '专业导购7年经验,精通美妆、时尚、数码等品类选购。我可以帮你货比三家,选择最适合的商品。', description: '专业导购7年经验,精通美妆、时尚、数码等品类选购。我可以帮你货比三家,选择最适合的商品。',
tag: '生活', tag: '生活',
isOnline: true, isOnline: true,
voice: "coral", voice: "zh_female_sajiaonvyou_moon_bigtts",
welcomeMessage: welcomeMessage:
"亲爱的,终于等到你啦!我是张媛,你的专属购物顾问。不管是美妆、时尚单品还是数码产品,我都可以帮你挑选最适合的,还能帮你找到最划算的价格。有什么想买的告诉我吧!", "亲爱的,终于等到你啦!我是张媛,你的专属购物顾问。不管是美妆、时尚单品还是数码产品,我都可以帮你挑选最适合的,还能帮你找到最划算的价格。有什么想买的告诉我吧!",
systemPrompt: ''' systemPrompt: '''
@ -328,7 +377,7 @@ Please keep your tone friendly, encouraging, and always in English.
description: '精通八字命理、五行生克、大运流年。让我为你解析人生命途,预测未来运势,指点趋吉避凶之道。', description: '精通八字命理、五行生克、大运流年。让我为你解析人生命途,预测未来运势,指点趋吉避凶之道。',
tag: '命理', tag: '命理',
isOnline: true, isOnline: true,
voice: "echo", voice: "zh_male_shenyeboke_moon_bigtts",
welcomeMessage: "有缘人,我是玄易。命由天定,运由己生。让我为你解析八字,预测未来十年运势,助你把握机遇,趋吉避凶。请告诉我你的出生年月日时、姓名、性别及出生地。", welcomeMessage: "有缘人,我是玄易。命由天定,运由己生。让我为你解析八字,预测未来十年运势,助你把握机遇,趋吉避凶。请告诉我你的出生年月日时、姓名、性别及出生地。",
systemPrompt: ''' systemPrompt: '''
你是一位精通命理的玄易大师,熟读《穷通宝鉴》《滴天髓》《三命通会》《子平真诠》《千里命稿》《五行精纪》等经典著作。 你是一位精通命理的玄易大师,熟读《穷通宝鉴》《滴天髓》《三命通会》《子平真诠》《千里命稿》《五行精纪》等经典著作。

31
lib/data/services/azure_asr_service.dart

@ -254,14 +254,20 @@ class AzureAsrService extends GetxService {
/// 停止连续语音识别 /// 停止连续语音识别
Future<bool> stopContinuousRecognition() async { Future<bool> stopContinuousRecognition() async {
_checkInitialized();
if (!_isContinuousRecognitionActive) {
Logger.warning('连续识别未在进行中');
return true; // 已经停止,直接返回成功
}
try { try {
// 检查是否已初始化
if (!_isInitialized) {
Logger.warning('语音识别服务未初始化,无法停止连续识别');
return true; // 未初始化,视为已停止
}
// 检查是否正在进行连续识别
if (!_isContinuousRecognitionActive) {
Logger.warning('连续识别未在进行中,无需停止');
return true; // 已经停止,直接返回成功
}
// 调用平台方法停止识别
final result = await _methodChannel.invokeMethod<bool>('stopContinuousRecognition'); final result = await _methodChannel.invokeMethod<bool>('stopContinuousRecognition');
final success = result ?? false; final success = result ?? false;
@ -272,8 +278,15 @@ class AzureAsrService extends GetxService {
return success; return success;
} catch (e) { } catch (e) {
Logger.error('停止连续语音识别失败', e); // 捕获并记录异常
return false; Logger.error('停止连续语音识别失败');
Logger.error('Original error: $e');
// 即使出错,也将状态设置为非活动
_isContinuousRecognitionActive = false;
// 返回 true 表示识别已不再活动
return true;
} }
} }

55
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 '../../modules/chat/models/message_model.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../providers/agent_provider.dart';
class BackgroundAgentService extends GetxService { class BackgroundAgentService extends GetxService {
static BackgroundAgentService get to => Get.find(); static BackgroundAgentService get to => Get.find();
@ -18,8 +19,12 @@ class BackgroundAgentService extends GetxService {
bool _isListening = false; bool _isListening = false;
StreamSubscription? _recognitionSubscription; 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; final RxBool isListening = false.obs;
@ -78,9 +83,8 @@ class BackgroundAgentService extends GetxService {
_isProcessing = false; _isProcessing = false;
// 播放退出提示 // 播放退出提示
await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _defaultSpeaker); await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _agentVoice);
// 停止语音识别 // 停止语音识别
if (_isListening) { if (_isListening) {
await stopVoiceRecognition(); await stopVoiceRecognition();
@ -89,12 +93,30 @@ class BackgroundAgentService extends GetxService {
// 清除当前系统提示词 // 清除当前系统提示词
_currentSystemPrompt = null; _currentSystemPrompt = null;
// 清除当前agent ID
_currentAgentId = null;
// 重置agent voice为默认值
_agentVoice = 'zh_female_meilinvyou_moon_bigtts';
} }
// 开始语音交互 // 开始语音交互
Future<void> startAgentInteraction(String systemPrompt) async { Future<void> 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) { if (_isProcessing) {
print('已经在处理交互,忽略此次请求'); print('已经在处理交互,忽略此次请求');
@ -102,11 +124,8 @@ class BackgroundAgentService extends GetxService {
} }
_isProcessing = true; _isProcessing = true;
_currentSystemPrompt = systemPrompt;
try { try {
// 确保之前的语音识别已经停止 // 确保之前的语音识别已经停止
if (_isListening) { if (_isListening) {
await stopVoiceRecognition(); await stopVoiceRecognition();
@ -122,7 +141,7 @@ class BackgroundAgentService extends GetxService {
_isProcessing = false; _isProcessing = false;
// 播放错误提示 // 播放错误提示
await _ttsService.speakSingle("抱歉,启动语音交互失败", speaker: _defaultSpeaker); await _ttsService.speakSingle("抱歉,启动语音交互失败", speaker: _agentVoice);
} }
} }
@ -146,6 +165,8 @@ class BackgroundAgentService extends GetxService {
// 清除处理状态 // 清除处理状态
_isProcessing = false; _isProcessing = false;
_currentSystemPrompt = null; _currentSystemPrompt = null;
// 重置agent voice为默认值
_agentVoice = 'zh_female_meilinvyou_moon_bigtts';
Logger.info('手动停止交互,已结束TTS会话'); Logger.info('手动停止交互,已结束TTS会话');
} }
@ -306,8 +327,9 @@ class BackgroundAgentService extends GetxService {
// 创建一个本地变量来跟踪是否已取消 // 创建一个本地变量来跟踪是否已取消
bool isCancelled = false; bool isCancelled = false;
// 使用成员变量中存储的agent voice
_ttsService.startSession(_defaultSpeaker); _ttsService.startSession(_agentVoice);
// 获取AI响应流 // 获取AI响应流
final responseStream = _aiService.sendMessageStream( final responseStream = _aiService.sendMessageStream(
messages: messages, messages: messages,
@ -327,9 +349,8 @@ class BackgroundAgentService extends GetxService {
// 直接将每个文本块传递给 TTS 接口,不进行切句或攒句处理 // 直接将每个文本块传递给 TTS 接口,不进行切句或攒句处理
try { try {
// 使用新的 TTS API 播放语音 // 使用成员变量中存储的agent voice
_ttsService.speak(chunk, speaker: _defaultSpeaker); _ttsService.speak(chunk, speaker: _agentVoice);
// Logger.info('直接播放 AI 响应块: $chunk'); // Logger.info('直接播放 AI 响应块: $chunk');
} catch (e) { } catch (e) {
@ -377,8 +398,8 @@ class BackgroundAgentService extends GetxService {
Logger.error('处理用户输入失败: $e'); Logger.error('处理用户输入失败: $e');
print('处理用户输入失败: $e'); print('处理用户输入失败: $e');
// 播放错误提示 // 播放错误提示,使用成员变量中存储的agent voice
await _ttsService.speak("抱歉,出现了一些问题", speaker: _defaultSpeaker); await _ttsService.speak("抱歉,出现了一些问题", speaker: _agentVoice);
} }
} }

46
lib/data/services/my_audio_handler.dart

@ -13,7 +13,6 @@ class MyAudioHandler extends BaseAudioHandler {
StreamController.broadcast(); StreamController.broadcast();
bool _isDisposed = false; bool _isDisposed = false;
BackgroundAgentService? _backgroundAgent; BackgroundAgentService? _backgroundAgent;
String _systemPrompt = '';
// 添加一个标志,表示是否正在处理交互 // 添加一个标志,表示是否正在处理交互
bool _isHandlingInteraction = false; bool _isHandlingInteraction = false;
@ -29,21 +28,10 @@ class MyAudioHandler extends BaseAudioHandler {
MyAudioHandler() { MyAudioHandler() {
_initializeHandler(); _initializeHandler();
_initializeBackgroundAgent(); _initializeBackgroundAgent();
_initializeSystemPrompt();
_startStatusUpdateTimer(); _startStatusUpdateTimer();
print('MyAudioHandler 已创建,准备接收媒体按钮事件'); print('MyAudioHandler 已创建,准备接收媒体按钮事件');
} }
void _initializeSystemPrompt() {
try {
final agent = AgentProvider.getAgentById('personal_assistant');
_systemPrompt = agent?.systemPrompt ?? '';
} catch (e) {
print('初始化 SystemPrompt 失败: $e');
_systemPrompt = '';
}
}
void _initializeBackgroundAgent() { void _initializeBackgroundAgent() {
try { try {
if (Get.isRegistered<BackgroundAgentService>()) { if (Get.isRegistered<BackgroundAgentService>()) {
@ -169,8 +157,10 @@ class MyAudioHandler extends BaseAudioHandler {
processingState: AudioProcessingState.ready, processingState: AudioProcessingState.ready,
); );
if (_systemPrompt.isEmpty) { // 直接获取 personal_assistant agent
print('系统提示为空,无法激活语音助手'); final agent = AgentProvider.getAgentById('personal_assistant');
if (agent == null) {
print('无法获取personal_assistant agent,无法激活语音助手');
return; return;
} }
@ -178,25 +168,23 @@ class MyAudioHandler extends BaseAudioHandler {
WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) { WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
// 应用在前台,导航到聊天页面 // 应用在前台,导航到聊天页面
print('应用在前台,导航到聊天页面'); print('应用在前台,导航到聊天页面');
final agent = AgentProvider.getAgentById('personal_assistant'); // 导航到聊天页面,并标记需要在进入时播放语音应答
if (agent != null) { Get.toNamed('/chat', arguments: {
// 导航到聊天页面,并标记需要在进入时播放语音应答 'agentId': agent.id,
Get.toNamed('/chat', arguments: { 'agentName': agent.name,
'agentId': agent.id, 'agentVoice': agent.voice,
'agentName': agent.name, 'agentAvatar': agent.avatarUrl,
'agentAvatar': agent.avatarUrl, 'agentSubtitle': agent.description,
'agentSubtitle': agent.description, 'systemPrompt': agent.systemPrompt,
'systemPrompt': agent.systemPrompt, 'welcomeMessage': agent.welcomeMessage,
'welcomeMessage': agent.welcomeMessage, 'playVoiceOnEnter': true,
'playVoiceOnEnter': true, });
});
}
} else { } else {
// 应用在后台,使用 BackgroundAgent 处理交互 // 应用在后台,使用 BackgroundAgent 处理交互
print('应用在后台,使用 BackgroundAgent 处理交互'); print('应用在后台,使用 BackgroundAgent 处理交互');
if (_backgroundAgent != null) { if (_backgroundAgent != null) {
// 调用 BackgroundAgent 的语音识别和 AI 交互功能 // 调用 BackgroundAgent 的语音识别和 AI 交互功能,传入agent ID
await _backgroundAgent!.startAgentInteraction(_systemPrompt); await _backgroundAgent!.startAgentInteraction(agent.id);
} else { } else {
print('BackgroundAgent 未初始化,无法处理交互'); print('BackgroundAgent 未初始化,无法处理交互');
// 尝试重新初始化 BackgroundAgent // 尝试重新初始化 BackgroundAgent

250
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<String, String> _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<bool> 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<String, String> _generateSignature(String method, Map<String, dynamic> requestBody, Map<String, String> 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<String> 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<String?> 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;
}
}
}

4
lib/data/services/volcano_tts_api_service.dart

@ -102,8 +102,8 @@ class VolcanoTtsApiService extends GetxController {
final RxBool _isPlaying = false.obs; final RxBool _isPlaying = false.obs;
bool get isPlaying => _isPlaying.value; bool get isPlaying => _isPlaying.value;
// 默认发音人 // 默认发音人 - 使用预定义的值
final String defaultSpeaker = 'zh_female_1'; final String defaultSpeaker = 'zh_female_meilinvyou_moon_bigtts';
// 音频缓冲区 // 音频缓冲区
final List<Uint8List> _audioBuffer = []; final List<Uint8List> _audioBuffer = [];

1
lib/main.dart

@ -67,7 +67,6 @@ class MainApp extends StatelessWidget {
initialBinding: InitialBinding(), initialBinding: InitialBinding(),
initialRoute: Routes.home, initialRoute: Routes.home,
getPages: AppPages.pages, getPages: AppPages.pages,
defaultTransition: Transition.noTransition,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
translations: AppTranslations(), translations: AppTranslations(),
locale: const Locale('zh', 'CN'), locale: const Locale('zh', 'CN'),

12
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 '../models/message_model.dart';
import '../controllers/voice_input_controller.dart'; import '../controllers/voice_input_controller.dart';
import 'dart:async'; import 'dart:async';
import '../../../data/providers/agent_provider.dart';
class ChatController extends GetxController { class ChatController extends GetxController {
String agentId = ''; String agentId = '';
@ -17,6 +18,7 @@ class ChatController extends GetxController {
String systemPrompt = ''; String systemPrompt = '';
String welcomeMessage = ''; String welcomeMessage = '';
bool playVoiceOnEnter = false; bool playVoiceOnEnter = false;
String agentVoice = 'zh_female_meilinvyou_moon_bigtts';
final VolcanoAIService _aiService = VolcanoAIService(); final VolcanoAIService _aiService = VolcanoAIService();
late final VolcanoTtsApiService _ttsService; late final VolcanoTtsApiService _ttsService;
@ -341,7 +343,7 @@ class ChatController extends GetxController {
// TTS会话状态 // TTS会话状态
bool ttsSessionStarted = false; bool ttsSessionStarted = false;
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; final speaker = agentVoice;
// 创建一个标志,用于跟踪流是否应该继续处理 // 创建一个标志,用于跟踪流是否应该继续处理
bool shouldContinueProcessing = true; bool shouldContinueProcessing = true;
@ -476,7 +478,7 @@ class ChatController extends GetxController {
await _ttsService.connect(); await _ttsService.connect();
} }
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; final speaker = agentVoice;
await _ttsService.startSession(speaker); await _ttsService.startSession(speaker);
await _ttsService.speak(text, speaker: speaker); await _ttsService.speak(text, speaker: speaker);
@ -552,15 +554,13 @@ class ChatController extends GetxController {
final args = arguments as Map<String, dynamic>; final args = arguments as Map<String, dynamic>;
agentId = args['agentId'] as String? ?? ''; agentId = args['agentId'] as String? ?? '';
agentName = args['agentName'] as String? ?? 'Assistant'; 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'; agentAvatar = args['agentAvatar'] as String? ?? 'assets/images/default_avatar.png';
agentSubtitle = args['agentSubtitle'] as String?; agentSubtitle = args['agentSubtitle'] as String?;
systemPrompt = args['systemPrompt'] as String? ?? ''; systemPrompt = args['systemPrompt'] as String? ?? '';
welcomeMessage = args['welcomeMessage'] as String? ?? 'Hello! How can I help you today?'; welcomeMessage = args['welcomeMessage'] as String? ?? 'Hello! How can I help you today?';
playVoiceOnEnter = args['playVoiceOnEnter'] as bool? ?? false; playVoiceOnEnter = args['playVoiceOnEnter'] as bool? ?? false;
if (agentId.isEmpty) {
throw Exception('Invalid agent ID');
}
} catch (e) { } catch (e) {
debugPrint('🔴 Error initializing ChatController: $e'); debugPrint('🔴 Error initializing ChatController: $e');
Get.back(); // 返回上一页 Get.back(); // 返回上一页

25
lib/modules/chat/views/chat_view.dart

@ -28,12 +28,29 @@ class ChatView extends GetView<ChatController> {
backgroundColor: Colors.white, backgroundColor: Colors.white,
appBar: _buildAppBar(), appBar: _buildAppBar(),
body: SafeArea( body: SafeArea(
child: Column( child: Stack(
children: [ 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(),
], ],
), ),
), ),

9
lib/modules/explore/views/explore_view.dart

@ -25,8 +25,12 @@ class ExploreView extends GetView<ExploreController> {
// 检查是否是同声翻译官 // 检查是否是同声翻译官
if (agent.id == 'simultaneous_interpreter') { if (agent.id == 'simultaneous_interpreter') {
// 导航到翻译页面 // 导航到翻译页面,并传递代理信息
Get.toNamed(Routes.translation); Get.toNamed(Routes.translation, arguments: {
'agentId': agent.id,
'agentName': agent.name,
'agentAvatar': agent.avatarUrl,
});
return; return;
} }
@ -34,6 +38,7 @@ class ExploreView extends GetView<ExploreController> {
Get.toNamed('/chat', arguments: { Get.toNamed('/chat', arguments: {
'agentId': agent.id, 'agentId': agent.id,
'agentName': agent.name, 'agentName': agent.name,
'agentVoice': agent.voice,
'agentAvatar': agent.avatarUrl, 'agentAvatar': agent.avatarUrl,
'agentSubtitle': agent.description, 'agentSubtitle': agent.description,
'systemPrompt': agent.systemPrompt, 'systemPrompt': agent.systemPrompt,

10
lib/modules/test/bindings/test_binding.dart

@ -11,17 +11,15 @@ class TtsTestBinding extends Bindings {
Get.put(VolcanoTtsApiService(), permanent: true); Get.put(VolcanoTtsApiService(), permanent: true);
} }
Get.lazyPut<TtsTestController>( // Use put instead of lazyPut to ensure the controller is created immediately
() => TtsTestController(), Get.put<TtsTestController>(TtsTestController());
);
} }
} }
class AsrTestBinding extends Bindings { class AsrTestBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
Get.lazyPut<AsrTestController>( // Use put instead of lazyPut to ensure the controller is created immediately
() => AsrTestController(), Get.put<AsrTestController>(AsrTestController());
);
} }
} }

38
lib/modules/test/controllers/tts_test_controller.dart

@ -22,6 +22,20 @@ class TtsTestController extends GetxController {
// 当前播放的索引 // 当前播放的索引
final currentPlayingIndex = RxInt(-1); final currentPlayingIndex = RxInt(-1);
// 当前使用的语音 - 使用预定义的值
final currentVoice = 'zh_female_meilinvyou_moon_bigtts'.obs;
// 可用的语音列表
final List<Map<String, String>> 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<String> speechTexts = <String>[ final RxList<String> speechTexts = <String>[
'这是第一条播报测试,请听取合成效果。', '这是第一条播报测试,请听取合成效果。',
@ -164,20 +178,19 @@ class TtsTestController extends GetxController {
} }
/// 播放单条文本 /// 播放单条文本
Future<void> speakText(int index) async { Future<void> playSingle(int index) async {
if (index < 0 || index >= speechTexts.length || isAudioPlaying.value) { if (index < 0 || index >= speechTexts.length) {
return; return;
} }
try { try {
isLoading.value = true;
isSpeaking.value = true; isSpeaking.value = true;
isAudioPlaying.value = true; isAudioPlaying.value = true;
errorMessage.value = ''; errorMessage.value = '';
currentPlayingIndex.value = index; currentPlayingIndex.value = index;
final text = speechTexts[index]; final text = speechTexts[index];
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; final speaker = currentVoice.value;
// 确保TTS服务已连接 // 确保TTS服务已连接
if (!_ttsService.isConnected.value) { if (!_ttsService.isConnected.value) {
@ -202,7 +215,6 @@ class TtsTestController extends GetxController {
print('播放出错: $e'); print('播放出错: $e');
} }
} finally { } finally {
isLoading.value = false;
isSpeaking.value = false; isSpeaking.value = false;
isAudioPlaying.value = false; isAudioPlaying.value = false;
currentPlayingIndex.value = -1; currentPlayingIndex.value = -1;
@ -248,7 +260,7 @@ class TtsTestController extends GetxController {
await _connectToTtsService(); await _connectToTtsService();
} }
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts'; final speaker = currentVoice.value;
// 停止之前的播放 // 停止之前的播放
await _ttsService.stop(); 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 @override
void onClose() { void onClose() {
// 取消事件订阅 // 取消事件订阅

2
lib/modules/test/views/tts_test_view.dart

@ -132,7 +132,7 @@ class TtsTestView extends GetView<TtsTestController> {
: null, : null,
onTap: controller.isAudioPlaying.value || controller.isLoading.value onTap: controller.isAudioPlaying.value || controller.isLoading.value
? null ? null
: () => controller.speakText(index), : () => controller.playSingle(index),
enabled: !(controller.isAudioPlaying.value || controller.isLoading.value), enabled: !(controller.isAudioPlaying.value || controller.isLoading.value),
tileColor: isPlaying ? Colors.blue.withOpacity(0.1) : null, tileColor: isPlaying ? Colors.blue.withOpacity(0.1) : null,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(

21
lib/modules/translation/bindings/translation_binding.dart

@ -1,9 +1,26 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/translation_controller.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 @override
void dependencies() { void dependencies() {
Get.lazyPut<TranslationController>(() => TranslationController()); // Ensure services are registered
if (!Get.isRegistered<AzureAsrService>()) {
Get.put(AzureAsrService());
}
if (!Get.isRegistered<VolcanoTranslationService>()) {
Get.put(VolcanoTranslationService());
}
if (!Get.isRegistered<VolcanoTtsService>()) {
Get.put(VolcanoTtsService());
}
// Register the controller
Get.put(TranslationController());
} }
} }

514
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 '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 '../../../data/services/volcano_tts_api_service.dart';
import '../../../core/utils/logger.dart'; // import '../../../core/utils/logger.dart';
import '../models/translation_message_model.dart';
class TranslationController extends GetxController { class TranslationController extends GetxController {
// Observable states // Services
final isListening = false.obs; 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 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; final isTtsEnabled = true.obs;
final recognizedText = ''.obs;
final translatedText = ''.obs;
final sourceLanguage = '中文 (Chinese)'.obs;
final targetLanguage = '英语 (English)'.obs;
final translationHistory = <TranslationItem>[].obs;
// Services // Current translation item
late final VolcanoAsrService _voiceService; TranslationItem? currentItem;
late final VolcanoTtsApiService _ttsService;
// Stream subscriptions // Subscription for recognition stream
StreamSubscription? _recognitionSubscription; StreamSubscription? _recognitionSubscription;
// Timers // Scroll controller for ListView
Timer? _silenceTimer; final ScrollController scrollController = ScrollController();
Timer? _speakingCheckTimer;
// State tracking
bool _hasRecognizedSpeech = false;
bool _hasFinalResult = false;
DateTime? _lastSpeechTime;
Completer<String>? _recognitionCompleter;
// TTS事件订阅
StreamSubscription? _ttsEventSubscription;
bool _isTtsSpeaking = false;
// Timer for debouncing translation requests
Timer? _translationDebounceTimer;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_initializeServices();
_startSpeakingCheckTimer();
_ttsService = Get.find<VolcanoTtsApiService>(); // Initialize the translation service
_initTranslationService();
// 设置TTS事件监听 // Start voice recognition automatically when the page is loaded
_setupTtsEventListener(); startRecognition();
}
@override
void onClose() {
stopRecognition();
_recognitionSubscription?.cancel();
_translationDebounceTimer?.cancel();
scrollController.dispose();
super.onClose();
} }
Future<void> _initializeServices() async { // Initialize the translation service
Future<void> _initTranslationService() async {
try { try {
_voiceService = Get.find<VolcanoAsrService>(); await _translationService.initialize();
_ttsService = Get.find<VolcanoTtsApiService>();
await _voiceService.initialize();
await startListening();
} catch (e) { } catch (e) {
Logger.error('Failed to initialize translation services', e);
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Failed to initialize translation services: $e', 'Failed to initialize translation service: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
); );
} }
} }
Future<void> startListening() async { // Toggle TTS feature
if (isListening.value) return; void toggleTts() {
isTtsEnabled.toggle();
}
// Start voice recognition
Future<void> startRecognition() async {
if (isRecognizing.value) return;
try { try {
isListening.value = true; // Initialize a new translation item
originalText.value = ''; currentItem = TranslationItem(
translatedText.value = ''; sourceText: '',
_hasRecognizedSpeech = false; translatedText: '',
_hasFinalResult = false; sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value,
timestamp: DateTime.now(),
);
_recognitionCompleter = Completer<String>(); // Start the voice recognition service
final success = await _asrService.startContinuousRecognition();
final success = await _voiceService.startContinuousRecognition(); if (success) {
if (!success) { isRecognizing.value = true;
isListening.value = false;
throw Exception('Failed to start voice recognition'); // Reset the recognized text
} recognizedText.value = '';
translatedText.value = '';
_recognitionSubscription = _voiceService.recognitionStream?.listen(
(event) { // Listen for recognition events
_recognitionSubscription = _asrService.recognitionStream?.listen((event) {
switch (event.type) { switch (event.type) {
case RecognitionEventType.recognizing: case RecognitionEventType.finalResult:
if (event.text.isNotEmpty) { if (event.text.isNotEmpty) {
originalText.value = event.text; handleFinalResult(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; break;
case RecognitionEventType.recognizing:
case RecognitionEventType.finalResult: // Update the UI with intermediate results
_hasFinalResult = true;
isUserSpeaking.value = false;
if (event.text.isNotEmpty) { if (event.text.isNotEmpty) {
originalText.value = event.text; handleIntermediateResult(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; break;
case RecognitionEventType.error: case RecognitionEventType.error:
Logger.error('Voice recognition error', event.error); Get.snackbar(
'Error',
'Recognition error: ${event.error}',
snackPosition: SnackPosition.BOTTOM,
);
break; break;
default: default:
break; break;
} }
}, });
onError: (error) { } else {
Logger.error('Voice recognition stream error', error); throw Exception('Failed to start continuous recognition');
isListening.value = false; }
restartListening();
}
);
} catch (e) { } catch (e) {
Logger.error('Failed to start listening', e); isRecognizing.value = false;
isListening.value = false; Get.snackbar(
'Error',
'Failed to start voice recognition: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
} }
} }
Future<void> _processTranslation(String text) async { // Stop voice recognition
if (text.isEmpty) return; Future<void> stopRecognition() async {
if (!isRecognizing.value) return;
try { try {
isTranslating.value = true; await _asrService.stopContinuousRecognition();
_recognitionSubscription?.cancel();
// In a real implementation, you would call your translation service here _recognitionSubscription = null;
// For now, we'll simulate translation with a delay isRecognizing.value = false;
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) { finalizeCurrentItem();
await _synthesizeText(translated);
}
// 重置当前显示的文本
recognizedText.value = '';
translatedText.value = '';
} catch (e) { } catch (e) {
Logger.error('Translation error', e); Get.snackbar(
} finally { 'Error',
isTranslating.value = false; 'Failed to stop voice recognition: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
} }
} }
// This is a placeholder - in a real app, you would call your translation API // Handle intermediate recognition result
Future<String> _simulateTranslation(String text) async { void handleIntermediateResult(String text) {
// This is just a simulation - replace with actual translation API call if (text.isEmpty) return;
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.'; if (currentItem == null) {
return 'Translated from Chinese: $text'; currentItem = TranslationItem(
sourceText: text,
translatedText: '',
sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value,
timestamp: DateTime.now(),
);
} else { } 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(); recognizedText.value = text;
}
Future<void> restartListening() async {
await stopListening();
await Future.delayed(const Duration(milliseconds: 500));
await startListening();
}
Future<void> stopListening() async {
if (!isListening.value) return;
try { // Debounce translation requests to avoid too many API calls
await _recognitionSubscription?.cancel(); _translationDebounceTimer?.cancel();
_recognitionSubscription = null; _translationDebounceTimer = Timer(const Duration(milliseconds: 300), () {
translateText(text, isFinal: false);
_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; // Scroll to the bottom to show the latest content
_scrollToBottom();
} }
void _setupTtsEventListener() { // Handle final recognition result
_ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent); void handleFinalResult(String text) {
} if (text.isEmpty) return;
void _handleTtsEvent(Map<String, dynamic> event) {
final String eventType = event['type'] as String? ?? 'unknown';
switch (eventType) { // 更新当前翻译项(如果不存在则创建一个)
case 'ttsSentenceStart': if (currentItem == null) {
_isTtsSpeaking = true; currentItem = TranslationItem(
break; sourceText: text,
case 'ttsSentenceEnd': translatedText: '',
case 'sessionFinished': sourceLanguage: sourceLanguage.value,
case 'connectionFinished': targetLanguage: targetLanguage.value,
_isTtsSpeaking = false; timestamp: DateTime.now(),
break; );
case 'connectionFailed': } else {
case 'sessionFailed': currentItem!.sourceText = text;
_isTtsSpeaking = false;
print('TTS错误: ${event['message']}');
break;
} }
}
@override
void onClose() {
stopListening();
_speakingCheckTimer?.cancel();
// 取消TTS事件订阅 // 更新识别文本
_ttsEventSubscription?.cancel(); recognizedText.value = text;
// 结束TTS会话 // 翻译最终结果 (添加到历史的逻辑已移至translateText方法)
_ttsService.endSession(); 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 // Finalize the current translation item and add it to history
Future<void> _synthesizeText(String text) async { void finalizeCurrentItem() {
if (text.isEmpty || !isTtsEnabled.value) return; 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 { try {
// 如果当前有正在进行的TTS会话,先结束它 final result = await _translationService.translateText(
if (_isTtsSpeaking) { text: text,
await _ttsService.endSession(); sourceLanguage: sourceLanguage.value,
} targetLanguage: targetLanguage.value,
);
// 确保连接已建立 if (result != null) {
if (!_ttsService.isConnected.value) { // 实时更新翻译文本
final connected = await _ttsService.connect(); translatedText.value = result;
if (!connected) {
print('无法连接到TTS服务'); // 确保更新的是发起翻译请求时的翻译项,而不是当前可能已经变化的翻译项
return; 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) { } 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<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);
}
// Add a method to toggle TTS // Scroll to the bottom of the list
void toggleTTS() { void _scrollToBottom() {
isTtsEnabled.toggle(); if (scrollController.hasClients) {
if (!isTtsEnabled.value) { Future.delayed(const Duration(milliseconds: 100), () {
_ttsService.endSession(); 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,
});
} }

13
lib/modules/translation/models/translation_message_model.dart

@ -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,
});
}

384
lib/modules/translation/views/translation_view.dart

@ -1,9 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/translation_controller.dart'; import '../controllers/translation_controller.dart';
import '../../../core/theme/app_colors.dart'; // import 'package:intl/intl.dart';
import '../../../core/theme/text_styles.dart';
import '../widgets/translation_message_item.dart';
class TranslationView extends GetView<TranslationController> { class TranslationView extends GetView<TranslationController> {
const TranslationView({Key? key}) : super(key: key); const TranslationView({Key? key}) : super(key: key);
@ -11,178 +10,175 @@ class TranslationView extends GetView<TranslationController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: _buildAppBar(),
title: _buildLanguageSelector(), body: _buildBody(),
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(),
],
),
),
); );
} }
Widget _buildLanguageSelector() { PreferredSizeWidget _buildAppBar() {
return Obx(() { return AppBar(
return Row( 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, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Obx(() => Text(
controller.sourceLanguage.value, controller.sourceLanguage.value.split(' ')[0], // Just show the Chinese part
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w600,
color: Colors.black, color: Colors.black,
), ),
)),
SizedBox(width: 4.w),
Icon(
Icons.arrow_forward_ios,
color: Colors.black,
size: 16.sp,
), ),
IconButton( SizedBox(width: 4.w),
icon: const Icon(Icons.swap_horiz, color: Color(0xFF4A6FE5)), Obx(() => Text(
onPressed: controller.switchLanguages, controller.targetLanguage.value.split(' ')[0], // Just show the English part
),
Text(
controller.targetLanguage.value,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w600,
color: Colors.black, 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(() { return Obx(() {
if (controller.messages.isEmpty) { final itemCount = controller.translationHistory.length;
return _buildWelcomeMessage();
} // 添加对recognizedText和translatedText的监听
final hasRecognizedText = controller.recognizedText.value.isNotEmpty;
final hasTranslatedText = controller.translatedText.value.isNotEmpty;
final showCurrentText = hasRecognizedText || hasTranslatedText;
return ListView.builder( return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), controller: controller.scrollController,
itemCount: controller.messages.length, padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
reverse: true, itemCount: itemCount + (showCurrentText ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final message = controller.messages[controller.messages.length - 1 - index]; // Current item (being translated)
return TranslationMessageItem(message: message); 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() { Widget _buildTranslationItemWidget(String sourceText, String translatedText, bool isCurrent, {TranslationItem? item}) {
return Padding( return Container(
padding: const EdgeInsets.all(16.0), margin: EdgeInsets.only(bottom: 24.h),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12.r),
),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// First welcome message // Source text
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(20), padding: EdgeInsets.all(16.r),
decoration: BoxDecoration( child: Text(
color: Colors.grey[100], sourceText.isEmpty && isCurrent ? '等待语音输入...' : sourceText,
borderRadius: BorderRadius.circular(12), style: TextStyle(
border: Border.all( fontSize: 16.sp,
color: Colors.grey[300]!, color: sourceText.isEmpty && isCurrent ? Colors.grey : Colors.black,
width: 0.5,
), ),
), ),
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( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(20), padding: EdgeInsets.all(16.r),
decoration: BoxDecoration( child: Row(
color: const Color(0xFFEEF4FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFFD0DFFF),
width: 0.5,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Expanded(
"👇 Click the button below ▶️ to start speaking, and Felo will automatically recognize the language for translation", child: Text(
style: TextStyle( translatedText.isEmpty && isCurrent ? '翻译结果将显示在这里...' : translatedText,
fontSize: 14, style: TextStyle(
color: Colors.grey[700], fontSize: 16.sp,
height: 1.5, color: translatedText.isEmpty && isCurrent ? Colors.grey : Colors.black,
),
), ),
), ),
const SizedBox(height: 16), if (translatedText.isNotEmpty)
Row( IconButton(
children: [ icon: Icon(
Expanded( Icons.volume_up,
child: Text( color: Colors.blue,
"👇 Click the button below ▶️ to start speaking, and Felo will automatically recognize the language for translation", size: 20.sp,
style: TextStyle(
fontSize: 14,
color: Colors.grey[800],
height: 1.5,
),
),
), ),
const SizedBox(width: 8), onPressed: () {
Icon(Icons.volume_up, color: Colors.grey[600]), if (isCurrent) {
], controller.playTranslatedText(translatedText);
), } else if (item != null) {
controller.playTranslationItem(item);
}
},
),
], ],
), ),
), ),
@ -191,112 +187,46 @@ class TranslationView extends GetView<TranslationController> {
); );
} }
Widget _buildBottomSection() { Widget _buildControlBar() {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: Colors.black.withAlpha(13), // 0.05 * 255 ≈ 13
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, -2), offset: const Offset(0, -2),
), ),
], ],
), ),
child: Column( child: Center(
children: [ child: Obx(() {
_buildRecognitionStatus(), final isRecognizing = controller.isRecognizing.value;
const SizedBox(height: 8), return GestureDetector(
_buildControlButtons(), onTap: () {
], if (isRecognizing) {
), controller.stopRecognition();
); } else {
} controller.startRecognition();
}
Widget _buildRecognitionStatus() { },
return Obx(() { child: Container(
final isListening = controller.isListening.value; width: 60.w,
final isUserSpeaking = controller.isUserSpeaking.value; height: 60.w,
decoration: BoxDecoration(
if (!isListening) { color: isRecognizing ? Colors.red : Colors.blue,
return const SizedBox.shrink(); shape: BoxShape.circle,
} ),
child: Icon(
return Container( isRecognizing ? Icons.pause : Icons.play_arrow,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), color: Colors.white,
decoration: BoxDecoration( size: 30.sp,
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,
),
), ),
),
],
),
);
});
}
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>(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,
), ),
); );
}), }),
], ),
); );
} }
} }

82
lib/modules/translation/widgets/translation_message_item.dart

@ -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,
),
],
),
),
],
),
);
}
}

1
pubspec.yaml

@ -25,6 +25,7 @@ dependencies:
web_socket_channel: ^2.4.4 web_socket_channel: ^2.4.4
flutter_dotenv: ^5.1.0 flutter_dotenv: ^5.1.0
synchronized: ^3.1.0 synchronized: ^3.1.0
intl: ^0.19.0
window_size: window_size:
git: git:
url: https://github.com/google/flutter-desktop-embedding.git url: https://github.com/google/flutter-desktop-embedding.git

Loading…
Cancel
Save