No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-01 11:09:48 +08:00
docs/adr macOS support alongside Linux/niri: one script branching on uname, with delivery stopping at the clipboard 2026-07-27 21:24:39 +08:00
config.example Use Parakeet Unified EN 0.6B for transcription 2026-09-01 11:09:48 +08:00
CONTEXT.md Use Parakeet Unified EN 0.6B for transcription 2026-09-01 11:09:48 +08:00
dictate Use Parakeet Unified EN 0.6B for transcription 2026-09-01 11:09:48 +08:00
README.md Use Parakeet Unified EN 0.6B for transcription 2026-09-01 11:09:48 +08:00

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:

  1. Record — the default mic at 16 kHz mono WAV, Parakeet's native format: pw-record on Linux, ffmpeg -f avfoundation -i ":default" on macOS (:default follows the system input the way pw-record does).
  2. Transcribe — onnx-asr runs NVIDIA Parakeet Unified EN 0.6B locally. The int8 ONNX model is auto-downloaded to ~/.cache/dictate/models on first use (about 635 MB).
  3. Clean — the raw transcript goes to DeepSeek /chat/completions with 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.
  4. Deliver — on Linux the text is copied to the clipboard (backup), then typed into the focused window with wtype, which uses the zwp_virtual_keyboard_manager_v1 protocol niri exposes — so it works in terminals exactly as if you typed it. On macOS it is copied with pbcopy and 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-asr uses the inference providers available in its ONNX Runtime installation. The Guix package uses CPU inference.