Spaces:
Running
Running
Download index.html from stefflastup/gpt2: direct link, hf CLI and curl.
- Browser
- Download file 17.2 kB
-
https://huggingface.co/spaces/stefflastup/gpt2/resolve/main/index.html
- Command line
-
hf download hf://spaces/stefflastup/gpt2/index.html
-
curl -L -o index.html https://huggingface.co/spaces/stefflastup/gpt2/resolve/main/index.html
17.2 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>GPT-2 — in your browser</title> | |
| <style> | |
| :root { | |
| --bg: #fbfaf9; | |
| --panel: #ffffff; | |
| --border: #e4e0db; | |
| --text: #1c1917; | |
| --muted: #78716c; | |
| --accent: #4338ca; | |
| --accent-soft: #eef2ff; | |
| --bar: #6366f1; | |
| --radius: 10px; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --bg: #17161a; | |
| --panel: #201f24; | |
| --border: #363139; | |
| --text: #f2efec; | |
| --muted: #a09a94; | |
| --accent: #a5b4fc; | |
| --accent-soft: #272335; | |
| --bar: #818cf8; | |
| } | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| padding: 2rem 1.25rem 4rem; | |
| background: var(--bg); | |
| color: var(--text); | |
| font: 15px/1.6 ui-sans-serif, -apple-system, "Segoe UI", system-ui, sans-serif; | |
| } | |
| main { max-width: 760px; margin: 0 auto; } | |
| h1 { font-size: 1.9rem; letter-spacing: -0.02em; margin: 0 0 .4rem; } | |
| .sub { color: var(--muted); margin: 0 0 .75rem; } | |
| .links { font-size: .85rem; color: var(--muted); margin-bottom: 1.5rem; } | |
| .links a { color: var(--accent); text-decoration: none; } | |
| .links a:hover { text-decoration: underline; } | |
| #status { | |
| display: flex; align-items: center; gap: .6rem; | |
| font-size: .82rem; color: var(--muted); | |
| background: var(--panel); border: 1px solid var(--border); | |
| border-radius: var(--radius); padding: .6rem .8rem; margin-bottom: 1.25rem; | |
| } | |
| #status .dot { | |
| width: 7px; height: 7px; border-radius: 50%; | |
| background: var(--muted); flex: none; | |
| } | |
| #status.ready .dot { background: #16a34a; } | |
| #status.busy .dot { background: #f59e0b; animation: pulse 1.1s infinite; } | |
| #status.error .dot { background: #dc2626; } | |
| @keyframes pulse { 50% { opacity: .25; } } | |
| #bar-wrap { flex: 1; height: 4px; background: var(--border); border-radius: 99px; overflow: hidden; display: none; } | |
| #bar-wrap.on { display: block; } | |
| #bar { height: 100%; width: 0; background: var(--bar); transition: width .2s; } | |
| .tabs { display: flex; gap: .25rem; border-bottom: 1px solid var(--border); margin-bottom: 1.25rem; } | |
| .tabs button { | |
| appearance: none; background: none; border: 0; border-bottom: 2px solid transparent; | |
| padding: .55rem .85rem; font: inherit; font-weight: 500; | |
| color: var(--muted); cursor: pointer; margin-bottom: -1px; | |
| } | |
| .tabs button[aria-selected="true"] { color: var(--text); border-bottom-color: var(--accent); } | |
| .panel[hidden] { display: none; } | |
| .row { display: flex; gap: .5rem; align-items: stretch; } | |
| textarea, input[type="text"] { | |
| width: 100%; font: inherit; color: var(--text); | |
| background: var(--panel); border: 1px solid var(--border); | |
| border-radius: var(--radius); padding: .65rem .8rem; resize: vertical; | |
| } | |
| textarea:focus, input:focus { outline: 2px solid var(--accent); outline-offset: -1px; } | |
| button.primary { | |
| font: inherit; font-weight: 600; cursor: pointer; white-space: nowrap; | |
| background: var(--accent); color: var(--bg); | |
| border: 0; border-radius: var(--radius); padding: .65rem 1.1rem; | |
| } | |
| button.primary:disabled { opacity: .5; cursor: default; } | |
| button.ghost { | |
| font: inherit; cursor: pointer; background: var(--panel); | |
| color: var(--text); border: 1px solid var(--border); | |
| border-radius: var(--radius); padding: .65rem 1rem; | |
| } | |
| .chips { display: flex; flex-wrap: wrap; gap: .4rem; margin: .8rem 0 0; } | |
| .chips button { | |
| font: inherit; font-size: .82rem; cursor: pointer; | |
| background: var(--panel); color: var(--muted); | |
| border: 1px solid var(--border); border-radius: 99px; padding: .3rem .7rem; | |
| max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; | |
| } | |
| .chips button:hover { color: var(--text); border-color: var(--accent); } | |
| #out { | |
| margin-top: 1rem; min-height: 9rem; white-space: pre-wrap; word-break: break-word; | |
| background: var(--panel); border: 1px solid var(--border); | |
| border-radius: var(--radius); padding: .9rem 1rem; | |
| font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: .88rem; line-height: 1.7; | |
| } | |
| #out .given { color: var(--muted); } | |
| #out .empty { color: var(--muted); font-style: italic; font-family: inherit; } | |
| .cursor { display: inline-block; width: .5em; background: var(--accent); animation: pulse .9s infinite; } | |
| details { margin-top: 1rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel); } | |
| summary { cursor: pointer; padding: .6rem .85rem; font-weight: 500; font-size: .9rem; } | |
| .knobs { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: .9rem; padding: .3rem .85rem 1rem; } | |
| .knob label { display: block; font-size: .8rem; color: var(--muted); margin-bottom: .3rem; } | |
| .knob input[type="range"] { width: 100%; accent-color: var(--accent); } | |
| .knob .val { font-variant-numeric: tabular-nums; color: var(--text); font-weight: 600; } | |
| #probs { margin-top: 1rem; display: flex; flex-direction: column; gap: .35rem; } | |
| .prob { display: grid; grid-template-columns: 8.5rem 1fr 4rem; gap: .6rem; align-items: center; font-size: .86rem; } | |
| .prob code { | |
| font-family: ui-monospace, Menlo, monospace; background: var(--accent-soft); | |
| color: var(--accent); padding: .15rem .4rem; border-radius: 5px; | |
| overflow: hidden; text-overflow: ellipsis; white-space: nowrap; | |
| } | |
| .prob .track { height: 9px; background: var(--border); border-radius: 99px; overflow: hidden; } | |
| .prob .fill { height: 100%; background: var(--bar); border-radius: 99px; } | |
| .prob .pct { text-align: right; font-variant-numeric: tabular-nums; color: var(--muted); } | |
| .note { font-size: .82rem; color: var(--muted); margin: .25rem 0 0; } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <h1>GPT-2</h1> | |
| <p class="sub">The original 124M-parameter GPT-2, running entirely in your browser. It only ever does one thing: predict the next token.</p> | |
| <p class="links"> | |
| <a href="https://huggingface.co/openai-community/gpt2" target="_blank" rel="noopener">Model card</a> · | |
| <a href="https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf" target="_blank" rel="noopener">Paper</a> · | |
| <a href="https://github.com/huggingface/transformers.js" target="_blank" rel="noopener">transformers.js</a> | |
| </p> | |
| <div id="status"><span class="dot"></span><span id="status-text">Model loads on your first run.</span><span id="bar-wrap"><span id="bar"></span></span></div> | |
| <div class="tabs" role="tablist"> | |
| <button role="tab" id="tab-complete" aria-selected="true" aria-controls="panel-complete">Completion</button> | |
| <button role="tab" id="tab-next" aria-selected="false" aria-controls="panel-next">Next token</button> | |
| </div> | |
| <section class="panel" id="panel-complete" role="tabpanel"> | |
| <div class="row"> | |
| <textarea id="prompt" rows="3" placeholder="Hello, I'm a language model,"></textarea> | |
| </div> | |
| <div class="row" style="margin-top:.5rem"> | |
| <button class="primary" id="go">Generate</button> | |
| <button class="ghost" id="stop" hidden>Stop</button> | |
| </div> | |
| <div class="chips" id="examples"></div> | |
| <div id="out"><span class="empty">Output appears here.</span></div> | |
| <details> | |
| <summary>Sampling settings</summary> | |
| <div class="knobs"> | |
| <div class="knob"><label>Max new tokens <span class="val" id="v-len">120</span></label><input type="range" id="k-len" min="8" max="400" step="8" value="120"></div> | |
| <div class="knob"><label>Temperature <span class="val" id="v-temp">0.80</span></label><input type="range" id="k-temp" min="0.1" max="2" step="0.05" value="0.8"></div> | |
| <div class="knob"><label>Top-p <span class="val" id="v-topp">0.95</span></label><input type="range" id="k-topp" min="0.1" max="1" step="0.01" value="0.95"></div> | |
| <div class="knob"><label>Top-k <span class="val" id="v-topk">50</span></label><input type="range" id="k-topk" min="0" max="200" step="1" value="50"></div> | |
| <div class="knob"><label>Repetition penalty <span class="val" id="v-rep">1.10</span></label><input type="range" id="k-rep" min="1" max="2" step="0.05" value="1.1"></div> | |
| </div> | |
| </details> | |
| </section> | |
| <section class="panel" id="panel-next" role="tabpanel" hidden> | |
| <p class="note" style="margin-bottom:.8rem">GPT-2's raw output — the probability it assigns to every possible next token. <code>␣</code> marks a leading space.</p> | |
| <div class="row"> | |
| <input type="text" id="nt-prompt" placeholder="The capital of France is"> | |
| <button class="primary" id="nt-go">Predict</button> | |
| </div> | |
| <div class="chips" id="nt-examples"></div> | |
| <div class="knobs" style="padding-left:0;padding-right:0"> | |
| <div class="knob"><label>Show top <span class="val" id="v-k">10</span> tokens</label><input type="range" id="k-k" min="3" max="25" step="1" value="10"></div> | |
| </div> | |
| <div id="probs"></div> | |
| </section> | |
| </main> | |
| <script type="module"> | |
| import { | |
| AutoTokenizer, AutoModelForCausalLM, TextStreamer, env, | |
| } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1'; | |
| env.allowLocalModels = false; | |
| // ONNX export of openai-community/gpt2 — identical weights, browser-runnable. | |
| const MODEL_ID = 'Xenova/gpt2'; | |
| const CONTEXT_LEN = 1024; // GPT-2's hard positional limit | |
| const $ = (id) => document.getElementById(id); | |
| const statusEl = $('status'), statusText = $('status-text'); | |
| const barWrap = $('bar-wrap'), bar = $('bar'); | |
| function setStatus(text, state = '') { | |
| statusText.textContent = text; | |
| statusEl.className = state; | |
| } | |
| // ---------------------------------------------------------------- model load | |
| let loadPromise = null; | |
| function load() { | |
| if (loadPromise) return loadPromise; | |
| const webgpu = !!navigator.gpu; | |
| const device = webgpu ? 'webgpu' : 'wasm'; | |
| const dtype = webgpu ? 'fp16' : 'int8'; | |
| const size = webgpu ? '251 MB' : '281 MB'; | |
| setStatus(`Downloading GPT-2 (${size}, ${device.toUpperCase()}) — cached after this.`, 'busy'); | |
| barWrap.classList.add('on'); | |
| const seen = new Map(); | |
| const progress = (p) => { | |
| if (p.status === 'progress' && p.total) seen.set(p.file, p.progress); | |
| if (!seen.size) return; | |
| const avg = [...seen.values()].reduce((a, b) => a + b, 0) / seen.size; | |
| bar.style.width = `${avg.toFixed(1)}%`; | |
| }; | |
| loadPromise = (async () => { | |
| const [tokenizer, model] = await Promise.all([ | |
| AutoTokenizer.from_pretrained(MODEL_ID, { progress_callback: progress }), | |
| AutoModelForCausalLM.from_pretrained(MODEL_ID, { device, dtype, progress_callback: progress }), | |
| ]); | |
| barWrap.classList.remove('on'); | |
| setStatus(`GPT-2 ready — running locally on ${device.toUpperCase()}. Nothing leaves your machine.`, 'ready'); | |
| return { tokenizer, model }; | |
| })().catch((err) => { | |
| loadPromise = null; | |
| barWrap.classList.remove('on'); | |
| setStatus(`Could not load the model: ${err.message}`, 'error'); | |
| throw err; | |
| }); | |
| return loadPromise; | |
| } | |
| // ---------------------------------------------------------------- completion | |
| const promptEl = $('prompt'), outEl = $('out'), goBtn = $('go'), stopBtn = $('stop'); | |
| let interrupted = false; | |
| const knob = (id, fmt) => { | |
| const input = $(`k-${id}`), out = $(`v-${id}`); | |
| const sync = () => { out.textContent = fmt(input.value); }; | |
| input.addEventListener('input', sync); | |
| sync(); | |
| return () => Number(input.value); | |
| }; | |
| const getLen = knob('len', (v) => v); | |
| const getTemp = knob('temp', (v) => Number(v).toFixed(2)); | |
| const getTopP = knob('topp', (v) => Number(v).toFixed(2)); | |
| const getTopK = knob('topk', (v) => v); | |
| const getRep = knob('rep', (v) => Number(v).toFixed(2)); | |
| const getK = knob('k', (v) => v); | |
| function render(given, generated, streaming) { | |
| outEl.innerHTML = ''; | |
| const a = document.createElement('span'); | |
| a.className = 'given'; | |
| a.textContent = given; | |
| outEl.append(a, document.createTextNode(generated)); | |
| if (streaming) { | |
| const c = document.createElement('span'); | |
| c.className = 'cursor'; | |
| c.textContent = ' '; | |
| outEl.append(c); | |
| } | |
| outEl.scrollTop = outEl.scrollHeight; | |
| } | |
| async function complete() { | |
| const prompt = promptEl.value.trim() || promptEl.placeholder; | |
| const maxNew = getLen(); | |
| goBtn.disabled = true; | |
| stopBtn.hidden = false; | |
| interrupted = false; | |
| render(prompt, '', true); | |
| try { | |
| const { tokenizer, model } = await load(); | |
| setStatus('Generating…', 'busy'); | |
| const inputs = tokenizer(prompt, { | |
| truncation: true, | |
| max_length: Math.max(64, CONTEXT_LEN - maxNew), | |
| }); | |
| let generated = ''; | |
| const streamer = new TextStreamer(tokenizer, { | |
| skip_prompt: true, | |
| skip_special_tokens: true, | |
| callback_function: (chunk) => { | |
| if (interrupted) throw new Error('__stop__'); | |
| generated += chunk; | |
| render(prompt, generated, true); | |
| }, | |
| }); | |
| const temperature = getTemp(); | |
| await model.generate({ | |
| ...inputs, | |
| streamer, | |
| max_new_tokens: maxNew, | |
| do_sample: temperature > 0, | |
| temperature, | |
| top_p: getTopP(), | |
| top_k: getTopK(), | |
| repetition_penalty: getRep(), | |
| }); | |
| render(prompt, generated, false); | |
| setStatus('Done.', 'ready'); | |
| } catch (err) { | |
| if (err.message === '__stop__') setStatus('Stopped.', 'ready'); | |
| else setStatus(`Generation failed: ${err.message}`, 'error'); | |
| } finally { | |
| goBtn.disabled = false; | |
| stopBtn.hidden = true; | |
| } | |
| } | |
| goBtn.addEventListener('click', complete); | |
| stopBtn.addEventListener('click', () => { interrupted = true; }); | |
| promptEl.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) complete(); | |
| }); | |
| // ---------------------------------------------------------------- next token | |
| const ntEl = $('nt-prompt'), ntBtn = $('nt-go'), probsEl = $('probs'); | |
| async function predict() { | |
| const prompt = ntEl.value.trim() || ntEl.placeholder; | |
| const k = getK(); | |
| ntBtn.disabled = true; | |
| probsEl.innerHTML = ''; | |
| try { | |
| const { tokenizer, model } = await load(); | |
| setStatus('Scoring next token…', 'busy'); | |
| const inputs = tokenizer(prompt, { truncation: true, max_length: CONTEXT_LEN - 1 }); | |
| const out = await model.generate({ | |
| ...inputs, | |
| max_new_tokens: 1, | |
| do_sample: false, | |
| output_scores: true, | |
| return_dict_in_generate: true, | |
| }); | |
| const logits = Array.from(out.scores[0].data); | |
| const max = Math.max(...logits); | |
| const exps = logits.map((x) => Math.exp(x - max)); | |
| const sum = exps.reduce((a, b) => a + b, 0); | |
| const top = logits | |
| .map((_, i) => i) | |
| .sort((a, b) => logits[b] - logits[a]) | |
| .slice(0, k) | |
| .map((id) => ({ id, p: exps[id] / sum })); | |
| const peak = top[0].p; | |
| for (const { id, p } of top) { | |
| const raw = tokenizer.decode([id]); | |
| const row = document.createElement('div'); | |
| row.className = 'prob'; | |
| const code = document.createElement('code'); | |
| code.textContent = raw.replace(/ /g, '␣').replace(/\n/g, '\\n') || '⟨empty⟩'; | |
| code.title = JSON.stringify(raw); | |
| const track = document.createElement('div'); | |
| track.className = 'track'; | |
| const fill = document.createElement('div'); | |
| fill.className = 'fill'; | |
| fill.style.width = `${(p / peak) * 100}%`; | |
| track.append(fill); | |
| const pct = document.createElement('div'); | |
| pct.className = 'pct'; | |
| pct.textContent = p >= 0.001 ? `${(p * 100).toFixed(1)}%` : '<0.1%'; | |
| row.append(code, track, pct); | |
| probsEl.append(row); | |
| } | |
| setStatus('Done.', 'ready'); | |
| } catch (err) { | |
| setStatus(`Prediction failed: ${err.message}`, 'error'); | |
| } finally { | |
| ntBtn.disabled = false; | |
| } | |
| } | |
| ntBtn.addEventListener('click', predict); | |
| ntEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') predict(); }); | |
| // ---------------------------------------------------------------- examples | |
| const EXAMPLES = [ | |
| "Hello, I'm a language model,", | |
| 'In a shocking finding, scientists discovered a herd of unicorns living in a remote valley', | |
| 'The Manhattan Bridge was completed in', | |
| 'Recipe for a perfect Sunday roast:', | |
| ]; | |
| const NT_EXAMPLES = [ | |
| 'The capital of France is', | |
| 'Once upon a', | |
| '2 + 2 =', | |
| 'My favourite programming language is', | |
| ]; | |
| for (const text of EXAMPLES) { | |
| const b = document.createElement('button'); | |
| b.textContent = text; | |
| b.addEventListener('click', () => { promptEl.value = text; complete(); }); | |
| $('examples').append(b); | |
| } | |
| for (const text of NT_EXAMPLES) { | |
| const b = document.createElement('button'); | |
| b.textContent = text; | |
| b.addEventListener('click', () => { ntEl.value = text; predict(); }); | |
| $('nt-examples').append(b); | |
| } | |
| // ---------------------------------------------------------------- tabs | |
| for (const id of ['complete', 'next']) { | |
| $(`tab-${id}`).addEventListener('click', () => { | |
| for (const other of ['complete', 'next']) { | |
| const selected = other === id; | |
| $(`tab-${other}`).setAttribute('aria-selected', String(selected)); | |
| $(`panel-${other}`).hidden = !selected; | |
| } | |
| }); | |
| } | |
| if (!navigator.gpu) { | |
| setStatus('No WebGPU in this browser — GPT-2 will run on WASM (slower). Loads on first run.'); | |
| } | |
| </script> | |
| </body> | |
| </html> | |