fcabla Claude Opus 4.8 (1M context) commited on
Commit
bb41be6
·
1 Parent(s): be44c1e

ASR: fix Cohere Transcribe loading on ZeroGPU (gated auth + device move + native class)

Browse files

Root cause of the dead mic: the model is gated and the Space ran unauthenticated,
so from_pretrained 401'd at download and the model never loaded (preload swallowed
it; every mic call returned ""). Fixed by adding the HF_TOKEN Space secret.

Code hardening so it actually transcribes once the gate opens:
- _load() now moves the fp16 model to CUDA inside the @spaces.GPU call (mirrors
game/model.py); without it, fp16 generate on CPU raises and returns "".
- Use the native CohereAsrForConditionalGeneration class (transformers>=5.11)
instead of AutoModelForSpeechSeq2Seq, which is not guaranteed to map cohere_asr.
- Proper librosa resample to 16k (guarded fallback to the old decimation).
- spaces.GPU duration 20->30 for the cold 2B host->device transfer.
- Add librosa + soundfile deps.
- Env-gated _selftest() (ASR_SELFTEST=1) transcribes the model's demo clip so the
deploy logs prove Cohere works without a mic.

No Whisper anywhere; on any failure returns "" and logs the exact error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. game/asr.py +46 -10
  2. requirements.txt +2 -0
game/asr.py CHANGED
@@ -2,8 +2,11 @@
2
 
3
  Backend: Cohere Transcribe 2B, exclusively (sponsor model; verbatim by design —
4
  transcribes what was SAID, not what it thinks you meant; EN + ES native).
5
- The witness may speak either language and the decoder prompt is language-specific,
6
- so we run both prompts and keep the transcript the model found more likely.
 
 
 
7
  """
8
  from __future__ import annotations
9
 
@@ -13,7 +16,7 @@ COHERE_ASR_ID = "CohereLabs/cohere-transcribe-03-2026"
13
 
14
  try:
15
  import spaces
16
- _gpu = spaces.GPU(duration=20)
17
  except Exception:
18
  def _gpu(fn):
19
  return fn
@@ -23,29 +26,36 @@ _backend = None # (processor, model)
23
 
24
 
25
  def _load():
 
 
26
  global _backend
27
  if _backend is None:
28
  with _lock:
29
  if _backend is None:
30
  import torch
31
- from transformers import AutoProcessor, AutoModelForSpeechSeq2Seq
32
  proc = AutoProcessor.from_pretrained(COHERE_ASR_ID)
33
- mdl = AutoModelForSpeechSeq2Seq.from_pretrained(
34
  COHERE_ASR_ID, torch_dtype=torch.float16)
35
  _backend = (proc, mdl)
36
  print("[asr] backend: cohere-transcribe", flush=True)
37
- return _backend
 
 
 
 
38
 
39
 
40
  def preload():
41
  _load()
 
42
 
43
 
44
  @_gpu
45
  def transcribe(audio: tuple[int, "np.ndarray"] | None, language: str = "en") -> str:
46
  """gr.Audio mic tuple -> witness text in the chosen language ('en'/'es').
47
- One pass (the witness picks the language), so no double decode. Empty
48
- string on any failure."""
49
  import numpy as np
50
 
51
  if audio is None:
@@ -64,9 +74,14 @@ def transcribe(audio: tuple[int, "np.ndarray"] | None, language: str = "en") ->
64
  import torch
65
  proc, mdl = _load()
66
  if sr != 16000:
67
- idx = np.linspace(0, len(wav) - 1, int(len(wav) * 16000 / sr))
68
- wav = wav[np.floor(idx).astype(int)]
 
 
 
 
69
  for lang in order:
 
70
  inputs = proc(wav, language=lang, sampling_rate=16000, return_tensors="pt")
71
  inputs = {k: (v.to(mdl.device, dtype=mdl.dtype) if v.dtype.is_floating_point
72
  else v.to(mdl.device)) for k, v in inputs.items()
@@ -83,3 +98,24 @@ def transcribe(audio: tuple[int, "np.ndarray"] | None, language: str = "en") ->
83
  except Exception as e:
84
  print(f"[asr] failed: {type(e).__name__}: {e}", flush=True)
85
  return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  Backend: Cohere Transcribe 2B, exclusively (sponsor model; verbatim by design —
4
  transcribes what was SAID, not what it thinks you meant; EN + ES native).
5
+ The witness picks the language in the UI; the decoder prompt is language-specific,
6
+ so we decode in that language and only retry the other one if the first is empty.
7
+
8
+ Gated model: the Space authenticates via the HF_TOKEN secret. On any failure the
9
+ function returns an empty string and logs the exact error — it NEVER fabricates text.
10
  """
