concept-collection / voicenote
voicenote / README.md
5.3 KBPreviewCodeBlameHistoryRaw

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#

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.