375 lines
11 KiB
JavaScript
375 lines
11 KiB
JavaScript
/**
|
||
* 语音克隆页面 JavaScript
|
||
*/
|
||
|
||
// 全局变量
|
||
let loadingModal = null;
|
||
let mediaRecorder = null;
|
||
let audioChunks = [];
|
||
let recordedBlob = null;
|
||
|
||
// DOM加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
initializeComponents();
|
||
bindEvents();
|
||
loadVoiceStatus();
|
||
});
|
||
|
||
/**
|
||
* 初始化组件
|
||
*/
|
||
function initializeComponents() {
|
||
loadingModal = new bootstrap.Modal(document.getElementById('loadingModal'));
|
||
}
|
||
|
||
/**
|
||
* 绑定事件
|
||
*/
|
||
function bindEvents() {
|
||
// 文件选择
|
||
document.getElementById('voice-file').addEventListener('change', handleFileSelect);
|
||
|
||
// 录音控制
|
||
document.getElementById('start-record').addEventListener('click', startRecording);
|
||
document.getElementById('stop-record').addEventListener('click', stopRecording);
|
||
|
||
// 表单提交
|
||
document.getElementById('voice-upload-form').addEventListener('submit', uploadVoiceSample);
|
||
document.getElementById('speech-generate-form').addEventListener('submit', generateSpeech);
|
||
}
|
||
|
||
/**
|
||
* 显示加载状态
|
||
*/
|
||
function showLoading(message = '正在处理中...', detail = '请稍候...') {
|
||
document.getElementById('loading-message').textContent = message;
|
||
document.getElementById('loading-detail').textContent = detail;
|
||
loadingModal.show();
|
||
}
|
||
|
||
/**
|
||
* 隐藏加载状态
|
||
*/
|
||
function hideLoading() {
|
||
loadingModal.hide();
|
||
}
|
||
|
||
/**
|
||
* 显示成功消息
|
||
*/
|
||
function showSuccess(message) {
|
||
showToast(message, 'success');
|
||
}
|
||
|
||
/**
|
||
* 显示错误消息
|
||
*/
|
||
function showError(message) {
|
||
showToast(message, 'danger');
|
||
}
|
||
|
||
/**
|
||
* 显示Toast消息
|
||
*/
|
||
function showToast(message, type = 'info') {
|
||
const colors = {
|
||
'success': 'bg-success',
|
||
'danger': 'bg-danger',
|
||
'warning': 'bg-warning',
|
||
'info': 'bg-info'
|
||
};
|
||
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast align-items-center text-white ${colors[type]} 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 loadVoiceStatus() {
|
||
try {
|
||
const response = await fetch('/voice-clone/api/voice-clone/status');
|
||
const result = await response.json();
|
||
|
||
updateStatusCard(result);
|
||
|
||
if (result.has_sample) {
|
||
// 启用语音生成功能
|
||
document.getElementById('generate-btn').disabled = false;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('加载语音状态失败:', error);
|
||
updateStatusCard({
|
||
success: false,
|
||
message: '无法加载语音状态'
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新状态卡片
|
||
*/
|
||
function updateStatusCard(result) {
|
||
const content = document.getElementById('voice-status-content');
|
||
|
||
if (result.has_sample) {
|
||
content.innerHTML = `
|
||
<div class="row align-items-center">
|
||
<div class="col-md-8">
|
||
<h5 class="fw-bold text-success mb-2">
|
||
<i class="fas fa-check-circle me-2"></i>专属声音已准备好!
|
||
</h5>
|
||
<p class="text-muted mb-1">AI识别内容:「${result.recognized_text}」</p>
|
||
<small class="text-muted">录制时间:${result.upload_time}</small>
|
||
</div>
|
||
<div class="col-md-4 text-end">
|
||
<div class="d-flex align-items-center justify-content-end">
|
||
<div class="icon-bubble me-3" style="background: linear-gradient(45deg, #28a745, #20c997); width: 60px; height: 60px;">
|
||
<i class="fas fa-microphone text-white"></i>
|
||
</div>
|
||
<div>
|
||
<span class="badge bg-success">已就绪</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else {
|
||
content.innerHTML = `
|
||
<div class="row align-items-center">
|
||
<div class="col-md-8">
|
||
<h5 class="fw-bold text-warning mb-2">
|
||
<i class="fas fa-exclamation-triangle me-2"></i>还没有录制语音样本
|
||
</h5>
|
||
<p class="text-muted mb-0">快来录制你的专属声音,让AI学会模仿你说话吧!</p>
|
||
</div>
|
||
<div class="col-md-4 text-end">
|
||
<div class="icon-bubble" style="background: linear-gradient(45deg, #ffc107, #ffca2c); width: 60px; height: 60px;">
|
||
<i class="fas fa-microphone-slash text-white"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理文件选择
|
||
*/
|
||
function handleFileSelect(e) {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
recordedBlob = null; // 重置录音数据
|
||
document.getElementById('upload-btn').disabled = false;
|
||
|
||
// 显示文件预览
|
||
const audioUrl = URL.createObjectURL(file);
|
||
showAudioPreview(audioUrl);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 开始录音
|
||
*/
|
||
async function startRecording() {
|
||
try {
|
||
// 重置文件选择
|
||
document.getElementById('voice-file').value = '';
|
||
|
||
const stream = await navigator.mediaDevices.getUserMedia({
|
||
audio: {
|
||
sampleRate: 16000,
|
||
channelCount: 1,
|
||
echoCancellation: true,
|
||
noiseSuppression: true
|
||
}
|
||
});
|
||
|
||
mediaRecorder = new MediaRecorder(stream, {
|
||
mimeType: 'audio/webm;codecs=opus'
|
||
});
|
||
|
||
audioChunks = [];
|
||
|
||
mediaRecorder.ondataavailable = function(event) {
|
||
if (event.data.size > 0) {
|
||
audioChunks.push(event.data);
|
||
}
|
||
};
|
||
|
||
mediaRecorder.onstop = function() {
|
||
recordedBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
||
const audioUrl = URL.createObjectURL(recordedBlob);
|
||
showAudioPreview(audioUrl);
|
||
|
||
// 启用上传按钮
|
||
document.getElementById('upload-btn').disabled = false;
|
||
};
|
||
|
||
mediaRecorder.start(100);
|
||
|
||
// 更新UI
|
||
document.getElementById('start-record').disabled = true;
|
||
document.getElementById('stop-record').disabled = false;
|
||
document.getElementById('record-status').textContent = '正在录音...';
|
||
document.getElementById('record-status').className = 'text-danger';
|
||
|
||
} catch (error) {
|
||
showError('录音失败,请检查麦克风权限');
|
||
console.error('录音失败:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 停止录音
|
||
*/
|
||
function stopRecording() {
|
||
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
||
mediaRecorder.stop();
|
||
mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
||
}
|
||
|
||
// 更新UI
|
||
document.getElementById('start-record').disabled = false;
|
||
document.getElementById('stop-record').disabled = true;
|
||
document.getElementById('record-status').textContent = '录音完成';
|
||
document.getElementById('record-status').className = 'text-success';
|
||
}
|
||
|
||
/**
|
||
* 显示音频预览
|
||
*/
|
||
function showAudioPreview(audioUrl) {
|
||
const previewAudio = document.getElementById('preview-audio');
|
||
const previewSource = document.getElementById('preview-source');
|
||
|
||
previewSource.src = audioUrl;
|
||
previewAudio.load();
|
||
document.getElementById('audio-preview').style.display = 'block';
|
||
}
|
||
|
||
/**
|
||
* 上传语音样本
|
||
*/
|
||
async function uploadVoiceSample(e) {
|
||
e.preventDefault();
|
||
|
||
const fileInput = document.getElementById('voice-file');
|
||
const file = fileInput.files[0];
|
||
|
||
if (!file && !recordedBlob) {
|
||
showError('请选择音频文件或先录音');
|
||
return;
|
||
}
|
||
|
||
showLoading('正在上传和训练你的声音...', '这可能需要几秒钟时间');
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
|
||
if (file) {
|
||
formData.append('audio', file);
|
||
} else if (recordedBlob) {
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
formData.append('audio', recordedBlob, `recording_${timestamp}.webm`);
|
||
}
|
||
|
||
const response = await fetch('/voice-clone/api/voice-clone/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showSuccess(result.message);
|
||
loadVoiceStatus(); // 重新加载状态
|
||
|
||
// 清空表单
|
||
document.getElementById('voice-upload-form').reset();
|
||
document.getElementById('audio-preview').style.display = 'none';
|
||
document.getElementById('upload-btn').disabled = true;
|
||
} else {
|
||
showError(result.message);
|
||
}
|
||
|
||
} catch (error) {
|
||
showError('上传失败,请检查网络连接');
|
||
console.error('上传失败:', error);
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成语音
|
||
*/
|
||
async function generateSpeech(e) {
|
||
e.preventDefault();
|
||
|
||
const text = document.getElementById('speech-text').value.trim();
|
||
if (!text) {
|
||
showError('请输入要合成的文本');
|
||
return;
|
||
}
|
||
|
||
showLoading('正在用你的声音生成语音...', '请耐心等待,这很神奇哦');
|
||
|
||
try {
|
||
const response = await fetch('/voice-clone/api/voice-clone/generate', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
text: text
|
||
})
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
showSuccess(result.message);
|
||
|
||
// 显示生成的音频
|
||
const generatedAudio = document.getElementById('generated-audio');
|
||
const generatedSource = document.getElementById('generated-source');
|
||
|
||
// 创建音频URL
|
||
const filename = result.audio_url.split('/').pop();
|
||
generatedSource.src = `/voice-test/download-audio/${filename}`;
|
||
generatedAudio.load();
|
||
document.getElementById('speech-result').style.display = 'block';
|
||
|
||
} else {
|
||
showError(result.message);
|
||
}
|
||
|
||
} catch (error) {
|
||
showError('生成失败,请检查网络连接');
|
||
console.error('生成失败:', error);
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|