'commit'
This commit is contained in:
@@ -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事件初始化完成');
|
||||
});
|
||||
|
Reference in New Issue
Block a user