A long clip used to die on a fixed 30 s timeout and the audio went with it. Now every conditioned clip is written to spool/ before upload and only removed once its text is on the clipboard; failures keep it, and a tray item retries spooled clips (copied together, never dispatched). Requests the API rejects outright (4xx other than 408/429) are not kept. Timeouts scale with the audio (30 s + 30 s/min, capped at 5 min). Clips over 5 min are cut inside pauses into ~4 min chunks uploaded three at a time and stitched in order; failed chunks are spooled individually and the partial transcript is copied with a gap marker. The tray disc fills clockwise as chunks return. mic_check.py passes the new timeout tuple. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016rLFNPygDpRkRwPQakdmYh
104 lines
4.9 KiB
Markdown
104 lines
4.9 KiB
Markdown
# Blurt
|
|
|
|
Tray-based push-to-talk voice-to-text for Linux and Windows. Toggle recording
|
|
with a hotkey, speak, toggle off — Blurt transcribes the clip and copies the
|
|
text to your clipboard.
|
|
|
|
If a transcript starts with `to my agent`, the rest is dispatched as an agent
|
|
task instead of copied.
|
|
|
|
## Setup
|
|
|
|
Requires [uv](https://docs.astral.sh/uv/) and Python 3.11+.
|
|
|
|
```sh
|
|
uv sync
|
|
```
|
|
|
|
### API key
|
|
|
|
Blurt reads the key for its current model's provider in this order:
|
|
environment variable → OS keyring → local file.
|
|
|
|
| Provider | Env var | Keyring (`service` / `user`) | File |
|
|
|------------|----------------------|---------------------------------|----------------------|
|
|
| OpenAI | `OPENAI_API_KEY` | `openai-api-key` / `felixm` | `api_key.txt` |
|
|
| OpenRouter | `OPENROUTER_API_KEY` | `openrouter-api-key` / `felixm` | `openrouter_key.txt` |
|
|
|
|
Key files and runtime config stay local (gitignored).
|
|
|
|
## Running
|
|
|
|
**Linux** — `uv run blurt` starts the tray icon. Bind a hotkey to
|
|
`./blurt-toggle`, which signals the running instance to start/stop recording.
|
|
|
|
**Windows** — launch `blurt.vbs` (runs Blurt with no console window). The
|
|
global hotkey is **F20**.
|
|
|
|
## Configuration
|
|
|
|
Set from the tray menu, or via files / env vars:
|
|
|
|
- **Model** — `model.txt` / `BLURT_TRANSCRIPTION_MODEL`. For lowest latency use
|
|
`openai/whisper-large-v3-turbo` (OpenRouter, served by Groq).
|
|
- **Language** — `language.txt` / `BLURT_TRANSCRIPTION_LANGUAGE` (`en`, `de`).
|
|
- **Input device** — tray menu (persisted by name in `device.txt` /
|
|
`BLURT_INPUT_DEVICE`). Pick an explicit mic: PortAudio device indices shift
|
|
when USB gear comes and goes, and the OS default source can change under you.
|
|
The menu hides playback endpoints and raw ALSA `hw:` devices that the sound
|
|
server already owns; `BLURT_INPUT_DEVICE` can still name one directly.
|
|
Devices that refuse 16 kHz (raw ALSA and PipeWire's JACK endpoints do) are
|
|
captured at their native rate and resampled.
|
|
|
|
Every clip is conditioned before upload — the stream-open click is trimmed off
|
|
the front (only as much of it as is actually there, up to 150 ms), DC bias is
|
|
filtered out, and the level is normalised towards -20 dBFS. Near-silent clips
|
|
are refused instead of being sent, since transcription models answer silence
|
|
with confident hallucinations.
|
|
|
|
Every clip is written to `spool/` (gitignored) before it is uploaded, and
|
|
deleted again as soon as its text reaches the clipboard. If transcription fails —
|
|
timeout, connection error, server error, empty response — the WAV stays there
|
|
and the notification says so; **Retry failed transcriptions (N)** in the tray
|
|
menu re-uploads the spooled clips oldest first, so a network hiccup never costs
|
|
you the words. A rejected request (any 4xx other than 408 and 429) is the
|
|
exception: the same audio can only be rejected again, so it is not kept, and the
|
|
notification says that too. Retried transcripts are copied to the clipboard
|
|
together, oldest first, once the whole run is done — and never dispatched to an
|
|
agent, since a spooled clip may be one part of a longer recording. The upload
|
|
timeout scales with the clip: 30 s plus 30 s per minute of audio, capped at
|
|
5 minutes. Clips longer than 5 minutes are split into chunks of about
|
|
4 minutes — each cut lands in a pause, not mid-word — and the chunks are
|
|
uploaded in parallel and stitched back together in order. The tray icon fills
|
|
clockwise as each chunk comes back, so you can see how far along the clip is.
|
|
If only some chunks come back, the transcript is copied with ` […] ` where the
|
|
gaps are, and just the failed chunks are kept in the spool as `…-partNN.wav`
|
|
for the retry.
|
|
|
|
Nothing else is cut: every second of the recording is uploaded. If a transcript comes
|
|
back missing words, check `blurt.log` — each clip logs its length, speech level,
|
|
noise floor, and `dead=`, the longest run of digital silence inside it. A
|
|
non-zero `dead=` means the input dropped out mid-clip, which is what following
|
|
the system default source invites; pin a device instead. Set
|
|
`BLURT_SAVE_CAPTURES=1` (or to a directory) to keep the raw and uploaded WAVs
|
|
for every clip and hear exactly what the API got.
|
|
|
|
## Checking the microphone
|
|
|
|
`uv run mic_check.py [device] [seconds]` records from Blurt's configured input,
|
|
prints levels before and after conditioning, saves the WAV it would have
|
|
uploaded, and transcribes it with the current model. Use it when transcripts
|
|
come back wrong: bad audio shows up as a DC bias, clipping, or a speech level
|
|
far below -30 dBFS.
|
|
|
|
Logs are written to `blurt.log` in your temp directory (openable from the tray
|
|
menu).
|
|
|
|
## Moving to another machine
|
|
|
|
`uv run export.py` writes a zip of just the source (no 200 MB+ `.venv`, no
|
|
caches, no key files) to your temp directory. It's pure Python, so it runs the
|
|
same on Linux and Windows — no shell needed — and it only reads, so it's safe
|
|
while Blurt is recording. Unzip on the other machine and `uv sync` to rebuild
|
|
`.venv` for that OS. (Or simply `git clone` the repo fresh.)
|