stt · azstt · oaistt · Rust · one core, two providers
stt
stt is a speech-to-text command and daemon. Give it a recording and it prints the transcript; run it with no arguments and it listens to the microphone, printing each sentence as you finish it. The same core works with Azure's MAI transcription and any OpenAI-compatible endpoint, in batch or over realtime sessions, and the daemon lets a machine without keys transcribe through one that has them.
stt meeting.m4a # any ffmpeg-readable file → one line
stt # microphone → live lines
stt --mic --partials # the line in progress on stderr
stt --stream lecture.wav --json # NDJSON events as they happen
stt --remote studio # microphone here, keys there
Batch or live
What you give stt decides how it works: a file is a batch request by default, the microphone is always a live session, and --stream makes a file behave like the microphone. Output is only ever the transcript, or JSON you asked for, on stdout; partials, errors and diagnostics go to stderr and never contain keys, audio or response bodies.
stt --print-request --provider openai
{"content_type":"multipart/form-data","fields":{"model":"gpt-4o-mini-transcribe","response_format":"json"},
"file":"audio.wav (audio/wav)","method":"POST","url":"https://api.openai.com/v1/audio/transcriptions"}
--print-request shows exactly what would be sent, with no key, no audio and no network.
Turn detection
Deciding when someone has finished a sentence is the hardest part of live transcription, and stt deliberately doesn't do it. It asks the provider's server-side voice activity detection to cut speech into turns, tunable by threshold, silence and prefix length, and prints each turn when the provider commits it. It never runs its own VAD and never fakes streaming by chopping audio into batch requests.
stt --print-request --provider openai --stream
{"url":"wss://api.openai.com/v1/realtime?model=gpt-realtime-whisper&intent=transcription",
"rate":24000,"model":"gpt-realtime-whisper","auth_header":"authorization",
"session_update":{"type":"session.update","session":{"type":"transcription","audio":{"input":{
"format":{"type":"audio/pcm","rate":24000},"noise_reduction":null,
"transcription":{"model":"gpt-realtime-whisper"},"turn_detection":{"type":"server_vad"}}}}}}
Some realtime models, gpt-realtime-whisper and mai-transcribe-2-streaming among them, reject turn detection. For those, stt falls back before sending any audio to a single commit at the end of input; they still stream text while you talk. --vad server insists on detection, semantic asks for semantic turns, and none always commits once.
Providers
| OpenAI-compatible (oaistt) | Azure MAI (azstt) | |
|---|---|---|
| Batch | multipart POST …/v1/audio/transcriptions | JSON POST …/mai/v1/audio/transcriptions, WAV as a data URL |
| Batch model | gpt-4o-mini-transcribe | mai-transcribe-2 (a deployment name) |
| Realtime | …/v1/realtime?model=M&intent=transcription | …/mai/v1/realtime?intent=transcription |
| Realtime model | gpt-realtime-whisper | mai-transcribe-2-streaming |
| Rate | 24 kHz; 16 kHz for mai-transcribe* through a proxy | 16 kHz |
| Credentials | Bearer OPENAI_API_KEY | api-key, or a Microsoft Entra token |
Unsupported options fail instead of disappearing: MAI has no prompt field, so --prompt with Azure is an error, not a no-op. Unpinned, stt tries Azure and then OpenAI; batch fallback tries each configured provider once within the deadline, and streaming fallback only covers connecting before any audio. An explicitly chosen provider never falls back.
Remote transcription
stt daemon holds the provider keys; clients need its URL and a bearer token. Batch requests carry an ID, so a lost reply is recovered by asking for the receipt (kept for 120 seconds) rather than paying to transcribe the same audio twice. Live sessions are relayed with event cursors.
- Bounded. At most eight streams by default, 60 seconds idle and an hour in total per session, 4096 retained events, 1 MiB per append and a 48 MiB batch body.
- Cancellable.
stt.transcribe.cancelis idempotent; a waiting request returnscancelledat once and a late provider result is discarded, though an upstream call already made may still be billed. - Shared plumbing. The daemon reuses the TTS family's endpoint, token and remote helpers, and the always-on lifetail transcriber reuses this core's realtime sessions.
- A NixOS module.
nixosModules.sttruns the user unitstt-daemon; keys load as systemd credentials, and literal keys in the module environment are rejected.