Spaces:
Running on Zero
Running on Zero
Live 3D preview while generating, spinner + timing stats
Browse files
app.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
import ctypes
|
| 2 |
import glob
|
| 3 |
-
import html
|
| 4 |
import re
|
| 5 |
import site
|
|
|
|
| 6 |
|
| 7 |
import gradio as gr
|
| 8 |
import spaces
|
|
@@ -61,13 +61,6 @@ def extract_html(text):
|
|
| 61 |
return m.group(0) if m else text.strip()
|
| 62 |
|
| 63 |
|
| 64 |
-
def iframe(code):
|
| 65 |
-
return (
|
| 66 |
-
f'<iframe sandbox="allow-scripts" srcdoc="{html.escape(code, quote=True)}" '
|
| 67 |
-
'style="width:100%;height:640px;border:0;border-radius:8px;background:#000"></iframe>'
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
# prefill the code fence so the model skips chatty preambles ("I cannot create files...")
|
| 72 |
NO_THINK = "<think>\n\n</think>\n\n```html\n"
|
| 73 |
|
|
@@ -76,16 +69,19 @@ NO_THINK = "<think>\n\n</think>\n\n```html\n"
|
|
| 76 |
@spaces.GPU(duration=107)
|
| 77 |
def generate(prompt, thinking, max_tokens, temperature, top_p, top_k):
|
| 78 |
global llm
|
|
|
|
| 79 |
if llm is None:
|
| 80 |
from llama_cpp import Llama # import inside GPU worker so CUDA is available
|
| 81 |
|
| 82 |
llm = Llama(MODEL_PATH, n_gpu_layers=-1, n_ctx=32768, flash_attn=True, verbose=False)
|
|
|
|
| 83 |
# ponytail: hand-built ChatML so thinking can be toggled; switch to create_chat_completion if template kwargs land
|
| 84 |
text = (
|
| 85 |
f"<|im_start|>system\n{SYSTEM_PROMPT}<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"
|
| 86 |
+ ("<think>\n" if thinking else NO_THINK)
|
| 87 |
)
|
| 88 |
out = "<think>\n" if thinking else "```html\n"
|
|
|
|
| 89 |
for chunk in llm(
|
| 90 |
text,
|
| 91 |
max_tokens=int(max_tokens),
|
|
@@ -97,18 +93,104 @@ def generate(prompt, thinking, max_tokens, temperature, top_p, top_k):
|
|
| 97 |
stream=True,
|
| 98 |
):
|
| 99 |
out += chunk["choices"][0]["text"]
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
|
| 104 |
GPU, WEB = "ZeroGPU (server)", "WebGPU (your browser)"
|
| 105 |
|
| 106 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
WEBGPU_JS = r"""
|
| 108 |
async (prompt, thinking, maxTokens, temperature, topP, topK) => {
|
|
|
|
| 109 |
const st = document.querySelector('#wg-status');
|
| 110 |
-
const say = (h) => { st.innerHTML = h; };
|
| 111 |
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
try {
|
| 113 |
window.wg ??= (async () => {
|
| 114 |
const V = 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.8.1/esm/';
|
|
@@ -116,58 +198,66 @@ async (prompt, thinking, maxTokens, temperature, topP, topK) => {
|
|
| 116 |
const w = new Wllama({ default: V + 'wasm/wllama.wasm' });
|
| 117 |
const load = () => w.loadModelFromHF({ repo: '__REPO__', quant: 'Q4_K_M' }, {
|
| 118 |
n_ctx: 18432, n_gpu_layers: 999,
|
| 119 |
-
progressCallback: ({ loaded, total }) =>
|
| 120 |
-
`Downloading model (cached after first run): ${(loaded / 1e9).toFixed(2)} / ${(total / 1e9).toFixed(2)} GB`
|
|
|
|
| 121 |
});
|
| 122 |
// an interrupted download leaves a partial cache that wllama won't repair; wipe it and retry once
|
| 123 |
-
try { await load(); } catch (e) {
|
| 124 |
return w;
|
| 125 |
})().catch((e) => { window.wg = null; throw e; });
|
| 126 |
-
if (!navigator.gpu) say('<b>No WebGPU in this browser, falling back to CPU (very slow).</b> Use Chrome/Edge 113+.');
|
| 127 |
const w = await window.wg;
|
|
|
|
|
|
|
| 128 |
const text = `<|im_start|>system\n__SYSTEM__<|im_end|>\n<|im_start|>user\n${prompt}<|im_end|>\n<|im_start|>assistant\n`
|
| 129 |
+ (thinking ? '<think>\n' : '__NO_THINK__');
|
| 130 |
-
|
| 131 |
-
const t0 = performance.now();
|
| 132 |
const it = await w.createCompletion({ prompt: text, max_tokens: maxTokens, temperature, top_p: topP, top_k: topK,
|
| 133 |
penalty_repeat: 1.0, stop: ['<|im_end|>'], stream: true });
|
| 134 |
for await (const c of it) {
|
| 135 |
out += c.choices[0].text;
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
| 138 |
}
|
| 139 |
-
const
|
| 140 |
-
|
| 141 |
-
const
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
| 146 |
} catch (e) {
|
| 147 |
-
|
| 148 |
-
|
| 149 |
}
|
|
|
|
| 150 |
}
|
| 151 |
-
""".replace("__REPO__", "ReallyFloppyPenguin/Qwen3.5-4B-3D-Coder-GGUF-split").replace(
|
| 152 |
"__SYSTEM__", SYSTEM_PROMPT.replace("\n", "\\n")
|
| 153 |
).replace("__NO_THINK__", NO_THINK.replace("\n", "\\n").replace("`", "\\`"))
|
| 154 |
|
| 155 |
|
| 156 |
def run(prompt, thinking, max_tokens, temperature, top_p, top_k):
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
| 161 |
with open("scene.html", "w", encoding="utf-8") as f:
|
| 162 |
-
f.write(
|
| 163 |
-
yield raw,
|
| 164 |
|
| 165 |
|
| 166 |
with gr.Blocks(title="3D World Generator (WebGPU)") as demo:
|
| 167 |
gr.Markdown(
|
| 168 |
"# 3D World Generator (WebGPU)\nDescribe a world; "
|
| 169 |
"[ccore/Qwen3.5-4B-3D-Coder](https://huggingface.co/ccore/Qwen3.5-4B-3D-Coder) writes a self-contained "
|
| 170 |
-
"Three.js scene. Drag/scroll in the preview to move the camera (controls are model-written, so they vary).
|
|
|
|
| 171 |
"**WebGPU** runs the model in your browser (2.7 GB download once, then cached; needs Chrome/Edge 113+). "
|
| 172 |
"**ZeroGPU** runs it on the server."
|
| 173 |
)
|
|
@@ -186,13 +276,19 @@ with gr.Blocks(title="3D World Generator (WebGPU)") as demo:
|
|
| 186 |
top_k = gr.Slider(1, 100, value=20, step=1, label="Top K")
|
| 187 |
gr.Examples(EXAMPLES, inputs=prompt, example_labels=EXAMPLE_LABELS, label="Examples from the model card")
|
| 188 |
with gr.Column(scale=2):
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
download = gr.File(label="Download scene.html")
|
| 191 |
with gr.Accordion("Raw model output", open=False):
|
| 192 |
code = gr.Code(language="html", label="Output")
|
| 193 |
inputs = [prompt, thinking, max_tokens, temperature, top_p, top_k]
|
| 194 |
-
btn.click(
|
| 195 |
-
|
|
|
|
|
|
|
| 196 |
backend.change(lambda b: (gr.update(visible=b == GPU), gr.update(visible=b == WEB)), backend, [btn, web_btn])
|
| 197 |
|
| 198 |
demo.queue().launch()
|
|
|
|
| 1 |
import ctypes
|
| 2 |
import glob
|
|
|
|
| 3 |
import re
|
| 4 |
import site
|
| 5 |
+
import time
|
| 6 |
|
| 7 |
import gradio as gr
|
| 8 |
import spaces
|
|
|
|
| 61 |
return m.group(0) if m else text.strip()
|
| 62 |
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
# prefill the code fence so the model skips chatty preambles ("I cannot create files...")
|
| 65 |
NO_THINK = "<think>\n\n</think>\n\n```html\n"
|
| 66 |
|
|
|
|
| 69 |
@spaces.GPU(duration=107)
|
| 70 |
def generate(prompt, thinking, max_tokens, temperature, top_p, top_k):
|
| 71 |
global llm
|
| 72 |
+
t0 = time.perf_counter()
|
| 73 |
if llm is None:
|
| 74 |
from llama_cpp import Llama # import inside GPU worker so CUDA is available
|
| 75 |
|
| 76 |
llm = Llama(MODEL_PATH, n_gpu_layers=-1, n_ctx=32768, flash_attn=True, verbose=False)
|
| 77 |
+
t1 = time.perf_counter()
|
| 78 |
# ponytail: hand-built ChatML so thinking can be toggled; switch to create_chat_completion if template kwargs land
|
| 79 |
text = (
|
| 80 |
f"<|im_start|>system\n{SYSTEM_PROMPT}<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"
|
| 81 |
+ ("<think>\n" if thinking else NO_THINK)
|
| 82 |
)
|
| 83 |
out = "<think>\n" if thinking else "```html\n"
|
| 84 |
+
n, t2 = 0, None
|
| 85 |
for chunk in llm(
|
| 86 |
text,
|
| 87 |
max_tokens=int(max_tokens),
|
|
|
|
| 93 |
stream=True,
|
| 94 |
):
|
| 95 |
out += chunk["choices"][0]["text"]
|
| 96 |
+
n += 1
|
| 97 |
+
t2 = t2 or time.perf_counter()
|
| 98 |
+
yield out, f"{n} tokens, {n / (time.perf_counter() - t2 + 1e-9):.1f} tok/s"
|
| 99 |
+
t3 = time.perf_counter()
|
| 100 |
+
t2 = t2 or t3
|
| 101 |
+
# logged so the @spaces.GPU duration can be sized from real runs
|
| 102 |
+
stats = (
|
| 103 |
+
f"GPU time {t3 - t0:.1f}s: model load {t1 - t0:.1f}s, prefill {t2 - t1:.1f}s, "
|
| 104 |
+
f"{n} tokens in {t3 - t2:.1f}s ({n / (t3 - t2 + 1e-9):.1f} tok/s)"
|
| 105 |
+
)
|
| 106 |
+
print(stats, flush=True)
|
| 107 |
+
yield out, stats
|
| 108 |
|
| 109 |
|
| 110 |
GPU, WEB = "ZeroGPU (server)", "WebGPU (your browser)"
|
| 111 |
|
| 112 |
+
# Shared by both backends: window.wgLive(rawOutput) re-renders the partial scene into #wg-live every few seconds.
|
| 113 |
+
# Unfinished JS is cut back to the last complete-looking line, open brackets are closed and a fallback render loop
|
| 114 |
+
# is appended; the new iframe only replaces the old one once that patched script actually ran (it posts 'wg-ok').
|
| 115 |
+
LIVE_JS = r"""
|
| 116 |
+
window.wgCode ??= (out) => {
|
| 117 |
+
const body = out.replace(/<think>[\s\S]*?(<\/think>|$)/g, '');
|
| 118 |
+
let m = body.match(/```html\s*([\s\S]*?)(```|$)/);
|
| 119 |
+
return m ? m[1].trim() : ((m = body.match(/<!DOCTYPE html[\s\S]*?(<\/html>|$)/i)) ? m[0] : body.trim());
|
| 120 |
+
};
|
| 121 |
+
window.wgLive ??= (() => {
|
| 122 |
+
const TAIL = "\n;try{const R=renderer,S=scene,C=camera;(function f(){try{R.render(S,C)}catch(e){}"
|
| 123 |
+
+ "requestAnimationFrame(f)})()}catch(e){}\nparent.postMessage('wg-ok','*');\n</scr" + "ipt>";
|
| 124 |
+
const closers = (s) => {
|
| 125 |
+
const st = [];
|
| 126 |
+
for (let i = 0; i < s.length; i++) {
|
| 127 |
+
const c = s[i];
|
| 128 |
+
if (c === '/' && s[i + 1] === '/') { i = s.indexOf('\n', i); if (i < 0) break; }
|
| 129 |
+
else if (c === '/' && s[i + 1] === '*') { i = s.indexOf('*/', i + 2) + 1; if (i <= 0) break; }
|
| 130 |
+
else if (c === '"' || c === "'" || c === '`') { while (++i < s.length && s[i] !== c) if (s[i] === '\\') i++; }
|
| 131 |
+
else if ('([{'.includes(c)) st.push({ '(': ')', '[': ']', '{': '}' }[c]);
|
| 132 |
+
else if (')]}'.includes(c)) st.pop();
|
| 133 |
+
}
|
| 134 |
+
return st.reverse().join('');
|
| 135 |
+
};
|
| 136 |
+
// -> [html, isPartial], or null when there is no script to run yet
|
| 137 |
+
const patch = (code) => {
|
| 138 |
+
const i = code.lastIndexOf('<script'), j = code.indexOf('>', i) + 1;
|
| 139 |
+
if (i < 0 || !j) return null;
|
| 140 |
+
if (/<\/script>/i.test(code.slice(j))) return [code, false];
|
| 141 |
+
const lines = code.slice(j).split('\n');
|
| 142 |
+
lines.pop();
|
| 143 |
+
while (lines.length && !/[;{},]\s*$/.test(lines.at(-1))) lines.pop();
|
| 144 |
+
const js = lines.join('\n');
|
| 145 |
+
return [code.slice(0, j) + js + '\n' + closers(js) + TAIL, true];
|
| 146 |
+
};
|
| 147 |
+
let latest = '', last = 0, timer = 0, pending = null, shown = '';
|
| 148 |
+
const box = () => document.querySelector('#wg-live');
|
| 149 |
+
const show = (f) => {
|
| 150 |
+
if (pending !== f) return;
|
| 151 |
+
pending = null; shown = f.srcdoc;
|
| 152 |
+
for (const c of [...box().children]) if (c !== f) c.remove();
|
| 153 |
+
f.style.visibility = 'visible';
|
| 154 |
+
};
|
| 155 |
+
addEventListener('message', (e) => { if (pending && e.source === pending.contentWindow && e.data === 'wg-ok') show(pending); });
|
| 156 |
+
const render = () => {
|
| 157 |
+
timer = 0;
|
| 158 |
+
const p = patch(window.wgCode(latest));
|
| 159 |
+
if (!p || p[0] === shown || p[0] === pending?.srcdoc) return;
|
| 160 |
+
if (pending && p[1]) { timer = setTimeout(render, 1000); return; } // let the in-flight partial finish first
|
| 161 |
+
pending?.remove();
|
| 162 |
+
last = performance.now();
|
| 163 |
+
const f = pending = document.createElement('iframe');
|
| 164 |
+
f.sandbox = 'allow-scripts';
|
| 165 |
+
f.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:0;visibility:hidden';
|
| 166 |
+
if (!p[1]) f.onload = () => show(f); // final scene: show it even if it errors
|
| 167 |
+
setTimeout(() => { if (pending === f) { f.remove(); pending = null; } }, 15000);
|
| 168 |
+
f.srcdoc = p[0];
|
| 169 |
+
box().append(f);
|
| 170 |
+
};
|
| 171 |
+
return (raw) => {
|
| 172 |
+
latest = raw;
|
| 173 |
+
if (!raw) { clearTimeout(timer); timer = 0; pending = null; shown = ''; box().replaceChildren(); return; }
|
| 174 |
+
timer ||= setTimeout(render, Math.max(0, 5000 - (performance.now() - last)));
|
| 175 |
+
};
|
| 176 |
+
})();
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
# Same prompt/extract flow as the server path, but llama.cpp runs in the browser via wllama.
|
| 180 |
WEBGPU_JS = r"""
|
| 181 |
async (prompt, thinking, maxTokens, temperature, topP, topK) => {
|
| 182 |
+
__LIVE__
|
| 183 |
const st = document.querySelector('#wg-status');
|
|
|
|
| 184 |
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
|
| 185 |
+
const t0 = performance.now(), sec = (a, b = performance.now()) => ((b - a) / 1000).toFixed(1);
|
| 186 |
+
const S = window.wgS = { phase: 'Loading wllama runtime', tail: '' };
|
| 187 |
+
const cpu = navigator.gpu ? '' : '<br><b>No WebGPU in this browser, running on CPU (very slow).</b> Use Chrome/Edge 113+.';
|
| 188 |
+
let k = 0, out = '', n = 0, tLoad, tFirst;
|
| 189 |
+
const tick = setInterval(() => {
|
| 190 |
+
st.innerHTML = `<b>${'⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'[k++ % 10]} ${S.phase}</b> · ${sec(t0)}s elapsed${cpu}`
|
| 191 |
+
+ (S.tail && `<pre style="white-space:pre-wrap;max-height:200px;overflow:auto">${esc(S.tail)}</pre>`);
|
| 192 |
+
}, 100);
|
| 193 |
+
window.wgLive('');
|
| 194 |
try {
|
| 195 |
window.wg ??= (async () => {
|
| 196 |
const V = 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.8.1/esm/';
|
|
|
|
| 198 |
const w = new Wllama({ default: V + 'wasm/wllama.wasm' });
|
| 199 |
const load = () => w.loadModelFromHF({ repo: '__REPO__', quant: 'Q4_K_M' }, {
|
| 200 |
n_ctx: 18432, n_gpu_layers: 999,
|
| 201 |
+
progressCallback: ({ loaded, total }) => { window.wgS.phase = loaded < total
|
| 202 |
+
? `Downloading model (cached after first run): ${(loaded / 1e9).toFixed(2)} / ${(total / 1e9).toFixed(2)} GB`
|
| 203 |
+
: 'Loading model onto GPU'; },
|
| 204 |
});
|
| 205 |
// an interrupted download leaves a partial cache that wllama won't repair; wipe it and retry once
|
| 206 |
+
try { await load(); } catch (e) { window.wgS.phase = 'Model cache incomplete, re-downloading'; await w.cacheManager.clear(); await load(); }
|
| 207 |
return w;
|
| 208 |
})().catch((e) => { window.wg = null; throw e; });
|
|
|
|
| 209 |
const w = await window.wg;
|
| 210 |
+
tLoad = performance.now();
|
| 211 |
+
S.phase = 'Reading prompt (prefill)';
|
| 212 |
const text = `<|im_start|>system\n__SYSTEM__<|im_end|>\n<|im_start|>user\n${prompt}<|im_end|>\n<|im_start|>assistant\n`
|
| 213 |
+ (thinking ? '<think>\n' : '__NO_THINK__');
|
| 214 |
+
out = thinking ? '<think>\n' : '```html\n';
|
|
|
|
| 215 |
const it = await w.createCompletion({ prompt: text, max_tokens: maxTokens, temperature, top_p: topP, top_k: topK,
|
| 216 |
penalty_repeat: 1.0, stop: ['<|im_end|>'], stream: true });
|
| 217 |
for await (const c of it) {
|
| 218 |
out += c.choices[0].text;
|
| 219 |
+
tFirst ??= performance.now();
|
| 220 |
+
S.phase = `Generating in your browser: ${++n} tokens, ${(n * 1000 / (performance.now() - tFirst + 1)).toFixed(1)} tok/s`;
|
| 221 |
+
S.tail = out.slice(-800);
|
| 222 |
+
window.wgLive(out);
|
| 223 |
}
|
| 224 |
+
const tEnd = performance.now();
|
| 225 |
+
tFirst ??= tEnd;
|
| 226 |
+
const msg = `Done in ${sec(t0, tEnd)}s: model load ${sec(t0, tLoad)}s, prefill ${sec(tLoad, tFirst)}s, `
|
| 227 |
+
+ `${n} tokens in ${sec(tFirst, tEnd)}s (${(n * 1000 / (tEnd - tFirst + 1)).toFixed(1)} tok/s)`;
|
| 228 |
+
console.log('[3D World Generator]', msg);
|
| 229 |
+
const url = URL.createObjectURL(new Blob([window.wgCode(out)], { type: 'text/html' }));
|
| 230 |
+
clearInterval(tick);
|
| 231 |
+
st.innerHTML = `${msg}${cpu}<br><a href="${url}" download="scene.html">Download scene.html</a>`;
|
| 232 |
} catch (e) {
|
| 233 |
+
clearInterval(tick);
|
| 234 |
+
st.innerHTML = `<b>WebGPU error:</b> ${esc(String(e))}. Try the ZeroGPU backend.`;
|
| 235 |
}
|
| 236 |
+
return [out];
|
| 237 |
}
|
| 238 |
+
""".replace("__LIVE__", LIVE_JS).replace("__REPO__", "ReallyFloppyPenguin/Qwen3.5-4B-3D-Coder-GGUF-split").replace(
|
| 239 |
"__SYSTEM__", SYSTEM_PROMPT.replace("\n", "\\n")
|
| 240 |
).replace("__NO_THINK__", NO_THINK.replace("\n", "\\n").replace("`", "\\`"))
|
| 241 |
|
| 242 |
|
| 243 |
def run(prompt, thinking, max_tokens, temperature, top_p, top_k):
|
| 244 |
+
t0 = time.perf_counter()
|
| 245 |
+
elapsed = lambda: f"{time.perf_counter() - t0:.0f}s elapsed" # noqa: E731
|
| 246 |
+
yield "", None, f"<b>⏳ Waiting for ZeroGPU, loading model</b> · {elapsed()}"
|
| 247 |
+
raw, info = "", ""
|
| 248 |
+
for raw, info in generate(prompt, thinking, max_tokens, temperature, top_p, top_k):
|
| 249 |
+
yield raw, None, f"<b>⏳ Generating on ZeroGPU: {info}</b> · {elapsed()}"
|
| 250 |
with open("scene.html", "w", encoding="utf-8") as f:
|
| 251 |
+
f.write(extract_html(raw))
|
| 252 |
+
yield raw, "scene.html", f"Done in {time.perf_counter() - t0:.1f}s total. {info}"
|
| 253 |
|
| 254 |
|
| 255 |
with gr.Blocks(title="3D World Generator (WebGPU)") as demo:
|
| 256 |
gr.Markdown(
|
| 257 |
"# 3D World Generator (WebGPU)\nDescribe a world; "
|
| 258 |
"[ccore/Qwen3.5-4B-3D-Coder](https://huggingface.co/ccore/Qwen3.5-4B-3D-Coder) writes a self-contained "
|
| 259 |
+
"Three.js scene. Drag/scroll in the preview to move the camera (controls are model-written, so they vary). "
|
| 260 |
+
"The preview rebuilds every few seconds while the scene is being written.\n\n"
|
| 261 |
"**WebGPU** runs the model in your browser (2.7 GB download once, then cached; needs Chrome/Edge 113+). "
|
| 262 |
"**ZeroGPU** runs it on the server."
|
| 263 |
)
|
|
|
|
| 276 |
top_k = gr.Slider(1, 100, value=20, step=1, label="Top K")
|
| 277 |
gr.Examples(EXAMPLES, inputs=prompt, example_labels=EXAMPLE_LABELS, label="Examples from the model card")
|
| 278 |
with gr.Column(scale=2):
|
| 279 |
+
# filled client-side by window.wgLive for both backends
|
| 280 |
+
gr.HTML(
|
| 281 |
+
'<div id="wg-live" style="position:relative;height:640px;border-radius:8px;overflow:hidden;'
|
| 282 |
+
'background:#000"></div>'
|
| 283 |
+
)
|
| 284 |
download = gr.File(label="Download scene.html")
|
| 285 |
with gr.Accordion("Raw model output", open=False):
|
| 286 |
code = gr.Code(language="html", label="Output")
|
| 287 |
inputs = [prompt, thinking, max_tokens, temperature, top_p, top_k]
|
| 288 |
+
btn.click(None, js="() => {" + LIVE_JS + "window.wgLive('');}")
|
| 289 |
+
btn.click(run, inputs, [code, download, status])
|
| 290 |
+
code.change(None, code, js="(c) => {" + LIVE_JS + "window.wgLive(c);}")
|
| 291 |
+
web_btn.click(None, inputs, [code], js=WEBGPU_JS)
|
| 292 |
backend.change(lambda b: (gr.update(visible=b == GPU), gr.update(visible=b == WEB)), backend, [btn, web_btn])
|
| 293 |
|
| 294 |
demo.queue().launch()
|