// 导入Live2D控制器 import { Live2DController } from './live2d-controller.js'; class AITeacherApp { constructor() { // PDF相关变量 this.pdfDoc = null; this.pageNum = 1; this.pageRendering = false; this.pageNumPending = null; this.scale = 1.0; this.canvas = document.getElementById('pdf-canvas'); this.ctx = this.canvas.getContext('2d'); this.messageTimeout = null; // Live2D控制器 this.live2dController = null; // 初始化PDF.js pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js'; // 初始化应用 this.global_setting = null; this.api_host = null; // 音频相关 this.audioPlayer = null; this.currentAudioBase64 = null; this.selectedVoice = 'zf_xiaoxiao'; this.speechSpeed = 1.0; this.init(); } async init() { this.global_setting = await fetch('setting.json').then(response => response.json()); this.api_host = window.location.host; if(this.api_host.includes(':')) { this.api_host = this.api_host.split(':')[0]; } this.api_host = `${this.api_host}:${this.global_setting.websocket_port}`; try { console.log('初始化AI教学助手...'); // 初始化Live2D控制器 try { console.log('初始化Live2D控制器...'); this.live2dController = new Live2DController('live2d-container'); } catch (error) { console.error('初始化Live2D控制器时出错:', error); } // 初始化音频播放器 this.audioPlayer = document.getElementById('audio-player'); // 初始化语音和语速控制 this.initVoiceControls(); await this.loadDefaultPDF(); this.setupEventListeners(); console.log('AI教学助手初始化成功'); } catch (error) { console.error('初始化AI教学助手时出错:', error); this.showMessage('初始化失败: ' + error.message, true); } } async loadDefaultPDF() { try { const defaultPdfPath = './public/pdf/test.pdf'; const loadingTask = pdfjsLib.getDocument(defaultPdfPath); this.pdfDoc = await loadingTask.promise; document.getElementById('page-count').textContent = this.pdfDoc.numPages; this.renderPage(this.pageNum); // 通知服务器加载PDF this.notifyServerPdfLoad(defaultPdfPath); } catch (error) { console.error('加载PDF时出错:', error); this.showMessage('PDF加载失败: ' + error.message, true); } } async notifyServerPdfLoad(pdfPath) { try { const response = await fetch(`http://${this.api_host}/api/load_pdf`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: pdfPath }) }); if (!response.ok) { throw new Error('服务器响应错误'); } const data = await response.json(); console.log('服务器PDF加载响应:', data); if (data.success) { this.showMessage(data.message, false); } else { this.showMessage(data.message, true); } } catch (error) { console.error('通知服务器加载PDF时出错:', error); this.showMessage('通知服务器加载PDF时出错: ' + error.message, true); } } renderPage(num) { this.pageRendering = true; this.pdfDoc.getPage(num).then((page) => { const viewport = page.getViewport({ scale: this.scale }); this.canvas.height = viewport.height; this.canvas.width = viewport.width; const renderContext = { canvasContext: this.ctx, viewport: viewport }; const renderTask = page.render(renderContext); renderTask.promise.then(() => { this.pageRendering = false; if (this.pageNumPending !== null) { this.renderPage(this.pageNumPending); this.pageNumPending = null; } // 清空讲解区域和停止音频播放 document.getElementById('explanation-text').textContent = '点击"生成讲解"按钮获取AI讲解'; this.stopAudio(); document.getElementById('play-btn').disabled = true; }); }); document.getElementById('page-num').value = num; } queueRenderPage(num) { if (this.pageRendering) { this.pageNumPending = num; } else { this.renderPage(num); } } onPrevPage() { if (this.pageNum <= 1) { return; } this.pageNum--; this.queueRenderPage(this.pageNum); } onNextPage() { if (this.pageNum >= this.pdfDoc.numPages) { return; } this.pageNum++; this.queueRenderPage(this.pageNum); } onPageNumChange(e) { const newPageNum = parseInt(e.target.value); if (newPageNum > 0 && newPageNum <= this.pdfDoc.numPages) { this.pageNum = newPageNum; this.queueRenderPage(this.pageNum); } } onZoomIn() { this.scale += 0.1; this.queueRenderPage(this.pageNum); } onZoomOut() { if (this.scale <= 0.2) return; this.scale -= 0.1; this.queueRenderPage(this.pageNum); } onZoomReset() { this.scale = 1.0; this.queueRenderPage(this.pageNum); } async onFileUpload(e) { const file = e.target.files[0]; if (file && file.type === 'application/pdf') { const fileReader = new FileReader(); fileReader.onload = async (event) => { const typedarray = new Uint8Array(event.target.result); try { const loadingTask = pdfjsLib.getDocument(typedarray); this.pdfDoc = await loadingTask.promise; this.pageNum = 1; document.getElementById('page-count').textContent = this.pdfDoc.numPages; this.renderPage(this.pageNum); this.showMessage('PDF加载成功', false); } catch (error) { console.error('加载PDF时出错:', error); this.showMessage('PDF加载失败: ' + error.message, true); } }; fileReader.readAsArrayBuffer(file); } else { this.showMessage('请选择有效的PDF文件', true); } } async onExplain() { try { // 显示加载中的消息 document.getElementById('explanation-text').textContent = '正在生成AI讲解...'; document.getElementById('play-btn').disabled = true; this.stopAudio(); // 获取当前选择的语音和语速 const voice = this.selectedVoice; const speed = this.speechSpeed; // 发送到服务器获取AI讲解和音频 const response = await fetch(`http://${this.api_host}/api/explain_with_audio`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ page: this.pageNum, voice: voice, speed: speed }) }); if (!response.ok) { throw new Error('服务器响应错误'); } const data = await response.json(); if (data.success) { document.getElementById('explanation-text').textContent = data.explanation; // 如果有音频数据,启用播放按钮并自动播放 if (data.audio_base64) { this.currentAudioBase64 = data.audio_base64; document.getElementById('play-btn').disabled = false; this.playAudio(); } else if (data.tts_error) { console.error('TTS生成失败:', data.tts_error); this.showMessage('语音生成失败,但文本讲解已生成', true); } // 如果Live2D控制器已初始化,播放说话动作 if (this.live2dController && this.live2dController.initialized) { this.live2dController.playMotion('Talk', 0); } } else { document.getElementById('explanation-text').textContent = data.explanation || '生成讲解失败'; this.showMessage('生成讲解失败', true); } } catch (error) { console.error('获取AI讲解时出错:', error); document.getElementById('explanation-text').textContent = '获取AI讲解时出错: ' + error.message; this.showMessage('获取AI讲解时出错: ' + error.message, true); } } playAudio() { if (!this.currentAudioBase64) { this.showMessage('没有可播放的音频', true); return; } try { // 将base64转换为Blob const byteCharacters = atob(this.currentAudioBase64); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: 'audio/wav' }); // 创建URL并设置到音频播放器 const audioUrl = URL.createObjectURL(blob); this.audioPlayer.src = audioUrl; this.audioPlayer.style.display = 'block'; // 播放音频 this.audioPlayer.play(); // 更新播放按钮状态 const playBtn = document.getElementById('play-btn'); playBtn.innerHTML = ' 暂停'; playBtn.classList.remove('btn-success'); playBtn.classList.add('btn-warning'); // 监听音频播放结束事件 this.audioPlayer.onended = () => { playBtn.innerHTML = ' 播放'; playBtn.classList.remove('btn-warning'); playBtn.classList.add('btn-success'); }; } catch (error) { console.error('播放音频时出错:', error); this.showMessage('播放音频时出错: ' + error.message, true); } } stopAudio() { if (this.audioPlayer) { this.audioPlayer.pause(); this.audioPlayer.currentTime = 0; this.audioPlayer.style.display = 'none'; // 更新播放按钮状态 const playBtn = document.getElementById('play-btn'); playBtn.innerHTML = ' 播放'; playBtn.classList.remove('btn-warning'); playBtn.classList.add('btn-success'); } } toggleAudio() { if (this.audioPlayer.paused) { this.audioPlayer.play(); document.getElementById('play-btn').innerHTML = ' 暂停'; document.getElementById('play-btn').classList.remove('btn-success'); document.getElementById('play-btn').classList.add('btn-warning'); } else { this.audioPlayer.pause(); document.getElementById('play-btn').innerHTML = ' 播放'; document.getElementById('play-btn').classList.remove('btn-warning'); document.getElementById('play-btn').classList.add('btn-success'); } } initVoiceControls() { // 初始化语音选择器 const voiceSelect = document.getElementById('voice-select'); voiceSelect.addEventListener('change', () => { this.selectedVoice = voiceSelect.value; }); // 初始化语速控制 const speedRange = document.getElementById('speed-range'); const speedValue = document.getElementById('speed-value'); speedRange.addEventListener('input', () => { this.speechSpeed = parseFloat(speedRange.value); speedValue.textContent = this.speechSpeed.toFixed(1); }); // 设置初始值 this.selectedVoice = voiceSelect.value; this.speechSpeed = parseFloat(speedRange.value); speedValue.textContent = this.speechSpeed.toFixed(1); } async loadVoices() { try { const response = await fetch(`http://${this.api_host}/api/voices`); if (!response.ok) { throw new Error('获取语音列表失败'); } const data = await response.json(); if (data.success && data.voices && data.voices.length > 0) { const voiceSelect = document.getElementById('voice-select'); // 清空现有选项 voiceSelect.innerHTML = ''; // 添加新选项 data.voices.forEach(voice => { const option = document.createElement('option'); option.value = voice.id; option.textContent = `${voice.name} (${voice.gender === 'female' ? '女' : '男'})`; voiceSelect.appendChild(option); }); // 更新选中的语音 this.selectedVoice = voiceSelect.value; } } catch (error) { console.error('加载语音列表时出错:', error); } } showMessage(message, isError = false) { const statusMessage = document.getElementById('status-message'); statusMessage.textContent = message; statusMessage.className = isError ? 'error' : 'success'; statusMessage.style.display = 'block'; // 清除之前的超时 if (this.messageTimeout) { clearTimeout(this.messageTimeout); } // 3秒后隐藏消息 this.messageTimeout = setTimeout(() => { statusMessage.style.display = 'none'; }, 3000); } setupEventListeners() { document.getElementById('prev-page').addEventListener('click', () => this.onPrevPage()); document.getElementById('next-page').addEventListener('click', () => this.onNextPage()); document.getElementById('page-num').addEventListener('change', (e) => this.onPageNumChange(e)); document.getElementById('zoom-in').addEventListener('click', () => this.onZoomIn()); document.getElementById('zoom-out').addEventListener('click', () => this.onZoomOut()); document.getElementById('zoom-reset').addEventListener('click', () => this.onZoomReset()); document.getElementById('pdf-upload').addEventListener('change', (e) => this.onFileUpload(e)); document.getElementById('explain-btn').addEventListener('click', () => this.onExplain()); document.getElementById('play-btn').addEventListener('click', () => this.toggleAudio()); document.getElementById('model-select').addEventListener('change', () => { const modelName = document.getElementById('model-select').value; this.live2dController.loadModel(modelName); }); // 尝试加载可用的语音列表 this.loadVoices(); } } // 当页面加载完成后初始化应用 window.addEventListener('DOMContentLoaded', () => { window.aiTeacherApp = new AITeacherApp(); });