18 changed files with 1288 additions and 532 deletions
@ -0,0 +1,191 @@ |
|||||
|
import Foundation |
||||
|
import AVFoundation |
||||
|
|
||||
|
/// 统一音频会话管理类 |
||||
|
/// 用于统一管理所有组件的音频会话配置 |
||||
|
@objc class AudioSessionManager: NSObject { |
||||
|
// MARK: - 单例实现 |
||||
|
|
||||
|
@objc static let shared = AudioSessionManager() |
||||
|
|
||||
|
// 音频会话 |
||||
|
private let audioSession = AVAudioSession.sharedInstance() |
||||
|
|
||||
|
// 是否已经配置过音频会话 |
||||
|
private var isConfigured = false |
||||
|
|
||||
|
// 当前配置类型 |
||||
|
private var currentCategory: AVAudioSession.Category = .playAndRecord |
||||
|
private var currentMode: AVAudioSession.Mode = .default |
||||
|
private var currentOptions: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP] |
||||
|
|
||||
|
// 私有初始化方法,确保只能通过shared访问 |
||||
|
private override init() { |
||||
|
super.init() |
||||
|
NSLog("[AudioSessionManager] 音频会话管理器已初始化") |
||||
|
} |
||||
|
|
||||
|
// MARK: - 音频会话配置 |
||||
|
|
||||
|
/// 在应用启动时调用一次性配置方法 |
||||
|
/// - Parameters: |
||||
|
/// - category: 音频会话类别 |
||||
|
/// - mode: 音频会话模式 |
||||
|
/// - options: 音频会话选项 |
||||
|
/// - Returns: 是否配置成功 |
||||
|
@objc func initialConfiguration(category: AVAudioSession.Category = .playAndRecord, |
||||
|
mode: AVAudioSession.Mode = .default, |
||||
|
options: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP]) -> Bool { |
||||
|
if isConfigured { |
||||
|
NSLog("[AudioSessionManager] 音频会话已经配置过,忽略初始化请求") |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
do { |
||||
|
NSLog("[AudioSessionManager] 执行一次性音频会话配置: \(category)") |
||||
|
try audioSession.setCategory(category, mode: mode, options: options) |
||||
|
try audioSession.setActive(true) |
||||
|
|
||||
|
// 记录当前配置 |
||||
|
currentCategory = category |
||||
|
currentMode = mode |
||||
|
currentOptions = options |
||||
|
isConfigured = true |
||||
|
|
||||
|
return true |
||||
|
} catch { |
||||
|
NSLog("[AudioSessionManager] 一次性配置音频会话失败: \(error.localizedDescription)") |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 配置音频会话,适用于语音识别和合成 |
||||
|
@objc func configureForVoiceProcessing() throws { |
||||
|
if !applyConfigurationIfNeeded(category: .playAndRecord, |
||||
|
mode: .spokenAudio, |
||||
|
options: [.allowBluetooth, .allowBluetoothA2DP, .duckOthers]) { |
||||
|
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过语音处理模式配置") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 配置音频会话,适用于蓝牙耳机连接和监听 |
||||
|
@objc func configureForBluetoothHeadset() throws { |
||||
|
if !applyConfigurationIfNeeded(category: .playAndRecord, |
||||
|
mode: .default, |
||||
|
options: [.allowBluetooth, .allowBluetoothA2DP]) { |
||||
|
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过蓝牙耳机模式配置") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 配置音频会话,适用于媒体播放和蓝牙媒体按钮 |
||||
|
@objc func configureForMediaPlayback() throws { |
||||
|
if !applyConfigurationIfNeeded(category: .playback, |
||||
|
mode: .default, |
||||
|
options: [.allowBluetooth, .allowBluetoothA2DP]) { |
||||
|
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过媒体播放模式配置") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 配置自定义音频会话 |
||||
|
@objc func configure(category: AVAudioSession.Category, |
||||
|
mode: AVAudioSession.Mode, |
||||
|
options: AVAudioSession.CategoryOptions) throws { |
||||
|
if !applyConfigurationIfNeeded(category: category, mode: mode, options: options) { |
||||
|
NSLog("[AudioSessionManager] 使用已存在的音频会话配置,跳过自定义模式配置") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 内部方法:如果尚未配置,应用新的配置 |
||||
|
private func applyConfigurationIfNeeded(category: AVAudioSession.Category, |
||||
|
mode: AVAudioSession.Mode, |
||||
|
options: AVAudioSession.CategoryOptions) -> Bool { |
||||
|
// 如果已经配置过,不再重复配置 |
||||
|
if isConfigured { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
do { |
||||
|
NSLog("[AudioSessionManager] 应用音频会话配置: \(category)") |
||||
|
try audioSession.setCategory(category, mode: mode, options: options) |
||||
|
try audioSession.setActive(true) |
||||
|
|
||||
|
// 记录当前配置 |
||||
|
currentCategory = category |
||||
|
currentMode = mode |
||||
|
currentOptions = options |
||||
|
isConfigured = true |
||||
|
|
||||
|
return true |
||||
|
} catch { |
||||
|
NSLog("[AudioSessionManager] 配置音频会话失败: \(error.localizedDescription)") |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 获取当前音频会话的配置信息 |
||||
|
@objc func getCurrentConfiguration() -> [String: Any] { |
||||
|
return [ |
||||
|
"isConfigured": isConfigured, |
||||
|
"category": "\(currentCategory)", |
||||
|
"mode": "\(currentMode)" |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
/// 仅在特殊情况下使用:强制重新配置音频会话 |
||||
|
@objc func forceReconfigure() -> Bool { |
||||
|
isConfigured = false |
||||
|
|
||||
|
do { |
||||
|
NSLog("[AudioSessionManager] 强制重新配置音频会话") |
||||
|
try audioSession.setCategory(currentCategory, mode: currentMode, options: currentOptions) |
||||
|
try audioSession.setActive(true) |
||||
|
isConfigured = true |
||||
|
return true |
||||
|
} catch { |
||||
|
NSLog("[AudioSessionManager] 强制重新配置失败: \(error.localizedDescription)") |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 激活音频会话(如果已配置) |
||||
|
@objc func activateSession() throws { |
||||
|
if isConfigured { |
||||
|
try audioSession.setActive(true) |
||||
|
} else { |
||||
|
NSLog("[AudioSessionManager] 警告: 尝试激活未配置的音频会话") |
||||
|
// 应用默认配置并激活 |
||||
|
try applyConfigurationIfNeeded(category: .playAndRecord, |
||||
|
mode: .default, |
||||
|
options: [.allowBluetooth, .allowBluetoothA2DP]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 停用音频会话(保留配置) |
||||
|
@objc func deactivateSession() throws { |
||||
|
try audioSession.setActive(false, options: .notifyOthersOnDeactivation) |
||||
|
} |
||||
|
|
||||
|
/// 获取当前音频会话路由描述 |
||||
|
@objc func getCurrentRoute() -> AVAudioSessionRouteDescription { |
||||
|
return audioSession.currentRoute |
||||
|
} |
||||
|
|
||||
|
/// 添加路由变化监听器 |
||||
|
@objc func addRouteChangeListener(_ observer: Any, selector: Selector) { |
||||
|
NotificationCenter.default.addObserver( |
||||
|
observer, |
||||
|
selector: selector, |
||||
|
name: AVAudioSession.routeChangeNotification, |
||||
|
object: nil |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
/// 移除路由变化监听器 |
||||
|
@objc func removeRouteChangeListener(_ observer: Any) { |
||||
|
NotificationCenter.default.removeObserver( |
||||
|
observer, |
||||
|
name: AVAudioSession.routeChangeNotification, |
||||
|
object: nil |
||||
|
) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
import 'package:meta/meta.dart'; |
||||
|
|
||||
|
/// 语音交互事件基类 |
||||
|
abstract class VoiceInteractionEvent { |
||||
|
final int timestamp; |
||||
|
|
||||
|
VoiceInteractionEvent({required this.timestamp}); |
||||
|
} |
||||
|
|
||||
|
/// 聊天历史事件 |
||||
|
class ChatHistoryEvent extends VoiceInteractionEvent { |
||||
|
final String agentId; |
||||
|
final String userMessage; |
||||
|
final String assistantMessage; |
||||
|
|
||||
|
ChatHistoryEvent({ |
||||
|
required this.agentId, |
||||
|
required this.userMessage, |
||||
|
required this.assistantMessage, |
||||
|
required int timestamp, |
||||
|
}) : super(timestamp: timestamp); |
||||
|
} |
||||
|
|
||||
|
/// 语音识别开始事件 |
||||
|
class RecognitionStartedEvent extends VoiceInteractionEvent { |
||||
|
RecognitionStartedEvent({ |
||||
|
required int timestamp, |
||||
|
}) : super(timestamp: timestamp); |
||||
|
} |
||||
@ -0,0 +1,58 @@ |
|||||
|
class Message { |
||||
|
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构造函数 |
||||
|
factory Message.fromJson(Map<String, dynamic> json) { |
||||
|
return Message( |
||||
|
role: json['role'] as String, |
||||
|
content: json['content'] as String, |
||||
|
timestamp: DateTime.parse(json['timestamp'] as String), |
||||
|
isLoading: json['isLoading'] as bool? ?? false, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 转换为JSON |
||||
|
Map<String, dynamic> toJson() { |
||||
|
return { |
||||
|
'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, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,341 @@ |
|||||
|
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'; |
||||
|
import '../../../modules/chat/models/message_model.dart'; |
||||
|
import '../../models/events/voice_interaction_event.dart'; |
||||
|
import '../chat_history_service.dart'; |
||||
|
import '../voice_interaction_service.dart'; |
||||
|
|
||||
|
/// Android语音交互服务 |
||||
|
/// |
||||
|
/// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口, |
||||
|
/// 用于管理后台语音交互服务的生命周期和接收语音交互事件 |
||||
|
class AndroidVoiceInteractionService extends GetxService implements VoiceInteractionService { |
||||
|
static AndroidVoiceInteractionService get to => Get.find(); |
||||
|
// 方法通道和事件通道 |
||||
|
static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction'); |
||||
|
static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events'); |
||||
|
|
||||
|
// 服务状态 |
||||
|
final _isServiceRunning = false.obs; |
||||
|
bool get isServiceRunning => _isServiceRunning.value; |
||||
|
|
||||
|
// 事件流控制器 |
||||
|
StreamController<VoiceInteractionEvent>? _eventStreamController; |
||||
|
Stream<VoiceInteractionEvent>? _eventStream; |
||||
|
@override |
||||
|
Stream<VoiceInteractionEvent> get eventStream => _eventStream ?? Stream.empty(); |
||||
|
|
||||
|
// 事件通道状态 |
||||
|
StreamSubscription? _eventSubscription; |
||||
|
|
||||
|
// 配置信息 |
||||
|
late String _azureSpeechKey; |
||||
|
late String _azureSpeechRegion; |
||||
|
late String _volcanoAiApiKey; |
||||
|
|
||||
|
// 初始化状态标志 |
||||
|
static bool _isInitialized = false; |
||||
|
|
||||
|
// 当前会话历史 |
||||
|
final List<Map<String, String>> _messageHistory = []; |
||||
|
|
||||
|
// 服务ID |
||||
|
static const String _serviceId = 'android_assistant'; |
||||
|
|
||||
|
/// 构造函数 |
||||
|
AndroidVoiceInteractionService() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/// 从环境变量加载配置 |
||||
|
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.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); |
||||
|
} |
||||
|
|
||||
|
if (_volcanoAiApiKey.isEmpty) { |
||||
|
Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 设置事件通道 |
||||
|
void _setupEventChannel() { |
||||
|
_eventSubscription = _eventChannel |
||||
|
.receiveBroadcastStream() |
||||
|
.listen((event) { |
||||
|
if (event is Map) { |
||||
|
final String eventType = event['type'] as String? ?? ''; |
||||
|
|
||||
|
// 直接处理事件,不再等待channelReady |
||||
|
_handleVoiceInteractionEvent(event); |
||||
|
} |
||||
|
}, onError: (error) { |
||||
|
Logger.error('语音交互事件通道错误: $error'); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/// 创建事件流 |
||||
|
void _createEventStream() { |
||||
|
_eventStreamController = StreamController<VoiceInteractionEvent>.broadcast(); |
||||
|
_eventStream = _eventStreamController?.stream; |
||||
|
} |
||||
|
|
||||
|
/// 初始化服务 |
||||
|
@override |
||||
|
Future<VoiceInteractionService> initialize() async { |
||||
|
try { |
||||
|
if (_isInitialized) { |
||||
|
return this; |
||||
|
} |
||||
|
_isInitialized = true; |
||||
|
|
||||
|
_loadConfig(); |
||||
|
_setupEventChannel(); |
||||
|
_createEventStream(); |
||||
|
// 检查服务是否正在运行 |
||||
|
await _checkServiceStatus(); |
||||
|
|
||||
|
await _startService(); |
||||
|
|
||||
|
Logger.info('语音交互服务初始化完成'); |
||||
|
return this; |
||||
|
} catch (e) { |
||||
|
Logger.error('语音交互服务初始化失败: $e'); |
||||
|
return this; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void onWakeup() { |
||||
|
Logger.info('语音交互服务唤醒'); |
||||
|
} |
||||
|
|
||||
|
/// 检查服务状态 |
||||
|
Future<void> _checkServiceStatus() async { |
||||
|
try { |
||||
|
final bool isRunning = await _channel.invokeMethod('isVoiceInteractionServiceRunning') ?? false; |
||||
|
_isServiceRunning.value = isRunning; |
||||
|
Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); |
||||
|
} catch (e) { |
||||
|
Logger.error('检查语音交互服务状态失败: $e'); |
||||
|
_isServiceRunning.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 启动语音交互服务 |
||||
|
Future<bool> _startService() async { |
||||
|
if (_isServiceRunning.value) { |
||||
|
Logger.info('语音交互服务已经在运行'); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
final bool result = await _channel.invokeMethod('startVoiceInteractionService', { |
||||
|
'azure_speech_key': _azureSpeechKey, |
||||
|
'azure_speech_region': _azureSpeechRegion, |
||||
|
'volcano_ai_api_key': _volcanoAiApiKey, |
||||
|
}) ?? false; |
||||
|
|
||||
|
if (result) { |
||||
|
_isServiceRunning.value = true; |
||||
|
Logger.info('语音交互服务启动成功'); |
||||
|
} else { |
||||
|
Logger.error('语音交互服务启动失败'); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
Logger.error('启动语音交互服务失败: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 停止语音交互服务 |
||||
|
Future<bool> _stopService() async { |
||||
|
if (!_isServiceRunning.value) { |
||||
|
Logger.info('语音交互服务未运行'); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
final bool result = await _channel.invokeMethod('stopVoiceInteractionService') ?? false; |
||||
|
|
||||
|
if (result) { |
||||
|
_isServiceRunning.value = false; |
||||
|
Logger.info('语音交互服务停止成功'); |
||||
|
} else { |
||||
|
Logger.error('语音交互服务停止失败'); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
Logger.error('停止语音交互服务失败: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 暂停语音交互(停止TTS和语音识别,但保持服务运行) |
||||
|
@override |
||||
|
Future<bool> pauseVoiceInteraction() async { |
||||
|
if (!_isServiceRunning.value) { |
||||
|
Logger.info('语音交互服务未运行,无法暂停'); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
final bool result = await _channel.invokeMethod('pauseVoiceInteraction') ?? false; |
||||
|
|
||||
|
if (result) { |
||||
|
Logger.info('语音交互暂停成功'); |
||||
|
} else { |
||||
|
Logger.error('语音交互暂停失败'); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} catch (e) { |
||||
|
Logger.error('暂停语音交互失败: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 处理来自原生端的语音交互事件 |
||||
|
void _handleVoiceInteractionEvent(dynamic event) { |
||||
|
if (event is! Map || _eventStreamController == null) return; |
||||
|
|
||||
|
final Map<dynamic, dynamic> eventMap = event; |
||||
|
final String eventType = eventMap['type'] as String? ?? ''; |
||||
|
final int timestamp = eventMap['timestamp'] as int? ?? 0; |
||||
|
|
||||
|
// 添加时间戳日志,帮助调试 |
||||
|
Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); |
||||
|
|
||||
|
switch (eventType) { |
||||
|
case 'chatHistory': |
||||
|
final String agentId = eventMap['agentId'] as String? ?? ''; |
||||
|
final String userMessage = eventMap['userMessage'] as String? ?? ''; |
||||
|
final String assistantMessage = eventMap['assistantMessage'] as String? ?? ''; |
||||
|
|
||||
|
// 添加到消息历史 |
||||
|
if (userMessage.isNotEmpty) { |
||||
|
_messageHistory.add({'role': 'user', 'content': userMessage}); |
||||
|
_messageHistory.add({'role': 'assistant', 'content': assistantMessage}); |
||||
|
|
||||
|
// 保持历史记录在一定长度 |
||||
|
while (_messageHistory.length > 10) { |
||||
|
_messageHistory.removeAt(0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_eventStreamController?.add(ChatHistoryEvent( |
||||
|
agentId: agentId, |
||||
|
userMessage: userMessage, |
||||
|
assistantMessage: assistantMessage, |
||||
|
timestamp: timestamp, |
||||
|
)); |
||||
|
|
||||
|
// 保存聊天记录 |
||||
|
_saveChatHistory(agentId, userMessage, assistantMessage, timestamp); |
||||
|
|
||||
|
Logger.info('收到聊天历史事件: agentId=$agentId'); |
||||
|
break; |
||||
|
|
||||
|
case 'recognitionStarted': |
||||
|
_eventStreamController?.add(RecognitionStartedEvent( |
||||
|
timestamp: timestamp, |
||||
|
)); |
||||
|
break; |
||||
|
|
||||
|
default: |
||||
|
Logger.warning('收到未知类型的语音交互事件: $eventType'); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
void onClose() { |
||||
|
// 清理资源 |
||||
|
_stopService(); |
||||
|
_eventSubscription?.cancel(); |
||||
|
_eventStreamController?.close(); |
||||
|
super.onClose(); |
||||
|
} |
||||
|
|
||||
|
/// 保存聊天记录 |
||||
|
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) { |
||||
|
try { |
||||
|
// 获取ChatHistoryService实例 |
||||
|
final chatHistoryService = Get.find<ChatHistoryService>(); |
||||
|
|
||||
|
// 创建用户消息和助手消息 |
||||
|
final userMsg = Message( |
||||
|
role: 'user', |
||||
|
content: userMessage, |
||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), |
||||
|
); |
||||
|
|
||||
|
final assistantMsg = Message( |
||||
|
role: 'assistant', |
||||
|
content: assistantMessage, |
||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息 |
||||
|
); |
||||
|
|
||||
|
// 加载现有历史记录 |
||||
|
final existingMessages = chatHistoryService.loadHistory(agentId); |
||||
|
|
||||
|
// 添加新消息 |
||||
|
existingMessages.addAll([userMsg, assistantMsg]); |
||||
|
|
||||
|
// 保存更新后的历史记录 |
||||
|
chatHistoryService.saveHistory(agentId, existingMessages); |
||||
|
} catch (e) { |
||||
|
Logger.error('保存聊天记录失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 是否处于活跃状态 |
||||
|
@override |
||||
|
bool isActive() { |
||||
|
return _isServiceRunning.value; |
||||
|
} |
||||
|
|
||||
|
/// 获取当前对话历史 |
||||
|
@override |
||||
|
List<Map<String, String>> getMessageHistory() { |
||||
|
return List<Map<String, String>>.from(_messageHistory); |
||||
|
} |
||||
|
|
||||
|
/// 获取当前服务ID |
||||
|
@override |
||||
|
String getServiceId() { |
||||
|
return _serviceId; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/// 聊天历史事件 |
||||
|
class ChatHistoryEvent extends VoiceInteractionEvent { |
||||
|
final String agentId; |
||||
|
final String userMessage; |
||||
|
final String assistantMessage; |
||||
|
|
||||
|
ChatHistoryEvent({ |
||||
|
required this.agentId, |
||||
|
required this.userMessage, |
||||
|
required this.assistantMessage, |
||||
|
required int timestamp, |
||||
|
}) : super(timestamp: timestamp); |
||||
|
} |
||||
|
|
||||
|
/// 语音识别开始事件 |
||||
|
class RecognitionStartedEvent extends VoiceInteractionEvent { |
||||
|
RecognitionStartedEvent({ |
||||
|
required int timestamp, |
||||
|
}) : super(timestamp: timestamp); |
||||
|
} |
||||
@ -0,0 +1,468 @@ |
|||||
|
import 'dart:async'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import '../asr_service.dart'; |
||||
|
import '../tts_service.dart'; |
||||
|
import '../volcano_ai_service.dart'; |
||||
|
import '../chat_history_service.dart'; |
||||
|
import '../../../modules/chat/models/message_model.dart'; |
||||
|
import '../../models/events/voice_interaction_event.dart'; |
||||
|
import '../../../core/utils/logger.dart'; |
||||
|
import '../voice_interaction_service.dart'; |
||||
|
|
||||
|
enum VoiceInteractionState { |
||||
|
idle, // 等待唤醒 |
||||
|
active, // 活跃状态 - 可以同时识别和播放 |
||||
|
} |
||||
|
|
||||
|
class IosVoiceInteractionService extends GetxService implements VoiceInteractionService { |
||||
|
final AsrService _asrService = Get.find<AsrService>(); |
||||
|
final TtsService _ttsService = Get.find<TtsService>(); |
||||
|
final VolcanoAIService _aiService = Get.find<VolcanoAIService>(); |
||||
|
final ChatHistoryService _chatHistoryService = Get.find<ChatHistoryService>(); |
||||
|
|
||||
|
// 当前状态 |
||||
|
final Rx<VoiceInteractionState> state = VoiceInteractionState.idle.obs; |
||||
|
|
||||
|
// 是否识别到用户语音 |
||||
|
final RxBool isSpeechDetected = false.obs; |
||||
|
|
||||
|
// 是否正在处理AI响应 |
||||
|
final RxBool isProcessingAI = false.obs; |
||||
|
|
||||
|
// 是否正在播放TTS |
||||
|
final RxBool isSpeaking = false.obs; |
||||
|
|
||||
|
// 对话历史记录 |
||||
|
final List<Map<String, String>> _messageHistory = []; |
||||
|
|
||||
|
// 最大历史记录数 |
||||
|
static const int _maxHistorySize = 10; |
||||
|
|
||||
|
// 超时计时器 |
||||
|
Timer? _inactivityTimer; |
||||
|
|
||||
|
// 流订阅 |
||||
|
StreamSubscription? _recognitionSubscription; |
||||
|
|
||||
|
// 当前识别的文本 |
||||
|
String _currentRecognizedText = ''; |
||||
|
|
||||
|
// 事件流控制器 |
||||
|
final StreamController<VoiceInteractionEvent> _eventStreamController = |
||||
|
StreamController<VoiceInteractionEvent>.broadcast(); |
||||
|
|
||||
|
// 获取事件流 |
||||
|
@override |
||||
|
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream; |
||||
|
|
||||
|
// 标识符,用于存储聊天历史 |
||||
|
final String _agentId = 'ios_assistant'; |
||||
|
|
||||
|
/// 初始化服务 |
||||
|
@override |
||||
|
Future<VoiceInteractionService> initialize() async { |
||||
|
try { |
||||
|
// 保持idle状态,不自动启动语音识别 |
||||
|
Logger.info('iOS语音交互服务初始化完成,处于idle状态'); |
||||
|
} catch (e) { |
||||
|
Logger.error('iOS语音交互服务初始化失败: $e'); |
||||
|
} |
||||
|
|
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
// 处理唤醒事件 |
||||
|
void onWakeup() { |
||||
|
_ttsService.speakOnce('我在呢!'); |
||||
|
isSpeaking.value = true; |
||||
|
|
||||
|
if (state.value == VoiceInteractionState.idle) { |
||||
|
_activateInteraction(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 激活交互 |
||||
|
void _activateInteraction() { |
||||
|
// 设置状态为活跃 |
||||
|
state.value = VoiceInteractionState.active; |
||||
|
|
||||
|
// 开始连续监听 |
||||
|
_startContinuousListening(); |
||||
|
|
||||
|
// 启动不活动计时器 |
||||
|
_startInactivityTimer(); |
||||
|
|
||||
|
// 发送识别开始事件 |
||||
|
_sendRecognitionStartedEvent(); |
||||
|
} |
||||
|
|
||||
|
// 开始连续监听 |
||||
|
Future<void> _startContinuousListening() async { |
||||
|
// 停止之前的监听 |
||||
|
_recognitionSubscription?.cancel(); |
||||
|
|
||||
|
try { |
||||
|
// 开始连续识别 |
||||
|
final recognitionStream = await _asrService.startContinuousRecognition(); |
||||
|
|
||||
|
// 监听识别结果 |
||||
|
_recognitionSubscription = recognitionStream.listen( |
||||
|
_handleRecognitionEvent, |
||||
|
onError: (error) { |
||||
|
Logger.error('语音识别错误: $error'); |
||||
|
_startContinuousListening(); // 尝试重新启动 |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
Logger.info('开始连续语音识别'); |
||||
|
} catch (e) { |
||||
|
Logger.error('启动语音识别失败: $e'); |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 处理识别事件 |
||||
|
void _handleRecognitionEvent(RecognitionEvent event) { |
||||
|
// 更新活动时间 |
||||
|
_updateActivityTime(); |
||||
|
|
||||
|
switch (event.type) { |
||||
|
case RecognitionEventType.intermediateResult: |
||||
|
// 检测用户是否开始说话 |
||||
|
if (event.text.isNotEmpty && !isSpeechDetected.value) { |
||||
|
isSpeechDetected.value = true; |
||||
|
|
||||
|
// 用户开始讲话时,立即中断当前响应 |
||||
|
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); |
||||
|
} |
||||
|
break; |
||||
|
|
||||
|
case RecognitionEventType.finalResult: |
||||
|
if (event.text.isNotEmpty) { |
||||
|
// 最终结果,处理用户输入 |
||||
|
_processUserInput(event.text); |
||||
|
} |
||||
|
// 重置语音检测状态 |
||||
|
isSpeechDetected.value = false; |
||||
|
break; |
||||
|
|
||||
|
case RecognitionEventType.sessionStarted: |
||||
|
Logger.info('语音识别会话开始'); |
||||
|
break; |
||||
|
|
||||
|
case RecognitionEventType.sessionStopped: |
||||
|
Logger.info('语音识别会话结束'); |
||||
|
|
||||
|
break; |
||||
|
|
||||
|
case RecognitionEventType.error: |
||||
|
case RecognitionEventType.canceled: |
||||
|
Logger.error('语音识别错误: ${event.error}'); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 中断当前响应 |
||||
|
void _interruptCurrentResponse(String reason) { |
||||
|
Logger.info(reason); |
||||
|
|
||||
|
// 如果正在活跃状态,需要中断当前操作 |
||||
|
if (state.value == VoiceInteractionState.active) { |
||||
|
// 停止TTS播放 |
||||
|
_ttsService.stop(); |
||||
|
isSpeaking.value = false; |
||||
|
isProcessingAI.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 处理用户输入 |
||||
|
Future<void> _processUserInput(String text) async { |
||||
|
Logger.info('处理用户输入: $text'); |
||||
|
|
||||
|
// 设置为处理状态 |
||||
|
isProcessingAI.value = true; |
||||
|
|
||||
|
// 添加用户消息到历史记录 |
||||
|
final userMessage = {'role': 'user', 'content': text}; |
||||
|
_addToHistory(userMessage); |
||||
|
|
||||
|
// 创建用户消息对象 |
||||
|
final userMsg = Message( |
||||
|
role: 'user', |
||||
|
content: text, |
||||
|
timestamp: DateTime.now(), |
||||
|
); |
||||
|
|
||||
|
// AI响应处理标志 |
||||
|
bool isProcessingCancelled = false; |
||||
|
|
||||
|
try { |
||||
|
// 构建消息历史 |
||||
|
final messageHistory = _buildMessageHistory(); |
||||
|
|
||||
|
// 调用AI服务获取响应 |
||||
|
final responseStream = _aiService.sendMessageStream( |
||||
|
messages: messageHistory, |
||||
|
systemPrompt: "你是一个智能助手,请简明扼要地回答问题。", |
||||
|
); |
||||
|
|
||||
|
String aiResponse = ''; |
||||
|
|
||||
|
// 等待AI响应 |
||||
|
await for (final chunk in responseStream) { |
||||
|
// 检查是否被用户打断 |
||||
|
if (isSpeechDetected.value) { |
||||
|
// 用户开始说话,标记处理被取消 |
||||
|
isProcessingCancelled = true; |
||||
|
Logger.info('AI响应生成过程中被用户打断'); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// 如果状态已改变(可能由其他原因导致),停止处理 |
||||
|
if (state.value != VoiceInteractionState.active) { |
||||
|
isProcessingCancelled = true; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
aiResponse += chunk; |
||||
|
|
||||
|
// 立即播放当前文本块,实现边生成边播放 |
||||
|
_speakStreamResponse(chunk); |
||||
|
} |
||||
|
|
||||
|
// 如果处理被取消,不继续后续操作 |
||||
|
if (isProcessingCancelled) { |
||||
|
isProcessingAI.value = false; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 确保完整响应被处理 |
||||
|
if (aiResponse.isNotEmpty) { |
||||
|
// 将AI响应添加到历史记录 |
||||
|
final assistantMessage = {'role': 'assistant', 'content': aiResponse}; |
||||
|
_addToHistory(assistantMessage); |
||||
|
|
||||
|
// 创建助手消息对象 |
||||
|
final assistantMsg = Message( |
||||
|
role: 'assistant', |
||||
|
content: aiResponse, |
||||
|
timestamp: DateTime.now(), |
||||
|
); |
||||
|
|
||||
|
// 保存聊天历史 |
||||
|
_saveChatHistory(userMsg, assistantMsg); |
||||
|
|
||||
|
// 发送聊天历史事件 |
||||
|
_sendChatHistoryEvent(userMsg.content, assistantMsg.content); |
||||
|
|
||||
|
// 刷新TTS流,确保所有文本都被播放 |
||||
|
await _ttsService.flushStream(); |
||||
|
isSpeaking.value = false; |
||||
|
} |
||||
|
|
||||
|
// 处理完成 |
||||
|
isProcessingAI.value = false; |
||||
|
} catch (e) { |
||||
|
Logger.error('AI响应处理失败: $e'); |
||||
|
// 错误恢复 |
||||
|
isProcessingAI.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 构建用于AI服务的消息历史 |
||||
|
List<Map<String, String>> _buildMessageHistory() { |
||||
|
return List<Map<String, String>>.from(_messageHistory); |
||||
|
} |
||||
|
|
||||
|
// 添加消息到历史记录 |
||||
|
void _addToHistory(Map<String, String> message) { |
||||
|
_messageHistory.add(message); |
||||
|
|
||||
|
// 限制历史记录大小 |
||||
|
while (_messageHistory.length > _maxHistorySize) { |
||||
|
_messageHistory.removeAt(0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 播放流式响应 |
||||
|
void _speakStreamResponse(String chunk) { |
||||
|
if (chunk.isEmpty) return; |
||||
|
|
||||
|
// 更新播放状态 |
||||
|
isSpeaking.value = true; |
||||
|
|
||||
|
// 使用流式TTS播放 |
||||
|
_ttsService.speakStream(chunk); |
||||
|
|
||||
|
// 更新活动时间 |
||||
|
_updateActivityTime(); |
||||
|
} |
||||
|
|
||||
|
// 停止TTS |
||||
|
void _stopTts() { |
||||
|
_ttsService.stop(); |
||||
|
isSpeaking.value = false; |
||||
|
} |
||||
|
|
||||
|
// 更新最后活动时间 |
||||
|
DateTime _lastActivityTime = DateTime.now(); |
||||
|
void _updateActivityTime() { |
||||
|
_lastActivityTime = DateTime.now(); |
||||
|
} |
||||
|
|
||||
|
// 启动不活动计时器(长时间无交互会切换到空闲状态) |
||||
|
void _startInactivityTimer() { |
||||
|
_cancelInactivityTimer(); |
||||
|
|
||||
|
_inactivityTimer = Timer.periodic(Duration(seconds: 5), (timer) { |
||||
|
// 计算空闲时间 |
||||
|
final idleTime = DateTime.now().difference(_lastActivityTime).inSeconds; |
||||
|
|
||||
|
// 如果空闲超过30秒,且不在播放或检测到语音,切换到空闲 |
||||
|
if (idleTime > 8 && |
||||
|
!isSpeaking.value && |
||||
|
!isProcessingAI.value && |
||||
|
!isSpeechDetected.value) { |
||||
|
_resetToIdle(); |
||||
|
timer.cancel(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// 取消不活动计时器 |
||||
|
void _cancelInactivityTimer() { |
||||
|
_inactivityTimer?.cancel(); |
||||
|
_inactivityTimer = null; |
||||
|
} |
||||
|
|
||||
|
// 重置到空闲状态 |
||||
|
void _resetToIdle() { |
||||
|
_ttsService.speakOnce('没有听到声音,暂停对话,双击耳机唤醒!'); |
||||
|
_cancelInactivityTimer(); |
||||
|
|
||||
|
// 停止TTS |
||||
|
_stopTts(); |
||||
|
|
||||
|
// 停止语音识别 |
||||
|
_recognitionSubscription?.cancel(); |
||||
|
_asrService.stopContinuousRecognition(); |
||||
|
|
||||
|
// 重置状态 |
||||
|
state.value = VoiceInteractionState.idle; |
||||
|
isSpeechDetected.value = false; |
||||
|
isProcessingAI.value = false; |
||||
|
isSpeaking.value = false; |
||||
|
|
||||
|
// 清空历史记录 |
||||
|
_messageHistory.clear(); |
||||
|
|
||||
|
Logger.info('语音交互已重置为空闲状态'); |
||||
|
} |
||||
|
|
||||
|
// 暂停语音交互 |
||||
|
@override |
||||
|
void pauseVoiceInteraction() { |
||||
|
if (state.value != VoiceInteractionState.idle) { |
||||
|
_stopTts(); |
||||
|
_recognitionSubscription?.cancel(); |
||||
|
_asrService.stopContinuousRecognition(); |
||||
|
|
||||
|
// 重置所有状态 |
||||
|
state.value = VoiceInteractionState.idle; |
||||
|
isSpeechDetected.value = false; |
||||
|
isProcessingAI.value = false; |
||||
|
isSpeaking.value = false; |
||||
|
|
||||
|
// 清空历史记录 |
||||
|
_messageHistory.clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 恢复语音交互 |
||||
|
void resumeVoiceInteraction() { |
||||
|
if (state.value == VoiceInteractionState.idle) { |
||||
|
_activateInteraction(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 手动停止交互 |
||||
|
void stopInteraction() { |
||||
|
_resetToIdle(); |
||||
|
} |
||||
|
|
||||
|
// 获取当前状态 |
||||
|
VoiceInteractionState getCurrentState() { |
||||
|
return state.value; |
||||
|
} |
||||
|
|
||||
|
// 是否处于活跃状态 |
||||
|
@override |
||||
|
bool isActive() { |
||||
|
return state.value != VoiceInteractionState.idle; |
||||
|
} |
||||
|
|
||||
|
// 获取当前对话历史 |
||||
|
@override |
||||
|
List<Map<String, String>> getMessageHistory() { |
||||
|
return List<Map<String, String>>.from(_messageHistory); |
||||
|
} |
||||
|
|
||||
|
// 获取当前服务ID |
||||
|
@override |
||||
|
String getServiceId() { |
||||
|
return _agentId; |
||||
|
} |
||||
|
|
||||
|
// 释放资源 |
||||
|
@override |
||||
|
void onClose() { |
||||
|
_cancelInactivityTimer(); |
||||
|
_recognitionSubscription?.cancel(); |
||||
|
_resetToIdle(); |
||||
|
_eventStreamController.close(); |
||||
|
super.onClose(); |
||||
|
} |
||||
|
|
||||
|
// 保存聊天历史 |
||||
|
void _saveChatHistory(Message userMsg, Message assistantMsg) { |
||||
|
try { |
||||
|
// 加载现有历史记录 |
||||
|
final existingMessages = _chatHistoryService.loadHistory(_agentId); |
||||
|
|
||||
|
// 添加新消息 |
||||
|
existingMessages.addAll([userMsg, assistantMsg]); |
||||
|
|
||||
|
// 保存更新后的历史记录 |
||||
|
_chatHistoryService.saveHistory(_agentId, existingMessages); |
||||
|
|
||||
|
Logger.info('保存聊天历史记录成功'); |
||||
|
} catch (e) { |
||||
|
Logger.error('保存聊天历史记录失败: $e'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 发送聊天历史事件 |
||||
|
void _sendChatHistoryEvent(String userMessage, String assistantMessage) { |
||||
|
final event = ChatHistoryEvent( |
||||
|
agentId: _agentId, |
||||
|
userMessage: userMessage, |
||||
|
assistantMessage: assistantMessage, |
||||
|
timestamp: DateTime.now().millisecondsSinceEpoch, |
||||
|
); |
||||
|
|
||||
|
_eventStreamController.add(event); |
||||
|
Logger.info('已发送聊天历史事件'); |
||||
|
} |
||||
|
|
||||
|
// 发送识别开始事件 |
||||
|
void _sendRecognitionStartedEvent() { |
||||
|
final event = RecognitionStartedEvent( |
||||
|
timestamp: DateTime.now().millisecondsSinceEpoch, |
||||
|
); |
||||
|
|
||||
|
_eventStreamController.add(event); |
||||
|
Logger.info('已发送识别开始事件'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -1,334 +1,33 @@ |
|||||
import 'dart:async'; |
import 'dart:async'; |
||||
import 'package:flutter/services.dart'; |
|
||||
import 'package:get/get.dart'; |
import 'package:get/get.dart'; |
||||
import '../../core/utils/logger.dart'; |
import '../models/events/voice_interaction_event.dart'; |
||||
import 'package:flutter_dotenv/flutter_dotenv.dart'; |
|
||||
import '../../modules/chat/models/message_model.dart'; |
|
||||
import 'chat_history_service.dart'; |
|
||||
import 'speech_impl/azure_asr_service.dart'; |
|
||||
import 'speech_impl/azure_tts_service.dart'; |
|
||||
import 'asr_service.dart'; |
|
||||
import 'tts_service.dart'; |
|
||||
|
|
||||
/// 语音交互服务 |
/// 语音交互服务抽象基类 |
||||
/// |
/// |
||||
/// 该服务提供了与Android端的VoiceInteractionService.kt通信的接口, |
/// 定义了iOS和Android平台共用的语音交互服务接口 |
||||
/// 用于管理后台语音交互服务的生命周期和接收语音交互事件 |
abstract class VoiceInteractionService { |
||||
class VoiceInteractionService extends GetxService { |
/// 事件流,用于发布语音交互相关事件 |
||||
static VoiceInteractionService get to => Get.find(); |
Stream<VoiceInteractionEvent> get eventStream; |
||||
// 方法通道和事件通道 |
|
||||
static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction'); |
|
||||
static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events'); |
|
||||
|
|
||||
// 服务状态 |
|
||||
final _isServiceRunning = false.obs; |
|
||||
bool get isServiceRunning => _isServiceRunning.value; |
|
||||
|
|
||||
// 事件流控制器 |
|
||||
StreamController<VoiceInteractionEvent>? _eventStreamController; |
|
||||
Stream<VoiceInteractionEvent>? _eventStream; |
|
||||
Stream<VoiceInteractionEvent>? get eventStream => _eventStream; |
|
||||
|
|
||||
// 事件通道状态 |
|
||||
bool _isEventChannelReady = false; |
|
||||
Completer<void>? _eventChannelReadyCompleter; |
|
||||
StreamSubscription? _eventSubscription; |
|
||||
|
|
||||
// 配置信息 |
|
||||
late String _azureSpeechKey; |
|
||||
late String _azureSpeechRegion; |
|
||||
late String _volcanoAiApiKey; |
|
||||
|
|
||||
// 初始化状态标志 |
|
||||
static bool _isInitialized = false; |
|
||||
|
|
||||
/// 构造函数 |
|
||||
VoiceInteractionService() { |
|
||||
|
|
||||
} |
|
||||
|
|
||||
/// 从环境变量加载配置 |
|
||||
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.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); |
|
||||
} |
|
||||
|
|
||||
if (_volcanoAiApiKey.isEmpty) { |
|
||||
Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY'); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 设置事件通道 |
|
||||
void _setupEventChannel() { |
|
||||
_eventChannelReadyCompleter = Completer<void>(); |
|
||||
|
|
||||
_eventSubscription = _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; |
|
||||
} |
|
||||
|
|
||||
_handleVoiceInteractionEvent(event); |
|
||||
} |
|
||||
}, onError: (error) { |
|
||||
Logger.error('语音交互事件通道错误: $error'); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
/// 创建事件流 |
|
||||
void _createEventStream() { |
|
||||
_eventStreamController = StreamController<VoiceInteractionEvent>.broadcast(); |
|
||||
_eventStream = _eventStreamController?.stream; |
|
||||
} |
|
||||
|
|
||||
/// 等待事件通道准备好 |
|
||||
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> initialize() async { |
/// |
||||
return this; |
/// 返回服务实例自身,以支持链式调用 |
||||
try { |
Future<VoiceInteractionService> initialize(); |
||||
if (_isInitialized) { |
|
||||
return this; |
/// 暂停语音交互 |
||||
} |
/// |
||||
_isInitialized = true; |
/// 停止正在进行的语音识别和TTS播放 |
||||
|
void pauseVoiceInteraction(); |
||||
|
|
||||
_loadConfig(); |
/// 唤醒语音交互 |
||||
_setupEventChannel(); |
void onWakeup(); |
||||
_createEventStream(); |
|
||||
// 检查服务是否正在运行 |
|
||||
await _checkServiceStatus(); |
|
||||
|
|
||||
// 等待事件通道准备好 |
|
||||
await _waitForEventChannel(); |
|
||||
|
|
||||
Logger.info('语音交互服务初始化完成'); |
|
||||
return this; |
|
||||
} catch (e) { |
|
||||
Logger.error('语音交互服务初始化失败: $e'); |
|
||||
return this; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 检查服务状态 |
|
||||
Future<void> _checkServiceStatus() async { |
|
||||
try { |
|
||||
final bool isRunning = await _channel.invokeMethod('isVoiceInteractionServiceRunning') ?? false; |
|
||||
_isServiceRunning.value = isRunning; |
|
||||
Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}'); |
|
||||
} catch (e) { |
|
||||
Logger.error('检查语音交互服务状态失败: $e'); |
|
||||
_isServiceRunning.value = false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 启动语音交互服务 |
|
||||
Future<bool> startService() async { |
|
||||
return true; |
|
||||
if (_isServiceRunning.value) { |
|
||||
Logger.info('语音交互服务已经在运行'); |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
final bool result = await _channel.invokeMethod('startVoiceInteractionService', { |
|
||||
'azure_speech_key': _azureSpeechKey, |
|
||||
'azure_speech_region': _azureSpeechRegion, |
|
||||
'volcano_ai_api_key': _volcanoAiApiKey, |
|
||||
}) ?? false; |
|
||||
|
|
||||
if (result) { |
|
||||
_isServiceRunning.value = true; |
|
||||
Logger.info('语音交互服务启动成功'); |
|
||||
} else { |
|
||||
Logger.error('语音交互服务启动失败'); |
|
||||
} |
|
||||
|
|
||||
return result; |
|
||||
} catch (e) { |
|
||||
Logger.error('启动语音交互服务失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 停止语音交互服务 |
|
||||
Future<bool> stopService() async { |
|
||||
if (!_isServiceRunning.value) { |
|
||||
Logger.info('语音交互服务未运行'); |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
final bool result = await _channel.invokeMethod('stopVoiceInteractionService') ?? false; |
|
||||
|
|
||||
if (result) { |
|
||||
_isServiceRunning.value = false; |
|
||||
Logger.info('语音交互服务停止成功'); |
|
||||
} else { |
|
||||
Logger.error('语音交互服务停止失败'); |
|
||||
} |
|
||||
|
|
||||
return result; |
|
||||
} catch (e) { |
|
||||
Logger.error('停止语音交互服务失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 暂停语音交互(停止TTS和语音识别,但保持服务运行) |
|
||||
Future<bool> pauseVoiceInteraction() async { |
|
||||
return true; |
|
||||
if (!_isServiceRunning.value) { |
|
||||
Logger.info('语音交互服务未运行,无法暂停'); |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
final bool result = await _channel.invokeMethod('pauseVoiceInteraction') ?? false; |
|
||||
|
|
||||
if (result) { |
|
||||
Logger.info('语音交互暂停成功'); |
|
||||
} else { |
|
||||
Logger.error('语音交互暂停失败'); |
|
||||
} |
|
||||
|
|
||||
return result; |
|
||||
} catch (e) { |
|
||||
Logger.error('暂停语音交互失败: $e'); |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 处理来自原生端的语音交互事件 |
/// 是否处于活跃状态 |
||||
void _handleVoiceInteractionEvent(dynamic event) { |
bool isActive(); |
||||
if (event is! Map || _eventStreamController == null) return; |
|
||||
|
|
||||
final Map<dynamic, dynamic> eventMap = event; |
|
||||
final String eventType = eventMap['type'] as String? ?? ''; |
|
||||
final int timestamp = eventMap['timestamp'] as int? ?? 0; |
|
||||
|
|
||||
// 添加时间戳日志,帮助调试 |
|
||||
Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}'); |
|
||||
|
|
||||
switch (eventType) { |
|
||||
case 'chatHistory': |
|
||||
final String agentId = eventMap['agentId'] as String? ?? ''; |
|
||||
final String userMessage = eventMap['userMessage'] as String? ?? ''; |
|
||||
final String assistantMessage = eventMap['assistantMessage'] as String? ?? ''; |
|
||||
|
|
||||
_eventStreamController?.add(ChatHistoryEvent( |
|
||||
agentId: agentId, |
|
||||
userMessage: userMessage, |
|
||||
assistantMessage: assistantMessage, |
|
||||
timestamp: timestamp, |
|
||||
)); |
|
||||
|
|
||||
// 保存聊天记录 |
|
||||
_saveChatHistory(agentId, userMessage, assistantMessage, timestamp); |
|
||||
|
|
||||
Logger.info('收到聊天历史事件: agentId=$agentId'); |
|
||||
break; |
|
||||
|
|
||||
case 'recognitionStarted': |
|
||||
_eventStreamController?.add(RecognitionStartedEvent( |
|
||||
timestamp: timestamp, |
|
||||
)); |
|
||||
break; |
|
||||
|
|
||||
default: |
|
||||
Logger.warning('收到未知类型的语音交互事件: $eventType'); |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@override |
/// 获取当前对话历史 |
||||
void onClose() { |
List<Map<String, String>> getMessageHistory(); |
||||
// 清理资源 |
|
||||
_eventSubscription?.cancel(); |
|
||||
_eventStreamController?.close(); |
|
||||
super.onClose(); |
|
||||
} |
|
||||
|
|
||||
/// 保存聊天记录 |
/// 获取当前服务ID |
||||
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) { |
String getServiceId(); |
||||
try { |
|
||||
// 获取ChatHistoryService实例 |
|
||||
final chatHistoryService = Get.find<ChatHistoryService>(); |
|
||||
|
|
||||
// 创建用户消息和助手消息 |
|
||||
final userMsg = Message( |
|
||||
role: 'user', |
|
||||
content: userMessage, |
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), |
|
||||
); |
|
||||
|
|
||||
final assistantMsg = Message( |
|
||||
role: 'assistant', |
|
||||
content: assistantMessage, |
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息 |
|
||||
); |
|
||||
|
|
||||
// 加载现有历史记录 |
|
||||
final existingMessages = chatHistoryService.loadHistory(agentId); |
|
||||
|
|
||||
// 添加新消息 |
|
||||
existingMessages.addAll([userMsg, assistantMsg]); |
|
||||
|
|
||||
// 保存更新后的历史记录 |
|
||||
chatHistoryService.saveHistory(agentId, existingMessages); |
|
||||
} catch (e) { |
|
||||
Logger.error('保存聊天记录失败: $e'); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 语音交互事件基类 |
|
||||
abstract class VoiceInteractionEvent { |
|
||||
final int timestamp; |
|
||||
|
|
||||
VoiceInteractionEvent({required this.timestamp}); |
|
||||
} |
|
||||
|
|
||||
/// 聊天历史事件 |
|
||||
class ChatHistoryEvent extends VoiceInteractionEvent { |
|
||||
final String agentId; |
|
||||
final String userMessage; |
|
||||
final String assistantMessage; |
|
||||
|
|
||||
ChatHistoryEvent({ |
|
||||
required this.agentId, |
|
||||
required this.userMessage, |
|
||||
required this.assistantMessage, |
|
||||
required int timestamp, |
|
||||
}) : super(timestamp: timestamp); |
|
||||
} |
|
||||
|
|
||||
/// 语音识别开始事件 |
|
||||
class RecognitionStartedEvent extends VoiceInteractionEvent { |
|
||||
RecognitionStartedEvent({ |
|
||||
required int timestamp, |
|
||||
}) : super(timestamp: timestamp); |
|
||||
} |
} |
||||
Loading…
Reference in new issue