/** * 军事科技 AI配音系统 - 前端脚本 */ const CONFIG = { API_BASE_URL: '', // 空字符串表示使用相对路径,通过Nginx代理 MAX_CHARS: 2000, EMOTIONS: { 'happy': { name: '开心', color: '#fbbf24' }, 'sad': { name: '悲伤', color: '#60a5fa' }, 'excited': { name: '激动', color: '#f97316' }, 'surprised': { name: '惊讶', color: '#a855f7' }, 'fearful': { name: '害怕', color: '#ec4899' }, 'disgusted': { name: '厌恶', color: '#14b8a6' } } }; const state = { currentEngine: 'minimax', 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(); } function applyEmotionTag(emotion, emotionName) { const selection = window.getSelection(); if (!selection.rangeCount) { showToast('请先选中要添加情绪的文字', 'error'); return; } const selectedText = selection.toString().trim(); if (!selectedText) { showToast('请先选中要添加情绪的文字', 'error'); return; } const tagHtml = `[${emotionName}]`; const range = selection.getRangeAt(0); const selectedContent = range.extractContents(); const wrapper = document.createElement('span'); wrapper.innerHTML = tagHtml; const textNode = document.createTextNode(selectedText); wrapper.appendChild(textNode); range.insertNode(wrapper); selection.removeAllRanges(); const space = document.createTextNode(' '); wrapper.after(space); elements.editor.focus(); showToast(`已添加「${emotionName}」情绪`, 'success'); clearEmotionSelection(); } function clearEmotionSelection() { state.selectedEmotion = null; elements.emotionBtns.forEach(btn => btn.classList.remove('active')); } function getPlainText() { // Remove emotion tags completely - don't include any emotion labels in the text const clone = elements.editor.cloneNode(true); // Completely remove emotion tags const tags = clone.querySelectorAll('.emotion-tag'); tags.forEach(tag => tag.remove()); return clone.innerText.trim(); } // 情绪标签映射:前端显示 → MiniMax标签 / CosyVoice指令 const EMOTION_LABELS = { 'happy': { miniMax: '[开心]', cosyvoice: '请用开心的语气' }, 'excited': { miniMax: '[激昂]', cosyvoice: '请用激昂的语气' }, 'surprised': { miniMax: '[惊讶]', cosyvoice: '请用惊讶的语气' }, 'sad': { miniMax: '[悲伤]', cosyvoice: '请用悲伤的语气' }, 'fearful': { miniMax: '[恐惧]', cosyvoice: '请用恐惧的语气' }, 'disgusted': { miniMax: '[厌恶]', cosyvoice: '请用厌恶的语气' } }; function getTextWithEmotionTags(engine) { const clone = elements.editor.cloneNode(true); if (engine === 'minimax') { const tags = clone.querySelectorAll('.emotion-tag'); tags.forEach(tag => { const emotion = tag.getAttribute('data-emotion'); const label = EMOTION_LABELS[emotion]?.miniMax || `[${emotion}]`; const textNode = document.createTextNode(label); tag.parentNode.replaceChild(textNode, tag); }); return clone.innerText.trim(); } else { const tags = clone.querySelectorAll('.emotion-tag'); tags.forEach(tag => tag.remove()); return clone.innerText.trim(); } } function getFirstEmotionInstruction() { const firstEmotionTag = elements.editor.querySelector('.emotion-tag'); if (firstEmotionTag) { const emotion = firstEmotionTag.getAttribute('data-emotion'); return EMOTION_LABELS[emotion]?.cosyvoice || null; } return null; } 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 textForApi = getTextWithEmotionTags(state.currentEngine); // 获取CosyVoice的情绪指令 const emotionInstruction = getFirstEmotionInstruction(); const requestData = { text: textForApi, engine: state.currentEngine, speed: parseFloat(elements.speedSlider.value), pitch: parseInt(elements.pitchSlider.value), volume: parseFloat(elements.volumeSlider.value) }; // CosyVoice使用instruction参数传递情绪 if (state.currentEngine === 'cosyvoice' && emotionInstruction) { requestData.instruction = emotionInstruction; } 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 = `
音频已生成,点击播放
暂无生成记录