Voice dictation to keyboard on Linux — cloud (Groq / OpenAI) or fully offline (Parakeet, German, CPU).
Hold a hotkey, speak, release — the transcribed text is typed wherever your cursor is. Works in any application (X11 and native Wayland alike), including editors, terminals, browsers, chat windows.
- Push-to-talk or tap-to-toggle modes
- System tray icon with status (ready / recording / transcribing)
- Hotkey bound to any function key, modifier (Ctrl / Alt / Shift), or Logitech-style mouse side button
- Four transcription backends:
- Yorik (home server) — sends the clip to a Yorik install on the LAN (
/v1/audio/transcriptions, personal API token from Yorik's Settings → API tokens). One Parakeet for every device in the house; this machine carries no model. - Groq cloud —
whisper-large-v3-turbo,whisper-large-v3 - OpenAI cloud —
gpt-4o-mini-transcribe,gpt-4o-transcribe,whisper-1 - Local offline —
parakeet-primeline(German fine-tune ofnvidia/parakeet-tdt-0.6b-v3, int8 ONNX via sherpa-onnx). CPU-only, ~50 ms decode for 2 s audio at 4 threads. No network, no API key.
- Yorik (home server) — sends the clip to a Yorik install on the LAN (
- Optional minimum-hold threshold to ignore accidental modifier presses
- API keys stored locally in
~/.config/dictate/config.json(mode 600) - Wayland-compatible: uses
evdevfor key capture andydotoolfor typing, falls back topynput/xdotoolon X11 - Windows / macOS:
pip install PyQt6 sounddevice pynput requests numpyand runpython gui.py; hotkeys and typing go throughpynput. Pair it with the Yorik provider and the machine needs no model.
- Linux (X11 or Wayland — see the Wayland section below)
- Python 3.10+
- A microphone that PipeWire/PulseAudio can see
- Either a Groq API key (free tier at console.groq.com), an OpenAI API key — or nothing at all: the Local provider needs no key and no network
git clone https://github.com/winidi/dictate.git
cd dictate
./install.shThe installer adds:
- system packages:
xdotool,ydotool,python3-evdev,libnotify-bin,python3-pip,libxcb-cursor0,libportaudio2 - Python packages:
PyQt6,sounddevice,pynput,requests,numpy,sherpa-onnx,soundfile,huggingface_hub - membership in group
input(needed for evdev on Wayland) - a desktop entry so Dictate shows up in your app menu
The Parakeet ONNX model itself (~640 MB) is not downloaded by install.sh — the download is triggered on demand from Settings when you first pick the local backend, so users who only want cloud transcription don't pay the disk cost.
If the installer added you to the input group you must log out and back in for it to take effect (or reboot — see WAYLAND_NOTES.md for the systemd-linger gotcha that can defeat a plain logout).
Launch Dictate from the app menu (or python3 gui.py). On first launch you will be prompted to pick a provider and paste an API key.
Default hotkey is F9. Hold it, speak, release. The transcription is typed at the cursor.
In Settings you can change:
- Provider — Groq, OpenAI, or Local (Parakeet DE)
- Mode — push-to-talk (hold) or toggle (tap to start / tap to stop)
- Hotkey — F1–F12, modifier keys (Ctrl / Alt / Shift / Super), or one of the Logitech-style mouse side buttons (
MOUSE BACK,MOUSE FORWARD,MOUSE SIDE,MOUSE EXTRA,MOUSE TASK). Mouse buttons only work with the evdev backend (i.e.input-group setup complete). - Model — provider-specific list (cloud only)
- CPU threads (Local only) — sherpa-onnx worker threads. 4 is the sweet spot on typical CPUs; more brings little benefit and starves the rest of the system.
- Min hold to send — recordings shorter than this duration are discarded. Recommended ~2.0s when bound to Ctrl / Alt / Shift so regular keyboard shortcuts do not trigger recording.
Selecting the Local (Parakeet DE, CPU offline) provider gives you German dictation with no network at all. On first selection Settings shows a "Download model (~640 MB)" button — the ONNX weights come from the pinned HuggingFace revision of flozen1981/parakeet-primeline-onnx and land in ~/.local/share/dictate/models/parakeet-primeline-onnx/.
Latency on a modest desktop CPU (measured on a Ryzen 9 workstation, 4 sherpa-onnx threads): about 50 ms decode for a 2 s clip, 100 ms for a 5 s clip. Model load + warmup happens once in the background at Dictate startup (~0.8 s).
Attribution (CC-BY-4.0): primeline (fine-tune), NVIDIA (base model), k2-fsa/sherpa-onnx (runtime).
On Wayland (GNOME Mutter in particular) the compositor does not forward global key events to XWayland, so pynput's X-based listener sees nothing. Dictate works around this by reading /dev/input/event* directly via evdev. The install.sh script sets this up. Typing likewise uses ydotool (via /dev/uinput) because xdotool cannot type into native Wayland windows.
If a hotkey silently does nothing after install, the two things to check are:
groups | grep input— you must be in theinputgroup in the current session. A fresh login is required; on Ubuntu the "Log out" menu item is often hidden when only one account exists, usegnome-session-quit --logout --no-promptor reboot.- The Dictate GUI window has a status label. If it never turns "Recording" while you hold the hotkey, evdev is not receiving events. If it turns "Recording" but not "Transcribing" when you release, your microphone is not producing audio — see the troubleshooting section.
A dictation went nowhere
- Every recording leaves one line in
~/.local/state/dictate/dictate.log(length, peak level, characters, decode time — never the text). A failed one also raises a desktop notification: no audio frames, silent microphone (peakunder 300 of 32768), nothing recognised, recording cancelled by another key or mouse button while the hotkey was held, or typing failed - The local Parakeet model cannot take more than 400 s in one pass; recordings over 90 s are split at a pause and decoded in pieces. Quiet audio is raised in level before decoding, and a piece that still comes back empty is retried in 20 s parts
- A recording that had sound but produced no text is saved as a WAV in
~/.local/state/dictate/failed/, so the failure can be replayed. This is your own voice on disk: only the newest 3 are kept, nothing is uploaded, and you can delete the folder at any time
Hotkey does nothing / status never changes to "Recording..."
- Verify
groups | grep inputin the same terminal you launched Dictate from - Check the launcher output for
keyboard listener: evdev. If it saysevdev unavailable (...) falling back to pynput, evdev could not open/dev/input/event*and pynput will be silently blind on Wayland
Status turns to "Transcribing..." but never returns to "Ready" (or comes back with empty text)
- Your PipeWire default source may point at a device that is not currently plugged in.
wpctl statusshows both the runtime default (*next to a source) and the configured default at the bottom (Default Configured Node Names). If the configured default is a mic you have unplugged,sd.rec()will hang instead of falling back - Fix with
wpctl set-default <ID>for a live source, or pick an input in GNOME Sound Settings
Text is transcribed (visible in the "Last transcription" pane) but nothing appears in your target window
ydotoolis missing or/dev/uinputis not writable. Reinstall withsudo apt install ydotooland confirmpython3 -c 'import os; os.open("/dev/uinput", os.O_WRONLY)'succeeds. On systemd systems membership ininputshould be enough
GUI freezes for a few seconds when releasing the hotkey
- Should not happen since the audio-stop + transcribe pipeline runs in a worker thread. If it does, check the launcher stderr for exceptions
See WAYLAND_NOTES.md for the longer story of the four combined bugs that had to be untangled to make this work on GNOME Wayland with NVIDIA.
There is also a headless dictate.py (unchanged from initial release, uses pynput + xdotool directly) that reads the API key from the GROQ_API_KEY environment variable:
export GROQ_API_KEY=gsk_...
python3 dictate.py --mode ptt --key f9The CLI variant only works on X11 for the reasons described above. Use gui.py on Wayland.
~/.config/dictate/config.json:
{
"provider": "groq",
"api_key": "gsk_...",
"openai_api_key": "sk-...",
"mode": "ptt",
"key": "f9",
"model": "whisper-large-v3-turbo",
"threshold": 0.0,
"local_stt_num_threads": 4
}For provider, valid values are "groq", "openai", or "parakeet_local".
MIT. See LICENSE.