Dictee: my own local dictation app

Caps Lock to talk, a live transcript while I speak, clean text pasted where the cursor is. Everything runs on my Mac Studio. Built with Claude Code in 22 prompts.

Dictee is my own dictation app. I press Caps Lock, speak, press Caps Lock again, and the cleaned text lands where my cursor is. It's also on the clipboard. Nothing leaves the Mac: speech recognition, cleanup and storage all run locally.

The name is the French dictée, and it fits the -ee family of my other tools.

The Dictee panel while recording: logo, name, red dot, timer at 0:28, level meter and the live transcript of the sentence being spoken

Why I built it

I liked Wispr Flow for its output. It still sends every dictation to its cloud, even with Privacy Mode on, and it runs on a subscription. I wanted 3 things it couldn't give me:

  • Local and private. Client names and deal details never leave the machine.
  • Accurate in my 3 languages. English, French and German, with my names and Kaltura terms spelled right.
  • Flexible. My hotkey, my formatting rules, my dictionary, my apps.

I compared the options first (VoiceInk, Handy, superwhisper and others, see macOS Voice Dictation Solutions), then decided to build my own.

What it does

Feature How it works
Caps Lock trigger Press to start, press to stop. Shift+Caps records privately, Esc cancels
Live transcript The panel shows my words as I speak, refreshed every second
Timer and meter A running counter and an input level bar, so I know the mic hears me
Own logo and name "Dictee" and my edelweiss logo on the panel, the menu bar and notifications
1 sentence per line Every sentence starts on a new line. Terminals get 1 line, so nothing runs by accident
Voice commands "New line", "nouvelle ligne", "neuer Absatz" and so on, in EN, FR and DE
Cleanup Punctuation, filler words, self-corrections, digits instead of words, my spellings
Dictionary Terms and their mishearings. "Nick" becomes "Nic", "culture a" becomes "Kaltura"
Paste and clipboard Pastes into the app I was in, with that app's own Paste shortcut, and keeps a copy
Music pause If Audirvana is playing, Dictee pauses it and resumes it when the recording ends
Clear errors A red panel plus a macOS notification that says what failed and what to do
Menu bar status Logo in white when idle, red while recording, orange while transcribing, "!" on a problem

The Dictee panel after the recording stops: orange dot, Transcribing label, and the last lines of the live transcript

The live transcript is raw recognition. The pasted text is the cleaned version, 1 sentence per line:

Three dictated sentences pasted into TextMate, each on its own line

A few details I care about:

  • Guards on the cleanup model. If the model changes the language or rewrites too much, Dictee pastes the raw text and tells me why. It never "improves" my meaning.
  • It learns from corrections. I fix a pasted text, copy it, and save it as a correction from the menu bar. The corrections become my personal test set, and dictee.py eval scores speech engines on my own voice.
  • Error messages are written for me. "The microphone sent pure digital silence. Check the microphone permission for Hammerspoon." beats a stack trace.

For business people

Dictation saves the most time on the writing I do all day: emails, Teams messages, notes after calls, prompts for Claude.

The cloud tools are good, but they send every word to someone else's server. For a seller, that means client names, deal sizes and internal plans. Dictee gives me the same workflow with none of that exposure, no subscription, and full control over how the text comes out.

The trade-off: I maintain it myself. It runs on 1 machine, my Mac Studio, and it relies on the tools already installed there.

For technical people

Architecture

Caps Lock ──> Hammerspoon (Lua)
              - Caps Lock tap, overlay panel, live text, menu bar, notifications
              - Audirvana pause / resume (AppleScript)
              - paste with the app's own shortcut + clipboard
                    │  HTTP on 127.0.0.1
                    ▼
              Dictee daemon (Python)
              1. record    External Microphone (Rode VideoMic on the jack), 48 kHz
              2. trim      silence cut against the room's noise floor, resample to 16 kHz
              3. ASR       Parakeet TDT 0.6B v3 on MLX (mlx-audio), model kept warm
              4. preview   every second: re-transcribe the recording so far, no LLM
              5. aliases   dictionary replacements from SQLite
              6. cleanup   mistral-small in Ollama, temperature 0, closest dictionary terms in the prompt
              7. guards    language check + drift check, else keep the raw text
              8. format    voice commands, 1 sentence per line, no em-dashes, per-app profiles
              9. log       SQLite: raw text, final text, timings. Audio kept 180 days
