Read your text aloud, get clean Markdown back — local dictation for macOS that spells your jargon right
Install · CLI · Vocabulary · Output · The app · Claude Code
Transcription runs on the Neural Engine with WhisperKit
(Whisper large-v3 turbo), seeded with your own vocabulary file so names and terms such as
Constantinides-Duffie, Koszegi-Rabin or heteroskedasticity come out spelled right.
Nothing leaves the Mac. Two front ends share one engine: the dictate command-line tool,
and Dictate.app, a small window plus a menu-bar microphone.
dictate -> YYYYMMDD-HHMM-memo.{wav,txt}
dictate section2 -> section2.{wav,txt}
dictate --no-transcribe record only
dictate --vocab FILE override the vocabulary file
dictate --html also render section2.html (house style)
dictate --from FILE.wav transcribe an existing recording (no microphone)
While recording, one status line shows the state, the recorded time (pauses excluded) and an input level meter:
* REC 00:01:23 ████████░░░░░░░░ -21 dB space: pause/resume q: stop
| key | action |
|---|---|
space |
pause / resume (appends to the same file) |
q |
stop and transcribe |
Ctrl-C |
stop and transcribe (the WAV is finalised first) |
Once transcription has started, Ctrl-C aborts it; the WAV is kept, and
dictate --from NAME.wav NAME transcribes it later.
- macOS 14 or later on Apple silicon (built and tested on macOS 15.7, M2 Max)
- Swift 6 toolchain; the Command Line Tools are enough, Xcode is not needed (for the app either)
pandoc, only for--html(used by the render script)
From Releases:
curl -L https://github.com/LouLouLibs/dictate/releases/latest/download/dictate-darwin-arm64 \
-o ~/.local/bin/dictate && chmod +x ~/.local/bin/dictate
mkdir -p ~/.config/dictate && curl -L \
https://github.com/LouLouLibs/dictate/releases/latest/download/vocab.txt -o ~/.config/dictate/vocab.txtDictate.app.zip is on the same page. It is ad-hoc signed (no Apple developer ID), so
after unzipping a download macOS will refuse to open it until you clear the quarantine
flag once: xattr -dr com.apple.quarantine Dictate.app, then move it to /Applications.
Building the app yourself (below) avoids that step.
swift build -c release
install -m 755 .build/release/dictate ~/bin/ # or ~/.local/bin
mkdir -p ~/.config/dictate && cp vocab.txt ~/.config/dictate/vocab.txt
scripts/make-app.sh --install # Dictate.app -> /ApplicationsThe first build fetches WhisperKit (SPM package argmax-oss-swift v1.1.0 or later;
github.com/argmaxinc/WhisperKit redirects there) and takes a minute or two. Later
builds are incremental. WhisperKit is the only dependency.
Try it without a microphone: dictate --from demo/sample.wav --vocab vocab.txt.
The first transcription downloads the CoreML model
openai_whisper-large-v3-v20240930_turbo (1.64 GB, 22 files) from Hugging Face into
~/.local/share/dictate/models/argmaxinc/whisperkit-coreml/<model>/
with a progress bar (a few minutes on a fast connection). Nothing goes into
~/Documents, WhisperKit's default. The tokenizer (openai/whisper-large-v3, a few
MB) is fetched the first time the model loads, into ~/.local/share/dictate/models/openai/.
After that no network is needed.
The first load of a freshly downloaded model compiles it for the Neural Engine,
which took about a minute here; dictate says so while it waits. macOS caches the
compiled model, so later loads take about a second, and a 40-second recording is
transcribed in about five.
If a download is interrupted, run dictate again: complete files are kept and the rest
is fetched. If a model ever looks corrupt, delete its folder and it is downloaded afresh.
Other variants from the same repository work with --model NAME, for example
openai_whisper-large-v3-v20240930_turbo_632MB (quantised, smaller and a bit faster),
openai_whisper-large-v3-v20240930 (the full 32-layer decoder, roughly 4x slower) or
distil-whisper_distil-large-v3_turbo. In testing, the full model did not spell names
any better than turbo; what matters is the vocabulary prompt (next section).
dictate is a plain executable, so macOS attributes microphone access to the terminal
app that runs it (Terminal, iTerm2, Ghostty, ...). The first time, macOS shows its
permission dialog for that app; click Allow. If the terminal has been denied,
dictate stops with a message; fix it under System Settings > Privacy & Security >
Microphone by enabling the terminal app, then run dictate again.
If the level meter stays empty, dictate warns after three seconds of dead silence:
check the permission and the selected input device (System Settings > Sound > Input).
~/.config/dictate/vocab.txt (or --vocab FILE) is a plain-text list, one term or
phrase per line, most important first; blank lines and # comments are ignored:
# Author pairs and models
Constantinides-Duffie
Koszegi-Rabin
# Econometrics
heteroskedasticity
HC3
The terms are passed to the Whisper decoder as DecodingOptions.promptTokens, the same
mechanism as initial_prompt in OpenAI's implementation: the model treats the prompt as
preceding transcript and follows its spellings. Every 30-second window of audio gets
the same prompt.
Two things about how Whisper reads the prompt shape the file:
- Position matters. The model attends most to the end of the prompt, the part
closest to the audio. In tests with synthesised speech, author pairs at the top of a
33-term list were still mangled ("Constantinides Duffy", "a semaglurist repo") while
the same names at the end, or in a 15-term list, came out perfectly.
dictatetherefore lists the file in reverse order in the prompt, so the top of the file is what the model sees last. Put the terms you care about most at the top. - Length is capped. Whisper keeps at most 223 prompt tokens (about 50 short terms).
If the file is longer,
dictatedrops lines from the bottom and prints a warning naming the first ones dropped. Shorter is also better: 10-25 terms the model actually gets wrong beat a long glossary. Common words need no help.
dictate prints the token count after every transcription. The bundled vocab.txt
(24 terms, 134 tokens) is a starting point; edit it freely. If the file is missing,
dictate warns on stderr and transcribes without a prompt.
NAME.txt is Markdown: a YAML block, followed by the transcript as prose paragraphs:
---
title: "section2"
date: 2026-08-24T15:42:10+02:00
audio: "section2.wav"
duration: "04:37"
model: "openai_whisper-large-v3-v20240930_turbo"
---
The paper studies ...
Section 2 develops ...Punctuation comes from the model; do not speak it. A new paragraph starts wherever the
segment timings show a silence of at least 1.5 s (--paragraph-gap SECS to tune, 0 to
disable), and wherever you paused the recording with space (the pause leaves no gap
in the audio, so dictate remembers where it happened).
NAME.wav is 16 kHz mono 16-bit PCM, Whisper's native input; a minute of speech is about
1.9 MB.
--html runs ~/.claude/skills/rendering-spec-docs/render-doc.sh on the transcript after
it is written and leaves NAME.html next to it.
dictate refuses to overwrite an existing NAME.wav or NAME.txt; pick another name or
remove the old files. To re-transcribe a recording, for instance after editing the
vocabulary: dictate --from section2.wav section2-v2.
Dictate.app wraps the same engine in a window and a menu-bar item. Build it with
scripts/make-app.sh # -> dist/Dictate.app
scripts/make-app.sh --install # also copies it to /ApplicationsNo Xcode: the script builds the DictateApp SwiftPM target, assembles the bundle
(Info.plist with the microphone usage description, icon, ad-hoc code signature) and is
done in seconds after the first build.
- Window: Record (⌘R) / Pause (space) / Stop & Transcribe (⌘↩), a timer, a level meter, transcription progress, then Copy / Open / Show in Finder for the transcript. Transcribe File… (⌘O) runs an existing WAV through the same pipeline.
- Menu bar: a microphone icon (filled while recording) with Start / Pause / Stop, the last transcript, the save folder, and Quit. Closing the window leaves the app in the menu bar; Show Dictate Window brings it back. When the app is in the background, a notification announces each finished transcript (macOS asks once whether to allow notifications).
- Settings (⌘,): the save folder (default
~/Documents/Dictate; files areYYYYMMDD-HHMM-memo.{wav,txt}, with-2,-3appended if you record twice in a minute), Also render HTML, the paragraph silence threshold, whether to show the menu-bar icon, the vocabulary file (Edit… opens~/.config/dictate/vocab.txt, creating it from the bundled default if missing), and the model: large-v3 Turbo (default), its quantised variant, the full large-v3, or Distil. Models and vocabulary are shared with the CLI; a model downloads on first use with either front end. - Menus: File opens an audio file to transcribe (⌘O) and lists the five most recent transcripts; Recording holds the same commands as the buttons plus Copy Last Transcript (⇧⌘C, the prose without the front matter), Show Last Transcript in Finder and Open Save Folder (⇧⌘F); Help opens the vocabulary file or the models folder; About Dictate shows the version.
- Opening recordings: drop a WAV (any audio file) on the window or the Dock icon,
use Finder's Open With, or run
open -a Dictate file.wav; each is transcribed into the save folder. Right-clicking the Dock icon offers Start / Pause / Stop. - Quitting while a recording is in progress asks whether to Stop & Transcribe, Discard Recording or Cancel; quitting mid-transcription warns that the audio is saved and can be transcribed later. Closing the window keeps the app running while the menu-bar icon is shown or work is in progress.
- Notifications for finished transcripts (sent when the app is in the background) have Open and Reveal in Finder actions.
- Global shortcut: Settings > General can enable ⌃⌥⌘R to start or stop recording from any app (a short sound confirms each). It is off by default and could not be exercised by automation on the development machine; if it does nothing for you, the key combination is probably taken by another app.
The first launch asks for microphone access for Dictate (separate from the terminal's grant). The first transcription in the app also pays the one-time Neural Engine compile (about a minute and a half): the compiled-model cache is per executable, so the app and the CLI each compile once.
For testing from a terminal, the app binary has two headless modes that write to a
temporary folder and print the transcript: .build/release/DictateApp --headless-smoke 4
(record four seconds, with a pause) and --headless-from FILE.wav.
The dictate skill
in claude-skills teaches Claude Code to
transcribe recordings with dictate --from, manage the vocabulary file and read the
Markdown output. Install all LouLouLibs skills with that repo's install.sh, or copy the
one folder into ~/.claude/skills/dictate.
swift build # debug build
swift run dictate-selftest # unit tests for the pure-logic library (DictateCore)
swift build -c releaseThe tests are a plain executable rather than a swift test target because the Command
Line Tools ship neither XCTest nor a usable Swift Testing module.
Layout: Sources/DictateCore holds the testable logic (argument parsing, vocabulary,
paragraphing, Markdown); Sources/DictateEngine the recorder (AVAudioRecorder), the
WhisperKit driver and the transcribe-write-render job shared by both front ends;
Sources/dictate the terminal handling (termios raw mode, signals, status line);
Sources/DictateApp the SwiftUI window, menu-bar item and headless test modes;
scripts/make-app.sh the bundling.