/** * 军事科技 AI配音系统 - 前端脚本 */ const CONFIG = { API_BASE_URL: '', MAX_CHARS: 2000, }; const ENGINE_CONFIG = { minimax: { name: 'MiniMax', emotionMode: 'global', color: '#00d4aa' }, cosyvoice: { name: 'CosyVoice', emotionMode: 'global', color: '#ff6b6b' }, doubao: { name: '豆包', emotionMode: 'global', color: '#7c5cfc' }, fish_audio: { name: 'Fish Audio', emotionMode: 'global', color: '#f59e0b' } }; // 情绪按钮与 API emotion 值的映射 const EMOTION_MAP = { 'happy': { emoji: '😊', label: '开心', apiValue: 'happy' }, 'sad': { emoji: '😢', label: '悲伤', apiValue: 'sad' }, 'angry': { emoji: '🔥', label: '激昂', apiValue: 'angry' }, 'surprised': { emoji: '❗', label: '惊讶', apiValue: 'surprised' }, 'fearful': { emoji: '😨', label: '紧张', apiValue: 'fearful' }, 'disgusted': { emoji: '😒', label: '厌恶', apiValue: 'disgusted' }, }; const state = { currentEngine: 'minimax', currentEmotion: 'neutral', selectedEmotion: null, generatedAudio: null, isPlaying: false, history: [] }; const elements = { editor: document.getElementById('editor'), editorPlaceholder: document.getElementById('editorPlaceholder'), charCount: document.getElementById('charCount'), engineSelect: document.getElementById('engine-select'), emotionBtns: document.querySelectorAll('.emotion-btn'), emotionGlobal: document.getElementById('emotion-global'), emotionPerSentence: document.getElementById('emotion-per-sentence'), speedSlider: document.getElementById('speedSlider'), speedValue: document.getElementById('speedValue'), pitchSlider: document.getElementById('pitchSlider'), pitchValue: document.getElementById('pitchValue'), volumeSlider: document.getElementById('volumeSlider'), volumeValue: document.getElementById('volumeValue'), generateBtn: document.getElementById('generateBtn'), clearBtn: document.getElementById('clearBtn'), audioDisplay: document.getElementById('audioDisplay'), audioControls: document.getElementById('audioControls'), playBtn: document.getElementById('playBtn'), progressFill: document.getElementById('progressFill'), timeDisplay: document.getElementById('timeDisplay'), downloadBtn: document.getElementById('downloadBtn'), audioInfo: document.getElementById('audioInfo'), infoEngine: document.getElementById('infoEngine'), historyList: document.getElementById('historyList'), loadingOverlay: document.getElementById('loadingOverlay'), loadingText: document.getElementById('loadingText'), toast: document.getElementById('toast') }; let audioElement = new Audio(); function showToast(message, type = 'info') { const toast = elements.toast; toast.querySelector('.toast-message').textContent = message; toast.className = `toast show ${type}`; setTimeout(() => toast.classList.remove('show'), 3000); } function showLoading(text = '正在处理...') { elements.loadingText.textContent = text; elements.loadingOverlay.style.display = 'flex'; } function hideLoading() { elements.loadingOverlay.style.display = 'none'; } function formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } function updateCharCount() { const text = elements.editor.innerText || ''; const count = text.replace(/\s/g, '').length; elements.charCount.textContent = count; if (text.trim() === '') { elements.editor.classList.add('is-empty'); } else { elements.editor.classList.remove('is-empty'); } } function getSelectedText() { return window.getSelection().toString().trim(); } /** * 切换情绪状态(全局选择模式) * 点击一个 emoji 按钮设置当前情绪,再点取消;同时只能选一个 */ function applyEmotionTag(emotion) { const emotionInfo = EMOTION_MAP[emotion]; if (!emotionInfo) return; if (state.currentEmotion === emotionInfo.apiValue) { // 再次点击同一情绪,取消选择 state.currentEmotion = 'neutral'; state.selectedEmotion = null; elements.emotionBtns.forEach(btn => btn.classList.remove('active')); showToast('已取消情绪选择'); } else { // 选择新情绪 state.currentEmotion = emotionInfo.apiValue; state.selectedEmotion = emotion; elements.emotionBtns.forEach(btn => { btn.classList.toggle('active', btn.dataset.emotion === emotion); }); showToast(`已选择「${emotionInfo.label}」情绪风格`, 'success'); } } function clearEmotionSelection() { state.selectedEmotion = null; state.currentEmotion = 'neutral'; elements.emotionBtns.forEach(btn => btn.classList.remove('active')); } function getPlainText() { // 直接返回纯文本,不再处理情绪标签 return elements.editor.innerText.trim(); } function clearEditor() { elements.editor.innerHTML = ''; updateCharCount(); showToast('已清空文稿'); } function updateSliderDisplay() { elements.speedValue.textContent = `${elements.speedSlider.value}x`; elements.pitchValue.textContent = elements.pitchSlider.value; elements.volumeValue.textContent = elements.volumeSlider.value; } function switchEngine(engine) { state.currentEngine = engine; const config = ENGINE_CONFIG[engine]; if (!config) return; // Update engine badge const badge = document.getElementById('engineBadge'); badge.textContent = `蓝皓 · ${config.name}`; badge.style.background = config.color; // Update engine select value elements.engineSelect.value = engine; // Switch emotion UI mode updateEmotionMode(config.emotionMode); // Reset emotion state clearEmotionSelection(); showToast(`已切换到${config.name}引擎`); } function updateEmotionMode(mode) { if (mode === 'global') { elements.emotionGlobal.style.display = 'flex'; elements.emotionPerSentence.style.display = 'none'; } else if (mode === 'perSentence') { elements.emotionGlobal.style.display = 'none'; elements.emotionPerSentence.style.display = 'block'; } } async function generateAudio() { const text = getPlainText(); if (!text) { showToast('请输入配音文稿', 'error'); return; } showLoading('正在生成音频...'); elements.generateBtn.disabled = true; try { const requestData = { text: text, engine: state.currentEngine, speed: parseFloat(elements.speedSlider.value), pitch: parseInt(elements.pitchSlider.value), volume: parseFloat(elements.volumeSlider.value), emotion: state.currentEmotion }; // CosyVoice使用instruction参数传递情绪 if (state.currentEngine === 'cosyvoice' && state.currentEmotion !== 'neutral') { const emotionInfo = Object.values(EMOTION_MAP).find(e => e.apiValue === state.currentEmotion); if (emotionInfo) { requestData.instruction = `请用${emotionInfo.label}的语气`; } } const response = await fetch('/api/synthesize', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || '生成失败'); } const data = await response.json(); if (data.audio) { const audioData = atob(data.audio); const audioArray = new Uint8Array(audioData.length); for (let i = 0; i < audioData.length; i++) { audioArray[i] = audioData.charCodeAt(i); } const audioBlob = new Blob([audioArray], { type: 'audio/mp3' }); const audioUrl = URL.createObjectURL(audioBlob); state.generatedAudio = { url: audioUrl, blob: audioBlob }; elements.audioDisplay.classList.add('has-audio'); elements.audioDisplay.innerHTML = `

