Browse Source

v003

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
e76ef07cc1
  1. 2
      .cursorrules
  2. 4
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  3. 8
      lib/core/bindings/initial_binding.dart
  4. 2
      lib/core/routes/app_pages.dart
  5. 94
      lib/core/widgets/message_bubble.dart
  6. 29
      lib/data/services/azure_asr_service.dart
  7. 689
      lib/data/services/background_agent_service.dart
  8. 97
      lib/data/services/chat_history_service.dart
  9. 117
      lib/data/services/my_audio_handler.dart
  10. 3
      lib/data/services/volcano_asr_service.dart
  11. 11
      lib/modules/chat/bindings/chat_binding.dart
  12. 1157
      lib/modules/chat/controllers/chat_controller.dart
  13. 348
      lib/modules/chat/controllers/voice_input_controller.dart
  14. 31
      lib/modules/chat/models/message_model.dart
  15. 849
      lib/modules/chat/views/chat_view.dart
  16. 302
      lib/modules/chat/widgets/voice_input_panel.dart
  17. 349
      lib/modules/translation/controllers/translation_controller.dart
  18. 77
      lib/modules/translation/controllers/translation_detail_controller.dart
  19. 86
      lib/modules/translation/views/translation_detail_view.dart
  20. 21
      lib/modules/translation/views/translation_history_view.dart
  21. 37
      lib/modules/translation/views/translation_view.dart
  22. 4
      linux/flutter/generated_plugin_registrant.cc
  23. 1
      linux/flutter/generated_plugins.cmake
  24. 1
      pubspec.yaml
  25. 3
      windows/flutter/generated_plugin_registrant.cc
  26. 1
      windows/flutter/generated_plugins.cmake

2
.cursorrules

@ -4,6 +4,6 @@
4. 尽可能使用GetX提供的各种功能, 而不是使用原生的StatefulWidget
5. 不要增加重试逻辑,出现失败直接返回错误
6. 尽可能避免使用try catch
7. 尽可能避免打印日志
7. 尽可能避免打印日志,仅在出错时记录日志
8. 请使用较小的代码量改动,最安全的方法进行代码编写和优化

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

@ -233,7 +233,7 @@ class AzureAsrHelper {
// 开始连续识别
recognizer.startContinuousRecognitionAsync().get()
isContinuousRecognitionActive = true
Log.d(TAG, "连续识别已开始")
Log.d(TAG, "连续识别已开始kt")
return true
}
@ -273,7 +273,7 @@ class AzureAsrHelper {
stopRecognitionSemaphore.acquire()
isContinuousRecognitionActive = false
Log.d(TAG, "连续识别已停止")
Log.d(TAG, "连续识别已停止kt")
callback.onSessionStopped()
return true
} catch (e: Exception) {

8
lib/core/bindings/initial_binding.dart

@ -7,6 +7,8 @@ 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 '../../data/services/background_agent_service.dart';
import '../../data/services/chat_history_service.dart';
import '../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
@ -23,6 +25,12 @@ class InitialBinding extends Bindings {
final volcanoTranslationService = Get.put(VolcanoTranslationService(), permanent: true);
final azureAsrService = Get.put(AzureAsrService(), permanent: true);
// 注册ChatHistoryService (先注册ChatHistoryService)
final chatHistoryService = Get.put(ChatHistoryService(), permanent: true);
// 注册后台代理服务 (后注册BackgroundAgentService)
final backgroundAgentService = Get.put(BackgroundAgentService(), permanent: true);
// 只注册全局控制器
Get.put(PermissionController(), permanent: true);

2
lib/core/routes/app_pages.dart

@ -31,7 +31,7 @@ abstract class AppPages {
),
GetPage(
name: Routes.chat,
page: () => const ChatView(),
page: () => ChatView(),
binding: ChatBinding(),
),
GetPage(

94
lib/core/widgets/message_bubble.dart

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../modules/chat/models/message_model.dart';
import '../theme/app_colors.dart';
class MessageBubble extends StatelessWidget {
final Message message;
final bool isUser;
final String? userAvatar;
final String? agentAvatar;
const MessageBubble({
Key? key,
required this.message,
required this.isUser,
this.userAvatar,
this.agentAvatar,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
child: Row(
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: Container(
padding: EdgeInsets.all(12.r),
decoration: BoxDecoration(
color: isUser ? AppColors.primary : Colors.grey[200],
borderRadius: BorderRadius.circular(16.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 消息内容
if (message.content.isNotEmpty)
SelectableText(
message.content,
style: TextStyle(
color: isUser ? Colors.white : Colors.black,
fontSize: 16.sp,
height: 1.5,
letterSpacing: 0.2,
),
textAlign: TextAlign.left,
maxLines: null,
enableInteractiveSelection: true,
textWidthBasis: TextWidthBasis.longestLine,
),
// 加载指示器
if (message.isLoading)
Padding(
padding: EdgeInsets.only(top: message.content.isNotEmpty ? 8.h : 0),
child: _buildLoadingIndicator(),
),
],
),
),
),
],
),
);
}
Widget _buildLoadingIndicator() {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 12.w,
height: 12.h,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
isUser ? Colors.white : AppColors.primary,
),
),
),
SizedBox(width: 8.w),
Text(
'思考中...',
style: TextStyle(
color: isUser ? Colors.white : Colors.black,
fontSize: 12.sp,
),
),
],
);
}
}

