/** * 学伴录音功能核心逻辑 * 模块化组织:录音管理、ASR处理、音频播放、UI控制 */ // ==================== 全局状态管理 ==================== const AudioState = { recording: { mediaRecorder: null, audioChunks: [], isRecording: false, maxDuration: 60000 // 60秒 }, playback: { audioElement: null, isPlaying: false } }; // ==================== 工具函数 ==================== const Utils = { // 获取URL参数 getUrlParam(name) { const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)'); const r = window.location.search.substr(1).match(reg); return r ? unescape(r[2]) : null; }, // 格式化时间显示 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}`; } }; // ==================== 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)}`; } } }; // ==================== 录音管理模块 ==================== 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; } }, // 开始录音 async startRecording() { if (AudioState.recording.isRecording) return; console.log('尝试开始录音'); const initialized = await this.initRecording(); 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; AudioState.recording.mediaRecorder.stop(); AudioState.recording.isRecording = false; UIController.updateRecordingButtons(false); console.log('停止录音'); // 停止音频流 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); // 创建AbortController用于超时控制 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 120000); // 120秒超时 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 // 添加超时信号 }); // 请求成功,清除超时定时器 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); } } catch (error) { // 清除超时定时器 clearTimeout(timeoutId); console.error('上传音频失败:', error); UIController.toggleElement('thinkingIndicator', false); // 发生错误时也要重新启用按钮 UIController.setStartRecordButtonEnabled(true); // 判断是否是超时错误 if (error.name === 'AbortError') { alert('请求超时,服务器响应时间过长,请稍后再试'); } else { alert('上传音频失败: ' + error.message); } } } }; // ==================== 结果显示模块 ==================== 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); } } }; // ==================== 音频播放模块 ==================== const AudioPlayer = { // 初始化音频播放器 initPlayer(audioUrl) { // 停止当前播放的音频 if (AudioState.playback.audioElement) { AudioState.playback.audioElement.pause(); } // 创建新的音频元素 AudioState.playback.audioElement = new Audio(audioUrl); AudioState.playback.isPlaying = false; // 绑定音频事件 AudioState.playback.audioElement.onloadedmetadata = () => { this.updateTimeDisplay(); this.play(); // 自动播放 }; AudioState.playback.audioElement.ontimeupdate = () => { this.updateProgress(); this.updateTimeDisplay(); }; AudioState.playback.audioElement.onended = () => { AudioState.playback.isPlaying = false; UIController.updatePlayButton(false); }; // 绑定播放按钮点击事件 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; }; } }, // 播放/暂停切换 togglePlay() { 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); } }; // ==================== 事件绑定 ==================== 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('未找到停止录音按钮'); } } }; // ==================== 初始化 ==================== // 页面加载完成后初始化 function initializeApp() { console.log('开始初始化学伴录音功能...'); // 检查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事件初始化完成'); });