Hold the globe key. Talk. The words appear where your cursor already was.
Local speech-to-text for macOS — dictation, meeting recording, and a library you can ask questions of. No account, no API key, nothing uploaded: the speech models are downloaded once and run on your own Mac.
You are already typing somewhere — Slack, Mail, a terminal, a code comment. Hold the globe key, say the sentence, let go. The text lands at the cursor in the app you were already in. Nothing switches, nothing is pasted by hand.
Not everyone has a globe key free, so it is only the default: SETTINGS will record whatever chord you would rather hold — ⌃⌥, ⇧⌘, whatever your hands already know. Modifiers only, and for a reason worth stating plainly — the keyboard is watched, never taken over, so anything in the chord also reaches the app underneath. ⌃⌥ types nothing anywhere; ⌃⌥D would arrive in your editor mid-sentence.
Then there is the other half: drop in audio or video and get a readable transcript with real timestamps, word-level follow-along during playback, inline editing that keeps the timings, full-text search across everything you have ever dictated, and export to typeset PDF, Markdown or plain text.
Calls, both sides. Start a recording and your microphone and whatever the Mac is playing are captured as two separate tracks, so the transcript knows who said what without guessing at voices. No bot joins the meeting — Zoom, Meet, Teams and a phone on speaker all work the same way, because it is the audio that is captured, not the app.
Names, overviews and answers. On macOS 26 with Apple Intelligence switched on, every recording gets a short real title, an overview with its key points and action items, and a note of the people and projects it was about. Then ASK YOUR NOTES puts a question to the whole library — what did we decide about pricing, what are my action items — and answers from the recordings that bear on it, citing the ones it used so you can open them and check. You can follow up in plain language: "write that as a paragraph", "make it shorter", "as bullets".
That half runs on Apple's on-device model, so it holds the same promise as the rest: nothing is uploaded, and there is still nothing to sign in to. Without Apple Intelligence — an older macOS, an ineligible Mac, or simply switched off — everything else works exactly as it always did, and ASK still finds and shows you the recordings that match.
Numbers invite scepticism, so this repo ships the benchmark rather than the claim:
scripts/bench.sh # 12s of synthesised speech
scripts/bench.sh path/to/your.wav # or your own audio
VOICEDUMPS_MODEL_SIZE=small scripts/bench.shApple M1 Pro, 16 GB, macOS 27, 8 threads. small model. Time from the moment
you stop speaking to text on the clipboard.
| You spoke for | Median | Worst of 3 |
|---|---|---|
| 1.9 s | 377 ms | 415 ms |
| 5.8 s | 423 ms | 429 ms |
| 12.0 s | 898 ms | 974 ms |
Note that the first two rows are the same number. whisper processes a fixed 30-second window, so everything shorter than that costs what the window costs — a two-second "ship it Thursday" and a six-second sentence land in the same ~400 ms.
Full engine figures, both models, 12 s of speech
small |
medium |
|
|---|---|---|
| Model load | 0.20 s | 0.43 s |
| Transcribe | 0.90 s | 2.29 s |
| Speed vs realtime | 13× | 5× |
| Resident memory | 260 MB | 601 MB |
Three honest footnotes, because the table is flattering enough without them:
- This is one laptop, not a spec sheet. Thermal state, threads and what else
is running all move it. Run
scripts/bench.shand see what your machine does. - The timings exclude model load on purpose, and that is legitimate. Dictation
calls
engine::warmthe instant the key goes down (dictation.rs), so the load overlaps with you still speaking. Speak for longer than half a second and you never pay it. - A genuinely first-ever load is much worse. Reading the 539 MB medium file when it is nowhere in the OS page cache measured 9.7 s here. Every subsequent load was ~0.4 s. First launch after install is the slow one.
Audio decoding is 2 ms of the budget — symphonia, in-process, no ffmpeg in
the transcription path at all.
Cloud dictation is the norm, and Wispr Flow is the best-known example — $15/month or $144/year, free tier capped at 2,000 words per week.
They publish their latency, which makes an honest comparison possible. Their inference provider's case study, quoting Wispr Flow's CTO, states the pipeline "runs end-to-end in under 700 milliseconds", and is explicit that this is a p99 target rather than a median (Baseten).
Against that published figure, for a normal dictation utterance:
| Latency after you stop speaking | |
|---|---|
| Wispr Flow (published, p99, cloud) | under 700 ms |
| VoiceDumps (measured, worst of 3, local) | 429 ms |
That is roughly 39% faster — and the comparison is deliberately stacked
against us: their p99 versus our worst case, on the small model.
Where that number stops being true. Our latency scales with how long you spoke; theirs is a mostly fixed pipeline cost because it transcribes while you talk. Past roughly ten seconds of speech we lose that race — 12 seconds took us ~900 ms. The win is on short utterances, which is what dictation mostly is.
And what their 700 ms buys that ours does not. That budget includes a fine-tuned Llama pass that reformats and contextualises the transcript, ~250 ms of it by their own account. We do not do that. We hand you what you said.
The rest is architecture, and it does not move:
| VoiceDumps | Cloud dictation | |
|---|---|---|
| Price | Free | Typically $12–15/month |
| Word limits | None | Free tiers are usually capped |
| Works on a plane | Yes | No |
| Where your voice goes | Stays on the disk it recorded to | Uploaded for inference |
| Account required | No | Yes |
| Network round trip | None | Once per utterance |
| Can have an outage | No | Yes |
A cloud service cannot start before your audio arrives. From this machine, just the TLS handshake — before a byte of audio moves or a single token is generated — measured 19 ms to an edge-cached endpoint and 544–564 ms to two regional speech APIs. Local pays zero of that, every time, and does not get slower on hotel wifi.
Where cloud genuinely wins: a datacentre GPU running a larger model beats small
on hard audio — thick accents, crosstalk, bad microphones — and it will format
your text for you. If that is your workload, run medium and take 5× realtime
instead of 13×.
|
Dictation, over any app
The pill is its own accessory process
( |
Transcripts you can actually read
Whisper emits 5–10 second fragments, which are miserable to read.
|
|
It stays on your machine
The app downloads its speech models once, on first run, and never fetches anything again — no account, no API key, and no service to call, so there is no key to leak. Your audio and your notes are never sent anywhere: transcription runs in-process on your own machine, before and after that one download. |
What the history knows
Words, pace, which apps you dictate into, and an activity grid — computed
locally in |
|
Both sides of a call, kept apart Your microphone and the Mac's own output are recorded as two streams and
transcribed separately, so attribution is a fact about which track a sentence
arrived on rather than a guess about a voice. The far side is taken with a
CoreAudio process tap in a helper
( |
A question, not a search box ASK reads the recordings that bear on what you asked and answers from them, with citations you can open. Retrieval runs two ways at once — full-text search for what notes say, a knowledge graph for what they are about — because each fails where the other works. The answer is generated under a schema rather than a prompt asking nicely for one, which is what stops a 3-billion-parameter model replying with a tool call instead of a sentence. |
Images are stills from the launch film in docs/media, which rebuilds the
product's own UI at 1080p rather than screenshotting it.
Apple Silicon, macOS 11 or later, about 5 MB.
curl -fsSL https://raw.githubusercontent.com/heynaavi/voiceDump/main/scripts/install.sh | bashThat fetches the latest DMG, checks its signature, copies the app to
/Applications, and clears the download quarantine flag so macOS does not
refuse to open it — see below for why that flag is the problem. No sudo,
nothing written outside /Applications, and
the script is forty lines you can read first, which you
should: piping anything into bash deserves that much.
Or grab the DMG from Releases and drag it to Applications the usual way — then read the next section, because macOS will stop you.
On first launch it downloads the speech models it needs — 729 MB on a machine with 16 GB of memory or more, 190 MB below that, where the larger model would only swap. They are saved beside your notes rather than inside the app, so they survive every update and you are asked once. After that the app never fetches anything unless you click the version number to check for a release.
If you installed with the command above, it will not — that is the whole reason the command exists. If you dragged the DMG across yourself, it will, and there is no way around it that does not cost $99 a year.
"VoiceDumps" Not Opened — Apple could not verify "VoiceDumps" is free of malware that may harm your Mac or compromise your privacy.
That dialog does not mean anything was found. It means the app is not notarized: not sent to Apple for scanning and signed with a paid Developer ID certificate. This is a one-person open-source project, so it is signed with an ad-hoc signature instead, and macOS flags anything downloaded from a browser that it cannot trace to a paid account. Every unsigned Mac app you have ever installed showed you this.
The build is reproducible — clone the repo, run npm run build:lite, and
compare. Do not take the dialog's advice and click Move to Bin; either:
xattr -dr com.apple.quarantine /Applications/VoiceDumps.appor open it once through System Settings → Privacy & Security, scroll to the message about VoiceDumps being blocked, and click Open Anyway. Either way it is asked once, not every launch. Recent macOS versions have narrowed the old Control-click → Open shortcut, so if that does not offer you an Open button, use one of the two above.
Two permissions on first run, both unavoidable for what it does:
| Permission | Why |
|---|---|
| Accessibility | The globe key is a modifier, not a keycode. It emits no key-down event, so no shortcut API can see it — reading it needs a CGEventTap. |
| Microphone | To hear you. |
Also set System Settings → Keyboard → "Press 🌐 to:" → Do Nothing, or macOS opens the emoji picker underneath the app.
Needs Node, Rust and CMake (whisper.cpp builds with it). No Python and no
ffmpeg for the transcription path.
npm install
npm run models # ~730 MB of ggml weights into ./models
npm run tauri dev # or: npm run build:lite → .app + .dmgThe models, and which one you get
Two quantised multilingual ggml models, fetched on first run into
~/Library/Application Support/dev.heynaavi.voicedump/models and kept there
across updates:
| File | On disk | Resident | |
|---|---|---|---|
small |
ggml-small-q5_1.bin |
190 MB | 260 MB |
medium |
ggml-medium-q5_0.bin |
539 MB | 601 MB |
Chosen at launch by memory, not chip generation: 16 GB or more gets medium,
below that small. A base M1 with 8 GB is already juggling a browser and an
editor, and making the whole machine swap to win a little accuracy is a bad
trade. Force either with VOICEDUMPS_MODEL_SIZE=small|medium.
Multilingual rather than the .en variants because the app already handles
non-English notes; q5 because the accuracy difference is inaudible next to
halving the download.
Architecture
| Layer | Path | Role |
|---|---|---|
| UI | src/ |
React + Tailwind. Reader, history, insights, drag-drop. |
| Shell | src-tauri/ |
Tauri v2. Owns the SQLite history, the globe-key tap and the engine. |
| Engine | src-tauri/src/engine.rs |
whisper.cpp via whisper-rs, Metal-accelerated. In-process. |
| Overlay | overlay-helper/ |
Swift NSPanel for the dictation pill and PDF typesetting. |
Transcription used to run in a Python sidecar on MLX. That worked but could
never ship: it needed a 1 GB virtualenv, the mlx stack, and ffmpeg on the
user's PATH. "Download the app and run it" is not possible on those terms, so
the engine moved in-process and the weights became a one-time download the app
manages itself.
The model stays warm. Loading costs real time, so one WhisperContext is
held between jobs and back-to-back dictations pay nothing for it. Two callers
racing — a warm-up and the transcription it was warming for — would each build a
context and spike memory, so the lock is deliberately held across the load.
The context is released on quit, and that teardown is load-bearing rather than
tidy: -[NSApplication terminate:] calls exit(), which drops nothing Rust
owns, so a live context was still holding Metal residency sets when ggml-metal
asserted they were all released — abort(), and macOS reporting a crash on an
ordinary quit. There is a regression test that runs the exit in a child process,
because the behaviour under test is what happens during exit.
The live preview is off by default (SETTINGS, at the foot of the sidebar, under DICTATION). Turned on, the overlay drafts your words while you are still speaking. Whisper has no incremental decode, so it transcribes forward — a cursor marks what has been read, each pass takes only what is new, and chunks are cut at the quietest moment in their tail so boundaries land between words rather than inside them. Pass cost is bounded by the chunk rather than by how long you have been talking, which is what keeps the lag flat.
It ships off because of what it costs to read. The preview runs on small while
the real transcription stays on medium, and it is thrown away the moment the
authoritative pass lands — so what you watch appear is a faster, worse guess at a
sentence you already know you said, and it will quietly change. Being shown a
mangled version of your own words invites you to correct something that was never
going to be pasted. It is genuinely useful as reassurance that the microphone is
working; it is not a draft, and defaulting it on presented it as one.
Nothing is inserted progressively, either way. Doing so would mean retracting text when the transcript revises itself — firing backspaces into whatever you were writing — so the preview stays in the panel and the paste happens once, from one clean read of the whole recording.
Text is pasted, not typed. Synthetic keystrokes are slow and get mangled by autocorrect and input methods, so the transcript goes to the clipboard and the app synthesises ⌘V.
Your data
Transcripts live in SQLite at
~/Library/Application Support/dev.heynaavi.voicedump/voicedumps.db, with an FTS5
index behind the search box.
Audio is copied into …/media/, organised by month, so playback survives you
moving or deleting the original — the store keeps both the archived copy and the
path it came from. The copy is normalised to mono AAC so that everything is
playable by the same <audio> element: symphonia decodes, and CoreAudio's own
/usr/bin/afconvert encodes. No ffmpeg, here or anywhere else.
Dictation records to a scratch WAV in …/dictation/, which is deleted the moment
the library has its copy. A capture that never reaches that point — a failed
transcription, a crash mid-recording — used to be left behind forever; since
0.4.1 anything still there after a day is swept at launch. Nothing that became a
transcript is ever touched by the sweep, because it is already gone by then.
Nothing is transmitted. There is no telemetry, no crash reporting and no update check in this build.
Memory is given back. The model loads on first use, not at launch, and is
released after five idle minutes — measured on an M1 Pro, that returns 512 MB of
the 578 MB medium costs. This matters because the app lives in the menu bar:
closing the window deliberately keeps the globe key alive, so "not quitting" is
the normal state, and a model held until quit is a model held all day.
Reloading costs ~560 ms and is free in practice, because dictation warms the
model on key down — the reload happens while you are still speaking. Set
VOICEDUMPS_IDLE_UNLOAD_SECS=0 if you would rather spend the memory.
Tuning
| Variable | Meaning |
|---|---|
VOICEDUMPS_MODEL_SIZE |
small or medium, overriding the memory-based choice |
VOICEDUMPS_MODEL_DIR |
Where to find the weights, ahead of the downloaded copy |
VOICEDUMPS_IDLE_UNLOAD_SECS |
Seconds of disuse before the model is released. Default 300; 0 keeps it loaded forever |
TEST_AUDIO |
Audio file for the engine tests and scripts/bench.sh |
cargo test --manifest-path src-tauri/Cargo.toml --no-default-featuresThe interesting ones need real inputs and skip loudly without them:
| Test | What it proves |
|---|---|
transcribes_real_audio |
A file decodes and transcribes with no Python and no ffmpeg in the path |
exits_cleanly_with_a_model_loaded |
Quitting with a model resident does not abort |
idle_policy |
A model in active use is never collected; 0 disables collection |
reaper_frees_a_live_model_off_thread |
Freeing a live Metal context from a worker thread doesn't abort, the memory actually returns, and the engine still reloads. #[ignore]d — needs the weights |
benchmark_latency |
The numbers above. #[ignore]d — it is a measurement, not an assertion |
fresh_captures_survive |
The sweep never deletes a capture that may still belong to a dictation in flight |
other_files_are_left_alone |
Age alone is not licence to delete: only our own .wav captures are swept |
an_older_result_is_not_a_measurement |
A transcript from before timing existed records no timing, rather than a zero |
Stated plainly, because a README that only lists wins is not worth reading:
- Apple Silicon only. The arm64 build does not run on Intel Macs, and Windows and Linux are not supported.
- The AI half needs macOS 26 and Apple Intelligence. Titles, overviews and ASK all run on Apple's on-device model, which does not exist before macOS 26 and can be switched off or unavailable on an eligible Mac. Everything else — dictation, transcription, meeting recording, search, export — works on macOS 11 and later regardless, and the app says which state it is in rather than failing quietly.
- ASK answers from about six recordings at a time. The on-device model has a 4,096-token window shared between the question, the notes and the answer, so a question that really needs forty notes gets the six that matched best. It cites them, so you can see what it read.
- Turning an answer into an email is unreliable. Reformatting works — paragraph, bullets, shorter, a poem — but "make that an email" sometimes returns the input unchanged when the answer it is working from is thin.
- Watch a folder for auto-transcription
- Local audio enhancement — podcast-grade cleanup, tunable
- Speaker names carried across meetings, not just within one
- One-click updates, once there is a signing key that never touches this repo



