Voice-to-text anywhere on your Linux desktop. Press a key, speak, release, paste.
Uses faster-whisper for local, offline speech-to-text — no cloud transcription, no API keys. Whisper handles capitalization and punctuation automatically.
demo.mp4
git clone https://github.com/vimalk78/dictate.git
cd dictate
bash install.sh
The installer first checks requirements and changes nothing if any are missing — it prints the exact commands to run (one dnf/apt-get line, plus adding you to the input group). Run them, log out and back in, and run bash install.sh again. The installer itself never uses sudo.
Two background services start on every login:
- dictate — daemon with Whisper model loaded in memory
- dictate-ptt — push-to-talk listener (system-wide keyboard detection)
The flow: press Right Ctrl in any window, speak, release the key. A bell rings when transcription is done. Press Ctrl+V to paste. Works in your terminal, browser, editor, Claude Code — anywhere.
No GPU required. On NVIDIA GPUs it uses the medium model for higher accuracy. On CPU (including Intel integrated) it uses the small model — still quite good for English. Works with your laptop mic, external mic, or AirPods.
The system-wide push-to-talk works directly with Claude Code — press Right Ctrl, speak your prompt, release, hear the bell, Ctrl+V into the Claude Code input. No special setup needed.
Optional: voice-enabled editor and /dictate command
Voice editor (Ctrl+G) — for longer prompts you want to review before sending:
EDITOR=dictate-editor claude
Press Ctrl+G to open nvim with voice keybindings (F5 record, F6 stop/transcribe, F7 spell check). Dictate in chunks, edit, then :wq to send.
/dictate command — speak directly into the Claude Code prompt:
mkdir -p ~/.claude/commands
cp dictate.claude-command ~/.claude/commands/dictate.md
Type /dictate, speak, pause when done.
Don't have a GPU on your laptop? Run the Whisper model on a GPU machine on your LAN and forward audio to it over TCP. The push-to-talk client doesn't know or care — it works exactly the same.
On the GPU machine (headless, no mic needed):
dictate --serve --listen 0.0.0.0:5555
On your laptop:
dictate --serve --server GPU_IP:5555
Or set the server permanently in ~/.config/dictate/config.toml:
server = "192.168.1.100:5555"Audio is sent as raw float32 over TCP (~64KB/s) — trivial on a LAN.
- Two background services — daemon (model loaded) + push-to-talk (keyboard listener). No terminal needed.
- System-wide key detection via
evdev— works in any window, any app. Detects keyboard disconnect/reconnect automatically (e.g. KVM switches). - Mic health monitoring — detects microphone disconnect/reconnect (e.g. USB re-plug, Bluetooth switch, KVM). Sends desktop notifications and re-calibrates automatically.
- Sound notifications — bell rings when transcription is done, so you know when to paste.
- Pre-loaded Whisper model — no startup delay per request. The model stays in memory.
- 1-second rolling pre-buffer — captures speech from the moment you press the key.
- Runs entirely locally — no internet, no cloud APIs, no data leaves your machine.
Switch transcription language on the fly — no restart needed.
dictate --lang hi # set language to Hindi
dictate --lang en # set language to English
dictate --lang # toggle through configured languages
The toggle cycles through languages in your config (default: ["en", "hi"]). A desktop notification shows the current language.
Note: multilingual transcription requires a model that supports the languages you want. The large-v3 model works well across languages. Smaller models like small are decent for English but weaker for Hindi and other languages.
Keyboard shortcut (recommended): bind dictate --lang to a key combo for instant switching:
# GNOME example: Super+F9
gsettings set org.gnome.settings-daemon.plugins.media-keys custom-keybindings "['/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/']"
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/ name 'Dictate Toggle Language'
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/ command 'dictate --lang'
gsettings set org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom0/ binding '<Super>F9'
Or add it manually in GNOME Settings → Keyboard → Custom Shortcuts.
dictate --serve # start daemon (keeps model loaded)
dictate --ptt # push-to-talk via daemon (system-wide, sound notifications)
dictate --once # send one request to daemon, print text
dictate --lang [CODE] # set or toggle language (cycles through `languages` in config.toml)
dictate --stop # stop daemon
dictate --stop-recording # stop current recording immediately
dictate --key PAUSE # use a different trigger key
dictate --model small # smaller/faster model
dictate --model large-v3 # best accuracy (needs >4GB VRAM)
dictate --language hi # Hindi, or any supported language
dictate --cpu # force CPU inference
dictate --list-devices # show available audio input devices
dictate --serve --listen 0.0.0.0:5555 # headless TCP transcription server
dictate --serve --server IP:5555 # daemon forwarding to remote GPU
Edit ~/.config/dictate/config.toml:
language = "en" # default transcription language
languages = ["en", "hi"] # languages to cycle with: dictate --lang
model = "large-v3" # Whisper model (auto-detected if not set)
key = "RIGHTCTRL"
pre_buffer_secs = 1.0
silence_secs = 3.0
wait_secs = 10.0
server = "" # "HOST:PORT" for network transcriptionSwitching microphones — dictate uses PipeWire's default input source. To switch between mics (e.g. webcam mic vs AirPods), change the default input in GNOME Settings → Sound → Input, then restart:
systemctl --user restart dictate.service dictate-ptt.service
Whisper can struggle with technical terms — "Claude" becomes "cloud", "Kubernetes" becomes "kubernetes". Hints fix this.
Hints are loaded from two directories, merged together:
| Directory | Scope | Ships with |
|---|---|---|
~/.config/dictate/hints.d/ |
Global (always loaded) | install.sh |
.dictate-hints.d/ in CWD |
Project-specific | You or /dictate-hints |
Each file contains one term per line (# comments supported). All files in both directories are merged and deduplicated.
Global hints are installed automatically — includes common Claude, dev tooling, and language terms.
Project hints — create .dictate-hints.d/ in your project root and drop files in:
.dictate-hints.d/
project.hints # MyClassName, my_function, ProjectName
infra.hints # Terraform, Ansible, Helm
Or use /dictate-hints in Claude Code to auto-generate from your codebase:
cp dictate-hints.claude-command ~/.claude/commands/dictate-hints.md
Hints are sent per-request — no daemon restart needed when switching projects.
| Hardware | Default model | Compute | Recommended override |
|---|---|---|---|
| NVIDIA GPU (4GB+) | medium | int8 (CUDA) | large-v3 or large-v3-turbo |
| CPU only (Intel, AMD) | small | int8 | — |
No GPU required. The small model on CPU is good enough for English dictation. NVIDIA GPU gives you the medium model for better accuracy. Set model = "large-v3" in config for the best quality if your GPU has enough VRAM.
- Linux desktop with PipeWire (tested on Fedora 43/44, Ubuntu 22.04/24.04)
- A microphone (laptop mic, USB mic, AirPods — anything that shows up as an input device)
- NVIDIA GPU (optional, falls back to CPU)
install.sh checks for all of these and tells you how to install what's missing:
| Requirement | Why | Fedora | Ubuntu/Debian |
|---|---|---|---|
Python 3.10+ (system /usr/bin/python3) |
runtime | python3 |
python3 |
| uv | creates the venv | uv |
curl -LsSf https://astral.sh/uv/install.sh | sh |
| gcc, Python headers, Linux headers | compile evdev (no binary wheels) |
gcc python3-devel kernel-headers |
gcc python3-dev linux-libc-dev |
| PortAudio | audio capture | portaudio |
libportaudio2 |
| xsel | clipboard | xsel |
xsel |
| pw-play | completion sounds | pipewire-utils |
pipewire-bin |
| notify-send | desktop notifications | libnotify |
libnotify-bin |
input group |
read the push-to-talk key | sudo usermod -aG input $USER |
same |
Python packages are installed at exact versions from requirements.lock (generated from requirements.txt with uv pip compile).
For Jetson (aarch64): build ctranslate2 from source first — see build-ctranslate2.sh.
First start hangs at Loading whisper model ... — the model is downloaded from Hugging Face on first use. If your network advertises IPv6 but doesn't route it, Python waits out each IPv6 timeout before trying IPv4 (browsers don't show the problem because they try both at once). Check with:
curl -6 -sS -m 6 -o /dev/null -w "%{http_code}\n" https://huggingface.co
000 means IPv6 is broken: restart your router, or disable IPv6 on the connection (nmcli connection modify "<name>" ipv6.method disabled).
First key press does nothing — the daemon may still be downloading or loading the model. Check journalctl --user -u dictate -f for Daemon ready.
git pull
bash install.sh
The installer detects the existing install, shows the installed and new versions, and asks before reinstalling. Your config, hints, transcription log, and downloaded models are kept; running services are restarted on the new code.
bash uninstall.sh
Removes the services, launcher, venv, and /dictate command. Asks before deleting your config, hints, transcription log, and downloaded Whisper models (--purge deletes them without asking). System-level leftovers (input group, system packages) are listed as commands for you to run.
- Fedora 43, NVIDIA GTX 1650 (4GB), Keychron K8, AirPods mic
- Fedora 43, Intel integrated (no GPU), laptop mic + AirPods
- Jetson Orin Nano (JetPack 6.x, 8GB), network transcription server
- Fedora 44, Intel HD 530 (no GPU), USB webcam mic
- Clean install in containers (
tests/install-test.sh): Fedora 44, Ubuntu 24.04, Ubuntu 22.04