webasmr / index.html
GCStream's picture
WebGPU fp16 path + F5-faithful synthesis pipeline
a24bf6c
Raw History Blame Contribute Delete
11.8 kB
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="color-scheme" content="dark" />
<title>WebASMR · 浏览器语音合成</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎧</text></svg>">
<meta name="description" content="F5-TTS flow-matching speech synthesis running fully in your browser. WebGPU accelerated, nothing leaves your device." />
<link rel="stylesheet" href="fonts/fonts.css" />
<script src="/coi-serviceworker.js"></script>
<style>
:root {
--bg: #020203;
--t1: rgba(255, 255, 255, 0.92);
--t2: rgba(255, 255, 255, 0.55);
--t3: rgba(255, 255, 255, 0.32);
--t4: rgba(255, 255, 255, 0.20);
--line: rgba(255, 255, 255, 0.08);
--line-soft: rgba(255, 255, 255, 0.06);
--panel: rgba(255, 255, 255, 0.03);
--panel-hover: rgba(255, 255, 255, 0.06);
--ok: #64ffa0;
--warn: #ffcd6b;
--danger: #ff7a6b;
--display: "Instrument Serif", Georgia, "Songti SC", "Times New Roman", serif;
--body: "Geist", -apple-system, BlinkMacSystemFont, "PingFang SC", "Segoe UI", sans-serif;
--mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--maxw: 860px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { height: 100%; }
body {
background: var(--bg);
color: var(--t1);
font-family: var(--body);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
min-height: 100dvh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
body::before {
content: "";
position: fixed;
inset: 0;
background: radial-gradient(58% 42% at 50% -6%, rgba(130, 150, 255, 0.09), transparent 70%);
pointer-events: none;
}
button, textarea, input, select { font: inherit; color: inherit; }
button { appearance: none; background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
::selection { background: rgba(255, 255, 255, 0.18); }
.wrap { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 24px; }
/* topbar */
.topbar {
position: sticky; top: 0; z-index: 10;
border-bottom: 1px solid var(--line-soft);
background: rgba(2, 2, 3, 0.72);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.topbar-inner { display: flex; align-items: center; justify-content: space-between; height: 56px; }
.brand { font-family: var(--mono); font-size: 12px; font-weight: 500; letter-spacing: 0.22em; text-transform: uppercase; color: var(--t1); }
.chip {
display: inline-flex; align-items: center; gap: 8px;
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--t2);
border: 1px solid var(--line);
border-radius: 999px;
padding: 5px 12px;
white-space: nowrap;
}
.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--t4); flex-shrink: 0; }
.chip.ready .dot { background: var(--ok); box-shadow: 0 0 8px rgba(100, 255, 160, 0.7); }
.chip.error .dot { background: var(--danger); }
/* hero */
.hero { padding: 76px 0 8px; }
.eyebrow {
display: inline-flex; align-items: center; gap: 10px;
font-family: var(--mono); font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase;
color: var(--t3); margin-bottom: 22px;
}
.eyebrow::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 10px rgba(100, 255, 160, 0.8); }
h1 {
font-family: var(--display);
font-weight: 400;
font-size: clamp(40px, 7vw, 72px);
line-height: 1.06;
letter-spacing: 0.01em;
max-width: 15ch;
}
h1 em { font-style: italic; color: var(--t2); }
.sub { margin-top: 22px; max-width: 52ch; color: var(--t2); font-size: 15.5px; }
.sub b { color: var(--t1); font-weight: 500; }
.stats {
display: flex; flex-wrap: wrap; gap: 10px 28px;
margin-top: 34px;
font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
color: var(--t3);
}
.stats span b { color: var(--t1); font-weight: 500; }
/* studio */
.studio { margin: 44px 0 0; border: 1px solid var(--line); border-radius: 18px; background: var(--panel); overflow: hidden; }
.studio-head {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 14px 20px; border-bottom: 1px solid var(--line-soft);
font-family: var(--mono); font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--t3);
}
.studio-body { padding: 20px; }
textarea#text {
width: 100%; min-height: 108px; resize: vertical;
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--line);
border-radius: 12px;
padding: 16px 18px;
font-size: 17px; line-height: 1.7;
outline: none;
transition: border-color 0.18s ease;
}
textarea#text:focus { border-color: rgba(255, 255, 255, 0.22); }
textarea#text::placeholder { color: var(--t3); }
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.chip-btn {
font-size: 13.5px; color: var(--t2);
border: 1px solid var(--line);
border-radius: 999px;
padding: 6px 15px;
transition: all 0.15s ease;
}
.chip-btn:hover { background: var(--panel-hover); color: var(--t1); }
.chip-btn.active { background: rgba(255, 255, 255, 0.1); color: var(--t1); border-color: rgba(255, 255, 255, 0.2); }
.options { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 22px; margin-top: 18px; }
.opt { display: inline-flex; align-items: center; gap: 10px; font-family: var(--mono); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--t3); }
.voice-pill { color: var(--t1); border: 1px solid var(--line); border-radius: 999px; padding: 5px 14px; font-size: 12px; letter-spacing: 0.02em; }
input[type="range"] { width: 120px; accent-color: rgba(255, 255, 255, 0.8); background: transparent; }
.opt output { color: var(--t1); min-width: 26px; }
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; margin-top: 22px; }
button.primary {
display: inline-flex; align-items: center; gap: 10px;
background: var(--t1); color: #050506;
font-weight: 500; font-size: 15px;
border-radius: 999px; padding: 11px 26px;
transition: transform 0.15s ease, opacity 0.15s ease;
}
button.primary:hover { transform: translateY(-1px); }
button.primary:disabled { opacity: 0.45; cursor: default; transform: none; }
button.ghost {
font-size: 14px; color: var(--t2);
border: 1px solid var(--line); border-radius: 999px; padding: 10px 20px;
transition: all 0.15s ease;
}
button.ghost:hover:not(:disabled) { background: var(--panel-hover); color: var(--t1); }
button.ghost:disabled { opacity: 0.35; cursor: default; }
.hint { font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em; color: var(--t3); }
/* progress */
.progress { margin-top: 20px; display: none; }
.progress.on { display: block; }
.bar { height: 3px; border-radius: 999px; background: rgba(255, 255, 255, 0.07); overflow: hidden; }
.bar i { display: block; height: 100%; width: 0%; background: var(--t1); border-radius: 999px; transition: width 0.2s ease; }
.bar.indeterminate i { width: 30% !important; animation: slide 1.2s ease-in-out infinite alternate; }
@keyframes slide { from { margin-left: 0; } to { margin-left: 70%; } }
.stage { display: flex; justify-content: space-between; gap: 12px; margin-top: 9px; font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--t3); }
.stage output { color: var(--t2); text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* result */
.result { margin-top: 18px; display: none; }
.result.on { display: block; }
audio { width: 100%; height: 40px; margin-top: 4px; opacity: 0.9; }
.meta { display: flex; flex-wrap: wrap; gap: 8px 24px; margin-top: 14px; font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; color: var(--t3); text-transform: uppercase; }
.meta b { color: var(--t1); font-weight: 500; }
.error { display: none; margin-top: 16px; border: 1px solid rgba(255, 122, 107, 0.35); background: rgba(255, 122, 107, 0.07); color: #ffb3a8; border-radius: 12px; padding: 12px 16px; font-size: 13.5px; }
.error.on { display: block; }
/* footer */
footer { margin-top: auto; border-top: 1px solid var(--line-soft); }
.footer-inner { display: flex; flex-wrap: wrap; gap: 10px 26px; align-items: center; justify-content: space-between; padding: 22px 0 26px; font-family: var(--mono); font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--t3); }
.footer-inner a:hover { color: var(--t1); }
@media (max-width: 640px) {
.hero { padding-top: 48px; }
.studio-body { padding: 16px; }
.footer-inner { justify-content: flex-start; }
}
</style>
</head>
<body>
<header class="topbar">
<div class="wrap topbar-inner">
<span class="brand">WebASMR</span>
<span class="chip" id="cap-chip"><span class="dot"></span><span id="cap-text">检测中</span></span>
</div>
</header>
<main class="wrap">
<section class="hero">
<span class="eyebrow">F5-TTS · FLOW MATCHING · ON-DEVICE</span>
<h1>闭上眼,<em>听见温柔</em>。</h1>
<p class="sub">中文语音合成完整运行在你的浏览器里 — <b>WebGPU</b> 加速的流匹配网络加 24kHz 声码器。文字不离开这台设备,<b>零上传</b>。</p>
<div class="stats" id="stats">
<span>SAMPLE RATE <b>24 KHZ</b></span>
<span>FLOW STEPS <b id="stat-steps">32</b></span>
<span id="stat-live" hidden>TOTAL <b id="stat-total">–</b></span>
<span id="stat-rt" hidden>REALTIME <b id="stat-realtime">–</b></span>
</div>
</section>
<section class="studio">
<div class="studio-head">
<span>STUDIO</span>
<span id="studio-state">IDLE</span>
</div>
<div class="studio-body">
<textarea id="text" spellcheck="false" placeholder="闭上眼睛,慢慢地呼吸。">闭上眼睛,慢慢地呼吸。</textarea>
<div class="chips" id="presets"></div>
<div class="options">
<span class="opt">音色 <span class="voice-pill">小莼(纯儿)</span></span>
<span class="opt">增益 <input type="range" id="gain" min="0.5" max="4" step="0.5" value="1"><output id="gain-out">1×</output></span>
</div>
<div class="actions">
<button class="primary" id="go">生成语音</button>
<button class="ghost" id="dl" disabled>下载 WAV</button>
<span class="hint" id="hint">首次使用需下载约 712 MB 模型(浏览器会缓存)</span>
</div>
<div class="progress" id="progress">
<div class="bar" id="bar"><i id="bar-fill"></i></div>
<div class="stage"><span id="stage-label">PREPARING</span><output id="stage-detail"></output></div>
</div>
<div class="result" id="result">
<audio id="player" controls></audio>
<div class="meta" id="meta"></div>
</div>
<div class="error" id="error"></div>
</div>
</section>
</main>
<footer>
<div class="wrap footer-inner">
<span>WebASMR · 浏览器内的温柔声音</span>
<span>
<a href="https://huggingface.co/DownFlow/webasmr-f5tts" target="_blank" rel="noreferrer">MODEL REPO</a>
&nbsp;·&nbsp;
<a href="https://huggingface.co/spaces/DownFlow/webasmr" target="_blank" rel="noreferrer">SPACE</a>
</span>
</div>
</footer>
<script type="module" src="/app.js"></script>
</body>
</html>