// 导入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();
});