Skip to content

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

Merged
vasilevklart merged 3 commits into
mstar-project:mainfrom
vasilevklart:users/kirill/omnivoice-libsndfile-first
Oct 26, 2026
Merged

vasilevklart merged 3 commits into
mstar-project:mainfrom
vasilevklart:users/kirill/omnivoice-libsndfile-first

Conversation

@vasilevklart

Copy link
Copy Markdown
Collaborator

What does this PR do?

Two small fixes around voice-cloning reference audio:

  • OmniVoice decodes the reference with torchcodec only, so it fails wherever FFmpeg can't decode audio (the case [Model][Bug fix] Decode request audio with libsndfile first, so uploads work with a video-only FFmpeg #639 handled for Model.load_audio). It now uses load_audio_file (libsndfile first, then torchcodec), and check_ref_audio gets a matching soundfile+torchcodec mode.
  • Chatterbox's adapter didn't run check_ref_audio, so a bad or very long clip only failed after admission. It now goes through _resolve_ref_audio like the OmniVoice and Qwen3-TTS adapters, which also means it accepts bare base64.

One side effect: a reference that isn't at 24 kHz is now resampled by torchaudio instead of FFmpeg, so OmniVoice output can differ slightly for those.

How was it tested?

  • New test_omnivoice_reads_a_wav_reference_without_ffmpeg (with the video-only FFmpeg stub from test_base_load_audio.py): a WAV reference goes through the adapter check and load_audio without touching torchcodec. On main the adapter refuses it.
  • Updated test_media_paths.py, test_audio_decode_limit.py and test_chatterbox_model.py for the new decode order and the Chatterbox check.
  • Those plus test_openai_router.py, test_qwen3_tts_reference_audio.py and test/omnivoice: 566 passed, 7 skipped (CPU, torch 2.9.1).
  • Haven't checked speaker similarity on GPU.

Checklist

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

…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 mstar-project#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.
@github-actions

Copy link
Copy Markdown
Contributor

Automated review

OmniVoice's load_audio now uses load_audio_file (libsndfile first, then torchcodec), and a new soundfile+torchcodec mode in check_ref_audio does the same check at the API. Chatterbox's adapter now sends ref_audio through _resolve_ref_audio in that mode, so a clip that can't be decoded or is too long gets a 400 before admission, and bare base64 is now accepted too. The code matches the description.

Findings: none.

I traced the new check mode against both loaders (load_audio_file, ChatterboxModel._decode_audio). Each tries libsndfile first and falls back to FFmpeg when libsndfile can't read the file. Neither falls back after a too-long refusal (RefAudioTooLongError / AudioRefusedError), and both fall back to torchcodec when soundfile isn't installed. So the API check and the data worker accept the same clips.

The description covers what was run, with the counts. It also mentions the torchaudio resampling side effect and that speaker similarity wasn't checked on GPU, which is the one open item. Since non-24 kHz references now go through a different resampler, a short before/after listen or a speaker-similarity number on one or two such clips would back up the "differs slightly" claim.


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

@dghosef dghosef self-assigned this Oct 24, 2026
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>
@mstar-review mstar-review Bot added size/M and removed size/S labels Oct 24, 2026

@dghosef dghosef 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.

lgtm

seconds = count_sound_file_seconds(path, MAX_REF_AUDIO_SECONDS)
_require_ref_audio_seconds(seconds)
try:
_check_ref_audio_soundfile(path)

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.

This only checks the header of the file. If the header is fine but the rest of the file is corrupt, this still passes. Just calling load_audio_file with the max_seconds argument would be better and will keep the check here in sync with the checks that are run later after admission.

Also should add a check to make sure the file is not empty.

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.
@vasilevklart

Copy link
Copy Markdown
Collaborator Author

Fixed the empty file case in 5d4e214. The full decode already runs in the model's load_audio after admission, so a corrupt body fails that request there

@vasilevklart

Copy link
Copy Markdown
Collaborator Author

@dghosef we can follow up after #796

@vasilevklart
vasilevklart merged commit 6b106d3 into mstar-project:main Oct 26, 2026
6 checks passed
arun-elr added a commit to arun-elr/mstar that referenced this pull request Oct 26, 2026
…sts create their upload dir 0700

mstar-project#779 checks the reference audio at the API, which saves it through the
per-upload dir check; under umask 002 a plain mkdir gives 0775, which that
check refuses before the case under test.
arun-elr added a commit to arun-elr/mstar that referenced this pull request Oct 26, 2026
…sts create their upload dir 0700

mstar-project#779 checks the reference audio at the API, which saves it through the
per-upload dir check; under umask 002 a plain mkdir gives 0775, which that
check refuses before the case under test.
dghosef added a commit that referenced this pull request Oct 27, 2026
Conflicts:
- mstar/model/nemotron_duplex/nemotron_duplex_model.py: imports take this branch's ClientChunk with main's KVLabel/LabelLayout/ProcessPromptOutput/Span (#832); process_prompt keeps this branch's body and comment and returns main's ProcessPromptOutput with the kv_layout, beside main's kv_labels and this branch's process_stream_chunk.
- test/modular/test_audio_decode_limit.py: this branch's (load, error, match, seconds) rows plus main's OmniVoice row (#779).
- test/modular/test_nemotron_duplex_model.py: this branch's tests (audio_in, session cap, appends) kept; every process_prompt result they read goes through .new_input_tensors, as main's #832 returns ProcessPromptOutput.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
arun-elr added a commit to arun-elr/mstar that referenced this pull request Oct 30, 2026
…sts create their upload dir 0700

mstar-project#779 checks the reference audio at the API, which saves it through the
per-upload dir check; under umask 002 a plain mkdir gives 0775, which that
check refuses before the case under test.
arun-elr added a commit to arun-elr/mstar that referenced this pull request Nov 5, 2026
…sts create their upload dir 0700

mstar-project#779 checks the reference audio at the API, which saves it through the
per-upload dir check; under umask 002 a plain mkdir gives 0775, which that
check refuses before the case under test.
arun-elr added a commit to arun-elr/mstar that referenced this pull request Nov 5, 2026
…upload dir 0700

mstar-project#659 added test_voxcpm2_model.py after this branch was written. Its media_dirs
fixture made the upload dir with a plain mkdir, which under umask 002 is 0775;
the per-upload check refuses that before the case under test, as it did for
mstar-project#779's and mstar-project#807's tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants