Happy_language/app/static/js/voice_clone.js
2025-09-22 06:06:19 +08:00

375 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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