Files
tts_trans/app/static/index.html
sunruiling 2a87020b48 refactor: 精简架构,去掉书籍管理,核心 TTS 代理
- 去掉 books/chapters CRUD、SQLAlchemy、SQLite 依赖
- 核心只剩 /api/tts + 智能分段 + 自动重试
- 新增 API_TOKEN 环境变量,管理接口 Bearer Token 鉴权
- 管理接口精简为 preview + config
- 前端重写:TTS 试听 + 配置查看 + 接口文档
- Dockerfile/docker-compose 清理,去掉数据库卷
2026-03-27 15:10:58 +08:00

220 lines
11 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>TTS Proxy Service</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0f1117;--card:#1a1d27;--border:#2a2d3a;--primary:#6c5ce7;--primary-hover:#7c6df7;--success:#00b894;--error:#ff6b6b;--warn:#fdcb6e;--text:#e8e8e8;--dim:#8b8fa3;--bright:#fff}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
.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}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}
.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)}
.btn:disabled{opacity:.5;cursor:not-allowed}
.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)}
.mt-2{margin-top:8px}
.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}
</style>
</head>
<body>
<div class="c">
<h1>🎙️ TTS Proxy Service</h1>
<p class="sub">小米 MiMo TTS 代理服务 · 智能分段 · 自动重试</p>
<div class="tabs">
<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>
</div>
<!-- TTS 试听 -->
<div id="p-tts" class="panel on">
<div class="card">
<div class="card-t">🎙️ TTS 试听</div>
<div class="row">
<div class="fg">
<label>音色(可选,留空用默认)</label>
<input id="pv-voice" placeholder="mimo_default">
</div>
<div class="fg">
<label>风格(可选)</label>
<input id="pv-style" placeholder="开心、语速慢、东北话...">
</div>
</div>
<div class="fg">
<label>文本内容</label>
<textarea id="pv-text" rows="5" placeholder="输入要合成的文本...长文本自动分段生成"></textarea>
</div>
<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>
</div>
</div>
</div>
<!-- 配置 -->
<div id="p-cfg" class="panel">
<div class="card">
<div class="card-t">⚙️ 当前配置</div>
<table>
<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>
</table>
<p class="hint mt-2">通过环境变量配置MIMO_API_KEY、MIMO_VOICE、MIMO_TTS_MODEL、API_TOKEN 等</p>
</div>
</div>
<!-- 接口说明 -->
<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>
</div>
<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>
</div>
<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>
</div>
<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>
</div>
<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>
</div>
</div>
</div>
<script>
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();
}
function toast(msg, ok = true) {
const t = document.createElement('div');
t.className = 'toast ' + (ok ? 'toast-ok' : 'toast-err');
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
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 = '⏳ 生成中...';
try {
const res = await fetch('/admin/api/preview', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text, style, voice})
});
const data = await res.json();
if (data.ok) {
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 = '🔊 生成试听';
}
async function loadCfg() {
try {
const res = await fetch('/admin/api/config');
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 ? '✅ 已配置' : '❌ 未配置(接口公开访问)';
} catch(e) {
toast('加载配置失败: ' + e.message, false);
}
}
</script>
</body>
</html>