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.
409 lines
10 KiB
409 lines
10 KiB
import 'dart:async';
|
|
import 'dart:math';
|
|
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 {
|
|
// Realtime服务实例
|
|
final RealtimeService _realtimeService = RealtimeService();
|
|
|
|
// 连接状态
|
|
final RxBool isConnected = false.obs;
|
|
final RxBool isConnecting = false.obs;
|
|
|
|
// 语音状态
|
|
final RxBool isListening = false.obs;
|
|
final RxBool isSpeaking = false.obs;
|
|
|
|
// 状态和字幕显示
|
|
final RxString currentStatus = '正在初始化...'.obs;
|
|
final RxString userSpeechText = ''.obs;
|
|
final RxString aiResponseText = ''.obs;
|
|
|
|
// 音频可视化数据
|
|
final RxList<double> audioLevels = <double>[].obs;
|
|
final RxDouble userVoiceLevel = 0.0.obs;
|
|
final RxDouble aiVoiceLevel = 0.0.obs;
|
|
|
|
// 动画控制器
|
|
late AnimationController pulseAnimationController;
|
|
late Animation<double> pulseAnimation;
|
|
|
|
// 状态监听
|
|
StreamSubscription<RealtimeEvent>? _eventSubscription;
|
|
Timer? _audioUpdateTimer;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_initializeAnimations();
|
|
_initializeAudioLevels();
|
|
_setupEventListener();
|
|
Logger.info('Realtime模块初始化完成');
|
|
|
|
// 自动开始连接
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_autoConnect();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
_cleanup();
|
|
super.onClose();
|
|
}
|
|
|
|
/// 初始化动画
|
|
void _initializeAnimations() {
|
|
pulseAnimationController = AnimationController(
|
|
duration: const Duration(milliseconds: 1000),
|
|
vsync: this,
|
|
);
|
|
|
|
pulseAnimation = Tween<double>(
|
|
begin: 0.8,
|
|
end: 1.2,
|
|
).animate(CurvedAnimation(
|
|
parent: pulseAnimationController,
|
|
curve: Curves.easeInOut,
|
|
));
|
|
}
|
|
|
|
/// 初始化音频级别数据
|
|
void _initializeAudioLevels() {
|
|
audioLevels.value = List.generate(20, (index) => 0.0);
|
|
}
|
|
|
|
/// 设置事件监听器
|
|
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 'error':
|
|
_handleError(event.data);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// 自动连接
|
|
Future<void> _autoConnect() async {
|
|
currentStatus.value = '正在连接AI服务器...';
|
|
await connect();
|
|
}
|
|
|
|
/// 处理连接状态变化
|
|
void _handleConnectionStatusChanged(String status) {
|
|
switch (status) {
|
|
case 'connected':
|
|
isConnected.value = true;
|
|
isConnecting.value = false;
|
|
currentStatus.value = '已连接,正在开始录音...';
|
|
_startAudioUpdates();
|
|
Logger.info('已连接到Vocode服务器');
|
|
// 连接成功后自动开始录音
|
|
Timer(const Duration(milliseconds: 500), () {
|
|
startListening();
|
|
});
|
|
break;
|
|
case 'connecting':
|
|
isConnecting.value = true;
|
|
currentStatus.value = '正在连接AI服务器...';
|
|
Logger.info('正在连接Vocode服务器...');
|
|
break;
|
|
case 'disconnected':
|
|
isConnected.value = false;
|
|
isConnecting.value = false;
|
|
currentStatus.value = '已断开连接';
|
|
_resetState();
|
|
Logger.info('已断开Vocode服务器连接');
|
|
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 = '正在聆听,请说话...';
|
|
pulseAnimationController.repeat(reverse: true);
|
|
Logger.info('开始录音');
|
|
break;
|
|
case 'processing':
|
|
// 用户说话结束,等待AI回应
|
|
isListening.value = false;
|
|
currentStatus.value = 'AI正在思考中...';
|
|
break;
|
|
case 'playing':
|
|
isListening.value = false;
|
|
isSpeaking.value = true;
|
|
currentStatus.value = 'AI正在回复...';
|
|
pulseAnimationController.repeat(reverse: true);
|
|
Logger.info('AI开始说话');
|
|
break;
|
|
case 'idle':
|
|
isListening.value = false;
|
|
isSpeaking.value = false;
|
|
currentStatus.value = isConnected.value ? '等待用户说话...' : '已断开连接';
|
|
pulseAnimationController.stop();
|
|
Logger.info('语音状态空闲');
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// 处理收到的文本消息
|
|
void _handleTextReceived(String text) {
|
|
Logger.info('收到文本消息: $text');
|
|
|
|
// 根据消息来源更新相应的文本
|
|
if (text.startsWith('[USER]')) {
|
|
userSpeechText.value = text.replaceFirst('[USER]', '').trim();
|
|
} else if (text.startsWith('[AI]')) {
|
|
aiResponseText.value = text.replaceFirst('[AI]', '').trim();
|
|
} else {
|
|
// 默认认为是AI回复
|
|
aiResponseText.value = text;
|
|
}
|
|
}
|
|
|
|
/// 处理错误
|
|
void _handleError(String error) {
|
|
Logger.error('Realtime错误: $error');
|
|
// 可以显示错误提示
|
|
}
|
|
|
|
/// 切换连接状态
|
|
Future<void> toggleConnection() async {
|
|
if (isConnected.value) {
|
|
await disconnect();
|
|
} else {
|
|
await connect();
|
|
}
|
|
}
|
|
|
|
/// 连接到AI服务
|
|
Future<void> connect() async {
|
|
if (isConnecting.value || isConnected.value) return;
|
|
|
|
try {
|
|
HapticFeedback.lightImpact();
|
|
|
|
// 初始化Realtime服务
|
|
final initialized = await _realtimeService.initialize(
|
|
serverUrl: 'ws://192.168.1.11:8000/ws', // 本地开发服务器使用ws协议
|
|
sampleRate: 16000,
|
|
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();
|
|
|
|
// 停止本地更新
|
|
_stopAudioUpdates();
|
|
pulseAnimationController.stop();
|
|
|
|
} catch (e) {
|
|
Logger.error('断开连接失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 开始/停止聆听
|
|
void toggleListening() {
|
|
if (!isConnected.value) return;
|
|
|
|
if (isListening.value) {
|
|
stopListening();
|
|
} else {
|
|
startListening();
|
|
}
|
|
}
|
|
|
|
/// 开始聆听
|
|
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 _startAudioUpdates() {
|
|
_audioUpdateTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
|
|
if (!isConnected.value) {
|
|
timer.cancel();
|
|
return;
|
|
}
|
|
|
|
_updateAudioLevels();
|
|
});
|
|
}
|
|
|
|
/// 停止音频数据更新
|
|
void _stopAudioUpdates() {
|
|
_audioUpdateTimer?.cancel();
|
|
_audioUpdateTimer = null;
|
|
}
|
|
|
|
/// 更新音频级别
|
|
void _updateAudioLevels() {
|
|
final random = Random();
|
|
|
|
// 更新用户语音级别
|
|
if (isListening.value) {
|
|
userVoiceLevel.value = 0.3 + random.nextDouble() * 0.7;
|
|
} else {
|
|
userVoiceLevel.value = userVoiceLevel.value * 0.8; // 衰减
|
|
}
|
|
|
|
// 更新AI语音级别
|
|
if (isSpeaking.value) {
|
|
aiVoiceLevel.value = 0.2 + random.nextDouble() * 0.8;
|
|
} else {
|
|
aiVoiceLevel.value = aiVoiceLevel.value * 0.9; // 衰减
|
|
}
|
|
|
|
// 更新音频级别数组
|
|
final newLevels = List<double>.generate(20, (index) {
|
|
if (isListening.value || isSpeaking.value) {
|
|
return random.nextDouble() * (isListening.value ? userVoiceLevel.value : aiVoiceLevel.value);
|
|
}
|
|
return audioLevels[index] * 0.95; // 衰减
|
|
});
|
|
|
|
audioLevels.value = newLevels;
|
|
}
|
|
|
|
/// 重置音频级别
|
|
void _resetAudioLevels() {
|
|
userVoiceLevel.value = 0.0;
|
|
aiVoiceLevel.value = 0.0;
|
|
audioLevels.value = List.generate(20, (index) => 0.0);
|
|
}
|
|
|
|
/// 关闭并返回上一页
|
|
Future<void> closeAndGoBack() async {
|
|
// 立即返回上一页,避免阻塞UI
|
|
Get.back();
|
|
|
|
// 之后异步进行清理,不阻塞页面切换
|
|
unawaited(_performShutdown());
|
|
}
|
|
|
|
/// 实际执行关闭逻辑(后台)
|
|
Future<void> _performShutdown() async {
|
|
try {
|
|
currentStatus.value = '正在关闭连接...';
|
|
|
|
if (isListening.value) {
|
|
await _realtimeService.stopRecording();
|
|
}
|
|
await _realtimeService.stopPlaying();
|
|
await _realtimeService.disconnect();
|
|
|
|
_resetState();
|
|
Logger.info('实时语音连接已关闭');
|
|
} catch (e) {
|
|
Logger.error('关闭连接失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 重置状态
|
|
void _resetState() {
|
|
isListening.value = false;
|
|
isSpeaking.value = false;
|
|
userSpeechText.value = '';
|
|
aiResponseText.value = '';
|
|
_resetAudioLevels();
|
|
pulseAnimationController.stop();
|
|
_stopAudioUpdates();
|
|
}
|
|
|
|
/// 清理资源
|
|
void _cleanup() async {
|
|
_eventSubscription?.cancel();
|
|
_stopAudioUpdates();
|
|
pulseAnimationController.dispose();
|
|
|
|
try {
|
|
await _realtimeService.dispose();
|
|
} catch (e) {
|
|
Logger.error('清理Realtime服务失败: $e');
|
|
}
|
|
}
|
|
}
|