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