Spaces:
Running
Running
Download scripts/export-space.py from thomwolf/premiers-mots: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/spaces/thomwolf/premiers-mots/resolve/main/scripts/export-space.py
- Command line
-
hf download hf://spaces/thomwolf/premiers-mots/scripts/export-space.py
-
curl -L -o export-space.py https://huggingface.co/spaces/thomwolf/premiers-mots/resolve/main/scripts/export-space.py
11 kB
| """Prepare a reviewable, prebuilt static Space. Never uploads or creates a repo.""" | |
| import argparse | |
| import hashlib | |
| import html | |
| import json | |
| import re | |
| import shutil | |
| from pathlib import Path | |
| from urllib.parse import urlsplit | |
| ROOT = Path(__file__).resolve().parents[1] | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument('--origin', required=True, help='Actual HTTPS app origin returned by HF, or the private preview origin') | |
| parser.add_argument('--site', help='Optional X handle, e.g. @yourhandle; no account is assumed') | |
| parser.add_argument('--output', default='artifacts/hf-space-preview') | |
| args = parser.parse_args() | |
| url = urlsplit(args.origin) | |
| if url.scheme != 'https' or not url.hostname or url.username or url.password or url.query or url.fragment or url.path not in ('', '/'): | |
| parser.error('--origin must be an HTTPS origin with no credentials, path, query or fragment') | |
| if args.site and not re.fullmatch(r'@[A-Za-z0-9_]{1,15}', args.site): | |
| parser.error('--site must be an X handle') | |
| origin = args.origin.rstrip('/') | |
| out = (ROOT / args.output).resolve() | |
| if not out.is_relative_to(ROOT / 'artifacts') or out == ROOT / 'artifacts': | |
| parser.error('output must be a dedicated subdirectory of artifacts/') | |
| if out.exists(): | |
| parser.error('output already exists; choose a fresh directory to preserve the previous export') | |
| required = ['dist/index.html', 'dist/player.html', 'public/card.png', 'package-lock.json', 'LICENSE', 'CORPUS-LICENSE.txt'] | |
| for name in required: | |
| if not (ROOT / name).is_file(): | |
| parser.error(f'missing {name}; build and create the real preview image first') | |
| out.mkdir(parents=True) | |
| shutil.copytree(ROOT / 'dist', out / 'dist') | |
| shutil.copy2(ROOT / 'public/card.png', out / 'dist/card.png') | |
| shutil.copytree(ROOT / 'src', out / 'src') | |
| for name in ['package.json', 'package-lock.json', 'vite.config.js', 'index.html', 'player.html', 'LICENSE', 'CORPUS-LICENSE.txt']: | |
| shutil.copy2(ROOT / name, out / name) | |
| (out / 'scripts').mkdir() | |
| shutil.copy2(ROOT / 'scripts/export-space.py', out / 'scripts/export-space.py') | |
| # Preserve exact source-form public assets needed to rebuild without a second copy | |
| # of the 61 MB runtime. This manifest also identifies all runtime dependencies. | |
| public_paths = [p.relative_to(ROOT / 'public').as_posix() for p in (ROOT / 'public').rglob('*') if p.is_file()] | |
| (out / 'public-files.json').write_text(json.dumps(sorted(public_paths), indent=2)+'\n') | |
| (out / 'restore-public.py').write_text('''from pathlib import Path | |
| import json, shutil | |
| root=Path(__file__).resolve().parent | |
| for name in json.loads((root/'public-files.json').read_text()): | |
| dest=root/'public'/name | |
| dest.parent.mkdir(parents=True,exist_ok=True) | |
| shutil.copy2(root/'dist'/name,dest) | |
| print('Restored public/ from the pinned static export.') | |
| ''') | |
| escape=lambda value:html.escape(value, quote=True) | |
| site=f'<meta name="twitter:site" content="{escape(args.site)}">' if args.site else '' | |
| share=f'''<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Train a tiny transformer from scratch on X</title> | |
| <meta name="twitter:card" content="player">{site} | |
| <meta name="twitter:title" content="Train a tiny transformer from scratch on X"> | |
| <meta name="twitter:description" content="Train a tiny transformer from scratch in your browser. Real loss and live text. Click to start."> | |
| <meta name="twitter:player" content="{escape(origin)}/player.html"> | |
| <meta name="twitter:player:width" content="640"><meta name="twitter:player:height" content="600"> | |
| <meta name="twitter:image" content="{escape(origin)}/card.png"> | |
| <meta name="twitter:image:alt" content="Actual training run: random characters at first, a short story after 350 steps. Results and timing depend on your device."> | |
| <meta property="og:type" content="website"><meta property="og:title" content="Train a tiny transformer from scratch on X"> | |
| <meta property="og:description" content="A tiny model learns to write from scratch on your device."> | |
| <meta property="og:image" content="{escape(origin)}/card.png"><meta property="og:url" content="{escape(origin)}/share.html"> | |
| <style>body{{font:18px/1.6 system-ui;background:#f6f5ef;color:#2c4337;max-width:720px;margin:40px auto;padding:20px}}img{{width:100%;height:auto;border-radius:12px}}a{{color:#32664d}}small{{font-size:13px}}</style></head> | |
| <body><h1>Train a tiny transformer from scratch on X.</h1><p>Train a tiny transformer from scratch on your device.</p> | |
| <a href="./player.html"><img src="./card.png" width="640" height="480" alt="Actual output, before and after training"></a> | |
| <p><a href="./player.html">Start the experiment ↗</a> · <a href="./">Open the full lab ↗</a></p> | |
| <small>This image shows a real training run. The model starts with random weights when you press Train. Timing depends on your device.</small></body></html>''' | |
| (out / 'dist/share.html').write_text(share) | |
| (out / 'dist/robots.txt').write_text('User-agent: *\nAllow: /\n') | |
| (out / 'README.md').write_text('''--- | |
| title: First words | |
| emoji: 🌱 | |
| colorFrom: green | |
| colorTo: yellow | |
| sdk: static | |
| app_file: dist/index.html | |
| fullWidth: true | |
| disable_embedding: false | |
| license: mit | |
| --- | |
| # First words | |
| Train a real character-level causal transformer from random weights in a browser | |
| Worker. This static Space only serves files. All training runs on the visitor’s | |
| device: no HF training server, API key, pretrained checkpoint or paid GPU. | |
| jax-js is pinned to 0.1.26. WebGPU is preferred, with WASM/CPU as fallback. | |
| Context: Thomas requested this prototype, inspired by Pieter Levels’ Windows XP | |
| experience embedded in a tweet. The standalone lab came first, followed by the | |
| compact Player Card. The lab also offers original NanoGPT/PyTorch and a modded | |
| research variant adapted to CPU. These are not the CUDA speed-record code. | |
| ## Try it | |
| - `/player.html`: compact card, explicit start, pause/resume, reset, live loss and | |
| training-stage generations. Open **Dataset & settings** to read the exact training | |
| text, inspect the model and edit hyperparameters. Expand shows the Hugging Face Space address with a copy button. Paste it into a new browser tab; weights are not transferred. X sandboxed popups are incompatible with the Hugging Face COOP headers. | |
| - `/`: full lab, including custom text and Python backends. | |
| - `/share.html?v=4`: English sharing page with Player Card metadata in its initial HTML. | |
| The card offers two datasets: short synthetic stories (CC0) and a repeating ABC | |
| pattern. Two models are available: Tiny (1 layer, width 32) and Small (2 layers, | |
| width 64), both with 4 attention heads. For stories they have 22,240 and 118,336 | |
| parameters respectively. Counts depend on the vocabulary and context. The UI | |
| shows exact parameter counts and FP32 weight size in KiB (1 KiB = 1,024 bytes). | |
| This excludes gradients, optimizer state, temporary buffers and the runtime. | |
| The compact header reads “Train a tiny transformer from scratch on X” and shows | |
| the actual compute backend once loaded. Generation panels are labeled “Start of | |
| training” and “Current stage of training”. The dropdowns for model and dataset | |
| remain visible. | |
| In **Dataset & settings**, learning rate, training steps, context, batch size, | |
| sampling interval, temperature, sample length and learning-rate schedule can be | |
| edited before training. The optimizer constants and seeds are also listed. | |
| Inputs are validated before a Worker starts and are locked during training. | |
| Changing dataset restores its defaults; reset preserves the current settings. | |
| Below the training samples, **Try your model** accepts a new prompt once training | |
| finishes. Generate runs inference with the trained weights, the configured sampling | |
| temperature and length, and a new seed. Stop cancels generation. Its separate output | |
| never replaces either training sample or changes the training loss, elapsed time, | |
| optimizer or weights. Unsupported characters are reported without losing the model. | |
| On shorter cards, the inference output sits beside its prompt to preserve space; | |
| very short legacy cards can scroll inside the results area while the footer stays | |
| visible. The dataset/settings panel scrolls independently. | |
| Stories use 350 steps, context 256, batch 4, AdamW, cooldown and temperature 0.4. | |
| ABC uses 100 steps and context 32. The loss plot labels training steps and | |
| cross-entropy in nats per character; the line and displayed loss use a rolling | |
| 20-step mean. The stories are deliberately repetitive and can be memorized; | |
| a low training loss does not establish general language ability. | |
| Weights exist only in memory. Reset or closing the page clears them. Cards pause | |
| when offscreen. A new sampling seed can still produce the same text. Generated | |
| output is never corrected after sampling. | |
| ## Build locally | |
| This export is prebuilt; no HF build or compute is needed. Tested with Node | |
| 24.18.1 and npm 11.16.0: | |
| ```sh | |
| python3 restore-public.py | |
| npm ci | |
| npm run build | |
| ``` | |
| Regenerate sharing metadata for the actual destination origin: | |
| ```sh | |
| python3 scripts/export-space.py --origin https://YOUR-APP-HOST | |
| ``` | |
| The export contains application sources and runtime assets. The full test harness | |
| and private development notes are kept in the development workspace. | |
| ## Validation and limits | |
| Forward/loss/gradients have been checked against finite differences and the | |
| NanoGPT transcription against PyTorch. Parameter counts are checked against | |
| actual model tensors for both sizes and both datasets. Browser tests cover real | |
| training and generation, pause/resume, reset, dataset preview and responsive | |
| layouts. English UI messages include the full lab and both Python Workers. | |
| The historical benchmark archive remains in French and is labeled accordingly. | |
| The public player has trained inside a local reproduction of X’s observed two | |
| iframe sandbox layers. The official X validator reported `Card loaded successfully`; | |
| Thomas subsequently confirmed rendering in X with a screenshot. These observations | |
| do not establish compatibility with every X client or mobile device. The card | |
| requests a 640 × 600 aspect ratio and also fits older 4:3 cards. X controls its | |
| metadata cache. No tweet has been posted by this agent. | |
| The Python/PyTorch runtime (~61 MB) downloads only when selected in the full lab. | |
| Software WebGPU has been tested; no physical GPU or mobile speed is promised. | |
| Component licenses are retained alongside their files. The original upstream | |
| NanoGPT model.py and the 32 pinned runtime assets are unchanged. | |
| ''') | |
| manifest={'origin':origin,'site':args.site,'published':False,'files':[]} | |
| for path in sorted(out.rglob('*')): | |
| if path.is_file():manifest['files'].append({'path':path.relative_to(out).as_posix(),'bytes':path.stat().st_size,'sha256':hashlib.sha256(path.read_bytes()).hexdigest()}) | |
| (out / 'export-manifest.json').write_text(json.dumps(manifest,indent=2)+'\n') | |
| print(json.dumps({'output':str(out),'files':len(manifest['files']),'bytes':sum(f['bytes'] for f in manifest['files']),'origin':origin,'published':False},indent=2)) | |