502 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			502 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						||
 * CosyVoice API 测试页面 JavaScript
 | 
						||
 */
 | 
						||
 | 
						||
// 全局变量
 | 
						||
let uploadedAudioPath = null;
 | 
						||
let loadingModal = null;
 | 
						||
 | 
						||
// DOM加载完成后初始化
 | 
						||
document.addEventListener('DOMContentLoaded', function() {
 | 
						||
    initializeComponents();
 | 
						||
    bindEvents();
 | 
						||
    loadAvailableVoices();
 | 
						||
});
 | 
						||
 | 
						||
/**
 | 
						||
 * 初始化组件
 | 
						||
 */
 | 
						||
function initializeComponents() {
 | 
						||
    loadingModal = new bootstrap.Modal(document.getElementById('loadingModal'));
 | 
						||
    
 | 
						||
    // 语速滑块显示
 | 
						||
    const speedSlider = document.getElementById('preset-speed');
 | 
						||
    const speedValue = document.getElementById('preset-speed-value');
 | 
						||
    speedSlider.addEventListener('input', function() {
 | 
						||
        speedValue.textContent = this.value;
 | 
						||
    });
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * 绑定事件
 | 
						||
 */
 | 
						||
function bindEvents() {
 | 
						||
    // 连接测试
 | 
						||
    document.getElementById('test-connection-btn').addEventListener('click', testConnection);
 | 
						||
    
 | 
						||
    // 预训练音色测试
 | 
						||
    document.getElementById('preset-voice-form').addEventListener('submit', generatePresetVoice);
 | 
						||
    document.getElementById('preset-random-seed').addEventListener('click', () => getRandomSeed('preset-seed'));
 | 
						||
    
 | 
						||
    // 自然语言控制测试
 | 
						||
    document.getElementById('natural-control-form').addEventListener('submit', generateNaturalControl);
 | 
						||
    document.getElementById('natural-random-seed').addEventListener('click', () => getRandomSeed('natural-seed'));
 | 
						||
    
 | 
						||
    // 语音克隆测试
 | 
						||
    document.getElementById('audio-upload-form').addEventListener('submit', uploadReferenceAudio);
 | 
						||
    document.getElementById('voice-clone-form').addEventListener('submit', generateVoiceClone);
 | 
						||
    document.getElementById('clone-random-seed').addEventListener('click', () => getRandomSeed('clone-seed'));
 | 
						||
    
 | 
						||
    // 清空日志
 | 
						||
    document.getElementById('clear-log').addEventListener('click', clearLog);
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * 显示加载状态
 | 
						||
 */
 | 
						||
function showLoading(message = '正在处理中...') {
 | 
						||
    document.getElementById('loading-message').textContent = message;
 | 
						||
    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);
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * 测试连接
 | 
						||
 */
 | 
						||
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">(${result.api_url})</small>
 | 
						||
                </span>
 | 
						||
            `;
 | 
						||
            addLog(`连接成功!可用音色数量: ${result.available_voices ? result.available_voices.length : 0}`, '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('获取随机种子失败');
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * 预训练音色语音生成
 | 
						||
 */
 | 
						||
async function generatePresetVoice(e) {
 | 
						||
    e.preventDefault();
 | 
						||
    
 | 
						||
    const text = document.getElementById('preset-text').value.trim();
 | 
						||
    const voice = document.getElementById('preset-voice').value;
 | 
						||
    const seed = parseInt(document.getElementById('preset-seed').value);
 | 
						||
    const speed = parseFloat(document.getElementById('preset-speed').value);
 | 
						||
    
 | 
						||
    if (!text) {
 | 
						||
        showError('请输入要合成的文本');
 | 
						||
        return;
 | 
						||
    }
 | 
						||
    
 | 
						||
    showLoading('正在生成语音...');
 | 
						||
    addLog(`开始预训练音色生成 - 音色: ${voice}, 种子: ${seed}, 语速: ${speed}x`);
 | 
						||
    
 | 
						||
    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: seed,
 | 
						||
                speed: speed
 | 
						||
            })
 | 
						||
        });
 | 
						||
        
 | 
						||
        const result = await response.json();
 | 
						||
        
 | 
						||
        if (result.success) {
 | 
						||
            // 显示音频播放器
 | 
						||
            const audioSource = document.getElementById('preset-audio-source');
 | 
						||
            const resultDiv = document.getElementById('preset-result');
 | 
						||
            
 | 
						||
            audioSource.src = result.audio_url;
 | 
						||
            audioSource.parentElement.load();
 | 
						||
            resultDiv.style.display = 'block';
 | 
						||
            
 | 
						||
            addLog(`预训练音色生成成功!音频地址: ${result.audio_url}`, 'success');
 | 
						||
            showSuccess('语音生成成功!');
 | 
						||
        } 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();
 | 
						||
    const seed = parseInt(document.getElementById('natural-seed').value);
 | 
						||
    
 | 
						||
    if (!text) {
 | 
						||
        showError('请输入要合成的文本');
 | 
						||
        return;
 | 
						||
    }
 | 
						||
    
 | 
						||
    if (!instruction) {
 | 
						||
        showError('请输入语音指令');
 | 
						||
        return;
 | 
						||
    }
 | 
						||
    
 | 
						||
    showLoading('正在生成语音...');
 | 
						||
    addLog(`开始自然语言控制生成 - 指令: ${instruction}, 种子: ${seed}`);
 | 
						||
    
 | 
						||
    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: seed
 | 
						||
            })
 | 
						||
        });
 | 
						||
        
 | 
						||
        const result = await response.json();
 | 
						||
        
 | 
						||
        if (result.success) {
 | 
						||
            // 显示音频播放器
 | 
						||
            const audioSource = document.getElementById('natural-audio-source');
 | 
						||
            const resultDiv = document.getElementById('natural-result');
 | 
						||
            
 | 
						||
            audioSource.src = result.audio_url;
 | 
						||
            audioSource.parentElement.load();
 | 
						||
            resultDiv.style.display = 'block';
 | 
						||
            
 | 
						||
            addLog(`自然语言控制生成成功!音频地址: ${result.audio_url}`, 'success');
 | 
						||
            showSuccess('语音生成成功!');
 | 
						||
        } else {
 | 
						||
            addLog(`自然语言控制生成失败: ${result.message}`, 'error');
 | 
						||
            showError(result.message);
 | 
						||
        }
 | 
						||
        
 | 
						||
    } catch (error) {
 | 
						||
        addLog(`自然语言控制生成出错: ${error.message}`, 'error');
 | 
						||
        showError('生成失败,请检查网络连接');
 | 
						||
    } finally {
 | 
						||
        hideLoading();
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * 上传参考音频
 | 
						||
 */
 | 
						||
async function uploadReferenceAudio(e) {
 | 
						||
    e.preventDefault();
 | 
						||
    
 | 
						||
    const fileInput = document.getElementById('reference-audio');
 | 
						||
    const file = fileInput.files[0];
 | 
						||
    
 | 
						||
    if (!file) {
 | 
						||
        showError('请选择音频文件');
 | 
						||
        return;
 | 
						||
    }
 | 
						||
    
 | 
						||
    showLoading('正在上传并识别音频...');
 | 
						||
    addLog(`开始上传音频文件: ${file.name} (${(file.size/1024/1024).toFixed(2)} MB)`);
 | 
						||
    
 | 
						||
    const formData = new FormData();
 | 
						||
    formData.append('audio', file);
 | 
						||
    
 | 
						||
    try {
 | 
						||
        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;
 | 
						||
            
 | 
						||
            // 显示识别结果
 | 
						||
            const resultDiv = document.getElementById('upload-result');
 | 
						||
            const recognizedText = document.getElementById('recognized-text');
 | 
						||
            
 | 
						||
            recognizedText.value = result.recognized_text || '';
 | 
						||
            resultDiv.style.display = 'block';
 | 
						||
            
 | 
						||
            // 启用克隆按钮
 | 
						||
            const cloneBtn = document.querySelector('#voice-clone-form button[type="submit"]');
 | 
						||
            cloneBtn.disabled = false;
 | 
						||
            
 | 
						||
            addLog(`音频上传成功!识别文本: ${result.recognized_text || '(无内容)'}`, 'success');
 | 
						||
            showSuccess('音频上传成功!');
 | 
						||
        } else {
 | 
						||
            addLog(`音频上传失败: ${result.message}`, 'error');
 | 
						||
            showError(result.message);
 | 
						||
        }
 | 
						||
        
 | 
						||
    } catch (error) {
 | 
						||
        addLog(`音频上传出错: ${error.message}`, 'error');
 | 
						||
        showError('上传失败,请检查网络连接');
 | 
						||
    } finally {
 | 
						||
        hideLoading();
 | 
						||
    }
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * 语音克隆生成
 | 
						||
 */
 | 
						||
async function generateVoiceClone(e) {
 | 
						||
    e.preventDefault();
 | 
						||
    
 | 
						||
    if (!uploadedAudioPath) {
 | 
						||
        showError('请先上传参考音频');
 | 
						||
        return;
 | 
						||
    }
 | 
						||
    
 | 
						||
    const text = document.getElementById('clone-text').value.trim();
 | 
						||
    const referenceText = document.getElementById('recognized-text').value.trim();
 | 
						||
    const seed = parseInt(document.getElementById('clone-seed').value);
 | 
						||
    
 | 
						||
    if (!text) {
 | 
						||
        showError('请输入要合成的文本');
 | 
						||
        return;
 | 
						||
    }
 | 
						||
    
 | 
						||
    showLoading('正在进行语音克隆...');
 | 
						||
    addLog(`开始语音克隆 - 种子: ${seed}`);
 | 
						||
    
 | 
						||
    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 audioSource = document.getElementById('clone-audio-source');
 | 
						||
            const resultDiv = document.getElementById('clone-result');
 | 
						||
            
 | 
						||
            audioSource.src = result.audio_url;
 | 
						||
            audioSource.parentElement.load();
 | 
						||
            resultDiv.style.display = 'block';
 | 
						||
            
 | 
						||
            addLog(`语音克隆成功!音频地址: ${result.audio_url}`, 'success');
 | 
						||
            showSuccess('语音克隆成功!');
 | 
						||
        } else {
 | 
						||
            addLog(`语音克隆失败: ${result.message}`, 'error');
 | 
						||
            showError(result.message);
 | 
						||
        }
 | 
						||
        
 | 
						||
    } catch (error) {
 | 
						||
        addLog(`语音克隆出错: ${error.message}`, 'error');
 | 
						||
        showError('克隆失败,请检查网络连接');
 | 
						||
    } finally {
 | 
						||
        hideLoading();
 | 
						||
    }
 | 
						||
}
 |