Upload whisper_decoder.py with huggingface_hub
Browse files- whisper_decoder.py +366 -0
whisper_decoder.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run the fine-tuned Whisper over a predicted mel.
|
| 2 |
+
|
| 3 |
+
Uses transformers' own `generate()` rather than a hand-rolled decoder. An
|
| 4 |
+
earlier ONNX export with custom beam search produced single punctuation marks
|
| 5 |
+
and empty strings where `generate()` produced fluent sentences from the
|
| 6 |
+
identical mel -- the model was being silently crippled by the decoding rather
|
| 7 |
+
than by the input.
|
| 8 |
+
|
| 9 |
+
The mel contract is the subtle part. The checkpoint keeps Whisper's standard
|
| 10 |
+
**80-channel** input even though it was fine-tuned on 12-bin prosody-predicted
|
| 11 |
+
mels: `num_mel_bins` in the training config truncates incoming mels, it never
|
| 12 |
+
resizes conv1. The training collator reconciles the two by zero-padding, and
|
| 13 |
+
this module reproduces that exactly:
|
| 14 |
+
|
| 15 |
+
conformer -> [T, 12] -> transpose -> [12, T] -> zero-pad -> [80, 3000]
|
| 16 |
+
|
| 17 |
+
Bins 12-79 are zeros. Getting this wrong fails at the first conv layer.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
|
| 24 |
+
import numpy as np
|
| 25 |
+
|
| 26 |
+
# Whisper's encoder window: 3000 mel frames at 100 Hz = 30 s.
|
| 27 |
+
ENCODER_FRAMES = 3000
|
| 28 |
+
ENCODER_MEL_BINS = 80
|
| 29 |
+
|
| 30 |
+
# The decoder prefix the checkpoint was fine-tuned behind.
|
| 31 |
+
#
|
| 32 |
+
# Its own forced_decoder_ids is [[1, None], [2, <|transcribe|>]] -- position 1,
|
| 33 |
+
# the language slot, is left unset. Decoding without pinning it does not make
|
| 34 |
+
# the model choose a language: it skips the slot and starts from
|
| 35 |
+
# <|startoftranscript|> alone, so the prefix is two tokens short of what
|
| 36 |
+
# training used.
|
| 37 |
+
#
|
| 38 |
+
# The published inference runs set language: en, and their recorded
|
| 39 |
+
# snapshot_meta.json confirms the full prefix
|
| 40 |
+
# [50258, 50259, 50359, 50363] = <|sot|> <|en|> <|transcribe|> <|notimestamps|>.
|
| 41 |
+
# Matching it is the point: a different prefix is a different model condition,
|
| 42 |
+
# so any comparison against those runs is confounded without it.
|
| 43 |
+
DEFAULT_LANGUAGE = "en"
|
| 44 |
+
|
| 45 |
+
# Generation limits the published inference runs used.
|
| 46 |
+
DEFAULT_MAX_NEW_TOKENS = 225
|
| 47 |
+
DEFAULT_NO_REPEAT_NGRAM = 3
|
| 48 |
+
|
| 49 |
+
# 10, matching the published inference config for this checkpoint.
|
| 50 |
+
#
|
| 51 |
+
# Wider is cheap and was tried: 385 ms at 10 beams against 634 ms at 40 on a
|
| 52 |
+
# healthy phrase, four times the hypotheses for a quarter more latency. It
|
| 53 |
+
# made the transcriptions worse -- a wider beam searches harder for the
|
| 54 |
+
# highest-likelihood sequence, and on a signal as weak as a drawn contour
|
| 55 |
+
# that is the YouTube-caption boilerplate the checkpoint overlearned. More
|
| 56 |
+
# beams find the attractor more reliably rather than finding better words.
|
| 57 |
+
DEFAULT_BEAMS = 10
|
| 58 |
+
|
| 59 |
+
# Phrases Whisper falls back on when the input carries no usable speech.
|
| 60 |
+
#
|
| 61 |
+
# Trained on YouTube captions, it learned what tends to accompany
|
| 62 |
+
# unintelligible audio: "Thank you." for silence, and shouted boilerplate
|
| 63 |
+
# from auto-generated or burned-in captions. Feeding it pure noise returns
|
| 64 |
+
# "I'M NOT GOING TO SAY ANYTHING" in three of the top four beams; feeding it
|
| 65 |
+
# a real drawing returns it in none. Naming it in the UI keeps it from
|
| 66 |
+
# reading as a transcription of the drawing.
|
| 67 |
+
HALLUCINATION_MARKERS = (
|
| 68 |
+
"i'm not going to say anything",
|
| 69 |
+
"thank you for watching",
|
| 70 |
+
"subscribe",
|
| 71 |
+
"thanks for watching",
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def looks_like_fallback(text: str) -> bool:
|
| 76 |
+
"""Whether a hypothesis is the model's no-speech boilerplate."""
|
| 77 |
+
lowered = text.strip().lower()
|
| 78 |
+
if not lowered:
|
| 79 |
+
return False
|
| 80 |
+
if len(lowered) > 3 and text.strip().isupper():
|
| 81 |
+
return True
|
| 82 |
+
return any(marker in lowered for marker in HALLUCINATION_MARKERS)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# Measured across the templates: real speech runs about 4.3 words/sec, p90
|
| 86 |
+
# 5.6. The drawing is the only evidence about how long the utterance is, so
|
| 87 |
+
# the token budget is derived from its duration rather than left at a flat
|
| 88 |
+
# 48 -- otherwise a half-second scribble can come back as a paragraph, which
|
| 89 |
+
# is the language model talking rather than a transcription.
|
| 90 |
+
WORDS_PER_SECOND = 5.6 # p90, so an unusually fast phrase is not truncated
|
| 91 |
+
TOKENS_PER_WORD = 2.5 # Whisper splits a word into roughly this many tokens
|
| 92 |
+
MIN_TOKENS = 4
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def pad_mel_for_whisper(
|
| 96 |
+
mel: np.ndarray,
|
| 97 |
+
mel_bins: int = ENCODER_MEL_BINS,
|
| 98 |
+
frames: int = ENCODER_FRAMES,
|
| 99 |
+
) -> np.ndarray:
|
| 100 |
+
"""[T, bins] from the conformer -> [bins_padded, frames] for Whisper.
|
| 101 |
+
|
| 102 |
+
Transposes to Whisper's [bins, time] layout, then zero-pads both axes.
|
| 103 |
+
This mirrors the training collator's `pad_mel_to_expected` branch, so the
|
| 104 |
+
model sees the same shape it was fine-tuned on.
|
| 105 |
+
"""
|
| 106 |
+
mel = np.asarray(mel, dtype=np.float32)
|
| 107 |
+
if mel.ndim != 2:
|
| 108 |
+
raise ValueError(f"expected a 2-D mel, got {mel.shape}")
|
| 109 |
+
|
| 110 |
+
# The conformer emits [T, bins]; Whisper wants [bins, T].
|
| 111 |
+
mel = mel.T
|
| 112 |
+
|
| 113 |
+
out = np.zeros((mel_bins, frames), dtype=np.float32)
|
| 114 |
+
bins = min(mel.shape[0], mel_bins)
|
| 115 |
+
time = min(mel.shape[1], frames)
|
| 116 |
+
out[:bins, :time] = mel[:bins, :time]
|
| 117 |
+
return out
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class Whisper:
|
| 121 |
+
"""Transcribes a predicted mel with the fine-tuned checkpoint."""
|
| 122 |
+
|
| 123 |
+
def __init__(
|
| 124 |
+
self,
|
| 125 |
+
model_dir: Path | str,
|
| 126 |
+
device: str | None = None,
|
| 127 |
+
num_beams: int = DEFAULT_BEAMS,
|
| 128 |
+
half: bool = False,
|
| 129 |
+
tokenizer_dir: Path | str | None = None,
|
| 130 |
+
) -> None:
|
| 131 |
+
import torch
|
| 132 |
+
from transformers import (WhisperForConditionalGeneration,
|
| 133 |
+
WhisperTokenizerFast)
|
| 134 |
+
|
| 135 |
+
model_dir = Path(model_dir)
|
| 136 |
+
if not (model_dir / "config.json").exists():
|
| 137 |
+
raise FileNotFoundError(
|
| 138 |
+
f"{model_dir} is not a Whisper checkpoint directory "
|
| 139 |
+
"(no config.json). Fetch the weights with "
|
| 140 |
+
"python scripts/fetch_models.py"
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
if device is None:
|
| 144 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 145 |
+
self.device = torch.device(device)
|
| 146 |
+
self.num_beams = num_beams
|
| 147 |
+
|
| 148 |
+
# fp16 is 1.8x faster (493 ms against 793 ms per phrase at beam 10)
|
| 149 |
+
# and halves the VRAM, and it transcribed all twelve templates
|
| 150 |
+
# identically -- the acoustic evidence in a drawn contour is far
|
| 151 |
+
# coarser than the gap between the precisions. fp32 is the default
|
| 152 |
+
# because that is what the published runs used.
|
| 153 |
+
#
|
| 154 |
+
# CPU stays fp32 regardless: torch has no fast half kernels there, so
|
| 155 |
+
# it would be slower, not faster.
|
| 156 |
+
self.dtype = (
|
| 157 |
+
torch.float16 if half and self.device.type == "cuda" else torch.float32
|
| 158 |
+
)
|
| 159 |
+
self.model = WhisperForConditionalGeneration.from_pretrained(
|
| 160 |
+
model_dir, dtype=self.dtype
|
| 161 |
+
)
|
| 162 |
+
self.model.eval().to(self.device)
|
| 163 |
+
self.mel_bins: int = self.model.config.num_mel_bins
|
| 164 |
+
|
| 165 |
+
# The checkpoint carries no tokenizer of its own; it is the stock
|
| 166 |
+
# whisper-medium one. Loading it from a local directory keeps first
|
| 167 |
+
# run offline -- otherwise this is a HuggingFace Hub fetch, which on
|
| 168 |
+
# a fresh Space or an air-gapped machine is a startup failure rather
|
| 169 |
+
# than a slow import.
|
| 170 |
+
source = str(tokenizer_dir) if tokenizer_dir else "openai/whisper-medium"
|
| 171 |
+
self.tokenizer = WhisperTokenizerFast.from_pretrained(source)
|
| 172 |
+
|
| 173 |
+
@property
|
| 174 |
+
def provider(self) -> str:
|
| 175 |
+
return str(self.device)
|
| 176 |
+
|
| 177 |
+
def _digit_token_ids(self) -> list[int]:
|
| 178 |
+
"""Whisper tokens containing a digit.
|
| 179 |
+
|
| 180 |
+
Banned under `constrain`: a prosody contour carries no evidence for
|
| 181 |
+
"15" over "fifteen", and numerals are a large, acoustically
|
| 182 |
+
meaningless slice of the vocabulary. Spelled-out numbers remain
|
| 183 |
+
available.
|
| 184 |
+
"""
|
| 185 |
+
if getattr(self, "_digits", None) is None:
|
| 186 |
+
# Decode each id rather than inspecting the raw token: Whisper's
|
| 187 |
+
# timestamp markers are spelled "<|24.08|>" and would otherwise
|
| 188 |
+
# match on the digits in their name. They decode to nothing and
|
| 189 |
+
# are suppressed already.
|
| 190 |
+
self._digits = [
|
| 191 |
+
index
|
| 192 |
+
for index in range(len(self.tokenizer))
|
| 193 |
+
if any(c.isdigit() for c in self.tokenizer.decode([index]))
|
| 194 |
+
]
|
| 195 |
+
return self._digits
|
| 196 |
+
|
| 197 |
+
@staticmethod
|
| 198 |
+
def tokens_for(mel: np.ndarray) -> int:
|
| 199 |
+
"""A token budget proportional to the drawing's length.
|
| 200 |
+
|
| 201 |
+
The mel runs at 100 Hz, so its frame count is the duration. Letting
|
| 202 |
+
generation run to a flat limit regardless lets a short scribble come
|
| 203 |
+
back as a long sentence, which is the decoder's prior rather than the
|
| 204 |
+
drawing's content.
|
| 205 |
+
"""
|
| 206 |
+
seconds = len(mel) / 100.0
|
| 207 |
+
words = seconds * WORDS_PER_SECOND
|
| 208 |
+
return max(MIN_TOKENS, int(round(words * TOKENS_PER_WORD)))
|
| 209 |
+
|
| 210 |
+
def token_budget(self, requested: int, language: str | None) -> int:
|
| 211 |
+
"""`requested`, clipped to what the decoder can actually emit.
|
| 212 |
+
|
| 213 |
+
Whisper's decoder has max_target_positions=448 and counts the prefix
|
| 214 |
+
against it, so a budget derived from the input length has to leave
|
| 215 |
+
room for the prefix or generate() raises -- which it did on a 4.8 s
|
| 216 |
+
clip, where the duration-derived budget came to 478 and the
|
| 217 |
+
four-token prefix pushed the total to 482.
|
| 218 |
+
"""
|
| 219 |
+
limit = int(getattr(self.model.config, "max_target_positions", 448))
|
| 220 |
+
prefix = len(self.decoder_prefix(language))
|
| 221 |
+
# One spare for EOS, which also occupies a position.
|
| 222 |
+
return max(1, min(int(requested), limit - prefix - 1))
|
| 223 |
+
|
| 224 |
+
def decoder_prefix(self, language: str | None = DEFAULT_LANGUAGE) -> list[int]:
|
| 225 |
+
"""The tokens the decoder is primed with before any content.
|
| 226 |
+
|
| 227 |
+
Built from the tokenizer with the language and task set, which is how
|
| 228 |
+
the published runs got theirs: their snapshot_meta.json records
|
| 229 |
+
[50258, 50259, 50359, 50363], and this reproduces exactly that.
|
| 230 |
+
"""
|
| 231 |
+
if language:
|
| 232 |
+
# prefix_tokens is a property derived from these two, so set them
|
| 233 |
+
# on the tokenizer rather than hand-assembling the sequence.
|
| 234 |
+
self.tokenizer.set_prefix_tokens(language=language, task="transcribe")
|
| 235 |
+
return [int(t) for t in self.tokenizer.prefix_tokens]
|
| 236 |
+
|
| 237 |
+
prefix = [self.model.config.decoder_start_token_id]
|
| 238 |
+
forced = getattr(self.model.generation_config, "forced_decoder_ids", None)
|
| 239 |
+
for _position, token in forced or []:
|
| 240 |
+
if token is not None:
|
| 241 |
+
prefix.append(int(token))
|
| 242 |
+
return prefix
|
| 243 |
+
|
| 244 |
+
def hypotheses(
|
| 245 |
+
self,
|
| 246 |
+
mel: np.ndarray,
|
| 247 |
+
max_tokens: int | None = None,
|
| 248 |
+
num_beams: int | None = None,
|
| 249 |
+
top_k: int | None = None,
|
| 250 |
+
language: str | None = DEFAULT_LANGUAGE,
|
| 251 |
+
no_repeat_ngram_size: int | None = None,
|
| 252 |
+
constrain: bool = True,
|
| 253 |
+
) -> list[tuple[str, float]]:
|
| 254 |
+
"""The beam's ranked candidates as (text, score), best first.
|
| 255 |
+
|
| 256 |
+
`language` pins the decoder prefix; see DEFAULT_LANGUAGE. Pass None
|
| 257 |
+
to leave the checkpoint's own forced_decoder_ids alone, which gives a
|
| 258 |
+
two-token-shorter prefix than training used.
|
| 259 |
+
|
| 260 |
+
On input this uncertain the top hypothesis is often a narrow winner,
|
| 261 |
+
so the runners-up say more about what the model heard than the single
|
| 262 |
+
best string does.
|
| 263 |
+
|
| 264 |
+
Scores are HuggingFace's length-normalised sequence log-probabilities
|
| 265 |
+
where available (higher is better), and NaN where the capture below
|
| 266 |
+
cannot recover them -- the ordering is always meaningful even when the
|
| 267 |
+
numbers are not.
|
| 268 |
+
"""
|
| 269 |
+
import torch
|
| 270 |
+
from transformers import GenerationMixin
|
| 271 |
+
|
| 272 |
+
beams = num_beams or self.num_beams
|
| 273 |
+
limits = {}
|
| 274 |
+
if language:
|
| 275 |
+
limits["language"] = language
|
| 276 |
+
|
| 277 |
+
# The published runs set no_repeat_ngram_size: 3 and nothing else.
|
| 278 |
+
ngram = (DEFAULT_NO_REPEAT_NGRAM if no_repeat_ngram_size is None
|
| 279 |
+
else no_repeat_ngram_size)
|
| 280 |
+
if ngram:
|
| 281 |
+
limits["no_repeat_ngram_size"] = ngram
|
| 282 |
+
|
| 283 |
+
# `constrain` is what the published dataset runs used: ban numerals,
|
| 284 |
+
# and bound the length by the utterance rather than by a flat limit.
|
| 285 |
+
# Letting generation run to 225 regardless lets a half-second clip
|
| 286 |
+
# come back as a paragraph, which is the decoder's prior talking
|
| 287 |
+
# rather than the input.
|
| 288 |
+
if constrain:
|
| 289 |
+
limits["bad_words_ids"] = [[i] for i in self._digit_token_ids()]
|
| 290 |
+
max_tokens = max_tokens or self.tokens_for(mel)
|
| 291 |
+
else:
|
| 292 |
+
max_tokens = max_tokens or DEFAULT_MAX_NEW_TOKENS
|
| 293 |
+
max_tokens = self.token_budget(max_tokens, language)
|
| 294 |
+
features = pad_mel_for_whisper(mel, self.mel_bins, ENCODER_FRAMES)
|
| 295 |
+
batch = torch.from_numpy(features).unsqueeze(0).to(self.device, self.dtype)
|
| 296 |
+
|
| 297 |
+
if beams <= 1:
|
| 298 |
+
with torch.no_grad():
|
| 299 |
+
ids = self.model.generate(
|
| 300 |
+
batch, num_beams=1, max_new_tokens=max_tokens, **limits
|
| 301 |
+
)
|
| 302 |
+
return [(self.tokenizer.batch_decode(
|
| 303 |
+
ids, skip_special_tokens=True)[0].strip(), float("nan"))]
|
| 304 |
+
|
| 305 |
+
# Whisper's generate() wrapper keeps only the top beam and does not
|
| 306 |
+
# honour num_return_sequences; asking it for all of them inflates the
|
| 307 |
+
# decoder cache until a 24 GB GPU runs out. The approach here is the
|
| 308 |
+
# one the reference inference script uses: patch
|
| 309 |
+
# GenerationMixin.generate, set num_return_sequences on the inner
|
| 310 |
+
# call, capture every beam, and hand Whisper back the single sequence
|
| 311 |
+
# it expects.
|
| 312 |
+
captured: list = []
|
| 313 |
+
original = GenerationMixin.generate
|
| 314 |
+
|
| 315 |
+
def capturing(model_self, input_features, **kwargs):
|
| 316 |
+
config = kwargs.get("generation_config")
|
| 317 |
+
width = config.num_beams if config else 1
|
| 318 |
+
if width <= 1:
|
| 319 |
+
return original(model_self, input_features, **kwargs)
|
| 320 |
+
|
| 321 |
+
config.num_return_sequences = width
|
| 322 |
+
# sequences_scores only comes back when the inner call is asked
|
| 323 |
+
# for a dict; Whisper's wrapper does not always pass this down.
|
| 324 |
+
kwargs["return_dict_in_generate"] = True
|
| 325 |
+
# ...and, since transformers 5, only when scores are explicitly
|
| 326 |
+
# requested. Without this the attribute is present but None, so
|
| 327 |
+
# every hypothesis came back scored NaN -- the ranking survived,
|
| 328 |
+
# being the beam's own order, but the numbers beside it were
|
| 329 |
+
# gone and nothing said so.
|
| 330 |
+
kwargs["output_scores"] = True
|
| 331 |
+
out = original(model_self, input_features, **kwargs)
|
| 332 |
+
config.num_return_sequences = 1
|
| 333 |
+
|
| 334 |
+
seqs = out.sequences if hasattr(out, "sequences") else out
|
| 335 |
+
captured.append((seqs, getattr(out, "sequences_scores", None)))
|
| 336 |
+
|
| 337 |
+
top1 = seqs[::width]
|
| 338 |
+
if hasattr(out, "sequences"):
|
| 339 |
+
out.sequences = top1
|
| 340 |
+
else:
|
| 341 |
+
out = top1
|
| 342 |
+
return out
|
| 343 |
+
|
| 344 |
+
GenerationMixin.generate = capturing
|
| 345 |
+
try:
|
| 346 |
+
with torch.no_grad():
|
| 347 |
+
self.model.generate(
|
| 348 |
+
batch,
|
| 349 |
+
num_beams=beams,
|
| 350 |
+
max_new_tokens=max_tokens,
|
| 351 |
+
return_dict_in_generate=True,
|
| 352 |
+
**limits,
|
| 353 |
+
)
|
| 354 |
+
finally:
|
| 355 |
+
GenerationMixin.generate = original
|
| 356 |
+
|
| 357 |
+
if not captured:
|
| 358 |
+
return []
|
| 359 |
+
seqs, scores = captured[0]
|
| 360 |
+
texts = self.tokenizer.batch_decode(seqs, skip_special_tokens=True)
|
| 361 |
+
values = (
|
| 362 |
+
scores.tolist() if scores is not None
|
| 363 |
+
else [float("nan")] * len(texts)
|
| 364 |
+
)
|
| 365 |
+
ranked = [(t.strip(), s) for t, s in zip(texts, values)]
|
| 366 |
+
return ranked[:top_k] if top_k else ranked
|