* Phase 2 02-02: add _safe_torchaudio_save + _safe_soundfile_write helpers
Centralizes WAV/audio writes through a single audited path that defends
against the four documented torchaudio.save failure modes (CUDA/MPS
tensor, non-contiguous, out-of-range, wrong dtype) AND the torchaudio
2.9+ TorchCodec-delegation behavior drift.
* services/audio_io.py:_safe_torchaudio_save now performs:
- .cpu() move (torchaudio cannot serialize CUDA/MPS)
- dtype coercion to torch.float32
- .clamp(-1.0, 1.0) (out-of-range = silent clipping on some backends)
- .unsqueeze(0) for 1D (mono) inputs
- .contiguous() (torch.cat of slices = non-contig = silent corruption)
- explicit encoding="PCM_S/PCM_F" + bits_per_sample so future
torchaudio backend selection cannot drift the on-disk format
- format passthrough for wav/flac/mp3/ogg with encoding-kwarg fallback
for older codec builds
* services/audio_io.py:_safe_soundfile_write — sibling helper for the
one sf.write call site (dub_core.py). Applies the same dtype/contig/
range checks before delegating to soundfile.write.
* services/audio_io.py:atomic_save_wav (existing P0 helper) now
delegates the actual encode to _safe_torchaudio_save so atomicity
and correctness compose: every byte that lands at the target path
was produced by the audited helper.
* tests/backend/services/test_audio_io.py — 29 tests (25 pass + 4
skipped for MPS dtype incompatibility): parametric round-trip across
dtype x device x contiguity, plus out-of-range clamp, format
passthrough, in-memory buffer, empty-tensor rejection, 1D auto-
unsqueeze, and a smoke check that atomic_save_wav inherits the
safety guarantees.
No new Python dependencies. SoniTranslate untouched (D1 locked).
Refs BUG-01 / #48.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 2 02-02: migrate router audio writes through audited helpers
Migrates all 12 grep-audit bare audio-write call sites in
backend/api/routers/ to route through services.audio_io. Closes the
last surface area of BUG-01 / #48 that the P0 atomic-write commit
(fb52140) did not cover.
Sites migrated (grep before → after):
generation.py:148 torchaudio.save → _safe_torchaudio_save
generation.py:162 torchaudio.save → _safe_torchaudio_save
openai_compat.py:155 torchaudio.save → _safe_torchaudio_save
openai_compat.py:160 torchaudio.save → _safe_torchaudio_save
openai_compat.py:168 torchaudio.save → _safe_torchaudio_save
openai_compat.py:172 torchaudio.save → _safe_torchaudio_save
openai_compat.py:178 torchaudio.save → _safe_torchaudio_save
openai_compat.py:182 torchaudio.save → _safe_torchaudio_save
openai_compat.py:193 torchaudio.save → _safe_torchaudio_save
dub_generate.py:509 torchaudio.save → _safe_torchaudio_save
batch.py:341 torchaudio.save → atomic_save_wav (track assembly)
dub_core.py:438 sf.write → _safe_soundfile_write
batch.py:341 specifically swapped to atomic_save_wav (not just the safe
helper) because it writes the final track to disk — same shape as
dub_generate.py:390 — and needs atomic publication, not only audited
encoding. atomic_save_wav already delegates internally to
_safe_torchaudio_save (per the Task 1 commit) so it inherits both
guarantees.
openai_compat.py:185 pcm branch produces raw int16 bytes (no
container), so it can't go through _safe_torchaudio_save; it now
inlines the same .cpu/.float32/.clamp/.contiguous sanity steps the
helper enforces.
tests/backend/test_dub_pipeline_wav.py:
- test_no_bare_audio_writes_in_routers (in-process grep gate)
- test_no_bare_audio_writes_via_subprocess_grep (CI-shell parity gate)
- test_track_assembly_handles_non_contig_after_torch_cat (the #48
smoking-gun reproduction — torch.cat of out-of-range non-contig
slices saved through the helper)
- test_atomic_save_wav_assembly_pattern (same shape, via
atomic_save_wav)
- test_safe_soundfile_write_dub_core_pattern (ASR transcribe-chunk
pattern from dub_core.py)
- test_dub_pipeline_produces_valid_wav (xfailed — Phase 0 fixture
sample_5s.mp4 not present; structural reproduction tests above
already cover the helper code path #48 went through)
Grep gate is green:
grep -nE '(torchaudio\.save|soundfile\.write|sf\.write)\(' \
backend/api/routers/ -r --include='*.py' \
| grep -v '_safe_torchaudio_save\|_safe_soundfile_write' \
| grep -v '^[^:]*:[[:space:]]*#' \
returns 0 lines.
Full suite green: 348 passed, 10 skipped, 13 xfailed, 1 xpassed.
SoniTranslate untouched (D1 locked).
Closes BUG-01 / #48.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 2 02-02: add execution summary
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>