Repository navigation
Conversation
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. Pages changed in this PR: |
eustlb
left a comment
There was a problem hiding this comment.
Self-review: notes on the main changes.
| "scikit-learn", | ||
| "scipy", | ||
| "sentencepiece>=0.1.91,!=0.1.92", | ||
| "soundfile>=0.13.0", |
There was a problem hiding this comment.
first versions with wheels on every supported platform and Python (3.11–3.14), so nothing compiles from source
There was a problem hiding this comment.
might be good to keep a comment exactly for that no? just super small on the side # ensures wheels are available
| "parameterized", | ||
| "psutil", | ||
| "diffusers", # Needed for VibeVoice TTS integration tests | ||
| "librosa", # reference implementation in audio tests |
There was a problem hiding this comment.
librosa and phonemizer move to [testing], so [dev] and every CI image keep them.
|
|
||
| # torchcodec handles audio/video; librosa only plain audio. `backend` lets callers pin one. | ||
| # torchcodec handles audio/video; soundfile only plain audio. `backend` lets callers pin one. | ||
| if backend == "librosa": |
There was a problem hiding this comment.
backend="librosa" is kept as a deprecated alias of "soundfile". Its output is identical, so callers only have to rename one string.
|
|
||
| requires_backends(load_audio, ["librosa"]) | ||
| return librosa.load(BytesIO(source) if isinstance(source, bytes) else source, sr=sampling_rate)[0] | ||
| # Same steps as `librosa.load`, so the output is identical: downmix by averaging, `soxr_hq` resampling, and |
There was a problem hiding this comment.
This replaces librosa.load, which is itself soundfile + soxr: same decode, mean downmix, soxr_hq resampling and fix_length. It's bit-exact with librosa.load on 40 synthetic cases (WAV/FLAC/OGG/MP3 × mono/stereo × 5 rates) and on the 14 non-torchcodec files of hf-internal-testing/all-audio-formats, including with the floor versions. auto falls back to it when torchcodec isn't usable.
There was a problem hiding this comment.
implementation yes, but please move into a separate helper function
| norm: str | None = None, | ||
| mel_scale: str = "htk", | ||
| triangularize_in_mel_space: bool = False, | ||
| dtype: str | np.dtype = np.float64, |
There was a problem hiding this comment.
Important for context mel_filter_bank(norm="slaney", mel_scale="slaney") and librosa.filters.mel(norm="slaney") is that librosa rounds each band to float32 before the slaney normalization (the frequency grids are bit-identical). The default float64 leaves every existing caller unchanged; float32 is bit-exact with librosa (72 configs checked, reproducer in the PR description).
| mel_filters = librosa.filters.mel( | ||
| sr=sampling_rate, n_fft=n_fft, n_mels=feature_size, fmin=0.0, fmax=sampling_rate / 2, norm="slaney" | ||
| # float32 rounding before the slaney normalization: bit-exact with `librosa.filters.mel(..., norm="slaney")` | ||
| mel_filters = mel_filter_bank( |
There was a problem hiding this comment.
This is the call the old TODO had commented out. Parakeet, cohere_asr and nemotron_asr_streaming only used librosa for librosa.filters.mel, and they had @requires(..., "librosa"), so without this they'd stop working for plain [audio] users. The filters are bit-identical, so the features don't change. pop2piano keeps librosa: it also needs essentia and pretty_midi, which were never in [audio].
|
|
||
| if do_phonemize: | ||
| self.init_backend(self.phonemizer_lang) | ||
| # Created on first use so that loading the tokenizer and decoding work without phonemizer / espeak-ng |
There was a problem hiding this comment.
The backend used to be created in __init__, so even loading facebook/wav2vec2-xlsr-53-espeak-cv-ft for inference needed phonemizer and espeak-ng, although only encoding text uses it. With phonemizer out of [audio], it's now created on first phonemize(), so loading and decoding work without it.
|
|
||
| sampling_rate = audio_processor.feature_extractor.sampling_rate | ||
| audio_array, _ = librosa.load(io.BytesIO(file_bytes), sr=sampling_rate, mono=True) | ||
| audio_array = load_audio(base64.b64encode(file_bytes).decode("ascii"), sampling_rate=sampling_rate) |
There was a problem hiding this comment.
This was librosa.load(BytesIO(...)). It now uses load_audio with the auto backend: torchcodec when usable, otherwise soundfile, which gives the same output as before.
There was a problem hiding this comment.
not sure but wdyt about adding a kwarg that handles this case directly?
There was a problem hiding this comment.
keeping it for another PR 👌
| if "image" in self.model.input_modalities or "video" in self.model.input_modalities: | ||
| requires_backends(self, "vision") | ||
| requires_backends(self, "torchvision") | ||
| if "audio" in self.model.input_modalities: |
There was a problem hiding this comment.
This gate never called librosa. load_audio raises its own clear error when decoding needs soundfile.
| for audio_item in audio: | ||
| if isinstance(audio_item, str): | ||
| waveform = self._load_audio(audio_item, sampling_rate) | ||
| waveform = load_audio(audio_item, sampling_rate=sampling_rate) |
There was a problem hiding this comment.
Default auto backend on purpose. apply_chat_template already decodes this model's audio paths and URLs with load_audio(...) (auto), so a direct processor(audio="clip.wav") call now behaves the same way. It also restores m4a / mp4 / webm when torchcodec is installed (the old librosa.load(path) handled them through audioread. Without torchcodec the output is identical to the old librosa path.
There was a problem hiding this comment.
ig it changes the integration tests slightly tho? or have you forced a backend there
There was a problem hiding this comment.
integration test relies on .apply_chat_template path that already call load_audio directly, actually this loading in the processor call is strange, I'll have a look (but not so important tbh)
| else: | ||
| # frequencies of FFT bins in Hz | ||
| fft_freqs = np.linspace(0, sampling_rate // 2, num_frequency_bins) | ||
| fft_freqs = np.linspace(0, sampling_rate / 2, num_frequency_bins) |
There was a problem hiding this comment.
// 2 → / 2: the Nyquist frequency is sampling_rate / 2. For even rates (every model default) the two are the same value, so nothing changes. For odd rates (e.g. 11025) the grid used to stop half a Hz short; it's now correct, and bit-exact with librosa as well (reproducer in the description covers 8001 / 11025 / 16001 / 22051).
There was a problem hiding this comment.
could we consider this as a separate fix tho? it's not related to the deps inherently
| RUN uv pip install --no-cache-dir GitPython | ||
| RUN uv pip install --no-cache-dir --upgrade 'torch<=2.15.0' 'torchaudio' 'torchvision' --index-url https://download.pytorch.org/whl/cpu | ||
| RUN uv pip install --no-cache-dir "git+https://github.com/huggingface/transformers.git@${REF}#egg=transformers[quality,testing,torch-speech,vision]" | ||
| RUN uv pip install --no-cache-dir "git+https://github.com/huggingface/transformers.git@${REF}#egg=transformers[quality,testing,vision]" |
There was a problem hiding this comment.
Drive-by: there is no torch-speech extra (removed from setup.py a long time ago) and uv pip ignores unknown extras, so the image doesn't change. It's dropped only because it still looks like an audio extra.
vasqu
left a comment
There was a problem hiding this comment.
Thanks a lot, some comments might not make sense as I saw things later but the core is good overall
I have a few smaller comments here and there
There was a problem hiding this comment.
Oh yea we have (had?) quite a lot of docs not using our load helpers, that also goes for vision. Mind to open an issue for the vision side to track to make this consistent
There was a problem hiding this comment.
question before reading the actual code: are we hard deprecating load_audio_librosa? Could we potentially keep an alias that warns and redirects to base load_audio?
There was a problem hiding this comment.
yep we already have an alias for load_audio_librosa :)
|
|
||
| ```bash | ||
| pip install transformers datasets evaluate jiwer soundfile librosa torchcodec | ||
| pip install transformers datasets evaluate jiwer soundfile torchcodec |
There was a problem hiding this comment.
not on you but might be nice to give options for each task at some point: so pip install transformers[audio-asr]
There was a problem hiding this comment.
agree, keeping track of this (on my long list 😅)
|
|
||
| sampling_rate = audio_processor.feature_extractor.sampling_rate | ||
| audio_array, _ = librosa.load(io.BytesIO(file_bytes), sr=sampling_rate, mono=True) | ||
| audio_array = load_audio(base64.b64encode(file_bytes).decode("ascii"), sampling_rate=sampling_rate) |
There was a problem hiding this comment.
not sure but wdyt about adding a kwarg that handles this case directly?
| `apt install espeak-ng` on Debian/Ubuntu, `brew install espeak-ng` on macOS). Please note that you may need to restart | ||
| your runtime after installation. |
There was a problem hiding this comment.
last sentence imo not necessary or am i heavily biased that this is obv? :D
| dtype=np.float32, | ||
| ) | ||
| self.mel_filters = torch.from_numpy(mel_filters).to(torch.float32) | ||
| self.mel_filters = torch.from_numpy(np.ascontiguousarray(mel_filters.T)) |
There was a problem hiding this comment.
np.ascontiguousarray(mel_filters.T) could be directly done within mel_filter_bank no?
There was a problem hiding this comment.
not really, since the output shape for it is already in the API it would be breaking :/
There was a problem hiding this comment.
no worries wouldve been a nice to have but no biggie
| for audio_item in audio: | ||
| if isinstance(audio_item, str): | ||
| waveform = self._load_audio(audio_item, sampling_rate) | ||
| waveform = load_audio(audio_item, sampling_rate=sampling_rate) |
There was a problem hiding this comment.
ig it changes the integration tests slightly tho? or have you forced a backend there
| if phonemizer_lang is not None: | ||
| self.phonemizer_lang = phonemizer_lang | ||
| self.init_backend(phonemizer_lang) | ||
| self._phonemizer_backend = None |
There was a problem hiding this comment.
backend now created lazily, on first call, so loading the tokenizer and decoding work without phonemizer / and espeak-ng.
Setting phonemizer_lang here drops the cached backend so the next rebuilds it for that language, the previous eager init_backend needed phonemizer + espeak-ng just to set language
| @require_soundfile | ||
| @require_soxr |
There was a problem hiding this comment.
I'd assume this combination to happen frequently, let's make anoth requires helper?
There was a problem hiding this comment.
totally, added require_audio that mirrors require_vision for core loading, cleaner
| except Exception: | ||
| self.skipTest(f"libsndfile cannot encode {audio_format}") |
063f9ce to
0a4eebb
Compare
|
[For maintainers] Suggested jobs to run (before merge) run-slow: cohere_asr, muse_spark, nemotron_asr_streaming, nemotron_h_omni, parakeet, qwen2_5_omni, qwen2_audio, vibevoice_acoustic_tokenizer, wav2vec2_phoneme |
load_audio gets a "soundfile" backend (soundfile decode + soxr_hq resample + librosa's length fix) that is bit-exact with librosa.load; "auto" falls back to it when torchcodec is unavailable. backend="librosa" becomes a deprecated alias (FutureWarning). soxr is imported behind is_soxr_available instead of piggybacking on librosa. Serving, load_audio_as and nemotron_h_omni now go through load_audio; the any_to_any librosa gate is removed; docs and docstrings use load_audio instead of librosa.load. librosa leaves the `[audio]` extra, replaced by what the new path uses: soundfile>=0.13.0 and soxr>=0.5.0 (both previously pulled in only transitively through librosa). librosa moves to `[testing]` as the reference implementation in audio tests, so `[dev]` and CI keep it. Floors: soundfile>=0.13.0 (first Linux aarch64 wheels, bundled libsndfile with MP3) and soxr>=0.5.0 (first cp312-abi3 wheel, covers 3.12-3.14): every supported platform gets wheels, nothing compiles.
…thout librosa
These three feature extractors only needed librosa for
`librosa.filters.mel(norm="slaney")` (the `mel_filter_bank` TODO: it works
in float64, librosa rounds each band to float32). Build the filters
natively with librosa's rounding order in a `_slaney_mel_filters` method
(parakeet, inherited by nemotron_asr_streaming through modular, and the
same lines in cohere_asr, which already duplicated the librosa call), so
they no longer require librosa and the slimmer `[audio]` doesn't depend
on the AudioProcessor refactor.
Bit-exact with librosa 0.11.0: new test compares all three classes'
filters with `librosa.filters.mel` over sr in {8k, 16k, 44.1k}, n_fft in
{256, 512, 1024}, n_mels in {64, 80, 128}, plus each default config.
`[audio]` now holds what the core audio path needs and nothing else: torchaudio (pipeline / torch-backend resampling), soundfile (decoding) and soxr (resampling for `load_audio` and the numpy backend). phonemizer moves to `[testing]` (so `[dev]` and CI are unchanged). (pyctcdecode / kenlm left with the CTC LM deprecation, librosa in the previous commit.) - `Wav2Vec2PhonemeCTCTokenizer` creates its phonemizer backend on first use, so loading and decoding (inference, ASR pipeline) need neither phonemizer nor espeak-ng; only phonemizing text does. The phonemizer error now also names the `espeak-ng` system dependency. - New `test-audio-extra` job in the nightly extras smoke test: a fresh environment with only `.[audio]`, running `utils/check_audio_extra.py` (WAV/FLAC/MP3 through `load_audio`, `auto` resolves to soundfile, moved-out packages absent, numpy >= 2). - Drop the non-existent `torch-speech` extra from the consistency image.
…float32)` The only difference between `mel_filter_bank(norm="slaney", mel_scale="slaney")` and `librosa.filters.mel(norm="slaney")` is that librosa rounds each band to float32 before the slaney normalization (the frequency grids are bit-identical). Add a `dtype` argument to `mel_filter_bank` (default float64, unchanged), cast the triangular filters to it before the normalization, and use it in parakeet, cohere_asr and nemotron_asr_streaming instead of their per-model `_slaney_mel_filters`. Filters stay bit-exact with librosa. The config-grid bit-exact test moves to `test_audio_utils.py` (6 rates x 4 n_fft x 3 n_mels); the per-model test checks each default config.
Out of scope for this PR (no equivalent exists for other modality extras); can come back as a follow-up covering all of them.
Bit-exactness with librosa is verified once (reproducer in the PR description); keep only a librosa-free test of `mel_filter_bank`'s `dtype` argument.
…er_bank`, soxr only to resample - NemotronH_Omni: direct file-path inputs go through `load_audio`'s default `auto` backend, like `apply_chat_template` already does for the same model, so m4a / mp4 / webm work again when torchcodec is installed. - `mel_filter_bank`: build the FFT grid up to `sampling_rate / 2` instead of `sampling_rate // 2`. No change for even rates; odd rates now get the right Nyquist frequency and are bit-exact with librosa too. - `load_audio_as`: require soxr only when `sampling_rate` is given.
0a4eebb to
7297598
Compare
Important
For the release notes (one entry covering #50408 and this PR):
🚨
transformers[audio]now installs onlytorchaudio,soundfileandsoxr. It no longer installs librosa,phonemizer, pyctcdecode or kenlm, shrinks from ~371 MiB to ~1.4 MiB on top of torch, and no longer downgrades numpy
to 1.x.
Who is affected: new environments (fresh venvs, Docker images, CI) built with
pip install transformers[audio].Upgrading an existing environment doesn't uninstall anything, so packages that are already installed keep working.
What breaks in such an environment:
import librosain your own codeModuleNotFoundErrorpip install librosaWav2Vec2PhonemeCTCTokenizer, or with a VITS tokenizer that hasphonemize=TrueImportErrorpip install phonemizer+ theespeak-ngsystem libraryWav2Vec2ProcessorWithLMImportErrorpip install pyctcdecode kenlm(deprecated, removed in v6)*WithLMcheckpointWhat keeps working without them:
load_audio: same output aslibrosa.load.Wav2Vec2PhonemeCTCTokenizer: phonemizer is now only needed to phonemize text.Deprecated:
load_audio(..., backend="librosa"); usebackend="soundfile"(same output).What does this PR do?
Slims
[audio]down to the audio core's dependencies:torchaudio,soundfile,soxr. librosa and phonemizer moveto
[testing]. Stacked on #50408 (pyctcdecode / kenlm).Assumption:
transformers[audio]installs the audio core, nothing moreThe audio core is the audio code shared across models and tasks:
audio_utils(load_audio, spectrograms, mel filters);[audio]should install exactly what the core needs, so that it works out of the box. Model-specific dependencies stayopt-in behind
@requires/requires_backends, and missing ones raise a clearImportError. This is already how[vision]works: it installs torchvision + Pillow, while opencv, scipy and pytesseract stay opt-in for the visionmodels that need them.
[audio]currently breaks this assumption both ways:librosa.load(= soundfile + soxr) and
librosa.filters.mel, both easy to do without it. Only pop2piano needs it beyond that.espeak-ng, which pip can't install.depends on them.
With #50408: ~371 MiB → ~1.4 MiB on top of torch, install time 59 s → 3 s, and numpy is no longer downgraded to 1.x.
🚨 What breaks
[audio].ImportErrorWav2Vec2PhonemeCTCTokenizer, VITSphonemize=True). It never worked from[audio]alone anyway (needs espeak-ng).Numbers change slightly in two places that loaded audio with librosa and now use
load_audio's defaultautobackend: when torchcodec is installed, they decode and resample through FFmpeg instead of soundfile + soxr. Without torchcodec, the output is identical to before./v1/audio/transcriptions): m4a / mp4 / webm uploads now work when torchcodec is installed (they never worked before: librosa doesn't fall back to audioread for in-memory bytes).processor(audio="clip.wav")): it now behaves likeapply_chat_templatefor the same model, which already usedauto.Tests
load_audioon 40 synthetic + 14 real formats;mel_filter_bank(dtype=np.float32)on 72 configs and the three extractors' default filters (librosa 1.0.0, numpy 2.5.4)soundfile==0.13.0,soxr==0.5.0) also bit-exactMel filters reproducer (not shipped as a test)