715 lines
22 KiB
JavaScript
715 lines
22 KiB
JavaScript
/**
|
||
* 语音克隆测试页面 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 = `<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);
|
||
}
|
||
|
||
/**
|
||
* 创建音频播放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 = '<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">支持语音克隆、识别、自然控制</small>
|
||
</span>
|
||
`;
|
||
addLog(`连接成功!核心功能可用:语音克隆、识别、自然控制`, '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('获取随机种子失败');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理文件选择
|
||
*/
|
||
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 = '<i class="fas fa-upload me-2"></i>上传并识别语音';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 开始录音
|
||
*/
|
||
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 = '<i class="fas fa-upload me-2"></i>上传并识别语音';
|
||
|
||
// 保存录音数据
|
||
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();
|
||
}
|
||
}
|