"""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'' if args.site else '' share=f''' Train a tiny transformer from scratch on X {site}

Train a tiny transformer from scratch on X.

Train a tiny transformer from scratch on your device.

Actual output, before and after training

Start the experiment ↗ · Open the full lab ↗

This image shows a real training run. The model starts with random weights when you press Train. Timing depends on your device.''' (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))