Shivam commited on
Commit
63a45c6
·
1 Parent(s): 0f02b5f

evaluation methods update

Browse files
backends/auto.py CHANGED
@@ -1,6 +1,8 @@
1
  """
2
- When the model id matches a known special stack, use that first; otherwise try:
3
- ASR pipeline -> SpeechSeq2Seq -> AutoModel (remote code) -> CTC.
 
 
4
  """
5
 
6
  from __future__ import annotations
@@ -8,12 +10,10 @@ from __future__ import annotations
8
  from collections.abc import Callable
9
 
10
  from . import (
11
- cohere_asr,
12
  granite_speech,
13
- transformers_auto_remote,
14
  transformers_ctc,
15
  transformers_pipeline,
16
- transformers_seq2seq,
17
  )
18
 
19
 
@@ -22,54 +22,34 @@ def _is_granite_speech_model(model_id: str) -> bool:
22
  return "ibm-granite/" in m and "granite-" in m and "-speech" in m
23
 
24
 
25
- def _is_cohere_transcribe_model(model_id: str) -> bool:
26
- m = model_id.lower()
27
- return "cohere-transcribe" in m or (
28
- m.startswith("coherelabs/") and "transcribe" in m
29
- )
30
-
31
-
32
  def build_transcriber(
33
  model_id: str,
34
  device_str: str,
35
  device_int: int,
36
  ) -> tuple[Callable[..., str], Callable[[], None]]:
37
- last_err: Exception | None = None
38
 
39
- # Prefer authoritative recipes before the generic ASR pipeline (avoids wrong wrappers).
40
  if _is_granite_speech_model(model_id):
41
  try:
42
  return granite_speech.build_transcriber(model_id, device_str)
43
  except Exception as e:
44
- last_err = e
45
-
46
- if _is_cohere_transcribe_model(model_id):
47
- try:
48
- return cohere_asr.build_transcriber(model_id, device_str)
49
- except Exception as e:
50
- last_err = e
51
 
52
  try:
53
  return transformers_pipeline.build_transcriber(model_id, device_int)
54
  except Exception as e:
55
- last_err = e
56
-
57
- try:
58
- return transformers_seq2seq.build_transcriber(model_id, device_str)
59
- except Exception as e:
60
- last_err = e
61
 
62
  try:
63
- return transformers_auto_remote.build_transcriber(model_id, device_str)
64
  except Exception as e:
65
- last_err = e
66
 
67
  try:
68
  return transformers_ctc.build_transcriber(model_id, device_str)
69
  except Exception as e:
70
- last_err = e
71
 
72
  raise RuntimeError(
73
- f"Could not load {model_id} with Granite speech, Cohere transcribe, pipeline, "
74
- f"SpeechSeq2Seq, AutoModel (remote), or CTC. Last error: {last_err}"
75
- ) from last_err
 
1
  """
2
+ Simplified backend cascade:
3
+ pipeline -> (Granite chat path if the id matches) -> universal -> CTC.
4
+
5
+ `universal` replaces the old SpeechSeq2Seq / AutoModel+remote / Cohere-specific backends.
6
  """
7
 
8
  from __future__ import annotations
 
10
  from collections.abc import Callable
11
 
12
  from . import (
 
13
  granite_speech,
 
14
  transformers_ctc,
15
  transformers_pipeline,
16
+ universal,
17
  )
18
 
19
 
 
22
  return "ibm-granite/" in m and "granite-" in m and "-speech" in m
23
 
24
 
 
 
 
 
 
 
 
25
  def build_transcriber(
26
  model_id: str,
27
  device_str: str,
28
  device_int: int,
29
  ) -> tuple[Callable[..., str], Callable[[], None]]:
30
+ errors: list[str] = []
31
 
 
32
  if _is_granite_speech_model(model_id):
33
  try:
34
  return granite_speech.build_transcriber(model_id, device_str)
35
  except Exception as e:
36
+ errors.append(f"granite_speech: {type(e).__name__}: {e}")
 
 
 
 
 
 
37
 
38
  try:
39
  return transformers_pipeline.build_transcriber(model_id, device_int)
40
  except Exception as e:
41
+ errors.append(f"pipeline: {type(e).__name__}: {e}")
 
 
 
 
 
42
 
