2026-03-27 13:41:07 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<title>TTS Proxy Service</title>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
<style>
|
|
|
|
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
2026-03-27 15:10:58 +08:00
|
|
|
|
:root{--bg:#0f1117;--card:#1a1d27;--border:#2a2d3a;--primary:#6c5ce7;--primary-hover:#7c6df7;--success:#00b894;--error:#ff6b6b;--warn:#fdcb6e;--text:#e8e8e8;--dim:#8b8fa3;--bright:#fff}
|
2026-03-27 13:41:07 +08:00
|
|
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
2026-03-27 15:10:58 +08:00
|
|
|
|
.c{max-width:800px;margin:0 auto;padding:24px}
|
|
|
|
|
|
h1{font-size:1.5rem;font-weight:700;margin-bottom:6px;background:linear-gradient(135deg,var(--primary),#a29bfe);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
|
|
|
|
|
.sub{color:var(--dim);font-size:.82rem;margin-bottom:28px}
|
2026-03-27 13:41:07 +08:00
|
|
|
|
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}
|
2026-03-27 15:10:58 +08:00
|
|
|
|
.card-t{font-size:1rem;font-weight:600;margin-bottom:14px;display:flex;align-items:center;gap:8px}
|
|
|
|
|
|
.fg{margin-bottom:12px}
|
|
|
|
|
|
.fg label{display:block;font-size:.78rem;color:var(--dim);margin-bottom:5px;text-transform:uppercase;letter-spacing:.5px}
|
|
|
|
|
|
input,textarea{width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:.88rem;outline:none;transition:border .2s}
|
|
|
|
|
|
input:focus,textarea:focus{border-color:var(--primary)}
|
|
|
|
|
|
textarea{resize:vertical;min-height:120px;font-family:inherit}
|
|
|
|
|
|
.row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
|
|
|
|
.btn{padding:9px 20px;border-radius:8px;border:none;cursor:pointer;font-size:.85rem;font-weight:500;transition:all .2s;display:inline-flex;align-items:center;gap:6px}
|
|
|
|
|
|
.btn-p{background:var(--primary);color:#fff}.btn-p:hover{background:var(--primary-hover);transform:translateY(-1px)}
|
2026-03-27 13:41:07 +08:00
|
|
|
|
.btn:disabled{opacity:.5;cursor:not-allowed}
|
2026-03-27 15:10:58 +08:00
|
|
|
|
.preview{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px;margin-top:12px}
|
|
|
|
|
|
audio{width:100%;margin-top:6px}
|
|
|
|
|
|
.dim{color:var(--dim)}
|
2026-03-27 13:41:07 +08:00
|
|
|
|
.mt-2{margin-top:8px}
|
2026-03-27 15:10:58 +08:00
|
|
|
|
.toast{position:fixed;top:20px;right:20px;padding:12px 20px;border-radius:10px;font-size:.85rem;z-index:200;animation:slideIn .3s ease;max-width:380px}
|
|
|
|
|
|
.toast-ok{background:var(--success);color:#fff}.toast-err{background:var(--error);color:#fff}
|
|
|
|
|
|
@keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
|
|
|
|
|
|
table{width:100%;border-collapse:collapse}
|
|
|
|
|
|
th{text-align:left;padding:8px 10px;font-size:.73rem;color:var(--dim);text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid var(--border)}
|
|
|
|
|
|
td{padding:8px 10px;border-bottom:1px solid var(--border);font-size:.84rem}
|
|
|
|
|
|
.code{font-family:'SF Mono',Menlo,monospace;font-size:.8rem;color:var(--primary)}
|
|
|
|
|
|
.tabs{display:flex;gap:4px;margin-bottom:20px;border-bottom:1px solid var(--border)}
|
|
|
|
|
|
.tab{padding:10px 18px;cursor:pointer;color:var(--dim);font-size:.88rem;border:none;background:none;transition:all .2s;border-bottom:2px solid transparent}
|
|
|
|
|
|
.tab:hover{color:var(--text)}.tab.on{color:var(--primary);border-bottom-color:var(--primary)}
|
|
|
|
|
|
.panel{display:none}.panel.on{display:block}
|
|
|
|
|
|
.hint{font-size:.78rem;color:var(--dim);margin-top:6px;line-height:1.5}
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<div class="c">
|
|
|
|
|
|
<h1>🎙️ TTS Proxy Service</h1>
|
|
|
|
|
|
<p class="sub">小米 MiMo TTS 代理服务 · 智能分段 · 自动重试</p>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="tabs">
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<button class="tab on" onclick="sw('tts',this)">🎙️ TTS 试听</button>
|
|
|
|
|
|
<button class="tab" onclick="sw('cfg',this)">⚙️ 配置</button>
|
|
|
|
|
|
<button class="tab" onclick="sw('api',this)">📖 接口说明</button>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<!-- TTS 试听 -->
|
|
|
|
|
|
<div id="p-tts" class="panel on">
|
2026-03-27 13:41:07 +08:00
|
|
|
|
<div class="card">
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<div class="card-t">🎙️ TTS 试听</div>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<div class="fg">
|
|
|
|
|
|
<label>音色(可选,留空用默认)</label>
|
|
|
|
|
|
<input id="pv-voice" placeholder="mimo_default">
|
2026-03-27 14:37:43 +08:00
|
|
|
|
</div>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<div class="fg">
|
|
|
|
|
|
<label>风格(可选)</label>
|
|
|
|
|
|
<input id="pv-style" placeholder="开心、语速慢、东北话...">
|
2026-03-27 14:37:43 +08:00
|
|
|
|
</div>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</div>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<div class="fg">
|
2026-03-27 13:41:07 +08:00
|
|
|
|
<label>文本内容</label>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<textarea id="pv-text" rows="5" placeholder="输入要合成的文本...长文本自动分段生成"></textarea>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</div>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<button class="btn btn-p" onclick="preview()" id="pv-btn">🔊 生成试听</button>
|
|
|
|
|
|
<div id="pv-result" class="preview" style="display:none">
|
|
|
|
|
|
<audio id="pv-audio" controls></audio>
|
|
|
|
|
|
<p class="dim mt-2" id="pv-info"></p>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<!-- 配置 -->
|
|
|
|
|
|
<div id="p-cfg" class="panel">
|
2026-03-27 13:41:07 +08:00
|
|
|
|
<div class="card">
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<div class="card-t">⚙️ 当前配置</div>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
<table>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<tr><th style="width:140px">项目</th><th>值</th></tr>
|
|
|
|
|
|
<tr><td>TTS Endpoint</td><td id="c-ep">-</td></tr>
|
|
|
|
|
|
<tr><td>模型</td><td id="c-md">-</td></tr>
|
|
|
|
|
|
<tr><td>默认音色</td><td id="c-vc">-</td></tr>
|
|
|
|
|
|
<tr><td>API Key</td><td id="c-ak">-</td></tr>
|
|
|
|
|
|
<tr><td>分段上限</td><td id="c-ch">-</td></tr>
|
|
|
|
|
|
<tr><td>访问令牌</td><td id="c-tk">-</td></tr>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</table>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<p class="hint mt-2">通过环境变量配置:MIMO_API_KEY、MIMO_VOICE、MIMO_TTS_MODEL、API_TOKEN 等</p>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<!-- 接口说明 -->
|
|
|
|
|
|
<div id="p-api" class="panel">
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-t">📖 核心接口</div>
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<tr><th>接口</th><th>方法</th><th>说明</th></tr>
|
|
|
|
|
|
<tr><td><code class="code">/api/tts</code></td><td>POST</td><td>实时 TTS,返回 MP3 音频流</td></tr>
|
|
|
|
|
|
<tr><td><code class="code">/health</code></td><td>GET</td><td>健康检查</td></tr>
|
|
|
|
|
|
</table>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</div>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-t">🔧 管理接口 <span class="dim" style="font-weight:400;font-size:.78rem">(需 Bearer Token)</span></div>
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<tr><th>接口</th><th>方法</th><th>说明</th></tr>
|
|
|
|
|
|
<tr><td><code class="code">/admin/api/preview</code></td><td>POST</td><td>TTS 试听,返回音频 URL</td></tr>
|
|
|
|
|
|
<tr><td><code class="code">/admin/api/config</code></td><td>GET</td><td>查看配置</td></tr>
|
|
|
|
|
|
</table>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</div>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-t">📤 /api/tts 请求格式</div>
|
|
|
|
|
|
<p class="hint">JSON 格式(推荐):</p>
|
|
|
|
|
|
<pre style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;margin:8px 0;font-size:.82rem;overflow-x:auto"><code>POST /api/tts
|
|
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
"text": "要合成的文本",
|
|
|
|
|
|
"style": "开心", // 可选
|
|
|
|
|
|
"voice": "mimo_default" // 可选
|
|
|
|
|
|
}</code></pre>
|
|
|
|
|
|
<p class="hint">Form 格式(兼容百度风格):</p>
|
|
|
|
|
|
<pre style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;margin:8px 0;font-size:.82rem"><code>POST /api/tts
|
|
|
|
|
|
Content-Type: application/x-www-form-urlencoded
|
|
|
|
|
|
|
|
|
|
|
|
tex=要合成的文本</code></pre>
|
|
|
|
|
|
<p class="hint mt-2">
|
|
|
|
|
|
<b>特性:</b>长文本自动分段(≤2000字/段)+ TTS 失败自动重试(最多 3 次)<br>
|
|
|
|
|
|
<b>听书 App 接入:</b>在 App 中配置 TTS 源 URL 为 <code class="code">http://服务器:端口/api/tts</code>
|
|
|
|
|
|
</p>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</div>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-t">🎭 MiMo TTS 风格参考</div>
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<tr><th>类别</th><th>示例</th></tr>
|
|
|
|
|
|
<tr><td>情感</td><td>开心 / 悲伤 / 生气 / 平静 / 惊讶</td></tr>
|
|
|
|
|
|
<tr><td>语速</td><td>语速慢 / 语速快 / 悄悄话</td></tr>
|
|
|
|
|
|
<tr><td>角色</td><td>像个大将军 / 像个小孩 / 孙悟空</td></tr>
|
|
|
|
|
|
<tr><td>方言</td><td>东北话 / 四川话 / 台湾腔 / 粤语</td></tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
<p class="hint mt-2">可组合使用:<code class="code">"style": "开心 语速快"</code></p>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</div>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-t">📱 听书 App 模板变量</div>
|
|
|
|
|
|
<table>
|
|
|
|
|
|
<tr><th>变量</th><th>说明</th></tr>
|
|
|
|
|
|
<tr><td><code class="code">{{speakText}}</code></td><td>朗读文本</td></tr>
|
|
|
|
|
|
<tr><td><code class="code">{{speakSpeed}}</code></td><td>语速,范围 5-50</td></tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
<p class="hint mt-2">
|
|
|
|
|
|
App 只能动态传文本和语速。voice/style 需在 JSON 配置中写死,或通过其他客户端调用 /api/tts 时传入。
|
|
|
|
|
|
</p>
|
2026-03-27 13:41:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-03-27 15:10:58 +08:00
|
|
|
|
function sw(name, el) {
|
|
|
|
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('on'));
|
|
|
|
|
|
document.querySelectorAll('.panel').forEach(p => p.classList.remove('on'));
|
|
|
|
|
|
el.classList.add('on');
|
|
|
|
|
|
document.getElementById('p-' + name).classList.add('on');
|
|
|
|
|
|
if (name === 'cfg') loadCfg();
|
2026-03-27 13:41:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 15:10:58 +08:00
|
|
|
|
function toast(msg, ok = true) {
|
2026-03-27 13:41:07 +08:00
|
|
|
|
const t = document.createElement('div');
|
2026-03-27 15:10:58 +08:00
|
|
|
|
t.className = 'toast ' + (ok ? 'toast-ok' : 'toast-err');
|
2026-03-27 13:41:07 +08:00
|
|
|
|
t.textContent = msg;
|
|
|
|
|
|
document.body.appendChild(t);
|
|
|
|
|
|
setTimeout(() => t.remove(), 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 15:10:58 +08:00
|
|
|
|
async function preview() {
|
|
|
|
|
|
const text = document.getElementById('pv-text').value.trim();
|
|
|
|
|
|
const style = document.getElementById('pv-style').value.trim();
|
|
|
|
|
|
const voice = document.getElementById('pv-voice').value.trim();
|
|
|
|
|
|
if (!text) { toast('请输入文本', false); return; }
|
|
|
|
|
|
const btn = document.getElementById('pv-btn');
|
|
|
|
|
|
btn.disabled = true; btn.textContent = '⏳ 生成中...';
|
2026-03-27 13:41:07 +08:00
|
|
|
|
try {
|
2026-03-27 15:10:58 +08:00
|
|
|
|
const res = await fetch('/admin/api/preview', {
|
2026-03-27 13:41:07 +08:00
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
2026-03-27 14:37:43 +08:00
|
|
|
|
body: JSON.stringify({text, style, voice})
|
2026-03-27 13:41:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
if (data.ok) {
|
2026-03-27 15:10:58 +08:00
|
|
|
|
document.getElementById('pv-result').style.display = 'block';
|
|
|
|
|
|
document.getElementById('pv-audio').src = data.url;
|
|
|
|
|
|
document.getElementById('pv-audio').play();
|
|
|
|
|
|
document.getElementById('pv-info').textContent = data.chunks > 1 ? `已自动分 ${data.chunks} 段生成并拼接` : '';
|
|
|
|
|
|
toast('生成成功');
|
|
|
|
|
|
} else { toast('生成失败', false); }
|
|
|
|
|
|
} catch(e) { toast('生成失败: ' + e.message, false); }
|
|
|
|
|
|
btn.disabled = false; btn.textContent = '🔊 生成试听';
|
2026-03-27 13:41:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 15:10:58 +08:00
|
|
|
|
async function loadCfg() {
|
2026-03-27 13:41:07 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch('/admin/api/config');
|
2026-03-27 15:10:58 +08:00
|
|
|
|
const c = await res.json();
|
|
|
|
|
|
document.getElementById('c-ep').textContent = c.endpoint || '-';
|
|
|
|
|
|
document.getElementById('c-md').textContent = c.model || '-';
|
|
|
|
|
|
document.getElementById('c-vc').textContent = c.voice || '-';
|
|
|
|
|
|
document.getElementById('c-ak').textContent = c.api_key || '-';
|
|
|
|
|
|
document.getElementById('c-ch').textContent = c.max_chunk + ' 字符';
|
|
|
|
|
|
document.getElementById('c-tk').textContent = c.token_set ? '✅ 已配置' : '❌ 未配置(接口公开访问)';
|
2026-03-27 13:41:07 +08:00
|
|
|
|
} catch(e) {
|
2026-03-27 15:10:58 +08:00
|
|
|
|
toast('加载配置失败: ' + e.message, false);
|
2026-03-27 13:41:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|