fix(captures): clean up audio files when create_capture fails

The create flow wrote raw audio (and a transcoded .wav for non-wav
sources) to data/captures before the DB row was committed, so any
failure between the write and the commit — a webm that decoded to a
0-length array, a whisper model that errored mid-transcribe, a SQLite
contention on the commit — left the audio on disk with nothing pointing
at it. Over enough flaky uploads the directory grows without bound.

Now every path written before the commit is tracked in a list, and the
whole stretch from the first write to db.commit() runs inside a
try/except that unlinks each tracked file on raise and re-raises. The
transcode branch removes the raw file from the cleanup list only when
the unlink actually succeeds, so an OSError on the raw-path delete
still hands cleanup the original blob to retry.
This commit is contained in:
Jamie Pine
2026-04-24 20:37:56 -07:00
parent 800e390108
commit 9525bff28a
+17
View File
@@ -71,7 +71,11 @@ async def create_capture(
suffix = ".wav" suffix = ".wav"
raw_path = config.get_captures_dir() / f"{capture_id}{suffix}" raw_path = config.get_captures_dir() / f"{capture_id}{suffix}"
written_files: list[Path] = []
try:
raw_path.write_bytes(audio_bytes) raw_path.write_bytes(audio_bytes)
written_files.append(raw_path)
# Decode once with librosa — its audioread fallback handles webm/opus # Decode once with librosa — its audioread fallback handles webm/opus
# via ffmpeg, which miniaudio (used inside mlx-audio's whisper) can't. # via ffmpeg, which miniaudio (used inside mlx-audio's whisper) can't.
@@ -105,10 +109,13 @@ async def create_capture(
# regardless of what format the client shipped. # regardless of what format the client shipped.
audio_path = config.get_captures_dir() / f"{capture_id}.wav" audio_path = config.get_captures_dir() / f"{capture_id}.wav"
sf.write(str(audio_path), audio, sr, format="WAV") sf.write(str(audio_path), audio, sr, format="WAV")
written_files.append(audio_path)
try: try:
raw_path.unlink() raw_path.unlink()
except OSError: except OSError:
pass pass
else:
written_files.remove(raw_path)
whisper = get_whisper_model() whisper = get_whisper_model()
resolved_stt = stt_model or whisper.model_size resolved_stt = stt_model or whisper.model_size
@@ -126,6 +133,16 @@ async def create_capture(
db.add(row) db.add(row)
db.commit() db.commit()
db.refresh(row) db.refresh(row)
except Exception:
# Anything between the first write and the commit means the audio on
# disk has no row pointing at it — clean up so data/captures doesn't
# accumulate orphan blobs across failed transcribes.
for path in written_files:
try:
path.unlink()
except OSError:
pass
raise
return _to_response(row) return _to_response(row)