29
lib/data/services/azure_asr_service.dart

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// 识别事件类型
enum RecognitionEventType {
@ -209,10 +210,8 @@ class AzureAsrService extends GetxService {
Future<bool> startContinuousRecognition() async {
_checkInitialized();
if (_isContinuousRecognitionActive) {
Logger.warning('连续识别已经在进行中');
// 先停止当前正在进行的识别任务
Logger.info('尝试停止当前正在进行的识别任务');
// 无论内部状态如何,先尝试停止任何可能正在进行的识别任务
Logger.info('在开始新的语音识别前,确保停止任何可能正在进行的识别任务');
try {
await stopContinuousRecognition();
// 添加短暂延迟,确保资源被正确释放
@ -221,11 +220,29 @@ class AzureAsrService extends GetxService {
Logger.error('停止当前识别任务失败', e);
// 继续尝试启动新的识别任务
}
}
try {
// 重置识别器,确保资源被正确释放
await _resetRecognizer();
// await _resetRecognizer();
// 重新初始化底层服务 - 通过连续调用两次resetRecognizer确保完全重置
Logger.info('重新初始化语音识别底层服务');
try {
// 第一次调用resetRecognizer
await _methodChannel.invokeMethod<bool>('resetRecognizer');
// 短暂延迟,确保第一次重置完成
await Future.delayed(Duration(milliseconds: 200));
// 第二次调用resetRecognizer,确保完全重置
await _methodChannel.invokeMethod<bool>('resetRecognizer');
// 再次短暂延迟
await Future.delayed(Duration(milliseconds: 200));
} catch (e) {
Logger.error('重新初始化底层服务失败', e);
// 继续尝试启动识别
}
// 创建事件流控制器
_eventStreamController = StreamController<RecognitionEvent>.broadcast();

689
lib/data/services/background_agent_service.dart

@ -1,465 +1,458 @@
import 'package:get/get.dart';
import 'dart:async';
import 'volcano_ai_service.dart';
import 'azure_asr_service.dart';
import 'volcano_tts_api_service.dart';
import '../../modules/chat/models/message_model.dart';
import 'package:get/get.dart';
import 'package:flutter/widgets.dart';
import '../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../providers/agent_provider.dart';
import '../models/agent.dart';
import '../../modules/chat/controllers/chat_controller.dart';
import '../../modules/chat/models/message_model.dart';
import 'azure_asr_service.dart';
import 'volcano_ai_service.dart';
import 'volcano_tts_api_service.dart';
import 'chat_history_service.dart';
/// 后台代理服务,用于在应用处于后台时处理AI交互
class BackgroundAgentService extends GetxService {
static BackgroundAgentService get to => Get.find();
final VolcanoAIService _aiService;
final VolcanoTtsApiService _ttsService;
final AzureAsrService _voiceRecognitionService;
final List<Message> _messageHistory = [];
bool _isProcessing = false;
bool _isListening = false;
StreamSubscription? _recognitionSubscription;
// 当前使用的agent ID
String? _currentAgentId;
// 当前使用的agent voice
String _agentVoice = 'zh_female_meilinvyou_moon_bigtts';
// 可观察的状态
// 服务实例
final AzureAsrService _asrService = Get.find<AzureAsrService>();
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
// 状态控制
final RxBool isProcessing = false.obs;
final RxBool isListening = false.obs;
final RxString recognizedText = ''.obs;
// 添加一个标志,表示是否已经识别到语音
bool _hasRecognizedSpeech = false;
// 添加一个标志,表示是否已经收到最终结果
bool _hasFinalResult = false;
// 单一的交互超时计时器 (30秒)
Timer? _interactionTimer;
// 语音识别状态
final RxString recognizedText = ''.obs;
// 添加一个变量来跟踪当前的AI响应流订阅
// 流式响应控制
StreamSubscription? _recognitionSubscription;
StreamSubscription? _aiResponseSubscription;
// 添加一个标志,表示是否应该取消当前的AI响应
bool _shouldCancelAiResponse = false;
// 定时器,用于检测无活动超时
Timer? _inactivityTimer;
// 当前系统提示词
String? _currentSystemPrompt;
// 当前处理的Agent
Agent? _currentAgent;
BackgroundAgentService()
: _aiService = VolcanoAIService(),
_ttsService = Get.find<VolcanoTtsApiService>(),
_voiceRecognitionService = Get.find<AzureAsrService>();
// 启动或重置交互计时器
void _resetInteractionTimer() {
// 取消之前的计时器
_interactionTimer?.cancel();
// 消息历史 - 与ChatController共享
List<Map<String, String>> get messageHistory {
// 首先尝试从ChatController获取消息历史
if (Get.isRegistered<ChatController>()) {
final chatController = Get.find<ChatController>();
// 检查ChatController是否正在与personal_assistant交互
if (chatController.agent?.id == 'personal_assistant') {
return chatController.messages.map((m) => {
'role': m.role,
'content': m.content,
}).toList();
}
}
// 设置30秒的超时计时器
_interactionTimer = Timer(const Duration(seconds: 30), () {
// 检查TTS是否正在播放
if (_ttsService.isPlaying) {
// 如果TTS正在播放,重置定时器
_resetInteractionTimer();
} else {
// 如果TTS不在播放,且30秒内没有检测到用户输入,退出交互
Logger.info('30秒内没有检测到用户输入,退出交互');
_endInteraction();
// 如果无法从ChatController获取,则使用本地存储的历史记录
if (_localMessageHistory.isEmpty) {
_loadChatHistory();
}
});
return _localMessageHistory;
}
// 结束交互
Future<void> _endInteraction() async {
// 完成当前的recognitionCompleter(如果有)
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.complete('');
// 本地消息历史备份
final List<Map<String, String>> _localMessageHistory = [];
// 构造函数
BackgroundAgentService() {
Logger.info('BackgroundAgentService 已创建');
}
_isProcessing = false;
// TTS是否正在播放,直接从VolcanoTtsApiService获取
bool get isTtsSpeaking => _ttsService.isPlaying;
// 播放退出提示
await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _agentVoice);
@override
void onClose() {
// 取消所有订阅
_recognitionSubscription?.cancel();
_aiResponseSubscription?.cancel();
// 停止语音识别
if (_isListening) {
await stopVoiceRecognition();
Logger.info('退出交互,已停止语音识别');
// 停止TTS
if (_ttsService.isPlaying) {
_ttsService.stop();
}
// 清除当前系统提示词
_currentSystemPrompt = null;
// 清除当前agent ID
_currentAgentId = null;
// 重置agent voice为默认值
_agentVoice = 'zh_female_meilinvyou_moon_bigtts';
// 停止语音识别
if (isListening.value) {
_asrService.stopContinuousRecognition();
}
// 开始语音交互
Future<void> startAgentInteraction(String agentId) async {
// 设置当前agent ID
_currentAgentId = agentId;
// 取消定时器
_inactivityTimer?.cancel();
// 获取agent信息,只查询一次
final agent = AgentProvider.getAgentById(agentId);
if (agent == null) {
Logger.error('无法获取agent: $agentId');
return;
super.onClose();
}
// 存储需要的值
_agentVoice = agent.voice;
_currentSystemPrompt = agent.systemPrompt;
// 播放提示音
await _ttsService.speakSingle("我在听", speaker: _agentVoice);
/// 开始代理交互
///
/// [agentId] 要交互的代理ID
Future<void> startAgentInteraction(String agentId) async {
// 先说"我在听",然后再播放欢迎消息
const listeningMessage = "我在听";
_ttsService.speakSingle(listeningMessage);
if (_isProcessing) {
print('已经在处理交互,忽略此次请求');
// 防止重复处理
if (isProcessing.value) {
Logger.warning('已经在处理交互,忽略此次请求');
return;
}
_isProcessing = true;
isProcessing.value = true;
try {
// 确保之前的语音识别已经停止
if (_isListening) {
await stopVoiceRecognition();
// 获取Agent
final agent = AgentProvider.getAgentById(agentId);
if (agent == null) {
Logger.error('无法获取Agent: $agentId');
isProcessing.value = false;
return;
}
// 启动语音识别
await startVoiceRecognition();
_currentAgent = agent;
// 启动交互计时器
_resetInteractionTimer();
} catch (e) {
Logger.error('启动语音交互失败: $e');
_isProcessing = false;
// 加载该Agent的聊天历史
_loadChatHistory();
// 播放错误提示
await _ttsService.speakSingle("抱歉,启动语音交互失败", speaker: _agentVoice);
}
}
// 启动语音识别 - 保持持续识别状态
await _startVoiceRecognition();
// 停止语音交互
Future<void> stopAgentInteraction() async {
// 取消交互计时器
_interactionTimer?.cancel();
_interactionTimer = null;
// 启动无活动定时器
_startInactivityTimer();
// 停止TTS播放
if (_ttsService.isPlaying) {
await _ttsService.stop();
}
// 停止语音识别
if (_isListening) {
await stopVoiceRecognition();
Logger.info('手动停止交互,已停止语音识别');
} catch (e) {
Logger.error('启动代理交互失败', e);
isProcessing.value = false;
}
// 清除处理状态
_isProcessing = false;
_currentSystemPrompt = null;
// 重置agent voice为默认值
_agentVoice = 'zh_female_meilinvyou_moon_bigtts';
Logger.info('手动停止交互,已结束TTS会话');
}
// 添加一个 Completer 用于在识别到最终结果时完成
Completer<String>? _recognitionCompleter;
// 开始语音识别
Future<void> startVoiceRecognition() async {
if (_isListening) {
await stopVoiceRecognition();
/// 启动语音识别
Future<void> _startVoiceRecognition() async {
if (isListening.value) {
// 如果已经在监听,不需要重新启动
Logger.info('语音识别已经在运行,无需重新启动');
return;
}
try {
Logger.info('开始初始化语音识别服务...');
// 确保语音识别服务已初始化
if (!await _voiceRecognitionService.initialize(
subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '',
serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia',
language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN',
)) {
// 初始化失败,直接抛出异常
Logger.error('语音识别服务初始化失败');
throw Exception('无法初始化语音识别服务');
}
// 开始连续识别
final success = await _voiceRecognitionService.startContinuousRecognition();
if (!success) {
Logger.error('启动连续语音识别失败');
throw Exception('无法启动语音识别');
}
_isListening = true;
// 启动语音识别
final success = await _asrService.startContinuousRecognition();
if (success) {
isListening.value = true;
recognizedText.value = '';
_hasRecognizedSpeech = false;
_hasFinalResult = false;
// 添加一个变量来存储完整的用户输入
String fullUserInput = '';
// 使用语音识别服务的recognitionStream而不是方法的返回值
_recognitionSubscription = _voiceRecognitionService.recognitionStream?.listen((event) {
if (event.type == RecognitionEventType.finalResult) {
// 添加日志输出最终识别结果
Logger.info('语音识别最终结果: ${event.text}');
// 监听识别事件
_recognitionSubscription = _asrService.recognitionStream?.listen(
(event) {
// 重置无活动定时器
_resetInactivityTimer();
switch (event.type) {
case RecognitionEventType.recognizing:
// 实时更新识别文本
if (event.text.isNotEmpty) {
// 将最终结果添加到完整输入中,并添加适当的标点符号
if (fullUserInput.isNotEmpty && !fullUserInput.endsWith('。') &&
!fullUserInput.endsWith('?') && !fullUserInput.endsWith('!') &&
!fullUserInput.endsWith('.') && !fullUserInput.endsWith('?') &&
!fullUserInput.endsWith('!')) {
fullUserInput += ',';
}
fullUserInput += event.text;
// 更新可观察的识别文本
recognizedText.value = fullUserInput;
_hasRecognizedSpeech = true;
_hasFinalResult = true;
// 重置交互计时器,因为用户刚刚说了话
_resetInteractionTimer();
recognizedText.value = event.text;
_processUserInput(fullUserInput);
// 重置用户输入
fullUserInput = '';
recognizedText.value = '';
_hasRecognizedSpeech = false;
_hasFinalResult = false;
// 如果正在播放TTS,检测到用户说话则中断当前播放和AI回复
if (_ttsService.isPlaying && event.text.trim().length > 3) {
_interruptCurrentResponse("检测到用户说话,中断当前响应");
}
} else if (event.type == RecognitionEventType.recognizing) {
// 添加日志输出中间识别结果
Logger.info('语音识别中间结果: ${event.text}');
}
break;
case RecognitionEventType.finalResult:
// 最终结果
if (event.text.isNotEmpty) {
// 更新当前的中间结果,但不添加到完整输入中
recognizedText.value = fullUserInput + (fullUserInput.isEmpty ? "" : ",") + event.text;
_hasRecognizedSpeech = true;
// 检测到用户说话,重置交互计时器
_resetInteractionTimer();
// 如果系统正在播放TTS或接收AI响应,检测到用户开始说话时立即中断
if (_ttsService.isPlaying && event.text.trim().isNotEmpty) {
// 停止当前TTS播放
_ttsService.stop();
// 设置标志,表示应该取消当前的AI响应
_shouldCancelAiResponse = true;
// 取消当前的AI响应流订阅
_aiResponseSubscription?.cancel();
_aiResponseSubscription = null;
recognizedText.value = event.text;
// 处理识别到的文本
_processRecognizedText(event.text);
// 添加日志输出中断TTS播放
Logger.info('检测到用户说话,中断TTS播放');
}
// 重置识别文本但保持识别状态
recognizedText.value = '';
}
} else if (event.type == RecognitionEventType.error) {
// 添加日志输出识别错误
Logger.error('语音识别错误: ${event.error}');
print('识别错误: ${event.error}');
break;
case RecognitionEventType.sessionStopped:
case RecognitionEventType.canceled:
// 会话结束,表示识别在其它地方被启动,直接退出后台服务
isListening.value = false;
Logger.info('语音识别会话被中止,退出后台代理服务');
stopInteraction();
break;
default:
break;
}
}, onError: (error) {
// 添加日志输出语音识别流错误
Logger.error('语音识别流错误: $error');
print('语音识别流错误: $error');
_isListening = false;
},
onError: (error) {
Logger.error('语音识别错误', error);
isListening.value = false;
});
// 启动交互计时器
_resetInteractionTimer();
// 如果出错,尝试重新启动语音识别
_restartVoiceRecognition();
},
);
} else {
Logger.error('启动语音识别失败');
isProcessing.value = false;
}
} catch (e) {
Logger.error('启动语音识别失败: $e');
print('启动语音识别失败: $e');
_isListening = false;
Logger.error('启动语音识别失败', e);
isListening.value = false;
rethrow;
isProcessing.value = false;
}
}
// 处理用户输入
Future<void> _processUserInput(String userInput) async {
if (userInput.isEmpty || _currentSystemPrompt == null) {
return;
}
/// 处理识别到的文本
Future<void> _processRecognizedText(String text) async {
if (text.trim().isEmpty || _currentAgent == null) return;
try {
// 保存用户消息到历史记录
_messageHistory.add(Message(
role: 'user',
content: userInput,
timestamp: DateTime.now(),
));
// 构建用于生成回应的消息列表
final messages = [
{'role': 'system', 'content': _currentSystemPrompt!},
{'role': 'user', 'content': userInput},
];
// 添加用户消息到历史记录
_addMessageToHistory('user', text);
// 启动TTS会话,使用当前Agent的voice
_ttsService.startSession(_currentAgent!.voice);
// 获取AI响应
String fullResponse = '';
Logger.info('开始生成AI响应...');
// 重置取消标志
_shouldCancelAiResponse = false;
// 创建一个本地变量来跟踪是否已取消
bool isCancelled = false;
// 使用成员变量中存储的agent voice
_ttsService.startSession(_agentVoice);
// 获取AI响应流
final responseStream = _aiService.sendMessageStream(
messages: messages,
systemPrompt: _currentSystemPrompt!,
messages: messageHistory,
systemPrompt: _currentAgent!.systemPrompt,
);
// 创建一个订阅来处理响应流
// 订阅响应流
_aiResponseSubscription = responseStream.listen(
(chunk) {
// 如果已经设置了取消标志,则不处理这个块
if (_shouldCancelAiResponse) {
isCancelled = true;
return;
}
fullResponse += chunk;
// 直接将每个文本块传递给 TTS 接口,不进行切句或攒句处理
try {
// 使用成员变量中存储的agent voice
_ttsService.speak(chunk, speaker: _agentVoice);
// 如果TTS启用,发送到TTS
_ttsService.speak(chunk);
// Logger.info('直接播放 AI 响应块: $chunk');
} catch (e) {
Logger.error('TTS 播放出错: $e');
}
},
onError: (e) {
Logger.error('AI响应流错误: $e');
print('AI响应流错误: $e');
// 清理订阅
_aiResponseSubscription = null;
// 重置无活动定时器
_resetInactivityTimer();
},
onDone: () {
// 添加AI响应到历史记录
_addMessageToHistory('assistant', fullResponse);
// 结束TTS会话
_ttsService.endSession();
// 如果已取消,不处理剩余的文本
if (!isCancelled && !_shouldCancelAiResponse) {
Logger.info('AI 响应生成完成');
}
_aiResponseSubscription = null;
// 注意:我们不再在这里重启语音识别,因为它应该已经在运行
},
onError: (error) {
Logger.error('AI响应错误', error);
// AI响应完成后,重新启动超时计时器
_resetInteractionTimer();
// 播放错误消息
const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。';
_ttsService.speak(errorMessage);
// 如果响应被取消,不保存到历史记录
if (!_shouldCancelAiResponse && fullResponse.isNotEmpty) {
// 保存助手回复到历史记录
_messageHistory.add(Message(
role: 'assistant',
content: fullResponse,
timestamp: DateTime.now(),
));
// 添加错误消息到历史记录
_addMessageToHistory('assistant', errorMessage);
// Logger.info('保存AI响应到历史记录,长度: ${fullResponse.length}');
}
// 结束TTS会话
_ttsService.endSession();
},
);
} catch (e) {
Logger.error('处理识别文本失败', e);
// 限制历史记录长度
if (_messageHistory.length > 20) {
_messageHistory.removeRange(0, _messageHistory.length - 20);
Logger.info('历史记录超过20条,已裁剪');
// 播放错误消息
const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。';
_ttsService.speak(errorMessage);
// 添加错误消息到历史记录
_addMessageToHistory('assistant', errorMessage);
}
}
);
} catch (e) {
Logger.error('处理用户输入失败: $e');
print('处理用户输入失败: $e');
// 播放错误提示,使用成员变量中存储的agent voice
await _ttsService.speak("抱歉,出现了一些问题", speaker: _agentVoice);
/// 重新启动语音识别(仅在识别会话意外终止时使用)
Future<void> _restartVoiceRecognition() async {
// 停止当前的语音识别
if (isListening.value) {
await _asrService.stopContinuousRecognition();
isListening.value = false;
// 取消当前的订阅
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
}
// 等待一段时间,确保资源被正确释放
await Future.delayed(Duration(milliseconds: 500));
// 重新启动语音识别
await _startVoiceRecognition();
}
// 停止语音识别并返回识别的文本
Future<String> stopVoiceRecognition() async {
if (!_isListening) {
return '';
/// 添加消息到历史记录
void _addMessageToHistory(String role, String content) {
// 添加到本地历史记录
_localMessageHistory.add({
'role': role,
'content': content,
});
// 保持本地历史记录不超过10条消息
if (_localMessageHistory.length > 10) {
_localMessageHistory.removeAt(0);
}
try {
// 取消订阅
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
// 如果ChatController可用,同步消息到ChatController
if (Get.isRegistered<ChatController>()) {
final chatController = Get.find<ChatController>();
if (chatController.agent?.id == 'personal_assistant') {
// 向ChatController添加消息
final message = Message(
role: role,
content: content,
timestamp: DateTime.now(),
);
chatController.addMessage(message);
}
} else {
// 如果ChatController不可用,保存到本地存储
_saveChatHistory();
}
}
// 停止语音识别
await _voiceRecognitionService.stopContinuousRecognition();
// 加载聊天历史
void _loadChatHistory() {
if (_currentAgent == null) return;
// 获取最终识别结果
final result = recognizedText.value;
try {
// 加载聊天历史
final messages = _historyService.loadHistory(_currentAgent!.id);
// 清空本地历史,然后添加加载的历史
_localMessageHistory.clear();
// 将Message转换为Map格式
if (messages.isNotEmpty) {
for (final message in messages) {
_localMessageHistory.add({
'role': message.role,
'content': message.content,
});
}
Logger.info('已加载${messages.length}条聊天历史记录,代理: ${_currentAgent!.id}');
}
} catch (e) {
Logger.error('加载聊天历史记录失败', e);
}
}
// 添加日志输出停止语音识别的最终结果
Logger.info('停止语音识别,最终结果: $result');
// 保存聊天历史
void _saveChatHistory() {
if (_currentAgent == null) return;
// 重置状态
_isListening = false;
isListening.value = false;
try {
// 将本地历史转换为Message列表
final messages = _localMessageHistory.map((m) => Message(
role: m['role']!,
content: m['content']!,
timestamp: DateTime.now(),
)).toList();
return result;
// 保存到ChatHistoryService
_historyService.saveHistory(_currentAgent!.id, messages);
} catch (e) {
// 添加日志输出停止语音识别失败
Logger.error('停止语音识别失败: $e');
print('停止语音识别失败: $e');
_isListening = false;
isListening.value = false;
Logger.error('保存聊天历史记录失败', e);
}
}
// 直接返回当前已识别的文本,不尝试重置
final result = recognizedText.value;
Logger.info('停止语音识别失败,使用当前识别结果: $result');
return result;
/// 启动无活动定时器
void _startInactivityTimer() {
_inactivityTimer?.cancel();
_inactivityTimer = Timer(Duration(seconds: 30), () {
// 如果30秒内没有TTS输出或用户说话,停止语音识别
if (!_ttsService.isPlaying && isProcessing.value) {
Logger.info('检测到30秒内无活动,停止语音识别');
stopInteraction();
}
});
}
List<Message> get messageHistory => List.unmodifiable(_messageHistory);
/// 重置无活动定时器
void _resetInactivityTimer() {
if (_inactivityTimer != null) {
_inactivityTimer!.cancel();
_startInactivityTimer();
}
}
/// 中断当前响应
void _interruptCurrentResponse(String reason) {
Logger.info(reason);
void clearHistory() {
_messageHistory.clear();
// 停止TTS
if (_ttsService.isPlaying) {
_ttsService.stop();
}
@override
void onClose() {
// 取消AI响应流订阅
_aiResponseSubscription?.cancel();
_aiResponseSubscription = null;
}
/// 响应新的媒体事件(双击媒体按钮)
Future<void> respondToNewMediaEvent() async {
// 不管当前状态如何,总是中断当前响应
_interruptCurrentResponse("检测到新的媒体事件,中断当前响应");
// 回答"我在听"
const listeningMessage = "我在听";
_ttsService.speakSingle(listeningMessage);
// 不需要重启语音识别,因为它应该已经在运行
// 如果识别会话已经终止,确保重新启动
if (!isListening.value) {
await _restartVoiceRecognition();
}
// 重置无活动定时器
_resetInactivityTimer();
}
/// 停止当前交互
Future<void> stopInteraction() async {
try {
// 保存聊天历史
_saveChatHistory();
// 取消所有订阅
_recognitionSubscription?.cancel();
_interactionTimer?.cancel();
_interactionTimer = null;
_aiResponseSubscription?.cancel();
// 停止TTS播放
// 停止TTS
if (_ttsService.isPlaying) {
_ttsService.stop();
}
super.onClose();
// 停止语音识别
// if (isListening.value) {
// await _asrService.stopContinuousRecognition();
// isListening.value = false;
// }
// 取消定时器
_inactivityTimer?.cancel();
// 重置状态
isProcessing.value = false;
_currentAgent = null;
Logger.info('已停止后台代理交互');
} catch (e) {
Logger.error('停止交互失败', e);
}
}
}

97
lib/data/services/chat_history_service.dart

@ -0,0 +1,97 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../modules/chat/models/message_model.dart';
import '../../core/utils/logger.dart';
/// 聊天记录管理服务,负责保存和加载聊天记录
class ChatHistoryService extends GetxService {
final GetStorage _storage = GetStorage();
// 存储键名
static const String _historyKeyPrefix = 'chat_history_';
static const int _maxHistoryCount = 20; // 每个对话最多保存的消息数
@override
void onInit() {
super.onInit();
Logger.info('ChatHistoryService 已初始化');
}
/// 生成历史记录存储键
String _getHistoryKey(String agentId) => '$_historyKeyPrefix$agentId';
/// 保存聊天记录
///
/// [agentId] 代理ID
/// [messages] 消息列表
Future<void> saveHistory(String agentId, List<Message> messages) async {
try {
// 过滤掉加载中的消息
final filteredMessages = messages.where((m) => !m.isLoading).toList();
// 如果消息超过最大数量,只保留最新的消息
final messagesToSave = filteredMessages.length > _maxHistoryCount
? filteredMessages.sublist(filteredMessages.length - _maxHistoryCount)
: filteredMessages;
// 转换为JSON
final jsonList = messagesToSave.map((m) => m.toJson()).toList();
final jsonString = jsonEncode(jsonList);
// 保存到存储
await _storage.write(_getHistoryKey(agentId), jsonString);
Logger.info('已保存${messagesToSave.length}条聊天记录,代理: $agentId');
} catch (e) {
Logger.error('保存聊天记录失败', e);
}
}
/// 加载聊天记录
///
/// [agentId] 代理ID
/// 返回消息列表,如果没有找到或解析错误则返回空列表
List<Message> loadHistory(String agentId) {
try {
final jsonString = _storage.read<String>(_getHistoryKey(agentId));
if (jsonString == null) {
return [];
}
final jsonList = jsonDecode(jsonString) as List;
final messages = jsonList.map((json) => Message.fromJson(json)).toList();
Logger.info('已加载${messages.length}条聊天记录,代理: $agentId');
return messages;
} catch (e) {
Logger.error('加载聊天记录失败', e);
return [];
}
}
/// 清除特定代理的聊天记录
Future<void> clearHistory(String agentId) async {
try {
await _storage.remove(_getHistoryKey(agentId));
Logger.info('已清除聊天记录,代理: $agentId');
} catch (e) {
Logger.error('清除聊天记录失败', e);
}
}
/// 清除所有聊天记录
Future<void> clearAllHistory() async {
try {
final keys = _storage.getKeys();
for (final key in keys) {
if (key.startsWith(_historyKeyPrefix)) {
await _storage.remove(key);
}
}
Logger.info('已清除所有聊天记录');
} catch (e) {
Logger.error('清除所有聊天记录失败', e);
}
}
}

117
lib/data/services/my_audio_handler.dart

@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'background_agent_service.dart';
import '../../data/providers/agent_provider.dart';
import '../../core/utils/logger.dart';
/// 自定义 AudioHandler,用于捕获媒体按键事件(例如蓝牙耳机按钮)
class MyAudioHandler extends BaseAudioHandler {
@ -29,19 +30,20 @@ class MyAudioHandler extends BaseAudioHandler {
_initializeHandler();
_initializeBackgroundAgent();
_startStatusUpdateTimer();
print('MyAudioHandler 已创建,准备接收媒体按钮事件');
Logger.info('MyAudioHandler 已创建,准备接收媒体按钮事件');
}
void _initializeBackgroundAgent() {
try {
if (Get.isRegistered<BackgroundAgentService>()) {
_backgroundAgent = Get.find<BackgroundAgentService>();
Logger.info('BackgroundAgent 初始化成功');
} else {
_backgroundAgent = Get.put(BackgroundAgentService(), permanent: true);
Logger.info('BackgroundAgent 已创建并初始化成功');
}
print('BackgroundAgent 初始化成功');
} catch (e) {
print('初始化 BackgroundAgent 失败: $e');
Logger.error('初始化 BackgroundAgent 失败', e);
_backgroundAgent = null;
}
}
@ -87,7 +89,7 @@ class MyAudioHandler extends BaseAudioHandler {
);
mediaItem.add(item);
print('AudioHandler 媒体项已设置');
Logger.info('AudioHandler 媒体项已设置');
// 立即更新播放状态,确保通知显示
_updatePlaybackState(
@ -95,7 +97,7 @@ class MyAudioHandler extends BaseAudioHandler {
processingState: AudioProcessingState.ready,
);
} catch (e) {
print('初始化 AudioHandler 失败: $e');
Logger.error('初始化 AudioHandler 失败', e);
}
}
@ -109,9 +111,9 @@ class MyAudioHandler extends BaseAudioHandler {
playing: true,
processingState: AudioProcessingState.ready,
);
print('定期更新播放状态成功');
Logger.info('定期更新播放状态成功');
} catch (e) {
print('定期更新播放状态失败: $e');
Logger.error('定期更新播放状态失败', e);
}
});
}
@ -132,19 +134,19 @@ class MyAudioHandler extends BaseAudioHandler {
// 如果状态变为播放,确保前台服务启动
if (playing) {
print('更新播放状态为播放中,确保前台服务启动');
Logger.info('更新播放状态为播放中,确保前台服务启动');
}
} catch (e) {
print('更新播放状态失败: $e');
Logger.error('更新播放状态失败', e);
}
}
Future<void> _handleInteraction() async {
print('处理媒体按钮交互');
Logger.info('处理媒体按钮交互');
// 防止重复处理
if (_isHandlingInteraction) {
print('已经在处理交互,忽略此次请求');
Logger.info('已经在处理交互,忽略此次请求');
return;
}
@ -160,14 +162,25 @@ class MyAudioHandler extends BaseAudioHandler {
// 直接获取 personal_assistant agent
final agent = AgentProvider.getAgentById('personal_assistant');
if (agent == null) {
print('无法获取personal_assistant agent,无法激活语音助手');
Logger.error('无法获取personal_assistant agent,无法激活语音助手');
_isHandlingInteraction = false;
return;
}
if (Get.context != null &&
WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed) {
// 检查app是否在前台
final isInForeground = Get.context != null &&
WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed;
if (isInForeground) {
// 应用在前台,导航到聊天页面
print('应用在前台,导航到聊天页面');
Logger.info('应用在前台,导航到聊天页面');
// 如果BackgroundAgent正在运行,先停止它
if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) {
await _backgroundAgent!.stopInteraction();
Logger.info('已停止后台代理服务,切换到前台模式');
}
// 导航到聊天页面,并标记需要在进入时播放语音应答
Get.toNamed('/chat', arguments: {
'agentId': agent.id,
@ -181,18 +194,26 @@ class MyAudioHandler extends BaseAudioHandler {
});
} else {
// 应用在后台,使用 BackgroundAgent 处理交互
print('应用在后台,使用 BackgroundAgent 处理交互');
Logger.info('应用在后台,使用 BackgroundAgent 处理交互');
if (_backgroundAgent != null) {
// 调用 BackgroundAgent 的语音识别和 AI 交互功能,传入agent ID
if (_backgroundAgent!.isProcessing.value) {
// 如果后台服务已在处理,则中断当前响应并回复"我在听"
Logger.info('检测到媒体按钮再次按下,中断当前AI回复');
await _backgroundAgent!.respondToNewMediaEvent();
} else {
// 首次启动后台服务
Logger.info('首次启动后台服务');
await _backgroundAgent!.startAgentInteraction(agent.id);
}
} else {
print('BackgroundAgent 未初始化,无法处理交互');
Logger.error('BackgroundAgent 未初始化,无法处理交互');
// 尝试重新初始化 BackgroundAgent
_initializeBackgroundAgent();
}
}
} catch (e) {
print('处理媒体按钮交互失败: $e');
Logger.error('处理媒体按钮交互失败', e);
} finally {
_isHandlingInteraction = false;
}
@ -200,7 +221,7 @@ class MyAudioHandler extends BaseAudioHandler {
@override
Future<void> play() async {
print('收到播放命令');
Logger.info('收到播放命令');
try {
_updatePlaybackState(
playing: true,
@ -208,13 +229,13 @@ class MyAudioHandler extends BaseAudioHandler {
);
await _handleInteraction();
} catch (e) {
print('处理播放命令失败: $e');
Logger.error('处理播放命令失败', e);
}
}
@override
Future<void> pause() async {
print('收到暂停命令');
Logger.info('收到暂停命令');
try {
_updatePlaybackState(
playing: false,
@ -222,16 +243,22 @@ class MyAudioHandler extends BaseAudioHandler {
);
await _handleInteraction();
} catch (e) {
print('处理暂停命令失败: $e');
Logger.error('处理暂停命令失败', e);
}
}
@override
Future<void> stop() async {
print('停止 AudioHandler');
Logger.info('停止 AudioHandler');
_isDisposed = true;
try {
_statusUpdateTimer?.cancel();
// 如果有正在运行的后台服务,停止它
if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) {
await _backgroundAgent!.stopInteraction();
}
await super.stop();
_customEventController.close();
_updatePlaybackState(
@ -239,13 +266,13 @@ class MyAudioHandler extends BaseAudioHandler {
processingState: AudioProcessingState.idle,
);
} catch (e) {
print('停止 AudioHandler 时发生错误: $e');
Logger.error('停止 AudioHandler 时发生错误', e);
}
}
@override
Future<void> skipToPrevious() async {
print('收到上一曲命令');
Logger.info('收到上一曲命令');
try {
_updatePlaybackState(
playing: true,
@ -253,13 +280,13 @@ class MyAudioHandler extends BaseAudioHandler {
);
await _handleInteraction();
} catch (e) {
print('处理上一曲命令失败: $e');
Logger.error('处理上一曲命令失败', e);
}
}
@override
Future<void> skipToNext() async {
print('收到下一曲命令');
Logger.info('收到下一曲命令');
try {
_updatePlaybackState(
playing: true,
@ -267,29 +294,29 @@ class MyAudioHandler extends BaseAudioHandler {
);
await _handleInteraction();
} catch (e) {
print('处理下一曲命令失败: $e');
Logger.error('处理下一曲命令失败', e);
}
}
@override
Future<void> seek(Duration position) async {
print('收到跳转命令: $position');
Logger.info('收到跳转命令: $position');
}
@override
Future<dynamic> customAction(String name,
[Map<String, dynamic>? extras]) async {
print('收到自定义命令: $name');
Logger.info('收到自定义命令: $name');
try {
if (name == 'media_button') {
print('收到媒体按钮事件');
Logger.info('收到媒体按钮事件');
// 检查是否是重复触发(防止短时间内多次触发)
final now = DateTime.now();
if (_lastMediaButtonEventTime != null) {
final difference = now.difference(_lastMediaButtonEventTime!);
if (difference.inSeconds < 2) {
print('媒体按钮事件触发过于频繁,忽略此次事件');
if (difference.inMilliseconds < 500) {
Logger.info('媒体按钮事件触发过于频繁,忽略此次事件');
return;
}
}
@ -304,31 +331,41 @@ class MyAudioHandler extends BaseAudioHandler {
await _handleInteraction();
} else if (name == 'ping') {
// 用于检查服务是否响应的测试命令
print('收到 ping 命令,服务正常运行');
Logger.info('收到 ping 命令,服务正常运行');
return 'pong';
}
} catch (e) {
print('处理自定义命令失败: $e');
Logger.error('处理自定义命令失败', e);
}
}
@override
Future<void> onTaskRemoved() async {
print('服务被系统移除');
Logger.info('服务被系统移除');
try {
// 如果有正在运行的后台服务,停止它
if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) {
await _backgroundAgent!.stopInteraction();
}
await stop();
} catch (e) {
print('处理服务移除失败: $e');
Logger.error('处理服务移除失败', e);
}
}
@override
Future<void> onNotificationDeleted() async {
print('通知被用户移除');
Logger.info('通知被用户移除');
try {
// 如果有正在运行的后台服务,停止它
if (_backgroundAgent != null && _backgroundAgent!.isProcessing.value) {
await _backgroundAgent!.stopInteraction();
}
await stop();
} catch (e) {
print('处理通知移除失败: $e');
Logger.error('处理通知移除失败', e);
}
}
}

3
lib/data/services/volcano_asr_service.dart

@ -240,6 +240,9 @@ class VolcanoAsrService extends GetxService {
/// 开始连续识别
Future<bool> startContinuousRecognition() async {
//启动前先尝试停止当前识别
if (_isListening.value) {
Logger.warning('已经在进行语音识别,请先停止当前识别');
return false;

11
lib/modules/chat/bindings/chat_binding.dart

@ -1,16 +1,9 @@
import 'package:get/get.dart';
import '../controllers/chat_controller.dart';
import '../../../data/services/volcano_tts_api_service.dart';
class ChatBinding implements Bindings {
class ChatBinding extends Bindings {
@override
void dependencies() {
// Ensure TTS service is registered first
if (!Get.isRegistered<VolcanoTtsApiService>()) {
Get.put(VolcanoTtsApiService());
}
// Create new controller instance
Get.put(ChatController());
Get.lazyPut<ChatController>(() => ChatController());
}
}

1157
lib/modules/chat/controllers/chat_controller.dart

File diff suppressed because it is too large

348
lib/modules/chat/controllers/voice_input_controller.dart

@ -1,348 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import '../../../data/services/azure_asr_service.dart';
import '../../../data/services/volcano_tts_api_service.dart';
import '../../../core/utils/logger.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
class VoiceInputController extends GetxController {
// Observable states
final isConnecting = true.obs;
final isMuted = false.obs;
final isRecording = false.obs;
final recognizedText = ''.obs;
// 添加一个新的可观察状态,表示用户是否正在说话
final isUserSpeaking = false.obs;
// 语音识别服务
late final AzureAsrService _voiceService;
late final VolcanoTtsApiService _ttsService;
// 连续识别相关
StreamSubscription? _recognitionSubscription;
// TTS相关订阅
StreamSubscription? _ttsEventSubscription;
bool _isTtsSpeaking = false;
// 添加一个标志,表示是否已经识别到语音
bool _hasRecognizedSpeech = false;
// 添加一个 Completer 用于在识别到最终结果时完成
Completer<String>? _recognitionCompleter;
// 添加一个标志,表示是否已经收到最终结果
bool _hasFinalResult = false;
// 添加一个计时器,用于定期更新用户说话状态
Timer? _speakingCheckTimer;
// 最后一次识别到语音的时间
DateTime? _lastSpeechTime;
// Callbacks
final Function(String) onRecordingResult;
final VoidCallback onClosePanel;
final Function(String) onRecognizing;
VoiceInputController({
required this.onRecordingResult,
required this.onClosePanel,
required this.onRecognizing,
});
@override
void onInit() {
super.onInit();
_initializeVoiceService();
_ttsService = Get.find<VolcanoTtsApiService>();
// 设置TTS事件监听
_setupTtsEventListener();
// 启动定期检查用户是否正在说话的计时器
_startSpeakingCheckTimer();
}
// 设置TTS事件监听
void _setupTtsEventListener() {
_ttsEventSubscription?.cancel();
_ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent);
}
// 处理TTS事件
void _handleTtsEvent(Map<String, dynamic> event) {
final eventType = event['eventType'];
switch (eventType) {
case 'ttsSentenceStart':
_isTtsSpeaking = true;
break;
case 'ttsSentenceEnd':
case 'sessionFinished':
case 'error':
_isTtsSpeaking = false;
break;
}
}
Future<void> _initializeVoiceService() async {
try {
// 获取语音识别服务实例
_voiceService = Get.find<AzureAsrService>();
// 初始化语音识别服务
await _voiceService.initialize(
subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '',
serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia',
language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN',
);
// 初始化完成后更新状态
isConnecting.value = false;
// 自动开始连续语音识别
await startContinuousRecognition();
} catch (e) {
Logger.error('初始化语音识别服务失败', e);
Get.snackbar(
'Error',
'初始化语音识别服务失败: $e',
snackPosition: SnackPosition.BOTTOM,
);
}
}
Future<void> startContinuousRecognition() async {
if (isConnecting.value) return;
if (isRecording.value) return; // 已经在录音中
try {
isRecording.value = true;
recognizedText.value = ''; // 清空识别文本,但不再在面板中显示
_hasRecognizedSpeech = false;
_hasFinalResult = false;
// 创建一个 Completer 来处理语音识别完成
_recognitionCompleter = Completer<String>();
// 开始连续语音识别
final success = await _voiceService.startContinuousRecognition();
if (!success) {
isRecording.value = false;
throw Exception('无法启动语音识别');
}
// 订阅识别事件流
_recognitionSubscription = _voiceService.recognitionStream?.listen((event) {
switch (event.type) {
case RecognitionEventType.recognizing:
// 不再更新面板中的识别文本,而是通过回调传递给ChatController
if (event.text.isNotEmpty) {
_hasRecognizedSpeech = true;
_lastSpeechTime = DateTime.now();
// 设置用户正在说话状态
isUserSpeaking.value = true;
// 如果系统正在播放TTS,检测到用户开始说话时立即中断
if (event.text.trim().isNotEmpty) {
print('检测到用户开始说话,中断TTS播放');
_ttsService.stop();
_ttsService.endSession(); // 结束当前TTS会话
}
}
// 调用识别中回调,将中间结果传递给ChatController
onRecognizing(event.text);
break;
case RecognitionEventType.finalResult:
// 更新最终识别结果
_hasFinalResult = true;
// 重置用户说话状态
isUserSpeaking.value = false;
if (event.text.isNotEmpty) {
// 不再更新面板中的识别文本
_hasRecognizedSpeech = true;
_lastSpeechTime = DateTime.now();
// 调用最终结果回调,发送识别到的句子
onRecordingResult(event.text);
// 清空识别状态,准备下一句,但不停止识别
_hasRecognizedSpeech = false;
_hasFinalResult = false;
// 完成当前识别,但不停止连续识别
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.complete(event.text);
}
} else {
// 如果最终结果为空,但之前有中间结果,仍然标记为有语音
if (_hasRecognizedSpeech) {
print('最终识别结果为空,但之前有中间结果,使用最后的中间结果');
// 清空识别状态,准备下一句,但不停止识别
_hasRecognizedSpeech = false;
_hasFinalResult = false;
}
}
break;
case RecognitionEventType.error:
print('语音识别错误: ${event.error}');
Get.snackbar(
'Error',
'语音识别错误: ${event.error}',
snackPosition: SnackPosition.BOTTOM,
);
// 尝试重新启动识别
restartRecognition();
break;
case RecognitionEventType.sessionStopped:
// 会话结束,尝试重新启动
isRecording.value = false;
restartRecognition();
break;
default:
break;
}
}, onError: (error) {
print('语音识别流错误: $error');
Get.snackbar(
'Error',
'语音识别流错误: $error',
snackPosition: SnackPosition.BOTTOM,
);
isRecording.value = false;
// 尝试重新启动识别
restartRecognition();
});
} catch (e) {
print('语音识别失败: $e');
Get.snackbar(
'Error',
'启动语音识别失败: $e',
snackPosition: SnackPosition.BOTTOM,
);
isRecording.value = false;
} finally {
// 清理资源
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.complete('');
}
_recognitionCompleter = null;
}
}
// 重新启动识别
Future<void> restartRecognition() async {
try {
// 先停止当前识别
await stopRecording();
// 延迟一下再重新启动
await Future.delayed(const Duration(milliseconds: 500));
// 重新启动连续识别
await startContinuousRecognition();
} catch (e) {
print('重新启动语音识别失败: $e');
}
}
Future<void> stopRecording() async {
if (!isRecording.value) return;
try {
// 取消事件订阅
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
// 停止连续识别
if (_voiceService.isContinuousRecognitionActive()) {
await _voiceService.stopContinuousRecognition();
}
isRecording.value = false;
_hasRecognizedSpeech = false;
_hasFinalResult = false;
} catch (e) {
print('停止语音识别失败: $e');
isRecording.value = false;
}
}
void toggleMute() {
// 只切换静音状态,不再影响录音
// 因为已启用回声消除,不需要在录音时停止TTS
isMuted.value = !isMuted.value;
}
// 手动开始录音(用户点击按钮)
void startRecording() {
if (!isRecording.value) {
startContinuousRecognition();
}
}
// 启动定期检查用户是否正在说话的计时器
void _startSpeakingCheckTimer() {
_speakingCheckTimer?.cancel();
_speakingCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
_checkIfUserIsSpeaking();
});
}
// 检查用户是否正在说话
void _checkIfUserIsSpeaking() {
// 如果有最近的识别文本,且距离现在不超过1秒,认为用户正在说话
if (_hasRecognizedSpeech && !_hasFinalResult && _lastSpeechTime != null) {
final now = DateTime.now();
final timeSinceLastSpeech = now.difference(_lastSpeechTime!);
// 如果最近1秒内有语音识别,认为用户正在说话
if (timeSinceLastSpeech.inMilliseconds < 1000) {
isUserSpeaking.value = true;
return;
}
}
// 默认情况下,如果正在录音但没有最近的语音识别,随机生成一个说话状态
// 保持这个功能,让麦克风图标有动画效果
if (isRecording.value) {
// 随机生成一个值,有20%的概率认为用户正在说话
isUserSpeaking.value = Random().nextDouble() < 0.2;
} else {
isUserSpeaking.value = false;
}
}
@override
void onClose() {
// 停止录音
stopRecording();
// 取消计时器
_speakingCheckTimer?.cancel();
// 取消TTS事件订阅
_ttsEventSubscription?.cancel();
// 重置状态
isConnecting.value = false;
isMuted.value = false;
isRecording.value = false;
recognizedText.value = '';
isUserSpeaking.value = false;
super.onClose();
}
}

31
lib/modules/chat/models/message_model.dart

@ -1,12 +1,14 @@
class Message {
final String role;
final String role; // 'user' or 'assistant'
final String content;
final DateTime timestamp;
final bool isLoading;
Message({
required this.role,
required this.content,
required this.timestamp,
this.isLoading = false,
});
// 从JSON构造函数
@ -15,6 +17,7 @@ class Message {
role: json['role'] as String,
content: json['content'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
isLoading: json['isLoading'] as bool? ?? false,
);
}
@ -24,6 +27,32 @@ class Message {
'role': role,
'content': content,
'timestamp': timestamp.toIso8601String(),
'isLoading': isLoading,
};
}
// 创建一个加载中的消息
factory Message.loading() {
return Message(
role: 'assistant',
content: '',
timestamp: DateTime.now(),
isLoading: true,
);
}
// 复制并修改
Message copyWith({
String? role,
String? content,
DateTime? timestamp,
bool? isLoading,
}) {
return Message(
role: role ?? this.role,
content: content ?? this.content,
timestamp: timestamp ?? this.timestamp,
isLoading: isLoading ?? this.isLoading,
);
}
}

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

@ -1,625 +1,490 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:math';
import '../controllers/chat_controller.dart';
import '../models/message_model.dart';
import '../../../data/services/volcano_tts_api_service.dart';
import '../widgets/voice_input_panel.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/message_bubble.dart';
class ChatView extends GetView<ChatController> {
const ChatView({Key? key}) : super(key: key);
ChatView({Key? key}) : super(key: key);
// 添加ScrollController用于控制滚动
final ScrollController _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// 停止TTS服务
final tts = Get.find<VolcanoTtsApiService>();
await tts.endSession();
// 停止语音输入
if (controller.isVoiceInputVisible.value) {
controller.stopVoiceInput();
}
return true;
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(),
body: SafeArea(
child: Stack(
children: [
// 主要内容
Column(
children: [
Expanded(
child: _buildMessageList(),
),
_buildInputSection(),
],
),
],
),
),
),
// 监听消息列表变化,自动滚动到底部
ever(controller.messages, (_) {
// 使用Future.delayed确保在UI更新后滚动
Future.delayed(Duration(milliseconds: 50), () {
if (_scrollController.hasClients &&
_scrollController.position.maxScrollExtent > 0 &&
(_scrollController.position.maxScrollExtent - _scrollController.offset) < 200) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 100),
curve: Curves.easeOut,
);
}
});
});
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
return Scaffold(
backgroundColor: Colors.white, // 设置Scaffold背景为白色
appBar: AppBar(
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: Colors.black,
size: 20.sp,
icon: Icon(Icons.arrow_back_ios, color: Colors.black),
onPressed: () => Get.back(),
),
onPressed: () async {
// 停止TTS服务
final tts = Get.find<VolcanoTtsApiService>();
await tts.endSession();
// 停止语音输入
if (controller.isVoiceInputVisible.value) {
controller.stopVoiceInput();
}
Get.back();
},
title: Row(
children: [
CircleAvatar(
backgroundImage: AssetImage(controller.agent.avatarUrl),
radius: 18.r,
),
title: Column(
SizedBox(width: 10.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
controller.agentName,
controller.agent.name,
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
if (controller.agentSubtitle != null)
Text(
controller.agentSubtitle!,
controller.agent.description.length > 20
? '${controller.agent.description.substring(0, 20)}...'
: controller.agent.description,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600],
color: Colors.grey,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
centerTitle: true,
),
],
),
actions: [
IconButton(
icon: Obx(() => Icon(
// TTS开关
Obx(() => IconButton(
icon: Icon(
controller.isTtsEnabled.value
? Icons.volume_up
: Icons.volume_off,
color: Colors.black,
size: 24.sp,
)),
onPressed: controller.toggleTTS,
color: controller.isTtsEnabled.value
? AppColors.primary
: Colors.grey,
),
IconButton(
icon: Icon(
Icons.more_horiz,
color: Colors.black,
size: 24.sp,
onPressed: controller.toggleTts,
)),
// 更多选项
PopupMenuButton(
icon: Icon(Icons.more_vert, color: Colors.black),
itemBuilder: (context) => [
PopupMenuItem(
value: 'clear',
child: Row(
children: [
Icon(Icons.delete_outline, color: Colors.red),
SizedBox(width: 8.w),
Text('清空对话'),
],
),
onPressed: () => _showMoreOptions(),
),
],
);
onSelected: (value) {
if (value == 'clear') {
controller.clearChat();
}
Widget _buildMessageList() {
return Stack(
},
),
],
elevation: 0,
backgroundColor: Colors.white,
),
body: Container(
color: Colors.white, // 确保body背景也是白色
child: Column(
children: [
Obx(() => ListView.builder(
// 消息列表
Expanded(
child: Obx(() => ListView.builder(
controller: controller.scrollController,
padding: EdgeInsets.only(
top: 16.h,
bottom: controller.isVoiceInputVisible.value
? 160.h
: (controller.isInputCollapsed.value ? 70.h : 90.h),
),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
itemCount: controller.messages.length,
itemBuilder: (context, index) => _buildMessage(controller.messages[index]),
reverse: false,
physics: const AlwaysScrollableScrollPhysics(),
clipBehavior: Clip.none,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
cacheExtent: 1000, // 增加缓存范围,减少重建
itemBuilder: (context, index) {
final message = controller.messages[index];
return MessageBubble(
key: ObjectKey(message), // 使用ObjectKey而不是ValueKey
message: message,
isUser: message.role == 'user',
);
},
)),
Obx(() {
if (!controller.isLoading.value) return const SizedBox.shrink();
return Positioned(
bottom: controller.isVoiceInputVisible.value
? 170.h
: (controller.isInputCollapsed.value ? 80.h : 100.h),
left: 0,
right: 0,
child: Container(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 16.w,
height: 16.w,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF4A6FE5)),
),
),
// 输入区域 - 根据当前模式显示不同的输入面板
Obx(() => controller.isVoiceRecognitionActive.value
? _buildVoiceInputPanel()
: _buildTextInputPanel()),
],
),
),
);
}),
],
);
}
Widget _buildInputSection() {
return Obx(() {
if (controller.isVoiceInputVisible.value) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
VoiceInputPanel(
onRecordingResult: controller.handleVoiceResult,
onClose: controller.stopVoiceInput,
onRecognizing: controller.handleRecognizing,
),
],
);
}
return _buildTextInput();
// 构建文本输入面板
Widget _buildTextInputPanel() {
// 创建一个TextEditingController来控制输入框
final TextEditingController textController = TextEditingController(text: controller.inputText.value);
// 监听输入变化
textController.addListener(() {
controller.inputText.value = textController.text;
});
}
Widget _buildTextInput() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
top: BorderSide(
color: Colors.grey[200]!,
width: 1,
),
),
),
child: Obx(() {
if (controller.isRecordingVoice.value) {
return _buildRecordingInput();
} else if (!controller.isVoiceMode.value) {
return _buildExpandedInput();
} else {
return _buildDefaultInput();
}
}),
);
}
Widget _buildDefaultInput() {
return Row(
children: [
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[100],
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: IconButton(
icon: Icon(
Icons.camera_alt,
color: Colors.grey[600],
size: 20.sp,
),
onPressed: () {
// 拍照功能
},
padding: EdgeInsets.zero,
),
),
SizedBox(width: 8.w),
Expanded(
child: GestureDetector(
onTap: () {
// 点击打开VoiceInputPanel
controller.startVoiceInput();
},
child: Container(
height: 48.h,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: Center(
child: Text(
'点击开始连续语音对话',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16.sp,
),
),
),
),
),
),
SizedBox(width: 8.w),
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[100],
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: IconButton(
icon: Icon(
Icons.keyboard,
color: Colors.grey[600],
size: 20.sp,
),
onPressed: controller.toggleInputMode,
padding: EdgeInsets.zero,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: Offset(0, -1),
),
],
);
}
Widget _buildExpandedInput() {
return Row(
children: [
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[100],
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: IconButton(
child: Row(
children: [
// 语音输入按钮
IconButton(
icon: Icon(
Icons.mic,
color: const Color(0xFF4A6FE5),
size: 20.sp,
),
onPressed: controller.toggleInputMode,
padding: EdgeInsets.zero,
Icons.mic_none,
color: Colors.grey,
),
onPressed: controller.startVoiceRecognition,
),
SizedBox(width: 8.w),
// 文本输入框
Expanded(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: TextField(
controller: controller.messageController,
controller: textController,
decoration: InputDecoration(
hintText: '输入消息...',
hintStyle: TextStyle(
color: Colors.grey[400],
fontSize: 14.sp,
hintText: '发消息...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.r),
borderSide: BorderSide.none,
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 12.h),
filled: true,
fillColor: Colors.grey[200],
contentPadding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 8.h,
),
maxLines: 5,
minLines: 1,
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
),
maxLines: null,
textInputAction: TextInputAction.send,
onSubmitted: (_) => controller.sendMessage(),
),
onSubmitted: (value) {
if (value.trim().isNotEmpty) {
controller.sendMessage(value);
textController.clear();
}
},
),
),
SizedBox(width: 8.w),
Container(
width: 40.w,
height: 40.w,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFF4A6FE5),
),
child: IconButton(
// 发送按钮
Obx(() => IconButton(
icon: Icon(
Icons.send,
color: Colors.white,
size: 20.sp,
),
onPressed: controller.sendMessage,
padding: EdgeInsets.zero,
),
),
color: controller.inputText.value.trim().isEmpty
? Colors.grey
: AppColors.primary,
),
onPressed: controller.inputText.value.trim().isEmpty
? null
: () {
controller.sendMessage(controller.inputText.value);
textController.clear();
},
)),
],
),
);
}
Widget _buildRecordingInput() {
// 构建语音输入面板
Widget _buildVoiceInputPanel() {
final screenWidth = Get.width;
return Container(
height: 48.h,
decoration: BoxDecoration(
color: const Color(0xFF4A6FE5),
borderRadius: BorderRadius.circular(24.r),
width: screenWidth,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFFFFFFF), // 纯白色开始
Color(0xFFE8D5F5), // 更深的紫色结束
],
stops: [0.3, 1.0], // 调整颜色分布,让白色占据更多空间
),
),
child: SafeArea(
child: SizedBox(
height: 100.h,
child: Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(
40,
(index) => Container(
width: 4.w,
height: 4.h,
margin: EdgeInsets.symmetric(horizontal: 1.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2.r),
),
),
),
),
),
);
}
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 左侧麦克风按钮
_buildMicrophoneButton(),
void _showMoreOptions() {
Get.bottomSheet(
Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.r),
topRight: Radius.circular(20.r),
),
),
// 中间音频波形和状态文本
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
ListTile(
leading: const Icon(Icons.delete_outline, color: Colors.red),
title: const Text('清空对话'),
onTap: () {
Get.back();
controller.clearChat();
},
// 音频波形
_buildAudioWaveform(),
SizedBox(height: 10.h),
// 状态文本
Obx(() => Text(
controller.recognizedText.isEmpty ? '你可以开始说话' : '正在聆听...',
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey.shade700,
fontWeight: FontWeight.w500,
),
)),
],
),
),
);
}
Widget _buildMessage(Message message) {
final isUser = message.role == 'user';
final isSystem = message.role == 'system';
final isRecognizing = isUser && message.content.startsWith('🎤 ');
if (isSystem) {
// 系统消息使用特殊样式
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: Colors.grey[300]!,
width: 0.5,
),
),
child: Row(
children: [
Icon(
Icons.info_outline,
color: Colors.grey[600],
size: 20.sp,
),
SizedBox(width: 8.w),
Expanded(
child: Text(
message.content,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[700],
height: 1.5,
// 右侧关闭按钮
_buildCloseButton(),
],
),
),
),
],
),
),
);
}
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Row(
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) ...[
CircleAvatar(
radius: 20.r,
backgroundImage: AssetImage(controller.agentAvatar),
backgroundColor: Colors.transparent,
),
SizedBox(width: 12.w),
],
Flexible(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
// 麦克风按钮
Widget _buildMicrophoneButton() {
return Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: isUser
? (isRecognizing ? Colors.purple.withOpacity(0.7) : const Color(0xFF4A6FE5))
: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(isUser ? 18.r : 4.r),
topRight: Radius.circular(isUser ? 4.r : 18.r),
bottomLeft: Radius.circular(18.r),
bottomRight: Radius.circular(18.r),
),
// 为正在识别的消息添加脉动效果
boxShadow: isRecognizing
? [
BoxShadow(
color: Colors.purple.withOpacity(0.3),
blurRadius: 8,
spreadRadius: 2,
),
]
: [],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isRecognizing) ...[
Icon(
Icons.mic,
color: Colors.white,
size: 16.sp,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 1,
),
SizedBox(width: 8.w),
],
Flexible(
child: Text(
// 如果是正在识别的消息,移除麦克风图标前缀
isRecognizing ? message.content.substring(2) : message.content,
style: TextStyle(
fontSize: 16.sp,
color: isUser ? Colors.white : Colors.black87,
height: 1.5,
),
),
child: Center(
child: Obx(() => IconButton(
icon: Icon(
controller.isListening.value ? Icons.mic : Icons.mic_none,
color: controller.isListening.value ? AppColors.primary : Colors.grey.shade400,
size: 28.sp,
),
if (isRecognizing) ...[
SizedBox(width: 8.w),
_buildPulsatingDot(),
],
],
onPressed: controller.stopVoiceRecognition,
padding: EdgeInsets.zero,
)),
),
);
}
// 关闭按钮
Widget _buildCloseButton() {
return Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 1,
),
],
),
if (isUser) ...[
SizedBox(width: 12.w),
CircleAvatar(
radius: 20.r,
backgroundColor: const Color(0xFFEEF4FF),
child: Icon(
Icons.person,
color: const Color(0xFF4A6FE5),
size: 24.sp,
child: Center(
child: IconButton(
icon: Icon(
Icons.close,
color: Colors.grey.shade600,
size: 28.sp,
),
onPressed: controller.stopVoiceRecognition,
padding: EdgeInsets.zero,
),
],
],
),
);
}
// 创建脉动点动画
Widget _buildPulsatingDot() {
return Obx(() {
// 使用controller.isRecognizing来控制动画是否显示
if (!controller.isRecognizing.value) return const SizedBox.shrink();
// 音频波形
Widget _buildAudioWaveform() {
final barCount = 23; // 条形数量
return _PulsatingDot();
});
}
}
return SizedBox(
width: 220.w,
height: 50.h,
child: Obx(() {
final isSpeaking = controller.recognizedText.isNotEmpty;
final isRec = controller.isListening.value;
// 脉动点动画组件
class _PulsatingDot extends StatefulWidget {
@override
_PulsatingDotState createState() => _PulsatingDotState();
}
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
barCount,
(index) {
// 创建动态高度效果
double height = isRec
? _getWaveHeight(index, isSpeaking, barCount).clamp(1.h, 22.5.h)
: 2.h;
class _PulsatingDotState extends State<_PulsatingDot> with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
// 动画持续时间
final duration = Duration(
milliseconds: isSpeaking ? 60 + (index % 4) * 20 : 150 + (index % 3) * 50
);
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
)..repeat(reverse: true);
_animation = Tween<double>(begin: 0.5, end: 1.0).animate(_animationController);
}
// 颜色计算
Color barColor;
if (isRec) {
if (isSpeaking) {
final middleIndex = barCount ~/ 2;
final distance = (index - middleIndex).abs();
final maxDistance = middleIndex;
final colorIntensity = 1.0 - (distance / maxDistance) * 0.2;
@override
void dispose() {
_animationController.dispose();
super.dispose();
barColor = index % 3 == 0
? AppColors.primary
: AppColors.primary.withOpacity(colorIntensity);
} else {
barColor = AppColors.primary.withOpacity(0.3);
}
} else {
barColor = Colors.grey.shade400;
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: 16.w,
height: 16.w,
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
children: [
Opacity(
opacity: 1.0 - _animation.value,
child: Container(
width: 16.w * _animation.value,
height: 16.w * _animation.value,
width: 5.5.w,
child: Center(
child: AnimatedContainer(
duration: duration,
curve: isSpeaking ? Curves.elasticOut : Curves.easeInOut,
width: isSpeaking ? 2.8.w : 2.5.w,
height: height,
margin: EdgeInsets.symmetric(horizontal: 1.2.w),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
shape: BoxShape.circle,
),
color: barColor,
borderRadius: BorderRadius.circular(2.r),
boxShadow: isSpeaking ? [
BoxShadow(
color: AppColors.primary.withOpacity(0.4),
blurRadius: 5,
spreadRadius: 1.2,
)
] : null,
),
),
Container(
width: 6.w,
height: 6.w,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
],
);
},
),
);
}),
);
}
// 生成动态波形高度
double _getWaveHeight(int index, bool isSpeaking, int barCount) {
try {
// 基础高度
double baseHeight = isSpeaking ? 5.h : 3.h;
// 获取当前时间毫秒数
final DateTime now = DateTime.now();
final int milliseconds = now.millisecondsSinceEpoch;
// 计算中心位置
final middleIndex = barCount ~/ 2;
final distanceFromMiddle = (index - middleIndex).abs();
// 使用多个正弦波叠加
double freqMultiplier = isSpeaking ? 2.2 : 1.0;
double ampMultiplier = isSpeaking ? 1.8 : 1.0;
double wave1 = sin((milliseconds / 150) * freqMultiplier + (index * 0.8)) * 2.h * ampMultiplier;
double wave2 = sin((milliseconds / 280) * freqMultiplier + (index * 0.5)) * 1.5.h * ampMultiplier;
double wave3 = sin((milliseconds / 340) * freqMultiplier + (index * 0.3)) * 1.25.h * ampMultiplier;
double wave4 = cos((milliseconds / 220) * freqMultiplier + (index * 0.7)) * 1.h * ampMultiplier;
// 基于距离中心的位置计算高度衰减
double positionFactor = isSpeaking
? 1.0 - (distanceFromMiddle / middleIndex) * 0.2
: 1.0 - (distanceFromMiddle / middleIndex) * 0.5;
if (isSpeaking) {
// 用户说话时的波形
double timeFactor = 0.9 + sin(milliseconds / 600) * 0.5;
// 增强中心突出效果
double centerBoost = pow(positionFactor, 1.2) * 10.h;
// 增加随机性
double randomFactor = (Random().nextDouble() * 4 - 2) * 1.75.h;
// 增强脉冲效果
double pulseEffect = sin(milliseconds / 300) * 2.5.h * positionFactor;
// 添加额外的随机波动
double voiceVariation = sin(milliseconds / 80 + index * 1.2) * 1.5.h * Random().nextDouble();
// 添加爆发效果
double burstEffect = 0;
if (Random().nextDouble() > 0.97) {
burstEffect = 5.h * positionFactor;
}
return baseHeight + (wave1 + wave2 + wave3 + wave4) * timeFactor +
centerBoost + randomFactor + pulseEffect + voiceVariation + burstEffect;
} else {
// 用户不说话时的波形
double idleWave = (wave1 + wave2) * 0.35 * positionFactor;
double gentlePulse = sin(milliseconds / 800) * 1.h * positionFactor;
return baseHeight + idleWave + gentlePulse;
}
} catch (e) {
// 发生错误时返回默认高度
return 2.h;
}
}
}

