- Shell 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| docs/adr | ||
| config.example | ||
| CONTEXT.md | ||
| dictate | ||
| README.md | ||
dictate
Push-button dictation for Wayland (niri) and macOS. One keypress records, the next turns what you said into text and hands it to the focused window.
On Linux that means it is typed for you — terminal, browser, Emacs, anything. On macOS it stops at the clipboard and you press ⌘V; macOS has no way to synthesize input without an Accessibility grant, and one that is silently denied looks exactly like the tool being broken. See ADR 0001.
The pipeline, one keybind end to end:
- Record — the default mic at 16 kHz mono WAV, Parakeet's native
format:
pw-recordon Linux,ffmpeg -f avfoundation -i ":default"on macOS (:defaultfollows the system input the waypw-recorddoes). - Transcribe —
onnx-asrruns NVIDIA Parakeet Unified EN 0.6B locally. The int8 ONNX model is auto-downloaded to~/.cache/dictate/modelson first use (about 635 MB). - Clean — the raw transcript goes to DeepSeek
/chat/completionswith a typist system prompt: phrase by phrase it judges whether you were speaking for the page or to the typist, so spoken spellings ("D-O-T files, one word"), corrections ("no, wait, I mean…"), retractions ("scratch that") and formatting commands ("new line") are executed rather than transcribed, while filler and false starts are dropped. Skipped when no API key is configured; any failure falls back to the raw transcript, so dictation works offline. - Deliver — on Linux the text is copied to the clipboard (backup),
then typed into the focused window with
wtype, which uses thezwp_virtual_keyboard_manager_v1protocol niri exposes — so it works in terminals exactly as if you typed it. On macOS it is copied withpbcopyand the notification tells you it is ready to paste.
Usage
dictate # same as: dictate toggle
dictate toggle # start recording / stop + transcribe + type
dictate cancel # abort the current recording, type nothing
dictate status # "recording" or "idle" (for bars/widgets)
Toggle, not hold-to-talk: niri keybinds fire on key press only (no release binds), and a toggle also survives long dictations without a cramped pinky.
Install
The script is self-contained; put it on PATH wherever you like. On macOS
it must be an absolute path for skhd (see below), so:
ln -s "$PWD/dictate" ~/.local/bin/dictate
A symlink rather than a copy: the repo stays the single source of truth, so an edit is live and the keybind exercises the file you are editing.
Config
~/.config/dictate/config — a POSIX sh fragment, sourced if present
(see config.example):
DICTATE_DEEPSEEK_API_KEY=sk-... # empty/absent = no LLM cleanup
DICTATE_DEEPSEEK_MODEL=deepseek-v4-flash
# DICTATE_PROMPT="..." # override the cleanup prompt
If DICTATE_DEEPSEEK_API_KEY is empty, the script falls back to reading
the bare key from ~/.config/dictate/deepseek-key (chmod 600). Use that
file when the config itself is tracked in dotfiles — on Guix Home,
deployed dotfiles pass through the world-readable store, so the secret
must stay in the untracked key file.
niri integration
Mod+V { spawn "dictate" "toggle"; }
Mod+Shift+V { spawn "dictate" "cancel"; }
Progress is shown as a single dunst notification that updates in place (dunst stack tag).
macOS integration
Keybinds go through skhd. The full
path is required: skhd inherits launchd's environment, which does not
carry ~/.local/bin.
cmd - d : "$HOME/.local/bin/dictate" toggle
cmd + alt - d : "$HOME/.local/bin/dictate" cancel
Note that a global cmd - d shadows the application-level ⌘D (bookmark in
browsers, Duplicate in Finder).
Notification Center has no equivalent of the dunst stack tag, so the four progress notifications stack rather than replacing one another.
Because skhd reads no shell profile, dictate keeps its runtime state in
/tmp/dictate-$(id -u) on macOS regardless of XDG_RUNTIME_DIR — the
hotkey and a terminal would otherwise disagree about whether a take is
live.
Dependencies
Linux: pw-record (pipewire), onnx-asr, curl, jq, wtype,
wl-copy (wl-clipboard), notify-send (libnotify). The
Guix package (mort packages dictate in the dotfiles repo) wraps the
script with all of these on PATH.
macOS: ffmpeg, onnx-asr, curl, jq, skhd — pbcopy and
osascript ship with the system.
brew install ffmpeg jq skhd
pipx install 'onnx-asr[cpu,hub]'
Notes
- On Linux, newlines are typed as Return. In a terminal that executes a line, so only say "new line" when you mean it. On macOS you paste, and a terminal's bracketed paste does not execute the lines.
- If typing fails (e.g. focus landed somewhere odd), the take is still on the clipboard.
- Parakeet Unified EN 0.6B is English-only and includes punctuation and capitalization. Dictate uses the int8 ONNX export to reduce disk and memory use.
onnx-asruses the inference providers available in its ONNX Runtime installation. The Guix package uses CPU inference.