/** * 学伴录音功能核心逻辑 * 模块化组织:录音管理、ASR处理、音频播放、UI控制 */ // ==================== 全局状态管理 ==================== const AudioState = { recording: { mediaRecorder: null, audioChunks: [], isRecording: false, maxDuration: 60000 // 60秒 }, playback: { audioElement: null, isPlaying: false, audioChunks: [], // 存储接收到的音频块 audioQueue: [], // 音频队列,用于流式播放 isStreamPlaying: false, // 是否正在流式播放 currentAudioIndex: 0 // 当前播放的音频索引 }, websocket: { connection: null, isConnected: false } }; // ==================== 工具函数 ==================== const Utils = { // 格式化时间显示 formatTime(seconds) { 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); }); } }; // ==================== UI控制器 ==================== const UIController = { // 显示/隐藏元素 toggleElement(elementId, show) { const element = document.getElementById(elementId); if (element) { element.style.display = show ? 'flex' : 'none'; } }, // 更新按钮状态 updateRecordingButtons(isRecording) { this.toggleElement('recordingIndicator', isRecording); this.toggleElement('startRecordBtn', !isRecording); this.toggleElement('stopRecordBtn', isRecording); }, // 禁用/启用帮我讲题按钮 setStartRecordButtonEnabled(enabled) { const startBtn = document.getElementById('startRecordBtn'); if (startBtn) { startBtn.disabled = !enabled; startBtn.style.opacity = enabled ? '1' : '0.5'; startBtn.style.cursor = enabled ? 'pointer' : 'not-allowed'; } }, // 更新播放按钮图标 updatePlayButton(isPlaying) { const btn = document.getElementById('playAudioBtn'); if (!btn) return; const playIcon = ''; const pauseIcon = ''; btn.innerHTML = isPlaying ? pauseIcon : playIcon; }, // 更新进度条 updateProgress(progress) { const progressBar = document.getElementById('progressBar'); if (progressBar) { progressBar.style.width = `${progress}%`; } }, // 更新时间显示 updateTimeDisplay(currentTime, duration) { const timeDisplay = document.getElementById('audioTime'); if (timeDisplay) { timeDisplay.textContent = `${Utils.formatTime(currentTime)} / ${Utils.formatTime(duration)}`; } } }; // ==================== WebSocket管理模块 ==================== const WebSocketManager = { // 初始化WebSocket连接 initConnection() { console.log('初始化WebSocket连接'); if (AudioState.websocket.connection && AudioState.websocket.connection.readyState === WebSocket.OPEN) { console.log('WebSocket连接已存在'); return; } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/api/xueban/streaming-chat`; console.log('正在建立WebSocket连接:', wsUrl); AudioState.websocket.connection = new WebSocket(wsUrl); // 连接打开 AudioState.websocket.connection.onopen = () => { console.log('WebSocket连接已建立'); AudioState.websocket.isConnected = true; }; // 连接关闭 AudioState.websocket.connection.onclose = () => { console.log('WebSocket连接已关闭'); AudioState.websocket.isConnected = false; }; // 连接错误 AudioState.websocket.connection.onerror = (error) => { console.error('WebSocket连接错误:', error); AudioState.websocket.isConnected = false; UIController.toggleElement('thinkingIndicator', false); UIController.setStartRecordButtonEnabled(true); alert('连接服务器失败,请稍后再试'); }; // 接收消息 AudioState.websocket.connection.onmessage = (event) => { console.log('收到WebSocket消息:', { type: typeof event.data, size: typeof event.data === 'string' ? event.data.length : event.data.size }); 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); // 标记流式播放结束 AudioState.playback.isStreamPlaying = false; // 如果有音频数据但尚未开始播放,则开始播放 if (AudioState.playback.audioQueue.length > 0 && !AudioState.playback.isPlaying) { console.log('开始播放队列中的音频'); AudioPlayer.processAudioQueue(); } break; case 'error': // 错误处理 console.error('收到错误消息:', data.message); UIController.toggleElement('thinkingIndicator', false); UIController.setStartRecordButtonEnabled(true); // 重置流式播放状态 AudioState.playback.isStreamPlaying = false; AudioState.playback.audioQueue = []; alert('处理失败: ' + data.message); break; default: console.log('未知消息类型:', data.type); } } catch (e) { console.error('解析JSON消息失败:', e); console.error('原始消息内容:', event.data); } } else { // 修改WebSocketManager.handleMessage方法(约第210-225行) // 二进制音频数据 console.log('收到音频数据,大小:', event.data.size); console.log('音频数据类型:', event.data.type); // 保存到原始音频块数组 AudioState.playback.audioChunks.push(event.data); // 添加到音频队列(用于流式播放) AudioState.playback.audioQueue.push(event.data); console.log('当前音频队列长度:', AudioState.playback.audioQueue.length); // 显示播放界面 UIController.toggleElement('resultContainer', true); // 关键修复:立即开始处理音频队列,实现流式播放 if (!AudioState.playback.isStreamPlaying) { AudioState.playback.isStreamPlaying = true; AudioPlayer.processAudioQueue(); } return; } }, // 合并所有音频块并播放 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) { console.error('合并和播放音频失败:', error); } }, // 关闭WebSocket连接 closeConnection() { if (AudioState.websocket.connection) { AudioState.websocket.connection.close(); AudioState.websocket.connection = null; AudioState.websocket.isConnected = false; console.log('WebSocket连接已关闭'); } } }; // ==================== 音频播放模块 ==================== 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) { playBtn.onclick = () => this.togglePlay(); } // 绑定进度条点击事件 const progressContainer = document.getElementById('audioProgress'); if (progressContainer) { progressContainer.onclick = (e) => { const rect = progressContainer.getBoundingClientRect(); const clickPosition = (e.clientX - rect.left) / rect.width; AudioState.playback.audioElement.currentTime = clickPosition * AudioState.playback.audioElement.duration; }; } }, // 播放/暂停切换 // 修改AudioPlayer.togglePlay方法 // 播放/暂停切换 togglePlay() { // 首先检查是否在流式播放 if (AudioState.playback.isStreamPlaying && AudioState.playback.streamAudioElement) { // 流式播放模式 if (AudioState.playback.streamAudioElement.paused) { // 如果当前是暂停状态,播放 AudioState.playback.streamAudioElement.play(); UIController.updatePlayButton(true); } else { // 如果当前正在播放,暂停 AudioState.playback.streamAudioElement.pause(); UIController.updatePlayButton(false); } return; } // 常规播放模式 if (!AudioState.playback.audioElement) return; if (AudioState.playback.isPlaying) { this.pause(); } else { this.play(); } }, // 播放 play() { if (!AudioState.playback.audioElement) return; try { AudioState.playback.audioElement.play(); AudioState.playback.isPlaying = true; UIController.updatePlayButton(true); } catch (e) { console.error('播放失败:', e); } }, // 暂停 pause() { if (!AudioState.playback.audioElement) return; AudioState.playback.audioElement.pause(); AudioState.playback.isPlaying = false; UIController.updatePlayButton(false); }, // 更新进度条 updateProgress() { if (!AudioState.playback.audioElement) return; const progress = (AudioState.playback.audioElement.currentTime / AudioState.playback.audioElement.duration) * 100; UIController.updateProgress(progress); }, // 更新时间显示 updateTimeDisplay() { if (!AudioState.playback.audioElement) return; const currentTime = AudioState.playback.audioElement.currentTime; const duration = AudioState.playback.audioElement.duration; UIController.updateTimeDisplay(currentTime, duration); }, // 在AudioPlayer.initStreamPlayer方法中添加暂停事件监听 // 初始化流式播放器 initStreamPlayer() { // 创建新的音频元素用于流式播放 if (!AudioState.playback.streamAudioElement) { AudioState.playback.streamAudioElement = new Audio(); // 监听音频结束事件 AudioState.playback.streamAudioElement.addEventListener('ended', () => { // 当前音频播放完毕,处理队列中的下一个音频 UIController.updatePlayButton(false); // 更新按钮状态 this.processAudioQueue(); }); // 监听暂停事件 AudioState.playback.streamAudioElement.addEventListener('pause', () => { // 当音频暂停时,更新按钮状态 UIController.updatePlayButton(false); }); // 监听播放事件 AudioState.playback.streamAudioElement.addEventListener('play', () => { // 当音频开始播放时,更新按钮状态 UIController.updatePlayButton(true); }); // 监听错误事件 AudioState.playback.streamAudioElement.addEventListener('error', (e) => { console.error('流式播放音频错误:', e); UIController.updatePlayButton(false); // 更新按钮状态 this.processAudioQueue(); }); } }, // 处理音频队列 processAudioQueue() { // 在AudioPlayer.processAudioQueue方法中,修改队列为空时的处理 // 如果队列为空,则返回 if (AudioState.playback.audioQueue.length === 0) { AudioState.playback.isStreamPlaying = false; console.log('音频队列为空,停止流式播放'); // 隐藏播放界面 - 这是新增的代码 UIController.toggleElement('audioPlayer', false); return; } // 从队列中取出第一个音频块 const audioBlob = AudioState.playback.audioQueue.shift(); console.log('从队列取出音频块,剩余队列长度:', AudioState.playback.audioQueue.length); // 创建音频URL并设置为源 const audioUrl = URL.createObjectURL(audioBlob); AudioState.playback.streamAudioElement.src = audioUrl; // 修改AudioPlayer.processAudioQueue方法中的播放部分 AudioState.playback.streamAudioElement.play() .then(() => { console.log('开始播放音频块'); // 关键修复:更新播放按钮状态为播放中 UIController.updatePlayButton(true); }) .catch(error => { console.error('播放音频块失败:', error); this.processAudioQueue(); }) .finally(() => { // 播放完成后释放URL对象 setTimeout(() => { URL.revokeObjectURL(audioUrl); }, 1000); }); } }; // ==================== 事件绑定模块 ==================== const EventBinder = { // 绑定所有事件 bindEvents() { // 绑定录音按钮事件 const startBtn = document.getElementById('startRecordBtn'); const stopBtn = document.getElementById('stopRecordBtn'); console.log('开始绑定事件,查找按钮元素...'); console.log('开始录音按钮:', startBtn); console.log('停止录音按钮:', stopBtn); if (startBtn) { startBtn.onclick = () => { console.log('点击开始录音按钮'); RecordingManager.startRecording(); }; console.log('已绑定开始录音按钮事件'); } else { console.error('未找到开始录音按钮'); } if (stopBtn) { stopBtn.onclick = () => { console.log('点击停止录音按钮'); RecordingManager.stopRecording(); }; console.log('已绑定停止录音按钮事件'); } else { console.error('未找到停止录音按钮'); } } }; // ==================== 录音管理模块 ==================== 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() { console.log('开始初始化学伴录音功能...'); // 初始化流式播放器 AudioPlayer.initStreamPlayer(); // 检查DOM是否已就绪 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { EventBinder.bindEvents(); console.log('学伴录音功能初始化完成(DOMContentLoaded)'); }); } else { // DOM已经加载完成,直接绑定事件 EventBinder.bindEvents(); console.log('学伴录音功能初始化完成(直接执行)'); } } // 立即执行初始化 initializeApp(); // 同时保留原有的DOMContentLoaded事件作为备用 document.addEventListener('DOMContentLoaded', () => { EventBinder.bindEvents(); console.log('学伴录音功能备用初始化完成'); }); // 页面加载完成后也尝试绑定(确保万无一失) window.addEventListener('load', () => { EventBinder.bindEvents(); console.log('学伴录音功能load事件初始化完成'); }); // 页面关闭时关闭WebSocket连接 window.addEventListener('beforeunload', () => { WebSocketManager.closeConnection(); });