Hands-free dictation that streams your speech straight into a chosen terminal tab — not just the focused window. Hit Start once and just talk: voice-activity detection picks out your speech and transcribes it live as you go (no key to hold, no per-phrase button). Pin a Terminator tab once and keep working anywhere else while transcribed text flows into that pinned shell (handy for talking to a coding-agent CLI). A PyQt5 tray app drives two interchangeable transcription engines:
- Offline (Vosk) — fully local models, no network, no API key.
- AssemblyAI — Universal-Streaming API for higher accuracy (needs a key).
Linux + X11 only.
Most dictation tools type into whatever window currently has focus. This one can
inject text directly into a specific pseudo-terminal via a small setuid
TIOCSTI helper, so the text lands in the pinned shell no matter which window
you're actually looking at. Unpinned, it falls back to typing into the focused
window with xdotool.
It's also cost-aware for AssemblyAI: that service bills by how long the streaming socket stays open (idle time included), so the engine stays dormant until it hears speech, opens one connection for the talking burst, and closes it after a spell of silence. The GUI shows a live running total of billable connection time.
Right-click the tray icon for a quick-switch menu:
- Show / Hide the control window.
- Start / Stop Listening — toggle transcription without opening the window.
- Pin a terminal — a live, checkable list of every open Terminator terminal. Click one to pin it (it gets a check mark); pick Focused window (no pin) to go back to typing into whatever's focused. The list rebuilds each time you open the menu, so newly-opened tabs show up immediately.
- Quit.
The pin you choose here stays in sync with the Dictate tab in the main window.
| File | Role |
|---|---|
voice_gui.py |
PyQt5 control panel + tray app (the main entry point) |
aai_print.py |
AssemblyAI Universal-Streaming engine (--stream to stdout) |
talk.py |
Vosk dictation engine (type or --print) |
dictate.py |
Standalone continuous Vosk dictation CLI |
recmeter.py |
Mic-test recorder with live level metering |
tiocsti-inject.c |
setuid-root helper that injects bytes into your own /dev/pts/* |
bin/ |
voicegui, talk, dictate launchers |
git clone https://github.com/Gwolfgit/voice-to-terminal.git
cd voice-to-terminal
./install.shinstall.sh installs the system packages, builds the venv, compiles + installs
the setuid injector (asks for sudo), and drops the launchers, icon, and desktop
entry into place. Then do the two manual steps it prints:
- Vosk models (offline engine): download from
https://alphacephei.com/vosk/models and unpack into
~/.local/share/vosk-talk/models/<lang>/(e.g.models/en/). - AssemblyAI key (streaming engine): export
ASSEMBLYAI_API_KEY, or write it to~/.config/agent-dictate/api_key(chmod 600), or paste it in the GUI's API tab. No key is bundled in this repo — supply your own.
Launch with voicegui, or pick Voice → Terminal from your app menu.
- Linux on X11 (keystroke injection uses
xdotool; Wayland won't work). - Terminator for the pin-a-tab feature
(
remotinatorprovides tab titles). Without it, dictation still types into the focused window. - PulseAudio/PipePulse (
parec/pactl) for mic capture. - System
python3+python3-pyqt5for the GUI; a venv for the engines.
The talk launcher can connect a Bluetooth headset and switch it to its
mic-capable HSP/HFP profile before listening. It's off by default; enable it
by exporting your headset's MAC:
export TALK_BT_MAC="AA:BB:CC:DD:EE:FF"Set TALK_NO_BT=1 to force-skip it.
TIOCSTI lets a process push characters into a terminal's input as if typed. The
helper is installed setuid root because the kernel requires elevated
privileges to write into a terminal that isn't the caller's controlling tty. To
contain that, the helper refuses any target that isn't a /dev/pts/* device
owned by the real (invoking) user — so you can only ever inject into your own
terminals. Read tiocsti-inject.c (it's ~60 lines) before installing if that
matters to you. If you'd rather not install it, leave the target unpinned and
the app types into the focused window instead.
MIT — see LICENSE.