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.
465 lines
15 KiB
465 lines
15 KiB
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 '../../core/utils/logger.dart';
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
import '../providers/agent_provider.dart';
|
|
|
|
class BackgroundAgentService extends GetxService {
|
|
static BackgroundAgentService get to => Get.find();
|
|
|
|
final VolcanoAIService _aiService;
|
|
final VolcanoTtsApiService _ttsService;
|
|
final AzureAsrService _voiceRecognitionService;
|
|
final List<Message> _messageHistory = [];
|
|
bool _isProcessing = false;
|
|
bool _isListening = false;
|
|
StreamSubscription? _recognitionSubscription;
|
|
|
|
|
|
// 当前使用的agent ID
|
|
String? _currentAgentId;
|
|
|
|
// 当前使用的agent voice
|
|
String _agentVoice = 'zh_female_meilinvyou_moon_bigtts';
|
|
|
|
// 可观察的状态
|
|
final RxBool isListening = false.obs;
|
|
final RxString recognizedText = ''.obs;
|
|
|
|
// 添加一个标志,表示是否已经识别到语音
|
|
bool _hasRecognizedSpeech = false;
|
|
|
|
// 添加一个标志,表示是否已经收到最终结果
|
|
bool _hasFinalResult = false;
|
|
|
|
// 单一的交互超时计时器 (30秒)
|
|
Timer? _interactionTimer;
|
|
|
|
// 添加一个变量来跟踪当前的AI响应流订阅
|
|
StreamSubscription? _aiResponseSubscription;
|
|
|
|
// 添加一个标志,表示是否应该取消当前的AI响应
|
|
bool _shouldCancelAiResponse = false;
|
|
|
|
// 当前系统提示词
|
|
String? _currentSystemPrompt;
|
|
|
|
BackgroundAgentService()
|
|
: _aiService = VolcanoAIService(),
|
|
_ttsService = Get.find<VolcanoTtsApiService>(),
|
|
_voiceRecognitionService = Get.find<AzureAsrService>();
|
|
|
|
// 启动或重置交互计时器
|
|
void _resetInteractionTimer() {
|
|
// 取消之前的计时器
|
|
_interactionTimer?.cancel();
|
|
|
|
// 设置30秒的超时计时器
|
|
_interactionTimer = Timer(const Duration(seconds: 30), () {
|
|
// 检查TTS是否正在播放
|
|
if (_ttsService.isPlaying) {
|
|
// 如果TTS正在播放,重置定时器
|
|
_resetInteractionTimer();
|
|
} else {
|
|
// 如果TTS不在播放,且30秒内没有检测到用户输入,退出交互
|
|
Logger.info('30秒内没有检测到用户输入,退出交互');
|
|
_endInteraction();
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
// 结束交互
|
|
Future<void> _endInteraction() async {
|
|
// 完成当前的recognitionCompleter(如果有)
|
|
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
|
|
_recognitionCompleter!.complete('');
|
|
}
|
|
|
|
_isProcessing = false;
|
|
|
|
// 播放退出提示
|
|
await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _agentVoice);
|
|
|
|
// 停止语音识别
|
|
if (_isListening) {
|
|
await stopVoiceRecognition();
|
|
Logger.info('退出交互,已停止语音识别');
|
|
}
|
|
|
|
// 清除当前系统提示词
|
|
_currentSystemPrompt = null;
|
|
// 清除当前agent ID
|
|
_currentAgentId = null;
|
|
// 重置agent voice为默认值
|
|
_agentVoice = 'zh_female_meilinvyou_moon_bigtts';
|
|
}
|
|
|
|
// 开始语音交互
|
|
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);
|
|
|
|
if (_isProcessing) {
|
|
print('已经在处理交互,忽略此次请求');
|
|
return;
|
|
}
|
|
|
|
_isProcessing = true;
|
|
|
|
try {
|
|
// 确保之前的语音识别已经停止
|
|
if (_isListening) {
|
|
await stopVoiceRecognition();
|
|
}
|
|
|
|
// 启动语音识别
|
|
await startVoiceRecognition();
|
|
|
|
// 启动交互计时器
|
|
_resetInteractionTimer();
|
|
} catch (e) {
|
|
Logger.error('启动语音交互失败: $e');
|
|
_isProcessing = false;
|
|
|
|
// 播放错误提示
|
|
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('手动停止交互,已停止语音识别');
|
|
}
|
|
|
|
// 清除处理状态
|
|
_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();
|
|
}
|
|
|
|
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 = '';
|
|
|
|
// 使用语音识别服务的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;
|
|
|
|
_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播放');
|
|
}
|
|
}
|
|
} 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();
|
|
|
|
} catch (e) {
|
|
Logger.error('启动语音识别失败: $e');
|
|
print('启动语音识别失败: $e');
|
|
_isListening = false;
|
|
isListening.value = false;
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// 处理用户输入
|
|
Future<void> _processUserInput(String userInput) async {
|
|
if (userInput.isEmpty || _currentSystemPrompt == null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 保存用户消息到历史记录
|
|
_messageHistory.add(Message(
|
|
role: 'user',
|
|
content: userInput,
|
|
timestamp: DateTime.now(),
|
|
));
|
|
|
|
// 构建用于生成回应的消息列表
|
|
final messages = [
|
|
{'role': 'system', 'content': _currentSystemPrompt!},
|
|
{'role': 'user', 'content': userInput},
|
|
];
|
|
|
|
String fullResponse = '';
|
|
|
|
Logger.info('开始生成AI响应...');
|
|
// 重置取消标志
|
|
_shouldCancelAiResponse = false;
|
|
|
|
// 创建一个本地变量来跟踪是否已取消
|
|
bool isCancelled = false;
|
|
|
|
// 使用成员变量中存储的agent voice
|
|
_ttsService.startSession(_agentVoice);
|
|
|
|
// 获取AI响应流
|
|
final responseStream = _aiService.sendMessageStream(
|
|
messages: messages,
|
|
systemPrompt: _currentSystemPrompt!,
|
|
);
|
|
|
|
// 创建一个订阅来处理响应流
|
|
_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;
|
|
},
|
|
onDone: () {
|
|
_ttsService.endSession();
|
|
// 如果已取消,不处理剩余的文本
|
|
if (!isCancelled && !_shouldCancelAiResponse) {
|
|
Logger.info('AI 响应生成完成');
|
|
}
|
|
|
|
_aiResponseSubscription = null;
|
|
|
|
// AI响应完成后,重新启动超时计时器
|
|
_resetInteractionTimer();
|
|
|
|
// 如果响应被取消,不保存到历史记录
|
|
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条,已裁剪');
|
|
}
|
|
}
|
|
);
|
|
} catch (e) {
|
|
Logger.error('处理用户输入失败: $e');
|
|
print('处理用户输入失败: $e');
|
|
|
|
// 播放错误提示,使用成员变量中存储的agent voice
|
|
await _ttsService.speak("抱歉,出现了一些问题", speaker: _agentVoice);
|
|
}
|
|
}
|
|
|
|
// 停止语音识别并返回识别的文本
|
|
Future<String> stopVoiceRecognition() async {
|
|
if (!_isListening) {
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
// 取消订阅
|
|
await _recognitionSubscription?.cancel();
|
|
_recognitionSubscription = null;
|
|
|
|
// 停止语音识别
|
|
await _voiceRecognitionService.stopContinuousRecognition();
|
|
|
|
// 获取最终识别结果
|
|
final result = recognizedText.value;
|
|
|
|
// 添加日志输出停止语音识别的最终结果
|
|
Logger.info('停止语音识别,最终结果: $result');
|
|
|
|
// 重置状态
|
|
_isListening = false;
|
|
isListening.value = false;
|
|
|
|
return result;
|
|
} catch (e) {
|
|
// 添加日志输出停止语音识别失败
|
|
Logger.error('停止语音识别失败: $e');
|
|
print('停止语音识别失败: $e');
|
|
_isListening = false;
|
|
isListening.value = false;
|
|
|
|
// 直接返回当前已识别的文本,不尝试重置
|
|
final result = recognizedText.value;
|
|
Logger.info('停止语音识别失败,使用当前识别结果: $result');
|
|
return result;
|
|
}
|
|
}
|
|
|
|
List<Message> get messageHistory => List.unmodifiable(_messageHistory);
|
|
|
|
void clearHistory() {
|
|
_messageHistory.clear();
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
_recognitionSubscription?.cancel();
|
|
_interactionTimer?.cancel();
|
|
_interactionTimer = null;
|
|
_aiResponseSubscription?.cancel();
|
|
|
|
// 停止TTS播放
|
|
if (_ttsService.isPlaying) {
|
|
_ttsService.stop();
|
|
}
|
|
|
|
super.onClose();
|
|
}
|
|
}
|