A local, low-latency push-to-talk dictation tool for Linux (Wayland). Hold a hotkey, speak, release — your words come back punctuated and on the clipboard, ready to paste. Everything runs on-device: no cloud, no telemetry, no subscription.
hold Ctrl + E ──► record ──► Parakeet STT ──► Ollama punctuation ──► clipboard
release │
paste anywhere with Ctrl + V
- Sub-second transcription — NVIDIA Parakeet TDT 0.6B v3 via NeMo, warmed and
resident on GPU; audio is buffered in RAM (
/dev/shm) so no disk I/O touches the hot path. - Automatic punctuation & casing — a small local LLM (via Ollama) restores
punctuation without ever changing a word (temperature
0.0, verbatim prompt). - Wayland-native hotkeys — reads
/dev/inputwithevdev, sidestepping Wayland's global-shortcut restrictions entirely (no X11, no XGrabKey hacks). - Hot-plug resilient — monitors multiple keyboards, rescans every 5 s, and survives devices being unplugged mid-session.
- Runs anywhere — native (venv + scripts) or fully containerized (Docker + NVIDIA runtime), with a desktop launcher and a systemd user service.
- Fits an 8 GB VRAM budget — measured end-to-end within the target on a GTX 1070 (see Resource budget).
- How it works
- Resource & VRAM budget
- Requirements
- Quick start
- Usage
- Configuration
- Docker
- Development
- Troubleshooting
- Project structure
- License
[ User presses & holds Ctrl + E ]
│
▼
[ sounddevice capture ]
16 kHz mono float32 frames pushed to an in-memory queue
│
[ User releases Ctrl + E ] capture stop is atomic (single lock);
│ transcription runs on a worker thread
▼
[ Parakeet TDT 0.6B v3 ] ──► raw text (NeMo ASR, CUDA)
│
▼
[ Ollama · bonsai-2:2b ] ──► punctuated, capitalized text
│ (words never altered)
▼
[ system clipboard ] pyperclip / wl-clipboard on Wayland
│
▼
[ Paste anywhere with Ctrl + V ]
Design notes:
/dev/shmfor audio — NeMo's file-basedtranscribe()API is handed a WAV on the RAM disk, so transcription latency stays near zero.- Atomic capture state —
start/stopof the microphone happen under one lock; only the slow STT/LLM work is pushed to a daemon thread, so a quick re-press can't interleave with teardown. - Graceful degradation — if Ollama is down, Dictator falls back to the raw (unpunctuated) transcript instead of failing.
Target ceiling: 8 GB.
| Component | VRAM footprint | Notes |
|---|---|---|
| NVIDIA driver & Wayland display | ~0.5 – 0.8 GB | Base desktop overhead |
| Parakeet TDT 0.6B v3 | ~1.5 – 2.0 GB | RNN-T / TDT acoustic model (CUDA) |
Ollama · bonsai-2:2b |
~1.5 – 2.0 GB | num_ctx: 1024 to keep KV cache small |
| Headroom | ~3.0 GB | Absorbs fragmentation, avoids CUDA OOM |
Measured on a GTX 1070 (8 GB): Dictator's process peaks around 4.9 GB during transcription, leaving ample headroom alongside a desktop session.
- OS: Linux with a Wayland session (X11 is not supported — hotkeys use
/dev/input). - GPU: NVIDIA GPU + driver (CUDA). CPU-only works but is slower.
- Python: 3.10+.
- Ollama: running locally, with the punctuation model pulled.
- Packages: PortAudio, wl-clipboard, and access to the
inputgroup.
The one-shot installer handles system packages, the venv, Python dependencies, the Ollama model, group membership, and the desktop launcher:
git clone https://github.com/Nick040791/Dictator.git
cd Dictator
./install.shAfter install: if you were added to the
inputgroup, log out and back in before running Dictator.
Step-by-step (equivalent to ./install.sh)
1. System packages (Ubuntu / Debian):
sudo apt update
sudo apt install -y libportaudio2 portaudio19-dev wl-clipboard python3-venv2. Python environment (CUDA-enabled PyTorch):
python3 -m venv ~/dictator-env
source ~/dictator-env/bin/activate
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
pip install -r requirements.txt
pip install "nemo_toolkit[asr]"3. Ollama punctuation model:
ollama serve &
ollama pull bonsai-2:2b4. Keyboard device access (Wayland):
sudo usermod -aG input "$USER"
# log out and back in for the group change to take effectsource ~/dictator-env/bin/activate
python dictator.pyHold Ctrl + E to record; release to transcribe, punctuate, and copy. Paste with Ctrl + V.
./scripts/start.sh # start (no-op if already running)
./scripts/stop.sh # stop
./scripts/restart.sh # restart
./scripts/toggle.sh # start or stop, depending on statestart.sh launches Dictator as a systemd user service (so it survives the
launcher exiting) and writes logs to ~/.local/share/dictator/dictator.log.
install.sh installs a .desktop entry. You can also install it manually —
the repo ships dictator.desktop with an @PROJECT_DIR@ placeholder that
install.sh resolves to your checkout:
./install.sh # resolves @PROJECT_DIR@ and installs the launcherThe icon toggles the service (start if stopped, stop if running); right-click exposes Start / Stop / Restart.
Every setting can be overridden by environment variable or CLI flag, so the same image runs across machines with no code edits.
| Env var | CLI flag | Default | Purpose |
|---|---|---|---|
SAMPLE_RATE |
--sample-rate |
16000 |
Capture sample rate (Hz) |
OLLAMA_URL |
--ollama-url |
http://localhost:11434/api/generate |
Ollama generate endpoint |
OLLAMA_MODEL |
--ollama-model |
bonsai-2:2b |
Ollama punctuation model |
DICTATOR_ASR_MODEL |
--asr-model |
nvidia/parakeet-tdt-0.6b-v3 |
NeMo/HF ASR model id |
DICTATOR_HOTKEY |
--hotkey |
ctrl+e |
Push-to-talk hotkey |
DICTATOR_TEMP_WAV |
— | /dev/shm/dictator_temp.wav |
RAM-disk temp file for STT |
# Example: use a different hotkey and punctuation model
python dictator.py --hotkey "ctrl+shift+space" --ollama-model bonsai-2:27bRun python dictator.py --help for the full reference.
An NVIDIA Container Toolkit setup is included. The compose file passes through
/dev/snd (mic), /dev/input (hotkeys), the Wayland socket (clipboard), and
/dev/shm (audio buffer), and sets the same env vars shown above.
docker compose up --build -d
docker compose logs -fThe compose file's
OLLAMA_*env vars are now actually consumed bydictator.py(they used to be ignored).
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
pytest # 22 unit tests, no GPU/model required (< 0.1 s)
ruff check . # lint
ruff format . # formatThe ASR model loads lazily on first use, so importing dictator is fast and
side-effect free — unit tests run without touching CUDA or the network.
CI (GitHub Actions) runs lint, tests, shell syntax checks, and Dockerfile lint on every push and PR.
- "No readable keyboard devices found" — your user isn't in the
inputgroup, or you haven't logged out/back in after adding it. - Ollama errors / fallback to raw transcript — ensure
ollama serveis running and the model is pulled (ollama pull bonsai-2:2b). - No audio captured — check mic permissions and that
libportaudio2is installed (pactl list sources shortto confirm a default source). - CUDA out of memory — make sure no other model process is holding VRAM
(
nvidia-smi); Dictator targets < 5 GB for its own process. - Logs —
tail -f ~/.local/share/dictator/dictator.log
Dictator/
├── dictator.py # main app: capture → STT → punctuation → clipboard
├── requirements.txt # lightweight runtime deps (torch/NeMo installed separately)
├── requirements-dev.txt # + pytest, ruff
├── install.sh # one-shot installer
├── scripts/ # start / stop / restart / toggle (systemd user service)
├── dictator.desktop # desktop launcher template (@PROJECT_DIR@ placeholder)
├── Dockerfile # CUDA runtime image
├── docker-compose.yaml # device/socket passthrough + env config
├── tests/ # unit tests (no GPU required)
└── .github/workflows/ # CI pipeline
MIT — see LICENSE.