Skip to content

[Model][Bug fix] Decode request audio with libsndfile first, so uploads work with a video-only FFmpeg - #639

Merged
merceod merged 2 commits into
mstar-project:mainfrom
arun-elr:fix/load-audio-soundfile-first
Oct 20, 2026
Merged

merceod merged 2 commits into
mstar-project:mainfrom
arun-elr:fix/load-audio-soundfile-first

Conversation

@arun-elr

@arun-elr arun-elr commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #534

What does this PR do?

In the Dynamo base image, torchcodec loads the base's video-only FFmpeg 8 before the full FFmpeg 6.1 the Dockerfile installs, so every audio upload fails to decode ("Invalid data found when processing input"). A host without FFmpeg's shared libraries fails the same way today. Model.load_audio now goes through the shared load_audio_file: libsndfile (soundfile) first, torchcodec for the containers libsndfile cannot open. WAV, FLAC, OGG and MP3 no longer depend on the FFmpeg build.

  • Decode path. load_audio in mstar/model/base.py decodes via load_audio_file: soundfile first, torchcodec second, torchaudio resample, channel mean. [API server][Model][Security] Refuse audio sample rates whose resampling kernel is unbounded #609's sample-rate bound in load_audio_file refuses a crafted header rate before torchaudio builds its kernel, which FFmpeg's resampler never needed on this path.
  • CI. pip install soundfile in CPU Core (.github/workflows/ci.yml): the new tests, and 26 cases in test_media_paths.py, skip without it.
  • Test. test/modular/test_base_load_audio.py: a torchcodec stand-in that fails as FFmpeg 8 does; 4 containers at 16 and 24 kHz; a crafted 100,003 or 48,001 Hz header refused before torchaudio resamples it.

Model.load_audio with a video-only FFmpeg, as in the Dynamo base image: on main WAV, FLAC, OGG Vorbis, MP3, M4A and WebM uploads all fail with Invalid data found; with this PR WAV, FLAC, OGG Vorbis and MP3 decode through libsndfile, and M4A and WebM still go to torchcodec as before

How was it tested?

On b81a833 (head, on main f17b321), CI's install plus soundfile: CPU Core, the ci.yml paths with -n 4: 5,064 passed, 610 skipped, 5 failed: three LingBot-World shell tests that need ftfy and scipy, which CI installs and this venv lacks (they fail the same way on main here), and two signal tests (test_e2e_validators.py, test_eval_framework.py) that fail only under nohup, which this run used, and pass when run directly; test_base_load_audio.py with test_resample_rate_bound.py: 187 passed. Earlier, on 6b0a375 (the head's own diff, on main 422d785), CPU, CI's install plus soundfile: ruff check . is clean. The CPU core list then: 2660 passed, 203 skipped, 2 failed (test_wan_vae_decode.py; the same 2 fail on main in this environment). python -m pytest test/modular/test_base_load_audio.py: 12 passed (11 fail on main, and the 2 crafted-rate cases fail on 2539338, before the rebase onto #609; the torchcodec fallback case passes on main too). Decode against main's torchcodec path (FFmpeg 6.1, 3 s clips): a 16 kHz mono WAV is sample-identical; details in "Decoded audio against main".

