Horseflow
Horseflow is self-hosted push-to-talk dictation. It records while a global
hotkey is held, transcribes locally with Whisper, cleans punctuation and false
starts with a local LLM, then pastes the result into the focused application.
- Local speech recognition with
faster-whisper
- Local cleanup through Ollama
- Linux client: hold Ctrl+Space
- macOS client: hold Command+Space
- The hotkey's Space event is swallowed before the focused application sees it
- No cloud API or account required
Architecture
keyboard hotkey -> local recording -> Horseflow API
|-> Whisper large-v3
`-> Ollama cleanup model
-> clipboard -> synthetic paste
The server and clients may run on the same machine or communicate over a
private network such as Tailscale.
Server
Requirements
- Linux host with Docker Compose
- NVIDIA GPU and NVIDIA Container Toolkit
- Approximately 11 GB VRAM for Whisper large-v3 and an 8B/9B quantized LLM
Configure
cd deploy
cp .env.example .env
nvidia-smi --query-gpu=uuid,name --format=csv
Edit .env. Set:
HORSEFLOW_GPU to the selected GPU UUID
HORSEFLOW_BIND_ADDRESS to 127.0.0.1
- model storage paths
- custom vocabulary and ASR context
Start
docker compose up -d ollama
docker compose exec ollama ollama pull qwen3:8b
docker compose up -d --build api
curl http://127.0.0.1:8100/health
The API documentation is available at /docs.
Run the server and clients on different machines
Use Tailscale Serve to expose the localhost-bound API as a stable, tailnet-only
HTTPS endpoint. Linux and macOS clients can use the same endpoint:
https://horseflow-server.example-tailnet.ts.net/dictate
Follow the complete cross-machine Tailscale setup. Do not
bind Horseflow publicly or use Tailscale Funnel.
Linux client
The Linux client exclusively grabs physical keyboards through evdev, forwards
normal input through a virtual keyboard, and swallows Ctrl+Space. It records
through PipeWire and pastes through ydotool.
Requirements
- Python 3.10+
pw-record
wl-copy
ydotool
notify-send
- Permission to read
/dev/input/event* and write /dev/uinput
On many distributions, the user must belong to the input and