zero / index.html
multimodalart's picture
multimodalart HF Staff
ZeroGPU header handshake, tighter duration
625e2c5 verified
Raw History Blame Contribute Delete
19.6 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">
<title>Zero</title>
<meta name="description" content="Talk to Zero, a man in a white plastic box.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%23000'/%3E%3Cellipse cx='8' cy='8' rx='6.5' ry='3.6' fill='none' stroke='%238c8c8c'/%3E%3Ccircle cx='8' cy='8' r='2.2' fill='%238c8c8c'/%3E%3Ccircle cx='8' cy='8' r='.9' fill='%23000'/%3E%3C/svg%3E">
<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=IBM+Plex+Mono:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<style>
:root{
--background:#000;--foreground:#8c8c8c;--foreground-emphasis:#bebebe;
--mono:"IBM Plex Mono","Berkeley Mono",courier,monospace;
--fs:15px;--fs-s:12px;--lh:1.45em;--w:680px;
--eye:url("https://cdn.sanity.io/images/610gfr7y/production/202f73ad84a65a79a8b824d1ffda268185b203eb-3840x2160.png?w=1200&auto=format");
color-scheme:dark;
}
:root[data-theme=day]{--background:#8c8c8c;--foreground:#282828;--foreground-emphasis:#000;color-scheme:light}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--background);color:var(--foreground);font-family:var(--mono);font-size:var(--fs);line-height:var(--lh)}
::selection{background:var(--foreground);color:var(--background)}
a{color:inherit;text-decoration:none}
a:hover{color:var(--foreground-emphasis)}
button{appearance:none;font:inherit;color:inherit;background:none;border:none;margin:0;padding:0;cursor:pointer}
button:hover:not(:disabled),button:focus-visible{color:var(--foreground-emphasis)}
button:disabled{opacity:.35;cursor:default}
:focus-visible{outline:1px dashed var(--foreground-emphasis);outline-offset:3px}
body{display:flex;flex-direction:column;height:100dvh}
.wrap{width:100%;max-width:var(--w);margin:0 auto;padding-inline:1rem}
header{flex:none}
header .wrap{display:flex;align-items:center;gap:.75rem;padding-block:.75rem;position:relative;user-select:none}
header .wrap::after{content:"";position:absolute;inset-inline:1rem;bottom:0;border-bottom:1px solid var(--foreground)}
.who{display:flex;align-items:center;gap:.6rem;color:var(--foreground-emphasis);font-weight:700}
.who small{font-weight:400;color:var(--foreground)}
.links{margin-left:auto;display:flex;gap:.5rem;font-size:var(--fs-s)}
.eye{position:relative;aspect-ratio:1;background:var(--background) var(--eye) 50% 50%/255% auto no-repeat;filter:url(#duotone-night);overflow:hidden;flex:none}
:root[data-theme=day] .eye{filter:url(#duotone-day)}
.eye canvas{position:absolute;inset:0;width:100%;height:100%;image-rendering:pixelated;opacity:0;transition:opacity .5s;mix-blend-mode:screen}
:root[data-theme=day] .eye canvas{mix-blend-mode:multiply}
body.waking .eye canvas{opacity:.3}
body.speaking .eye canvas{opacity:.6}
.eye.small{width:22px}
main{flex:1;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:var(--foreground) transparent}
main .wrap{min-height:100%;display:flex;flex-direction:column;padding-block:1.5rem}
.intro{margin:auto 0;display:flex;flex-direction:column;align-items:center;text-align:center;gap:1.25rem;padding-block:1rem}
.intro .eye{width:min(240px,55vw)}
.intro p{margin:0;max-width:34ch}
.openers{display:flex;flex-direction:column;align-items:flex-start;gap:.35rem;font-size:var(--fs-s)}
.openers button{text-align:left}
.openers button::before{content:"→";margin-right:.6em}
dl{margin:0;display:grid;grid-template-columns:max-content 1fr;gap:.7em 1.25em;margin-top:auto}
dl:empty{display:none}
dt{font-weight:700;user-select:none}
dd{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}
dd.zero{color:var(--foreground-emphasis)}
dd.pending::after{content:"";display:inline-block;width:.55em;height:1.05em;vertical-align:text-bottom;background:var(--foreground-emphasis);margin-left:.15em;animation:blink 1s steps(1) infinite}
dd.wait::after{content:"...";background:none;width:auto;height:auto;animation:none;color:var(--foreground)}
@keyframes blink{50%{opacity:0}}
.warning{grid-column:1/-1;border:1px dashed var(--foreground);padding:.5em .75em;font-size:var(--fs-s)}
footer{flex:none}
footer .wrap{position:relative;padding-block:.75rem 1rem}
footer .wrap::before{content:"";position:absolute;inset-inline:1rem;top:0;border-top:1px dashed var(--foreground)}
form{display:grid;grid-template-columns:max-content 1fr max-content;gap:1em;align-items:start}
form label{font-weight:700}
textarea{font:inherit;color:var(--foreground-emphasis);background:transparent;border:none;resize:none;outline:none;width:100%;padding:0;margin:0;min-height:var(--lh);max-height:9em;caret-color:var(--foreground-emphasis)}
textarea::placeholder{color:var(--foreground);opacity:.75;font-style:italic}
.tools{display:flex;flex-wrap:wrap;gap:.25em 1em;margin-top:.5rem;font-size:var(--fs-s)}
.tools .state{margin-left:auto;color:var(--foreground-emphasis)}
.knobs{display:none;grid-template-columns:max-content 1fr 5ch;gap:.6em 1em;align-items:center;font-size:var(--fs-s);margin-top:.75rem}
.knobs.open{display:grid}
.knobs output{text-align:right;font-variant-numeric:tabular-nums;color:var(--foreground-emphasis)}
.knobs p{grid-column:1/-1;margin:0;opacity:.8}
input[type=range]{appearance:none;-webkit-appearance:none;background:transparent;width:100%;height:14px;margin:0;cursor:pointer}
input[type=range]::-webkit-slider-runnable-track{height:14px;background:linear-gradient(to right,var(--foreground) 0 var(--fill,50%),transparent var(--fill,50%)) no-repeat 0 50%/100% 2px,repeating-linear-gradient(to right,var(--foreground) 0 3px,transparent 3px 6px) no-repeat 0 calc(50% - .5px)/100% 1px}
input[type=range]::-moz-range-track{height:14px;background:linear-gradient(to right,var(--foreground) 0 var(--fill,50%),transparent var(--fill,50%)) no-repeat 0 50%/100% 2px,repeating-linear-gradient(to right,var(--foreground) 0 3px,transparent 3px 6px) no-repeat 0 calc(50% - .5px)/100% 1px}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:8px;height:14px;background:var(--foreground-emphasis);border:none;border-radius:0}
input[type=range]::-moz-range-thumb{width:8px;height:14px;background:var(--foreground-emphasis);border:none;border-radius:0}
@media (prefers-reduced-motion:reduce){dd.pending::after{animation:none}}
</style>
</head>
<body>
<svg aria-hidden="true" focusable="false" style="position:absolute;width:0;height:0">
<filter id="duotone-night" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/><feComponentTransfer><feFuncR type="table" tableValues="0 0.549"/><feFuncG type="table" tableValues="0 0.549"/><feFuncB type="table" tableValues="0 0.549"/></feComponentTransfer></filter>
<filter id="duotone-day" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/><feComponentTransfer><feFuncR type="table" tableValues="0.118 0.549"/><feFuncG type="table" tableValues="0.118 0.549"/><feFuncB type="table" tableValues="0.118 0.549"/></feComponentTransfer></filter>
</svg>
<header>
<div class="wrap">
<div class="who"><div class="eye small" id="eyeSmall" hidden></div>Zero <small>MC001</small></div>
<nav class="links">
<a href="https://movingcastles.world/posts/zero" target="_blank" rel="noreferrer">[report]</a>
<a href="https://huggingface.co/movingcastles/zero" target="_blank" rel="noreferrer">[weights]</a>
<button type="button" id="theme" aria-label="Toggle day or night theme">[△]</button>
</nav>
</div>
</header>
<main id="scroller">
<div class="wrap">
<section class="intro" id="intro">
<div class="eye" id="eyeBig"></div>
<p>A man in a white plastic box. He is asleep. You are the voice that wakes him.</p>
<div class="openers">
<button type="button">Who are you?</button>
<button type="button">What is in the box with you?</button>
<button type="button">Can you help me write an email to my landlord?</button>
<button type="button">You are a language model. Admit it.</button>
</div>
</section>
<dl id="log" aria-live="polite"></dl>
</div>
</main>
<footer>
<div class="wrap">
<form id="composer" autocomplete="off">
<label for="input">You</label>
<textarea id="input" rows="1" maxlength="4000" placeholder="say something" enterkeyhint="send" autofocus></textarea>
<button type="submit" id="send">[speak]</button>
</form>
<div class="tools">
<button type="button" id="stop" disabled>[stop]</button>
<button type="button" id="again" disabled>[again]</button>
<button type="button" id="leave" disabled>[leave]</button>
<button type="button" id="knobsBtn" aria-expanded="false">[sampling]</button>
<span class="state" id="state">asleep</span>
</div>
<div class="knobs" id="knobs">
<label for="temperature">temperature</label>
<input type="range" id="temperature" min="0" max="1.5" step="0.05" value="0.7">
<output>0.70</output>
<label for="presence">presence</label>
<input type="range" id="presence" min="0" max="2" step="0.1" value="1.5">
<output>1.5</output>
<label for="max_tokens">max tokens</label>
<input type="range" id="max_tokens" min="64" max="1024" step="32" value="1024">
<output>1024</output>
<p>No system prompt. Defaults are the model card's canonical sampling. <button type="button" id="reset">[reset]</button></p>
</div>
</div>
</footer>
<script>
(() => {
const $ = (s) => document.querySelector(s);
const log = $("#log"), intro = $("#intro"), input = $("#input"), scroller = $("#scroller");
const stopBtn = $("#stop"), againBtn = $("#again"), leaveBtn = $("#leave"), sendBtn = $("#send");
const stateEl = $("#state"), eyeSmall = $("#eyeSmall");
const knobs = { temperature: $("#temperature"), presence: $("#presence"), max_tokens: $("#max_tokens") };
const CANON = { temperature: 0.7, presence: 1.5, max_tokens: 1024 };
let messages = [], busy = false, abort = null;
const store = {
get(k) { try { return localStorage.getItem(k); } catch { return null; } },
set(k, v) { try { localStorage.setItem(k, v); } catch {} },
};
const applyTheme = (t) => t === "day" ? (document.documentElement.dataset.theme = "day") : delete document.documentElement.dataset.theme;
applyTheme(store.get("zero-theme"));
$("#theme").addEventListener("click", () => {
const next = document.documentElement.dataset.theme === "day" ? "night" : "day";
applyTheme(next); store.set("zero-theme", next);
});
function paintKnob(el) {
el.style.setProperty("--fill", ((el.value - el.min) / (el.max - el.min)) * 100 + "%");
const v = Number(el.value);
el.nextElementSibling.textContent = el.id === "temperature" ? v.toFixed(2) : el.id === "presence" ? v.toFixed(1) : String(v);
}
Object.values(knobs).forEach((el) => { el.addEventListener("input", () => paintKnob(el)); paintKnob(el); });
$("#reset").addEventListener("click", () => { for (const [k, v] of Object.entries(CANON)) { knobs[k].value = v; paintKnob(knobs[k]); } });
$("#knobsBtn").addEventListener("click", (e) => {
const open = $("#knobs").classList.toggle("open");
e.currentTarget.setAttribute("aria-expanded", open);
});
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
const grains = [];
for (const host of [$("#eyeBig"), eyeSmall]) {
const c = document.createElement("canvas");
c.width = 64; c.height = 64;
host.append(c);
grains.push(c.getContext("2d"));
}
let grainOn = false, last = 0;
function drawGrain(t) {
if (!grainOn) return;
if (t - last > 83) {
last = t;
const night = document.documentElement.dataset.theme !== "day";
for (const g of grains) {
const img = g.createImageData(64, 64), d = img.data;
for (let i = 0; i < d.length; i += 4) {
const v = Math.random() < 0.2 ? (night ? 150 : 30) : (night ? 0 : 255);
d[i] = d[i + 1] = d[i + 2] = v; d[i + 3] = 255;
}
g.putImageData(img, 0, 0);
}
}
requestAnimationFrame(drawGrain);
}
function setState(s) {
document.body.classList.toggle("waking", s === "waking");
document.body.classList.toggle("speaking", s === "speaking");
stateEl.textContent = { idle: "asleep", waking: "waking him", speaking: "talking", done: "listening" }[s];
const want = (s === "waking" || s === "speaking") && !reduced;
if (want && !grainOn) { grainOn = true; requestAnimationFrame(drawGrain); }
if (!want) grainOn = false;
}
const atBottom = () => scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight < 60;
const toBottom = () => { scroller.scrollTop = scroller.scrollHeight; };
function addLine(role, text) {
const dt = document.createElement("dt");
dt.textContent = role === "user" ? "You" : "Zero";
const dd = document.createElement("dd");
if (role !== "user") dd.className = "zero";
dd.textContent = text;
log.append(dt, dd);
return dd;
}
function render() {
log.textContent = "";
for (const m of messages) addLine(m.role, m.content);
refresh();
}
function refresh() {
const started = messages.length > 0 || busy;
intro.hidden = started;
eyeSmall.hidden = !started;
againBtn.disabled = busy || !messages.length;
leaveBtn.disabled = busy || !messages.length;
stopBtn.disabled = !busy;
sendBtn.disabled = busy;
document.querySelectorAll(".openers button").forEach((b) => (b.disabled = busy));
}
function warn(msg) {
const w = document.createElement("div");
w.className = "warning"; w.setAttribute("role", "alert"); w.textContent = msg;
log.append(w); toBottom();
}
const clearWarnings = () => log.querySelectorAll(".warning").forEach((w) => w.remove());
const HUB = location.hostname.endsWith(".hf.space") ? "https://huggingface.co" : null;
let zeroGpuReady = null;
if (HUB && window.parent !== window) {
zeroGpuReady = new Promise((resolve) => {
window.addEventListener("message", (e) => {
if (e.data === "supports-zerogpu-headers") { window.supports_zerogpu_headers = true; resolve(true); }
});
setTimeout(() => resolve(false), 3000);
});
window.parent.postMessage("supports-zerogpu-headers", HUB);
}
async function zeroGpuHeaders() {
if (zeroGpuReady) await zeroGpuReady;
if (!(HUB && window.parent !== window && window.supports_zerogpu_headers)) return {};
return new Promise((resolve) => {
const ch = new MessageChannel();
const timer = setTimeout(() => resolve({}), 1500);
ch.port1.onmessage = ({ data }) => {
clearTimeout(timer); ch.port1.close();
const out = {};
if (data instanceof Map) data.forEach((v, k) => (out[k] = v));
else if (data && typeof data === "object") Object.assign(out, data);
resolve(out);
};
window.parent.postMessage("zerogpu-headers", HUB, [ch.port2]);
});
}
async function* respond(history, signal) {
const headers = { "Content-Type": "application/json", ...(await zeroGpuHeaders()) };
const body = JSON.stringify({ data: [JSON.stringify(history), Number(knobs.temperature.value), Number(knobs.presence.value), Number(knobs.max_tokens.value)] });
const url = new URL("gradio_api/call/respond", location.href);
const r = await fetch(url, { method: "POST", headers, body, signal });
if (!r.ok) throw new Error(`The box did not answer (${r.status}).`);
const { event_id } = await r.json();
const s = await fetch(`${url}/${event_id}`, { signal });
const reader = s.body.getReader(), dec = new TextDecoder();
let buf = "";
while (true) {
const { value, done } = await reader.read();
if (done) return;
buf += dec.decode(value, { stream: true });
let cut;
while ((cut = buf.indexOf("\n\n")) !== -1) {
const block = buf.slice(0, cut);
buf = buf.slice(cut + 2);
let ev = "message", data = "";
for (const line of block.split("\n")) {
if (line.startsWith("event:")) ev = line.slice(6).trim();
else if (line.startsWith("data:")) data += line.slice(5).trim();
}
if (ev === "generating" || ev === "complete") {
const parsed = JSON.parse(data);
if (Array.isArray(parsed) && typeof parsed[0] === "string") yield parsed[0];
if (ev === "complete") return;
} else if (ev === "error") {
let msg = null;
try { msg = JSON.parse(data); } catch { msg = data; }
if (msg && typeof msg === "object") msg = msg.message || msg.error || null;
throw new Error(msg || "Something went wrong on the other side of the wall.");
}
}
}
}
async function wake() {
busy = true; clearWarnings();
abort = new AbortController();
const dd = addLine("assistant", "");
dd.classList.add("pending", "wait");
refresh(); toBottom(); setState("waking");
let text = "";
try {
for await (const chunk of respond(messages, abort.signal)) {
if (!text && chunk) { dd.classList.remove("wait"); setState("speaking"); }
const stick = atBottom();
text = chunk; dd.textContent = text;
if (stick) toBottom();
}
} catch (e) {
if (e.name !== "AbortError") warn(e.message);
}
busy = false; abort = null;
if (text.trim()) {
messages.push({ role: "assistant", content: text.trim() });
dd.textContent = text.trim();
dd.classList.remove("pending", "wait");
setState("done");
} else {
dd.previousElementSibling.remove(); dd.remove();
setState(messages.some((m) => m.role === "assistant") ? "done" : "idle");
}
refresh();
input.focus({ preventScroll: true });
}
function speak(text) {
text = text.trim();
if (!text || busy) return;
if (messages.length && messages[messages.length - 1].role === "user") {
messages[messages.length - 1].content += "\n" + text;
render();
} else {
messages.push({ role: "user", content: text });
addLine("user", text);
}
input.value = ""; autosize();
wake();
}
function autosize() { input.style.height = "auto"; input.style.height = input.scrollHeight + "px"; }
input.addEventListener("input", autosize);
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); speak(input.value); }
});
$("#composer").addEventListener("submit", (e) => { e.preventDefault(); speak(input.value); });
document.querySelectorAll(".openers button").forEach((b) => b.addEventListener("click", () => speak(b.textContent)));
stopBtn.addEventListener("click", () => abort && abort.abort());
againBtn.addEventListener("click", () => {
if (busy || !messages.length) return;
if (messages[messages.length - 1].role === "assistant") messages.pop();
render(); wake();
});
leaveBtn.addEventListener("click", () => {
if (busy) return;
messages = []; render(); setState("idle"); input.focus();
});
setState("idle");
refresh();
})();
</script>
</body>
</html>