Real-time speech-to-text transcription with keyboard simulation for Linux.
Talkie is a speech recognition application that transcribes audio input and simulates keyboard events to inject text into the active window. It runs continuously in the background with a Tk-based control interface.
The application monitors microphone input, performs voice activity detection, transcribes speech using configurable recognition engines, applies light text processing (spacing, voice-command macros, sentence capitalization), and types the results via the Linux uinput subsystem.
-
Real-time audio transcription
-
Multiple speech recognition engines:
- Vosk (Kaldi, streaming, in-process)
- sherpa-onnx (in-process) — one engine that auto-detects the model kind and runs streaming Zipformer, offline Parakeet (TDT & CTC), Moonshine, Whisper, SenseVoice, and NVIDIA Canary
Both engines run in-process (critcl); there is no external/Python engine.
-
Voice activity detection: energy threshold or Silero VAD (OpenVINO, CPU/NPU)
-
Capability-aware endpointing: streaming models finalize on their own end-of-utterance signal; batch models finalize on VAD-silence or partial-stability
-
Utterance-level confidence filtering
-
Keyboard event simulation via uinput
-
Voice command macro system (punctuation, symbols, formatting)
-
External control via file-based IPC
-
Persistent JSON configuration with XDG support
-
Single-instance enforcement (TCP socket on port 47823)
-
Feedback logging for STT analysis
-
Automatic audio stream health monitoring and recovery
Note: An earlier GEC/homophone pipeline (grammar/punctuation/homophone correction via OpenVINO) was removed — the experiments were not reliable. Modern engines (Parakeet, Moonshine, Whisper, Canary) emit their own punctuation and casing.
src/
├── talkie.tcl # Main application entry point
├── talkie.sh # Startup script (library paths, CLI)
├── config.tcl # Configuration management
├── engine.tcl # Audio capture + speech processing workers, engine registry
├── stt.tcl # Common stt:: engine dispatch (in-process critcl engines)
├── finalization.tcl # engine::should_finalize (self-endpoint vs partial-stability)
├── audio.tcl # Result display, transcription state, device enumeration
├── worker.tcl # Reusable worker thread abstraction
├── output.tcl # Post-processing (filters, textproc) + uinput typing
├── textproc.tcl # Macro-based text preprocessing and voice commands
├── ui-layout.tcl # Tk interface
├── feedback.tcl # Feedback logging
├── vad_silero.tcl # Silero VAD (OpenVINO, CPU/NPU) with resampling
├── vosk.tcl # Vosk engine helpers
├── pa/ # PortAudio critcl bindings
├── audio/ # Audio energy calculation critcl bindings
├── vosk/ # Vosk critcl bindings
├── sherpa/ # sherpa-onnx critcl bindings (online + offline recognizers)
├── ov/ # OpenVINO inference bindings (for Silero VAD)
├── uinput/ # uinput critcl bindings
└── tests/ # Test suite (tcltest)
Audio processing is fully decoupled from the main thread through a multi-worker architecture:
┌─────────────────────────────────────────────────────────────────┐
│ Main Thread │
│ ┌──────────────────────┐ ┌─────────────────────────────────┐ │
│ │ Tk GUI (5Hz) │ │ Result Display │ │
│ │ - Controls │ │ - final_text(), partial_text()│ │
│ │ - Audio / VAD level │ │ - Timing info display │ │
│ └──────────────────────┘ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
▲ ▲
│ thread::send -async │ thread::send -async
│ (UI updates) │ (display notifications)
┌───────┴───────────────┐ ┌─────────────┴───────────────────────┐
│ Audio Worker │ │ Processing Worker │
│ ┌─────────────────┐ │ │ ┌───────────────────────────────┐ │
│ │ PortAudio │──┼──┼─▶│ VAD (energy or Silero) │ │
│ │ Callbacks (40Hz) │ │ │ │ STT (vosk / sherpa-onnx / ...) │ │
│ └─────────────────┘ │ │ │ finalization decision │ │
└───────────────────────┘ │ └───────────────┬───────────────┘ │
└──────────────────┼───────────────────┘
│ thread::send -async
▼
┌─────────────────────────────────┐
│ Output Worker │
│ killword + confidence filter, │
│ textproc, then uinput typing │
└─────────────────────────────────┘
Pipeline: Audio → Processing → Output (Output also notifies Main for display)
Data Flow:
- Audio Worker: PortAudio delivers ~25ms chunks, queues to Processing (never blocks). Stale chunks (>500ms old) are dropped to prevent backlog after suspend/idle.
- Processing Worker: VAD (energy threshold or Silero) + speech recognition. For
self-endpoint engines (streaming sherpa/vosk) it finalizes on the recognizer's endpoint; forexternalengines it usesengine::should_finalize(energy-silence OR partial-stability). - Output Worker: killword + utterance-level confidence filtering,
textproc(spacing/voice-commands/capitalization), then uinput typing. Lowercases ALL-CAPS recognizer output so sentence-casing applies (Parakeet/Moonshine/Whisper/Canary already emit proper case). - Main Thread: GUI updates throttled to 5Hz.
The common contract lives in src/stt.tcl (stt:: namespace): create, process → {partial endpoint}, final → {text confidence}, reset, destroy. Engines are declared in engine_registry (engine.tcl) with per-engine endpointing and emits_partials.
The sherpa-onnx engine inspects the selected model directory (sherpa::detect_kind) and dispatches to the right recognizer:
| Detected kind | Example model | Endpointing |
|---|---|---|
| online-transducer | streaming Zipformer | self (streaming, partials) |
| offline-transducer | Parakeet TDT 0.6B | external (batch) |
| offline-ctc | Parakeet CTC 110M | external (batch) |
| moonshine | Moonshine tiny.en | external (batch) |
| whisper | whisper tiny.en | external (batch) |
| sense-voice | SenseVoice | external (batch) |
| canary | NVIDIA Canary 180M | external (batch) |
Batch (offline) recognizers buffer the utterance's audio during process and decode once in final-result.
- Linux kernel with uinput support
- Tcl/Tk 9 (see
MEMORY/CLAUDE notes for the specific toolchain) - PortAudio
- User must be a member of the
inputgroup for uinput access
- sherpa-onnx C shared library + headers — install with
tools/install-sherpa-onnx-lib.sh <url>(into~/.local)
- OpenVINO (the
ovcritcl package); Intel NPU optional (falls back to CPU, then to energy threshold)
- Tk, Thread, json
- jbr::unix, jbr::filewatch, jbr::pipe (used by textproc)
- pa, audio, uinput, vosk, sherpa, ov (critcl)
Place under the models/ directory:
- Vosk:
models/vosk/vosk-model-en-us-0.22-lgraph - sherpa-onnx:
models/sherpa-onnx/<any supported model>— all sherpa models (streaming, Parakeet, Moonshine, Whisper, SenseVoice, Canary) live here and appear together in the Model dropdown; the engine auto-detects the kind. See MODELS.md.
talkie.map— voice command macro definitions
cd src
make buildThis compiles the PortAudio, audio, uinput, Vosk, sherpa, and OpenVINO critcl packages.
# Find the latest linux-x64-shared release at
# https://github.com/k2-fsa/sherpa-onnx/releases and pass its URL:
tools/install-sherpa-onnx-lib.sh <sherpa-onnx-...-linux-x64-shared.tar.bz2 URL>sudo modprobe uinput
echo "uinput" | sudo tee /etc/modules-load.d/uinput.conf # optional, persist
sudo usermod -a -G input $USER # then log out/inSee MODELS.md. Example (a fast, high-quality English offline model):
tools/install-parakeet-model.sh # Parakeet TDT 0.6B into models/sherpa-onnx/cd src
./talkie.shThe GUI window appears. Only one instance runs at a time; additional launches raise the existing window. The startup script configures library paths and pins to P-cores on Intel hybrid CPUs.
./talkie.sh start # Enable transcription (and mute audio if slim available)
./talkie.sh stop # Disable transcription (and unmute audio)
./talkie.sh toggle # Toggle transcription state
./talkie.sh state # Display current state as JSON
./talkie.sh --help # Show helpecho '{"transcribing": true}' > ~/.talkie # Start
echo '{"transcribing": false}' > ~/.talkie # StopThe application monitors this file and updates state within 500ms.
During transcription, speak these commands to insert punctuation and symbols. Defined in talkie.map, processed by textproc.tcl.
Sentence endings (end of utterance only): "period" → ., "question mark" → ?, "exclamation mark"/"exclamation point" → !
Line breaks (end of utterance only): "new line"/"newline" → \n, "new paragraph" → \n\n
Mid-sentence: "comma" → ,, "colon" → :, "semicolon" → ;, "ellipsis" → ...
Connectors (no surrounding space): "hyphen"/"dash" → -, "apostrophe" → '
Quotes/brackets: "open/close quote" → ", "open/close paren" → ( )
Symbols: "at sign" @, "hashtag"/"pound sign" #, "dollar sign" $, "asterisk" *, "slash" /, "underscore" _, "ampersand" &, "percent" %, "plus sign" +, "equals" =, "less than" <, "greater than" >
Configuration file: $XDG_CONFIG_HOME/talkie.conf or ~/.talkie.conf (JSON, auto-saved on change).
{
"speech_engine": "sherpa-onnx",
"sherpa_modelfile": "sherpa-onnx-moonshine-tiny-en-int8",
"sherpa_num_threads": 4,
"vosk_modelfile": "vosk-model-en-us-0.22-lgraph",
"input_device": "default",
"vad_engine": "threshold",
"vad_device": "CPU",
"vad_threshold": 0.5,
"vad_end_threshold": 0.35,
"audio_threshold": 25.0,
"silence_seconds": 0.3,
"partial_stable_seconds": 0.6,
"min_duration": 0.30,
"lookback_seconds": 0.5,
"spike_suppression_seconds": 0.3,
"confidence_threshold": 100,
"typing_delay_ms": 5
}- speech_engine:
"vosk"or"sherpa-onnx". - sherpa_modelfile: model directory under
models/sherpa-onnx/; the kind (streaming/offline/CTC/whisper/...) is auto-detected. - sherpa_num_threads: CPU threads for sherpa inference (default 4). Strongly affects offline decode latency on multi-core machines.
- vad_engine:
"threshold"(energy) or"silero". vad_device:CPU/NPU(Silero only). vad_threshold / vad_end_threshold: Silero Schmitt-trigger thresholds. - audio_threshold: energy-VAD threshold. silence_seconds: silence before finalizing. partial_stable_seconds: finalize a segment when a non-empty partial stays unchanged this long (external-endpoint engines;
<= 0disables). - confidence_threshold: utterance-level confidence filter (Vosk provides it; models without confidence pass through).
- lookback_seconds, spike_suppression_seconds, min_duration, typing_delay_ms: as named.
All parameters can be adjusted via the GUI or by editing the config file. Changes take effect immediately via variable traces; engine and model changes hot-swap without restarting.
Talkie logs to ~/.config/talkie/feedback.jsonl (JSON Lines) for analysis.
| Type | Description | Fields |
|---|---|---|
inject |
Text sent to uinput | text |
- Sample Rate: device rate (e.g. 44.1kHz), resampled as needed
- Chunk Size: ~25ms; Callback Rate: 40Hz on the audio worker
- VAD: energy threshold or Silero (with Schmitt-trigger hysteresis)
- Lookback: configurable pre-speech buffering (default 0.5s)
- Backlog protection: stale chunks (>500ms) dropped to recover from suspend/idle
- Streaming Zipformer: real-time partials, no post-speech wait.
- Moonshine tiny.en: ~0.18s to decode a ~7s utterance (RTF ~0.03); excellent English quality.
- Parakeet TDT 0.6B: ~0.6s for a ~7s utterance at 4 threads (RTF ~0.09); highest English accuracy.
- Offline decode scales with utterance length and
sherpa_num_threads.
cd src && make build # all critcl packages
cd src/sherpa && make test # sherpa binding tests (needs the C lib + models)
cd src/tests && tclsh all_tests.tcl- Add an entry to
engine_registryinsrc/engine.tcl. - Add a critcl package under
src/and astt::createbranch insrc/stt.tcl.
Wire its config struct + a create_offline_*_recognizer command in src/sherpa/sherpa.tcl, add a detect_kind name marker + a load_*_model wrapper + a load_auto case in src/sherpa/sherpa_procs.tcl.
Edit talkie.map:
"spoken phrase" "output" [attachment]
Attachment: < (attach left), > (attach right), <> (both), omit for normal spacing. Append $ to match end-of-utterance only.
Verify input group membership (groups | grep input) after logout/in, and load the module (sudo modprobe uinput). On Void Linux: make fix-uinput (temporary) or make install-uinput-service (permanent).
List devices and update the config: pactl list sources short (PulseAudio).
Verify the model directory in the config matches an actual directory under models/. A failed engine no longer crashes startup — the GUI comes up with a warning so you can pick a working engine in Settings.
The prebuilt sherpa-onnx library exposes CPU/CUDA/CoreML, not an Intel-NPU path for ASR. The VAD "Device" (CPU/NPU) option applies to Silero VAD only, not the STT model.
MIT
