wolfplus 2 years ago
parent
commit
a32acc0a07
  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. 43
      lib/data/services/azure_asr_service.dart
  7. 743
      lib/data/services/background_agent_service.dart
  8. 97
      lib/data/services/chat_history_service.dart
  9. 119
      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. 1283
      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. 969
      lib/modules/chat/views/chat_view.dart
  16. 302
      lib/modules/chat/widgets/voice_input_panel.dart
  17. 463
      lib/modules/translation/controllers/translation_controller.dart
  18. 81
      lib/modules/translation/controllers/translation_detail_controller.dart
  19. 160
      lib/modules/translation/views/translation_detail_view.dart
  20. 75
      lib/modules/translation/views/translation_history_view.dart
  21. 81
      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,
),
),
],
);
}
}

43
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,23 +210,39 @@ class AzureAsrService extends GetxService {
Future<bool> startContinuousRecognition() async {
_checkInitialized();
if (_isContinuousRecognitionActive) {
Logger.warning('连续识别已经在进行中');
// 先停止当前正在进行的识别任务
Logger.info('尝试停止当前正在进行的识别任务');
try {
await stopContinuousRecognition();
// 添加短暂延迟,确保资源被正确释放
await Future.delayed(Duration(milliseconds: 300));
} catch (e) {
Logger.error('停止当前识别任务失败', e);
// 继续尝试启动新的识别任务
}
// 无论内部状态如何,先尝试停止任何可能正在进行的识别任务
Logger.info('在开始新的语音识别前,确保停止任何可能正在进行的识别任务');
try {
await stopContinuousRecognition();
// 添加短暂延迟,确保资源被正确释放
await Future.delayed(Duration(milliseconds: 300));
} catch (e) {
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();

743
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;
// 服务实例
final AzureAsrService _asrService = Get.find<AzureAsrService>();
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final VolcanoTtsApiService _ttsService = Get.find<VolcanoTtsApiService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
// 当前使用的agent ID
String? _currentAgentId;
// 当前使用的agent voice
String _agentVoice = 'zh_female_meilinvyou_moon_bigtts';
// 可观察的状态
// 状态控制
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;
BackgroundAgentService()
: _aiService = VolcanoAIService(),
_ttsService = Get.find<VolcanoTtsApiService>(),
_voiceRecognitionService = Get.find<AzureAsrService>();
// 当前处理的Agent
Agent? _currentAgent;
// 启动或重置交互计时器
void _resetInteractionTimer() {
// 取消之前的计时器
_interactionTimer?.cancel();
// 设置30秒的超时计时器
_interactionTimer = Timer(const Duration(seconds: 30), () {
// 检查TTS是否正在播放
if (_ttsService.isPlaying) {
// 如果TTS正在播放,重置定时器
_resetInteractionTimer();
} else {
// 如果TTS不在播放,且30秒内没有检测到用户输入,退出交互
Logger.info('30秒内没有检测到用户输入,退出交互');
_endInteraction();
// 消息历史 - 与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();
}
});
}
}
// 结束交互
Future<void> _endInteraction() async {
// 完成当前的recognitionCompleter(如果有)
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.complete('');
// 如果无法从ChatController获取,则使用本地存储的历史记录
if (_localMessageHistory.isEmpty) {
_loadChatHistory();
}
_isProcessing = false;
return _localMessageHistory;
}
// 本地消息历史备份
final List<Map<String, String>> _localMessageHistory = [];
// 构造函数
BackgroundAgentService() {
Logger.info('BackgroundAgentService 已创建');
}
// TTS是否正在播放,直接从VolcanoTtsApiService获取
bool get isTtsSpeaking => _ttsService.isPlaying;
@override
void onClose() {
// 取消所有订阅
_recognitionSubscription?.cancel();
_aiResponseSubscription?.cancel();
// 播放退出提示
await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _agentVoice);
// 停止TTS
if (_ttsService.isPlaying) {
_ttsService.stop();
}
// 停止语音识别
if (_isListening) {
await stopVoiceRecognition();
Logger.info('退出交互,已停止语音识别');
if (isListening.value) {
_asrService.stopContinuousRecognition();
}
// 清除当前系统提示词
_currentSystemPrompt = null;
// 清除当前agent ID
_currentAgentId = null;
// 重置agent voice为默认值
_agentVoice = 'zh_female_meilinvyou_moon_bigtts';
// 取消定时器
_inactivityTimer?.cancel();
super.onClose();
}
// 开始语音交互
/// 开始代理交互
///
/// [agentId] 要交互的代理ID
Future<void> startAgentInteraction(String agentId) async {
// 设置当前agent ID
_currentAgentId = agentId;
// 获取agent信息,只查询一次
final agent = AgentProvider.getAgentById(agentId);
if (agent == null) {
Logger.error('无法获取agent: $agentId');
return;
}
// 存储需要的值
_agentVoice = agent.voice;
_currentSystemPrompt = agent.systemPrompt;
// 播放提示音
await _ttsService.speakSingle("我在听", speaker: _agentVoice);
// 先说"我在听",然后再播放欢迎消息
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);
}
}
// 停止语音交互
Future<void> stopAgentInteraction() async {
// 取消交互计时器
_interactionTimer?.cancel();
_interactionTimer = null;
// 停止TTS播放
if (_ttsService.isPlaying) {
await _ttsService.stop();
}
// 停止语音识别
if (_isListening) {
await stopVoiceRecognition();
Logger.info('手动停止交互,已停止语音识别');
// 启动语音识别 - 保持持续识别状态
await _startVoiceRecognition();
// 启动无活动定时器
_startInactivityTimer();
} 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;
isListening.value = true;
recognizedText.value = '';
_hasRecognizedSpeech = false;
_hasFinalResult = false;
// 添加一个变量来存储完整的用户输入
String fullUserInput = '';
// 启动语音识别
final success = await _asrService.startContinuousRecognition();
// 使用语音识别服务的recognitionStream而不是方法的返回值
_recognitionSubscription = _voiceRecognitionService.recognitionStream?.listen((event) {
if (event.type == RecognitionEventType.finalResult) {
// 添加日志输出最终识别结果
Logger.info('语音识别最终结果: ${event.text}');
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();
_processUserInput(fullUserInput);
// 重置用户输入
fullUserInput = '';
recognizedText.value = '';
_hasRecognizedSpeech = false;
_hasFinalResult = false;
}
} else if (event.type == RecognitionEventType.recognizing) {
// 添加日志输出中间识别结果
Logger.info('语音识别中间结果: ${event.text}');
if (event.text.isNotEmpty) {
// 更新当前的中间结果,但不添加到完整输入中
recognizedText.value = fullUserInput + (fullUserInput.isEmpty ? "" : ",") + event.text;
if (success) {
isListening.value = true;
recognizedText.value = '';
// 监听识别事件
_recognitionSubscription = _asrService.recognitionStream?.listen(
(event) {
// 重置无活动定时器
_resetInactivityTimer();
_hasRecognizedSpeech = true;
// 检测到用户说话,重置交互计时器
_resetInteractionTimer();
// 如果系统正在播放TTS或接收AI响应,检测到用户开始说话时立即中断
if (_ttsService.isPlaying && event.text.trim().isNotEmpty) {
// 停止当前TTS播放
_ttsService.stop();
// 设置标志,表示应该取消当前的AI响应
_shouldCancelAiResponse = true;
// 取消当前的AI响应流订阅
_aiResponseSubscription?.cancel();
_aiResponseSubscription = null;
// 添加日志输出中断TTS播放
Logger.info('检测到用户说话,中断TTS播放');
switch (event.type) {
case RecognitionEventType.recognizing:
// 实时更新识别文本
if (event.text.isNotEmpty) {
recognizedText.value = event.text;
// 如果正在播放TTS,检测到用户说话则中断当前播放和AI回复
if (_ttsService.isPlaying && event.text.trim().length > 3) {
_interruptCurrentResponse("检测到用户说话,中断当前响应");
}
}
break;
case RecognitionEventType.finalResult:
// 最终结果
if (event.text.isNotEmpty) {
recognizedText.value = event.text;
// 处理识别到的文本
_processRecognizedText(event.text);
// 重置识别文本但保持识别状态
recognizedText.value = '';
}
break;
case RecognitionEventType.sessionStopped:
case RecognitionEventType.canceled:
// 会话结束,表示识别在其它地方被启动,直接退出后台服务
isListening.value = false;
Logger.info('语音识别会话被中止,退出后台代理服务');
stopInteraction();
break;
default:
break;
}
}
} else if (event.type == RecognitionEventType.error) {
// 添加日志输出识别错误
Logger.error('语音识别错误: ${event.error}');
print('识别错误: ${event.error}');
}
}, onError: (error) {
// 添加日志输出语音识别流错误
Logger.error('语音识别流错误: $error');
print('语音识别流错误: $error');
_isListening = false;
isListening.value = false;
});
// 启动交互计时器
_resetInteractionTimer();
},
onError: (error) {
Logger.error('语音识别错误', error);
isListening.value = false;
// 如果出错,尝试重新启动语音识别
_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(),
));
// 添加用户消息到历史记录
_addMessageToHistory('user', text);
// 构建用于生成回应的消息列表
final messages = [
{'role': 'system', 'content': _currentSystemPrompt!},
{'role': 'user', 'content': userInput},
];
// 启动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);
// Logger.info('直接播放 AI 响应块: $chunk');
} catch (e) {
Logger.error('TTS 播放出错: $e');
}
},
onError: (e) {
Logger.error('AI响应流错误: $e');
print('AI响应流错误: $e');
// 清理订阅
_aiResponseSubscription = null;
// 如果TTS启用,发送到TTS
_ttsService.speak(chunk);
// 重置无活动定时器
_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(),
));
// Logger.info('保存AI响应到历史记录,长度: ${fullResponse.length}');
}
// 限制历史记录长度
if (_messageHistory.length > 20) {
_messageHistory.removeRange(0, _messageHistory.length - 20);
Logger.info('历史记录超过20条,已裁剪');
}
}
// 添加错误消息到历史记录
_addMessageToHistory('assistant', errorMessage);
// 结束TTS会话
_ttsService.endSession();
},
);
} catch (e) {
Logger.error('处理用户输入失败: $e');
print('处理用户输入失败: $e');
Logger.error('处理识别文本失败', e);
// 播放错误消息
const errorMessage = '抱歉,我遇到了一些问题,无法回应您的请求。';
_ttsService.speak(errorMessage);
// 播放错误提示,使用成员变量中存储的agent voice
await _ttsService.speak("抱歉,出现了一些问题", speaker: _agentVoice);
// 添加错误消息到历史记录
_addMessageToHistory('assistant', errorMessage);
}
}
// 停止语音识别并返回识别的文本
Future<String> stopVoiceRecognition() async {
if (!_isListening) {
return '';
/// 重新启动语音识别(仅在识别会话意外终止时使用)
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();
}
/// 添加消息到历史记录
void _addMessageToHistory(String role, String content) {
// 添加到本地历史记录
_localMessageHistory.add({
'role': role,
'content': content,
});
// 保持本地历史记录不超过10条消息
if (_localMessageHistory.length > 10) {
_localMessageHistory.removeAt(0);
}
// 如果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();
}
}
// 加载聊天历史
void _loadChatHistory() {
if (_currentAgent == null) return;
try {
// 取消订阅
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
// 停止语音识别
await _voiceRecognitionService.stopContinuousRecognition();
// 获取最终识别结果
final result = recognizedText.value;
// 加载聊天历史
final messages = _historyService.loadHistory(_currentAgent!.id);
// 添加日志输出停止语音识别的最终结果
Logger.info('停止语音识别,最终结果: $result');
// 清空本地历史,然后添加加载的历史
_localMessageHistory.clear();
// 重置状态
_isListening = false;
isListening.value = false;
return result;
// 将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');
print('停止语音识别失败: $e');
_isListening = false;
isListening.value = false;
Logger.error('加载聊天历史记录失败', e);
}
}
// 保存聊天历史
void _saveChatHistory() {
if (_currentAgent == null) return;
try {
// 将本地历史转换为Message列表
final messages = _localMessageHistory.map((m) => Message(
role: m['role']!,
content: m['content']!,
timestamp: DateTime.now(),
)).toList();
// 直接返回当前已识别的文本,不尝试重置
final result = recognizedText.value;
Logger.info('停止语音识别失败,使用当前识别结果: $result');
return result;
// 保存到ChatHistoryService
_historyService.saveHistory(_currentAgent!.id, messages);
} catch (e) {
Logger.error('保存聊天历史记录失败', e);
}
}
List<Message> get messageHistory => List.unmodifiable(_messageHistory);
void clearHistory() {
_messageHistory.clear();
/// 启动无活动定时器
void _startInactivityTimer() {
_inactivityTimer?.cancel();
_inactivityTimer = Timer(Duration(seconds: 30), () {
// 如果30秒内没有TTS输出或用户说话,停止语音识别
if (!_ttsService.isPlaying && isProcessing.value) {
Logger.info('检测到30秒内无活动,停止语音识别');
stopInteraction();
}
});
}
@override
void onClose() {
_recognitionSubscription?.cancel();
_interactionTimer?.cancel();
_interactionTimer = null;
_aiResponseSubscription?.cancel();
/// 重置无活动定时器
void _resetInactivityTimer() {
if (_inactivityTimer != null) {
_inactivityTimer!.cancel();
_startInactivityTimer();
}
}
/// 中断当前响应
void _interruptCurrentResponse(String reason) {
Logger.info(reason);
// 停止TTS播放
// 停止TTS
if (_ttsService.isPlaying) {
_ttsService.stop();
}
super.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();
_aiResponseSubscription?.cancel();
// 停止TTS
if (_ttsService.isPlaying) {
_ttsService.stop();
}
// 停止语音识别
// 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);
}
}
}