43
  try:
44
+ return universal.build_transcriber(model_id, device_str)
45
  except Exception as e:
46
+ errors.append(f"universal: {type(e).__name__}: {e}")
47
 
48
  try:
49
  return transformers_ctc.build_transcriber(model_id, device_str)
50
  except Exception as e:
51
+ errors.append(f"ctc: {type(e).__name__}: {e}")
52
 
53
  raise RuntimeError(
54
+ f"Could not load {model_id}. Tried: " + " | ".join(errors)
55
+ )
 
backends/cohere_asr.py DELETED
@@ -1,56 +0,0 @@
1
- """
2
- Cohere Transcribe (CohereLabs/cohere-transcribe-*): `CohereAsrForConditionalGeneration.transcribe()`.
3
-
4
- This path does **not** use generic `generate()` + `batch_decode`; the model exposes `transcribe(processor=..., audio_arrays=..., ...)`.
5
- Requires a recent `transformers` (Cohere ASR support). Gated checkpoints need `HF_TOKEN` on the Space.
6
- Language: env `FFASR_COHERE_LANGUAGE` (default `en`).
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import os
12
- from collections.abc import Callable
13
-
14
- import numpy as np
15
- import torch
16
- from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor
17
-
18
-
19
- def build_transcriber(model_id: str, device_str: str) -> tuple[Callable[..., str], Callable[[], None]]:
20
- processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
21
- use_cuda = device_str == "cuda" and torch.cuda.is_available()
22
- dtype = torch.bfloat16 if use_cuda and torch.cuda.is_bf16_supported() else (
23
- torch.float16 if use_cuda else torch.float32
24
- )
25
- model = AutoModelForSpeechSeq2Seq.from_pretrained(
26
- model_id,
27
- trust_remote_code=True,
28
- torch_dtype=dtype,
29
- ).to(device_str)
30
- model.eval()
31
-
32
- if not hasattr(model, "transcribe"):
33
- raise RuntimeError(
34
- "Loaded class has no transcribe(); upgrade transformers to a build with Cohere ASR support."
35
- )
36
-
37
- lang = os.environ.get("FFASR_COHERE_LANGUAGE", "en").strip() or "en"
38
-
39
- def transcribe(audio_np: np.ndarray, sampling_rate: int = 16000) -> str:
40
- arr = np.asarray(audio_np, dtype=np.float32).reshape(-1)
41
- texts = model.transcribe(
42
- processor=processor,
43
- audio_arrays=[arr],
44
- sample_rates=[int(sampling_rate)],
45
- language=lang,
46
- )
47
- if not texts:
48
- return ""
49
- t = texts[0]
50
- return str(t).strip() if t is not None else ""
51
-
52
- def cleanup() -> None:
53
- nonlocal model, processor
54
- del model, processor
55
-
56
- return transcribe, cleanup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backends/registry.py CHANGED
@@ -8,49 +8,47 @@ from collections.abc import Callable
8
 
9
  from . import (
10
  auto,
11
- cohere_asr,
12
  granite_speech,
13
- transformers_auto_remote,
14
  transformers_ctc,
15
  transformers_pipeline,
16
- transformers_seq2seq,
17
  )
18
 
 
19
  # id -> (short label for CSV, one-line description for UI)
20
  FAMILY_META: dict[str, tuple[str, str]] = {
21
  "auto": (
22
  "Auto",
23
- "Pipeline, then Granite/Cohere-specific paths when the model id matches, then SpeechSeq2Seq, AutoModel+remote, CTC.",
24
  ),
25
  "transformers_pipeline": (
26
  "Transformers pipeline",
27
  "`pipeline('automatic-speech-recognition')` — Whisper, many bundled ASR heads.",
28
  ),
29
- "transformers_seq2seq": (
30
- "Transformers SpeechSeq2Seq",
31
- "`AutoModelForSpeechSeq2Seq` + `generate()` — use when pipeline fails (e.g. some remote-code models).",
32
- ),
33
- "transformers_ctc": (
34
- "Transformers CTC",
35
- "`AutoModelForCTC` — Wav2Vec2 / HuBERT CTC and similar.",
36
- ),
37
- "transformers_auto_remote": (
38
- "AutoModel + remote code (Whisper input_features)",
39
- "`AutoModel` + `trust_remote_code` + `generate(input_features)` — efficient-speech / custom Whisper checkpoints. "
40
- "Optional env `FFASR_REMOTE_PROCESSOR_ID` if the processor is not in the model repo (default fallback: openai/whisper-large-v3).",
41
  ),
42
  "granite_speech": (
43
  "IBM Granite speech (chat prompt)",
44
  "`ibm-granite/granite-*-speech`: chat template + `processor(prompt, wav)` + decode new tokens only. "
45
  "Optional env `FFASR_GRANITE_PROMPT` to override the default transcription prompt.",
46
  ),
47
- "cohere_asr": (
48
- "Cohere Transcribe",
49
- "`CohereLabs/cohere-transcribe-*`: uses the model's `transcribe()` API (not generic ASR pipeline). "
50
- "Gated checkpoints require Hub access + `HF_TOKEN`. Env `FFASR_COHERE_LANGUAGE` (default `en`).",
51
  ),
52
  }
53
 
 
 
 
 
 
 
 
54
  FAMILY_IDS: tuple[str, ...] = tuple(FAMILY_META.keys())
55
 
56
  # Gradio Dropdown expects each tuple as (label, value): label is shown; value is passed to Python.
@@ -60,17 +58,14 @@ DROPDOWN_CHOICES: list[tuple[str, str]] = [
60
 
61
 
62
  def normalize_family_id(raw: str | None) -> str | None:
63
- """
64
- Map UI / API input to a canonical family id.
65
-
66
- Gradio must receive (label, value) tuples in DROPDOWN_CHOICES; if an old client
67
- still sends the long label string, map it back to the id when possible.
68
- """
69
  if raw is None:
70
  return None
71
  s = raw.strip()
72
  if s in FAMILY_META:
73
  return s
 
 
74
  for fid, (short, desc) in FAMILY_META.items():
75
  long_label = f"{short} — {desc}"
76
  if s == long_label:
@@ -83,6 +78,8 @@ def default_family_id() -> str:
83
 
84
 
85
  def resolve_label(family_id: str) -> str:
 
 
86
  return FAMILY_META.get(family_id, (family_id, ""))[0]
87
 
88
 
@@ -92,18 +89,15 @@ def build_transcriber(
92
  device_str: str,
93
  device_int: int,
94
  ) -> tuple[Callable[..., str], Callable[[], None]]:
95
- if family_id == "auto":
 
96
  return auto.build_transcriber(model_id, device_str, device_int)
97
- if family_id == "transformers_pipeline":
98
  return transformers_pipeline.build_transcriber(model_id, device_int)
99
- if family_id == "transformers_seq2seq":
100
- return transformers_seq2seq.build_transcriber(model_id, device_str)
101
- if family_id == "transformers_ctc":
102
- return transformers_ctc.build_transcriber(model_id, device_str)
103
- if family_id == "transformers_auto_remote":
104
- return transformers_auto_remote.build_transcriber(model_id, device_str)
105
- if family_id == "granite_speech":
106
  return granite_speech.build_transcriber(model_id, device_str)
107
- if family_id == "cohere_asr":
108
- return cohere_asr.build_transcriber(model_id, device_str)
109
  raise ValueError(f"Unknown model family: {family_id!r}. Choose one of {FAMILY_IDS}.")
 
8
 
9
  from . import (
10
  auto,
 
11
  granite_speech,
 
12
  transformers_ctc,
13
  transformers_pipeline,
14
+ universal,
15
  )
16
 
17
+
18
  # id -> (short label for CSV, one-line description for UI)
19
  FAMILY_META: dict[str, tuple[str, str]] = {
20
  "auto": (
21
  "Auto",
22
+ "Pipeline → (Granite chat if id matches) → universal (AutoModelForSpeechSeq2Seq or AutoModel+remote) → CTC.",
23
  ),
24
  "transformers_pipeline": (
25
  "Transformers pipeline",
26
  "`pipeline('automatic-speech-recognition')` — Whisper, many bundled ASR heads.",
27
  ),
28
+ "universal": (
29
+ "Universal speech-seq2seq",
30
+ "`AutoModelForSpeechSeq2Seq` (or `AutoModel` + `trust_remote_code`) + `generate()` + `decode()`. "
31
+ "Works for Whisper, Cohere Transcribe, efficient-speech / custom Whisper variants, etc. "
32
+ "Env: `FFASR_PROCESSOR_ID`, `FFASR_LANGUAGE` (default `en`), `FFASR_MAX_NEW_TOKENS`.",
 
 
 
 
 
 
 
33
  ),
34
  "granite_speech": (
35
  "IBM Granite speech (chat prompt)",
36
  "`ibm-granite/granite-*-speech`: chat template + `processor(prompt, wav)` + decode new tokens only. "
37
  "Optional env `FFASR_GRANITE_PROMPT` to override the default transcription prompt.",
38
  ),
39
+ "transformers_ctc": (
40
+ "Transformers CTC",
41
+ "`AutoModelForCTC` — Wav2Vec2 / HuBERT CTC and similar.",
 
42
  ),
43
  }
44
 
45
+ # Aliases: keep older submissions / CSV rows working after the cleanup.
46
+ _FAMILY_ALIASES: dict[str, str] = {
47
+ "transformers_seq2seq": "universal",
48
+ "transformers_auto_remote": "universal",
49
+ "cohere_asr": "universal",
50
+ }
51
+
52
  FAMILY_IDS: tuple[str, ...] = tuple(FAMILY_META.keys())
53
 
54
  # Gradio Dropdown expects each tuple as (label, value): label is shown; value is passed to Python.
 
58
 
59
 
60
  def normalize_family_id(raw: str | None) -> str | None:
61
+ """Map UI / API input (or an older CSV value) to a canonical family id."""
 
 
 
 
 
62
  if raw is None:
63
  return None
64
  s = raw.strip()
65
  if s in FAMILY_META:
66
  return s
67
+ if s in _FAMILY_ALIASES:
68
+ return _FAMILY_ALIASES[s]
69
  for fid, (short, desc) in FAMILY_META.items():
70
  long_label = f"{short} — {desc}"
71
  if s == long_label:
 
78
 
79
 
80
  def resolve_label(family_id: str) -> str:
81
+ if family_id in _FAMILY_ALIASES:
82
+ family_id = _FAMILY_ALIASES[family_id]
83
  return FAMILY_META.get(family_id, (family_id, ""))[0]
84
 
85
 
 
89
  device_str: str,
90
  device_int: int,
91
  ) -> tuple[Callable[..., str], Callable[[], None]]:
92
+ fid = _FAMILY_ALIASES.get(family_id, family_id)
93
+ if fid == "auto":
94
  return auto.build_transcriber(model_id, device_str, device_int)
95
+ if fid == "transformers_pipeline":
96
  return transformers_pipeline.build_transcriber(model_id, device_int)
97
+ if fid == "universal":
98
+ return universal.build_transcriber(model_id, device_str)
99
+ if fid == "granite_speech":
 
 
 
 
100
  return granite_speech.build_transcriber(model_id, device_str)
101
+ if fid == "transformers_ctc":
102
+ return transformers_ctc.build_transcriber(model_id, device_str)
103
  raise ValueError(f"Unknown model family: {family_id!r}. Choose one of {FAMILY_IDS}.")
backends/transformers_auto_remote.py DELETED
@@ -1,114 +0,0 @@
1
- """
2
- Hugging Face `AutoModel` + `trust_remote_code=True` with Whisper-style `input_features` + `generate()`.
3
-
4
- Used by compressed / custom Whisper checkpoints (e.g. efficient-speech/*) that are **not**
5
- registered as `AutoModelForSpeechSeq2Seq` but follow: processor → `input_features` → `model.generate(...)`.
6
-
7
- Processor repo often differs from the weight repo (e.g. weights on `efficient-speech/...`,
8
- processor on `openai/whisper-large-v3`). Override with env `FFASR_REMOTE_PROCESSOR_ID`.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import os
14
- from collections.abc import Callable
15
-
16
- import numpy as np
17
- import torch
18
- from transformers import AutoModel, AutoProcessor
19
-
20
-
21
- def _whisper_like_generate(model, processor, input_features: torch.Tensor):
22
- """
23
- LiteWhisper / Whisper checkpoints need task+language (or forced_decoder_ids); otherwise
24
- `generate` can fail validation on `decoder_input_ids` length vs `max_new_tokens`.
25
- """
26
- base = dict(max_new_tokens=2048, num_beams=1)
27
- attempts: list[dict] = [
28
- {**base, "task": "transcribe", "language": "en"},
29
- {**base, "task": "transcribe", "language": "english"},
30
- ]
31
-
32
- for extra in attempts:
33
- try:
34
- return model.generate(input_features, **extra)
35
- except Exception:
36
- try:
37
- return model.generate(input_features=input_features, **extra)
38
- except Exception:
39
- continue
40
-
41
- try:
42
- if hasattr(processor, "get_decoder_prompt_ids"):
43
- fd = processor.get_decoder_prompt_ids(language="english", task="transcribe")
44
- return model.generate(
45
- input_features,
46
- forced_decoder_ids=fd,
47
- max_new_tokens=2048,
48
- num_beams=1,
49
- )
50
- except Exception:
51
- pass
52
-
53
- try:
54
- return model.generate(input_features, **base)
55
- except Exception:
56
- return model.generate(input_features=input_features, **base)
57
-
58
-
59
- def _load_processor(model_id: str):
60
- """
61
- If `FFASR_REMOTE_PROCESSOR_ID` is set, use it (matches many model cards: weights on one repo,
62
- processor on `openai/whisper-large-v3`). Otherwise try the checkpoint repo, then Whisper large-v3.
63
- """
64
- override = os.environ.get("FFASR_REMOTE_PROCESSOR_ID", "").strip()
65
- if override:
66
- return AutoProcessor.from_pretrained(override, trust_remote_code=True)
67
- fallback = "openai/whisper-large-v3"
68
- try:
69
- return AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
70
- except Exception:
71
- return AutoProcessor.from_pretrained(fallback, trust_remote_code=True)
72
-
73
-
74
- def build_transcriber(model_id: str, device_str: str) -> tuple[Callable[..., str], Callable[[], None]]:
75
- processor = _load_processor(model_id)
76
- use_cuda = device_str == "cuda" and torch.cuda.is_available()
77
- dtype = torch.float16 if use_cuda else torch.float32
78
-
79
- model = AutoModel.from_pretrained(
80
- model_id,
81
- trust_remote_code=True,
82
- torch_dtype=dtype,
83
- ).to(device_str)
84
- model.eval()
85
-
86
- def transcribe(audio_np: np.ndarray, sampling_rate: int = 16000) -> str:
87
- wav = np.asarray(audio_np, dtype=np.float32).reshape(-1)
88
- inputs = processor(wav, sampling_rate=sampling_rate, return_tensors="pt")
89
- if hasattr(inputs, "input_features"):
90
- input_features = inputs.input_features
91
- elif isinstance(inputs, dict) and "input_features" in inputs:
92
- input_features = inputs["input_features"]
93
- else:
94
- raise RuntimeError(
95
- "Processor did not return input_features; set FFASR_REMOTE_PROCESSOR_ID to a "
96
- "Whisper-compatible processor (e.g. openai/whisper-large-v3)."
97
- )
98
- input_features = input_features.to(device_str)
99
- if input_features.dtype != dtype:
100
- input_features = input_features.to(dtype=dtype)
101
-
102
- with torch.no_grad():
103
- generated_ids = _whisper_like_generate(model, processor, input_features)
104
-
105
- text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
106
- return str(text).strip()
107
-
108
- def cleanup() -> None:
109
- # `nonlocal` is required: otherwise `del model` makes `model` local to `cleanup` and raises
110
- # UnboundLocalError ("cannot access local variable 'model'...").
111
- nonlocal model, processor
112
- del model, processor
113
-
114
- return transcribe, cleanup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backends/transformers_seq2seq.py DELETED
@@ -1,45 +0,0 @@
1
- """
2
- Hugging Face SpeechSeq2Seq: AutoProcessor + AutoModelForSpeechSeq2Seq + generate().
3
- For models that do not work with the ASR pipeline (e.g. some remote-code Granite stacks).
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from collections.abc import Callable
9
-
10
- import numpy as np
11
- import torch
12
- from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor
13
-
14
-
15
- def _prepare_inputs(processor, waveform_batched, sampling_rate: int):
16
- try:
17
- return processor(waveform_batched, sampling_rate=sampling_rate, return_tensors="pt")
18
- except TypeError:
19
- try:
20
- return processor(audios=waveform_batched, sampling_rate=sampling_rate, return_tensors="pt")
21
- except TypeError:
22
- return processor(waveform_batched, return_tensors="pt")
23
-
24
-
25
- def build_transcriber(model_id: str, device_str: str) -> tuple[Callable[..., str], Callable[[], None]]:
26
- processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
27
- model = AutoModelForSpeechSeq2Seq.from_pretrained(model_id, trust_remote_code=True).to(device_str)
28
- model.eval()
29
-
30
- def transcribe(audio_np: np.ndarray, sampling_rate: int = 16000) -> str:
31
- wav = torch.from_numpy(audio_np).float()
32
- if wav.dim() == 1:
33
- wav = wav.unsqueeze(0)
34
- inputs = _prepare_inputs(processor, wav, sampling_rate)
35
- inputs = {k: v.to(device_str) if hasattr(v, "to") else v for k, v in inputs.items()}
36
- with torch.no_grad():
37
- generated_ids = model.generate(**inputs, max_new_tokens=256)
38
- pred_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
39
- return pred_text
40
-
41
- def cleanup() -> None:
42
- nonlocal model, processor
43
- del model, processor
44
-
45
- return transcribe, cleanup
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backends/universal.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ One ASR backend for ~every speech-seq2seq model: built-in (Whisper, CohereAsr, ...) and
3
+ remote-code (efficient-speech, custom Whisper variants, etc.).
4
+
5
+ Flow:
6
+ processor(audio, sr, return_tensors="pt", [language="en"])
7
+ model.generate(**inputs, max_new_tokens=_safe_cap(model))
8
+ processor.decode(outputs, ...) (falls back to processor.batch_decode)
9
+
10
+ Loading order:
11
+ 1. `AutoModelForSpeechSeq2Seq.from_pretrained(..., trust_remote_code=True)`
12
+ Covers standard HF classes (Whisper, CohereAsrForConditionalGeneration, ...).
13
+ 2. `AutoModel.from_pretrained(..., trust_remote_code=True)`
14
+ Covers repos that expose their generator class via `auto_map["AutoModel"]`
15
+ (e.g. `efficient-speech/lite-whisper-*`).
16
+
17
+ Environment overrides (all optional):
18
+ FFASR_PROCESSOR_ID — force a fallback processor (e.g. `openai/whisper-large-v3`).
19
+ FFASR_LANGUAGE — pass language=... to processor/generate when supported. Default: `en`.
20
+ FFASR_MAX_NEW_TOKENS — override generate() length budget. Default: auto (safe cap).
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ from collections.abc import Callable
27
+
28
+ import numpy as np
29
+ import torch
30
+ from transformers import AutoModel, AutoModelForSpeechSeq2Seq, AutoProcessor
31
+
32
+
33
+ def _safe_max_new_tokens(model, env_override: int | None, default_cap: int = 256) -> int:
34
+ """
35
+ Whisper-style models enforce `len(decoder_prefix) + max_new_tokens ≤ max_target_positions`.
36
+ Derive a cap from the config so we never trip that validation.
37
+ """
38
+ if env_override is not None and env_override > 0:
39
+ return int(env_override)
40
+ cfg = getattr(model, "config", None)
41
+ if cfg is None:
42
+ return default_cap
43
+ mtp = (
44
+ getattr(cfg, "max_target_positions", None)
45
+ or getattr(cfg, "max_length", None)
46
+ or getattr(cfg, "max_position_embeddings", None)
47
+ )
48
+ try:
49
+ mtp = int(mtp) if mtp else 0
50
+ except Exception:
51
+ mtp = 0
52
+ if mtp and mtp <= 2048:
53
+ # Leave headroom for task/language/timestamp prefix tokens.
54
+ return max(32, min(448, mtp - 16))
55
+ return default_cap
56
+
57
+
58
+ def _pick_dtype(device_str: str) -> torch.dtype:
59
+ use_cuda = device_str == "cuda" and torch.cuda.is_available()
60
+ if use_cuda and torch.cuda.is_bf16_supported():
61
+ return torch.bfloat16
62
+ if use_cuda:
63
+ return torch.float16
64
+ return torch.float32
65
+
66
+
67
+ def _load_processor(model_id: str):
68
+ override = os.environ.get("FFASR_PROCESSOR_ID", "").strip()
69
+ if override:
70
+ return AutoProcessor.from_pretrained(override, trust_remote_code=True)
71
+ try:
72
+ return AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
73
+ except Exception:
74
+ return AutoProcessor.from_pretrained(
75
+ "openai/whisper-large-v3", trust_remote_code=True
76
+ )
77
+
78
+
79
+ def _load_model(model_id: str, dtype: torch.dtype, device_str: str):
80
+ errors: list[str] = []
81
+ for cls in (AutoModelForSpeechSeq2Seq, AutoModel):
82
+ try:
83
+ model = cls.from_pretrained(
84
+ model_id,
85
+ trust_remote_code=True,
86
+ torch_dtype=dtype,
87
+ ).to(device_str)
88
+ return model, cls.__name__
89
+ except Exception as e:
90
+ errors.append(f"{cls.__name__}: {type(e).__name__}: {e}")
91
+ raise RuntimeError("; ".join(errors))
92
+
93
+
94
+ def _call_processor(processor, audio: np.ndarray, sampling_rate: int, language: str):
95
+ """Most ASR processors accept `sampling_rate` and either ignore or consume `language`."""
96
+ tries = (
97
+ dict(sampling_rate=sampling_rate, return_tensors="pt", language=language),
98
+ dict(sampling_rate=sampling_rate, return_tensors="pt"),
99
+ )
100
+ last_exc: Exception | None = None
101
+ for kwargs in tries:
102
+ try:
103
+ return processor(audio, **kwargs)
104
+ except (TypeError, ValueError) as e:
105
+ last_exc = e
106
+ continue
107
+ if last_exc is not None:
108
+ raise last_exc
109
+ return processor(audio, sampling_rate=sampling_rate, return_tensors="pt")
110
+
111
+
112
+ def _move_to_device(inputs, device_str: str, dtype: torch.dtype):
113
+ if hasattr(inputs, "to"):
114
+ inputs = inputs.to(device_str)
115
+ try:
116
+ inputs = inputs.to(dtype=dtype)
117
+ except Exception:
118
+ pass
119
+ batch = dict(inputs)
120
+ meta: dict = {}
121
+ for key in ("audio_chunk_index",):
122
+ if key in batch:
123
+ meta[key] = batch.pop(key)
124
+ return batch, meta
125
+
126
+
127
+ def _generate(model, batch: dict, max_new_tokens: int, language: str):
128
+ """Try language-aware generate first (Whisper path); fall back to plain generate."""
129
+ base = dict(max_new_tokens=max_new_tokens, num_beams=1)
130
+
131
+ attempts = (
132
+ {**base, "task": "transcribe", "language": language},
133
+ base,
134
+ )
135
+ last: Exception | None = None
136
+ for extra in attempts:
137
+ try:
138
+ return model.generate(**batch, **extra)
139
+ except TypeError as e:
140
+ last = e
141
+ except Exception as e:
142
+ # Whisper sometimes raises when a kwarg (language) is unsupported for this model.
143
+ last = e
144
+ if "language" in extra:
145
+ continue
146
+ raise
147
+ if last is not None:
148
+ raise last
149
+ return model.generate(**batch, **base)
150
+
151
+
152
+ def _decode(processor, outputs, meta: dict, language: str) -> str:
153
+ """Prefer `processor.decode` (Cohere / long-form chunking); else `batch_decode`."""
154
+ if hasattr(processor, "decode"):
155
+ try:
156
+ if meta.get("audio_chunk_index") is not None:
157
+ return str(
158
+ processor.decode(
159
+ outputs,
160
+ skip_special_tokens=True,
161
+ audio_chunk_index=meta["audio_chunk_index"],
162
+ language=language,
163
+ )
164
+ ).strip()
165
+ result = processor.decode(outputs, skip_special_tokens=True)
166
+ if isinstance(result, str):
167
+ return result.strip()
168
+ except TypeError:
169
+ pass
170
+ except Exception:
171
+ pass
172
+ try:
173
+ decoded = processor.batch_decode(outputs, skip_special_tokens=True)
174
+ return str(decoded[0]).strip() if decoded else ""
175
+ except Exception:
176
+ if hasattr(processor, "tokenizer"):
177
+ decoded = processor.tokenizer.batch_decode(outputs, skip_special_tokens=True)
178
+ return str(decoded[0]).strip() if decoded else ""
179
+ raise
180
+
181
+
182
+ def build_transcriber(model_id: str, device_str: str) -> tuple[Callable[..., str], Callable[[], None]]:
183
+ processor = _load_processor(model_id)
184
+ dtype = _pick_dtype(device_str)
185
+ model, _cls = _load_model(model_id, dtype, device_str)
186
+ model.eval()
187
+
188
+ language = os.environ.get("FFASR_LANGUAGE", "en").strip() or "en"
189
+ env_cap_raw = os.environ.get("FFASR_MAX_NEW_TOKENS", "").strip()
190
+ env_cap = int(env_cap_raw) if env_cap_raw.isdigit() else None
191
+ max_new = _safe_max_new_tokens(model, env_cap)
192
+
193
+ def transcribe(audio_np: np.ndarray, sampling_rate: int = 16000) -> str:
194
+ arr = np.asarray(audio_np, dtype=np.float32).reshape(-1)
195
+ inputs = _call_processor(processor, arr, int(sampling_rate), language)
196
+ batch, meta = _move_to_device(inputs, device_str, dtype)
197
+ with torch.no_grad():
198
+ outputs = _generate(model, batch, max_new, language)
199
+ return _decode(processor, outputs, meta, language)
200
+
201
+ def cleanup() -> None:
202
+ nonlocal model, processor
203
+ del model, processor
204
+
205
+ return transcribe, cleanup
constants.py CHANGED
@@ -16,7 +16,7 @@ INTRODUCTION_TEXT = (
16
  "We report the [WER](https://huggingface.co/learn/audio-course/chapter5/evaluation#word-error-rate) "
17
  "(lower the better) for each condition, plus an average. "
18
  "Models are ranked by Average WER, lowest to highest.\n\n"
19
- "On submit, choose a **model family** (Transformers pipeline, SpeechSeq2Seq, AutoModel+remote, CTC, or Auto) "
20
  "so the server loads the checkpoint the right way.\n\n"
21
  "The test data is **private** and never leaves our infrastructure. "
22
  "Submit a HuggingFace model ID below and we run the evaluation server-side.\n\n"
@@ -99,7 +99,7 @@ only accessed by this Space at inference time. This prevents data leakage into t
99
 
100
  ## Model compatibility
101
 
102
- Supported stacks include **Auto** (Granite speech / Cohere transcribe when the model id matches, then pipeline, SpeechSeq2Seq, AutoModel+remote, CTC), plus explicit families: **Transformers pipeline**, **SpeechSeq2Seq**, **AutoModel + remote code**, **IBM Granite speech**, **Cohere Transcribe**, **CTC**. Gated Hub models need a token with access. Pick the family that matches your checkpoint (or Auto if unsure).
103
 
104
  ## API usage
105
 
 
16
  "We report the [WER](https://huggingface.co/learn/audio-course/chapter5/evaluation#word-error-rate) "
17
  "(lower the better) for each condition, plus an average. "
18
  "Models are ranked by Average WER, lowest to highest.\n\n"
19
+ "On submit, choose a **model family** (Transformers pipeline, Universal speech-seq2seq, Granite speech, CTC, or Auto) "
20
  "so the server loads the checkpoint the right way.\n\n"
21
  "The test data is **private** and never leaves our infrastructure. "
22
  "Submit a HuggingFace model ID below and we run the evaluation server-side.\n\n"
 
99
 
100
  ## Model compatibility
101
 
102
+ Supported stacks: **Auto** (Transformers pipeline → Granite speech chat path if the id matches → Universal speech-seq2seq → CTC). Explicit families: **Transformers pipeline**, **Universal speech-seq2seq** (`AutoModelForSpeechSeq2Seq` or `AutoModel` with `trust_remote_code` — Whisper, Cohere Transcribe, efficient-speech / custom Whisper variants), **IBM Granite speech**, **CTC**. Gated Hub models need a token with access. Pick the family that matches your checkpoint (or Auto if unsure).
103
 
104
  ## API usage
105