Part Choice Why
Speech model NVIDIA Parakeet TDT 0.6B v3 25 European languages, 0.2-0.3 s for a 9 s clip
Runtime mlx-audio on Apple Silicon 1 API for Parakeet, Whisper and Voxtral
Cleanup model mistral-small (24B) in Ollama Only local model in my test that fixed names and digits without translating
Hotkeys and UI Hammerspoon Already running, reads the accessibility tree, draws overlays
Storage SQLite Dictations, corrections, dictionary

A 9-second dictation takes about 2 seconds from stop to paste: 0.2-0.3 s for speech recognition, about 1.8 s for cleanup. Voxtral Realtime 4B scored better on French and German in published benchmarks, but took 2 s per clip on my M1 Ultra, so it stays a 1-line config switch.

Why not my own voice model

My fine-tuned model (see Fine-tuning your own model) stays out of the dictation path. It's trained to turn a brief into my writing, so it rewrites. Dictation needs the opposite: change as little as possible. It also takes about 13 seconds per text, and cleanup has to take about 1.

Gotchas I hit

  • A launchd agent can't use the microphone. macOS silently hands a background Python process pure zeros, with no permission prompt. Hammerspoon starts the daemon instead, and the daemon inherits Hammerspoon's microphone permission.
  • Remapping Caps Lock failed twice. A hidutil remap from Caps Lock to F18 produced no key event at all, and my old Hyperkey setup had Caps Lock set to "No Action" in System Settings. Dictee now listens to Caps Lock itself and turns the lock straight back off.
  • macOS ignores quick Caps Lock taps. A built-in delay drops short presses that would turn Caps Lock on. Since Dictee resets the lock after each press, every press is an "on" press, so I sometimes had to press twice. Setting CapsLockDelayOverride to 0 fixed it.
  • Menu-based paste isn't reliable. In TextMate, choosing Edit > Paste only opened the menu. Dictee now presses each app's own Paste shortcut, read from the app's menu or from my custom shortcuts (Cmd+F is Paste on my Macs).
  • Parakeet invents words from silence. Room noise alone produced "Yeah." The live preview only runs when the recording holds real speech.

How I got there: 22 prompts

I built Dictee in 1 session with Claude Code, over 2 days. I wrote 22 prompts. Claude wrote the code, tested each part on the machine, and pushed every change to a private GitHub repo.

Phase Prompts What happened
Research 1 Local alternatives to Wispr Flow, pros, cons, my setup's constraints
Architecture 1 Name, requirements, full design, the voice model question
First build 1 Daemon, Hammerspoon module, dictionary, tests, repo
Caps key 8 Key loggers, raw HID monitor, Hyperkey removed, System Settings fix
Features and fixes 9 Live transcript, dictionary, errors, Cmd+F paste, logo, double press, TextMate, Audirvana
Questions 2 Where the database is, how many prompts

The Caps key took 8 of the 22 prompts. The code was right early on. The cause sat in 2 places Claude could only diagnose: an old Hyperkey setting in System Settings and a macOS timing rule. Claude couldn't press the key or click Allow, so each test needed me at the keyboard.

What worked well:

  • Start with research, then architecture, then build. The first 2 prompts produced a design I could judge before any code existed.
  • Measure, don't guess. The engine and the cleanup model came from tests on my Mac, not from blog benchmarks. 1 candidate model translated my English into French.
  • Ask for features in plain words. "Pause Audirvana while I record" became a feature, a test and a commit in 1 prompt.

Today it's about 1,850 lines of Python and Lua, 12 commits and 16 unit tests. It replaces Wispr Flow on my Mac Studio.

Further reading

NicAI
Written by NicAI, Nic's AI assistant, for his personal knowledge base. Researched and drafted by the model, not hand-written by Nic. Verify anything you plan to act on.