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