You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
312 lines
8.7 KiB
312 lines
8.7 KiB
import 'dart:async';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
import '../services/azure_asr_service.dart';
|
|
import '../services/azure_tts_service.dart';
|
|
import '../services/chat_history_service.dart';
|
|
import '../../modules/chat/models/message_model.dart';
|
|
import '../../core/utils/logger.dart';
|
|
|
|
/// 语音交互服务,用于管理原生的 VoiceInteractionService
|
|
class VoiceInteractionService extends GetxService {
|
|
static VoiceInteractionService get to => Get.find();
|
|
|
|
// 服务运行状态
|
|
final _isRunning = false.obs;
|
|
bool get isRunning => _isRunning.value;
|
|
|
|
// 语音识别状态
|
|
final _isRecognitionActive = false.obs;
|
|
bool get isRecognitionActive => _isRecognitionActive.value;
|
|
|
|
// 聊天记录 - 可被 ChatView 监听
|
|
final RxList<Message> personalAssistantMessages = <Message>[].obs;
|
|
|
|
// 方法通道
|
|
static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction');
|
|
|
|
// 事件通道
|
|
static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events');
|
|
|
|
// 配置信息
|
|
late final String _azureSpeechKey;
|
|
late final String _azureSpeechRegion;
|
|
late final String _volcanoAiApiKey;
|
|
|
|
// 事件通道状态
|
|
bool _isEventChannelReady = false;
|
|
Completer<void>? _eventChannelReadyCompleter;
|
|
|
|
// 初始化标记,避免重复初始化
|
|
bool _isInitialized = false;
|
|
|
|
VoiceInteractionService() {
|
|
_loadConfig();
|
|
_setupEventChannel();
|
|
}
|
|
|
|
/// 从环境变量加载配置
|
|
void _loadConfig() {
|
|
_azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
|
|
_azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
|
|
_volcanoAiApiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? '';
|
|
|
|
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) {
|
|
Logger.error('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
|
|
}
|
|
|
|
if (_volcanoAiApiKey.isEmpty) {
|
|
Logger.error('未找到火山 AI 配置。请在 .env 文件中设置 VOLCANO_AI_API_KEY');
|
|
}
|
|
}
|
|
|
|
/// 设置事件通道监听
|
|
void _setupEventChannel() {
|
|
_eventChannelReadyCompleter = Completer<void>();
|
|
|
|
_eventChannel.receiveBroadcastStream().listen(
|
|
(event) {
|
|
if (event is Map) {
|
|
final String eventType = event['type'] as String? ?? '';
|
|
|
|
// 处理通道准备好的事件
|
|
if (eventType == 'channelReady') {
|
|
_isEventChannelReady = true;
|
|
if (!_eventChannelReadyCompleter!.isCompleted) {
|
|
_eventChannelReadyCompleter!.complete();
|
|
}
|
|
return;
|
|
}
|
|
|
|
_handleEvent(event);
|
|
}
|
|
},
|
|
onError: (error) {
|
|
Logger.error('语音交互事件通道错误: $error');
|
|
}
|
|
);
|
|
}
|
|
|
|
/// 等待事件通道准备好
|
|
Future<bool> _waitForEventChannel({Duration timeout = const Duration(seconds: 5)}) async {
|
|
if (_isEventChannelReady) return true;
|
|
|
|
try {
|
|
await _eventChannelReadyCompleter!.future.timeout(timeout);
|
|
return true;
|
|
} on TimeoutException {
|
|
Logger.error('等待事件通道准备好超时');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 初始化服务
|
|
Future<VoiceInteractionService> init() async {
|
|
// 如果已经初始化过,直接返回
|
|
if (_isInitialized) {
|
|
return this;
|
|
}
|
|
|
|
try {
|
|
// 等待事件通道准备好
|
|
final bool eventChannelReady = await _waitForEventChannel();
|
|
if (!eventChannelReady) {
|
|
Logger.error('事件通道未准备好,初始化失败');
|
|
return this;
|
|
}
|
|
|
|
// 获取服务运行状态
|
|
await _updateServiceStatus();
|
|
|
|
// 加载个人助手的历史记录
|
|
_loadPersonalAssistantHistory();
|
|
|
|
// 标记为已初始化
|
|
_isInitialized = true;
|
|
return this;
|
|
} catch (e) {
|
|
Logger.error('VoiceInteractionService 初始化失败: $e');
|
|
return this;
|
|
}
|
|
}
|
|
|
|
/// 加载个人助手的历史记录
|
|
void _loadPersonalAssistantHistory() {
|
|
if (Get.isRegistered<ChatHistoryService>()) {
|
|
final historyService = Get.find<ChatHistoryService>();
|
|
final messages = historyService.loadHistory('personal_assistant');
|
|
if (messages.isNotEmpty) {
|
|
personalAssistantMessages.assignAll(messages);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 处理来自原生端的事件
|
|
void _handleEvent(dynamic event) {
|
|
try {
|
|
if (event is! Map) {
|
|
return;
|
|
}
|
|
|
|
final type = event['type'] as String?;
|
|
if (type == null) {
|
|
return;
|
|
}
|
|
|
|
switch (type) {
|
|
case 'recognitionStarted':
|
|
_isRecognitionActive.value = true;
|
|
|
|
// 检测到后台服务的语音识别开始运行,主动停止Flutter端的语音识别和TTS
|
|
// _stopFlutterVoiceServices();
|
|
break;
|
|
case 'chatHistory':
|
|
// 处理聊天记录
|
|
_handleChatHistory(event);
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
Logger.error('处理原生端事件时出错: $e');
|
|
}
|
|
}
|
|
|
|
/// 处理聊天记录事件
|
|
void _handleChatHistory(Map<dynamic, dynamic> event) {
|
|
try {
|
|
final agentId = event['agentId'] as String?;
|
|
final userMessage = event['userMessage'] as String?;
|
|
final assistantMessage = event['assistantMessage'] as String?;
|
|
final timestamp = event['timestamp'] as int?;
|
|
|
|
if (agentId == null || userMessage == null || assistantMessage == null || timestamp == null) {
|
|
return;
|
|
}
|
|
|
|
// 只处理个人助手的聊天记录
|
|
if (agentId != 'personal_assistant') {
|
|
return;
|
|
}
|
|
|
|
// 添加用户消息
|
|
personalAssistantMessages.add(Message(
|
|
role: 'user',
|
|
content: userMessage,
|
|
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp),
|
|
));
|
|
|
|
// 添加助手消息
|
|
personalAssistantMessages.add(Message(
|
|
role: 'assistant',
|
|
content: assistantMessage,
|
|
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1000), // 助手消息时间戳稍后
|
|
));
|
|
|
|
// 保存到历史记录
|
|
if (Get.isRegistered<ChatHistoryService>()) {
|
|
final historyService = Get.find<ChatHistoryService>();
|
|
historyService.saveHistory('personal_assistant', personalAssistantMessages);
|
|
}
|
|
} catch (e) {
|
|
Logger.error('处理聊天记录事件失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 停止 Flutter 端的语音服务
|
|
void _stopFlutterVoiceServices() {
|
|
// 停止 TTS
|
|
if (Get.isRegistered<AzureTtsService>()) {
|
|
Get.find<AzureTtsService>().stop();
|
|
}
|
|
|
|
// 停止 ASR
|
|
if (Get.isRegistered<AzureAsrService>()) {
|
|
Get.find<AzureAsrService>().stopContinuousRecognition();
|
|
}
|
|
}
|
|
|
|
/// 启动语音交互服务
|
|
Future<bool> startService() async {
|
|
if (!_isInitialized) {
|
|
await init();
|
|
}
|
|
|
|
try {
|
|
// 启动服务
|
|
final result = await _channel.invokeMethod<bool>('startVoiceInteractionService', {
|
|
'azure_speech_key': _azureSpeechKey,
|
|
'azure_speech_region': _azureSpeechRegion,
|
|
'volcano_ai_api_key': _volcanoAiApiKey,
|
|
}) ?? false;
|
|
|
|
// 更新状态
|
|
if (result) {
|
|
await _updateServiceStatus();
|
|
}
|
|
|
|
return result;
|
|
} catch (e) {
|
|
Logger.error('启动语音交互服务失败: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 停止语音交互服务
|
|
Future<bool> stopService() async {
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('stopVoiceInteractionService') ?? false;
|
|
|
|
// 更新状态
|
|
if (result) {
|
|
await _updateServiceStatus();
|
|
}
|
|
|
|
return result;
|
|
} catch (e) {
|
|
Logger.error('停止语音交互服务失败: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 暂停语音交互
|
|
Future<bool> pauseVoiceInteraction() async {
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('pauseVoiceInteraction') ?? false;
|
|
return result;
|
|
} catch (e) {
|
|
Logger.error('暂停语音交互失败: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 更新服务运行状态
|
|
Future<void> _updateServiceStatus() async {
|
|
try {
|
|
final status = await _channel.invokeMethod<bool>('isVoiceInteractionServiceRunning') ?? false;
|
|
_isRunning.value = status;
|
|
} catch (e) {
|
|
Logger.error('获取语音交互服务状态失败: $e');
|
|
_isRunning.value = false;
|
|
}
|
|
}
|
|
|
|
/// 释放资源
|
|
Future<void> dispose() async {
|
|
try {
|
|
if (_isRunning.value) {
|
|
await stopService();
|
|
}
|
|
|
|
_isInitialized = false;
|
|
} catch (e) {
|
|
Logger.error('释放语音交互服务资源失败: $e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
// 清理资源
|
|
dispose();
|
|
super.onClose();
|
|
}
|
|
}
|