/** * 语音克隆测试页面 JavaScript * 核心功能:语音样本采集 → 识别 → 克隆 → 对比 */ // 全局变量 let loadingModal = null; let mediaRecorder = null; let audioChunks = []; let uploadedAudioPath = null; let recognizedText = ""; let sampleAudioUrl = null; let recordedAudioBlob = null; // 当前工作流程状态 let currentStep = 1; // DOM加载完成后初始化 document.addEventListener('DOMContentLoaded', function() { initializeComponents(); bindEvents(); loadAvailableVoices(); updateStepIndicators(); }); /** * 初始化组件 */ function initializeComponents() { loadingModal = new bootstrap.Modal(document.getElementById('loadingModal')); } /** * 绑定事件 */ function bindEvents() { // 连接测试 document.getElementById('test-connection-btn').addEventListener('click', testConnection); // 语音样本采集 document.getElementById('voice-sample-form').addEventListener('submit', uploadVoiceSample); document.getElementById('voice-sample-upload').addEventListener('change', handleFileSelect); document.getElementById('start-recording').addEventListener('click', startRecording); document.getElementById('stop-recording').addEventListener('click', stopRecording); // 语音克隆生成 document.getElementById('clone-generation-form').addEventListener('submit', generateClonedVoice); document.getElementById('clone-random-seed').addEventListener('click', () => getRandomSeed('clone-seed')); // 高级功能 document.getElementById('preset-voice-form').addEventListener('submit', generatePresetVoice); document.getElementById('natural-control-form').addEventListener('submit', generateNaturalControl); // 其他 document.getElementById('clear-log').addEventListener('click', clearLog); } /** * 更新步骤指示器 */ function updateStepIndicators() { for (let i = 1; i <= 4; i++) { const indicator = document.getElementById(`step-${i}-indicator`); indicator.classList.remove('active', 'completed'); if (i < currentStep) { indicator.classList.add('completed'); } else if (i === currentStep) { indicator.classList.add('active'); } } // 更新连接线 document.querySelectorAll('.step-line').forEach((line, index) => { if (index + 1 < currentStep) { line.classList.add('completed'); } else { line.classList.remove('completed'); } }); } /** * 跳转到指定步骤 */ function goToStep(step) { currentStep = step; updateStepIndicators(); // 启用/禁用相应按钮 if (step >= 3) { document.getElementById('generate-clone-btn').disabled = false; } } /** * 重置工作流程 */ function resetWorkflow() { currentStep = 1; updateStepIndicators(); // 清空数据 uploadedAudioPath = null; recognizedText = ""; sampleAudioUrl = null; recordedAudioBlob = null; // 重置界面 document.getElementById('sample-player').style.display = 'none'; document.getElementById('recognition-result').style.display = 'none'; document.getElementById('recognition-waiting').style.display = 'block'; document.getElementById('comparison-result').style.display = 'none'; document.getElementById('comparison-waiting').style.display = 'block'; // 重置按钮状态 document.getElementById('upload-sample-btn').disabled = true; document.getElementById('generate-clone-btn').disabled = true; // 清空文件选择 document.getElementById('voice-sample-upload').value = ''; addLog('工作流程已重置,可以重新开始', 'info'); } /** * 显示加载状态 */ function showLoading(message = '正在处理中...', detail = '请稍候...') { document.getElementById('loading-message').textContent = message; document.getElementById('loading-detail').textContent = detail; loadingModal.show(); } /** * 隐藏加载状态 */ function hideLoading() { loadingModal.hide(); } /** * 添加日志 */ function addLog(message, type = 'info') { const logContainer = document.getElementById('test-log'); const timestamp = new Date().toLocaleTimeString(); const logEntry = document.createElement('div'); const colors = { 'info': 'text-primary', 'success': 'text-success', 'error': 'text-danger', 'warning': 'text-warning' }; logEntry.className = `mb-2 ${colors[type] || 'text-primary'}`; logEntry.innerHTML = `[${timestamp}] ${message}`; logContainer.appendChild(logEntry); logContainer.scrollTop = logContainer.scrollHeight; } /** * 清空日志 */ function clearLog() { const logContainer = document.getElementById('test-log'); logContainer.innerHTML = '

