This commit is contained in:
2025-08-31 10:22:31 +08:00
parent 75a751aba0
commit 58e4e06d8a
6 changed files with 515 additions and 151 deletions

View File

@@ -13,7 +13,12 @@ const AudioState = {
},
playback: {
audioElement: null,
isPlaying: false
isPlaying: false,
audioChunks: [] // 存储接收到的音频块
},
websocket: {
connection: null,
isConnected: false
}
};
@@ -31,6 +36,16 @@ const Utils = {
const mins = Math.floor(seconds / 60).toString().padStart(2, '0');
const secs = Math.floor(seconds % 60).toString().padStart(2, '0');
return `${mins}:${secs}`;
},
// 将Blob转换为Base64
blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
};
@@ -89,153 +104,141 @@ const UIController = {
}
};
// ==================== 录音管理模块 ====================
const RecordingManager = {
// 初始化录音
async initRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
AudioState.recording.mediaRecorder = new MediaRecorder(stream);
AudioState.recording.audioChunks = [];
// 设置录音数据收集回调
AudioState.recording.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
AudioState.recording.audioChunks.push(event.data);
}
};
// 设置录音完成回调
AudioState.recording.mediaRecorder.onstop = () => {
const audioBlob = new Blob(AudioState.recording.audioChunks, { type: 'audio/wav' });
console.log('录音完成,音频数据大小:', audioBlob.size);
ASRProcessor.processAudio(audioBlob);
};
return true;
} catch (error) {
console.error('获取麦克风权限失败:', error);
alert('请授权麦克风权限以使用录音功能');
return false;
// ==================== WebSocket管理模块 ====================
const WebSocketManager = {
// 初始化WebSocket连接
initConnection() {
console.log('初始化WebSocket连接');
if (AudioState.websocket.connection &&
AudioState.websocket.connection.readyState === WebSocket.OPEN) {
console.log('WebSocket连接已存在');
return;
}
},
// 开始录音
async startRecording() {
if (AudioState.recording.isRecording) return;
console.log('尝试开始录音');
const initialized = await this.initRecording();
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/xueban/streaming-chat`;
if (initialized && AudioState.recording.mediaRecorder) {
AudioState.recording.mediaRecorder.start();
AudioState.recording.isRecording = true;
UIController.updateRecordingButtons(true);
console.log('开始录音成功');
// 设置最长录音时间
setTimeout(() => this.stopRecording(), AudioState.recording.maxDuration);
}
},
// 停止录音
stopRecording() {
if (!AudioState.recording.isRecording || !AudioState.recording.mediaRecorder) return;
console.log('正在建立WebSocket连接:', wsUrl);
AudioState.websocket.connection = new WebSocket(wsUrl);
AudioState.recording.mediaRecorder.stop();
AudioState.recording.isRecording = false;
UIController.updateRecordingButtons(false);
console.log('停止录音');
// 连接打开
AudioState.websocket.connection.onopen = () => {
console.log('WebSocket连接已建立');
AudioState.websocket.isConnected = true;
};
// 停止音频流
if (AudioState.recording.mediaRecorder.stream) {
AudioState.recording.mediaRecorder.stream.getTracks().forEach(track => track.stop());
}
}
};
// ==================== ASR处理模块 ====================
const ASRProcessor = {
// 处理音频数据
async processAudio(audioBlob) {
console.log('开始上传音频到服务器');
UIController.toggleElement('thinkingIndicator', true);
// 禁用帮我讲题按钮,防止在思考过程中重复点击
UIController.setStartRecordButtonEnabled(false);
// 连接关闭
AudioState.websocket.connection.onclose = () => {
console.log('WebSocket连接已关闭');
AudioState.websocket.isConnected = false;
};
// 创建AbortController用于超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000); // 120秒超时
// 连接错误
AudioState.websocket.connection.onerror = (error) => {
console.error('WebSocket连接错误:', error);
AudioState.websocket.isConnected = false;
UIController.toggleElement('thinkingIndicator', false);
UIController.setStartRecordButtonEnabled(true);
alert('连接服务器失败,请稍后再试');
};
try {
const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav');
const response = await fetch('/api/xueban/upload-audio', {
method: 'POST',
body: formData,
signal: controller.signal // 添加超时信号
// 接收消息
AudioState.websocket.connection.onmessage = (event) => {
console.log('收到WebSocket消息:', {
type: typeof event.data,
size: typeof event.data === 'string' ? event.data.length : event.data.size
});
// 请求成功,清除超时定时器
clearTimeout(timeoutId);
if (!response.ok) throw new Error('服务器响应错误');
const data = await response.json();
console.log('处理结果:', data);
UIController.toggleElement('thinkingIndicator', false);
// 思考结束,重新启用帮我讲题按钮
UIController.setStartRecordButtonEnabled(true);
if (data.success) {
ResultDisplay.showResults(data.data);
} else {
alert('音频处理失败: ' + data.message);
this.handleMessage(event);
};
},
// 处理接收到的消息
async handleMessage(event) {
// 检查消息类型
if (typeof event.data === 'string') {
// JSON消息
try {
const data = JSON.parse(event.data);
console.log('解析JSON消息成功:', data);
switch (data.type) {
case 'asr_result':
// 显示ASR识别结果
console.log('收到ASR结果:', data.text);
const asrTextElement = document.getElementById('asrResultText');
if (asrTextElement) {
asrTextElement.textContent = data.text || '未识别到内容';
}
break;
case 'end':
// 处理结束
console.log('流式处理完成');
console.log('当前音频块数量:', AudioState.playback.audioChunks.length);
UIController.toggleElement('thinkingIndicator', false);
UIController.setStartRecordButtonEnabled(true);
// 合并所有音频块并播放
if (AudioState.playback.audioChunks.length > 0) {
console.log('开始合并和播放音频');
this.combineAndPlayAudio();
} else {
console.warn('没有收到音频数据,无法播放');
}
break;
case 'error':
// 错误处理
console.error('收到错误消息:', data.message);
UIController.toggleElement('thinkingIndicator', false);
UIController.setStartRecordButtonEnabled(true);
alert('处理失败: ' + data.message);
break;
default:
console.log('未知消息类型:', data.type);
}
} catch (e) {
console.error('解析JSON消息失败:', e);
console.error('原始消息内容:', event.data);
}
} else {
// 二进制音频数据
console.log('收到音频数据,大小:', event.data.size);
console.log('音频数据类型:', event.data.type);
AudioState.playback.audioChunks.push(event.data);
console.log('当前音频块数量:', AudioState.playback.audioChunks.length);
}
},
// 合并所有音频块并播放
combineAndPlayAudio() {
try {
console.log('开始合并音频块,数量:', AudioState.playback.audioChunks.length);
// 创建一个新的Blob包含所有音频块
const combinedBlob = new Blob(AudioState.playback.audioChunks, { type: 'audio/wav' });
console.log('合并后的Blob大小:', combinedBlob.size);
// 创建音频URL
const audioUrl = URL.createObjectURL(combinedBlob);
console.log('创建音频URL:', audioUrl);
// 初始化音频播放器
AudioPlayer.initPlayer(audioUrl);
} catch (error) {
// 清除超时定时器
clearTimeout(timeoutId);
console.error('上传音频失败:', error);
UIController.toggleElement('thinkingIndicator', false);
// 发生错误时也要重新启用按钮
UIController.setStartRecordButtonEnabled(true);
// 判断是否是超时错误
if (error.name === 'AbortError') {
alert('请求超时,服务器响应时间过长,请稍后再试');
} else {
alert('上传音频失败: ' + error.message);
}
console.error('合并和播放音频失败:', error);
}
}
};
// ==================== 结果显示模块 ====================
const ResultDisplay = {
// 显示ASR识别结果和反馈
showResults(data) {
const resultContainer = document.getElementById('resultContainer');
if (resultContainer) {
resultContainer.style.display = 'flex';
}
// 显示识别文本
const asrTextElement = document.getElementById('asrResultText');
if (asrTextElement) {
asrTextElement.textContent = data.asr_text || '未识别到内容';
}
// 显示反馈文本
const feedbackTextElement = document.getElementById('feedbackResultText');
if (feedbackTextElement) {
feedbackTextElement.textContent = data.feedback_text || '无反馈内容';
}
// 如果有音频URL初始化音频播放器
if (data.audio_url) {
AudioPlayer.initPlayer(data.audio_url);
},
// 关闭WebSocket连接
closeConnection() {
if (AudioState.websocket.connection) {
AudioState.websocket.connection.close();
AudioState.websocket.connection = null;
AudioState.websocket.isConnected = false;
console.log('WebSocket连接已关闭');
}
}
};
@@ -244,31 +247,49 @@ const ResultDisplay = {
const AudioPlayer = {
// 初始化音频播放器
initPlayer(audioUrl) {
console.log('AudioPlayer.initPlayer 被调用音频URL:', audioUrl);
// 停止当前播放的音频
if (AudioState.playback.audioElement) {
console.log('停止当前播放的音频');
AudioState.playback.audioElement.pause();
}
// 创建新的音频元素
console.log('创建新的音频元素');
AudioState.playback.audioElement = new Audio(audioUrl);
AudioState.playback.isPlaying = false;
// 绑定音频事件
AudioState.playback.audioElement.onloadedmetadata = () => {
console.log('音频元数据加载完成');
this.updateTimeDisplay();
this.play(); // 自动播放
};
AudioState.playback.audioElement.onplay = () => {
console.log('音频开始播放');
};
AudioState.playback.audioElement.onpause = () => {
console.log('音频暂停');
};
AudioState.playback.audioElement.ontimeupdate = () => {
this.updateProgress();
this.updateTimeDisplay();
};
AudioState.playback.audioElement.onended = () => {
console.log('音频播放结束');
AudioState.playback.isPlaying = false;
UIController.updatePlayButton(false);
};
AudioState.playback.audioElement.onerror = (error) => {
console.error('音频播放错误:', error);
};
// 绑定播放按钮点击事件
const playBtn = document.getElementById('playAudioBtn');
if (playBtn) {
@@ -337,7 +358,7 @@ const AudioPlayer = {
}
};
// ==================== 事件绑定 ====================
// ==================== 事件绑定模块 ====================
const EventBinder = {
// 绑定所有事件
bindEvents() {
@@ -371,6 +392,202 @@ const EventBinder = {
}
};
// ==================== 录音管理模块 ====================
const RecordingManager = {
// 初始化录音
async initRecording() {
try {
// 获取用户媒体设备
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 创建媒体录制器
AudioState.recording.mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
});
// 监听数据可用事件
AudioState.recording.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
AudioState.recording.audioChunks.push(event.data);
}
};
// 监听停止事件
AudioState.recording.mediaRecorder.onstop = async () => {
console.log('录音停止,开始处理音频数据');
// 创建音频Blob
const audioBlob = new Blob(AudioState.recording.audioChunks, { type: 'audio/webm' });
console.log('录音Blob大小:', audioBlob.size);
// 更新UI
UIController.toggleElement('thinkingIndicator', true);
// 初始化WebSocket连接
WebSocketManager.initConnection();
// 等待连接建立
await this.waitForConnection();
// 发送音频数据 - 这里是错误的调用
// const success = await WebSocketManager.sendAudio(audioBlob);
// 修复后的正确调用
const success = await RecordingManager.sendAudio(audioBlob);
if (!success) {
console.error('发送音频数据失败');
UIController.toggleElement('thinkingIndicator', false);
UIController.setStartRecordButtonEnabled(true);
}
// 清空音频块
AudioState.recording.audioChunks = [];
// 停止所有音频轨道
stream.getTracks().forEach(track => track.stop());
};
console.log('录音初始化成功');
return true;
} catch (error) {
console.error('录音初始化失败:', error);
alert('录音初始化失败,请授予麦克风权限后重试');
return false;
}
},
// 开始录音
async startRecording() {
console.log('开始录音');
// 检查是否已经在录音
if (AudioState.recording.isRecording) {
console.warn('已经在录音中');
return;
}
// 初始化录音
const initialized = await this.initRecording();
if (!initialized) {
console.error('录音初始化失败,无法开始录音');
return;
}
// 开始录音
AudioState.recording.isRecording = true;
AudioState.recording.mediaRecorder.start();
// 更新UI
UIController.updateRecordingButtons(true);
console.log('录音开始成功');
// 设置最大录音时长
setTimeout(() => {
if (AudioState.recording.isRecording) {
console.log('达到最大录音时长,自动停止录音');
this.stopRecording();
}
}, AudioState.recording.maxDuration);
},
// 停止录音
stopRecording() {
console.log('停止录音');
if (!AudioState.recording.isRecording || !AudioState.recording.mediaRecorder) {
console.warn('当前没有在录音');
return;
}
// 停止录音
AudioState.recording.mediaRecorder.stop();
AudioState.recording.isRecording = false;
// 更新UI
UIController.updateRecordingButtons(false);
console.log('录音停止命令已发送');
},
// 等待WebSocket连接建立
waitForConnection() {
return new Promise((resolve) => {
const checkConnection = () => {
console.log('检查WebSocket连接状态:', AudioState.websocket.isConnected);
if (AudioState.websocket.isConnected &&
AudioState.websocket.connection &&
AudioState.websocket.connection.readyState === WebSocket.OPEN) {
console.log('WebSocket连接已建立可以发送数据');
resolve();
} else {
console.log('WebSocket连接未建立等待...');
setTimeout(checkConnection, 100);
}
};
checkConnection();
});
},
// 发送音频数据
async sendAudio(audioBlob) {
console.log('=== 开始执行sendAudio方法 ===');
// 参数验证
if (!audioBlob) {
console.error('sendAudio方法参数错误: audioBlob为空');
return false;
}
console.log('音频数据参数:', {
exists: !!audioBlob,
size: audioBlob.size,
type: audioBlob.type
});
// 连接状态检查
console.log('WebSocket连接状态:', {
isConnected: AudioState.websocket.isConnected,
connectionExists: !!AudioState.websocket.connection,
readyState: AudioState.websocket.connection ? AudioState.websocket.connection.readyState : 'N/A'
});
if (!AudioState.websocket.isConnected ||
!AudioState.websocket.connection ||
AudioState.websocket.connection.readyState !== WebSocket.OPEN) {
console.error('WebSocket连接未建立无法发送音频数据');
return false;
}
try {
console.log('将音频数据转换为Base64');
// 将音频数据转换为Base64
const base64Audio = await Utils.blobToBase64(audioBlob);
console.log('音频数据Base64长度:', base64Audio.length);
const payload = {
audio_data: base64Audio
};
console.log('准备发送的载荷:', {
keys: Object.keys(payload),
audioDataLength: payload.audio_data.length
});
// 发送音频数据
console.log('发送音频数据到WebSocket');
AudioState.websocket.connection.send(JSON.stringify(payload));
console.log('=== 音频数据发送成功 ===');
return true;
} catch (error) {
console.error('发送音频数据失败:', error);
return false;
}
}
};
// ==================== 初始化 ====================
// 页面加载完成后初始化
function initializeApp() {
@@ -403,3 +620,8 @@ window.addEventListener('load', () => {
EventBinder.bindEvents();
console.log('学伴录音功能load事件初始化完成');
});
// 页面关闭时关闭WebSocket连接
window.addEventListener('beforeunload', () => {
WebSocketManager.closeConnection();
});