gpt2 / index.html
stefflastup's picture
Upload folder using huggingface_hub
3f5a977 verified
Raw History Blame Contribute Delete
17.2 kB
<!doctype html>
<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>