操作记录将显示在这里...

'; } /** * 显示错误信息 */ function showError(message) { const toast = document.createElement('div'); toast.className = 'toast align-items-center text-white bg-danger border-0 position-fixed top-0 end-0 m-3'; toast.style.zIndex = '9999'; toast.innerHTML = `
${message}
`; document.body.appendChild(toast); const bsToast = new bootstrap.Toast(toast); bsToast.show(); setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 5000); } /** * 显示成功信息 */ function showSuccess(message) { const toast = document.createElement('div'); toast.className = 'toast align-items-center text-white bg-success border-0 position-fixed top-0 end-0 m-3'; toast.style.zIndex = '9999'; toast.innerHTML = `
${message}
`; document.body.appendChild(toast); const bsToast = new bootstrap.Toast(toast); bsToast.show(); setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 3000); } /** * 创建音频播放URL */ function createAudioUrl(audioPath) { const filename = audioPath.split('/').pop(); return `/voice-test/download-audio/${filename}`; } /** * 测试连接 */ async function testConnection() { const btn = document.getElementById('test-connection-btn'); const statusDiv = document.getElementById('connection-status'); btn.disabled = true; btn.innerHTML = '测试中...'; statusDiv.innerHTML = '正在测试连接...'; addLog('开始测试CosyVoice服务连接...'); try { const response = await fetch('/voice-test/api/voice-test/connection', { method: 'POST', headers: { 'Content-Type': 'application/json', } }); const result = await response.json(); if (result.success) { statusDiv.innerHTML = ` 连接成功 支持语音克隆、识别、自然控制 `; addLog(`连接成功!核心功能可用:语音克隆、识别、自然控制`, 'success'); // 更新音色列表 if (result.available_voices) { updateVoiceOptions(result.available_voices); } } else { statusDiv.innerHTML = ` 连接失败: ${result.message} `; addLog(`连接失败: ${result.message}`, 'error'); } } catch (error) { statusDiv.innerHTML = ` 请求失败: ${error.message} `; addLog(`请求失败: ${error.message}`, 'error'); } finally { btn.disabled = false; btn.innerHTML = '测试连接'; } } /** * 加载可用音色 */ async function loadAvailableVoices() { try { const response = await fetch('/voice-test/api/voice-test/voices'); const result = await response.json(); if (result.success) { updateVoiceOptions(result.voices); } } catch (error) { console.error('加载音色失败:', error); } } /** * 更新音色选项 */ function updateVoiceOptions(voices) { const voiceSelect = document.getElementById('preset-voice'); voiceSelect.innerHTML = ''; voices.forEach(voice => { const option = document.createElement('option'); option.value = voice; option.textContent = voice; voiceSelect.appendChild(option); }); } /** * 获取随机种子 */ async function getRandomSeed(inputId) { try { const response = await fetch('/voice-test/api/voice-test/random-seed'); const result = await response.json(); if (result.success) { document.getElementById(inputId).value = result.seed; addLog(`生成随机种子: ${result.seed}`); } } catch (error) { showError('获取随机种子失败'); } } /** * 处理文件选择 */ function handleFileSelect(e) { const file = e.target.files[0]; if (file) { // 重置录音状态 recordedAudioBlob = null; sampleAudioUrl = null; addLog(`选择了音频文件: ${file.name} (${(file.size/1024/1024).toFixed(2)} MB)`); document.getElementById('upload-sample-btn').disabled = false; document.getElementById('upload-sample-btn').innerHTML = '上传并识别语音'; } } /** * 开始录音 */ async function startRecording() { try { // 重置文件选择 document.getElementById('voice-sample-upload').value = ''; const stream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000, // 设置采样率为16kHz channelCount: 1, // 单声道 echoCancellation: true, noiseSuppression: true } }); // 创建MediaRecorder,明确指定格式 const options = { mimeType: 'audio/webm;codecs=opus' // 使用webm格式 }; // 检查浏览器支持的格式 if (!MediaRecorder.isTypeSupported(options.mimeType)) { if (MediaRecorder.isTypeSupported('audio/webm')) { options.mimeType = 'audio/webm'; } else if (MediaRecorder.isTypeSupported('audio/wav')) { options.mimeType = 'audio/wav'; } else { // 使用默认格式 delete options.mimeType; } } mediaRecorder = new MediaRecorder(stream, options); audioChunks = []; mediaRecorder.ondataavailable = function(event) { if (event.data.size > 0) { audioChunks.push(event.data); } }; mediaRecorder.onstop = function() { // 创建音频Blob recordedAudioBlob = new Blob(audioChunks, { type: mediaRecorder.mimeType || 'audio/webm' }); const audioUrl = URL.createObjectURL(recordedAudioBlob); // 显示录音预览 const sampleAudio = document.getElementById('sample-audio'); const sampleSource = document.getElementById('sample-audio-source'); sampleSource.src = audioUrl; sampleAudio.load(); document.getElementById('sample-player').style.display = 'block'; // 启用上传按钮 document.getElementById('upload-sample-btn').disabled = false; document.getElementById('upload-sample-btn').innerHTML = '上传并识别语音'; // 保存录音数据 sampleAudioUrl = audioUrl; addLog(`录音完成,格式: ${mediaRecorder.mimeType || 'default'}, 大小: ${(recordedAudioBlob.size/1024).toFixed(1)} KB`, 'success'); }; mediaRecorder.start(100); // 每100ms收集一次数据 // 更新UI document.getElementById('start-recording').disabled = true; document.getElementById('stop-recording').disabled = false; document.getElementById('recording-status').textContent = '正在录音...'; document.getElementById('recording-status').className = 'text-danger'; addLog('开始录音...', 'info'); } catch (error) { addLog(`录音失败: ${error.message}`, 'error'); showError('录音失败,请检查麦克风权限'); } } /** * 停止录音 */ function stopRecording() { if (mediaRecorder && mediaRecorder.state !== 'inactive') { mediaRecorder.stop(); mediaRecorder.stream.getTracks().forEach(track => track.stop()); } // 更新UI document.getElementById('start-recording').disabled = false; document.getElementById('stop-recording').disabled = true; document.getElementById('recording-status').textContent = '录音已完成'; document.getElementById('recording-status').className = 'text-success'; addLog('录音停止', 'info'); } /** * 上传语音样本并进行识别 */ async function uploadVoiceSample(e) { e.preventDefault(); showLoading('正在上传和识别语音...', '包括格式转换和语音识别,请稍候'); addLog('开始上传语音样本进行识别...'); try { const fileInput = document.getElementById('voice-sample-upload'); const file = fileInput.files[0]; let formData = new FormData(); if (file) { // 上传文件 formData.append('audio', file); addLog(`上传文件: ${file.name}`); } else if (recordedAudioBlob) { // 上传录音 - 使用正确的文件名和类型 const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `recording_${timestamp}.webm`; formData.append('audio', recordedAudioBlob, filename); addLog(`上传录音: ${filename}, 大小: ${(recordedAudioBlob.size/1024).toFixed(1)} KB`); } else { showError('请选择音频文件或先录音'); hideLoading(); return; } // 上传并识别 const response = await fetch('/voice-test/api/voice-test/upload-audio', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { // 保存音频路径 uploadedAudioPath = result.file_path; recognizedText = result.recognized_text || ''; // 显示识别结果 document.getElementById('recognized-text').value = recognizedText; document.getElementById('recognition-result').style.display = 'block'; document.getElementById('recognition-waiting').style.display = 'none'; // 更新步骤 goToStep(2); setTimeout(() => goToStep(3), 1000); addLog(`语音识别成功: "${recognizedText}"`, 'success'); addLog(`音频处理信息: ${result.file_info?.format || '已转换格式'}`, 'info'); showSuccess('语音样本上传成功!AI已识别出内容'); // 保存原始音频用于对比 const originalAudio = document.getElementById('original-audio'); const originalSource = document.getElementById('original-audio-source'); originalSource.src = createAudioUrl(uploadedAudioPath); originalAudio.load(); } else { addLog(`语音识别失败: ${result.message}`, 'error'); showError(result.message); } } catch (error) { addLog(`上传出错: ${error.message}`, 'error'); showError('上传失败,请检查网络连接'); } finally { hideLoading(); } } /** * 生成克隆语音 */ async function generateClonedVoice(e) { e.preventDefault(); if (!uploadedAudioPath) { showError('请先上传语音样本'); return; } const text = document.getElementById('clone-text').value.trim(); const seed = parseInt(document.getElementById('clone-seed').value); const referenceText = document.getElementById('recognized-text').value.trim(); if (!text) { showError('请输入要合成的文本'); return; } showLoading('正在克隆你的声音...', '这是最复杂的步骤,请耐心等待'); addLog(`开始语音克隆 - 目标文本: "${text.substring(0, 20)}..."`); addLog(`使用音频文件: ${uploadedAudioPath}`); try { const response = await fetch('/voice-test/api/voice-test/generate/clone', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text: text, reference_audio_path: uploadedAudioPath, reference_text: referenceText, seed: seed }) }); const result = await response.json(); if (result.success) { // 显示克隆语音 const clonedAudio = document.getElementById('cloned-audio'); const clonedSource = document.getElementById('cloned-audio-source'); clonedSource.src = createAudioUrl(result.audio_url); clonedAudio.load(); // 显示对比界面 document.getElementById('comparison-result').style.display = 'block'; document.getElementById('comparison-waiting').style.display = 'none'; // 更新到最后步骤 goToStep(4); addLog(`🎉 语音克隆成功!请对比原声和克隆效果`, 'success'); showSuccess('语音克隆完成!请播放音频对比效果'); } else { addLog(`语音克隆失败: ${result.message}`, 'error'); showError(result.message || '语音克隆失败,请重试'); } } catch (error) { addLog(`克隆出错: ${error.message}`, 'error'); showError('克隆失败,请检查网络连接'); } finally { hideLoading(); } } /** * 预训练音色语音生成(高级功能) */ async function generatePresetVoice(e) { e.preventDefault(); const text = document.getElementById('preset-text').value.trim(); const voice = document.getElementById('preset-voice').value; if (!text) { showError('请输入要合成的文本'); return; } showLoading('正在生成预训练音色语音...'); try { const response = await fetch('/voice-test/api/voice-test/generate/preset', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text: text, voice: voice, seed: 42, speed: 1.0 }) }); const result = await response.json(); if (result.success) { const audioSource = document.getElementById('preset-audio-source'); const resultDiv = document.getElementById('preset-result'); audioSource.src = createAudioUrl(result.audio_url); audioSource.parentElement.load(); resultDiv.style.display = 'block'; addLog(`预训练音色生成成功 - ${voice}`, 'success'); } else { addLog(`预训练音色生成失败: ${result.message}`, 'error'); showError(result.message); } } catch (error) { addLog(`生成出错: ${error.message}`, 'error'); showError('生成失败,请检查网络连接'); } finally { hideLoading(); } } /** * 自然语言控制语音生成(高级功能) */ async function generateNaturalControl(e) { e.preventDefault(); const text = document.getElementById('natural-text').value.trim(); const instruction = document.getElementById('natural-instruction').value.trim(); if (!text) { showError('请输入要合成的文本'); return; } if (!instruction) { showError('请输入语音指令'); return; } showLoading('正在生成自然语言控制语音...'); try { const response = await fetch('/voice-test/api/voice-test/generate/natural', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text: text, instruction: instruction, seed: 42 }) }); const result = await response.json(); if (result.success) { const audioSource = document.getElementById('natural-audio-source'); const resultDiv = document.getElementById('natural-result'); audioSource.src = createAudioUrl(result.audio_url); audioSource.parentElement.load(); resultDiv.style.display = 'block'; addLog(`自然语言控制生成成功`, 'success'); } else { addLog(`自然语言控制生成失败: ${result.message}`, 'error'); showError(result.message); } } catch (error) { addLog(`生成出错: ${error.message}`, 'error'); showError('生成失败,请检查网络连接'); } finally { hideLoading(); } }