/** * 军事科技 AI配音系统 - 前端脚本 */ const CONFIG = { API_BASE_URL: '', // 空字符串表示使用相对路径,通过Nginx代理 MAX_CHARS: 2000, }; // 情绪按钮与 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'), switchBtns: document.querySelectorAll('.switch-btn'), emotionBtns: document.querySelectorAll('.emotion-btn'), 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; elements.switchBtns.forEach(btn => { btn.classList.toggle('active', btn.dataset.engine === engine); }); const engineName = engine === 'minimax' ? 'MiniMax' : 'CosyVoice'; document.getElementById('engineBadge').textContent = `蓝皓 · ${engineName}`; showToast(`已切换到${engineName}引擎`); } 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 = `
音频已生成,点击播放
暂无生成记录