ai-clone/frontend-ai/unpackage/dist/build/app-plus/static/upload.html
2026-03-05 14:29:21 +08:00

817 lines
34 KiB
HTML
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.

<!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>