Real-user checks on one H200 (served on b733780, the same commits on #609's previous head dec613d): Whisper-large-v3 served from this PR and from main on 115 uploads (WAV in 10 encodings, FLAC, MP3, OGG, M4A, 16 sample rates, crafted and broken headers, 30 min files). With torchcodec hidden, main answers 103 of them with a 500 and this PR serves 94, as main does with FFmpeg; with FFmpeg both serve 95, 84 of 92 transcripts are identical (the rest differ at resampler level), and three of main's 500s for broken WAVs become a 200 or a 400. Before the rebase onto #609, one 320 KB WAV with a 768,001 Hz header pushed the server process to 192 GB until the kernel killed it.

Not tested

The Dynamo image itself (needs an image build); the stand-in fails the way the issue's FFmpeg 8 does, and an H200 without torchcodec stands in for it in the served check.

Not here: OmniVoice's own load_audio still uses torchcodec only. A FLAC whose header leaves the sample count out is refused by libsndfile's whole-file read and still needs FFmpeg (#708). M4A and WebM in the Dynamo image still need the issue's option 1 (keep FFmpeg 8 off the M* stage's loader path).

Merge order with #644

#644 also rewrites Model.load_audio (a torchcodec decode capped at 30 min). Neither PR is stacked on the other: whichever lands second resolves load_audio to load_audio_file, which #644 caps, so the result keeps libsndfile first and the cap.

Decoded audio against main

Synthetic 3 s clips (220 Hz tone plus noise), decoded to 16 kHz mono by main's AudioDecoder(path, sample_rate=16000, num_channels=1) and by load_audio_file(path, 16000); torch 2.12.1, torchcodec 0.17.0, Ubuntu's FFmpeg 6.1, libsndfile 1.2.2.

input difference
WAV, 16 kHz, mono identical (same 48000 samples)
WAV, 24 kHz, mono 35.1 dB SNR (torchaudio's resampler vs FFmpeg's)
WAV, 16 kHz, stereo main is 1.4142x louder: FFmpeg downmixes to (L+R)/sqrt(2), this PR averages, as librosa (librosa.load(mono=True)) and M*'s other decode paths do
OGG Vorbis, 48 kHz / MP3, 24 kHz, mono 38.4 / 34.0 dB SNR (different decoders and resamplers)

Same sample count in every case.

Tests in detail

test_base_load_audio.py swaps in a torchcodec.decoders.AudioDecoder that raises "Could not open input file ... Invalid data found when processing input" and records its calls:

  • WAV, FLAC, OGG Vorbis and MP3, each mono at 16 kHz and stereo at 24 kHz, decode to 16 kHz mono float32 at the right length and level, without a torchcodec call (8 cases).
  • A 16 kHz float WAV returns exactly the file's samples.
  • An M4A still goes to torchcodec, and its error is unchanged.

Checklist

  • ruff check . passes
  • Added or updated tests / docs where relevant

AI assistance: written with AI coding agent

@github-actions

Copy link
Copy Markdown
Contributor

Automated review

This PR changes Model.load_audio to decode through the existing load_audio_file helper. That helper tries libsndfile (soundfile) first, then falls back to torchcodec when soundfile is missing or can't open the container. It downmixes by averaging channels and resamples with torchaudio. The PR also adds a CPU test with a fake torchcodec that fails the way the video-only FFmpeg does, registers that test in test/cpu-core.txt, and installs soundfile in the CPU Core CI job. The code matches the description.

Findings: none.

I checked the following:

  • Dynamo images get the new path. Both Dynamo Dockerfiles install .[all] (which includes soundfile) and libsndfile1.
  • Installs without soundfile behave as on main. For example, the asr extra doesn't include soundfile; there, the ImportError is caught and decoding falls back to torchcodec as before.
  • Resampling needs no new dependency. torchaudio is already a core dependency, so the resample step adds nothing to install.
  • Other models are unaffected. The models that override load_audio (Qwen3-TTS, Chatterbox, OmniVoice, Nemotron duplex) don't go through this path.

The description is accurate and states what was tested and what wasn't. It also discloses the one behavior change for current users: stereo input now comes out 1/√2 as loud as FFmpeg's downmix produced.


Advisory, not a merge gate. Rules: AGENTS.md. Reply to discuss.

@arun-elr
arun-elr force-pushed the fix/load-audio-soundfile-first branch 7 times, most recently from 9ecb45d to 5cce1fd Compare October 19, 2026 10:40

@merceod merceod left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked on klone (1bca43a, same own files as this head). libsndfile first works for the uploads tried, and with #609 under it the 48,001 Hz and 100,003 Hz uploads are refused in 0.02-0.05 s. The first head opened the unbounded-resample hole (#589) on every model through the base loader (a 3 s WAV at 48,001 Hz took 4.5 s and 13 GB, at 100,003 Hz 9.2 s and 26.5 GB); stacking on #609 closed it and the description says so now. CPU tests pass, ruff clean. Nothing to fix.

@arun-elr
arun-elr force-pushed the fix/load-audio-soundfile-first branch 7 times, most recently from 6b0a375 to 4086c07 Compare October 19, 2026 14:39
@NSagan271

Copy link
Copy Markdown
Collaborator

🤖 Automated live test, AI-generated (Claude Code). Not checked line by line by a human. Numbers come from one allocation, so treat small deltas as provisional.

Live test: [Model][Bug fix] Decode request audio with libsndfile first, for a video-only FFmpeg

Works: with FFmpeg's audio decoders removed, WAV, FLAC, OGG and MP3 transcribe on the PR and fail on base.

1x H200, Whisper large-v3, Python frontend, SHM transport · PR head 253933824 vs main dd1baa889 · head now 8f7d86522: rebased onto #609 (sample-rate checks in the same loader) plus a test; retested under the same shim: served Whisper rows match the tested head (3/3), and on CPU Model.load_audio gives the same lengths and RMS; a 2 MHz WAV is now refused (#609). PR tests pass (12); since the retest, head is 4086c0726: same own diff (#609 merged to main)

Claim Result
Uploads decode without FFmpeg audio decoders ✅ simulated by an LD_PRELOAD shim on the real FFmpeg 5 libraries, so avcodec_find_decoder finds no audio decoder. WAV 16 kHz, WAV 24 kHz stereo, FLAC, OGG and MP3 return 200 with the correct transcript (transcriptions and /generate, 3/3, both heads). Base returns 400 for all 7 formats
M4A and WebM still go to torchcodec ✅ 400 on both arms and both PR heads
Same transcripts with a normal FFmpeg ✅ identical across 7 formats × 3 runs per arm

Other

  • Edge cases: under the shim, a FLAC whose header leaves the sample count out still fails on the PR. libsndfile refuses it and the request falls back to torchcodec (CPU check, both PR heads).
  • Not simulated: the Dynamo image's real FFmpeg 8 load order and its "Invalid data found" error. Under the simulation, base fails with a 400 rather than a 500.

@arun-elr

Copy link
Copy Markdown
Contributor Author

Thanks for the live test.

under the shim, a FLAC whose header leaves the sample count out still fails on the PR. libsndfile refuses it and the request falls back to torchcodec

Not simulated: the Dynamo image's real FFmpeg 8 load order and its "Invalid data found" error. Under the simulation, base fails with a 400 rather than a 500.

  • Noise for the claim: the shim and the PR's own stand-in both take FFmpeg's audio decoding away, which is what this change routes around; the status main returns in that state depends on the error FFmpeg raises, not on this PR. The image build stays under Not tested.

Since the approval: no code change. The description now has a merge-order note for #644 (both rewrite Model.load_audio; whichever lands second keeps load_audio_file, which #644 caps) and links #708. The tested head and the current head have the same own diff.

The Dynamo base image's FFmpeg 8 is video-only, and torchcodec loads it
before the full FFmpeg the image installs.

Fixes mstar-project#534
…o resamples it

The libsndfile path resamples with torchaudio, whose kernel for a rate
coprime to 16 kHz grows with rate * 16000; on top of the resample-rate
bound, such a header is a 400, not gigabytes of kernel in the data worker.
@arun-elr
arun-elr force-pushed the fix/load-audio-soundfile-first branch from ab60fb8 to b81a833 Compare October 20, 2026 02:21
@merceod
merceod merged commit 973638e into mstar-project:main Oct 20, 2026
5 checks passed
vasilevklart added a commit that referenced this pull request Oct 26, 2026
…libsndfile first, and check Chatterbox's at the API (#779)

* [TTS][API server] Read OmniVoice and Chatterbox reference audio with libsndfile first, and check Chatterbox's at the API

OmniVoice decoded its cloning reference with torchcodec only, so on a server
whose FFmpeg has no audio decoders (the base image #639 fixed Model.load_audio
for) even a WAV reference failed; its API check used torchcodec too.
Chatterbox's adapter never checked ref_audio at all: a bad clip failed only
after admission, and a reference up to the data worker's 30-minute cap was
accepted where the other voice-clone adapters stop at 60 s.

- check_ref_audio gains decoder="soundfile+torchcodec", the base loader's
  order: libsndfile, then FFmpeg for what it cannot open.
- OmniVoice's load_audio uses load_audio_file at the codec rate, and its
  adapter checks with that mode.
- Chatterbox's adapter goes through _resolve_ref_audio with that mode, like
  the OmniVoice and Qwen3-TTS adapters, so it also takes bare base64.

A reference not at the codec rate is now resampled by torchaudio instead of
FFmpeg's resampler, so OmniVoice's cloned output can move slightly for it.

* Docs: say what Chatterbox's ref_audio check accepts and refuses

Routing Chatterbox's ref_audio through the shared check means bare base64
is now accepted, and a clip over 60 s or below 8 kHz is a 400 instead of
being trimmed by the model. Update the adapter docstring, the
MAX_REF_AUDIO_SECONDS comment and docs/models.rst to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* media_io: refuse a reference clip with no samples

A WAV with a valid header and 0 frames passed check_ref_audio in both
libsndfile modes: sound_file_seconds reads 0 frames as "not declared", the
count then gives 0 s, and only the upper bound was checked.

---------

Co-authored-by: Kirill Vasilev <290354918+vasilevklart@users.noreply.github.com>
Co-authored-by: Joseph Tan <jdtan638@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dynamo base image: audio uploads fail to decode because the base's FFmpeg 8 is video-only

3 participants