Architecture and execution model
Threads and queue, the text-injection cascade, the control socket, and per-language typography.
Three threads, one queue
Audio capture, recognition and the terminal UI run on separate daemon threads in src/main.py, joined by a thread-safe queue.Queue.
audio_capture.py reads 16 kHz mono PCM from PortAudio and pushes it onto the queue. recognition.py pulls it into a local KaldiRecognizer.
When recognition falls behind, the oldest audio chunk is dropped rather than letting latency drift — the overflow count shows up in --status and --diagnose.
Text injection: a clipboard paste, with a backend cascade
Once text_processing.py has formatted a recognised utterance, system_control.py puts it on the clipboard and simulates a paste. The backend is chosen once at startup.
| Path | Tools | Condition |
|---|---|---|
| Wayland | wtype + wl-clipboard | Layout-independent (wtype works by keysym). Needs a compositor that exposes the virtual-keyboard protocol (wlroots desktops); otherwise falls back to ydotool. |
| X11 | xdotool + xclip | Standard path under X11. |
| Fallback | ydotool + wl-clipboard | Injects at the driver level (/dev/uinput) via the ydotoold daemon; keycodes are layout-dependent; requires membership of the input group. |
The keystroke engine needs a clipboard tool beside it: wl-clipboard under Wayland — required even on the ydotool fallback — and xclip under X11. The native packages pull wl-clipboard in (the Debian package also recommends xclip); a source install adds them itself. Without one, the dictated text never reaches the clipboard and the paste re-inserts whatever was last copied by hand.
The previous clipboard contents are read back and restored after the paste (configurable via clipboard.restore, on by default). Non-text content — images, files — is left untouched. Under Wayland with a GTK application the restore can beat the window’s own clipboard read; clipboard.defer_restore holds the restore until the dictation session ends so the paste never picks up the old contents.
If no backend for the current session works, the tool enters degraded mode: recognised text keeps printing to the terminal, and automatic typing is disabled for the rest of the session rather than retried — and failing — on every word.
A few windows also refuse the paste — some Java applications, protected fields. The first time that happens, the tool switches to degraded mode the same way.
The control socket
A running session opens a private Unix socket at $XDG_RUNTIME_DIR/vosk-cli-dictation.sock (mode 0600). The same command invoked with --toggle, --finalize, --sleep-toggle, --status or --stop connects to that socket instead of starting a second session.
This is what makes desktop keyboard shortcuts work everywhere under Wayland, including the native-Wayland windows that pynput global hotkeys never see.
Only one session runs per user at a time; a stale socket left by a crash is detected and replaced at the next start.
Two layers of vocabulary correction
A wrong word and a miscased word are different failures, and config.yaml keeps their fixes separate on purpose.
recognition_aliases
The model heard the wrong words; replace them outright ("tirez" → "tiret").
custom_vocabulary
The model heard right but lowercased a name; force its casing ("vosk" → "Vosk").
Per-language typography
text_processing.py applies the output rules of the dictated language, not only its punctuation words.
- French — inserts a narrow no-break space (U+202F) before
? ! : ;and». - German — uses
„ "quotation marks and capitalises only sentence starts. - Spanish — inserts
¿and¡only on an explicit command.
Each set lives under language_settings.<lang>.typography, because the correct behaviour genuinely differs by language, not just by vocabulary.
Interface translation
The tool’s interface language and its dictation language are two separate settings.
Interface language
The tool’s own strings use the GNU gettext toolchain — .po catalogues compiled to .mo, extracted from the Python source with Babel.
Dictation language
language_settings in config.yaml governs the language you speak, its punctuation words and its typography — not the UI language.
See docs/configuration.md for every key, and Guides to get started.