| app.js | |
| index.html | |
| models.js | |
| README.md | |
| styles.css | |
| worker.js | |
| worklet.js |
voicenote#
Voice dictation that never leaves your browser. Press Record, talk, and watch a document build itself. Edit it as you go, pause and resume, copy it out when you're done.
There is no server and no API key. Speech recognition runs entirely on your machine via transformers.js — the model weights are downloaded once from HuggingFace, cached by the browser, and every subsequent visit works offline. No audio is ever transmitted.
Using it#
- Record / Pause — starts and stops listening. Text is appended when you pause between sentences; greyed italic text below the document is the in-progress guess for what you're currently saying.
- The document is a plain textarea. Type in it, fix mistakes, rearrange — dictation appends to the end and leaves your cursor alone.
- Copy puts the whole document on the clipboard. Clear empties it.
- Everything is saved to
localStorage, so a reload picks up where you left off. There is exactly one document.
Models#
Pick a model from the dropdown; the size shown is the one-time download for the backend you're actually going to run on, and already-cached models are marked downloaded. Your choice is remembered.
| Model | CPU | WebGPU | Notes |
|---|---|---|---|
| Moonshine Tiny | 32 MB | 79 MB | Fastest; lighter punctuation |
| Whisper Tiny | 44 MB | 122 MB | Quick; less accurate on hard words |
| Moonshine Base | 67 MB | 157 MB | Fast and accurate; lighter punctuation |
| Whisper Base | 80 MB | 209 MB | Default — good accuracy and punctuation |
| Whisper Small | 252 MB | 588 MB | Most accurate; slow without WebGPU |
The two families behave quite differently on short utterances. Whisper pads every clip to 30 seconds, so a two-second phrase costs the same as a full one; Moonshine scales with the real audio length. Measured on one CPU core for a 3-second phrase: Moonshine Tiny 0.17 s, Whisper Tiny 1.3 s, Whisper Base 2.7 s. Whisper punctuates noticeably better, which is why it is the default — but if dictation feels sluggish on the CPU backend, Moonshine is the fix.
Backends#
WebGPU is used automatically whenever the browser offers an adapter, and the footer always names the backend actually in use. It is much faster than the CPU path, which matters most for the larger Whisper models.
The catch is download size. The two backends want different quantizations — int8 throughout on CPU, versus an fp32 encoder and a 4-bit decoder on WebGPU, since int8 matmul is poorly served there — and the 4-bit decoder only quantizes matmul weights, leaving the embeddings at full precision. The result is a 2–3× larger download on WebGPU, which is why the picker quotes both.
Detecting an adapter does not guarantee one that works, so a WebGPU failure falls back to CPU. That fallback restarts the worker rather than retrying in place: once ONNX Runtime has failed to bring up WebGPU, its backend registry stays poisoned and a CPU request in the same worker resolves right back to WebGPU and fails identically. Phrases spoken during a load or a restart are held on the main thread and submitted once a model is ready, so switching backends never costs you words.
How it works#
worklet.js captures microphone audio at 16 kHz. app.js runs an energy-based
voice-activity detector over it that adapts to your room's noise floor: it keeps
a rolling pre-roll buffer so the start of a word is never clipped, opens an
utterance when speech begins, and closes it after ~0.9 s of silence. Closed
utterances go to worker.js for transcription and are appended to the document;
while you are still speaking, the same audio is periodically re-transcribed to
produce the live interim line.
Segmentation is tuned to avoid splitting sentences at dramatic mid-sentence pauses — short fragments transcribe poorly because the model loses the surrounding words it needs for context — and to keep trailing silence, since a final fricative is quiet enough to read as silence and trimming into it eats the end of the word.
Only one transcription runs at a time; interim requests are skipped whenever the worker is busy, so a slow model degrades to fewer live updates rather than an ever-growing backlog. Finished phrases are never skipped — if no model is ready yet they queue on the main thread (up to about two minutes of speech) and are submitted as soon as one is.
Why transformers.js 3.8.1#
Pinned deliberately. In 4.2.0 the int8 path fails to create an ONNX session for encoder-decoder ASR models:
qdq_actions.cc:137 TransposeDQWeightsForMatMulNBits
Missing required scale: model.decoder.embed_tokens.weight_merged_0_scale
This affects Whisper and Moonshine alike, so the whole model list is unusable on 4.x. 3.8.1 runs all five correctly.
Running locally#
Any static file server will do — there is no build step:
python3 -m http.server 8000
Then open http://localhost:8000. A secure context is required for microphone
access, which localhost and HTTPS both satisfy.
Browser support#
Needs AudioWorklet, module workers, and WebAssembly — Chrome, Edge, Firefox,
and Safari 15+ all qualify. WebGPU is used where available and is not required.
The CPU backend runs single-threaded: multi-threaded WASM needs cross-origin
isolation headers, which GitHub Pages cannot set.