# 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). To use [Microsoft MAI-Transcribe 2](https://openrouter.ai/microsoft/mai-transcribe-2), select **OpenRouter: MAI-Transcribe 2** from the tray menu or set `BLURT_TRANSCRIPTION_MODEL=microsoft/mai-transcribe-2`. Requires an OpenRouter API key. - **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. By default, Blurt transcribes while you speak. It conditions the incoming audio, then seals and uploads phrases as pauses provide natural boundaries. A chunk is at least about 8 seconds long; early cuts require roughly 800 ms of quiet, while the required pause falls towards 500 ms as the chunk approaches the 20-second target. After 45 seconds it uses the quietest recent point even without a full pause, so one uninterrupted passage cannot hold up the whole recording. Pause detection follows the microphone's noise floor and only skips near-digital silence; ordinary quiet speech and the silence around phrase boundaries remain in the audio. Up to three chunks are transcribed concurrently. Their results are retained by position and delivered once, in recording order, after you stop and the final chunk finishes. Starting another recording does not mix the two sessions, and each session keeps the model, language, provider, and API key selection it had when recording began even if you change the tray settings meanwhile. The tray icon shows progress as chunks finish. While recording, the red icon brightens and its three level bars rise with microphone volume, settling back down during pauses. This shows incoming audio activity; it does not indicate that the transcription service has recognized words. Each session has a recovery directory under `spool/` (gitignored). Sealed chunks, completed text, and a checkpoint of the unsealed tail are kept there until the complete transcript reaches the clipboard or agent. The tail is checkpointed about once a second; an abrupt crash can lose audio since the last checkpoint. **Retry failed transcriptions (N)** re-uploads unfinished chunks and reconstructs the saved recording, including text that succeeded before a network error, timeout, server error, or empty response. A partial result is copied with ` […] ` and retained for retry; retried results are copied rather than dispatched to an agent. The upload timeout scales with each chunk: 30 seconds plus 30 seconds per minute of audio, capped at five minutes. Set `BLURT_STREAMING=0` before starting Blurt to use the legacy whole-clip flow. That mode trims up to 150 ms of stream-open click, filters DC bias, normalises towards -20 dBFS, and rejects near-silent clips. It spools the complete clip before upload, splits recordings longer than five minutes near pauses into roughly four-minute parallel uploads, retains retryable failures, and marks missing chunks with ` […] `. Rejected requests (4xx other than 408 and 429) are discarded because retrying the same request cannot fix them. If a transcript comes back missing words, check `blurt.log`; streaming mode logs chunk boundaries, noise floor, and `dead=`, the longest run of digital silence per chunk, plus the time from Stop to delivery. A non-zero `dead=` can indicate an input dropout. Pinning an input device avoids the system default moving between sources. In legacy mode, set `BLURT_SAVE_CAPTURES=1` (or to a directory) to retain raw and uploaded WAVs and hear exactly what the API received. ## 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.)