302
lib/modules/chat/widgets/voice_input_panel.dart

@ -1,302 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:math';
import 'dart:async';
import '../controllers/voice_input_controller.dart';
class VoiceInputPanel extends GetView<VoiceInputController> {
const VoiceInputPanel({
Key? key,
required this.onRecordingResult,
required this.onClose,
required this.onRecognizing,
}) : super(key: key);
final Function(String) onRecordingResult;
final VoidCallback onClose;
final Function(String) onRecognizing;
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFFFFFFF), // 纯白色开始
Color(0xFFE8D5F5), // 更深的紫色结束
],
stops: [0.3, 1.0], // 调整颜色分布,让白色占据更多空间
),
),
child: SafeArea(
child: Container(
height: 120.h,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 主要内容:麦克风图标、音频波形、关闭按钮
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 左侧麦克风图标
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: Center(
child: Obx(() => Icon(
controller.isRecording.value ? Icons.mic : Icons.mic_none,
color: controller.isRecording.value ? Colors.purple : Colors.purple.shade300,
size: 28.sp,
)),
),
),
// 中间音频波形和状态文本
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildAudioWaveform(),
SizedBox(height: 10.h),
// 文字提示 - 根据状态显示不同文本
Obx(() => Text(
_getStatusText(),
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey.shade700,
fontWeight: FontWeight.w500,
),
)),
],
),
// 右侧关闭按钮
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 1,
),
],
),
child: Center(
child: IconButton(
icon: Icon(
Icons.close,
color: Colors.grey.shade600,
size: 28.sp,
),
onPressed: onClose,
padding: EdgeInsets.zero,
),
),
),
],
),
),
// 底部进度条和识别文本
Padding(
padding: EdgeInsets.only(top: 15.h),
child: Column(
children: [
// 移除显示当前识别的文本,因为现在直接显示在聊天窗口中
// 进度条
Container(
width: 250.w,
height: 4.h,
decoration: BoxDecoration(
color: Colors.purple.withOpacity(0.2),
borderRadius: BorderRadius.circular(2.r),
),
child: Obx(() => controller.isRecording.value
? _buildProgressIndicator()
: Container()
),
),
],
),
),
],
),
),
),
),
);
}
// 根据状态返回不同的提示文本
String _getStatusText() {
if (controller.isConnecting.value) {
return '正在连接语音服务...';
} else if (controller.isMuted.value) {
return '语音已静音';
} else if (controller.isRecording.value) {
return '正在聆听...';
} else {
return '你可以开始说话';
}
}
// 构建进度指示器
Widget _buildProgressIndicator() {
return Container(
width: 250.w,
height: 4.h,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.purple.shade300, Colors.purple.shade100],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(2.r),
),
);
}
// 检查用户是否正在说话
bool _isUserSpeaking() {
// 使用控制器的isUserSpeaking属性
return controller.isUserSpeaking.value;
}
Widget _buildAudioWaveform() {
return Obx(() {
final isSpeaking = _isUserSpeaking();
final barCount = 15; // 增加条形数量,使波形更丰富
// 根据控制器状态动态生成波形高度
return Container(
width: 180.w, // 增加宽度以容纳更多条形
height: 40.h, // 增加高度以允许更大的波动
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
barCount,
(index) {
// 创建动态高度效果,模拟声波
double height = controller.isRecording.value
? _getWaveHeight(index, isSpeaking, barCount) // 当正在录音时显示动态高度
: 4.h; // 默认高度
// 根据位置计算不同的动画持续时间,使波形更自然
final duration = Duration(
milliseconds: 100 + (index % 3) * 50
);
// 根据是否说话和位置计算颜色
Color barColor;
if (controller.isRecording.value) {
if (isSpeaking) {
// 创建渐变色效果,从中间向两边渐变
final middleIndex = barCount ~/ 2;
final distance = (index - middleIndex).abs();
final maxDistance = middleIndex;
final colorIntensity = 1.0 - (distance / maxDistance) * 0.5;
barColor = Colors.purple.withOpacity(colorIntensity);
} else {
barColor = Colors.purple.shade200;
}
} else {
barColor = Colors.grey.shade400;
}
return AnimatedContainer(
duration: duration,
curve: Curves.easeInOut, // 使用更平滑的动画曲线
width: 3.w, // 减小宽度,使波形更精细
height: height,
margin: EdgeInsets.symmetric(horizontal: 1.5.w), // 减小间距
decoration: BoxDecoration(
color: barColor,
borderRadius: BorderRadius.circular(2.r),
// 添加轻微的阴影效果,增强视觉效果
boxShadow: isSpeaking ? [
BoxShadow(
color: Colors.purple.withOpacity(0.2),
blurRadius: 3,
spreadRadius: 0.5,
)
] : null,
),
);
},
),
),
);
});
}
// 生成动态波形高度
double _getWaveHeight(int index, bool isSpeaking, int barCount) {
// 基础高度
double baseHeight = 6.h;
// 获取当前时间毫秒数
final DateTime now = DateTime.now();
final int milliseconds = now.millisecondsSinceEpoch;
// 计算中心位置
final middleIndex = barCount ~/ 2;
final distanceFromMiddle = (index - middleIndex).abs();
// 使用多个正弦波叠加,创建更自然的波形
// 每个条形使用不同的频率和相位,创建不规则波形
double wave1 = sin((milliseconds / 200) + (index * 0.7)) * 3.h;
double wave2 = sin((milliseconds / 350) + (index * 0.5)) * 2.h;
double wave3 = sin((milliseconds / 500) + (index * 0.3)) * 1.5.h;
// 基于距离中心的位置计算高度衰减
double positionFactor = 1.0 - (distanceFromMiddle / middleIndex) * 0.5;
if (isSpeaking) {
// 用户说话时的波形
// 创建一个随时间变化的振幅因子,使波形看起来更有活力
double timeFactor = 0.7 + sin(milliseconds / 1000) * 0.3;
// 中心位置的条形高度最高
double centerBoost = pow(positionFactor, 1.5) * 10.h;
// 添加随机因素,使波形更自然
double randomFactor = (Random().nextDouble() * 2 - 1) * 2.h;
// 根据时间添加脉动效果
double pulseEffect = sin(milliseconds / 600) * 3.h * positionFactor;
// 组合所有效果
return baseHeight + (wave1 + wave2 + wave3) * timeFactor + centerBoost + randomFactor + pulseEffect;
} else {
// 用户不说话时的波形 - 更平静,但仍有微小变化
double idleWave = (wave1 + wave2) * 0.5 * positionFactor;
return baseHeight + idleWave;
}
}
}

