2026-08-29 20:44:33 +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>火山 TTS · 安装引导</title>
|
|
|
|
|
|
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.4.21/vue.global.prod.js"></script>
|
|
|
|
|
|
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.6.7/axios.min.js"></script>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
:root {
|
2026-08-30 00:51:23 +08:00
|
|
|
|
--bg: #0a0e1a;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
--bg-card: #131826;
|
|
|
|
|
|
--bg-elev: #1a2138;
|
2026-08-30 00:51:23 +08:00
|
|
|
|
--bg-hover: #1f2740;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
--border: #232b42;
|
2026-08-30 00:51:23 +08:00
|
|
|
|
--border-strong: #2e3754;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
--text: #e4e7ee;
|
2026-08-30 00:51:23 +08:00
|
|
|
|
--text-dim: #94a3b8;
|
|
|
|
|
|
--text-mute: #64748b;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
--accent: #00d4ff;
|
2026-08-30 00:51:23 +08:00
|
|
|
|
--accent-hover: #33ddff;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
--accent-2: #7b2ff7;
|
2026-08-30 00:51:23 +08:00
|
|
|
|
--accent-soft: rgba(0, 212, 255, 0.10);
|
|
|
|
|
|
--accent-glow: rgba(0, 212, 255, 0.35);
|
2026-08-29 20:44:33 +08:00
|
|
|
|
--success: #22c55e;
|
2026-08-30 00:51:23 +08:00
|
|
|
|
--success-soft: rgba(34, 197, 94, 0.12);
|
2026-08-29 20:44:33 +08:00
|
|
|
|
--warning: #f59e0b;
|
2026-08-30 00:51:23 +08:00
|
|
|
|
--warning-soft: rgba(245, 158, 11, 0.12);
|
2026-08-29 20:44:33 +08:00
|
|
|
|
--danger: #ef4444;
|
2026-08-30 00:51:23 +08:00
|
|
|
|
--danger-soft: rgba(239, 68, 68, 0.12);
|
|
|
|
|
|
--radius: 8px;
|
|
|
|
|
|
--radius-lg: 12px;
|
|
|
|
|
|
--radius-xl: 16px;
|
|
|
|
|
|
--shadow: 0 4px 16px rgba(0,0,0,0.25);
|
|
|
|
|
|
--shadow-lg: 0 20px 50px rgba(0,0,0,0.5);
|
2026-08-29 20:44:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
|
|
|
|
background: var(--bg); color: var(--text);
|
2026-08-30 00:51:23 +08:00
|
|
|
|
min-height: 100vh; padding: 40px 24px; font-size: 14px; line-height: 1.5;
|
|
|
|
|
|
-webkit-font-smoothing: antialiased;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
background-image:
|
2026-08-30 00:51:23 +08:00
|
|
|
|
radial-gradient(ellipse 70% 50% at 30% 0%, rgba(0,212,255,0.07), transparent),
|
|
|
|
|
|
radial-gradient(ellipse 50% 40% at 100% 100%, rgba(123,47,247,0.05), transparent);
|
|
|
|
|
|
background-attachment: fixed;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
#app { max-width: 760px; margin: 0 auto; }
|
|
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
/* === Header === */
|
2026-08-29 20:44:33 +08:00
|
|
|
|
.header { display: flex; align-items: center; gap: 16px; margin-bottom: 28px; }
|
|
|
|
|
|
.logo {
|
2026-08-30 00:51:23 +08:00
|
|
|
|
width: 52px; height: 52px; border-radius: 14px;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
2026-08-30 00:51:23 +08:00
|
|
|
|
font-weight: 700; font-size: 20px; color: #fff;
|
|
|
|
|
|
box-shadow: 0 6px 24px var(--accent-glow);
|
2026-08-29 20:44:33 +08:00
|
|
|
|
}
|
2026-08-30 00:51:23 +08:00
|
|
|
|
.header-text h1 { font-size: 22px; font-weight: 600; letter-spacing: -0.01em; }
|
|
|
|
|
|
.header-text .sub { font-size: 13px; color: var(--text-dim); margin-top: 4px; }
|
2026-08-29 20:44:33 +08:00
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
/* === Steps === */
|
|
|
|
|
|
.steps { display: flex; align-items: center; gap: 0; margin-bottom: 28px; padding: 16px 20px; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); }
|
|
|
|
|
|
.step { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
|
|
|
|
|
|
.step-num {
|
|
|
|
|
|
width: 28px; height: 28px; border-radius: 50%;
|
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
|
font-size: 12px; font-weight: 600; flex-shrink: 0;
|
|
|
|
|
|
background: var(--bg-elev); color: var(--text-mute);
|
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
|
}
|
|
|
|
|
|
.step.active .step-num { background: var(--accent); color: #00121a; border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
|
|
|
|
|
|
.step.done .step-num { background: var(--success-soft); color: var(--success); border-color: rgba(34,197,94,0.3); }
|
|
|
|
|
|
.step-label { font-size: 13px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
|
|
.step.active .step-label, .step.done .step-label { color: var(--text); }
|
|
|
|
|
|
.step-line { flex: 1; height: 1px; background: var(--border); margin: 0 12px; min-width: 16px; }
|
|
|
|
|
|
.step.done + .step-line { background: var(--accent); opacity: 0.4; }
|
|
|
|
|
|
|
|
|
|
|
|
/* === Card === */
|
|
|
|
|
|
.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 24px; margin-bottom: 16px; }
|
|
|
|
|
|
.card-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 18px; gap: 12px; }
|
|
|
|
|
|
.card-title { font-size: 16px; font-weight: 600; }
|
|
|
|
|
|
.card-sub { font-size: 12px; color: var(--text-dim); margin-top: 4px; line-height: 1.6; }
|
|
|
|
|
|
.step-badge { font-size: 11px; padding: 3px 8px; border-radius: 999px; background: var(--accent-soft); color: var(--accent); border: 1px solid rgba(0,212,255,0.3); font-weight: 500; }
|
2026-08-29 20:44:33 +08:00
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
/* === Form === */
|
|
|
|
|
|
.field { margin-bottom: 16px; }
|
|
|
|
|
|
.field label { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 6px; font-weight: 500; }
|
|
|
|
|
|
.field label .req { color: var(--danger); margin-left: 2px; }
|
2026-08-29 20:44:33 +08:00
|
|
|
|
.field input, .field select {
|
|
|
|
|
|
width: 100%; background: var(--bg-elev); border: 1px solid var(--border);
|
2026-08-30 00:51:23 +08:00
|
|
|
|
color: var(--text); padding: 10px 12px; border-radius: var(--radius); font-size: 13px;
|
|
|
|
|
|
font-family: inherit; transition: border-color 0.15s, box-shadow 0.15s;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
}
|
2026-08-30 00:51:23 +08:00
|
|
|
|
.field input:focus, .field select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
|
|
|
|
.field input::placeholder { color: var(--text-mute); }
|
|
|
|
|
|
.field select {
|
|
|
|
|
|
appearance: none;
|
|
|
|
|
|
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'><path fill='%238a92a6' d='M6 8L1 3h10z'/></svg>");
|
|
|
|
|
|
background-repeat: no-repeat; background-position: right 10px center; padding-right: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.field .hint { font-size: 11px; color: var(--text-mute); margin-top: 5px; line-height: 1.5; }
|
|
|
|
|
|
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
|
|
|
|
|
.field-row .field { margin-bottom: 0; }
|
|
|
|
|
|
code { font-family: ui-monospace, SFMono-Regular, monospace; font-size: 0.9em; background: var(--bg-elev); padding: 1px 5px; border-radius: 4px; color: var(--accent); }
|
2026-08-29 20:44:33 +08:00
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
/* === Voice list === */
|
|
|
|
|
|
.voice-row {
|
|
|
|
|
|
display: grid; grid-template-columns: 1fr 1fr 1.4fr 1.2fr 36px;
|
|
|
|
|
|
gap: 10px; align-items: center; margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-row input { font-size: 12px; padding: 8px 10px; font-family: ui-monospace, SFMono-Regular, monospace; }
|
|
|
|
|
|
.voice-row .rm {
|
|
|
|
|
|
background: transparent; border: 1px solid var(--border);
|
|
|
|
|
|
color: var(--text-mute); cursor: pointer; padding: 7px 0;
|
|
|
|
|
|
border-radius: var(--radius); font-size: 15px;
|
|
|
|
|
|
transition: all 0.12s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.voice-row .rm:hover:not(:disabled) { border-color: var(--danger); color: var(--danger); background: var(--danger-soft); }
|
|
|
|
|
|
.voice-row .rm:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
|
|
|
|
.voice-row-head {
|
|
|
|
|
|
display: grid; grid-template-columns: 1fr 1fr 1.4fr 1.2fr 36px;
|
|
|
|
|
|
gap: 10px; font-size: 11px; color: var(--text-mute);
|
|
|
|
|
|
text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;
|
|
|
|
|
|
padding: 0 2px 6px;
|
|
|
|
|
|
}
|
2026-08-29 20:44:33 +08:00
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
/* === Buttons === */
|
2026-08-29 20:44:33 +08:00
|
|
|
|
.btn {
|
2026-08-30 00:51:23 +08:00
|
|
|
|
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
background: var(--bg-elev); border: 1px solid var(--border); color: var(--text);
|
2026-08-30 00:51:23 +08:00
|
|
|
|
padding: 10px 18px; border-radius: var(--radius); cursor: pointer; font-size: 13px; font-weight: 500;
|
|
|
|
|
|
font-family: inherit; transition: all 0.12s;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
}
|
2026-08-30 00:51:23 +08:00
|
|
|
|
.btn:hover:not(:disabled) { border-color: var(--border-strong); background: var(--bg-hover); }
|
|
|
|
|
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
|
|
.btn-primary { background: var(--accent); color: #00121a; border-color: var(--accent); }
|
|
|
|
|
|
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); }
|
2026-08-29 20:44:33 +08:00
|
|
|
|
.btn-add { background: transparent; border: 1px dashed var(--border); color: var(--text-dim); width: 100%; justify-content: center; padding: 10px; }
|
2026-08-30 00:51:23 +08:00
|
|
|
|
.btn-add:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); border-style: solid; }
|
2026-08-29 20:44:33 +08:00
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
/* === Actions === */
|
|
|
|
|
|
.actions { display: flex; justify-content: space-between; align-items: center; margin-top: 24px; padding: 20px 24px; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg); }
|
|
|
|
|
|
.actions .hint-text { color: var(--text-mute); font-size: 12px; }
|
2026-08-29 20:44:33 +08:00
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
/* === Alerts === */
|
|
|
|
|
|
.alert { display: flex; align-items: flex-start; gap: 10px; padding: 10px 14px; border-radius: var(--radius); margin-bottom: 16px; font-size: 13px; }
|
|
|
|
|
|
.alert-icon { font-size: 15px; flex-shrink: 0; }
|
|
|
|
|
|
.alert-body { flex: 1; }
|
|
|
|
|
|
.alert-danger { background: var(--danger-soft); border: 1px solid rgba(239,68,68,0.3); color: var(--danger); }
|
|
|
|
|
|
.alert-warning { background: var(--warning-soft); border: 1px solid rgba(245,158,11,0.3); color: var(--warning); }
|
2026-08-29 20:44:33 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div id="app">
|
2026-08-30 00:51:23 +08:00
|
|
|
|
|
2026-08-29 20:44:33 +08:00
|
|
|
|
<div class="header">
|
|
|
|
|
|
<div class="logo">TTS</div>
|
2026-08-30 00:51:23 +08:00
|
|
|
|
<div class="header-text">
|
2026-08-29 20:44:33 +08:00
|
|
|
|
<h1>火山 TTS 聚合平台 · 首次安装</h1>
|
|
|
|
|
|
<div class="sub">填写以下信息完成初始化。token 来自启动日志或环境变量 <code>TTS_ADMIN_KEY</code>。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
<!-- Steps -->
|
|
|
|
|
|
<div class="steps">
|
|
|
|
|
|
<div class="step" :class="{active: step===1, done: step>1}">
|
|
|
|
|
|
<div class="step-num">1</div>
|
|
|
|
|
|
<div class="step-label">上游凭证</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step-line"></div>
|
|
|
|
|
|
<div class="step" :class="{active: step===2, done: step>2}">
|
|
|
|
|
|
<div class="step-num">2</div>
|
|
|
|
|
|
<div class="step-label">默认路由</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step-line"></div>
|
|
|
|
|
|
<div class="step" :class="{active: step===3, done: step>3}">
|
|
|
|
|
|
<div class="step-num">3</div>
|
|
|
|
|
|
<div class="step-label">音色列表</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="step-line"></div>
|
|
|
|
|
|
<div class="step" :class="{active: step===4}">
|
|
|
|
|
|
<div class="step-num">4</div>
|
|
|
|
|
|
<div class="step-label">完成安装</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-08-29 20:44:33 +08:00
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
<div v-if="errorMsg" class="alert alert-danger">
|
|
|
|
|
|
<span class="alert-icon">⚠</span>
|
|
|
|
|
|
<div class="alert-body">{{ errorMsg }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step 1: 上游凭证 -->
|
|
|
|
|
|
<div v-show="step===1" class="card">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="card-title">OpenAI 鉴权 + 火山引擎凭证</div>
|
|
|
|
|
|
<div class="card-sub">两个 Key 仅在此页面输入,不会回显,装完不用再碰 env。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="step-badge">第 1 步</span>
|
|
|
|
|
|
</div>
|
2026-08-29 20:44:33 +08:00
|
|
|
|
<div class="field">
|
2026-08-29 23:18:51 +08:00
|
|
|
|
<label>OpenAI 鉴权 Key <span class="req">*</span></label>
|
|
|
|
|
|
<input type="password" v-model="form.auth_key" placeholder="自定,例如 my-secret-key-2024" autocomplete="off">
|
|
|
|
|
|
<div class="hint">客户端用这个 Key 调 <code>/v1/audio/speech</code>,也是登录 <code>/admin</code> 的密码。装完可在 <code>/admin → 设置</code> 改。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>火山 TTS API Key <span class="req">*</span></label>
|
2026-08-29 20:44:33 +08:00
|
|
|
|
<input type="password" v-model="form.api_key" placeholder="例如: 5b4d7c2a-..." autocomplete="off">
|
2026-08-30 00:51:23 +08:00
|
|
|
|
<div class="hint">调火山 v3 上游用的凭证,从 <a href="https://console.volcengine.com/" target="_blank" style="color: var(--accent);">火山控制台</a> 获取。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Step 2: 默认路由 -->
|
|
|
|
|
|
<div v-show="step===2" class="card">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="card-title">默认路由与格式</div>
|
|
|
|
|
|
<div class="card-sub">未传 voice/format 时使用。装完可在 <code>/admin → 设置</code> 改。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="step-badge">第 2 步</span>
|
2026-08-29 20:44:33 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field-row">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>默认资源 ID <span class="req">*</span></label>
|
2026-08-30 15:57:00 +08:00
|
|
|
|
<input v-model="form.default_resource_id" placeholder="seed-icl-2.0">
|
|
|
|
|
|
<div class="hint">音色所属的计费资源 ID;本项目为豆包声音复刻大模型 2.0,固定使用 <code>seed-icl-2.0</code>。</div>
|
2026-08-29 20:44:33 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>默认音色 <span class="req">*</span></label>
|
|
|
|
|
|
<input v-model="form.default_speaker" placeholder="zh_female_qingxin">
|
|
|
|
|
|
<div class="hint">未传 voice 字段时使用。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field-row">
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>默认输出格式</label>
|
|
|
|
|
|
<select v-model="form.default_format">
|
|
|
|
|
|
<option value="mp3">mp3</option>
|
|
|
|
|
|
<option value="wav">wav</option>
|
|
|
|
|
|
<option value="opus">opus (ogg_opus)</option>
|
|
|
|
|
|
<option value="pcm">pcm</option>
|
|
|
|
|
|
<option value="aac">aac</option>
|
|
|
|
|
|
<option value="flac">flac</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>采样率 (Hz)</label>
|
|
|
|
|
|
<input type="number" v-model.number="form.sample_rate" placeholder="24000">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>初始化 Token <span class="req">*</span></label>
|
|
|
|
|
|
<input type="password" v-model="form.token" placeholder="启动日志里打印的一次性 token,或 TTS_ADMIN_KEY" autocomplete="off">
|
|
|
|
|
|
<div class="hint">公网部署建议设置 TTS_ADMIN_KEY;否则每次启动会重新生成。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
<!-- Step 3: 音色列表 -->
|
|
|
|
|
|
<div v-show="step===3" class="card">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="card-title">音色列表 <span style="font-weight: 400; color: var(--text-dim); font-size: 12px;">(至少 1 条)</span></div>
|
|
|
|
|
|
<div class="card-sub">填入火山 TTS 音色 ID(S_ 开头的为复刻音色)。可后续在 /admin 中增删。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="step-badge">第 3 步</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="voice-row-head">
|
|
|
|
|
|
<div>对外名 (voice)</div>
|
|
|
|
|
|
<div>Speaker</div>
|
|
|
|
|
|
<div>资源 ID</div>
|
|
|
|
|
|
<div>Model</div>
|
|
|
|
|
|
<div></div>
|
|
|
|
|
|
</div>
|
2026-08-29 20:44:33 +08:00
|
|
|
|
<div v-for="(v, i) in form.voices" :key="i" class="voice-row">
|
2026-08-30 00:51:23 +08:00
|
|
|
|
<input v-model="v.name" placeholder="qian">
|
2026-08-29 20:44:33 +08:00
|
|
|
|
<input v-model="v.speaker" placeholder="S_xxx">
|
2026-08-30 15:57:00 +08:00
|
|
|
|
<input v-model="v.resource_id" placeholder="seed-icl-2.0">
|
2026-08-29 20:44:33 +08:00
|
|
|
|
<input v-model="v.model" placeholder="seed-tts-2.0-standard">
|
|
|
|
|
|
<button class="rm" @click="removeVoice(i)" :disabled="form.voices.length <= 1" title="删除">×</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="btn btn-add" @click="addVoice">+ 添加音色</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
<!-- Step 4: 确认 -->
|
|
|
|
|
|
<div v-show="step===4" class="card">
|
|
|
|
|
|
<div class="card-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div class="card-title">确认安装</div>
|
|
|
|
|
|
<div class="card-sub">检查以下配置,点击「完成安装」提交。提交后立即进入正常模式。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="step-badge">第 4 步</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 14px;">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="font-size: 11px; color: var(--text-mute); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">OpenAI Key</div>
|
|
|
|
|
|
<div style="font-family: ui-monospace, monospace; color: var(--accent); font-size: 13px;">{{ form.auth_key ? mask(form.auth_key) : '(未填)' }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="font-size: 11px; color: var(--text-mute); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">火山 API Key</div>
|
|
|
|
|
|
<div style="font-family: ui-monospace, monospace; color: var(--accent); font-size: 13px;">{{ form.api_key ? mask(form.api_key) : '(未填)' }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="font-size: 11px; color: var(--text-mute); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">默认资源 ID</div>
|
|
|
|
|
|
<div style="font-family: ui-monospace, monospace; font-size: 13px;">{{ form.default_resource_id || '—' }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="font-size: 11px; color: var(--text-mute); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">默认音色</div>
|
|
|
|
|
|
<div style="font-family: ui-monospace, monospace; font-size: 13px;">{{ form.default_speaker || '—' }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="font-size: 11px; color: var(--text-mute); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">输出格式 / 采样率</div>
|
|
|
|
|
|
<div style="font-size: 13px;">{{ form.default_format }} · {{ form.sample_rate }} Hz</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style="font-size: 11px; color: var(--text-mute); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">音色数</div>
|
|
|
|
|
|
<div style="font-size: 13px;">{{ form.voices.length }} 条 <span class="text-dim" style="font-size: 12px;">({{ form.voices.filter(v => v.name).length }} 已填名)</span></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Nav -->
|
2026-08-29 20:44:33 +08:00
|
|
|
|
<div class="actions">
|
2026-08-30 00:51:23 +08:00
|
|
|
|
<button class="btn" @click="prev" :disabled="step===1 || submitting">← 上一步</button>
|
|
|
|
|
|
<div class="hint-text">提交后立即进入正常模式;后续管理请访问 <code>/admin</code>。</div>
|
|
|
|
|
|
<div style="display: flex; gap: 8px;">
|
|
|
|
|
|
<button v-if="step < 4" class="btn btn-primary" @click="next">下一步 →</button>
|
|
|
|
|
|
<button v-else class="btn btn-primary" @click="submit" :disabled="submitting">
|
|
|
|
|
|
{{ submitting ? '安装中...' : '完成安装' }}
|
|
|
|
|
|
</button>
|
2026-08-29 20:44:33 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
const { createApp, ref, onMounted } = Vue;
|
|
|
|
|
|
createApp({
|
|
|
|
|
|
setup() {
|
2026-08-30 00:51:23 +08:00
|
|
|
|
const step = ref(1);
|
2026-08-29 20:44:33 +08:00
|
|
|
|
const form = ref({
|
2026-08-29 23:18:51 +08:00
|
|
|
|
auth_key: '',
|
2026-08-29 20:44:33 +08:00
|
|
|
|
api_key: '',
|
2026-08-30 15:57:00 +08:00
|
|
|
|
default_resource_id: 'seed-icl-2.0',
|
2026-08-29 20:44:33 +08:00
|
|
|
|
default_speaker: '',
|
|
|
|
|
|
default_format: 'mp3',
|
|
|
|
|
|
sample_rate: 24000,
|
|
|
|
|
|
token: '',
|
2026-08-30 15:57:00 +08:00
|
|
|
|
voices: [{ name: '', speaker: '', resource_id: 'seed-icl-2.0', model: 'seed-tts-2.0-standard' }],
|
2026-08-29 20:44:33 +08:00
|
|
|
|
});
|
|
|
|
|
|
const errorMsg = ref('');
|
|
|
|
|
|
const submitting = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
const addVoice = () => {
|
2026-08-30 15:57:00 +08:00
|
|
|
|
form.value.voices.push({ name: '', speaker: '', resource_id: 'seed-icl-2.0', model: 'seed-tts-2.0-standard' });
|
2026-08-29 20:44:33 +08:00
|
|
|
|
};
|
|
|
|
|
|
const removeVoice = (i) => {
|
|
|
|
|
|
if (form.value.voices.length > 1) form.value.voices.splice(i, 1);
|
|
|
|
|
|
};
|
2026-08-30 00:51:23 +08:00
|
|
|
|
const next = () => {
|
|
|
|
|
|
errorMsg.value = '';
|
|
|
|
|
|
if (step.value === 1) {
|
|
|
|
|
|
if (!form.value.auth_key || !form.value.api_key) {
|
|
|
|
|
|
errorMsg.value = '请填写 OpenAI 鉴权 Key 和火山 API Key';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (step.value === 2) {
|
|
|
|
|
|
if (!form.value.default_resource_id || !form.value.default_speaker || !form.value.token) {
|
|
|
|
|
|
errorMsg.value = '请填写默认资源 ID、默认音色和初始化 Token';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (step.value === 3) {
|
|
|
|
|
|
if (!form.value.voices.length || form.value.voices.some(v => !v.name || !v.speaker || !v.resource_id)) {
|
|
|
|
|
|
errorMsg.value = '音色列表不完整(每条都需要 name + speaker + resource_id)';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
step.value = Math.min(4, step.value + 1);
|
|
|
|
|
|
};
|
|
|
|
|
|
const prev = () => { errorMsg.value = ''; step.value = Math.max(1, step.value - 1); };
|
|
|
|
|
|
const mask = (s) => {
|
|
|
|
|
|
if (!s) return '';
|
|
|
|
|
|
if (s.length <= 8) return '****';
|
|
|
|
|
|
return s.slice(0, 4) + '****' + s.slice(-4);
|
|
|
|
|
|
};
|
2026-08-29 20:44:33 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await axios.get('/api/setup/prefill');
|
|
|
|
|
|
const s = r.data.settings || {};
|
|
|
|
|
|
if (s.default_resource_id) form.value.default_resource_id = s.default_resource_id;
|
|
|
|
|
|
if (s.default_speaker) form.value.default_speaker = s.default_speaker;
|
|
|
|
|
|
if (s.default_format) form.value.default_format = s.default_format;
|
|
|
|
|
|
if (s.sample_rate) form.value.sample_rate = parseInt(s.sample_rate, 10) || 24000;
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log('prefill skipped:', e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const submit = async () => {
|
|
|
|
|
|
errorMsg.value = '';
|
|
|
|
|
|
submitting.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await axios.post('/api/setup', {
|
|
|
|
|
|
token: form.value.token,
|
|
|
|
|
|
settings: {
|
2026-08-29 23:18:51 +08:00
|
|
|
|
auth_key: form.value.auth_key,
|
2026-08-29 20:44:33 +08:00
|
|
|
|
api_key: form.value.api_key,
|
|
|
|
|
|
default_resource_id: form.value.default_resource_id,
|
|
|
|
|
|
default_speaker: form.value.default_speaker,
|
|
|
|
|
|
default_format: form.value.default_format,
|
|
|
|
|
|
sample_rate: String(form.value.sample_rate || 24000),
|
|
|
|
|
|
},
|
|
|
|
|
|
voices: form.value.voices,
|
|
|
|
|
|
});
|
2026-08-30 00:51:23 +08:00
|
|
|
|
window.location.href = r.data.redirect || '/admin';
|
2026-08-29 20:44:33 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
const msg = e.response?.data?.error?.message || e.message;
|
2026-08-30 00:51:23 +08:00
|
|
|
|
errorMsg.value = '安装失败: ' + msg;
|
2026-08-29 20:44:33 +08:00
|
|
|
|
submitting.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-30 00:51:23 +08:00
|
|
|
|
return { step, form, errorMsg, submitting, addVoice, removeVoice, next, prev, mask, submit };
|
2026-08-29 20:44:33 +08:00
|
|
|
|
},
|
|
|
|
|
|
}).mount('#app');
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|