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. 23
      lib/data/services/azure_asr_service.dart
  9. 53
      lib/data/services/background_agent_service.dart
  10. 26
      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. 10
      lib/modules/chat/controllers/chat_controller.dart
  15. 19
      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. 482
      lib/modules/translation/controllers/translation_controller.dart
  22. 13
      lib/modules/translation/models/translation_message_model.dart
  23. 348
      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 {
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 {

13
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" -> {

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_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);
}
}
}

1
lib/core/routes/app_pages.dart

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

1
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';
}

103
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: '''
你是一位精通命理的玄易大师,熟读《穷通宝鉴》《滴天髓》《三命通会》《子平真诠》《千里命稿》《五行精纪》等经典著作。

23
lib/data/services/azure_asr_service.dart

@ -254,14 +254,20 @@ class AzureAsrService extends GetxService {
/// 停止连续语音识别
Future<bool> stopContinuousRecognition() async {
_checkInitialized();
try {
// 检查是否已初始化
if (!_isInitialized) {
Logger.warning('语音识别服务未初始化,无法停止连续识别');
return true; // 未初始化,视为已停止
}
// 检查是否正在进行连续识别
if (!_isContinuousRecognitionActive) {
Logger.warning('连续识别未在进行中');
Logger.warning('连续识别未在进行中,无需停止');
return true; // 已经停止,直接返回成功
}
try {
// 调用平台方法停止识别
final result = await _methodChannel.invokeMethod<bool>('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;
}
}

53
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,8 +83,7 @@ class BackgroundAgentService extends GetxService {
_isProcessing = false;
// 播放退出提示
await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _defaultSpeaker);
await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _agentVoice);
// 停止语音识别
if (_isListening) {
@ -89,12 +93,30 @@ class BackgroundAgentService extends GetxService {
// 清除当前系统提示词
_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) {
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;
// 使用成员变量中存储的agent voice
_ttsService.startSession(_agentVoice);
_ttsService.startSession(_defaultSpeaker);
// 获取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);
}
}

26
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<BackgroundAgentService>()) {
@ -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,
'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

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;
bool get isPlaying => _isPlaying.value;
// 默认发音人
final String defaultSpeaker = 'zh_female_1';
// 默认发音人 - 使用预定义的值
final String defaultSpeaker = 'zh_female_meilinvyou_moon_bigtts';
// 音频缓冲区
final List<Uint8List> _audioBuffer = [];

1
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'),

10
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<String, dynamic>;
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(); // 返回上一页

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

@ -28,7 +28,22 @@ class ChatView extends GetView<ChatController> {
backgroundColor: Colors.white,
appBar: _buildAppBar(),
body: SafeArea(
child: Column(
child: Stack(
children: [
// 代理头像作为背景
if (controller.agentAvatar.isNotEmpty)
Positioned.fill(
child: Opacity(
opacity: 0.1, // 设置透明度,使其不影响内容阅读
child: Image.asset(
controller.agentAvatar,
fit: BoxFit.contain,
),
),
),
// 主要内容
Column(
children: [
Expanded(
child: _buildMessageList(),
@ -36,6 +51,8 @@ class ChatView extends GetView<ChatController> {
_buildInputSection(),
],
),
],
),
),
),
);

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

@ -25,8 +25,12 @@ class ExploreView extends GetView<ExploreController> {
// 检查是否是同声翻译官
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<ExploreController> {
Get.toNamed('/chat', arguments: {
'agentId': agent.id,
'agentName': agent.name,
'agentVoice': agent.voice,
'agentAvatar': agent.avatarUrl,
'agentSubtitle': agent.description,
'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.lazyPut<TtsTestController>(
() => TtsTestController(),
);
// Use put instead of lazyPut to ensure the controller is created immediately
Get.put<TtsTestController>(TtsTestController());
}
}
class AsrTestBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<AsrTestController>(
() => AsrTestController(),
);
// Use put instead of lazyPut to ensure the controller is created immediately
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 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>[
'这是第一条播报测试,请听取合成效果。',
@ -164,20 +178,19 @@ class TtsTestController extends GetxController {
}
/// 播放单条文本
Future<void> speakText(int index) async {
if (index < 0 || index >= speechTexts.length || isAudioPlaying.value) {
Future<void> 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() {
// 取消事件订阅

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

@ -132,7 +132,7 @@ class TtsTestView extends GetView<TtsTestController> {
: 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(

21
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>(() => 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());
}
}

482
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<AzureAsrService>();
final VolcanoTranslationService _translationService = Get.find<VolcanoTranslationService>();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
// 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 = <TranslationMessage>[].obs;
// TTS启用状态
final isTtsEnabled = true.obs;
final recognizedText = ''.obs;
final translatedText = ''.obs;
final sourceLanguage = '中文 (Chinese)'.obs;
final targetLanguage = '英语 (English)'.obs;
final translationHistory = <TranslationItem>[].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<String>? _recognitionCompleter;
// Scroll controller for ListView
final ScrollController scrollController = ScrollController();
// TTS事件订阅
StreamSubscription? _ttsEventSubscription;
bool _isTtsSpeaking = false;
// Timer for debouncing translation requests
Timer? _translationDebounceTimer;
@override
void onInit() {
super.onInit();
_initializeServices();
_startSpeakingCheckTimer();
_ttsService = Get.find<VolcanoTtsApiService>();
// Initialize the translation service
_initTranslationService();
// 设置TTS事件监听
_setupTtsEventListener();
// Start voice recognition automatically when the page is loaded
startRecognition();
}
Future<void> _initializeServices() async {
try {
_voiceService = Get.find<VolcanoAsrService>();
_ttsService = Get.find<VolcanoTtsApiService>();
@override
void onClose() {
stopRecognition();
_recognitionSubscription?.cancel();
_translationDebounceTimer?.cancel();
scrollController.dispose();
super.onClose();
}
await _voiceService.initialize();
await startListening();
// Initialize the translation service
Future<void> _initTranslationService() async {
try {
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<void> startListening() async {
if (isListening.value) return;
// Toggle TTS feature
void toggleTts() {
isTtsEnabled.toggle();
}
// Start voice recognition
Future<void> 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<String>();
// 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');
}
if (success) {
isRecognizing.value = true;
// 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) {
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<void> _processTranslation(String text) async {
if (text.isEmpty) return;
// Stop voice recognition
Future<void> 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;
await _asrService.stopContinuousRecognition();
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
isRecognizing.value = false;
// Add to messages
messages.add(TranslationMessage(
original: text,
translated: translated,
timestamp: DateTime.now(),
isFromUser: true,
));
// Speak the translation if TTS is enabled
if (isTtsEnabled.value) {
await _synthesizeText(translated);
}
// 添加当前项到历史(如果有内容)
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<String> _simulateTranslation(String text) async {
// This is just a simulation - replace with actual translation API call
if (sourceLanguage.value == "Chinese" && targetLanguage.value == "English") {
// Simulate Chinese to English translation
if (text.contains('测试')) return 'I am now starting to test the function of translating spoken Chinese into English.';
return 'Translated from Chinese: $text';
// 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;
// 实时更新识别文本
recognizedText.value = text;
// Clear current messages when switching languages
messages.clear();
}
// Debounce translation requests to avoid too many API calls
_translationDebounceTimer?.cancel();
_translationDebounceTimer = Timer(const Duration(milliseconds: 300), () {
translateText(text, isFinal: false);
});
Future<void> restartListening() async {
await stopListening();
await Future.delayed(const Duration(milliseconds: 500));
await startListening();
// Scroll to the bottom to show the latest content
_scrollToBottom();
}
Future<void> stopListening() async {
if (!isListening.value) return;
try {
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
_silenceTimer?.cancel();
_silenceTimer = null;
// Handle final recognition result
void handleFinalResult(String text) {
if (text.isEmpty) return;
if (_voiceService.isContinuousRecognitionActive()) {
await _voiceService.stopRecognition();
// 更新当前翻译项(如果不存在则创建一个)
if (currentItem == null) {
currentItem = TranslationItem(
sourceText: text,
translatedText: '',
sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value,
timestamp: DateTime.now(),
);
} else {
currentItem!.sourceText = text;
}
isListening.value = false;
isUserSpeaking.value = false;
_hasRecognizedSpeech = false;
_hasFinalResult = false;
} catch (e) {
Logger.error('Failed to stop listening', e);
}
}
// 更新识别文本
recognizedText.value = text;
void _startSpeakingCheckTimer() {
_speakingCheckTimer?.cancel();
_speakingCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
_checkIfUserIsSpeaking();
// 翻译最终结果 (添加到历史的逻辑已移至translateText方法)
translateText(text, isFinal: true).then((_) {
// 作为安全措施,确保当前项被添加到历史中
// 这是为了处理translateText中可能因为条件不满足而没有添加到历史的情况
finalizeCurrentItem();
});
}
void _checkIfUserIsSpeaking() {
if (_hasRecognizedSpeech && !_hasFinalResult && _lastSpeechTime != null) {
final timeSinceLastSpeech = DateTime.now().difference(_lastSpeechTime!);
if (timeSinceLastSpeech.inMilliseconds < 1000) {
isUserSpeaking.value = true;
return;
}
// Scroll to the bottom to show the latest content
_scrollToBottom();
}
isUserSpeaking.value = false;
// 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;
}
}
void _setupTtsEventListener() {
_ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent);
// Handle voice recognition result (legacy method, kept for compatibility)
void handleVoiceResult(String text) {
handleFinalResult(text);
}
void _handleTtsEvent(Map<String, dynamic> event) {
final String eventType = event['type'] as String? ?? 'unknown';
// Translate the recognized text
Future<void> translateText(String text, {bool isFinal = true}) async {
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;
}
}
isTranslating.value = true;
@override
void onClose() {
stopListening();
_speakingCheckTimer?.cancel();
// 保存当前的翻译项引用,确保异步返回时更新正确的项
final TranslationItem? itemToUpdate = currentItem;
// 取消TTS事件订阅
_ttsEventSubscription?.cancel();
try {
final result = await _translationService.translateText(
text: text,
sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value,
);
// 结束TTS会话
_ttsService.endSession();
if (result != null) {
// 实时更新翻译文本
translatedText.value = result;
super.onClose();
}
// 确保更新的是发起翻译请求时的翻译项,而不是当前可能已经变化的翻译项
if (itemToUpdate != null && itemToUpdate.sourceText == text) {
itemToUpdate.translatedText = result;
// Add a method to synthesize text using the new TTS service
Future<void> _synthesizeText(String text) async {
if (text.isEmpty || !isTtsEnabled.value) return;
// 如果是最终结果,并且当前项仍然是发起请求时的项,则添加到历史
if (isFinal && itemToUpdate == currentItem) {
translationHistory.add(itemToUpdate);
currentItem = null; // 重要:将当前项设为null,表示已完成
try {
// 如果当前有正在进行的TTS会话,先结束它
if (_isTtsSpeaking) {
await _ttsService.endSession();
// 重置当前显示的文本,避免与历史项重复
recognizedText.value = '';
translatedText.value = '';
}
// 确保连接已建立
if (!_ttsService.isConnected.value) {
final connected = await _ttsService.connect();
if (!connected) {
print('无法连接到TTS服务');
return;
// Play TTS if enabled and this is a final result
if (isTtsEnabled.value && isFinal) {
playTranslatedText(result);
}
}
}
} catch (e) {
Get.snackbar(
'Error',
'Translation failed: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
} finally {
isTranslating.value = false;
}
}
// 开始合成
await _ttsService.synthesize(text, 'zh_female_1'); // 使用默认说话人
// Play the translated text using TTS
Future<void> playTranslatedText(String text) async {
if (text.isEmpty) return;
try {
await _ttsService.speakSingle(text);
} catch (e) {
print('TTS播放出错: $e');
Get.snackbar(
'Error',
'Failed to play translation: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
}
}
// Add a method to toggle TTS
void toggleTTS() {
isTtsEnabled.toggle();
if (!isTtsEnabled.value) {
_ttsService.endSession();
// Play a specific translation item
Future<void> playTranslationItem(TranslationItem item) async {
await playTranslatedText(item.translatedText);
}
// Scroll to the bottom of the list
void _scrollToBottom() {
if (scrollController.hasClients) {
Future.delayed(const Duration(milliseconds: 100), () {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
}
}
// Model class for translation items
class TranslationItem {
String sourceText;
String translatedText;
final String sourceLanguage;
final String targetLanguage;
final DateTime timestamp;
TranslationItem({
required this.sourceText,
required this.translatedText,
required this.sourceLanguage,
required this.targetLanguage,
required this.timestamp,
});
}

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

348
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<TranslationController> {
const TranslationView({Key? key}) : super(key: key);
@ -11,177 +10,174 @@ class TranslationView extends GetView<TranslationController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: _buildLanguageSelector(),
centerTitle: true,
appBar: _buildAppBar(),
body: _buildBody(),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => Get.back(),
icon: Icon(
Icons.arrow_back_ios,
color: Colors.black,
size: 20.sp,
),
actions: [
IconButton(
icon: const Icon(Icons.settings_outlined, color: Colors.black),
onPressed: () {
// Navigate to settings
// Stop recognition before navigating back
controller.stopRecognition();
Get.back();
},
),
],
),
body: Container(
color: Colors.white,
child: Column(
children: [
Expanded(
child: _buildMessagesList(),
),
_buildBottomSection(),
],
),
),
);
}
Widget _buildLanguageSelector() {
return Obx(() {
return Row(
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 _buildMessagesList() {
return Obx(() {
if (controller.messages.isEmpty) {
return _buildWelcomeMessage();
Widget _buildBody() {
return Column(
children: [
Expanded(
child: _buildTranslationContent(),
),
_buildControlBar(),
],
);
}
Widget _buildTranslationContent() {
return Obx(() {
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),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// First welcome message
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
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),
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
borderRadius: BorderRadius.circular(12.r),
),
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(
// Source text
Container(
width: double.infinity,
padding: EdgeInsets.all(16.r),
child: Text(
"Welcome to Felo Translator. 👆 Click the button above to switch languages",
sourceText.isEmpty && isCurrent ? '等待语音输入...' : sourceText,
style: TextStyle(
fontSize: 14,
color: Colors.grey[800],
height: 1.5,
fontSize: 16.sp,
color: sourceText.isEmpty && isCurrent ? Colors.grey : Colors.black,
),
),
),
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,
),
),
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",
translatedText.isEmpty && isCurrent ? '翻译结果将显示在这里...' : translatedText,
style: TextStyle(
fontSize: 14,
color: Colors.grey[800],
height: 1.5,
fontSize: 16.sp,
color: translatedText.isEmpty && isCurrent ? Colors.grey : Colors.black,
),
),
),
const SizedBox(width: 8),
Icon(Icons.volume_up, color: Colors.grey[600]),
],
if (translatedText.isNotEmpty)
IconButton(
icon: Icon(
Icons.volume_up,
color: Colors.blue,
size: 20.sp,
),
onPressed: () {
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(
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(),
],
),
);
child: Center(
child: Obx(() {
final isRecognizing = controller.isRecognizing.value;
return GestureDetector(
onTap: () {
if (isRecognizing) {
controller.stopRecognition();
} else {
controller.startRecognition();
}
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),
},
child: Container(
width: 60.w,
height: 60.w,
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,
color: isRecognizing ? Colors.red : Colors.blue,
shape: BoxShape.circle,
),
),
),
],
),
);
});
}
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,
isRecognizing ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 30.sp,
),
),
);
}),
],
),
);
}
}

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
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

Loading…
Cancel
Save