Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>Devil Studio — TTS Demo</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |
| <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet" /> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #0a0a0a; | |
| --surface: #111111; | |
| --surface2: #181818; | |
| --border: #242424; | |
| --accent: #ff3c00; | |
| --accent2: #ff6b35; | |
| --muted: #444; | |
| --text: #e8e8e8; | |
| --dim: #666; | |
| --mono: 'DM Mono', monospace; | |
| --sans: 'DM Sans', sans-serif; | |
| --disp: 'Bebas Neue', sans-serif; | |
| --r: 4px; | |
| } | |
| html { font-size: 16px; scroll-behavior: smooth; } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--sans); | |
| font-weight: 300; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* noise */ | |
| body::before { | |
| content: ''; | |
| position: fixed; inset: 0; | |
| background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E"); | |
| pointer-events: none; z-index: 999; opacity: 0.35; | |
| } | |
| /* grid */ | |
| body::after { | |
| content: ''; | |
| position: fixed; inset: 0; | |
| background-image: linear-gradient(rgba(255,60,0,0.025) 1px,transparent 1px),linear-gradient(90deg,rgba(255,60,0,0.025) 1px,transparent 1px); | |
| background-size: 40px 40px; | |
| pointer-events: none; z-index: 0; | |
| } | |
| /* ── Header ──────────────────────────────────────────────────── */ | |
| header { | |
| position: relative; z-index: 10; | |
| padding: 36px 48px 0; | |
| display: flex; align-items: flex-start; justify-content: space-between; | |
| animation: fadeDown .6s ease both; | |
| } | |
| .logo-name { | |
| font-family: var(--disp); | |
| font-size: clamp(40px,6vw,76px); | |
| letter-spacing: .04em; line-height: .9; | |
| background: linear-gradient(135deg,#fff 0%,#ff3c00 55%,#ff6b35 100%); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; | |
| } | |
| .logo-sub { | |
| font-family: var(--mono); font-size: 10px; | |
| letter-spacing: .28em; text-transform: uppercase; | |
| color: var(--dim); padding-left: 2px; margin-top: 4px; display: block; | |
| } | |
| .status-badge { | |
| display: flex; align-items: center; gap: 8px; | |
| font-family: var(--mono); font-size: 11px; | |
| letter-spacing: .1em; color: var(--dim); | |
| text-transform: uppercase; padding-top: 10px; | |
| } | |
| .dot { | |
| width: 7px; height: 7px; border-radius: 50%; | |
| background: #2a2a2a; transition: background .3s, box-shadow .3s; | |
| } | |
| .dot.online { background: #00e676; box-shadow: 0 0 8px rgba(0,230,118,.6); animation: pulse 2s infinite; } | |
| .dot.error { background: var(--accent); } | |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.45} } | |
| /* ── Divider ──────────────────────────────────────────────────── */ | |
| .hero { | |
| position: relative; z-index: 10; | |
| padding: 40px 48px 0; | |
| animation: fadeDown .7s .1s ease both; | |
| } | |
| .hero-line { | |
| display: block; | |
| font-family: var(--disp); | |
| font-size: clamp(16px,3vw,34px); | |
| letter-spacing: .1em; color: var(--muted); line-height: 1; | |
| } | |
| .divider { | |
| width: 80px; height: 2px; background: var(--accent); | |
| margin: 20px 0; position: relative; | |
| } | |
| .divider::after { | |
| content: ''; | |
| position: absolute; left: 80px; top: 0; | |
| width: 300px; height: 2px; | |
| background: linear-gradient(90deg,var(--accent),transparent); opacity: .18; | |
| } | |
| /* ── Layout ───────────────────────────────────────────────────── */ | |
| main { | |
| position: relative; z-index: 10; | |
| display: grid; grid-template-columns: 1fr 320px; | |
| gap: 2px; padding: 28px 48px 48px; max-width: 1300px; | |
| } | |
| @media(max-width:860px){ | |
| main { grid-template-columns:1fr; padding:20px; } | |
| header,.hero { padding-left:20px; padding-right:20px; } | |
| } | |
| /* ── Panel ────────────────────────────────────────────────────── */ | |
| .panel { | |
| background: var(--surface); border: 1px solid var(--border); | |
| padding: 26px; position: relative; | |
| animation: fadeUp .6s .2s ease both; | |
| } | |
| .panel-r { border-left: none; animation-delay: .32s; display:flex; flex-direction:column; gap:22px; } | |
| @media(max-width:860px){ | |
| .panel-r { border-left:1px solid var(--border); border-top:none; } | |
| } | |
| .sec-label { | |
| font-family: var(--mono); font-size: 10px; | |
| letter-spacing: .28em; text-transform: uppercase; color: var(--dim); | |
| margin-bottom: 14px; | |
| display: flex; align-items: center; gap: 10px; | |
| } | |
| .sec-label::after { content:''; flex:1; height:1px; background:var(--border); } | |
| /* ── Textarea ─────────────────────────────────────────────────── */ | |
| .ta-wrap { position: relative; } | |
| textarea { | |
| width: 100%; min-height: 170px; | |
| background: var(--surface2); border: 1px solid var(--border); | |
| color: var(--text); font-family: var(--sans); font-size: 15px; | |
| font-weight: 300; line-height: 1.75; padding: 14px 16px; | |
| resize: vertical; outline: none; border-radius: var(--r); | |
| transition: border-color .2s; caret-color: var(--accent); | |
| } | |
| textarea:focus { border-color: var(--accent); } | |
| textarea::placeholder { color: var(--dim); } | |
| .cc { | |
| position: absolute; bottom: 10px; right: 12px; | |
| font-family: var(--mono); font-size: 10px; color: var(--dim); pointer-events: none; | |
| } | |
| /* ── Speed ────────────────────────────────────────────────────── */ | |
| .speed-row { | |
| display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; | |
| } | |
| .speed-label { font-family:var(--mono); font-size:10px; letter-spacing:.22em; text-transform:uppercase; color:var(--dim); } | |
| .speed-num { font-family:var(--mono); font-size:18px; font-weight:500; color:var(--accent); } | |
| input[type=range] { | |
| -webkit-appearance:none; appearance:none; | |
| width:100%; height:2px; background:var(--border); outline:none; cursor:pointer; border-radius:1px; display:block; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance:none; width:15px; height:15px; border-radius:50%; | |
| background:var(--accent); cursor:pointer; | |
| box-shadow:0 0 8px rgba(255,60,0,.5); transition:box-shadow .2s,transform .1s; | |
| } | |
| input[type=range]::-webkit-slider-thumb:hover { box-shadow:0 0 16px rgba(255,60,0,.8); transform:scale(1.2); } | |
| .speed-marks { | |
| display:flex; justify-content:space-between; | |
| font-family:var(--mono); font-size:9px; color:var(--dim); margin-top:5px; | |
| } | |
| /* ── Generate btn ─────────────────────────────────────────────── */ | |
| .btn-gen { | |
| width:100%; margin-top:18px; padding:15px; | |
| background:var(--accent); border:none; color:#fff; | |
| font-family:var(--disp); font-size:20px; letter-spacing:.12em; | |
| cursor:pointer; border-radius:var(--r); position:relative; overflow:hidden; | |
| transition:background .2s,transform .1s; | |
| } | |
| .btn-gen::before { | |
| content:''; position:absolute; inset:0; | |
| background:linear-gradient(135deg,rgba(255,255,255,.1),transparent 55%); pointer-events:none; | |
| } | |
| .btn-gen:hover { background:var(--accent2); } | |
| .btn-gen:active { transform:scale(.99); } | |
| .btn-gen:disabled { background:var(--muted); cursor:not-allowed; transform:none; } | |
| .btn-inner { display:inline-flex; align-items:center; gap:10px; } | |
| .spinner { | |
| display:none; width:15px; height:15px; | |
| border:2px solid rgba(255,255,255,.3); border-top-color:#fff; | |
| border-radius:50%; animation:spin .7s linear infinite; | |
| } | |
| .btn-gen.loading .spinner { display:block; } | |
| .btn-gen.loading .blabel { opacity:.7; } | |
| @keyframes spin { to{transform:rotate(360deg)} } | |
| /* ── Audio output ─────────────────────────────────────────────── */ | |
| .out-section { margin-top:18px; } | |
| .wvz { | |
| background:var(--surface2); border:1px solid var(--border); | |
| border-radius:var(--r); height:72px; | |
| display:flex; align-items:center; justify-content:center; | |
| overflow:hidden; position:relative; margin-bottom:10px; | |
| } | |
| .wvz-ph { font-family:var(--mono); font-size:11px; color:var(--dim); letter-spacing:.2em; text-transform:uppercase; } | |
| .wvz-bars { display:none; align-items:center; gap:2px; height:100%; padding:10px 16px; } | |
| .wvz-bars.on { display:flex; } | |
| .bar { | |
| width:3px; border-radius:2px; background:var(--accent); opacity:.7; | |
| animation:wa 1.2s ease-in-out infinite; | |
| } | |
| @keyframes wa { 0%,100%{transform:scaleY(.25)} 50%{transform:scaleY(1)} } | |
| audio { | |
| width:100%; height:38px; outline:none; border-radius:var(--r); | |
| filter:invert(1) hue-rotate(180deg); opacity:.8; | |
| } | |
| .act-row { display:flex; gap:7px; margin-top:9px; } | |
| .act-btn { | |
| flex:1; padding:9px; background:var(--surface2); border:1px solid var(--border); | |
| color:var(--dim); font-family:var(--mono); font-size:11px; | |
| letter-spacing:.1em; text-transform:uppercase; cursor:pointer; | |
| border-radius:var(--r); transition:all .15s; | |
| display:flex; align-items:center; justify-content:center; gap:6px; | |
| text-decoration:none; | |
| } | |
| .act-btn:hover { border-color:var(--accent); color:var(--accent); } | |
| /* ── Right panel pieces ───────────────────────────────────────── */ | |
| .model-cards { display:flex; flex-direction:column; gap:5px; } | |
| .mc { | |
| background:var(--surface2); border:1px solid var(--border); | |
| border-radius:var(--r); padding:10px 13px; | |
| cursor:pointer; transition:border-color .2s,background .2s; | |
| display:flex; align-items:center; justify-content:space-between; | |
| } | |
| .mc:hover { border-color:var(--muted); } | |
| .mc.sel { border-color:var(--accent); background:rgba(255,60,0,.06); } | |
| .mc-name { font-family:var(--mono); font-size:12px; font-weight:500; color:var(--text); letter-spacing:.04em; } | |
| .mc-desc { font-size:11px; color:var(--dim); margin-top:2px; } | |
| .mc-badge { | |
| font-family:var(--mono); font-size:9px; letter-spacing:.1em; text-transform:uppercase; | |
| padding:3px 7px; border-radius:2px; border:1px solid var(--border); color:var(--dim); | |
| transition:border-color .2s,color .2s; | |
| } | |
| .mc.sel .mc-badge { border-color:var(--accent); color:var(--accent); } | |
| .voice-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:5px; } | |
| .vbtn { | |
| background:var(--surface2); border:1px solid var(--border); | |
| color:var(--dim); font-family:var(--mono); font-size:11px; | |
| padding:9px 4px; text-align:center; cursor:pointer; | |
| border-radius:var(--r); transition:all .15s; letter-spacing:.04em; | |
| } | |
| .vbtn:hover { border-color:var(--muted); color:var(--text); } | |
| .vbtn.sel { border-color:var(--accent); color:var(--accent); background:rgba(255,60,0,.06); } | |
| .fmt-chips { display:flex; gap:5px; flex-wrap:wrap; } | |
| .chip { | |
| font-family:var(--mono); font-size:11px; letter-spacing:.1em; | |
| text-transform:uppercase; padding:6px 13px; | |
| border:1px solid var(--border); border-radius:var(--r); | |
| cursor:pointer; color:var(--dim); background:var(--surface2); transition:all .15s; | |
| } | |
| .chip:hover { border-color:var(--muted); color:var(--text); } | |
| .chip.sel { border-color:var(--accent); color:var(--accent); background:rgba(255,60,0,.06); } | |
| .srv-info { font-family:var(--mono); font-size:11px; color:var(--dim); line-height:2; } | |
| /* ── Status bar ───────────────────────────────────────────────── */ | |
| .sbar { | |
| grid-column:1/-1; border:1px solid var(--border); border-top:none; | |
| background:var(--surface); padding:9px 26px; | |
| display:flex; align-items:center; gap:22px; flex-wrap:wrap; | |
| font-family:var(--mono); font-size:10px; color:var(--dim); letter-spacing:.07em; | |
| animation:fadeUp .6s .5s ease both; | |
| } | |
| .si { display:flex; align-items:center; gap:5px; } | |
| .si .k { color:var(--muted); } | |
| .si .v { color:var(--text); } | |
| .si .v.ac { color:var(--accent); } | |
| .mbars { display:flex; align-items:flex-end; gap:2px; margin-left:auto; } | |
| .mb { width:3px; height:10px; background:var(--border); border-radius:1px; transition:background .3s; } | |
| .mb.lit { background:#00e676; } | |
| /* ── Toast ────────────────────────────────────────────────────── */ | |
| .toast { | |
| position:fixed; bottom:22px; right:22px; | |
| background:#1a0a0a; border:1px solid var(--accent); | |
| color:var(--text); font-family:var(--mono); font-size:12px; | |
| padding:13px 17px; border-radius:var(--r); z-index:1000; | |
| max-width:300px; transform:translateY(70px); opacity:0; | |
| transition:all .3s ease; box-shadow:0 0 20px rgba(255,60,0,.15); | |
| } | |
| .toast.show { transform:translateY(0); opacity:1; } | |
| @keyframes fadeDown { from{opacity:0;transform:translateY(-14px)} to{opacity:1;transform:translateY(0)} } | |
| @keyframes fadeUp { from{opacity:0;transform:translateY(14px)} to{opacity:1;transform:translateY(0)} } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div> | |
| <div class="logo-name">DEVIL STUDIO</div> | |
| <span class="logo-sub">Text · to · Speech · API / v1.0.0</span> | |
| </div> | |
| <div class="status-badge"> | |
| <div class="dot" id="dot"></div> | |
| <span id="statusTxt">Connecting…</span> | |
| </div> | |
| </header> | |
| <div class="hero"> | |
| <span class="hero-line">SYNTHESISE SPEECH.</span> | |
| <span class="hero-line">INSTANTLY.</span> | |
| <div class="divider"></div> | |
| </div> | |
| <main> | |
| <!-- ── Left panel ──────────────────────────────────────────── --> | |
| <div class="panel"> | |
| <div class="sec-label">Input</div> | |
| <div class="ta-wrap"> | |
| <textarea id="tin" placeholder="Type or paste your text here…" maxlength="5000" spellcheck="true">Devil Studio delivers low-latency, high-quality speech synthesis powered by KittenTTS — three models, eight voices, permanently loaded in memory and ready to respond.</textarea> | |
| <span class="cc"><span id="cc">0</span> / 5000</span> | |
| </div> | |
| <div style="margin-top:18px;"> | |
| <div class="speed-row"> | |
| <span class="speed-label">Speed</span> | |
| <span class="speed-num" id="sv">1.00×</span> | |
| </div> | |
| <input type="range" id="spd" min="0.25" max="4" step="0.05" value="1.0" /> | |
| <div class="speed-marks"> | |
| <span>0.25×</span><span>1×</span><span>2×</span><span>3×</span><span>4×</span> | |
| </div> | |
| </div> | |
| <button class="btn-gen" id="genBtn" onclick="generate()"> | |
| <span class="btn-inner"> | |
| <div class="spinner"></div> | |
| <span class="blabel">GENERATE SPEECH</span> | |
| </span> | |
| </button> | |
| <!-- Output --> | |
| <div class="out-section" id="outSection" style="display:none;"> | |
| <div style="height:14px;"></div> | |
| <div class="sec-label">Output</div> | |
| <div class="wvz"> | |
| <span class="wvz-ph" id="wvph">AWAITING SIGNAL</span> | |
| <div class="wvz-bars" id="wvbars"></div> | |
| </div> | |
| <audio id="ap" controls></audio> | |
| <div class="act-row"> | |
| <a class="act-btn" id="dlBtn" href="#" download="speech.wav">↓ Download</a> | |
| <button class="act-btn" onclick="copyCurl()">⌘ Copy cURL</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ── Right panel ─────────────────────────────────────────── --> | |
| <div class="panel panel-r"> | |
| <div> | |
| <div class="sec-label">Model</div> | |
| <div class="model-cards"> | |
| <div class="mc sel" data-m="tts-1" onclick="selModel(this)"> | |
| <div><div class="mc-name">tts-1</div><div class="mc-desc">Nano · 15M · Fastest</div></div> | |
| <span class="mc-badge">Speed</span> | |
| </div> | |
| <div class="mc" data-m="tts-1-hd" onclick="selModel(this)"> | |
| <div><div class="mc-name">tts-1-hd</div><div class="mc-desc">Micro · 40M · Balanced</div></div> | |
| <span class="mc-badge">Balance</span> | |
| </div> | |
| <div class="mc" data-m="tts-1-hd-mini" onclick="selModel(this)"> | |
| <div><div class="mc-name">tts-1-hd-mini</div><div class="mc-desc">Mini · 80M · Best Quality</div></div> | |
| <span class="mc-badge">Quality</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="sec-label">Voice</div> | |
| <div class="voice-grid"> | |
| <button class="vbtn sel" data-v="Jasper" onclick="selVoice(this)">Jasper</button> | |
| <button class="vbtn" data-v="Bella" onclick="selVoice(this)">Bella</button> | |
| <button class="vbtn" data-v="Luna" onclick="selVoice(this)">Luna</button> | |
| <button class="vbtn" data-v="Bruno" onclick="selVoice(this)">Bruno</button> | |
| <button class="vbtn" data-v="Rosie" onclick="selVoice(this)">Rosie</button> | |
| <button class="vbtn" data-v="Hugo" onclick="selVoice(this)">Hugo</button> | |
| <button class="vbtn" data-v="Kiki" onclick="selVoice(this)">Kiki</button> | |
| <button class="vbtn" data-v="Leo" onclick="selVoice(this)">Leo</button> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="sec-label">Format</div> | |
| <div class="fmt-chips"> | |
| <div class="chip sel" data-f="wav" onclick="selFmt(this)">WAV</div> | |
| <div class="chip" data-f="flac" onclick="selFmt(this)">FLAC</div> | |
| <div class="chip" data-f="pcm" onclick="selFmt(this)">PCM</div> | |
| <div class="chip" data-f="mp3" onclick="selFmt(this)">MP3*</div> | |
| </div> | |
| <p style="font-size:10px;color:var(--dim);margin-top:7px;font-family:var(--mono);"> | |
| * MP3 / OPUS / AAC served as WAV (ffmpeg not bundled) | |
| </p> | |
| </div> | |
| <div> | |
| <div class="sec-label">Server</div> | |
| <div class="srv-info" id="srvInfo">Fetching status…</div> | |
| </div> | |
| </div> | |
| <!-- ── Status bar ──────────────────────────────────────────── --> | |
| <div class="sbar"> | |
| <div class="si"><span class="k">ENDPOINT</span> <span class="v">pyxilabs-srv-tts-01.hf.space</span></div> | |
| <div class="si"><span class="k">LATENCY</span> <span class="v ac" id="latD">—</span></div> | |
| <div class="si"><span class="k">LAST GEN</span> <span class="v" id="lgD">—</span></div> | |
| <div class="mbars" id="mbars"> | |
| <div class="mb"></div><div class="mb"></div><div class="mb"></div> | |
| <div class="mb"></div><div class="mb"></div> | |
| </div> | |
| </div> | |
| </main> | |
| <div class="toast" id="toast"></div> | |
| <script> | |
| const API = 'https://pyxilabs-srv-tts-01.hf.space'; | |
| let model = 'tts-1'; | |
| let voice = 'Jasper'; | |
| let fmt = 'wav'; | |
| let blobUrl = null; | |
| // refs | |
| const tin = document.getElementById('tin'); | |
| const ccEl = document.getElementById('cc'); | |
| const spd = document.getElementById('spd'); | |
| const svEl = document.getElementById('sv'); | |
| const genBtn = document.getElementById('genBtn'); | |
| const outSec = document.getElementById('outSection'); | |
| const ap = document.getElementById('ap'); | |
| const dlBtn = document.getElementById('dlBtn'); | |
| const wvbars = document.getElementById('wvbars'); | |
| const wvph = document.getElementById('wvph'); | |
| const latD = document.getElementById('latD'); | |
| const lgD = document.getElementById('lgD'); | |
| const dotEl = document.getElementById('dot'); | |
| const stxtEl = document.getElementById('statusTxt'); | |
| const toastEl = document.getElementById('toast'); | |
| const srvInfo = document.getElementById('srvInfo'); | |
| const mbarsEl = document.getElementById('mbars'); | |
| // char count | |
| tin.addEventListener('input', () => ccEl.textContent = tin.value.length); | |
| ccEl.textContent = tin.value.length; | |
| // speed | |
| spd.addEventListener('input', () => svEl.textContent = (+spd.value).toFixed(2) + '×'); | |
| // selections | |
| function selModel(el) { | |
| document.querySelectorAll('.mc').forEach(e => e.classList.remove('sel')); | |
| el.classList.add('sel'); model = el.dataset.m; | |
| } | |
| function selVoice(el) { | |
| document.querySelectorAll('.vbtn').forEach(e => e.classList.remove('sel')); | |
| el.classList.add('sel'); voice = el.dataset.v; | |
| } | |
| function selFmt(el) { | |
| document.querySelectorAll('.chip').forEach(e => e.classList.remove('sel')); | |
| el.classList.add('sel'); fmt = el.dataset.f; | |
| } | |
| // waveform | |
| function buildWave() { | |
| wvbars.innerHTML = ''; | |
| for (let i = 0; i < 58; i++) { | |
| const b = document.createElement('div'); | |
| b.className = 'bar'; | |
| b.style.height = (20 + Math.random() * 60) + '%'; | |
| b.style.animationDelay = (i / 58 * 1.2) + 's'; | |
| wvbars.appendChild(b); | |
| } | |
| } | |
| // latency meter | |
| function setMeter(ms) { | |
| const bars = mbarsEl.querySelectorAll('.mb'); | |
| const lit = ms < 500 ? 5 : ms < 1000 ? 4 : ms < 2000 ? 3 : ms < 3500 ? 2 : 1; | |
| bars.forEach((b, i) => b.classList.toggle('lit', (5 - i) <= lit)); | |
| } | |
| // toast | |
| let tt; | |
| function toast(msg, dur = 3500) { | |
| toastEl.textContent = msg; | |
| toastEl.classList.add('show'); | |
| clearTimeout(tt); | |
| tt = setTimeout(() => toastEl.classList.remove('show'), dur); | |
| } | |
| // copy curl | |
| function copyCurl() { | |
| const txt = (tin.value.trim() || 'Hello!').replace(/'/g, "\\'"); | |
| const speed = (+spd.value).toFixed(2); | |
| const cmd = `curl -X POST ${API}/v1/audio/speech \\\n -H "Content-Type: application/json" \\\n -d '{"model":"${model}","input":"${txt}","voice":"${voice}","response_format":"${fmt}","speed":${speed}}' \\\n --output speech.${fmt}`; | |
| navigator.clipboard.writeText(cmd) | |
| .then(() => toast('⌘ cURL command copied!')) | |
| .catch(() => toast('Copy failed.')); | |
| } | |
| // status | |
| async function checkStatus() { | |
| try { | |
| const r = await fetch(`${API}/v1/status`); | |
| if (!r.ok) throw new Error(); | |
| const d = await r.json(); | |
| dotEl.className = 'dot online'; | |
| stxtEl.textContent = 'Online'; | |
| const models = (d.models || []).map(m => { | |
| const col = m.status === 'idle' ? '#00e676' : m.status === 'running' ? 'var(--accent)' : 'var(--dim)'; | |
| return `<span style="color:${col}">■</span> ${m.name} <span style="color:var(--text)">${m.status.toUpperCase()}</span>`; | |
| }).join('<br>'); | |
| const sys = d.system || {}; | |
| const mem = sys.memory || {}; | |
| srvInfo.innerHTML = `${models}<br>CPU <span style="color:var(--text)">${sys.cpu_usage_percent ?? '—'}%</span> · MEM <span style="color:var(--text)">${mem.used_mb ?? '—'} / ${mem.total_mb ?? '—'} MB</span> · UP <span style="color:var(--text)">${d.uptime ?? '—'}</span>`; | |
| } catch { | |
| dotEl.className = 'dot error'; | |
| stxtEl.textContent = 'Offline'; | |
| srvInfo.innerHTML = '<span style="color:var(--accent)">Cannot reach server</span>'; | |
| } | |
| } | |
| // generate | |
| async function generate() { | |
| const text = tin.value.trim(); | |
| if (!text) { toast('⚠ Please enter some text.'); return; } | |
| genBtn.disabled = true; | |
| genBtn.classList.add('loading'); | |
| genBtn.querySelector('.blabel').textContent = 'SYNTHESISING…'; | |
| const t0 = performance.now(); | |
| try { | |
| const res = await fetch(`${API}/v1/audio/speech`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ model, input: text, voice, response_format: fmt, speed: +spd.value }), | |
| }); | |
| if (!res.ok) { | |
| const e = await res.json().catch(() => ({ detail: res.statusText })); | |
| throw new Error(e.detail || `HTTP ${res.status}`); | |
| } | |
| const blob = await res.blob(); | |
| const ms = Math.round(performance.now() - t0); | |
| if (blobUrl) URL.revokeObjectURL(blobUrl); | |
| blobUrl = URL.createObjectURL(blob); | |
| ap.src = blobUrl; ap.load(); ap.play().catch(() => {}); | |
| const ext = fmt === 'mp3' ? 'wav' : fmt; | |
| dlBtn.href = blobUrl; | |
| dlBtn.download = `devil-studio-${voice.toLowerCase()}.${ext}`; | |
| outSec.style.display = 'block'; | |
| buildWave(); | |
| wvph.style.display = 'none'; | |
| wvbars.classList.add('on'); | |
| const latStr = ms >= 1000 ? (ms / 1000).toFixed(2) + 's' : ms + 'ms'; | |
| latD.textContent = latStr; | |
| lgD.textContent = new Date().toLocaleTimeString(); | |
| setMeter(ms); | |
| checkStatus(); | |
| } catch (err) { | |
| toast('✗ ' + err.message, 5000); | |
| } finally { | |
| genBtn.disabled = false; | |
| genBtn.classList.remove('loading'); | |
| genBtn.querySelector('.blabel').textContent = 'GENERATE SPEECH'; | |
| } | |
| } | |
| document.addEventListener('keydown', e => { if ((e.ctrlKey||e.metaKey) && e.key==='Enter') generate(); }); | |
| checkStatus(); | |
| setInterval(checkStatus, 30000); | |
| </script> | |
| </body> | |
| </html> |