Spaces:
Running on Zero
Running on Zero
Download index.html from multimodalart/zero: direct link, hf CLI and curl.
- Browser
- Download file 19.6 kB
-
https://huggingface.co/spaces/multimodalart/zero/resolve/main/index.html
- Command line
-
hf download hf://spaces/multimodalart/zero/index.html
-
curl -L -o index.html https://huggingface.co/spaces/multimodalart/zero/resolve/main/index.html
19.6 kB
| <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> | |