Transcripts came back short, but the log showed the whole clip going up: captured samples matched wall clock and upload duration matched the recording minus exactly the fixed 150 ms head trim. So nothing was being cut in transit — the losses had to be at the edges or inside the audio, and neither was visible. Trim the stream-open click by measurement instead of by the clock: cut only the leading frames that are genuinely louder than the rest of the clip, up to the old cap. Mics that do not click now keep their first syllable. Measure each frame against its own mean so a steady DC bias — or a stream frozen on one value — reads as the silence it is, and report the longest run of digital silence as dead=. A live mic never goes below DEAD_AIR_DBFS, so anything there is input that never arrived, which is what following the system default source invites. Warn on startup when no device is pinned, log the noise floor next to the speech level, and log how much of the clip the output limiter had to clamp. BLURT_SAVE_CAPTURES keeps the raw and uploaded WAVs per clip, so the next short transcript can be checked against what the API actually got. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
3.6 KiB
Markdown
85 lines
3.6 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.
|
|
|
|
Nothing else is cut: the whole 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.)
|