![]()
A Chrome extension that swaps everyday words for harder equivalents as you read. Hover a changed word to see what it replaced; click to put it back.
The substitution is judged on-device by a fine-tuned cross-encoder. Nothing leaves the browser, and there is no API key or server.
proposer ──▶ gate ──▶ judge ──▶ swap
lookup table sense & BERT-base in-place, revertible
1,271 triggers difficulty on WebGPU
filters or WASM
Passing the candidate as input rather than as a class means new vocabulary can be added to the table without retraining the model.
Measured on 906 held-out spans whose vocabulary never appeared in training:
| Coverage | 41.4% of proposals accepted |
| Precision | 90.4% |
| Visible errors | 4.0 per 100 spans |
| Latency | 14.3 ms per candidate, single CPU thread |
| Page cost | ~660 ms for a 20-span viewport (much less on WebGPU) |
Production spans need 2.30 model calls each: 80% of triggers map to exactly one hard word, plus one call for the leave-it-alone option.
Delivery on real pages, per 1,000 words of prose:
| source | swaps | median difficulty gain |
|---|---|---|
| Reddit (long-form) | 8.6 | 10.6× rarer |
| Stack Exchange | 4.6 | 16.0× rarer |
Delivery scales with passage length, not with register — 30% of short forum replies contain no eligible span at all, while every 100+ word passage does.
python scripts/vendor_ort.py # stage onnxruntime-web
python scripts/export_runtime_assets.py # model, vocab, tables
python scripts/build_extension.py # bundle the content script
Then load extension/ unpacked at chrome://extensions with Developer mode
on. Requires Chrome 116+.
scripts/tests/run_all.sh # everything
| suite | what it covers |
|---|---|
test_clean.py, test_senses.py |
text normalization and sense constraints |
test_tokenizer_parity.py |
JS tokenizer vs HuggingFace, exact token ids |
test_proposer_parity.py |
JS proposer vs the Python pipeline, same spans |
test_inference_parity.py |
the shipped ONNX file vs PyTorch |
test_serialize.mjs |
inference queue never overlaps |
test_content_smoke.mjs |
content script against a real DOM |
test_scenarios.mjs |
rapid scroll, toggling, SPA navigation, shadow DOM |
The three parity suites exist because the runtime is a JavaScript reimplementation of a Python pipeline, and a divergence there is silent: the extension keeps working and simply feeds the model inputs it was not trained on. All three assert exact equality, not similarity.
extension/ the extension: manifest, content script, offscreen host
lib/ tokenizer, segmenter, proposer, judge, generated tables
scripts/ corpus pipeline, training, evaluation, build
pipeline/ cleaning, segmentation, dedup, sense constraints
tests/ unit and parity suites
data/inventory/ vocabulary and the sense-keyed substitution table
docs/ engineering notes
Model checkpoints, the ONNX export, the corpus and its derived tables are not
in version control; they are produced by the pipeline. See docs/model.md.
Nothing leaves the browser: no server, no account, no network request at
runtime. Full policy: privacy.html
(source in PRIVACY.md, rendered by scripts/build_privacy_page.py).
The target vocabulary is GregMat’s 900-word GRE list, used as the priority set the substitution table is built around. The list and its definitions are Greg’s work; this project supplies only the machinery for putting those words in front of a reader. If you are studying for the GRE, his course is the reason this extension has anything worth teaching.
Corpora: Project Gutenberg, Wikipedia, C4 (realnewslike), and the Stack
Exchange data dump.