/**
* 语音克隆测试页面 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 = `
`;
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 = `
`;
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();
}
}