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.

407 lines
11 KiB

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:realtime/realtime.dart';
import '../../../core/utils/logger.dart';
class RealtimeController extends GetxController with GetTickerProviderStateMixin {
static const int _sampleRate = 16000;
// Realtime服务实例
final RealtimeService _realtimeService = RealtimeService();
// 音频振幅值(RMS)
final RxDouble audioAmplitude = 0.0.obs;
// 连接状态
final RxBool isConnected = false.obs;
final RxBool isConnecting = false.obs;
// 语音状态
final RxBool isListening = false.obs;
final RxBool isSpeaking = false.obs;
// UI状态
final RxString currentStatus = ''.obs; // 移除默认文字
final RxString userSpeechText = ''.obs;
final RxBool isUserSpeechFinal = true.obs;
final RxList<String> aiResponseMessages = <String>[].obs;
final RxBool isSubtitleMode = true.obs;
// 字幕显示用的最新消息
final RxString latestUserMessage = ''.obs;
final RxString latestAiMessage = ''.obs;
// 实时字幕更新用的临时消息
final RxString partialUserMessage = ''.obs;
final RxString streamingAiMessage = ''.obs;
// 状态监听
StreamSubscription<RealtimeEvent>? _eventSubscription;
@override
void onInit() {
super.onInit();
_setupEventListener();
// 自动开始连接
WidgetsBinding.instance.addPostFrameCallback((_) {
_autoConnect();
});
}
@override
void onClose() {
_cleanup();
super.onClose();
}
/// 设置事件监听器
void _setupEventListener() {
_eventSubscription = _realtimeService.eventStream.listen(
(event) {
_handleRealtimeEvent(event);
},
onError: (error) {
Logger.error('Realtime事件流错误: $error');
},
);
}
/// 处理Realtime事件
void _handleRealtimeEvent(RealtimeEvent event) {
switch (event.type) {
case 'connectionStatusChanged':
_handleConnectionStatusChanged(event.data);
break;
case 'voiceStatusChanged':
_handleVoiceStatusChanged(event.data);
break;
case 'textReceived':
_handleTextReceived(event.data);
break;
case 'audioRms':
_handleAudioRms(event.data);
break;
case 'error':
_handleError(event.data);
break;
}
}
/// 自动连接
Future<void> _autoConnect() async {
currentStatus.value = ''; // 不显示任何文字
await connect();
}
/// 处理连接状态变化
void _handleConnectionStatusChanged(String status) {
switch (status) {
case 'connected':
isConnected.value = true;
isConnecting.value = false;
currentStatus.value = ''; // 不显示任何文字
// 连接成功后自动开始录音
Timer(const Duration(milliseconds: 500), () {
startListening();
});
break;
case 'connecting':
isConnecting.value = true;
currentStatus.value = ''; // 不显示任何文字
break;
case 'disconnected':
isConnected.value = false;
isConnecting.value = false;
currentStatus.value = ''; // 不显示任何文字
_resetState();
break;
case 'error':
isConnected.value = false;
isConnecting.value = false;
currentStatus.value = '连接失败,请检查网络'; // 保留错误提示
_resetState();
Logger.error('Vocode服务器连接错误');
break;
}
}
/// 处理语音状态变化
void _handleVoiceStatusChanged(String status) {
switch (status) {
case 'recording':
isListening.value = true;
isSpeaking.value = false;
currentStatus.value = ''; // 不显示任何文字
break;
case 'processing':
isListening.value = false;
currentStatus.value = ''; // 不显示任何文字
break;
case 'playing':
isListening.value = false;
isSpeaking.value = true;
currentStatus.value = ''; // 不显示任何文字
break;
case 'idle':
isListening.value = false;
isSpeaking.value = false;
currentStatus.value = ''; // 不显示任何文字
// 空闲时音频振幅归零
audioAmplitude.value = 0.0;
break;
}
}
/// 处理收到的文本消息 - 标准Vocode WebSocket协议
void _handleTextReceived(dynamic payload) {
if (payload is String) {
try {
// 解析JSON消息
final Map<String, dynamic> json = jsonDecode(payload);
final String messageType = json['type'] ?? '';
switch (messageType) {
case 'websocket_transcript':
// 处理统一的文本消息格式
final String text = json['text'] ?? '';
final String sender = json['sender'] ?? '';
final bool isFinal = json['is_final'] ?? false;
// final dynamic timestamp = json['timestamp']; // 保留供未来使用
if (sender.toLowerCase() == 'human') {
// 用户语音识别消息
if (isFinal) {
// ASR最终结果
if (text.isNotEmpty) {
latestUserMessage.value = text;
userSpeechText.value = text;
isUserSpeechFinal.value = true;
}
// 清空部分消息
partialUserMessage.value = '';
} else {
// ASR实时识别
if (text.isNotEmpty) {
partialUserMessage.value = text;
isUserSpeechFinal.value = false;
}
}
} else if (sender.toLowerCase() == 'bot') {
// AI回复消息
if (isFinal) {
// LLM回复完成标识 (text为空)
if (streamingAiMessage.value.isNotEmpty) {
// 将流式消息确认为最终消息
latestAiMessage.value = streamingAiMessage.value;
if (aiResponseMessages.isNotEmpty) {
aiResponseMessages[aiResponseMessages.length - 1] = streamingAiMessage.value;
} else {
aiResponseMessages.add(streamingAiMessage.value);
}
streamingAiMessage.value = '';
}
} else {
// LLM流式回复
if (text.isNotEmpty) {
streamingAiMessage.value += text;
}
}
}
break;
case 'websocket_ready':
// 连接就绪
break;
case 'websocket_stop':
// 会话结束
break;
case 'websocket_start':
// 会话开始
break;
default:
Logger.error('收到未知消息类型: "$messageType"');
}
} catch (e) {
Logger.error('解析WebSocket消息失败: $e');
}
} else {
Logger.error('收到非字符串类型的payload: ${payload.runtimeType}');
}
}
/// 处理音频RMS值
void _handleAudioRms(dynamic rms) {
if (rms is double || rms is num) {
// 平滑处理RMS值,避免抖动太大,增大幅度
final double rawValue = rms.toDouble().clamp(0.0, 1.0);
// 增强幅度:平方根函数减少小值,对数函数增强大值
final double enhancedValue = (rawValue * 3.0).clamp(0.0, 1.0);
// 更强的平滑处理,减少抖动
audioAmplitude.value = audioAmplitude.value * 0.85 + enhancedValue * 0.15;
}
}
/// 处理错误
void _handleError(String error) {
Logger.error('Realtime错误: $error');
}
/// 切换字幕模式
void toggleSubtitleMode() {
isSubtitleMode.value = !isSubtitleMode.value;
}
/// 清空聊天记录
void clearChat() {
userSpeechText.value = '';
isUserSpeechFinal.value = true;
aiResponseMessages.clear();
latestUserMessage.value = '';
latestAiMessage.value = '';
partialUserMessage.value = '';
streamingAiMessage.value = '';
HapticFeedback.lightImpact();
}
/// 连接到AI服务
Future<void> connect() async {
if (isConnecting.value || isConnected.value) return;
try {
HapticFeedback.lightImpact();
// 初始化Realtime服务
bool initialized = await _realtimeService.initialize(
// serverUrl: 'ws://mcp.ideapsound.com:8000/ws',
serverUrl: 'ws://192.168.1.11:8000/ws',
sampleRate: _sampleRate,
channels: 1,
bitsPerSample: 16,
);
if (!initialized) {
throw Exception('初始化Realtime服务失败');
}
// 连接服务器
final connected = await _realtimeService.connect();
if (!connected) {
throw Exception('连接Vocode服务器失败');
}
} catch (e) {
Logger.error('连接失败: $e');
isConnecting.value = false;
_resetState();
}
}
/// 断开连接
Future<void> disconnect() async {
try {
HapticFeedback.mediumImpact();
if (isListening.value) {
await _realtimeService.stopRecording();
}
await _realtimeService.stopPlaying();
await _realtimeService.disconnect();
} catch (e) {
Logger.error('断开连接失败: $e');
}
}
/// 开始聆听
void startListening() {
if (!isConnected.value || isListening.value) return;
HapticFeedback.lightImpact();
_realtimeService.startRecording().then((success) {
if (!success) {
Logger.error('开始录音失败');
}
});
}
/// 停止聆听
void stopListening() {
if (!isListening.value) return;
_realtimeService.stopRecording().then((success) {
if (!success) {
Logger.error('停止录音失败');
}
});
}
/// 切换聆听状态
void toggleListening() {
if (!isConnected.value) return;
if (isListening.value) {
stopListening();
} else {
startListening();
}
}
/// 关闭并返回上一页
Future<void> closeAndGoBack() async {
Get.back();
// 异步清理资源
Future.delayed(Duration.zero, () async {
try {
if (isListening.value) {
await _realtimeService.stopRecording();
}
await _realtimeService.stopPlaying();
await _realtimeService.disconnect();
await _realtimeService.dispose();
_resetState();
} catch (e) {
Logger.error('关闭连接失败: $e');
}
});
}
/// 重置状态
void _resetState() {
isConnected.value = false;
isConnecting.value = false;
isListening.value = false;
isSpeaking.value = false;
userSpeechText.value = '';
isUserSpeechFinal.value = true;
aiResponseMessages.clear();
latestUserMessage.value = '';
latestAiMessage.value = '';
partialUserMessage.value = '';
streamingAiMessage.value = '';
audioAmplitude.value = 0.0;
}
/// 清理资源
void _cleanup() async {
_eventSubscription?.cancel();
try {
await _realtimeService.dispose();
} catch (e) {
Logger.error('清理Realtime服务失败: $e');
}
}
}