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 captureQueueRecognitionInjection cascadeControl socket
The audio-to-text path runs on daemon threads joined by one queue. The control socket is not a stage of that path — it is the side channel a keyboard shortcut uses to drive the running session.

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.

PathToolsCondition
Waylandwtype + wl-clipboardLayout-independent (wtype works by keysym). Needs a compositor that exposes the virtual-keyboard protocol (wlroots desktops); otherwise falls back to ydotool.
X11xdotool + xclipStandard path under X11.
Fallbackydotool + wl-clipboardInjects 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.