Fix Whisper ASR timestamp regression - #1742
Conversation
Three fixes for the timestamp regression between v3.8.1 and v4.2.0: 1. Use actual audio length for seek loop bounds: The seek loop used input_features.dims[2] (always 3000 = padded 30s) instead of generation_config.num_frames (actual audio length). For clips shorter than 30s, this caused the loop to process silence as real audio, producing hallucinated text and wrong timestamps. 2. Apply max_initial_timestamp_index constraint: A `continue` statement in WhisperTimeStampLogitsProcessor skipped the max_initial_timestamp_index check, allowing the model to generate any timestamp as its first token. The Python reference implementation has no such early exit. 3. Guard against infinite seek loop: If segment_offset is zero (e.g., from a degenerate timestamp pair), the loop ran forever.
|
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. |
nico-martin
left a comment
There was a problem hiding this comment.
Hi @anshusaurav, thank you so much for looking into this! The diagnosis and overall direction make sense. One edge case remains: num_frames can be 0, so the truthy check falls back to the padded tensor. Could you use a nullish check and add direct tests for short audio, timestamp masking, and the non-progress case? I'm holding off on merging until the zero-frame bug and untested seek-loop changes are covered.
| // input_features shape: [batch=1, n_mels, total_frames] | ||
| const input_features = inputs; | ||
| const total_frames = input_features.dims[2]; | ||
| const total_frames = generation_config.num_frames |
There was a problem hiding this comment.
num_frames can legitimately be 0. This truthiness check then falls back to the padded 3,000-frame tensor and reintroduces padding processing. Please distinguish an omitted value from zero, for example with generation_config.num_frames != null, and add a zero/short-frame regression test.
Address review feedback: - Change truthy check to nullish check (num_frames != null) so that num_frames=0 is not mistaken for an omitted value, which would incorrectly fall back to the padded tensor length - Add tests for zero num_frames, short audio with timestamps, and timestamp token presence
|
Thanks for the thorough review @nico-martin! Just pushed the requested changes:
|
|
Thanks, the nullish check correctly fixes the num_frames=0 case. The added tests still do not exercise the changed seek-loop behavior, however: each passes max_new_tokens, and generate() explicitly bypasses _generate_with_seek() whenever that option is present. Could you adjust the coverage so that:
The current timestamp-presence assertion and short-input smoke test would pass with the corresponding fixes reverted. Also, the added Tensor import is currently unused. |
Summary
Fixes #1684 — Whisper ASR timestamp regression between v3.8.1 and v4.2.0.
Three fixes targeting the seek loop and logits processor introduced in commit
43b2662("Overdue Whisper fixes #1594"):Use actual audio length for seek loop bounds (
modeling_whisper.js): The seek loop usedinput_features.dims[2](always 3000 = padded 30s) astotal_framesinstead ofgeneration_config.num_frames(set by the ASR pipeline to the real mel frame count). For clips shorter than 30 seconds, this caused the loop to process silence/padding as real audio, producing hallucinated text and incorrect timestamps.Apply
max_initial_timestamp_indexconstraint (logits_process.js): Acontinuestatement inWhisperTimeStampLogitsProcessor._call()skipped themax_initial_timestamp_indexcheck at line 326, making it dead code. This allowed the model to generate any timestamp as its first token instead of being constrained to the allowed range. The Python reference implementation has no such early exit — both blocks execute sequentially.Guard against infinite seek loop (
modeling_whisper.js): Ifsegment_offsetcomputes to zero (e.g., from a degenerate timestamp pair), the seek loop would run forever. Added abreakwhensegment_offset <= 0.Test plan
[applause]segment is no longer droppedtest_modeling_whisper.js) still pass — they usemax_new_tokenswhich bypasses the seek loop