Browse Source

tts asr 0.0.3

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
d0ad858911
  1. 584
      lib/data/services/background_agent_service.dart
  2. 2
      lib/data/services/my_audio_handler.dart
  3. 300
      lib/data/services/volcano_tts_api_service.dart
  4. 6
      lib/data/services/volcano_tts_service.dart
  5. 136
      lib/modules/chat/controllers/chat_controller.dart
  6. 3
      lib/modules/chat/controllers/voice_input_controller.dart
  7. 62
      lib/modules/test/controllers/tts_test_controller.dart
  8. 16
      lib/modules/test/views/tts_test_view.dart

584
lib/data/services/background_agent_service.dart

@ -14,15 +14,12 @@ class BackgroundAgentService extends GetxService {
final VolcanoTtsApiService _ttsService;
final AzureAsrService _voiceRecognitionService;
final List<Message> _messageHistory = [];
String _pendingTtsText = '';
static const int _minTtsLength = 20;
bool _isProcessing = false;
bool _isListening = false;
StreamSubscription? _recognitionSubscription;
// TTS相关订阅
StreamSubscription? _ttsEventSubscription;
bool _isTtsSpeaking = false;
// 默认语音类型
static const String _defaultSpeaker = 'zh_female_shuangkuaisisi_moon_bigtts';
// 可观察的状态
final RxBool isListening = false.obs;
@ -31,23 +28,11 @@ class BackgroundAgentService extends GetxService {
// 添加一个标志,表示是否已经识别到语音
bool _hasRecognizedSpeech = false;
// 添加一个标志,表示是否应该继续循环交互
bool _shouldContinueInteraction = false;
// 添加一个 Completer 用于在识别到最终结果时完成
Completer<String>? _recognitionCompleter;
// 添加一个标志,表示是否已经收到最终结果
bool _hasFinalResult = false;
// 添加一个计时器,用于在一段时间没有新的识别结果时提交当前结果
Timer? _silenceTimer;
// 添加一个计时器,用于检测用户长时间没有说话
Timer? _noSpeechTimer;
// 最后一次识别到语音的时间
// DateTime? _lastSpeechTime; // Removing unused field
// 单一的交互超时计时器 (30秒)
Timer? _interactionTimer;
// 添加一个变量来跟踪当前的AI响应流订阅
StreamSubscription? _aiResponseSubscription;
@ -55,185 +40,250 @@ class BackgroundAgentService extends GetxService {
// 添加一个标志,表示是否应该取消当前的AI响应
bool _shouldCancelAiResponse = false;
// 添加计数器,用于跟踪连续无输入的次数
// int _noSpeechCount = 0; // Removing unused field
// 当前系统提示词
String? _currentSystemPrompt;
BackgroundAgentService()
: _aiService = VolcanoAIService(),
_ttsService = Get.find<VolcanoTtsApiService>(),
_voiceRecognitionService = Get.find<AzureAsrService>() {
// 设置TTS事件监听
_setupTtsEventListener();
}
// 设置TTS事件监听
void _setupTtsEventListener() {
_ttsEventSubscription?.cancel();
_ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent);
}
_voiceRecognitionService = Get.find<AzureAsrService>();
// 处理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;
}
}
// 启动无语音超时计时器
void _startNoSpeechTimer() {
// 启动或重置交互计时器
void _resetInteractionTimer() {
// 取消之前的计时器
_noSpeechTimer?.cancel();
_interactionTimer?.cancel();
// 设置30秒的超时计时器
_noSpeechTimer = Timer(const Duration(seconds: 30), () {
_interactionTimer = Timer(const Duration(seconds: 30), () {
// 检查TTS是否正在播放
if (_isTtsSpeaking) {
if (_ttsService.isPlaying) {
// 如果TTS正在播放,重置定时器
Logger.info('TTS正在播放,重置30秒超时计时器');
_startNoSpeechTimer();
_resetInteractionTimer();
} else {
// 如果TTS不在播放,且30秒内没有检测到用户输入,退出交互
Logger.info('30秒内没有检测到用户输入,退出交互');
_exitInteraction();
_endInteraction();
}
});
}
// 退出交互
Future<void> _exitInteraction() async {
// 结束交互
Future<void> _endInteraction() async {
// 完成当前的recognitionCompleter(如果有)
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.complete('');
}
// 设置标志,停止循环交互
_shouldContinueInteraction = false;
_isProcessing = false;
// 播放退出提示
try {
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
await _ttsService.synthesize("没有听到您说话,已退出语音交互", speaker);
} catch (e) {
print('播放退出提示失败: $e');
await _ttsService.speakSingle("没有听到您说话,已退出语音交互", speaker: _defaultSpeaker);
// 停止语音识别
if (_isListening) {
await stopVoiceRecognition();
Logger.info('退出交互,已停止语音识别');
}
// 清除当前系统提示词
_currentSystemPrompt = null;
}
// 处理蓝牙耳机按钮触发的交互
Future<void> handleAgentInteraction(String systemPrompt) async {
// 开始语音交互
Future<void> startAgentInteraction(String systemPrompt) async {
// 播放提示音
await _ttsService.speakSingle("我在听", speaker: _defaultSpeaker);
if (_isProcessing) {
print('已经在处理交互,忽略此次请求');
return;
}
// 确保TTS服务已连接
if (!_ttsService.isConnected.value) {
_isProcessing = true;
_currentSystemPrompt = systemPrompt;
try {
await _ttsService.connect();
// 确保之前的语音识别已经停止
if (_isListening) {
await stopVoiceRecognition();
}
// 启动语音识别
await startVoiceRecognition();
// 启动交互计时器
_resetInteractionTimer();
} catch (e) {
print('连接TTS服务失败: $e');
return;
Logger.error('启动语音交互失败: $e');
_isProcessing = false;
// 播放错误提示
await _ttsService.speakSingle("抱歉,启动语音交互失败", speaker: _defaultSpeaker);
}
}
// 设置循环交互标志为 true
_shouldContinueInteraction = true;
// 停止语音交互
Future<void> stopAgentInteraction() async {
// 取消交互计时器
_interactionTimer?.cancel();
_interactionTimer = null;
try {
// 确保之前的语音识别已经停止
// 停止TTS播放
if (_ttsService.isPlaying) {
await _ttsService.stop();
}
// 停止语音识别
if (_isListening) {
await stopVoiceRecognition();
Logger.info('手动停止交互,已停止语音识别');
}
// 循环进行交互,直到用户 30 秒没有说话或手动停止
while (_shouldContinueInteraction) {
await _processSingleInteraction(systemPrompt);
// 清除处理状态
_isProcessing = false;
_currentSystemPrompt = null;
Logger.info('手动停止交互,已结束TTS会话');
}
} finally {
// 确保在交互结束时停止语音识别
// 添加一个 Completer 用于在识别到最终结果时完成
Completer<String>? _recognitionCompleter;
// 开始语音识别
Future<void> startVoiceRecognition() async {
if (_isListening) {
await stopVoiceRecognition();
}
// 重置语音识别服务
try {
await _voiceRecognitionService.initialize(
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',
);
} catch (e) {
print('重置语音识别服务失败: $e');
}
)) {
// 初始化失败,直接抛出异常
Logger.error('语音识别服务初始化失败');
throw Exception('无法初始化语音识别服务');
}
// 开始连续识别
final success = await _voiceRecognitionService.startContinuousRecognition();
if (!success) {
Logger.error('启动连续语音识别失败');
throw Exception('无法启动语音识别');
}
// 处理单次交互
Future<void> _processSingleInteraction(String systemPrompt) async {
_isProcessing = true;
_isListening = true;
isListening.value = true;
recognizedText.value = '';
_hasRecognizedSpeech = false;
_hasFinalResult = false;
try {
// 先播放一个简短的提示音或提示语,表示开始监听
try {
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
await _ttsService.synthesize("我在听", speaker);
} catch (e) {
print('播放提示音失败: $e');
// 继续执行,不要因为提示音失败而中断整个流程
// 添加一个变量来存储完整的用户输入
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;
// 开始语音识别
String userInput = '';
// 更新可观察的识别文本
recognizedText.value = fullUserInput;
try {
// 确保之前的语音识别已经停止
if (_isListening) {
await stopVoiceRecognition();
_hasRecognizedSpeech = true;
_hasFinalResult = true;
// 重置交互计时器,因为用户刚刚说了话
_resetInteractionTimer();
_processUserInput(fullUserInput);
// 重置用户输入
fullUserInput = '';
recognizedText.value = '';
_hasRecognizedSpeech = false;
_hasFinalResult = false;
}
} else if (event.type == RecognitionEventType.recognizing) {
// 添加日志输出中间识别结果
Logger.info('语音识别中间结果: ${event.text}');
// 尝试启动语音识别
await startVoiceRecognition();
if (event.text.isNotEmpty) {
// 更新当前的中间结果,但不添加到完整输入中
recognizedText.value = fullUserInput + (fullUserInput.isEmpty ? "" : ",") + event.text;
// 创建一个 Completer 来处理语音识别完成
_recognitionCompleter = Completer<String>();
_hasRecognizedSpeech = true;
// 启动无语音超时计时器
_startNoSpeechTimer();
// 检测到用户说话,重置交互计时器
_resetInteractionTimer();
// 等待语音识别完成
userInput = await _recognitionCompleter!.future;
// 如果系统正在播放TTS或接收AI响应,检测到用户开始说话时立即中断
if (_ttsService.isPlaying && event.text.trim().isNotEmpty) {
// 停止当前TTS播放
_ttsService.stop();
// 取消无语音超时计时器
_noSpeechTimer?.cancel();
_noSpeechTimer = null;
// 设置标志,表示应该取消当前的AI响应
_shouldCancelAiResponse = true;
// 如果用户输入为空,重新启动超时计时器并返回
if (userInput.trim().isEmpty) {
_startNoSpeechTimer();
return;
// 取消当前的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) {
print('语音识别过程出错: $e');
_startNoSpeechTimer(); // 出错时也启动超时计时器
Logger.error('启动语音识别失败: $e');
print('启动语音识别失败: $e');
_isListening = false;
isListening.value = false;
rethrow;
}
}
// 处理用户输入
Future<void> _processUserInput(String userInput) async {
if (userInput.isEmpty || _currentSystemPrompt == null) {
return;
} finally {
// 清理资源,但保持语音识别状态
_recognitionCompleter = null;
_silenceTimer?.cancel();
_silenceTimer = null;
}
try {
// 保存用户消息到历史记录
_messageHistory.add(Message(
role: 'user',
@ -243,22 +293,25 @@ class BackgroundAgentService extends GetxService {
// 构建用于生成回应的消息列表
final messages = [
{'role': 'system', 'content': systemPrompt},
{'role': 'system', 'content': _currentSystemPrompt!},
{'role': 'user', 'content': userInput},
];
String fullResponse = '';
try {
Logger.info('开始生成AI响应...');
// 重置取消标志
_shouldCancelAiResponse = false;
// 创建一个本地变量来跟踪是否已取消
bool isCancelled = false;
_ttsService.startSession(_defaultSpeaker);
// 获取AI响应流
final responseStream = _aiService.sendMessageStream(
messages: messages,
systemPrompt: systemPrompt,
systemPrompt: _currentSystemPrompt!,
);
// 创建一个订阅来处理响应流
@ -272,78 +325,34 @@ class BackgroundAgentService extends GetxService {
fullResponse += chunk;
// 累积文本并处理TTS
_pendingTtsText += chunk;
// 查找最后一个完整句子的结束位置
int lastSentenceEnd = _findLastSentenceEnd(_pendingTtsText);
if (lastSentenceEnd > 0) {
// 提取完整的句子
String sentenceToSpeak = _pendingTtsText.substring(0, lastSentenceEnd + 1);
// 只有当句子长度超过最小长度时才播放
if (sentenceToSpeak.length >= _minTtsLength) {
// 直接将每个文本块传递给 TTS 接口,不进行切句或攒句处理
try {
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
_ttsService.synthesize(sentenceToSpeak, speaker);
// 使用新的 TTS API 播放语音
_ttsService.speak(chunk, speaker: _defaultSpeaker);
// Logger.info('直接播放 AI 响应块: $chunk');
} catch (e) {
print('播放TTS失败: $e');
}
// 更新待处理文本,移除已播放的部分
_pendingTtsText = _pendingTtsText.substring(lastSentenceEnd + 1);
}
Logger.error('TTS 播放出错: $e');
}
},
onError: (e) {
Logger.error('AI响应流错误: $e');
print('AI响应流错误: $e');
// 清理订阅
_aiResponseSubscription = null;
},
onDone: () {
_ttsService.endSession();
// 如果已取消,不处理剩余的文本
if (!isCancelled && !_shouldCancelAiResponse) {
// 处理剩余的文本
if (_pendingTtsText.isNotEmpty) {
try {
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
_ttsService.synthesize(_pendingTtsText, speaker);
} catch (e) {
print('播放剩余TTS失败: $e');
Logger.info('AI 响应生成完成');
}
}
}
_pendingTtsText = '';
_aiResponseSubscription = null;
// AI响应完成后,重新启动超时计时器
_startNoSpeechTimer();
}
);
// 等待响应流完成
await _aiResponseSubscription!.asFuture();
} catch (e) {
print('AI响应生成失败: $e');
// 如果AI响应失败,使用默认回复
fullResponse = '抱歉,我现在无法回答您的问题。请稍后再试。';
// 播放错误提示
try {
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
await _ttsService.synthesize(fullResponse, speaker);
} catch (_) {}
// 启动超时计时器
_startNoSpeechTimer();
} finally {
// 清理资源
_aiResponseSubscription?.cancel();
_aiResponseSubscription = null;
_shouldCancelAiResponse = false;
}
_resetInteractionTimer();
// 如果响应被取消,不保存到历史记录
if (!_shouldCancelAiResponse && fullResponse.isNotEmpty) {
@ -353,154 +362,23 @@ class BackgroundAgentService extends GetxService {
content: fullResponse,
timestamp: DateTime.now(),
));
// Logger.info('保存AI响应到历史记录,长度: ${fullResponse.length}');
}
// 限制历史记录长度
if (_messageHistory.length > 20) {
_messageHistory.removeRange(0, _messageHistory.length - 20);
}
} catch (e) {
print('交互过程出错: $e');
try {
// 使用默认语音类型
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
await _ttsService.synthesize("抱歉,出现了一些问题", speaker);
} catch (_) {}
// 发生错误时停止循环交互
_shouldContinueInteraction = false;
} finally {
_isProcessing = false;
Logger.info('历史记录超过20条,已裁剪');
}
}
// 停止循环交互
void stopContinuousInteraction() {
_shouldContinueInteraction = false;
print('手动停止循环交互');
}
// 开始语音识别
Future<void> startVoiceRecognition() async {
if (_isListening) {
await stopVoiceRecognition();
}
try {
// 确保语音识别服务已初始化
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',
)) {
// 尝试重新初始化
await Future.delayed(const Duration(milliseconds: 500));
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',
)) {
throw Exception('无法初始化语音识别服务');
}
}
// 开始连续识别
final success = await _voiceRecognitionService.startContinuousRecognition();
if (!success) {
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.completeError(Exception('无法启动语音识别'));
}
return;
}
_isListening = true;
isListening.value = true;
recognizedText.value = '';
_hasRecognizedSpeech = false;
_hasFinalResult = false;
// 使用语音识别服务的recognitionStream而不是方法的返回值
_recognitionSubscription = _voiceRecognitionService.recognitionStream?.listen((event) {
if (event.type == RecognitionEventType.finalResult) {
recognizedText.value = event.text;
if (event.text.isNotEmpty) {
_hasRecognizedSpeech = true;
_hasFinalResult = true;
// 收到最终结果,立即完成识别过程
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.complete(event.text);
}
} else {
// 如果最终结果为空,但有中间结果,使用最后的中间结果
if (_hasRecognizedSpeech && recognizedText.value.isNotEmpty) {
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.complete(recognizedText.value);
}
} else {
// 如果最终结果为空,且没有中间结果,重新启动超时计时器
_startNoSpeechTimer();
}
}
} else if (event.type == RecognitionEventType.recognizing) {
recognizedText.value = event.text;
if (event.text.isNotEmpty) {
_hasRecognizedSpeech = true;
// 检测到用户说话,重置无语音计时器
_noSpeechTimer?.cancel();
_startNoSpeechTimer();
// 如果系统正在播放TTS或接收AI响应,检测到用户开始说话时立即中断
if (_isTtsSpeaking && event.text.trim().isNotEmpty) {
_ttsService.endSession(); // 结束当前TTS会话
// 设置标志,表示应该取消当前的AI响应
_shouldCancelAiResponse = true;
// 取消当前的AI响应流订阅
_aiResponseSubscription?.cancel();
_aiResponseSubscription = null;
// 清空待处理的TTS文本
_pendingTtsText = '';
}
// 取消之前的静默计时器
_silenceTimer?.cancel();
// 取消之前的无语音超时计时器,用户正在说话
_noSpeechTimer?.cancel();
_noSpeechTimer = null;
// 设置新的静默计时器,如果 2 秒内没有新的识别结果,则认为用户已经停止说话
_silenceTimer = Timer(const Duration(seconds: 2), () {
if (_hasRecognizedSpeech && !_hasFinalResult &&
_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.complete(recognizedText.value);
}
});
}
} else if (event.type == RecognitionEventType.error) {
print('识别错误: ${event.error}');
}
}, onError: (error) {
print('语音识别流错误: $error');
_isListening = false;
isListening.value = false;
// 发生错误时完成 completer
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
_recognitionCompleter!.completeError(error);
}
});
);
} catch (e) {
print('启动语音识别失败: $e');
_isListening = false;
isListening.value = false;
rethrow;
Logger.error('处理用户输入失败: $e');
print('处理用户输入失败: $e');
// 播放错误提示
await _ttsService.speak("抱歉,出现了一些问题", speaker: _defaultSpeaker);
}
}
@ -511,14 +389,6 @@ class BackgroundAgentService extends GetxService {
}
try {
// 取消静默计时器
_silenceTimer?.cancel();
_silenceTimer = null;
// 取消无语音超时计时器
_noSpeechTimer?.cancel();
_noSpeechTimer = null;
// 取消订阅
await _recognitionSubscription?.cancel();
_recognitionSubscription = null;
@ -529,42 +399,26 @@ class BackgroundAgentService extends GetxService {
// 获取最终识别结果
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;
// 尝试强制重置语音识别服务
try {
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',
);
} catch (e) {
print('强制重置语音识别服务失败: $e');
}
return recognizedText.value; // 返回当前已识别的文本
}
// 直接返回当前已识别的文本,不尝试重置
final result = recognizedText.value;
Logger.info('停止语音识别失败,使用当前识别结果: $result');
return result;
}
int _findLastSentenceEnd(String text) {
final sentenceEnds = [
text.lastIndexOf('。'),
text.lastIndexOf('!'),
text.lastIndexOf('?'),
text.lastIndexOf('.'),
text.lastIndexOf('!'),
text.lastIndexOf('?'),
];
return sentenceEnds.reduce((max, pos) => pos > max ? pos : max);
}
List<Message> get messageHistory => List.unmodifiable(_messageHistory);
@ -576,14 +430,14 @@ class BackgroundAgentService extends GetxService {
@override
void onClose() {
_recognitionSubscription?.cancel();
_silenceTimer?.cancel();
_noSpeechTimer?.cancel();
_interactionTimer?.cancel();
_interactionTimer = null;
_aiResponseSubscription?.cancel();
_ttsEventSubscription?.cancel();
_shouldContinueInteraction = false;
// 断开TTS服务连接
_ttsService.disconnect();
// 停止TTS播放
if (_ttsService.isPlaying) {
_ttsService.stop();
}
super.onClose();
}

2
lib/data/services/my_audio_handler.dart

@ -196,7 +196,7 @@ class MyAudioHandler extends BaseAudioHandler {
print('应用在后台,使用 BackgroundAgent 处理交互');
if (_backgroundAgent != null) {
// 调用 BackgroundAgent 的语音识别和 AI 交互功能
await _backgroundAgent!.handleAgentInteraction(_systemPrompt);
await _backgroundAgent!.startAgentInteraction(_systemPrompt);
} else {
print('BackgroundAgent 未初始化,无法处理交互');
// 尝试重新初始化 BackgroundAgent

300
lib/data/services/volcano_tts_api_service.dart

@ -164,7 +164,10 @@ class VolcanoTtsApiService extends GetxController {
/// 设置音频播放器状态监听
void _setupAudioPlayerListeners() {
_audioPlayer.playerStateStream.listen((state) {
if (state.playing) {
final processingState = state.processingState;
final playing = state.playing;
if (processingState == ProcessingState.ready && playing) {
_isPlaying.value = true;
} else {
_isPlaying.value = false;
@ -173,16 +176,9 @@ class VolcanoTtsApiService extends GetxController {
_audioPlayer.processingStateStream.listen((state) {
if (state == ProcessingState.completed) {
if (kDebugMode) {
print('播放完成,处理状态: $state');
}
// 播放完成后检查播放列表是否为空
if (_playlist.length > 0) {
final currentIndex = _audioPlayer.currentIndex;
if (kDebugMode) {
print('播放列表中还有 ${_playlist.length} 个项目,当前索引: $currentIndex');
}
// 自动播放下一个项目
_advanceToNextItem();
@ -197,16 +193,9 @@ class VolcanoTtsApiService extends GetxController {
_audioPlayer.sequenceStateStream.listen((sequenceState) {
if (sequenceState == null) return;
if (kDebugMode) {
print('序列状态变化: 当前索引=${sequenceState.currentIndex}, 序列长度=${sequenceState.sequence.length}');
}
// 如果当前是最后一个项目且播放已完成,标记播放完成
if (sequenceState.currentIndex == sequenceState.sequence.length - 1 &&
_audioPlayer.processingState == ProcessingState.completed) {
if (kDebugMode) {
print('播放列表播放完成');
}
_isPlaying.value = false;
}
});
@ -221,36 +210,10 @@ class VolcanoTtsApiService extends GetxController {
_checkForNextItem(position);
}
});
// 监听播放错误
_audioPlayer.playbackEventStream.listen((event) {
if (event.processingState == ProcessingState.completed) {
// 播放完成,已在上面处理
} else if (event.processingState == ProcessingState.idle && _audioPlayer.playerState.playing == false) {
// 播放器空闲且未播放,可能是出错了
if (kDebugMode) {
print('播放出错: ${_audioPlayer.playerState.processingState}');
}
// 直接报告错误,不尝试恢复
_eventController.add({
'eventType': 'playbackError',
'error': '音频播放出错'
});
}
});
}
/// 处理播放错误 - 已不再使用,保留方法签名以避免引用错误
Future<void> _handlePlaybackError() async {
// 不再尝试自动恢复,直接报告错误
if (kDebugMode) {
print('播放出错,不尝试自动恢复');
}
}
/// 检查是否需要准备下一个音频项目
Future<void> _checkForNextItem(Duration position) async {
try {
// 只有在播放中且有下一个项目时才检查
if (!_isPlaying.value || _audioPlayer.currentIndex == null) return;
@ -280,27 +243,8 @@ class VolcanoTtsApiService extends GetxController {
final isLastItem = currentIndex >= sequenceState.sequence.length - 1;
// 如果接近结束(剩余时间小于200毫秒),准备下一个项目或完成播放
if (duration - position <= const Duration(milliseconds: 200)) {
if (!isLastItem) {
// 如果不是最后一个项目,预加载下一个
if (kDebugMode) {
print('当前音频(索引 $currentIndex)接近结束,准备下一个项目');
}
// 不再执行预加载操作,因为这可能导致播放不稳定
if (duration - position <= const Duration(milliseconds: 200) && !isLastItem) {
// 让播放器自然过渡到下一个项目
} else {
// 如果是最后一个项目,确保它能够完成播放
if (kDebugMode) {
print('最后一个音频项目(索引 $currentIndex)接近结束');
}
}
}
} catch (e) {
// 忽略错误,不影响正常播放
if (kDebugMode) {
print('检查下一个项目时发生错误: $e');
}
}
}
@ -315,23 +259,15 @@ class VolcanoTtsApiService extends GetxController {
if (sequenceState == null) {
// 如果没有序列状态,直接开始播放
if (kDebugMode) {
print('没有序列状态,从头开始播放');
}
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
_isPlaying.value = true;
return;
}
// 如果当前索引无效,从头开始播放
if (currentIndex == null || currentIndex < 0) {
if (kDebugMode) {
print('当前索引无效,从头开始播放');
}
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
_isPlaying.value = true;
return;
}
@ -340,32 +276,19 @@ class VolcanoTtsApiService extends GetxController {
// 检查是否还有下一个项目
if (nextIndex < sequenceState.sequence.length) {
if (kDebugMode) {
print('前进到下一个音频项目,索引: $nextIndex');
}
// 跳转到下一个项目并开始播放
await _audioPlayer.seek(Duration.zero, index: nextIndex);
await _audioPlayer.play();
_isPlaying.value = true;
} else {
// 已经是最后一个项目
if (kDebugMode) {
print('已经是最后一个音频项目,播放完成');
}
// 确保最后一个项目播放完成
if (_audioPlayer.position < _audioPlayer.duration!) {
await _audioPlayer.seek(_audioPlayer.duration!);
}
// 标记为未播放状态
_isPlaying.value = false;
}
} catch (e) {
if (kDebugMode) {
print('前进到下一个项目时发生错误: $e');
}
// 简单记录错误,不做复杂处理
}
}
@ -374,13 +297,12 @@ class VolcanoTtsApiService extends GetxController {
if (_isConnected) {
return true; // 已经连接
}
print('连接');
try {
isConnecting.value = true;
if (kDebugMode) {
print('正在连接到火山语音服务...');
}
// 生成连接ID
_connectionId = const Uuid().v4();
@ -390,9 +312,7 @@ class VolcanoTtsApiService extends GetxController {
if (!kIsWeb && (io.Platform.isAndroid || io.Platform.isIOS || io.Platform.isMacOS || io.Platform.isLinux || io.Platform.isWindows)) {
// 移动平台和桌面平台 - 使用IOWebSocketChannel
if (kDebugMode) {
print('使用IOWebSocketChannel连接: $uri');
}
_channel = IOWebSocketChannel.connect(
uri,
@ -407,10 +327,7 @@ class VolcanoTtsApiService extends GetxController {
// Web平台 - 使用WebSocketChannel
// 注意:在Web平台上,我们无法直接设置WebSocket头部
// 这可能会导致认证失败,需要与服务提供商确认Web平台的认证方式
if (kDebugMode) {
print('警告:在Web平台上无法设置WebSocket头部,可能导致认证失败');
print('使用WebSocketChannel连接: $uri');
}
_channel = WebSocketChannel.connect(uri);
}
@ -422,9 +339,7 @@ class VolcanoTtsApiService extends GetxController {
cancelOnError: false,
);
if (kDebugMode) {
print('WebSocket连接已建立,发送开始连接事件...');
}
// 发送开始连接事件
await _startConnection();
@ -435,23 +350,14 @@ class VolcanoTtsApiService extends GetxController {
// 设置超时
final timeout = Timer(const Duration(seconds: 10), () {
if (!completer.isCompleted) {
if (kDebugMode) {
print('等待连接响应超时');
}
completer.complete(false);
_handleError(Exception('连接超时'));
}
});
if (kDebugMode) {
print('等待连接响应...');
}
// 监听连接事件
final subscription = eventStream.listen((event) {
if (kDebugMode) {
print('收到事件: ${event['eventType']}');
}
if (event['eventType'] == 'connectionStarted') {
if (!completer.isCompleted) {
@ -479,15 +385,8 @@ class VolcanoTtsApiService extends GetxController {
if (result) {
_isConnected = true;
isConnected.value = true;
if (kDebugMode) {
print('火山语音服务连接成功');
}
} else {
if (kDebugMode) {
print('火山语音服务连接失败');
}
}
}
return result;
} catch (e) {
if (kDebugMode) {
@ -502,6 +401,7 @@ class VolcanoTtsApiService extends GetxController {
/// 开始TTS会话
Future<bool> startSession(String speaker) async {
print('开始会话');
if (!_isConnected) {
final connected = await connect();
if (!connected) return false;
@ -589,6 +489,7 @@ class VolcanoTtsApiService extends GetxController {
if (!_isSessionActive) {
return true; // 没有活跃会话
}
print('结束会话');
try {
// 发送结束会话事件
@ -630,6 +531,8 @@ class VolcanoTtsApiService extends GetxController {
/// 断开连接
Future<bool> disconnect() async {
print('断开连接');
if (!_isConnected) {
return true; // 已经断开
}
@ -686,10 +589,6 @@ class VolcanoTtsApiService extends GetxController {
/// 处理接收到的消息
void _handleMessage(dynamic message) {
try {
if (kDebugMode) {
print('收到WebSocket消息: ${message.runtimeType}');
}
if (message is! List<int>) {
_handleError(Exception('收到非二进制消息: $message'));
return;
@ -707,53 +606,30 @@ class VolcanoTtsApiService extends GetxController {
// 处理事件
final event = response['event'] as int?;
if (event != null) {
if (kDebugMode) {
print('处理事件: $event');
}
switch (event) {
case eventConnectionStarted:
if (kDebugMode) {
print('连接已建立 (eventConnectionStarted)');
}
_eventController.add({'eventType': 'connectionStarted'});
break;
case eventConnectionFailed:
if (kDebugMode) {
print('连接失败 (eventConnectionFailed): ${response['errorDetails']}');
}
_eventController.add({
'eventType': 'connectionFailed',
'error': response['errorDetails'] ?? '连接失败'
});
break;
case eventSessionStarted:
if (kDebugMode) {
print('会话已开始 (eventSessionStarted)');
}
_eventController.add({'eventType': 'sessionStarted'});
break;
case eventSessionFailed:
if (kDebugMode) {
print('会话失败 (eventSessionFailed): ${response['errorDetails']}');
}
_eventController.add({
'eventType': 'sessionFailed',
'error': response['errorDetails'] ?? '会话失败'
});
break;
case eventTtsSentenceStart:
if (kDebugMode) {
print('TTS句子开始 (eventTtsSentenceStart)');
}
_eventController.add({'eventType': 'ttsSentenceStart'});
// 准备新的音频流
_prepareAudioStream();
break;
case eventTtsSentenceEnd:
if (kDebugMode) {
print('TTS句子结束 (eventTtsSentenceEnd)');
}
_eventController.add({'eventType': 'ttsSentenceEnd'});
// 结束音频流
_finishAudioStream();
@ -763,46 +639,28 @@ class VolcanoTtsApiService extends GetxController {
final payload = response['payload'] as Uint8List?;
if (payload != null && payload.isNotEmpty) {
try {
if (kDebugMode) {
print('收到TTS音频数据 (eventTtsResponse): ${payload.length} 字节');
}
_eventController.add({'eventType': 'ttsResponse'});
_audioDataController.add(payload);
// 将音频数据添加到流中
_addAudioData(payload);
} catch (audioError) {
if (kDebugMode) {
print('处理音频数据时发生错误: $audioError');
}
// 继续处理,不中断整个流程
}
}
break;
case eventSessionFinished:
if (kDebugMode) {
print('会话已结束 (eventSessionFinished)');
}
_eventController.add({'eventType': 'sessionFinished'});
break;
case eventConnectionFinished:
if (kDebugMode) {
print('连接已关闭 (eventConnectionFinished)');
}
_eventController.add({'eventType': 'connectionFinished'});
break;
default:
if (kDebugMode) {
print('未知事件: $event');
}
_eventController.add({'eventType': 'unknown', 'eventCode': event});
break;
}
}
} catch (e) {
if (kDebugMode) {
print('处理WebSocket消息时发生异常: $e');
}
_handleError(e);
}
}
@ -812,9 +670,7 @@ class VolcanoTtsApiService extends GetxController {
try {
// 清空音频缓冲区
_audioBuffer.clear();
if (kDebugMode) {
print('准备新的音频流');
}
} catch (e) {
if (kDebugMode) {
print('准备音频流时发生错误: $e');
@ -828,9 +684,7 @@ class VolcanoTtsApiService extends GetxController {
// 将音频数据添加到缓冲区
if (data.isNotEmpty) {
_audioBuffer.add(data);
if (kDebugMode && _audioBuffer.length % 5 == 0) {
print('音频缓冲区现有 ${_audioBuffer.length} 块数据,总大小: ${_audioBuffer.fold<int>(0, (sum, data) => sum + data.length)} 字节');
}
}
} catch (e) {
if (kDebugMode) {
@ -844,10 +698,7 @@ class VolcanoTtsApiService extends GetxController {
void _finishAudioStream() {
try {
if (_audioBuffer.isNotEmpty) {
if (kDebugMode) {
final totalSize = _audioBuffer.fold<int>(0, (sum, data) => sum + data.length);
print('音频流已结束,准备播放 ${_audioBuffer.length} 块数据,总大小: $totalSize 字节');
}
// 合并所有音频数据
final totalSize = _audioBuffer.fold<int>(0, (sum, data) => sum + data.length);
@ -865,9 +716,7 @@ class VolcanoTtsApiService extends GetxController {
// 将合并后的数据添加到播放列表
_addToPlaylist(mergedData);
} else {
if (kDebugMode) {
print('音频流已结束,但没有收集到音频数据');
}
}
} catch (e) {
if (kDebugMode) {
@ -882,10 +731,6 @@ class VolcanoTtsApiService extends GetxController {
try {
if (audioData.isEmpty) return;
if (kDebugMode) {
print('将音频数据添加到播放列表,大小: ${audioData.length} 字节');
}
// 确保播放列表已初始化
if (!_playlistInitialized) {
await _initializePlaylist();
@ -894,29 +739,17 @@ class VolcanoTtsApiService extends GetxController {
// 创建音频源
final audioSource = BytesAudioSource(audioData);
// 记录当前播放状态和位置
final wasPlaying = _isPlaying.value;
final currentPosition = await _audioPlayer.position;
final currentIndex = _audioPlayer.currentIndex;
// 添加到播放列表
await _playlist.add(audioSource);
if (kDebugMode) {
print('音频已添加到播放列表,当前列表长度: ${_playlist.length}');
}
// 如果当前没有播放,开始播放
if (!_isPlaying.value) {
await _startPlayback();
} else if (wasPlaying && _audioPlayer.processingState == ProcessingState.completed) {
} else if (_audioPlayer.processingState == ProcessingState.completed) {
// 如果播放已完成但状态还没更新,手动前进到下一个
await _advanceToNextItem();
}
} catch (e) {
if (kDebugMode) {
print('添加音频到播放列表失败: $e');
}
// 直接报告错误,不尝试恢复
_eventController.add({
'eventType': 'playbackError',
@ -925,37 +758,13 @@ class VolcanoTtsApiService extends GetxController {
}
}
/// 重新初始化播放列表 - 已不再使用自动恢复功能,保留方法签名以避免引用错误
Future<void> _reinitializePlaylist() async {
if (kDebugMode) {
print('重新初始化播放列表方法已被调用,但不再执行自动恢复');
}
// 直接报告错误
_eventController.add({
'eventType': 'playbackError',
'error': '播放列表初始化失败'
});
// 标记为未初始化
_playlistInitialized = false;
throw Exception('播放列表初始化失败,不再尝试自动恢复');
}
/// 开始播放收集到的音频数据
Future<void> _startPlayback() async {
try {
if (!_playlistInitialized || _playlist.length == 0) {
if (kDebugMode) {
print('播放列表为空或未初始化,无法播放');
}
return;
}
if (kDebugMode) {
print('开始播放音频列表,列表长度: ${_playlist.length}');
}
// 检查当前播放状态
final processingState = _audioPlayer.processingState;
@ -967,32 +776,18 @@ class VolcanoTtsApiService extends GetxController {
nextIndex = math.min(_audioPlayer.currentIndex! + 1, _playlist.length - 1);
}
if (kDebugMode) {
print('播放已完成,跳转到索引 $nextIndex');
}
// 跳转到下一个项目
await _audioPlayer.seek(Duration.zero, index: nextIndex);
}
// 开始播放
await _audioPlayer.play();
// 更新播放状态
_isPlaying.value = true;
} catch (e) {
if (kDebugMode) {
print('开始播放时发生错误: $e');
}
// 直接报告错误,不尝试恢复
_eventController.add({
'eventType': 'playbackError',
'error': '开始播放失败: $e'
});
// 更新播放状态
_isPlaying.value = false;
}
}
@ -1017,9 +812,7 @@ class VolcanoTtsApiService extends GetxController {
_eventController.add({'eventType': 'disconnected'});
if (kDebugMode) {
print('火山语音WebSocket连接已关闭');
}
}
/// 解析响应
@ -1490,19 +1283,48 @@ class VolcanoTtsApiService extends GetxController {
// 这个方法在当前实现中不需要特殊处理
}
/// 重新初始化音频播放器 - 已不再使用自动恢复功能,保留方法签名以避免引用错误
Future<void> _reinitializeAudioPlayer() async {
if (kDebugMode) {
print('重新初始化音频播放器方法已被调用,但不再执行自动恢复');
/// 单句合成辅助函数
///
/// 一次性完成连接、会话创建、合成和播放的过程
/// 适合单句或少量文本的快速合成场景
///
/// [text] 要合成的文本
/// [speaker] 发音人,如果为null则使用默认发音人
/// [autoDisconnect] 合成完成后是否自动断开连接,默认为false
///
/// 返回合成是否成功
Future<bool> speakSingle(String text, {String? speaker}) async {
if (text.isEmpty) return false;
try {
// 使用默认发音人
final actualSpeaker = speaker ?? defaultSpeaker;
// 1. 确保连接
if (!_isConnected) {
final connected = await connect();
if (!connected) return false;
}
// 直接报告错误
_eventController.add({
'eventType': 'playbackError',
'error': '音频播放器初始化失败'
});
// 2. 确保会话已开始
if (!_isSessionActive) {
final sessionStarted = await startSession(actualSpeaker);
if (!sessionStarted) return false;
}
throw Exception('音频播放器初始化失败,不再尝试自动恢复');
// 3. 合成文本
isSynthesizing.value = true;
final success = await synthesize(text, actualSpeaker);
// 不再自动断开连接,让调用者决定何时结束会话和断开连接
await endSession();
return success;
} catch (e) {
_handleError(e);
return false;
}
}
}

6
lib/data/services/volcano_tts_service.dart

@ -228,10 +228,8 @@ class VolcanoTtsService extends GetxService {
return _voiceType;
}
/// 检查是否正在播放(简化版,直接返回true)
bool isActuallyPlaying() {
return true;
}
/// 检查是否正在播放
bool get isPlaying => true;
/// 检查是否正在合成(简化版,直接返回true)
bool isActuallySynthesizing() {

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

@ -80,7 +80,29 @@ class ChatController extends GetxController {
// 如果TTS正在播放,先停止它
if (_isTtsSpeaking) {
try {
// 立即停止当前声音播放
_ttsService.stop();
// 结束当前TTS会话
_ttsService.endSession();
_isTtsSpeaking = false;
} catch (e) {
debugPrint('停止TTS播放失败: $e');
}
}
// 如果正在处理AI响应,中断处理
if (isLoading.value) {
isLoading.value = false;
// 清除当前正在生成的消息
if (_currentAssistantMessage != null) {
// 如果消息内容为空,则移除该消息
if (_currentAssistantMessage!.content.isEmpty) {
messages.remove(_currentAssistantMessage);
}
_currentAssistantMessage = null;
}
currentStreamMessage.value = '';
}
isVoiceInputVisible.value = true;
@ -125,6 +147,33 @@ class ChatController extends GetxController {
if (!isVoiceConnecting.value && isVoiceInputVisible.value) {
// 不再需要禁用TTS,因为已启用回声消除
// 当用户开始说话时,停止当前TTS播放
if (_isTtsSpeaking) {
try {
// 立即停止当前声音播放
_ttsService.stop();
// 结束当前TTS会话
_ttsService.endSession();
_isTtsSpeaking = false;
} catch (e) {
debugPrint('停止TTS播放失败: $e');
}
}
// 如果正在处理AI响应,中断处理
if (isLoading.value) {
isLoading.value = false;
// 清除当前正在生成的消息
if (_currentAssistantMessage != null) {
// 如果消息内容为空,则移除该消息
if (_currentAssistantMessage!.content.isEmpty) {
messages.remove(_currentAssistantMessage);
}
_currentAssistantMessage = null;
}
currentStreamMessage.value = '';
}
isRecording.value = true;
}
}
@ -188,6 +237,18 @@ class ChatController extends GetxController {
void handleRecognizing(String text) {
if (text.isEmpty) return;
// 如果TTS正在播放,确保停止它
if (_isTtsSpeaking) {
try {
// 立即停止当前声音播放
_ttsService.stop();
// 不需要每次都结束会话,只需要停止当前播放
_isTtsSpeaking = false;
} catch (e) {
debugPrint('停止TTS播放失败: $e');
}
}
// 更新识别中的文本
recordingText.value = text;
@ -282,6 +343,13 @@ class ChatController extends GetxController {
bool ttsSessionStarted = false;
const speaker = 'zh_female_shuangkuaisisi_moon_bigtts';
// 创建一个标志,用于跟踪流是否应该继续处理
bool shouldContinueProcessing = true;
// 创建监听器,用于检测用户交互
StreamSubscription? voiceInputSubscription;
StreamSubscription? voicePanelSubscription;
try {
isLoading.value = true;
@ -305,12 +373,52 @@ class ChatController extends GetxController {
ttsSessionStarted = await _ttsService.startSession(speaker);
}
// 设置监听器,检测用户是否开始说话
voiceInputSubscription = isRecording.listen((isRecordingNow) {
if (isRecordingNow) {
// 用户开始说话,设置标志为false
shouldContinueProcessing = false;
// 如果TTS正在播放,立即停止
if (_isTtsSpeaking) {
try {
_ttsService.stop();
_isTtsSpeaking = false;
} catch (e) {
debugPrint('停止TTS播放失败: $e');
}
}
}
});
// 设置监听器,检测用户是否打开语音输入面板
voicePanelSubscription = isVoiceInputVisible.listen((isVisible) {
if (isVisible) {
// 用户打开语音输入面板,设置标志为false
shouldContinueProcessing = false;
// 如果TTS正在播放,立即停止
if (_isTtsSpeaking) {
try {
_ttsService.stop();
_isTtsSpeaking = false;
} catch (e) {
debugPrint('停止TTS播放失败: $e');
}
}
}
});
// 处理AI流式响应
await for (final chunk in _aiService.sendMessageStream(
messages: messages.map((m) => {'role': m.role, 'content': m.content}).toList(),
systemPrompt: systemPrompt,
)) {
if (_isDisposed) break;
// 检查是否应该继续处理
if (_isDisposed || !shouldContinueProcessing) {
// 用户开始说话或控制器已销毁,中止流处理
break;
}
// 更新UI
currentStreamMessage.value += chunk;
@ -346,6 +454,10 @@ class ChatController extends GetxController {
}
}
} finally {
// 取消监听器
voiceInputSubscription?.cancel();
voicePanelSubscription?.cancel();
// 重置状态
if (!_isDisposed) {
_currentAssistantMessage = null;
@ -607,12 +719,32 @@ class ChatController extends GetxController {
// 停止语音输入和TTS
stopVoiceInput();
// 确保TTS完全停止并清理资源
if (isTtsEnabled.value) {
try {
_ttsService.stop();
// 停止当前TTS播放并清除未播放的数据
_ttsService.endSession();
_ttsEventSubscription?.cancel();
// 确保断开TTS连接,释放所有资源
_ttsService.disconnect();
} catch (e) {
debugPrint('关闭TTS服务时出错: $e');
}
}
// 取消TTS事件订阅
if (_ttsEventSubscription != null) {
_ttsEventSubscription!.cancel();
_ttsEventSubscription = null;
}
// 重置状态
currentStreamMessage.value = '';
_currentAssistantMessage = null;
_isTtsSpeaking = false;
// 释放控制器
messageController.dispose();

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

@ -158,8 +158,9 @@ class VoiceInputController extends GetxController {
isUserSpeaking.value = true;
// 如果系统正在播放TTS,检测到用户开始说话时立即中断
if (_isTtsSpeaking && event.text.trim().isNotEmpty) {
if (event.text.trim().isNotEmpty) {
print('检测到用户开始说话,中断TTS播放');
_ttsService.stop();
_ttsService.endSession(); // 结束当前TTS会话
}

62
lib/modules/test/controllers/tts_test_controller.dart

@ -13,6 +13,12 @@ class TtsTestController extends GetxController {
final isLoading = false.obs;
final statusMessage = '未连接'.obs;
// 添加实际音频播放状态
final isAudioPlaying = false.obs;
// 播放状态检查定时器
Timer? _playbackCheckTimer;
// 当前播放的索引
final currentPlayingIndex = RxInt(-1);
@ -33,6 +39,7 @@ class TtsTestController extends GetxController {
// 初始化UI状态
isSpeaking.value = false;
isAudioPlaying.value = false;
errorMessage.value = '';
isLoading.value = false;
@ -43,6 +50,7 @@ class TtsTestController extends GetxController {
} else {
statusMessage.value = '未连接';
isSpeaking.value = false;
isAudioPlaying.value = false;
}
});
@ -53,7 +61,7 @@ class TtsTestController extends GetxController {
// 监听TTS播放状态
ever(_ttsService.isPlaying.obs, (bool playing) {
isSpeaking.value = playing;
isAudioPlaying.value = playing;
if (!playing && currentPlayingIndex.value >= 0) {
// 如果播放结束,重置当前播放索引
currentPlayingIndex.value = -1;
@ -65,6 +73,9 @@ class TtsTestController extends GetxController {
// 连接到TTS服务
_connectToTtsService();
// 启动播放状态检查定时器
_startPlaybackCheckTimer();
}
// 设置事件监听
@ -154,12 +165,14 @@ class TtsTestController extends GetxController {
/// 播放单条文本
Future<void> speakText(int index) async {
if (index < 0 || index >= speechTexts.length || isSpeaking.value) {
if (index < 0 || index >= speechTexts.length || isAudioPlaying.value) {
return;
}
try {
isLoading.value = true;
isSpeaking.value = true;
isAudioPlaying.value = true;
errorMessage.value = '';
currentPlayingIndex.value = index;
@ -177,14 +190,12 @@ class TtsTestController extends GetxController {
// 播放文本
await _ttsService.speak(text, speaker: speaker);
// 等待播放完成
while (_ttsService.isActuallyPlaying()) {
await Future.delayed(const Duration(milliseconds: 100));
}
// 结束会话
await _ttsService.endSession();
// 确保播放列表被清空
await _ttsService.stop();
} catch (e) {
errorMessage.value = '播放出错';
if (kDebugMode) {
@ -192,19 +203,44 @@ class TtsTestController extends GetxController {
}
} finally {
isLoading.value = false;
isSpeaking.value = false;
isAudioPlaying.value = false;
currentPlayingIndex.value = -1;
}
}
// 启动播放状态检查定时器
void _startPlaybackCheckTimer() {
_playbackCheckTimer?.cancel();
_playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
// 更新实际播放状态
final actuallyPlaying = _ttsService.isPlaying;
isAudioPlaying.value = actuallyPlaying;
// 如果不再播放,确保重置状态
if (!actuallyPlaying && isSpeaking.value) {
// 检查是否真的播放完成了
if (!_ttsService.isSynthesizing.value && currentPlayingIndex.value >= 0) {
// 播放已完成,重置状态
isSpeaking.value = false;
currentPlayingIndex.value = -1;
// 更新UI
update();
}
}
});
}
/// 连续播放所有文本
Future<void> speakContinuousTexts() async {
if (isSpeaking.value || isLoading.value) {
if (isAudioPlaying.value || isLoading.value) {
return;
}
try {
isLoading.value = true;
isSpeaking.value = true;
isAudioPlaying.value = true;
errorMessage.value = '';
// 确保TTS服务已连接
@ -236,6 +272,7 @@ class TtsTestController extends GetxController {
// 结束会话
await _ttsService.endSession();
statusMessage.value = '播放完成';
} catch (e) {
@ -254,6 +291,11 @@ class TtsTestController extends GetxController {
currentPlayingIndex.value = -1;
isSpeaking.value = false;
isLoading.value = false;
// 确保最终状态正确
isAudioPlaying.value = false;
// 强制更新UI
update();
}
}
@ -281,6 +323,7 @@ class TtsTestController extends GetxController {
} finally {
isSpeaking.value = false;
isLoading.value = false;
isAudioPlaying.value = false;
}
}
@ -289,6 +332,9 @@ class TtsTestController extends GetxController {
// 取消事件订阅
_eventSubscription?.cancel();
// 取消播放状态检查定时器
_playbackCheckTimer?.cancel();
// 断开TTS服务连接
_ttsService.disconnect();

16
lib/modules/test/views/tts_test_view.dart

@ -44,17 +44,17 @@ class TtsTestView extends GetView<TtsTestController> {
children: [
Obx(() {
return ElevatedButton.icon(
onPressed: controller.isSpeaking.value || controller.isLoading.value
onPressed: controller.isAudioPlaying.value || controller.isLoading.value
? null
: () => controller.speakContinuousTexts(),
icon: controller.isSpeaking.value
icon: controller.isAudioPlaying.value
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2)
)
: const Icon(Icons.playlist_play),
label: Text(controller.isSpeaking.value ? '播放中...' : '全部播放'),
label: Text(controller.isAudioPlaying.value ? '播放中...' : '全部播放'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
@ -62,7 +62,7 @@ class TtsTestView extends GetView<TtsTestController> {
);
}),
Obx(() => ElevatedButton.icon(
onPressed: controller.isSpeaking.value
onPressed: controller.isAudioPlaying.value
? () => controller.stopSpeaking()
: null,
icon: const Icon(Icons.stop),
@ -103,7 +103,7 @@ class TtsTestView extends GetView<TtsTestController> {
itemCount: controller.speechTexts.length,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final isPlaying = controller.isSpeaking.value &&
final isPlaying = controller.isAudioPlaying.value &&
controller.currentPlayingIndex.value == index;
return ListTile(
@ -130,10 +130,10 @@ class TtsTestView extends GetView<TtsTestController> {
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
onTap: controller.isSpeaking.value || controller.isLoading.value
onTap: controller.isAudioPlaying.value || controller.isLoading.value
? null
: () => controller.speakText(index),
enabled: !(controller.isSpeaking.value || controller.isLoading.value),
enabled: !(controller.isAudioPlaying.value || controller.isLoading.value),
tileColor: isPlaying ? Colors.blue.withOpacity(0.1) : null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
@ -171,7 +171,7 @@ class TtsTestView extends GetView<TtsTestController> {
style: TextStyle(
color: controller.isLoading.value
? Colors.orange
: controller.isSpeaking.value
: controller.isAudioPlaying.value
? Colors.green
: Colors.grey,
fontWeight: FontWeight.bold,

Loading…
Cancel
Save