349
lib/modules/translation/controllers/translation_controller.dart

@ -11,7 +11,8 @@ import '../../../data/services/volcano_tts_api_service.dart';
class TranslationController extends GetxController {
// Services
final AzureAsrService _asrService = Get.find<AzureAsrService>();
final VolcanoTranslationService _translationService = Get.find<VolcanoTranslationService>();
final VolcanoTranslationService _translationService =
Get.find<VolcanoTranslationService>();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
final GetStorage _storage = GetStorage();
@ -23,8 +24,6 @@ class TranslationController extends GetxController {
final isRecognizing = false.obs;
final isTranslating = false.obs;
final isTtsEnabled = true.obs;
final recognizedText = ''.obs;
final translatedText = ''.obs;
final sourceLanguage = '中文 (Chinese)'.obs;
final targetLanguage = '英语 (English)'.obs;
final translationHistory = <TranslationItem>[].obs;
@ -63,7 +62,7 @@ class TranslationController extends GetxController {
};
// Current translation item
TranslationItem? currentItem;
final Rx<TranslationItem?> currentItem = Rx<TranslationItem?>(null);
// Subscription for recognition stream
StreamSubscription? _recognitionSubscription;
@ -135,7 +134,7 @@ class TranslationController extends GetxController {
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
// Initialize a new translation item
currentItem = TranslationItem(
currentItem.value = TranslationItem(
sourceText: '',
translatedText: '',
sourceLanguage: sourceLanguage.value,
@ -151,12 +150,15 @@ class TranslationController extends GetxController {
if (success) {
isRecognizing.value = true;
// Reset the recognized text
recognizedText.value = '';
translatedText.value = '';
// Reset the current item
if (currentItem.value != null) {
currentItem.value!.sourceText = '';
currentItem.value!.translatedText = '';
}
// Listen for recognition events
_recognitionSubscription = _asrService.recognitionStream?.listen((event) {
_recognitionSubscription =
_asrService.recognitionStream?.listen((event) {
switch (event.type) {
case RecognitionEventType.finalResult:
if (event.text.isNotEmpty) {
@ -203,21 +205,14 @@ class TranslationController extends GetxController {
_recognitionSubscription = null;
isRecognizing.value = false;
// 添加当前项到历史(如果有内容)
finalizeCurrentItem();
// 重置当前会话ID
currentSessionId = null;
// 重置当前显示的文本
recognizedText.value = '';
translatedText.value = '';
// 重置当前项
currentItem.value = null;
} catch (e) {
Get.snackbar(
'Error',
'Failed to stop voice recognition: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
Get.snackbar('Error', '停止语音识别失败: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM);
}
}
@ -226,13 +221,10 @@ class TranslationController extends GetxController {
if (text.isEmpty) return;
// 如果当前没有活跃的翻译项,创建一个新的
if (currentItem == null) {
// 如果没有当前会话ID,创建一个新的
if (currentSessionId == null) {
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
}
if (currentItem.value == null) {
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
currentItem = TranslationItem(
currentItem.value = TranslationItem(
sourceText: text,
translatedText: '',
sourceLanguage: sourceLanguage.value,
@ -242,195 +234,198 @@ class TranslationController extends GetxController {
isFirstInSession: translationHistory.isEmpty ||
translationHistory.last.sessionId != currentSessionId,
);
currentItem.refresh();
} else {
// 更新当前翻译项
currentItem!.sourceText = text;
currentItem.value!.sourceText = text;
// 通知 GetX 更新
currentItem.refresh();
}
// 实时更新识别文本
recognizedText.value = text;
// Debounce translation requests to avoid too many API calls
// 防抖处理翻译请求
_translationDebounceTimer?.cancel();
_translationDebounceTimer = Timer(const Duration(milliseconds: 300), () {
translateText(text, isFinal: false);
if (currentItem.value != null &&
currentItem.value!.sourceText.isNotEmpty) {
translateText(currentItem.value!.sourceText);
}
});
// Scroll to the bottom to show the latest content
// 滚动到底部显示最新内容
_scrollToBottom();
}
// Handle final recognition result
void handleFinalResult(String text) {
if (text.isEmpty) return;
void handleFinalResult(String text) async {
// 如果没有文本或当前项为空,直接返回
if (text.isEmpty || currentItem.value == null) return;
// 更新当前翻译项(如果不存在则创建一个)
if (currentItem == null) {
// 如果没有当前会话ID,创建一个新的
if (currentSessionId == null) {
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
}
currentItem = TranslationItem(
sourceText: text,
translatedText: '',
sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value,
timestamp: DateTime.now(),
sessionId: currentSessionId!,
isFirstInSession: translationHistory.isEmpty ||
translationHistory.last.sessionId != currentSessionId,
);
} else {
currentItem!.sourceText = text;
}
// 更新当前项的源文本
currentItem.value!.sourceText = text;
currentItem.refresh();
// 更新识别文本
recognizedText.value = text;
// 添加到历史记录
translationHistory.add(currentItem.value!);
saveTranslationHistory();
// 翻译最终结果 (添加到历史的逻辑已移至translateText方法)
translateText(text, isFinal: true).then((_) {
// 作为安全措施,确保当前项被添加到历史中
// 这是为了处理translateText中可能因为条件不满足而没有添加到历史的情况
finalizeCurrentItem();
});
// 重置当前项
currentItem.value = null;
// Scroll to the bottom to show the latest content
// 对最终结果进行一次翻译,确保翻译结果是最新的
final translationResult = await translateText(text);
if (translationResult != null) {
if (isTtsEnabled.value) {
await playTranslatedText(translationResult);
}
}
// 滚动到底部显示最新内容
_scrollToBottom();
}
// Finalize the current translation item and add it to history
void finalizeCurrentItem() {
if (currentItem != null &&
currentItem!.sourceText.isNotEmpty &&
currentItem!.translatedText.isNotEmpty &&
!translationHistory.contains(currentItem)) {
translationHistory.add(currentItem!);
saveTranslationHistory();
currentItem = null;
}
// 检测文本语言
String detectLanguage(String text) {
if (text.isEmpty) return sourceLanguage.value;
// 检测各种语言特征
final bool containsChinese = RegExp(r'[\u4e00-\u9fa5]').hasMatch(text);
final bool containsLatin = RegExp(r'[a-zA-Z]').hasMatch(text);
final bool containsJapanese =
RegExp(r'[\u3040-\u309F\u30A0-\u30FF]').hasMatch(text);
final bool containsKorean =
RegExp(r'[\uAC00-\uD7AF\u1100-\u11FF]').hasMatch(text);
final bool containsCyrillic = RegExp(r'[\u0400-\u04FF]').hasMatch(text);
final bool containsArabic = RegExp(r'[\u0600-\u06FF]').hasMatch(text);
final bool containsThai = RegExp(r'[\u0E00-\u0E7F]').hasMatch(text);
// 获取当前设置的源语言代码
final String sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh';
// 根据文本特征判断语言
if (containsChinese && sourceCode != 'zh') {
return '中文 (Chinese)';
} else if (containsJapanese && sourceCode != 'ja') {
return '日语 (Japanese)';
} else if (containsKorean && sourceCode != 'ko') {
return '韩语 (Korean)';
} else if (containsCyrillic && sourceCode != 'ru') {
return '俄语 (Russian)';
} else if (containsArabic && sourceCode != 'ar') {
return '阿拉伯语 (Arabic)';
} else if (containsThai && sourceCode != 'th') {
return '泰语 (Thai)';
} else if (containsLatin && sourceCode == 'zh') {
return '英语 (English)';
}
// Handle voice recognition result (legacy method, kept for compatibility)
void handleVoiceResult(String text) {
handleFinalResult(text);
// 默认返回当前设置的源语言
return sourceLanguage.value;
}
// Translate the recognized text
Future<void> translateText(String text, {bool isFinal = true}) async {
if (text.isEmpty) return;
Future<String?> translateText(String sourceText) async {
if (sourceText.isEmpty) return null;
isTranslating.value = true;
// 保存当前的翻译项引用,确保异步返回时更新正确的项
final TranslationItem? itemToUpdate = currentItem;
String? translationResult;
try {
// 获取源语言和目标语言的代码
final String sourceCode = supportedLanguages[sourceLanguage.value] ?? 'zh';
// 尝试检测输入文本的语言(这里简化处理,实际项目可能需要更复杂的语言检测)
bool shouldSwapLanguages = false;
// 简单的语言检测逻辑 - 检查是否包含中文字符
final bool containsChinese = RegExp(r'[\u4e00-\u9fa5]').hasMatch(text);
final bool containsLatin = RegExp(r'[a-zA-Z]').hasMatch(text);
// 如果源语言是中文但文本不包含中文字符,且包含拉丁字符,可能需要交换语言
if (sourceCode == 'zh' && !containsChinese && containsLatin) {
shouldSwapLanguages = true;
}
// 如果源语言不是中文但文本包含中文字符,可能需要交换语言
else if (sourceCode != 'zh' && containsChinese) {
shouldSwapLanguages = true;
}
// 检测源文本语言
final detectedLanguage = detectLanguage(sourceText);
final bool shouldSwapLanguages = detectedLanguage != sourceLanguage.value;
// 调用翻译服务
final result = await _translationService.translateText(
text: text,
sourceLanguage: shouldSwapLanguages ? targetLanguage.value : sourceLanguage.value,
targetLanguage: shouldSwapLanguages ? sourceLanguage.value : targetLanguage.value,
text: sourceText,
sourceLanguage:
shouldSwapLanguages ? detectedLanguage : sourceLanguage.value,
targetLanguage:
shouldSwapLanguages ? sourceLanguage.value : targetLanguage.value,
);
if (result != null) {
// 实时更新翻译文本
translatedText.value = result;
// 确保更新的是发起翻译请求时的翻译项,而不是当前可能已经变化的翻译项
if (itemToUpdate != null && itemToUpdate.sourceText == text) {
itemToUpdate.translatedText = result;
// 如果是最终结果,并且当前项仍然是发起请求时的项,则添加到历史
if (isFinal && itemToUpdate == currentItem) {
// 检查是否已经存在相同会话ID的项
bool isFirstInSession = true;
for (var item in translationHistory) {
if (item.sessionId == itemToUpdate.sessionId) {
isFirstInSession = false;
break;
}
}
translationResult = result;
// 更新翻译结果
if (currentItem.value != null &&
currentItem.value!.sourceText.contains(sourceText)) {
// 直接更新并刷新
currentItem.value!.translatedText = result;
currentItem.refresh();
} else {
// 在历史记录中查找匹配的项
final index = translationHistory.indexWhere(
(historyItem) => historyItem.sourceText == sourceText);
// 创建一个新的TranslationItem,包含正确的isFirstInSession值
final updatedItem = TranslationItem(
sourceText: itemToUpdate.sourceText,
translatedText: itemToUpdate.translatedText,
sourceLanguage: itemToUpdate.sourceLanguage,
targetLanguage: itemToUpdate.targetLanguage,
timestamp: itemToUpdate.timestamp,
sessionId: itemToUpdate.sessionId,
isFirstInSession: isFirstInSession,
);
if (index != -1) {
// 直接更新历史记录项
translationHistory[index].translatedText = result;
// 通知 GetX 更新
translationHistory.refresh();
translationHistory.add(updatedItem);
saveTranslationHistory();
currentItem = null; // 重要:将当前项设为null,表示已完成
// 重置当前显示的文本,避免与历史项重复
recognizedText.value = '';
translatedText.value = '';
}
// Play TTS if enabled and this is a final result
if (isTtsEnabled.value && isFinal) {
playTranslatedText(result);
}
// 滚动到底部显示最新内容
_scrollToBottom();
}
}
return translationResult;
} catch (e) {
Get.snackbar(
'Error',
'Translation failed: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
);
Get.snackbar('Error', '翻译失败: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM);
return null;
} 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,
);
Get.snackbar('Error', '播放翻译失败: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM);
}
}
// 停止所有操作(语音识别和TTS)
Future<void> stopAll() async {
try {
// 停止语音识别
if (isRecognizing.value) {
await stopRecognition();
}
// 停止TTS播放
await _ttsService.stop();
} catch (e) {
Get.snackbar('Error', '停止操作失败: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM);
}
}
// Play a specific translation item
// 播放特定翻译项
Future<void> playTranslationItem(TranslationItem item) async {
if (item.translatedText.isEmpty) return;
await playTranslatedText(item.translatedText);
}
// Scroll to the bottom of the list
// 播放文本
Future<void> playText(String text) async {
if (text.isEmpty) return;
await playTranslatedText(text);
}
// 滚动到列表底部
void _scrollToBottom() {
if (scrollController.hasClients) {
if (!scrollController.hasClients) {
Future.delayed(const Duration(milliseconds: 100), _scrollToBottom);
return;
}
try {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
@ -439,16 +434,12 @@ class TranslationController extends GetxController {
);
} catch (e) {
// 如果滚动失败,尝试延迟后再次滚动
Future.delayed(const Duration(milliseconds: 200), () {
Future.delayed(const Duration(milliseconds: 100), () {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
}
} else {
// 如果scrollController还没有客户端,延迟后再尝试
Future.delayed(const Duration(milliseconds: 200), _scrollToBottom);
}
}
// 切换源语言和目标语言
@ -488,13 +479,14 @@ class TranslationController extends GetxController {
void loadTranslationHistory() {
final historyJson = _storage.read(_historyKey);
if (historyJson != null) {
try {
final List<dynamic> historyList = jsonDecode(historyJson);
translationHistory.value = historyList.map((item) => TranslationItem.fromJson(item)).toList();
// 确保在历史记录加载后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
translationHistory.value =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
} catch (e) {
// 如果解析失败,清除历史记录
_storage.remove(_historyKey);
}
}
}
@ -502,7 +494,8 @@ class TranslationController extends GetxController {
void saveTranslationHistory() {
// 限制历史记录数量,保留最新的记录
if (translationHistory.length > _maxHistoryItems) {
translationHistory.removeRange(0, translationHistory.length - _maxHistoryItems);
translationHistory.removeRange(
0, translationHistory.length - _maxHistoryItems);
}
final List<Map<String, dynamic>> historyJson =
@ -524,7 +517,7 @@ class TranslationItem {
final String sourceLanguage;
final String targetLanguage;
final DateTime timestamp;
final String sessionId;
String sessionId;
final bool isFirstInSession;
TranslationItem({
@ -538,13 +531,21 @@ class TranslationItem {
});
factory TranslationItem.fromJson(Map<String, dynamic> json) {
DateTime timestamp;
try {
timestamp = DateTime.parse(json['timestamp']);
} catch (e) {
timestamp = DateTime.now();
}
return TranslationItem(
sourceText: json['sourceText'],
translatedText: json['translatedText'],
sourceLanguage: json['sourceLanguage'],
targetLanguage: json['targetLanguage'],
timestamp: DateTime.parse(json['timestamp']),
sessionId: json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
sourceText: json['sourceText'] ?? '',
translatedText: json['translatedText'] ?? '',
sourceLanguage: json['sourceLanguage'] ?? '中文 (Chinese)',
targetLanguage: json['targetLanguage'] ?? '英语 (English)',
timestamp: timestamp,
sessionId:
json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
isFirstInSession: json['isFirstInSession'] ?? false,
);
}

77
lib/modules/translation/controllers/translation_detail_controller.dart

@ -3,6 +3,7 @@ import '../../../data/services/volcano_ai_service.dart';
import '../controllers/translation_controller.dart';
import '../controllers/translation_history_controller.dart';
import 'package:get_storage/get_storage.dart';
import 'dart:async';
class TranslationDetailController extends GetxController {
// 依赖注入
@ -13,6 +14,9 @@ class TranslationDetailController extends GetxController {
// 存储键前缀
static const String _summaryKeyPrefix = 'translation_summary_';
// 超时设置(秒)
final int _timeoutDuration = 30;
// 状态变量
final selectedTabIndex = 0.obs;
final isGeneratingSummary = false.obs;
@ -76,6 +80,64 @@ class TranslationDetailController extends GetxController {
return sessionItems.map((e) => e.translatedText).join('\n\n');
}
// 使用延长的超时时间重试提取摘要
Future<String> retryExtractSummaryWithExtendedTimeout(String text) async {
// 延长超时时间(60秒)
final int extendedTimeout = 60;
isGeneratingSummary.value = true;
try {
// 构建提示词
final messages = [
{'role': 'user', 'content': '请为以下文本生成一个简洁的摘要,突出重点内容:\n\n$text'},
];
// 系统提示词
const systemPrompt = '''
你是一个专业的文本摘要助手,擅长从会议记录、对话和文本中提取关键信息并生成简洁的摘要。
请按照以下格式生成摘要:
1. 首先用1-2句话对整体内容进行简要概括
2. 然后使用项目符号列表(•)列出关键点或重要信息
3. 如果是对话内容,请特别标注出讨论的结论或达成的共识
请遵循以下原则:
1. 保留原文的核心信息和关键点
2. 使用简洁、清晰的语言
3. 保持客观,不添加个人观点
4. 摘要总长度应该是原文的1/3或更短
5. 确保列表中的每个要点都简明扼要,突出重点
''';
String result;
try {
// 使用延长的超时限制
result = await _aiService.sendMessage(
messages: messages,
systemPrompt: systemPrompt,
).timeout(Duration(seconds: extendedTimeout), onTimeout: () {
throw TimeoutException('请求超时,请稍后重试');
});
} on TimeoutException catch (_) {
summaryResult.value = '生成摘要超时,请检查网络连接后重试';
return summaryResult.value;
}
summaryResult.value = result;
// 保存摘要
saveSummary(result);
return result;
} catch (e) {
summaryResult.value = '生成摘要失败: ${e.toString()}';
return summaryResult.value;
} finally {
isGeneratingSummary.value = false;
}
}
// 提取摘要
Future<String> extractSummary(String text) async {
isGeneratingSummary.value = true;
@ -103,11 +165,20 @@ class TranslationDetailController extends GetxController {
5. 确保列表中的每个要点都简明扼要,突出重点
''';
// 调用AI服务生成摘要
final result = await _aiService.sendMessage(
// 添加超时处理
String result;
try {
// 使用Future.timeout添加超时限制
result = await _aiService.sendMessage(
messages: messages,
systemPrompt: systemPrompt,
);
).timeout(Duration(seconds: _timeoutDuration), onTimeout: () {
throw TimeoutException('请求超时,请稍后重试');
});
} on TimeoutException catch (_) {
summaryResult.value = '生成摘要超时,请稍后重试';
return summaryResult.value;
}
summaryResult.value = result;

86
lib/modules/translation/views/translation_detail_view.dart

@ -211,10 +211,14 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
SizedBox(width: 12.w),
// 播放按钮
GestureDetector(
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
controller.playTranslationItem(item);
},
borderRadius: BorderRadius.circular(20.r),
splashColor: Colors.blue.withOpacity(0.2),
child: Container(
width: 28.w,
height: 28.h,
@ -229,6 +233,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
),
),
),
],
),
),
@ -316,16 +321,16 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
// 播放按钮
GestureDetector(
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
controller.playText(controller.summaryResult.value);
},
child: Container(
borderRadius: BorderRadius.circular(20.r),
splashColor: Colors.blue.withOpacity(0.2),
child: Padding(
padding: EdgeInsets.all(8.r),
decoration: BoxDecoration(
color: Colors.blue.withAlpha(10),
shape: BoxShape.circle,
),
child: Icon(
Icons.volume_up,
color: Colors.blue,
@ -333,9 +338,12 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
),
),
),
SizedBox(width: 12.w),
// 复制按钮
GestureDetector(
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
// 复制摘要到剪贴板
Clipboard.setData(ClipboardData(text: controller.summaryResult.value));
@ -346,12 +354,10 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
duration: Duration(seconds: 2),
);
},
child: Container(
borderRadius: BorderRadius.circular(20.r),
splashColor: Colors.blue.withOpacity(0.2),
child: Padding(
padding: EdgeInsets.all(8.r),
decoration: BoxDecoration(
color: Colors.blue.withAlpha(10),
shape: BoxShape.circle,
),
child: Icon(
Icons.copy,
color: Colors.blue,
@ -359,6 +365,7 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
),
),
),
],
),
),
@ -455,7 +462,58 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
// 提取摘要方法
void _extractSummary(String text) async {
await controller.extractSummary(text);
final result = await controller.extractSummary(text);
// 检查是否超时,如果超时则显示重试选项
if (result.contains('超时')) {
Get.dialog(
AlertDialog(
title: Text(
'请求超时',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
content: Text(
'生成摘要请求超时,可能是网络问题或服务器繁忙。',
style: TextStyle(
fontSize: 16.sp,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'取消',
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey[700],
),
),
),
TextButton(
onPressed: () {
Get.back();
// 使用延长的超时时间重试
controller.retryExtractSummaryWithExtendedTimeout(text);
},
child: Text(
'重试',
style: TextStyle(
fontSize: 16.sp,
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
),
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
);
}
}
// 分享内容方法

21
lib/modules/translation/views/translation_history_view.dart

@ -126,15 +126,20 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
// 构建组内的历史记录项
Widget _buildHistoryItemInGroup(TranslationItem item) {
return GestureDetector(
return InkWell(
onTap: () {
// 点击查看详情
_showTranslationDetail(item);
},
child: Stack(
children: [
Padding(
splashColor: Colors.blue.withOpacity(0.1),
highlightColor: Colors.blue.withOpacity(0.05),
borderRadius: BorderRadius.circular(8.r),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -163,18 +168,14 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
],
),
),
Positioned(
right: 12.w,
top: 0,
bottom: 0,
child: Icon(
Icon(
Icons.chevron_right,
color: Colors.grey[350],
size: 20.sp,
),
),
],
),
),
);
}

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

@ -28,8 +28,8 @@ class TranslationView extends GetView<TranslationController> {
size: 20.sp,
),
onPressed: () {
// Stop recognition before navigating back
controller.stopRecognition();
// Stop recognition and TTS before navigating back
controller.stopAll();
Get.back();
},
),
@ -116,14 +116,12 @@ class TranslationView extends GetView<TranslationController> {
size: 24.sp,
),
onPressed: () async {
// 停止语音识别,释放资源
controller.stopRecognition();
// 停止语音识别和TTS,释放资源
await controller.stopAll();
await Get.to(
() => const TranslationHistoryView(),
binding: TranslationHistoryBinding(),
);
// 返回时重新启动语音识别
controller.startRecognition();
},
tooltip: '查看历史记录',
),
@ -146,21 +144,20 @@ class TranslationView extends GetView<TranslationController> {
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;
// 使用 currentItem 来判断是否显示当前项
final hasCurrentItem = controller.currentItem.value != null &&
(controller.currentItem.value!.sourceText.isNotEmpty || controller.currentItem.value!.translatedText.isNotEmpty);
return ListView.builder(
controller: controller.scrollController,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
itemCount: itemCount + (showCurrentText ? 1 : 0),
itemCount: itemCount + (hasCurrentItem ? 1 : 0),
itemBuilder: (context, index) {
// Current item (being translated)
if (showCurrentText && index == itemCount) {
if (hasCurrentItem && index == itemCount) {
return _buildTranslationItemWidget(
controller.recognizedText.value,
controller.translatedText.value,
controller.currentItem.value!.sourceText,
controller.currentItem.value!.translatedText,
true,
);
}
@ -256,13 +253,18 @@ class TranslationView extends GetView<TranslationController> {
),
],
),
child: Center(
child: Obx(() {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 主要控制按钮 - 开始/暂停语音识别
Obx(() {
final isRecognizing = controller.isRecognizing.value;
return GestureDetector(
onTap: () {
if (isRecognizing) {
controller.stopRecognition();
controller.stopAll();
} else {
controller.startRecognition();
}
@ -282,6 +284,7 @@ class TranslationView extends GetView<TranslationController> {
),
);
}),
],
),
);
}

4
linux/flutter/generated_plugin_registrant.cc

@ -6,11 +6,15 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <record_linux/record_linux_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_size/window_size_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) record_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
record_linux_plugin_register_with_registrar(record_linux_registrar);

1
linux/flutter/generated_plugins.cmake

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
record_linux
url_launcher_linux
window_size

1
pubspec.yaml

@ -36,6 +36,7 @@ dependencies:
uuid: ^4.3.3
record: ^5.2.1
share_plus: ^10.0.0
image_picker: ^1.1.2
dev_dependencies:
flutter_test:

3
windows/flutter/generated_plugin_registrant.cc

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
@ -13,6 +14,8 @@
#include <window_size/window_size_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
RecordWindowsPluginCApiRegisterWithRegistrar(

1
windows/flutter/generated_plugins.cmake

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
permission_handler_windows
record_windows
share_plus

Loading…
Cancel
Save