11
  from __future__ import annotations
12
 
 
16
 
17
  try:
18
  import spaces
19
+ _gpu = spaces.GPU(duration=30) # cold 2B host->device transfer + short generate
20
  except Exception:
21
  def _gpu(fn):
22
  return fn
 
26
 
27
 
28
  def _load():
29
+ """ZeroGPU-canonical (mirrors game/model.py): weights load once on CPU; the
30
+ @spaces.GPU call moves them to CUDA. Keeps GPU calls short and admissible."""
31
  global _backend
32
  if _backend is None:
33
  with _lock:
34
  if _backend is None:
35
  import torch
36
+ from transformers import AutoProcessor, CohereAsrForConditionalGeneration
37
  proc = AutoProcessor.from_pretrained(COHERE_ASR_ID)
38
+ mdl = CohereAsrForConditionalGeneration.from_pretrained(
39
  COHERE_ASR_ID, torch_dtype=torch.float16)
40
  _backend = (proc, mdl)
41
  print("[asr] backend: cohere-transcribe", flush=True)
42
+ proc, mdl = _backend
43
+ import torch
44
+ if torch.cuda.is_available() and mdl.device.type != "cuda":
45
+ mdl.to("cuda")
46
+ return proc, mdl
47
 
48
 
49
  def preload():
50
  _load()
51
+ _selftest()
52
 
53
 
54
  @_gpu
55
  def transcribe(audio: tuple[int, "np.ndarray"] | None, language: str = "en") -> str:
56
  """gr.Audio mic tuple -> witness text in the chosen language ('en'/'es').
57
+ Decodes in the witness's language; only retries the other language if that
58
+ pass is empty. Empty string on any failure (never fabricated text)."""
59
  import numpy as np
60
 
61
  if audio is None:
 
74
  import torch
75
  proc, mdl = _load()
76
  if sr != 16000:
77
+ try: # proper anti-aliased resample; never hard-fail on a missing dep
78
+ import librosa
79
+ wav = librosa.resample(np.ascontiguousarray(wav), orig_sr=sr, target_sr=16000)
80
+ except Exception:
81
+ idx = np.linspace(0, len(wav) - 1, int(len(wav) * 16000 / sr))
82
+ wav = wav[np.floor(idx).astype(int)]
83
  for lang in order:
84
+ # `language` is a REQUIRED processor arg: it builds the decoder prompt.
85
  inputs = proc(wav, language=lang, sampling_rate=16000, return_tensors="pt")
86
  inputs = {k: (v.to(mdl.device, dtype=mdl.dtype) if v.dtype.is_floating_point
87
  else v.to(mdl.device)) for k, v in inputs.items()
 
98
  except Exception as e:
99
  print(f"[asr] failed: {type(e).__name__}: {e}", flush=True)
100
  return ""
101
+
102
+
103
+ def _selftest():
104
+ """Deploy-time smoke test (set ASR_SELFTEST=1): transcribe the model's own
105
+ demo clip so the Space logs prove Cohere works end-to-end, no mic needed."""
106
+ import os
107
+ if os.getenv("ASR_SELFTEST") != "1":
108
+ return
109
+ try:
110
+ import numpy as np
111
+ import soundfile as sf
112
+ from huggingface_hub import hf_hub_download
113
+ path = hf_hub_download(repo_id=COHERE_ASR_ID,
114
+ filename="demo/voxpopuli_test_en_demo.wav")
115
+ wav, sr = sf.read(path, dtype="float32")
116
+ text = transcribe((int(sr), np.asarray(wav)), "en")
117
+ print(f"[asr-selftest] OK: {text!r}", flush=True)
118
+ except Exception as e:
119
+ import traceback
120
+ print(f"[asr-selftest] FAIL: {type(e).__name__}: {e}\n{traceback.format_exc()}",
121
+ flush=True)
requirements.txt CHANGED
@@ -6,3 +6,5 @@ torch
6
  spaces
7
  voxcpm
8
  numpy
 
 
 
6
  spaces
7
  voxcpm
8
  numpy
9
+ librosa # proper 48k->16k resample for mic audio (Cohere wants 16k)
10
+ soundfile # audio I/O backend (librosa + ASR self-test demo wav)