ai-clone/frontend-ai/static/upload.html

817 lines
34 KiB
HTML
Raw Normal View History

2026-03-05 14:29:21 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传音频</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #FDF8F2 0%, #F5EDE3 100%);
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 600px;
margin: 0 auto;
}
.upload-card {
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: bold;
color: #8B7355;
margin-bottom: 20px;
text-align: center;
}
.upload-area {
border: 3px dashed #D4B996;
border-radius: 15px;
padding: 40px 20px;
text-align: center;
background: rgba(212, 185, 150, 0.05);
cursor: pointer;
transition: all 0.3s;
margin-bottom: 20px;
}
.upload-area:hover {
border-color: #8B7355;
background: rgba(139, 115, 85, 0.1);
}
.upload-area.dragover {
border-color: #8B7355;
background: rgba(139, 115, 85, 0.15);
}
.upload-icon {
font-size: 48px;
margin-bottom: 10px;
}
.upload-text {
font-size: 16px;
color: #666;
margin-bottom: 10px;
}
.upload-hint {
font-size: 12px;
color: #999;
}
.file-input {
display: none;
}
.form-group {
margin-bottom: 20px;
}
.label {
display: block;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.input {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s;
}
.input:focus {
outline: none;
border-color: #8B7355;
}
.hint {
font-size: 12px;
color: #999;
margin-top: 5px;
}
.file-info {
background: #e8f5e9;
border-left: 4px solid #4caf50;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
.file-info.show {
display: block;
}
.file-name {
font-size: 14px;
color: #2e7d32;
font-weight: 600;
margin-bottom: 5px;
}
.file-size {
font-size: 12px;
color: #66bb6a;
}
.tips-card {
background: rgba(212, 185, 150, 0.1);
padding: 20px;
border-radius: 15px;
border-left: 4px solid #8B7355;
margin-bottom: 20px;
}
.tips-title {
font-size: 14px;
font-weight: bold;
color: #8B7355;
margin-bottom: 10px;
}
.tips-list {
font-size: 13px;
color: #555;
line-height: 1.8;
}
.tip-item {
margin-bottom: 5px;
}
.btn {
width: 100%;
padding: 15px;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
}
.btn-secondary {
background: linear-gradient(135deg, #9e9e9e 0%, #757575 100%);
}
.btn-secondary:hover {
background: linear-gradient(135deg, #757575 0%, #616161 100%);
}
.btn-danger {
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
}
.btn-danger:hover {
background: linear-gradient(135deg, #d32f2f 0%, #c62828 100%);
}
.btn-primary {
background: linear-gradient(135deg, #8B7355 0%, #6D8B8B 100%);
color: white;
box-shadow: 0 4px 15px rgba(139, 115, 85, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(139, 115, 85, 0.4);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.message {
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
font-size: 14px;
display: none;
}
.message.show {
display: block;
}
.message.success {
background: #d4edda;
color: #155724;
border-left: 4px solid #28a745;
}
.message.error {
background: #f8d7da;
color: #721c24;
border-left: 4px solid #dc3545;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin-bottom: 20px;
display: none;
}
.progress-bar.show {
display: block;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #8B7355 0%, #6D8B8B 100%);
width: 0%;
transition: width 0.3s;
}
</style>
</head>
<body>
<div class="container">
<div class="upload-card">
<h1 class="title">🎤 录制音色</h1>
<div id="message" class="message"></div>
<!-- 示例文案 -->
<div style="background: #FFF9F0; border-radius: 12px; padding: 20px; margin-bottom: 20px; border: 2px solid #FFE4C4;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div style="font-size: 14px; font-weight: 600; color: #8B7355;">📝 参考文案(照着读)</div>
<button id="refreshTextBtn" style="background: none; border: none; color: #8B7355; cursor: pointer; font-size: 20px; padding: 5px;" title="换一句">🔄</button>
</div>
<div id="sampleText" style="font-size: 16px; line-height: 1.8; color: #333; padding: 12px; background: white; border-radius: 8px; border-left: 4px solid #8B7355;"></div>
</div>
<!-- 录音模式 -->
<div id="recordMode">
<div class="upload-area" style="padding: 40px;">
<div class="upload-icon" id="recordIcon">🎙️</div>
<div class="upload-text" id="recordText">点击开始录音</div>
<div class="upload-hint" id="recordHint">建议录制10-20秒清晰人声</div>
<button id="recordBtn" class="btn btn-primary" style="margin-top: 20px;">开始录音</button>
<div id="recordTimer" style="font-size: 24px; font-weight: bold; margin-top: 10px; display: none;">00:00</div>
</div>
</div>
<div id="fileInfo" class="file-info">
<div class="file-name" id="fileName"></div>
<div class="file-size" id="fileSize"></div>
</div>
<div class="form-group">
<label class="label">音色显示名称</label>
<input type="text" id="displayName" class="input" placeholder="请输入音色显示名称(如:妈妈的声音)" maxlength="20">
<div class="hint">💡 音色名称将显示在音色列表中</div>
</div>
<div class="form-group">
<label class="label" style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="useCosyVoice" style="width: 16px; height: 16px;">
使用 CosyVoice-v3-plus 模型
</label>
<div class="hint">选中方可进行方言复刻</div>
</div>
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>
<button id="uploadBtn" class="btn btn-primary" disabled>🎤 创建音色</button>
</div>
<div class="tips-card">
<div class="tips-title">📋 音频要求</div>
<div class="tips-list">
<div class="tip-item">✓ 推荐时长10-20秒最长60秒</div>
<div class="tip-item">✓ 必须包含至少3秒连续清晰朗读</div>
<div class="tip-item">✓ 避免背景音乐、噪音或其他人声</div>
<div class="tip-item">✓ 采样率 ≥ 24kHz单声道</div>
</div>
</div>
</div>
<script>
// 配置API地址 - 从URL参数获取或使用本地地址
const urlParams = new URLSearchParams(window.location.search);
// 默认指向生产环境,可通过 ?apiBase=... 覆盖
const API_BASE_URL = urlParams.get('apiBase') || 'http://115.190.167.176:20002';
const fileInfo = document.getElementById('fileInfo');
const fileName = document.getElementById('fileName');
const fileSize = document.getElementById('fileSize');
const displayName = document.getElementById('displayName');
const uploadBtn = document.getElementById('uploadBtn');
const useCosyVoice = document.getElementById('useCosyVoice');
const message = document.getElementById('message');
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
let selectedFile = null;
let mediaRecorder = null;
let audioChunks = [];
let recordingTimer = null;
let recordingSeconds = 0;
// 示例文案列表(注重口齿清晰和发音准确)
const sampleTexts = [
'春天来了,桃花盛开,柳树发芽,燕子从南方飞回来了。公园里到处都是游玩的人群,孩子们在草地上奔跑嬉戏,老人们在树荫下悠闲地聊天下棋。',
'清晨的阳光透过窗帘洒进房间,鸟儿在枝头欢快地歌唱。我起床后打开窗户,深深地吸了一口新鲜空气,感觉整个人都精神了许多。',
'超市里的商品琳琅满目,蔬菜水果新鲜诱人。我推着购物车慢慢挑选,买了西红柿、黄瓜、苹果和香蕉,还有一些日常生活用品。',
'图书馆里安静极了,只能听到翻书的沙沙声。学生们都在认真地看书学习,有的在做笔记,有的在查阅资料,整个环境充满了浓厚的学习氛围。',
'傍晚时分,夕阳西下,天边的云彩被染成了金黄色和橘红色。我站在阳台上眺望远方,看着城市的灯光逐渐亮起,心情格外平静。',
'周末的时候,我喜欢去公园散步。沿着湖边的小路慢慢走,看着湖面上波光粼粼,偶尔有几只野鸭游过,感觉特别惬意舒适。',
'厨房里飘来阵阵香味,妈妈正在准备晚餐。她熟练地切菜、炒菜,不一会儿就做好了几道色香味俱全的家常菜,让人食欲大增。',
'秋天是收获的季节,田野里金黄的稻谷随风摇曳。农民伯伯们忙着收割庄稼,脸上洋溢着丰收的喜悦,到处都是一片繁忙景象。',
'下雨了,雨点打在窗户上发出滴答滴答的声音。街道上的行人撑着五颜六色的雨伞匆匆走过,汽车开过水洼溅起一片片水花。',
'冬天的早晨特别冷,呼出的气都变成了白雾。人们都穿上了厚厚的棉衣,戴着帽子和手套,小心翼翼地走在结冰的路面上。',
'音乐会上,钢琴家的手指在琴键上灵活地跳动,优美的旋律在大厅里回荡。观众们都静静地聆听,完全沉浸在美妙的音乐世界中。',
'博物馆里陈列着各种各样的文物和艺术品。游客们仔细观看每一件展品,认真阅读旁边的说明文字,不时发出赞叹的声音。',
'火车站里人来人往,非常热闹。有的人拖着行李箱急匆匆地赶路,有的人在候车室里耐心等待,广播里不断传来列车到站和发车的通知。',
'花园里种满了各种各样的花草树木。玫瑰、牡丹、菊花竞相开放,蝴蝶和蜜蜂在花丛中飞舞,整个花园充满了生机和活力。',
'夜晚的星空格外美丽,繁星点点,银河横跨天际。我躺在草地上仰望星空,试图找出北斗七星和其他熟悉的星座,感受宇宙的浩瀚无垠。'
];
// 随机显示示例文案
function showRandomText() {
const sampleTextEl = document.getElementById('sampleText');
const randomIndex = Math.floor(Math.random() * sampleTexts.length);
sampleTextEl.textContent = sampleTexts[randomIndex];
}
// 页面加载时显示随机文案
showRandomText();
// 刷新按钮点击事件
document.getElementById('refreshTextBtn').addEventListener('click', () => {
showRandomText();
});
// 转换音频为WAV格式
async function convertToWav(blob) {
// 如果Blob已经是WAV格式直接返回
if (blob.type === 'audio/wav' || blob.type === 'audio/x-wav') {
return blob;
}
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const arrayBuffer = await blob.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
// 转换为WAV
const wavBuffer = audioBufferToWav(audioBuffer);
return new Blob([wavBuffer], { type: 'audio/wav' });
} catch (err) {
throw new Error('音频解码失败: ' + err.message);
}
}
// AudioBuffer转WAV
function audioBufferToWav(buffer) {
const numChannels = buffer.numberOfChannels;
const sampleRate = buffer.sampleRate;
const format = 1; // PCM
const bitDepth = 16;
const bytesPerSample = bitDepth / 8;
const blockAlign = numChannels * bytesPerSample;
const dataLength = buffer.length * blockAlign;
const bufferLength = 44 + dataLength;
const arrayBuffer = new ArrayBuffer(bufferLength);
const view = new DataView(arrayBuffer);
// 写入WAV文件头
let pos = 0;
// "RIFF" chunk descriptor
writeString(view, pos, 'RIFF'); pos += 4;
view.setUint32(pos, bufferLength - 8, true); pos += 4;
writeString(view, pos, 'WAVE'); pos += 4;
// "fmt " sub-chunk
writeString(view, pos, 'fmt '); pos += 4;
view.setUint32(pos, 16, true); pos += 4; // SubChunk1Size (16 for PCM)
view.setUint16(pos, format, true); pos += 2; // AudioFormat (1 for PCM)
view.setUint16(pos, numChannels, true); pos += 2;
view.setUint32(pos, sampleRate, true); pos += 4;
view.setUint32(pos, sampleRate * blockAlign, true); pos += 4; // ByteRate
view.setUint16(pos, blockAlign, true); pos += 2;
view.setUint16(pos, bitDepth, true); pos += 2;
// "data" sub-chunk
writeString(view, pos, 'data'); pos += 4;
view.setUint32(pos, dataLength, true); pos += 4;
// 写入音频数据
const channels = [];
for (let i = 0; i < numChannels; i++) {
channels.push(buffer.getChannelData(i));
}
let offset = 0;
while (offset < buffer.length) {
for (let i = 0; i < numChannels; i++) {
let sample = channels[i][offset];
// 限制在 [-1, 1] 范围内
sample = Math.max(-1, Math.min(1, sample));
// 转换为16位整数
sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
view.setInt16(pos, sample, true);
pos += 2;
}
offset++;
}
return arrayBuffer;
function writeString(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
}
// 录音功能
const recordBtn = document.getElementById('recordBtn');
const recordTimer = document.getElementById('recordTimer');
const recordIcon = document.getElementById('recordIcon');
const recordText = document.getElementById('recordText');
const recordHint = document.getElementById('recordHint');
recordBtn.addEventListener('click', async () => {
if (!mediaRecorder || mediaRecorder.state === 'inactive') {
// 开始录音
try {
// 检查浏览器是否支持录音
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
alert('您的浏览器不支持录音功能,请使用文件上传');
return;
}
// 首次使用时,提示用户即将请求麦克风权限
const hasShownPermissionHint = sessionStorage.getItem('micPermissionHintShown');
if (!hasShownPermissionHint) {
const userConfirm = confirm('📢 需要使用麦克风录音\n\n点击"确定"后,请在弹出的权限请求中允许使用麦克风。\n\n如果不想授权可以使用"上传文件"功能。');
if (!userConfirm) {
return;
}
sessionStorage.setItem('micPermissionHintShown', 'true');
}
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 48000
}
});
// 尝试不同的MIME类型
let mimeType = '';
const mimeTypes = [
'audio/webm;codecs=opus',
'audio/webm',
'audio/ogg;codecs=opus',
'audio/mp4',
''
];
for (const type of mimeTypes) {
if (type === '' || MediaRecorder.isTypeSupported(type)) {
mimeType = type;
break;
}
}
const options = mimeType ? { mimeType } : {};
mediaRecorder = new MediaRecorder(stream, options);
audioChunks = [];
recordingSeconds = 0;
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
audioChunks.push(e.data);
}
};
mediaRecorder.onstop = async () => {
// 使用浏览器原生格式创建Blob
const audioBlob = new Blob(audioChunks, { type: mediaRecorder.mimeType });
// 检查录音时长
if (recordingSeconds < 3) {
alert('❌ 录音时长不足\n\n录音时长太短未满足最小时长要求至少3秒。\n\n请重新录制建议录制10-20秒清晰人声。');
// 停止所有音轨
stream.getTracks().forEach(track => track.stop());
return;
}
// 检查录音数据是否有效
if (audioChunks.length === 0 || audioBlob.size === 0) {
alert('❌ 录音数据无效\n\n未能捕获到有效的音频数据可能是录音质量不够或麦克风问题。\n\n请检查麦克风设置后重新录制或使用"上传文件"功能。');
// 停止所有音轨
stream.getTracks().forEach(track => track.stop());
return;
}
// 转换为WAV格式
try {
const wavBlob = await convertToWav(audioBlob);
// 再次检查转换后的文件大小
if (wavBlob.size < 1000) {
alert('❌ 录音质量不够\n\n音频文件过小可能录音质量不够或未满足最小时长要求。\n\n请确保\n• 录音时长至少3秒\n• 麦克风正常工作\n• 环境安静,声音清晰\n\n建议重新录制10-20秒清晰人声。');
stream.getTracks().forEach(track => track.stop());
return;
}
const audioFile = new File([wavBlob], `recording_${Date.now()}.wav`, { type: 'audio/wav' });
handleFile(audioFile);
} catch (err) {
console.error('音频转换错误:', err);
alert('❌ 录音处理失败\n\n音频转换失败可能是录音质量不够或格式不支持。\n\n错误信息' + err.message + '\n\n请重新录制或使用"上传文件"功能。');
}
// 停止所有音轨
stream.getTracks().forEach(track => track.stop());
};
// 每100ms收集一次数据
mediaRecorder.start(100);
recordBtn.textContent = '停止录音';
recordBtn.classList.add('btn-danger');
recordBtn.classList.remove('btn-primary');
recordIcon.textContent = '🔴';
recordText.textContent = '正在录音...';
recordHint.textContent = '点击停止录音按钮结束';
recordTimer.style.display = 'block';
// 开始计时
recordingTimer = setInterval(() => {
recordingSeconds++;
const minutes = Math.floor(recordingSeconds / 60);
const seconds = recordingSeconds % 60;
recordTimer.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}, 1000);
} catch (err) {
console.error('录音错误:', err);
console.error('错误名称:', err.name);
console.error('错误消息:', err.message);
// 检查错误消息内容,判断是否为权限问题
const errorMsg = (err.message || '').toLowerCase();
const errorName = (err.name || '').toLowerCase();
const isPermissionError =
errorName.includes('notallowed') ||
errorName.includes('permission') ||
errorMsg.includes('permission') ||
errorMsg.includes('denied') ||
errorMsg.includes('not allowed') ||
errorMsg.includes('user denied');
const isDeviceError =
errorName.includes('notfound') ||
errorName.includes('devicesnotfound') ||
errorMsg.includes('not found');
const isOccupiedError =
errorName.includes('notreadable') ||
errorName.includes('trackstart') ||
errorMsg.includes('not readable') ||
errorMsg.includes('occupied') ||
errorMsg.includes('in use');
// 根据错误类型给出友好提示
if (isPermissionError) {
alert('❌ 需要麦克风权限\n\n录音功能需要麦克风权限才能使用。\n\n请前往\n手机设置 → 应用管理 → 时光意境 → 权限管理 → 开启麦克风权限');
} else if (isDeviceError) {
alert('❌ 未找到麦克风设备\n\n请检查\n1. 麦克风是否已连接\n2. 应用是否有麦克风权限\n\n如需开启权限请前往\n手机设置 → 应用管理 → 时光意境 → 权限管理');
} else if (isOccupiedError) {
alert('❌ 麦克风被占用或权限未开启\n\n可能原因\n1. 其他应用正在使用麦克风\n2. 应用权限未开启\n\n请前往\n手机设置 → 应用管理 → 时光意境 → 权限管理 → 开启麦克风权限');
} else if (errorName.includes('security')) {
alert('❌ 安全限制:无法访问麦克风\n\n请检查应用麦克风权限是否已开启。\n\n前往\n手机设置 → 应用管理 → 时光意境 → 权限管理');
} else {
alert('❌ 无法访问麦克风\n\n错误信息' + err.message + '\n\n请前往系统设置检查应用麦克风权限\n手机设置 → 应用管理 → 时光意境 → 权限管理');
}
}
} else {
// 停止录音
mediaRecorder.stop();
recordBtn.textContent = '开始录音';
recordBtn.classList.remove('btn-danger');
recordBtn.classList.add('btn-primary');
recordIcon.textContent = '🎙️';
recordText.textContent = '录音完成';
recordHint.textContent = '可以重新录制或上传';
recordTimer.style.display = 'none';
clearInterval(recordingTimer);
}
});
// 移除文件上传相关事件监听
// 处理文件
function handleFile(file) {
if (!file) return;
// 验证文件类型
const validTypes = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/m4a', 'audio/aac', 'audio/x-m4a'];
const fileExt = file.name.split('.').pop().toLowerCase();
const validExts = ['mp3', 'wav', 'm4a', 'aac'];
if (!validTypes.includes(file.type) && !validExts.includes(fileExt)) {
showMessage('error', '请选择音频文件MP3, WAV, M4A, AAC');
return;
}
// 验证文件大小
if (file.size > 10 * 1024 * 1024) {
showMessage('error', '文件大小不能超过 10MB');
return;
}
selectedFile = file;
// 显示文件信息
fileName.textContent = `📄 ${file.name}`;
fileSize.textContent = `大小: ${(file.size / 1024).toFixed(2)} KB`;
fileInfo.classList.add('show');
// 启用上传按钮
updateUploadButton();
showMessage('success', `已选择文件: ${file.name}`);
}
// 监听显示名称输入
displayName.addEventListener('input', updateUploadButton);
// 更新上传按钮状态
function updateUploadButton() {
uploadBtn.disabled = !selectedFile || !displayName.value.trim();
}
// 上传文件
uploadBtn.addEventListener('click', async () => {
if (!selectedFile || !displayName.value.trim()) {
showMessage('error', '请选择文件并输入音色名称');
return;
}
uploadBtn.disabled = true;
uploadBtn.textContent = '⏳ 上传中...';
progressBar.classList.add('show');
// 获取登录信息 - 从URL参数或localStorage
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token') || localStorage.getItem('token') || '';
const userId = urlParams.get('userId') || localStorage.getItem('userId') || '';
console.log('[Upload] token:', token ? '有' : '无', 'userId:', userId || '无');
if (!token || !userId) {
showMessage('error', '请先登录。token: ' + (token ? '有' : '无') + ', userId: ' + (userId || '无'));
uploadBtn.disabled = false;
uploadBtn.textContent = '🎤 创建音色';
progressBar.classList.remove('show');
return;
}
try {
const formData = new FormData();
formData.append('audio', selectedFile);
const voiceName = displayName.value.trim();
formData.append('name', voiceName);
formData.append('displayName', voiceName);
const xhr = new XMLHttpRequest();
// 上传进度
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
progressFill.style.width = percent + '%';
}
});
// 上传完成
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
try {
const data = JSON.parse(xhr.responseText);
if (data.success) {
// 使用弹窗提示成功
alert(`✅ 音色创建成功!\n\n音色名称${displayName.value}\n\n您可以在"声音克隆"中使用该音色了。`);
// 重置表单
selectedFile = null;
displayName.value = '';
fileInfo.classList.remove('show');
progressBar.classList.remove('show');
progressFill.style.width = '0%';
uploadBtn.textContent = '🎤 创建音色';
updateUploadButton();
// 通知父页面刷新并返回
if (window.parent) {
window.parent.postMessage({ type: 'voiceCreated' }, '*');
}
} else {
showMessage('error', data.message || '创建失败');
uploadBtn.disabled = false;
uploadBtn.textContent = '🎤 创建音色';
}
} catch (e) {
showMessage('error', '服务器响应格式错误');
uploadBtn.disabled = false;
uploadBtn.textContent = '🎤 创建音色';
}
} else {
showMessage('error', `上传失败: HTTP ${xhr.status}`);
uploadBtn.disabled = false;
uploadBtn.textContent = '🎤 创建音色';
}
});
// 上传失败
xhr.addEventListener('error', () => {
showMessage('error', '网络错误,上传失败');
uploadBtn.disabled = false;
uploadBtn.textContent = '🎤 创建音色';
progressBar.classList.remove('show');
});
let endpoint = `${API_BASE_URL}/api/voice/create`;
if (useCosyVoice && useCosyVoice.checked) {
endpoint = `${API_BASE_URL}/api/voice/cosy-create`;
}
xhr.open('POST', endpoint);
// 设置认证请求头
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
if (userId) {
xhr.setRequestHeader('X-User-Id', userId);
}
xhr.send(formData);
} catch (error) {
console.error('上传错误:', error);
showMessage('error', '上传失败: ' + error.message);
uploadBtn.disabled = false;
uploadBtn.textContent = '🎤 创建音色';
progressBar.classList.remove('show');
}
});
// 显示消息
function showMessage(type, text) {
message.className = `message ${type} show`;
message.textContent = text;
setTimeout(() => {
message.classList.remove('show');
}, 5000);
}
</script>
</body>
</html>