Files
blurt/mic_check.py
felixmandClaude Fable 5.1 82bc350521 Keep long recordings: spool, retry, chunked upload, progress ring
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
2026-09-05 19:03:50 -04:00

102 lines
3.7 KiB
Python

"""Diagnose Blurt's capture path: record, measure, transcribe, save the WAV.
Usage: uv run mic_check.py [device] [seconds]
device is a sounddevice index or name substring; omitted = whatever
Blurt itself is configured to use (device.txt, else system default).
"""
import io
import sys
import tempfile
from pathlib import Path
import numpy as np
import sounddevice as sd
from scipy.io import wavfile
import blurt
OUT_WAV = Path(tempfile.gettempdir()) / "blurt_mic_check.wav"
def report(label: str, samples: np.ndarray) -> None:
s = blurt.audio_stats(samples)
frame = blurt.SAMPLE_RATE // 50
energy = np.array([
np.sqrt(np.mean(np.square(samples[i:i + frame])) + 1e-20)
for i in range(0, max(len(samples) - frame, 1), frame)
])
snr = 20 * np.log10(np.percentile(energy, 90) / (np.percentile(energy, 10) + 1e-12))
print(f" {label}")
print(f" rms {s['rms_dbfs']:+7.1f} dBFS (speech wants -30..-15)")
print(f" peak {s['peak_dbfs']:+7.1f} dBFS")
print(f" dc bias {s['dc']:+7.4f} (wants ~0.0000)")
print(f" clipped {s['clipped_pct']:7.3f} %")
print(f" loud/quiet spread {snr:.1f} dB (real speech wants > 25)")
def main() -> None:
args = sys.argv[1:]
device = blurt.load_input_device_name() or None
if args and args[0]:
try:
device = int(args[0])
except ValueError:
device = args[0]
seconds = float(args[1]) if len(args) > 1 else 6.0
print("Input devices:")
for i, d in enumerate(sd.query_devices()):
if d["max_input_channels"] > 0:
print(f" {i:>3} {d['name']}")
resolved = sd.query_devices(device, "input") if device else sd.query_devices(kind="input")
rate, channels = blurt.input_stream_settings(device)
print(f"\nRecording {seconds:g}s from: {resolved['name']} at {rate} Hz / {channels} ch — speak now.")
frames = sd.rec(
int(seconds * rate),
samplerate=rate,
channels=channels,
dtype=blurt.DTYPE,
device=device,
)
sd.wait()
audio = frames.mean(axis=1) if channels > 1 else frames[:, 0]
audio = blurt.resample_to_target(audio, rate)
click = blurt.leading_click_samples(audio)
audio = audio[click:]
print(f"\nCapture (trimmed {click * 1000 / blurt.SAMPLE_RATE:.0f} ms of stream-open click):")
report("raw", audio)
conditioned = blurt.condition_audio(audio)
report("after Blurt conditioning", conditioned)
buffer = io.BytesIO()
wavfile.write(buffer, blurt.SAMPLE_RATE, (conditioned * 32767).astype(np.int16))
wav_bytes = buffer.getvalue()
OUT_WAV.write_bytes(wav_bytes)
print(f"\nWAV saved: {OUT_WAV} ({len(wav_bytes)} bytes) — play it to hear what the API hears")
app = blurt.BlurtApp.__new__(blurt.BlurtApp)
app.api_keys = {"openai": blurt.load_api_key(), "openrouter": blurt.load_openrouter_key()}
app.transcription_model = blurt.load_transcription_model()
app.transcription_language = blurt.load_transcription_language()
app.session = blurt.requests.Session()
provider = app._provider_for_model(app.transcription_model)
if not app.api_keys.get(provider):
print(f"\nNo {provider} key available; skipping transcription.")
return
print(f"\nTranscribing with {app.transcription_model} ({app.transcription_language})…")
timeout = blurt.transcription_timeout(len(conditioned) / blurt.SAMPLE_RATE)
result = (
app._transcribe_openrouter(wav_bytes, timeout)
if provider == "openrouter"
else app._transcribe_openai(wav_bytes, timeout)
)
print("\n--- transcript ---")
print(result.get("text", result))
if __name__ == "__main__":
main()