srv_tts_01 / index.html
PyxiLabs's picture
Create index.html
7fd1e0c verified
<!DOCTYPE html>
<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 &nbsp;/&nbsp; 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></span><span></span><span></span><span></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>&nbsp;<span class="v">pyxilabs-srv-tts-01.hf.space</span></div>
<div class="si"><span class="k">LATENCY</span>&nbsp;<span class="v ac" id="latD"></span></div>
<div class="si"><span class="k">LAST GEN</span>&nbsp;<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> &nbsp;·&nbsp; MEM <span style="color:var(--text)">${mem.used_mb ?? '—'} / ${mem.total_mb ?? '—'} MB</span> &nbsp;·&nbsp; 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>