chitra-tiny-webgpu / index.html
mfn
WebGPU demo
4cd411d
Raw History Blame Contribute Delete
10.1 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chitra Tiny WebGPU</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3E%3Crect x=%271.5%27 y=%272.5%27 width=%2713%27 height=%2711%27 rx=%271%27 fill=%27none%27 stroke=%27%23b4432b%27 stroke-width=%271.6%27/%3E%3Cpath d=%27M3 12l3.5-4 2.5 2.5L11 8l2 4z%27 fill=%27%23b4432b%27/%3E%3C/svg%3E">
<style>
:root{--bg:#f4f0e8;--panel:#fbf9f4;--line:#e3ddd0;--text:#1c1b18;--muted:#6b675e;--accent:#b4432b;--accent-text:#fff;--chip:#ece6da;--tile:#e9e3d6}
@media (prefers-color-scheme:dark){:root{--bg:#141311;--panel:#1d1b18;--line:#34302a;--text:#ece8e1;--muted:#a39d92;--accent:#d2593e;--accent-text:#fff;--chip:#27241f;--tile:#24211d}}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font:15px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
main{max-width:860px;margin:0 auto;padding:28px 16px 40px}
h1{font-size:26px;margin:0 0 6px;letter-spacing:-.01em}
p{margin:0 0 10px}
a{color:var(--accent)}
.muted{color:var(--muted);font-size:14px}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:14px;margin-top:16px}
label{display:block;font-size:13px;color:var(--muted);margin-bottom:6px}
textarea,input[type=text]{width:100%;border:1px solid var(--line);border-radius:4px;background:var(--bg);color:var(--text);font:inherit;padding:8px 10px}
textarea{min-height:64px;resize:vertical}
input[type=text]{height:36px}
textarea:focus,input:focus{outline:2px solid var(--accent);outline-offset:-1px}
details{margin-top:12px;border-top:1px solid var(--line);padding-top:10px}
summary{cursor:pointer;font-size:14px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px}
.span2{grid-column:1/-1}
input[type=number]{width:100%;height:36px;border:1px solid var(--line);border-radius:4px;background:var(--bg);color:var(--text);font:inherit;padding:0 10px}
input[type=range]{width:100%;accent-color:var(--accent);height:36px;margin:0}
.seedrow{display:flex;gap:10px;align-items:center}
.seedrow label{margin:0;display:flex;gap:6px;align-items:center;white-space:nowrap;color:var(--text);font-size:14px}
.row{display:flex;gap:10px;align-items:center;margin-top:12px;flex-wrap:wrap}
button{height:40px;border:0;border-radius:4px;background:var(--accent);color:var(--accent-text);font:600 15px system-ui,sans-serif;padding:0 22px;cursor:pointer}
button:disabled{opacity:.5;cursor:default}
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
.chip{background:var(--chip);border:1px solid var(--line);color:var(--text);height:auto;min-height:32px;padding:5px 10px;font-weight:400;font-size:13px;text-align:left;max-width:100%}
.bar{height:4px;background:var(--line);border-radius:2px;overflow:hidden;margin-top:10px;display:none}
.bar>div{height:100%;width:0;background:var(--accent)}
#out{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px}
#out a{display:block;aspect-ratio:1;background:var(--tile);border-radius:4px;overflow:hidden}
#out img{width:100%;height:100%;display:block;image-rendering:auto}
#out .empty{grid-column:1/-1;color:var(--muted);font-size:14px;padding:30px 0;text-align:center}
footer{margin-top:28px;font-size:14px}
@media (max-width:520px){.grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<main>
<h1>Chitra Tiny</h1>
<p>Type what you want to see and get small 256×256 pictures, made entirely in your browser. Nothing leaves your device.
Best at photos, paintings, landscapes, buildings, vehicles and food; weak at people, text and counting.</p>
<p class="muted">Model: <a href="https://huggingface.co/udgamlabs/Chitra-Tiny" target="_blank" rel="noopener">udgamlabs/Chitra-Tiny</a> · Server demo: <a href="https://huggingface.co/spaces/mrfakename/chitra-tiny" target="_blank" rel="noopener">mrfakename/chitra-tiny</a></p>
<div class="panel">
<label for="prompt">Prompt</label>
<textarea id="prompt" maxlength="500">a lighthouse on a rocky coast at sunset</textarea>
<details>
<summary>Settings</summary>
<div class="grid">
<div class="span2"><label for="neg">Avoid (optional)</label><input id="neg" type="text" placeholder="e.g. blurry, dark"></div>
<div><label for="n">Number of pictures: <span id="nv">2</span></label><input id="n" type="range" min="1" max="4" step="1" value="2"></div>
<div><label for="steps">Steps (fewer is faster): <span id="stepsv">30</span></label><input id="steps" type="range" min="10" max="100" step="5" value="30"></div>
<div><label for="guid">Prompt strength: <span id="guidv">7</span></label><input id="guid" type="range" min="1" max="12" step="0.5" value="7"></div>
<div><label for="seed">Seed</label><div class="seedrow"><input id="seed" type="number" min="0" step="1" value="0"><label><input id="rand" type="checkbox" checked> Random</label></div></div>
</div>
</details>
<div class="row"><button id="go" disabled>Loading…</button><span id="status" class="muted"></span></div>
<div class="bar" id="bar"><div></div></div>
</div>
<div class="panel">
<label>Pictures</label>
<div id="out"><div class="empty">Your pictures will show up here.</div></div>
</div>
<div class="panel">
<label>Try one</label>
<div class="chips" id="chips"></div>
</div>
<footer class="muted">The first visit downloads about 745 MB, then it is saved in your browser. Needs a browser with WebGPU (recent Chrome, Edge or Safari).<br><a href="https://x.com/realmrfakename" target="_blank" rel="noopener">x.com/realmrfakename</a></footer>
</main>
<script>
const EXAMPLES = [
"a lighthouse on a rocky coast at sunset",
"an oil painting of a vase of flowers",
"a mountain landscape with snow and pine trees",
"a watercolor painting of a bird on a branch",
"a red car parked in front of a house",
"a bridge over a river in autumn",
"a black and white photo of a city street",
"a bowl of soup next to a spoon",
];
const $ = id => document.getElementById(id);
const go = $("go"), status = $("status"), bar = $("bar"), fill = bar.firstElementChild, out = $("out");
let state = "probe", urls = [];
const worker = new Worker("worker.js", { type: "module" });
for (const t of EXAMPLES) {
const b = document.createElement("button"); b.className = "chip"; b.textContent = t;
b.onclick = () => { $("prompt").value = t; if (state === "ready") generate(); }; $("chips").appendChild(b);
}
for (const [id, fmt] of [["n", v => v], ["steps", v => v], ["guid", v => Number(v).toString()]])
$(id).oninput = e => $(id + "v").textContent = fmt(e.target.value);
const progress = f => { bar.style.display = "block"; fill.style.width = (100 * f).toFixed(1) + "%"; };
const done = () => { bar.style.display = "none"; fill.style.width = "0"; };
function setButton(text, enabled) { go.textContent = text; go.disabled = !enabled; }
function load() { state = "loading"; setButton("Loading…", false); status.textContent = "Downloading…"; worker.postMessage({ type: "load" }); }
function generate() {
if (state !== "ready") return;
const prompt = $("prompt").value.trim();
if (!prompt) { status.textContent = "Type a prompt first."; return; }
if ($("rand").checked) $("seed").value = Math.floor(Math.random() * 2147483647);
state = "busy"; setButton("Generating…", false); status.textContent = "Starting…"; progress(0);
worker.postMessage({ type: "generate", prompt, negative: $("neg").value.trim(), n: Number($("n").value),
steps: Number($("steps").value), guidance: Number($("guid").value), seed: Math.max(0, parseInt($("seed").value) || 0) });
}
go.onclick = () => state === "idle" ? load() : generate();
$("prompt").addEventListener("keydown", e => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); generate(); } });
const mb = b => (b / 1e6).toFixed(0);
worker.onmessage = ({ data }) => {
if (data.type === "probe") {
if (!data.webgpu) { state = "none"; setButton("Not supported", false); status.textContent = "This browser has no WebGPU. Try recent Chrome, Edge or Safari, or the server demo."; return; }
if (data.cached) load(); else { state = "idle"; setButton("Download model (745 MB)", true); status.textContent = "One-time download, then saved in your browser."; }
} else if (data.type === "download") {
progress(data.got / data.total); status.textContent = `Downloading… ${mb(data.got)} / ${mb(data.total)} MB`;
} else if (data.type === "status") {
status.textContent = data.text;
} else if (data.type === "ready") {
state = "ready"; done(); setButton("Generate", true); status.textContent = "Ready";
} else if (data.type === "step") {
progress(data.done / data.total); status.textContent = `Step ${data.done} of ${data.total}`;
} else if (data.type === "images") {
state = "ready"; done(); setButton("Generate", true);
urls.forEach(u => URL.revokeObjectURL(u)); urls = data.images.map(b => URL.createObjectURL(b));
out.replaceChildren(...urls.map((u, i) => {
const a = document.createElement("a"); a.href = u; a.download = `chitra-${data.seed}-${i}.png`; a.title = "Click to save";
const img = document.createElement("img"); img.src = u; img.alt = $("prompt").value; a.appendChild(img); return a;
}));
status.textContent = `Took ${data.took.toFixed(1)} s`;
document.body.dataset.done = "1";
} else if (data.type === "error") {
done();
if (data.message === "NO_WEBGPU") { state = "none"; setButton("Not supported", false); status.textContent = "WebGPU is not available here. Try recent Chrome, Edge or Safari, or the server demo."; return; }
const wasReady = state === "busy";
state = wasReady ? "ready" : "idle"; setButton(wasReady ? "Generate" : "Try again", true);
status.textContent = "Something went wrong: " + data.message;
document.body.dataset.error = data.message;
}
};
worker.postMessage({ type: "probe" });
</script>
</body>
</html>