This commit is contained in:
2025-08-28 15:44:34 +08:00
parent 8b47a68c97
commit 864cb2a76a

View File

@@ -1,152 +1,372 @@
// 学伴录音功能核心逻辑
// 模型配置
/**
* 学伴录音功能核心逻辑
* 模块化组织录音管理、ASR处理、音频播放、UI控制
*/
// 录音相关变量
let mediaRecorder; let audioChunks = []; let isRecording = false;
// 音频播放相关变量
let audioElement = null; let isPlaying = false;
// 获取URL参数
function getUrlParam(name) {
const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
const r = window.location.search.substr(1).match(reg);
return r ? unescape(r[2]) : null;
}
// 开始录音
function startRecording() {
if (isRecording) return;
console.log("尝试开始录音");
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
mediaRecorder = new MediaRecorder(stream);
audioChunks = [];
mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
console.log("录音完成,音频数据大小:", audioBlob.size);
uploadAudioToServer(audioBlob);
};
mediaRecorder.start();
isRecording = true;
document.getElementById('recordingIndicator').style.display = 'flex';
document.getElementById('startRecordBtn').style.display = 'none';
document.getElementById('stopRecordBtn').style.display = 'flex';
console.log("开始录音成功");
// 设置最长录音时间为60秒
setTimeout(stopRecording, 60000);
})
.catch(error => {
console.error("获取麦克风权限失败:", error);
alert("请授权麦克风权限以使用录音功能");
});
}
// 停止录音
function stopRecording() {
if (!isRecording || !mediaRecorder) return;
mediaRecorder.stop();
isRecording = false;
document.getElementById('recordingIndicator').style.display = 'none';
document.getElementById('startRecordBtn').style.display = 'flex';
document.getElementById('stopRecordBtn').style.display = 'none';
console.log("停止录音");
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(track => track.stop());
// ==================== 全局状态管理 ====================
const AudioState = {
recording: {
mediaRecorder: null,
audioChunks: [],
isRecording: false,
maxDuration: 60000 // 60秒
},
playback: {
audioElement: null,
isPlaying: false
}
}
};
// 上传音频到服务器
function uploadAudioToServer(audioBlob) {
console.log("开始上传音频到服务器");
document.getElementById('thinkingIndicator').style.display = 'flex';
// ==================== 工具函数 ====================
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;
},
const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav');
// 格式化时间显示
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}`;
}
};
fetch('/api/xueban/upload-audio', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) throw new Error('服务器响应错误');
return response.json();
})
.then(data => {
console.log("处理结果:", data);
document.getElementById('thinkingIndicator').style.display = 'none';
if (data.success) {
showResults(data.data);
} else {
alert('音频处理失败: ' + data.message);
// ==================== UI控制器 ====================
const UIController = {
// 显示/隐藏元素
toggleElement(elementId, show) {
const element = document.getElementById(elementId);
if (element) {
element.style.display = show ? 'flex' : 'none';
}
})
.catch(error => {
console.error("上传音频失败:", error);
document.getElementById('thinkingIndicator').style.display = 'none';
alert('上传音频失败: ' + error.message);
});
}
},
// 更新按钮状态
updateRecordingButtons(isRecording) {
this.toggleElement('recordingIndicator', isRecording);
this.toggleElement('startRecordBtn', !isRecording);
this.toggleElement('stopRecordBtn', isRecording);
},
// 更新播放按钮图标
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)}`;
}
}
};
// 显示ASR识别结果和反馈
function showResults(data) {
const resultContainer = document.getElementById('resultContainer');
resultContainer.style.display = 'flex';
// ==================== 录音管理模块 ====================
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());
}
}
};
document.getElementById('asrResultText').textContent = data.asr_text || '未识别到内容';
document.getElementById('feedbackResultText').textContent = data.feedback_text || '无反馈内容';
// ==================== ASR处理模块 ====================
const ASRProcessor = {
// 处理音频数据
async processAudio(audioBlob) {
console.log('开始上传音频到服务器');
UIController.toggleElement('thinkingIndicator', true);
try {
const formData = new FormData();
formData.append('file', audioBlob, 'recording.wav');
const response = await fetch('/api/xueban/upload-audio', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('服务器响应错误');
const data = await response.json();
console.log('处理结果:', data);
UIController.toggleElement('thinkingIndicator', false);
if (data.success) {
ResultDisplay.showResults(data.data);
} else {
alert('音频处理失败: ' + data.message);
}
} catch (error) {
console.error('上传音频失败:', error);
UIController.toggleElement('thinkingIndicator', false);
alert('上传音频失败: ' + error.message);
}
}
};
if (data.audio_url) {
if (audioElement) audioElement.pause();
audioElement = new Audio(data.audio_url);
audioElement.onloadedmetadata = function() {
updateAudioTimeDisplay();
try { audioElement.play(); isPlaying = true; updatePlayButton(); }
catch (e) { console.error("自动播放失败:", e); }
// ==================== 结果显示模块 ====================
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(); // 自动播放
};
audioElement.ontimeupdate = function() { updateAudioProgress(); updateAudioTimeDisplay(); };
audioElement.onended = function() { isPlaying = false; updatePlayButton(); };
document.getElementById('playAudioBtn').onclick = togglePlayAudio;
document.getElementById('audioProgress').onclick = function(e) {
const rect = this.getBoundingClientRect();
audioElement.currentTime = (e.clientX - rect.left) / rect.width * audioElement.duration;
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('学伴录音功能初始化完成(直接执行)');
}
}
// 音频播放控制函数
function togglePlayAudio() {
if (!audioElement) return;
isPlaying ? audioElement.pause() : audioElement.play();
isPlaying = !isPlaying;
updatePlayButton();
}
// 立即执行初始化
initializeApp();
function updatePlayButton() {
const btn = document.getElementById('playAudioBtn');
btn.innerHTML = isPlaying ?
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M6 19H10V5H6V19ZM14 19H18V5H14V19Z" fill="white"/></svg>' :
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M8 5V19L19 12L8 5Z" fill="white"/></svg>';
}
// 同时保留原有的DOMContentLoaded事件作为备用
document.addEventListener('DOMContentLoaded', () => {
EventBinder.bindEvents();
console.log('学伴录音功能备用初始化完成');
});
function updateAudioProgress() {
if (!audioElement) return;
const progress = (audioElement.currentTime / audioElement.duration) * 100;
document.getElementById('progressBar').style.width = `${progress}%`;
}
function updateAudioTimeDisplay() {
if (!audioElement) return;
const format = s => `${Math.floor(s/60).toString().padStart(2,'0')}:${Math.floor(s%60).toString().padStart(2,'0')}`;
document.getElementById('audioTime').textContent = `${format(audioElement.currentTime)} / ${format(audioElement.duration)}`;
}
// 页面加载完成后也尝试绑定(确保万无一失)
window.addEventListener('load', () => {
EventBinder.bindEvents();
console.log('学伴录音功能load事件初始化完成');
});