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.
348 lines
11 KiB
348 lines
11 KiB
import 'dart:async';
|
|
import 'dart:math';
|
|
import 'package:get/get.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import '../../../data/services/azure_asr_service.dart';
|
|
import '../../../data/services/volcano_tts_api_service.dart';
|
|
import '../../../core/utils/logger.dart';
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
|
|
class VoiceInputController extends GetxController {
|
|
// Observable states
|
|
final isConnecting = true.obs;
|
|
final isMuted = false.obs;
|
|
final isRecording = false.obs;
|
|
final recognizedText = ''.obs;
|
|
|
|
// 添加一个新的可观察状态,表示用户是否正在说话
|
|
final isUserSpeaking = false.obs;
|
|
|
|
// 语音识别服务
|
|
late final AzureAsrService _voiceService;
|
|
late final VolcanoTtsApiService _ttsService;
|
|
|
|
// 连续识别相关
|
|
StreamSubscription? _recognitionSubscription;
|
|
|
|
// TTS相关订阅
|
|
StreamSubscription? _ttsEventSubscription;
|
|
bool _isTtsSpeaking = false;
|
|
|
|
// 添加一个标志,表示是否已经识别到语音
|
|
bool _hasRecognizedSpeech = false;
|
|
|
|
// 添加一个 Completer 用于在识别到最终结果时完成
|
|
Completer<String>? _recognitionCompleter;
|
|
|
|
// 添加一个标志,表示是否已经收到最终结果
|
|
bool _hasFinalResult = false;
|
|
|
|
// 添加一个计时器,用于定期更新用户说话状态
|
|
Timer? _speakingCheckTimer;
|
|
|
|
// 最后一次识别到语音的时间
|
|
DateTime? _lastSpeechTime;
|
|
|
|
// Callbacks
|
|
final Function(String) onRecordingResult;
|
|
final VoidCallback onClosePanel;
|
|
final Function(String) onRecognizing;
|
|
|
|
VoiceInputController({
|
|
required this.onRecordingResult,
|
|
required this.onClosePanel,
|
|
required this.onRecognizing,
|
|
});
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_initializeVoiceService();
|
|
_ttsService = Get.find<VolcanoTtsApiService>();
|
|
|
|
// 设置TTS事件监听
|
|
_setupTtsEventListener();
|
|
|
|
// 启动定期检查用户是否正在说话的计时器
|
|
_startSpeakingCheckTimer();
|
|
}
|
|
|
|
// 设置TTS事件监听
|
|
void _setupTtsEventListener() {
|
|
_ttsEventSubscription?.cancel();
|
|
_ttsEventSubscription = _ttsService.eventStream.listen(_handleTtsEvent);
|
|
}
|
|
|
|
// 处理TTS事件
|
|
void _handleTtsEvent(Map<String, dynamic> event) {
|
|
final eventType = event['eventType'];
|
|
|
|
switch (eventType) {
|
|
case 'ttsSentenceStart':
|
|
_isTtsSpeaking = true;
|
|
break;
|
|
case 'ttsSentenceEnd':
|
|
case 'sessionFinished':
|
|
case 'error':
|
|
_isTtsSpeaking = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Future<void> _initializeVoiceService() async {
|
|
try {
|
|
// 获取语音识别服务实例
|
|
_voiceService = Get.find<AzureAsrService>();
|
|
|
|
// 初始化语音识别服务
|
|
await _voiceService.initialize(
|
|
subscriptionKey: dotenv.env['AZURE_ASR_SUBSCRIPTION_KEY'] ?? '',
|
|
serviceRegion: dotenv.env['AZURE_ASR_SERVICE_REGION'] ?? 'eastasia',
|
|
language: dotenv.env['AZURE_ASR_LANGUAGE'] ?? 'zh-CN',
|
|
);
|
|
|
|
// 初始化完成后更新状态
|
|
isConnecting.value = false;
|
|
|
|
// 自动开始连续语音识别
|
|
await startContinuousRecognition();
|
|
} catch (e) {
|
|
Logger.error('初始化语音识别服务失败', e);
|
|
Get.snackbar(
|
|
'Error',
|
|
'初始化语音识别服务失败: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> startContinuousRecognition() async {
|
|
if (isConnecting.value) return;
|
|
if (isRecording.value) return; // 已经在录音中
|
|
|
|
try {
|
|
isRecording.value = true;
|
|
recognizedText.value = ''; // 清空识别文本,但不再在面板中显示
|
|
_hasRecognizedSpeech = false;
|
|
_hasFinalResult = false;
|
|
|
|
// 创建一个 Completer 来处理语音识别完成
|
|
_recognitionCompleter = Completer<String>();
|
|
|
|
// 开始连续语音识别
|
|
final success = await _voiceService.startContinuousRecognition();
|
|
if (!success) {
|
|
isRecording.value = false;
|
|
throw Exception('无法启动语音识别');
|
|
}
|
|
|
|
// 订阅识别事件流
|
|
_recognitionSubscription = _voiceService.recognitionStream?.listen((event) {
|
|
switch (event.type) {
|
|
case RecognitionEventType.recognizing:
|
|
// 不再更新面板中的识别文本,而是通过回调传递给ChatController
|
|
if (event.text.isNotEmpty) {
|
|
_hasRecognizedSpeech = true;
|
|
_lastSpeechTime = DateTime.now();
|
|
|
|
// 设置用户正在说话状态
|
|
isUserSpeaking.value = true;
|
|
|
|
// 如果系统正在播放TTS,检测到用户开始说话时立即中断
|
|
if (event.text.trim().isNotEmpty) {
|
|
print('检测到用户开始说话,中断TTS播放');
|
|
_ttsService.stop();
|
|
_ttsService.endSession(); // 结束当前TTS会话
|
|
}
|
|
}
|
|
|
|
// 调用识别中回调,将中间结果传递给ChatController
|
|
onRecognizing(event.text);
|
|
break;
|
|
|
|
case RecognitionEventType.finalResult:
|
|
// 更新最终识别结果
|
|
_hasFinalResult = true;
|
|
|
|
// 重置用户说话状态
|
|
isUserSpeaking.value = false;
|
|
|
|
if (event.text.isNotEmpty) {
|
|
// 不再更新面板中的识别文本
|
|
_hasRecognizedSpeech = true;
|
|
_lastSpeechTime = DateTime.now();
|
|
|
|
// 调用最终结果回调,发送识别到的句子
|
|
onRecordingResult(event.text);
|
|
|
|
// 清空识别状态,准备下一句,但不停止识别
|
|
_hasRecognizedSpeech = false;
|
|
_hasFinalResult = false;
|
|
|
|
// 完成当前识别,但不停止连续识别
|
|
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
|
|
_recognitionCompleter!.complete(event.text);
|
|
}
|
|
} else {
|
|
// 如果最终结果为空,但之前有中间结果,仍然标记为有语音
|
|
if (_hasRecognizedSpeech) {
|
|
print('最终识别结果为空,但之前有中间结果,使用最后的中间结果');
|
|
|
|
// 清空识别状态,准备下一句,但不停止识别
|
|
_hasRecognizedSpeech = false;
|
|
_hasFinalResult = false;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case RecognitionEventType.error:
|
|
print('语音识别错误: ${event.error}');
|
|
Get.snackbar(
|
|
'Error',
|
|
'语音识别错误: ${event.error}',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
// 尝试重新启动识别
|
|
restartRecognition();
|
|
break;
|
|
|
|
case RecognitionEventType.sessionStopped:
|
|
// 会话结束,尝试重新启动
|
|
isRecording.value = false;
|
|
restartRecognition();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}, onError: (error) {
|
|
print('语音识别流错误: $error');
|
|
Get.snackbar(
|
|
'Error',
|
|
'语音识别流错误: $error',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
isRecording.value = false;
|
|
// 尝试重新启动识别
|
|
restartRecognition();
|
|
});
|
|
|
|
} catch (e) {
|
|
print('语音识别失败: $e');
|
|
Get.snackbar(
|
|
'Error',
|
|
'启动语音识别失败: $e',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
isRecording.value = false;
|
|
} finally {
|
|
// 清理资源
|
|
if (_recognitionCompleter != null && !_recognitionCompleter!.isCompleted) {
|
|
_recognitionCompleter!.complete('');
|
|
}
|
|
_recognitionCompleter = null;
|
|
}
|
|
}
|
|
|
|
// 重新启动识别
|
|
Future<void> restartRecognition() async {
|
|
try {
|
|
// 先停止当前识别
|
|
await stopRecording();
|
|
// 延迟一下再重新启动
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
// 重新启动连续识别
|
|
await startContinuousRecognition();
|
|
} catch (e) {
|
|
print('重新启动语音识别失败: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> stopRecording() async {
|
|
if (!isRecording.value) return;
|
|
|
|
try {
|
|
// 取消事件订阅
|
|
await _recognitionSubscription?.cancel();
|
|
_recognitionSubscription = null;
|
|
|
|
// 停止连续识别
|
|
if (_voiceService.isContinuousRecognitionActive()) {
|
|
await _voiceService.stopContinuousRecognition();
|
|
}
|
|
|
|
isRecording.value = false;
|
|
_hasRecognizedSpeech = false;
|
|
_hasFinalResult = false;
|
|
} catch (e) {
|
|
print('停止语音识别失败: $e');
|
|
isRecording.value = false;
|
|
}
|
|
}
|
|
|
|
void toggleMute() {
|
|
// 只切换静音状态,不再影响录音
|
|
// 因为已启用回声消除,不需要在录音时停止TTS
|
|
isMuted.value = !isMuted.value;
|
|
}
|
|
|
|
// 手动开始录音(用户点击按钮)
|
|
void startRecording() {
|
|
if (!isRecording.value) {
|
|
startContinuousRecognition();
|
|
}
|
|
}
|
|
|
|
// 启动定期检查用户是否正在说话的计时器
|
|
void _startSpeakingCheckTimer() {
|
|
_speakingCheckTimer?.cancel();
|
|
_speakingCheckTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
|
|
_checkIfUserIsSpeaking();
|
|
});
|
|
}
|
|
|
|
// 检查用户是否正在说话
|
|
void _checkIfUserIsSpeaking() {
|
|
// 如果有最近的识别文本,且距离现在不超过1秒,认为用户正在说话
|
|
if (_hasRecognizedSpeech && !_hasFinalResult && _lastSpeechTime != null) {
|
|
final now = DateTime.now();
|
|
final timeSinceLastSpeech = now.difference(_lastSpeechTime!);
|
|
|
|
// 如果最近1秒内有语音识别,认为用户正在说话
|
|
if (timeSinceLastSpeech.inMilliseconds < 1000) {
|
|
isUserSpeaking.value = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 默认情况下,如果正在录音但没有最近的语音识别,随机生成一个说话状态
|
|
// 保持这个功能,让麦克风图标有动画效果
|
|
if (isRecording.value) {
|
|
// 随机生成一个值,有20%的概率认为用户正在说话
|
|
isUserSpeaking.value = Random().nextDouble() < 0.2;
|
|
} else {
|
|
isUserSpeaking.value = false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
// 停止录音
|
|
stopRecording();
|
|
|
|
// 取消计时器
|
|
_speakingCheckTimer?.cancel();
|
|
|
|
// 取消TTS事件订阅
|
|
_ttsEventSubscription?.cancel();
|
|
|
|
// 重置状态
|
|
isConnecting.value = false;
|
|
isMuted.value = false;
|
|
isRecording.value = false;
|
|
recognizedText.value = '';
|
|
isUserSpeaking.value = false;
|
|
|
|
super.onClose();
|
|
}
|
|
}
|