Files
dsProject/dsLightRag/static/YunXiao/xueban.js
2025-08-31 14:54:19 +08:00

741 lines
28 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,
audioChunks: [], // 存储接收到的音频块
audioQueue: [], // 音频队列,用于流式播放
isStreamPlaying: false, // 是否正在流式播放
currentAudioIndex: 0 // 当前播放的音频索引
},
websocket: {
connection: null,
isConnected: false
}
};
// ==================== 工具函数 ====================
const Utils = {
// 格式化时间显示
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}`;
},
// 将Blob转换为Base64
blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
};
// ==================== 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)}`;
}
}
};
// ==================== WebSocket管理模块 ====================
const WebSocketManager = {
// 初始化WebSocket连接
initConnection() {
console.log('初始化WebSocket连接');
if (AudioState.websocket.connection &&
AudioState.websocket.connection.readyState === WebSocket.OPEN) {
console.log('WebSocket连接已存在');
return;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/xueban/streaming-chat`;
console.log('正在建立WebSocket连接:', wsUrl);
AudioState.websocket.connection = new WebSocket(wsUrl);
// 连接打开
AudioState.websocket.connection.onopen = () => {
console.log('WebSocket连接已建立');
AudioState.websocket.isConnected = true;
};
// 连接关闭
AudioState.websocket.connection.onclose = () => {
console.log('WebSocket连接已关闭');
AudioState.websocket.isConnected = false;
};
// 连接错误
AudioState.websocket.connection.onerror = (error) => {
console.error('WebSocket连接错误:', error);
AudioState.websocket.isConnected = false;
UIController.toggleElement('thinkingIndicator', false);
UIController.setStartRecordButtonEnabled(true);
alert('连接服务器失败,请稍后再试');
};
// 接收消息
AudioState.websocket.connection.onmessage = (event) => {
console.log('收到WebSocket消息:', {
type: typeof event.data,
size: typeof event.data === 'string' ? event.data.length : event.data.size
});
this.handleMessage(event);
};
},
// 处理接收到的消息
async handleMessage(event) {
// 检查消息类型
if (typeof event.data === 'string') {
// JSON消息
try {
const data = JSON.parse(event.data);
console.log('解析JSON消息成功:', data);
switch (data.type) {
case 'asr_result':
// 显示ASR识别结果
console.log('收到ASR结果:', data.text);
const asrTextElement = document.getElementById('asrResultText');
if (asrTextElement) {
asrTextElement.textContent = data.text || '未识别到内容';
}
break;
case 'end':
// 处理结束
console.log('流式处理完成');
console.log('当前音频块数量:', AudioState.playback.audioChunks.length);
UIController.toggleElement('thinkingIndicator', false);
UIController.setStartRecordButtonEnabled(true);
// 标记流式播放结束
AudioState.playback.isStreamPlaying = false;
// 如果有音频数据但尚未开始播放,则开始播放
if (AudioState.playback.audioQueue.length > 0 && !AudioState.playback.isPlaying) {
console.log('开始播放队列中的音频');
AudioPlayer.processAudioQueue();
}
break;
case 'error':
// 错误处理
console.error('收到错误消息:', data.message);
UIController.toggleElement('thinkingIndicator', false);
UIController.setStartRecordButtonEnabled(true);
// 重置流式播放状态
AudioState.playback.isStreamPlaying = false;
AudioState.playback.audioQueue = [];
alert('处理失败: ' + data.message);
break;
default:
console.log('未知消息类型:', data.type);
}
} catch (e) {
console.error('解析JSON消息失败:', e);
console.error('原始消息内容:', event.data);
}
} else {
// 修改WebSocketManager.handleMessage方法约第210-225行
// 二进制音频数据
console.log('收到音频数据,大小:', event.data.size);
console.log('音频数据类型:', event.data.type);
// 保存到原始音频块数组
AudioState.playback.audioChunks.push(event.data);
// 添加到音频队列(用于流式播放)
AudioState.playback.audioQueue.push(event.data);
console.log('当前音频队列长度:', AudioState.playback.audioQueue.length);
// 显示播放界面
UIController.toggleElement('resultContainer', true);
// 关键修复:立即开始处理音频队列,实现流式播放
if (!AudioState.playback.isStreamPlaying) {
AudioState.playback.isStreamPlaying = true;
AudioPlayer.processAudioQueue();
}
return;
}
},
// 合并所有音频块并播放
combineAndPlayAudio() {
try {
console.log('开始合并音频块,数量:', AudioState.playback.audioChunks.length);
// 创建一个新的Blob包含所有音频块
const combinedBlob = new Blob(AudioState.playback.audioChunks, { type: 'audio/wav' });
console.log('合并后的Blob大小:', combinedBlob.size);
// 创建音频URL
const audioUrl = URL.createObjectURL(combinedBlob);
console.log('创建音频URL:', audioUrl);
// 初始化音频播放器
AudioPlayer.initPlayer(audioUrl);
} catch (error) {
console.error('合并和播放音频失败:', error);
}
},
// 关闭WebSocket连接
closeConnection() {
if (AudioState.websocket.connection) {
AudioState.websocket.connection.close();
AudioState.websocket.connection = null;
AudioState.websocket.isConnected = false;
console.log('WebSocket连接已关闭');
}
}
};
// ==================== 音频播放模块 ====================
const AudioPlayer = {
// 初始化音频播放器
initPlayer(audioUrl) {
console.log('AudioPlayer.initPlayer 被调用音频URL:', audioUrl);
// 停止当前播放的音频
if (AudioState.playback.audioElement) {
console.log('停止当前播放的音频');
AudioState.playback.audioElement.pause();
}
// 创建新的音频元素
console.log('创建新的音频元素');
AudioState.playback.audioElement = new Audio(audioUrl);
AudioState.playback.isPlaying = false;
// 绑定音频事件
AudioState.playback.audioElement.onloadedmetadata = () => {
console.log('音频元数据加载完成');
this.updateTimeDisplay();
this.play(); // 自动播放
};
AudioState.playback.audioElement.onplay = () => {
console.log('音频开始播放');
};
AudioState.playback.audioElement.onpause = () => {
console.log('音频暂停');
};
AudioState.playback.audioElement.ontimeupdate = () => {
this.updateProgress();
this.updateTimeDisplay();
};
AudioState.playback.audioElement.onended = () => {
console.log('音频播放结束');
AudioState.playback.isPlaying = false;
UIController.updatePlayButton(false);
};
AudioState.playback.audioElement.onerror = (error) => {
console.error('音频播放错误:', error);
};
// 绑定播放按钮点击事件
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;
};
}
},
// 播放/暂停切换
// 修改AudioPlayer.togglePlay方法
// 播放/暂停切换
togglePlay() {
// 首先检查是否在流式播放
if (AudioState.playback.isStreamPlaying && AudioState.playback.streamAudioElement) {
// 流式播放模式
if (AudioState.playback.streamAudioElement.paused) {
// 如果当前是暂停状态,播放
AudioState.playback.streamAudioElement.play();
UIController.updatePlayButton(true);
} else {
// 如果当前正在播放,暂停
AudioState.playback.streamAudioElement.pause();
UIController.updatePlayButton(false);
}
return;
}
// 常规播放模式
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);
},
// 在AudioPlayer.initStreamPlayer方法中添加暂停事件监听
// 初始化流式播放器
initStreamPlayer() {
// 创建新的音频元素用于流式播放
if (!AudioState.playback.streamAudioElement) {
AudioState.playback.streamAudioElement = new Audio();
// 监听音频结束事件
AudioState.playback.streamAudioElement.addEventListener('ended', () => {
// 当前音频播放完毕,处理队列中的下一个音频
UIController.updatePlayButton(false); // 更新按钮状态
this.processAudioQueue();
});
// 监听暂停事件
AudioState.playback.streamAudioElement.addEventListener('pause', () => {
// 当音频暂停时,更新按钮状态
UIController.updatePlayButton(false);
});
// 监听播放事件
AudioState.playback.streamAudioElement.addEventListener('play', () => {
// 当音频开始播放时,更新按钮状态
UIController.updatePlayButton(true);
});
// 监听错误事件
AudioState.playback.streamAudioElement.addEventListener('error', (e) => {
console.error('流式播放音频错误:', e);
UIController.updatePlayButton(false); // 更新按钮状态
this.processAudioQueue();
});
}
},
// 处理音频队列
processAudioQueue() {
// 在AudioPlayer.processAudioQueue方法中修改队列为空时的处理
// 如果队列为空,则返回
if (AudioState.playback.audioQueue.length === 0) {
AudioState.playback.isStreamPlaying = false;
console.log('音频队列为空,停止流式播放');
// 隐藏播放界面 - 这是新增的代码
UIController.toggleElement('audioPlayer', false);
return;
}
// 从队列中取出第一个音频块
const audioBlob = AudioState.playback.audioQueue.shift();
console.log('从队列取出音频块,剩余队列长度:', AudioState.playback.audioQueue.length);
// 创建音频URL并设置为源
const audioUrl = URL.createObjectURL(audioBlob);
AudioState.playback.streamAudioElement.src = audioUrl;
// 修改AudioPlayer.processAudioQueue方法中的播放部分
AudioState.playback.streamAudioElement.play()
.then(() => {
console.log('开始播放音频块');
// 关键修复:更新播放按钮状态为播放中
UIController.updatePlayButton(true);
})
.catch(error => {
console.error('播放音频块失败:', error);
this.processAudioQueue();
})
.finally(() => {
// 播放完成后释放URL对象
setTimeout(() => {
URL.revokeObjectURL(audioUrl);
}, 1000);
});
}
};
// ==================== 事件绑定模块 ====================
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('未找到停止录音按钮');
}
}
};
// ==================== 录音管理模块 ====================
const RecordingManager = {
// 初始化录音
async initRecording() {
try {
// 获取用户媒体设备
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 创建媒体录制器
AudioState.recording.mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
});
// 监听数据可用事件
AudioState.recording.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
AudioState.recording.audioChunks.push(event.data);
}
};
// 监听停止事件
AudioState.recording.mediaRecorder.onstop = async () => {
console.log('录音停止,开始处理音频数据');
// 创建音频Blob
const audioBlob = new Blob(AudioState.recording.audioChunks, { type: 'audio/webm' });
console.log('录音Blob大小:', audioBlob.size);
// 更新UI
UIController.toggleElement('thinkingIndicator', true);
// 初始化WebSocket连接
WebSocketManager.initConnection();
// 等待连接建立
await this.waitForConnection();
// 发送音频数据 - 这里是错误的调用
// const success = await WebSocketManager.sendAudio(audioBlob);
// 修复后的正确调用
const success = await RecordingManager.sendAudio(audioBlob);
if (!success) {
console.error('发送音频数据失败');
UIController.toggleElement('thinkingIndicator', false);
UIController.setStartRecordButtonEnabled(true);
}
// 清空音频块
AudioState.recording.audioChunks = [];
// 停止所有音频轨道
stream.getTracks().forEach(track => track.stop());
};
console.log('录音初始化成功');
return true;
} catch (error) {
console.error('录音初始化失败:', error);
alert('录音初始化失败,请授予麦克风权限后重试');
return false;
}
},
// 开始录音
async startRecording() {
console.log('开始录音');
// 检查是否已经在录音
if (AudioState.recording.isRecording) {
console.warn('已经在录音中');
return;
}
// 初始化录音
const initialized = await this.initRecording();
if (!initialized) {
console.error('录音初始化失败,无法开始录音');
return;
}
// 开始录音
AudioState.recording.isRecording = true;
AudioState.recording.mediaRecorder.start();
// 更新UI
UIController.updateRecordingButtons(true);
console.log('录音开始成功');
// 设置最大录音时长
setTimeout(() => {
if (AudioState.recording.isRecording) {
console.log('达到最大录音时长,自动停止录音');
this.stopRecording();
}
}, AudioState.recording.maxDuration);
},
// 停止录音
stopRecording() {
console.log('停止录音');
if (!AudioState.recording.isRecording || !AudioState.recording.mediaRecorder) {
console.warn('当前没有在录音');
return;
}
// 停止录音
AudioState.recording.mediaRecorder.stop();
AudioState.recording.isRecording = false;
// 更新UI
UIController.updateRecordingButtons(false);
console.log('录音停止命令已发送');
},
// 等待WebSocket连接建立
waitForConnection() {
return new Promise((resolve) => {
const checkConnection = () => {
console.log('检查WebSocket连接状态:', AudioState.websocket.isConnected);
if (AudioState.websocket.isConnected &&
AudioState.websocket.connection &&
AudioState.websocket.connection.readyState === WebSocket.OPEN) {
console.log('WebSocket连接已建立可以发送数据');
resolve();
} else {
console.log('WebSocket连接未建立等待...');
setTimeout(checkConnection, 100);
}
};
checkConnection();
});
},
// 发送音频数据
async sendAudio(audioBlob) {
console.log('=== 开始执行sendAudio方法 ===');
// 参数验证
if (!audioBlob) {
console.error('sendAudio方法参数错误: audioBlob为空');
return false;
}
console.log('音频数据参数:', {
exists: !!audioBlob,
size: audioBlob.size,
type: audioBlob.type
});
// 连接状态检查
console.log('WebSocket连接状态:', {
isConnected: AudioState.websocket.isConnected,
connectionExists: !!AudioState.websocket.connection,
readyState: AudioState.websocket.connection ? AudioState.websocket.connection.readyState : 'N/A'
});
if (!AudioState.websocket.isConnected ||
!AudioState.websocket.connection ||
AudioState.websocket.connection.readyState !== WebSocket.OPEN) {
console.error('WebSocket连接未建立无法发送音频数据');
return false;
}
try {
console.log('将音频数据转换为Base64');
// 将音频数据转换为Base64
const base64Audio = await Utils.blobToBase64(audioBlob);
console.log('音频数据Base64长度:', base64Audio.length);
const payload = {
audio_data: base64Audio
};
console.log('准备发送的载荷:', {
keys: Object.keys(payload),
audioDataLength: payload.audio_data.length
});
// 发送音频数据
console.log('发送音频数据到WebSocket');
AudioState.websocket.connection.send(JSON.stringify(payload));
console.log('=== 音频数据发送成功 ===');
return true;
} catch (error) {
console.error('发送音频数据失败:', error);
return false;
}
}
};
// ==================== 初始化 ====================
function initializeApp() {
console.log('开始初始化学伴录音功能...');
// 初始化流式播放器
AudioPlayer.initStreamPlayer();
// 检查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事件初始化完成');
});
// 页面关闭时关闭WebSocket连接
window.addEventListener('beforeunload', () => {
WebSocketManager.closeConnection();
});