715 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			715 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						||
 * 语音克隆测试页面 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 = `<small>[${timestamp}]</small> ${message}`;
 | 
						||
    
 | 
						||
    logContainer.appendChild(logEntry);
 | 
						||
    logContainer.scrollTop = logContainer.scrollHeight;
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * 清空日志
 | 
						||
 */
 | 
						||
function clearLog() {
 | 
						||
    const logContainer = document.getElementById('test-log');
 | 
						||
    logContainer.innerHTML = '<p class="text-muted">操作记录将显示在这里...</p>';
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * 显示错误信息
 | 
						||
 */
 | 
						||
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 = `
 | 
						||
        <div class="d-flex">
 | 
						||
            <div class="toast-body">
 | 
						||
                <i class="fas fa-exclamation-circle me-2"></i>${message}
 | 
						||
            </div>
 | 
						||
            <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
 | 
						||
        </div>
 | 
						||
    `;
 | 
						||
    
 | 
						||
    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 = `
 | 
						||
        <div class="d-flex">
 | 
						||
            <div class="toast-body">
 | 
						||
                <i class="fas fa-check-circle me-2"></i>${message}
 | 
						||
            </div>
 | 
						||
            <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
 | 
						||
        </div>
 | 
						||
    `;
 | 
						||
    
 | 
						||
    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 = '<i class="fas fa-spinner fa-spin me-2"></i>测试中...';
 | 
						||
    statusDiv.innerHTML = '<span class="text-info">正在测试连接...</span>';
 | 
						||
    
 | 
						||
    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 = `
 | 
						||
                <span class="text-success">
 | 
						||
                    <i class="fas fa-check-circle me-2"></i>连接成功
 | 
						||
                    <small class="text-muted">支持语音克隆、识别、自然控制</small>
 | 
						||
                </span>
 | 
						||
            `;
 | 
						||
            addLog(`连接成功!核心功能可用:语音克隆、识别、自然控制`, 'success');
 | 
						||
            
 | 
						||
            // 更新音色列表
 | 
						||
            if (result.available_voices) {
 | 
						||
                updateVoiceOptions(result.available_voices);
 | 
						||
            }
 | 
						||
        } else {
 | 
						||
            statusDiv.innerHTML = `
 | 
						||
                <span class="text-danger">
 | 
						||
                    <i class="fas fa-times-circle me-2"></i>连接失败: ${result.message}
 | 
						||
                </span>
 | 
						||
            `;
 | 
						||
            addLog(`连接失败: ${result.message}`, 'error');
 | 
						||
        }
 | 
						||
        
 | 
						||
    } catch (error) {
 | 
						||
        statusDiv.innerHTML = `
 | 
						||
            <span class="text-danger">
 | 
						||
                <i class="fas fa-times-circle me-2"></i>请求失败: ${error.message}
 | 
						||
            </span>
 | 
						||
        `;
 | 
						||
        addLog(`请求失败: ${error.message}`, 'error');
 | 
						||
    } finally {
 | 
						||
        btn.disabled = false;
 | 
						||
        btn.innerHTML = '<i class="fas fa-wifi me-2"></i>测试连接';
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * 加载可用音色
 | 
						||
 */
 | 
						||
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 = '<i class="fas fa-upload me-2"></i>上传并识别语音';
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * 开始录音
 | 
						||
 */
 | 
						||
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 = '<i class="fas fa-upload me-2"></i>上传并识别语音';
 | 
						||
            
 | 
						||
            // 保存录音数据
 | 
						||
            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();
 | 
						||
    }
 | 
						||
}
 |