Files
dsProject/dsLightRag/static/YunXiao/xueban.js
2025-08-28 16:10:45 +08:00

406 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 学伴录音功能核心逻辑
* 模块化组织录音管理、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 = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M8 5V19L19 12L8 5Z" fill="white"/></svg>';
const pauseIcon = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M6 19H10V5H6V19ZM14 19H18V5H14V19Z" fill="white"/></svg>';
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事件初始化完成');
});