- CSharp 75%
- Markdown 25%
vttcli
A terminal-based voice-to-text transcriber for Linux. Records your microphone and transcribes speech to text in real time using a local Whisper model — no API keys, no cloud, no internet required after setup.
Requirements
| Local | Docker | |
|---|---|---|
| .NET | 10.0 SDK | Included in image |
| Audio | arecord (ALSA) + microphone | --device /dev/snd |
| Model | ggml-tiny.bin (~75 MB) | Downloaded during build |
| OS | Linux | Linux |
Quick start
Local
wget https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin
dotnet run --project src/VttCli ./ggml-tiny.bin
Docker
docker build -t vttcli .
docker run --device /dev/snd -it vttcli
The --device /dev/snd flag passes your host's sound card into the container. Without it, arecord has nothing to read from.
Controls
| Key | Action |
|---|---|
Space | Toggle recording on/off |
S | Save transcript to transcript_<timestamp>.txt |
Q | Quit |
Architecture
┌──────────┐ raw PCM ┌──────────────┐ float[] ┌──────────────┐
│ arecord │──────────────▶│ AudioRecorder │─────────────▶│ Transcriber │
│ (ALSA) │ 16kHz/16/1 │ (pipe→buf) │ 32-bit PCM │ (whisper.net)│
└──────────┘ └──────────────┘ └──────┬───────┘
│ segments
▼
┌──────────────┐
│ Terminal.Gui │
│ TUI │
└──────────────┘
Real-time streaming
Transcription uses a sliding window approach:
arecordstreams raw PCM to stdout, which .NET reads in 4 KB chunks- Chunks accumulate in a thread-safe buffer
- Every 3 seconds, the last 5 seconds of audio (overlapping window) is sent to Whisper
- Recognized segments are appended to the TUI transcript view
This gives ~3 seconds of perceived latency. The overlapping window prevents cutting words at chunk boundaries.
Audio format
All audio is captured as 16 kHz, 16-bit signed little-endian, mono — the native format Whisper expects. Raw bytes are converted to float[] normalized to [-1.0, 1.0] before being passed to the processor.
Model selection
The default model is ggml-tiny.bin (~75 MB). You can swap it for a larger model for better accuracy:
| Model | Size | Relative speed | Notes |
|---|---|---|---|
ggml-tiny.bin | 75 MB | Fastest | Default. Good for clear speech |
ggml-base.bin | 142 MB | Fast | Better accuracy |
ggml-small.bin | 466 MB | Medium | Good balance |
ggml-medium.bin | 1.5 GB | Slow | High accuracy |
ggml-large.bin | 2.9 GB | Slowest | Best accuracy |
Download from HuggingFace and pass the path as the first argument:
dotnet run --project src/VttCli ./ggml-base.bin
Language
The transcriber defaults to English. To change the language, edit the language parameter in Transcriber.cs:
public Transcriber(string modelPath, string language = "en")
Use ISO 639-1 codes (e.g. "de", "fr", "es", "ja").
Tech stack
- .NET 10 — runtime
- Terminal.Gui v2.4 — TUI framework
- Whisper.net v1.9 — C# bindings for whisper.cpp
- ALSA
arecord— audio capture (shells out, no native .NET audio dependencies)
Project structure
vttcli/
├── vttcli.slnx
├── Dockerfile
├── .dockerignore
└── src/VttCli/
├── VttCli.csproj # NuGet refs: Terminal.Gui, Whisper.net, Whisper.net.Runtime
├── Program.cs # Entry point, TUI layout, key bindings, orchestration
├── AudioRecorder.cs # Spawns arecord, reads PCM from stdout into a buffer
└── Transcriber.cs # Loads Whisper model, processes audio, fires segment events
Troubleshooting
"arecord: command not found" — Install ALSA utils: sudo apt install alsa-utils
No audio / silent recording — Check your default input device with arecord -l. In Docker, ensure --device /dev/snd is set and the host audio system isn't PulseAudio-exclusive (ALSA needs direct device access).
Model not found — The first CLI argument is the model path. Default is ggml-tiny.bin in the current directory.
Slow transcription — The tiny model runs on CPU. If it can't keep up with real-time, try a smaller audio window or switch to a machine with AVX2 support (the native libs auto-detect this).