119
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
await _backgroundAgent!.startAgentInteraction(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());
}
}

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

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

File diff suppressed because it is too large

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

463
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,12 +24,10 @@ 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;
// 支持的语言列表 - 改为Map结构,键为显示名称,值为语言代码
final Map<String, String> supportedLanguages = {
'中文 (Chinese)': 'zh',
@ -61,16 +60,16 @@ class TranslationController extends GetxController {
'挪威语 (Norwegian)': 'no',
'马来语 (Malay)': 'ms',
};
// Current translation item
TranslationItem? currentItem;
final Rx<TranslationItem?> currentItem = Rx<TranslationItem?>(null);
// Subscription for recognition stream
StreamSubscription? _recognitionSubscription;
// Scroll controller for ListView
final ScrollController scrollController = ScrollController();
// Timer for debouncing translation requests
Timer? _translationDebounceTimer;
@ -80,13 +79,13 @@ class TranslationController extends GetxController {
@override
void onInit() {
super.onInit();
// 加载历史记录
loadTranslationHistory();
// Initialize the translation service
_initTranslationService();
// 添加后处理,确保在历史记录加载后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
// 延迟执行滚动操作,确保视图已完全渲染
@ -94,7 +93,7 @@ class TranslationController extends GetxController {
_scrollToBottom();
});
});
// Start voice recognition automatically when the page is loaded
startRecognition();
}
@ -107,7 +106,7 @@ class TranslationController extends GetxController {
scrollController.dispose();
super.onClose();
}
// Initialize the translation service
Future<void> _initTranslationService() async {
try {
@ -129,13 +128,13 @@ class TranslationController extends GetxController {
// Start voice recognition
Future<void> startRecognition() async {
if (isRecognizing.value) return;
try {
// 生成新的会话ID
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
// Initialize a new translation item
currentItem = TranslationItem(
currentItem.value = TranslationItem(
sourceText: '',
translatedText: '',
sourceLanguage: sourceLanguage.value,
@ -144,19 +143,22 @@ class TranslationController extends GetxController {
sessionId: currentSessionId!,
isFirstInSession: true, // 标记为会话中的第一条
);
// Start the voice recognition service
final success = await _asrService.startContinuousRecognition();
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) {
@ -196,258 +198,247 @@ class TranslationController extends GetxController {
// Stop voice recognition
Future<void> stopRecognition() async {
if (!isRecognizing.value) return;
try {
await _asrService.stopContinuousRecognition();
_recognitionSubscription?.cancel();
_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);
}
}
// Handle intermediate recognition result
void handleIntermediateResult(String text) {
if (text.isEmpty) return;
// 如果当前没有活跃的翻译项,创建一个新的
if (currentItem == null) {
// 如果没有当前会话ID,创建一个新的
if (currentSessionId == null) {
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
}
currentItem = TranslationItem(
if (currentItem.value == null) {
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
currentItem.value = TranslationItem(
sourceText: text,
translatedText: '',
sourceLanguage: sourceLanguage.value,
targetLanguage: targetLanguage.value,
timestamp: DateTime.now(),
sessionId: currentSessionId!,
isFirstInSession: translationHistory.isEmpty ||
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;
// 更新当前翻译项(如果不存在则创建一个)
if (currentItem == null) {
// 如果没有当前会话ID,创建一个新的
if (currentSessionId == null) {
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
void handleFinalResult(String text) async {
// 如果没有文本或当前项为空,直接返回
if (text.isEmpty || currentItem.value == null) return;
// 更新当前项的源文本
currentItem.value!.sourceText = text;
currentItem.refresh();
// 添加到历史记录
translationHistory.add(currentItem.value!);
saveTranslationHistory();
// 重置当前项
currentItem.value = null;
// 对最终结果进行一次翻译,确保翻译结果是最新的
final translationResult = await translateText(text);
if (translationResult != null) {
if (isTtsEnabled.value) {
await playTranslatedText(translationResult);
}
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;
}
// 更新识别文本
recognizedText.value = text;
// 翻译最终结果 (添加到历史的逻辑已移至translateText方法)
translateText(text, isFinal: true).then((_) {
// 作为安全措施,确保当前项被添加到历史中
// 这是为了处理translateText中可能因为条件不满足而没有添加到历史的情况
finalizeCurrentItem();
});
// Scroll to the bottom to show the latest content
// 滚动到底部显示最新内容
_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;
}
}
// 创建一个新的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,
);
translationHistory.add(updatedItem);
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);
if (index != -1) {
// 直接更新历史记录项
translationHistory[index].translatedText = result;
// 通知 GetX 更新
translationHistory.refresh();
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) {
try {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} catch (e) {
// 如果滚动失败,尝试延迟后再次滚动
Future.delayed(const Duration(milliseconds: 200), () {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
}
} else {
// 如果scrollController还没有客户端,延迟后再尝试
Future.delayed(const Duration(milliseconds: 200), _scrollToBottom);
if (!scrollController.hasClients) {
Future.delayed(const Duration(milliseconds: 100), _scrollToBottom);
return;
}
try {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} catch (e) {
// 如果滚动失败,尝试延迟后再次滚动
Future.delayed(const Duration(milliseconds: 100), () {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
}
}
@ -456,19 +447,19 @@ class TranslationController extends GetxController {
final temp = sourceLanguage.value;
sourceLanguage.value = targetLanguage.value;
targetLanguage.value = temp;
// 如果正在识别,需要停止并重新开始
if (isRecognizing.value) {
stopRecognition();
startRecognition();
}
}
// 设置源语言
void setSourceLanguage(String language) {
if (sourceLanguage.value != language) {
sourceLanguage.value = language;
// 如果正在识别,需要停止并重新开始
if (isRecognizing.value) {
stopRecognition();
@ -476,7 +467,7 @@ class TranslationController extends GetxController {
}
}
}
// 设置目标语言
void setTargetLanguage(String language) {
if (targetLanguage.value != language) {
@ -488,13 +479,14 @@ class TranslationController extends GetxController {
void loadTranslationHistory() {
final historyJson = _storage.read(_historyKey);
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
translationHistory.value = historyList.map((item) => TranslationItem.fromJson(item)).toList();
// 确保在历史记录加载后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
try {
final List<dynamic> historyList = jsonDecode(historyJson);
translationHistory.value =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
} catch (e) {
// 如果解析失败,清除历史记录
_storage.remove(_historyKey);
}
}
}
@ -502,10 +494,11 @@ 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 =
final List<Map<String, dynamic>> historyJson =
translationHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(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,
);
}
@ -560,4 +561,4 @@ class TranslationItem {
'isFirstInSession': isFirstInSession,
};
}
}
}

81
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(
messages: messages,
systemPrompt: systemPrompt,
);
// 添加超时处理
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;

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

@ -211,21 +211,26 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
),
SizedBox(width: 12.w),
// 播放按钮
GestureDetector(
onTap: () {
controller.playTranslationItem(item);
},
child: Container(
width: 28.w,
height: 28.h,
decoration: BoxDecoration(
color: Colors.blue.withAlpha(10),
shape: BoxShape.circle,
),
child: Icon(
Icons.volume_up,
color: Colors.blue,
size: 16.sp,
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,
decoration: BoxDecoration(
color: Colors.blue.withAlpha(10),
shape: BoxShape.circle,
),
child: Icon(
Icons.volume_up,
color: Colors.blue,
size: 16.sp,
),
),
),
),
@ -316,46 +321,48 @@ class TranslationDetailView extends GetView<TranslationDetailController> {
mainAxisAlignment: MainAxisAlignment.end,
children: [
// 播放按钮
GestureDetector(
onTap: () {
controller.playText(controller.summaryResult.value);
},
child: Container(
padding: EdgeInsets.all(8.r),
decoration: BoxDecoration(
color: Colors.blue.withAlpha(10),
shape: BoxShape.circle,
),
child: Icon(
Icons.volume_up,
color: Colors.blue,
size: 18.sp,
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
controller.playText(controller.summaryResult.value);
},
borderRadius: BorderRadius.circular(20.r),
splashColor: Colors.blue.withOpacity(0.2),
child: Padding(
padding: EdgeInsets.all(8.r),
child: Icon(
Icons.volume_up,
color: Colors.blue,
size: 18.sp,
),
),
),
),
SizedBox(width: 12.w),
// 复制按钮
GestureDetector(
onTap: () {
// 复制摘要到剪贴板
Clipboard.setData(ClipboardData(text: controller.summaryResult.value));
Get.snackbar(
'已复制',
'摘要已复制到剪贴板',
snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 2),
);
},
child: Container(
padding: EdgeInsets.all(8.r),
decoration: BoxDecoration(
color: Colors.blue.withAlpha(10),
shape: BoxShape.circle,
),
child: Icon(
Icons.copy,
color: Colors.blue,
size: 18.sp,
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
// 复制摘要到剪贴板
Clipboard.setData(ClipboardData(text: controller.summaryResult.value));
Get.snackbar(
'已复制',
'摘要已复制到剪贴板',
snackPosition: SnackPosition.BOTTOM,
duration: Duration(seconds: 2),
);
},
borderRadius: BorderRadius.circular(20.r),
splashColor: Colors.blue.withOpacity(0.2),
child: Padding(
padding: EdgeInsets.all(8.r),
child: Icon(
Icons.copy,
color: Colors.blue,
size: 18.sp,
),
),
),
),
@ -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),
),
),
);
}
}
// 分享内容方法

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

@ -126,54 +126,55 @@ class TranslationHistoryView extends GetView<TranslationHistoryController> {
// 构建组内的历史记录项
Widget _buildHistoryItemInGroup(TranslationItem item) {
return GestureDetector(
return InkWell(
onTap: () {
// 点击查看详情
_showTranslationDetail(item);
},
child: Stack(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Text(
item.sourceText,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: Colors.black,
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: [
// 源文本
Text(
item.sourceText,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: Colors.black,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 6.h),
// 翻译文本
Text(
item.translatedText,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[700],
SizedBox(height: 6.h),
// 翻译文本
Text(
item.translatedText,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[700],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
),
Positioned(
right: 12.w,
top: 0,
bottom: 0,
child: Icon(
Icon(
Icons.chevron_right,
color: Colors.grey[350],
size: 20.sp,
),
),
],
],
),
),
);
}

81
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,32 +253,38 @@ class TranslationView extends GetView<TranslationController> {
),
],
),
child: Center(
child: Obx(() {
final isRecognizing = controller.isRecognizing.value;
return GestureDetector(
onTap: () {
if (isRecognizing) {
controller.stopRecognition();
} else {
controller.startRecognition();
}
},
child: Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: isRecognizing ? Colors.red : Colors.blue,
shape: BoxShape.circle,
),
child: Icon(
isRecognizing ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 30.sp,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 主要控制按钮 - 开始/暂停语音识别
Obx(() {
final isRecognizing = controller.isRecognizing.value;
return GestureDetector(
onTap: () {
if (isRecognizing) {
controller.stopAll();
} else {
controller.startRecognition();
}
},
child: Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: isRecognizing ? Colors.red : Colors.blue,
shape: BoxShape.circle,
),
child: Icon(
isRecognizing ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 30.sp,
),
),
),
);
}),
);
}),
],
),
);
}

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