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();
|
||
}
|
||
}
|