/** * 军事科技 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 = `
音频已生成,点击播放
暂无生成记录