音频已生成,点击播放

`; elements.audioControls.style.display = 'flex'; elements.audioInfo.style.display = 'block'; elements.infoEngine.textContent = ENGINE_CONFIG[state.currentEngine].name; audioElement.src = audioUrl; addToHistory(text); showToast('音频生成成功!', 'success'); } } catch (error) { console.error('TTS Error:', error); showToast(error.message || '生成失败,请重试', 'error'); } finally { hideLoading(); elements.generateBtn.disabled = false; } } function addToHistory(text) { const item = { id: Date.now(), text: text.substring(0, 50) + (text.length > 50 ? '...' : ''), engine: state.currentEngine, timestamp: new Date().toLocaleString('zh-CN') }; state.history.unshift(item); if (state.history.length > 10) state.history.pop(); renderHistory(); localStorage.setItem('tts_history', JSON.stringify(state.history)); } function renderHistory() { if (state.history.length === 0) { elements.historyList.innerHTML = '

暂无生成记录

'; return; } elements.historyList.innerHTML = state.history.map(item => `
${item.text}
`).join(''); document.querySelectorAll('.history-item').forEach(el => { el.addEventListener('click', () => { const id = parseInt(el.dataset.id); const item = state.history.find(h => h.id === id); if (item) { elements.editor.innerText = item.text; updateCharCount(); } }); }); } function togglePlay() { if (!state.generatedAudio) return; if (state.isPlaying) { audioElement.pause(); elements.playBtn.querySelector('.icon-play').style.display = 'block'; elements.playBtn.querySelector('.icon-pause').style.display = 'none'; } else { audioElement.play(); elements.playBtn.querySelector('.icon-play').style.display = 'none'; elements.playBtn.querySelector('.icon-pause').style.display = 'block'; } state.isPlaying = !state.isPlaying; } function downloadAudio() { if (!state.generatedAudio) return; // 文件命名规则:节目名_引擎名_日期_随机数.mp3 const engineName = ENGINE_CONFIG[state.currentEngine].name; const date = new Date().toISOString().slice(0, 10).replace(/-/g, ''); const randomNum = Math.floor(Math.random() * 10000).toString().padStart(4, '0'); const fileName = `军事科技_${engineName}_${date}_${randomNum}.mp3`; const link = document.createElement('a'); link.href = state.generatedAudio.url; link.download = fileName; link.click(); showToast('开始下载', 'success'); } elements.editor.addEventListener('input', updateCharCount); elements.editor.addEventListener('paste', () => { setTimeout(updateCharCount, 0); }); elements.engineSelect.addEventListener('change', (e) => { switchEngine(e.target.value); }); elements.emotionBtns.forEach(btn => { btn.addEventListener('click', () => { const emotion = btn.dataset.emotion; applyEmotionTag(emotion); }); }); elements.speedSlider.addEventListener('input', updateSliderDisplay); elements.pitchSlider.addEventListener('input', updateSliderDisplay); elements.volumeSlider.addEventListener('input', updateSliderDisplay); elements.generateBtn.addEventListener('click', generateAudio); elements.clearBtn.addEventListener('click', clearEditor); elements.playBtn.addEventListener('click', togglePlay); elements.downloadBtn.addEventListener('click', downloadAudio); audioElement.addEventListener('timeupdate', () => { const progress = (audioElement.currentTime / audioElement.duration) * 100; elements.progressFill.style.width = `${progress}%`; elements.timeDisplay.textContent = `${formatTime(audioElement.currentTime)} / ${formatTime(audioElement.duration || 0)}`; }); audioElement.addEventListener('ended', () => { state.isPlaying = false; elements.playBtn.querySelector('.icon-play').style.display = 'block'; elements.playBtn.querySelector('.icon-pause').style.display = 'none'; elements.progressFill.style.width = '0%'; }); elements.audioDisplay.addEventListener('click', () => { if (state.generatedAudio && !state.isPlaying) { togglePlay(); } }); async function init() { // 登录检测 try { const res = await fetch('/api/me'); if (!res.ok) { window.location.href = '/login.html'; return; } const data = await res.json(); const user = data.user; // 在 header 右侧显示用户信息 const headerInfo = document.querySelector('.header-info'); headerInfo.innerHTML = ` 蓝皓 ${user.display_name} ${user.role === 'admin' ? '管理后台' : ''} 退出 `; document.getElementById('logoutLink').addEventListener('click', async (e) => { e.preventDefault(); await fetch('/api/logout', { method: 'POST' }); window.location.href = '/login.html'; }); } catch (e) { window.location.href = '/login.html'; return; } updateCharCount(); updateSliderDisplay(); const savedHistory = localStorage.getItem('tts_history'); if (savedHistory) { try { state.history = JSON.parse(savedHistory); renderHistory(); } catch (e) {} } console.log('军事科技 AI配音系统已初始化'); } init();