Thedeezat commited on
Commit
b7c127b
·
verified ·
1 Parent(s): c762f54

Italian ASR app bundle only

Browse files
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ .Python
4
+ *.so
5
+ .venv/
6
+ venv/
7
+ .env
README.md ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Italian Speech To Text
3
+ emoji: 🏃
4
+ colorFrom: yellow
5
+ colorTo: pink
6
+ sdk: gradio
7
+ sdk_version: 6.12.0
8
+ app_file: app.py
9
+ pinned: false
10
+ short_description: Italian ASR + English translation (Whisper + Marian). Optional local fine-tuned model in models/whisper_finetuned_it/.
11
+ ---
12
+
13
+ # Italian → Italian + English
14
+
15
+ Speak **Italian**; get **Italian transcription** and **English** translation.
16
+
17
+ - Default: `openai/whisper-small` + `Helsinki-NLP/opus-mt-it-en`
18
+ - Optional: copy your fine-tuned Whisper into `models/whisper_finetuned_it/` and set `ASR_REALTIME_MODE=finetuned` in Space variables.
19
+
20
+ ## Add your model
21
+
22
+ 1. Copy training outputs into `models/whisper_finetuned_it/` (`config.json`, tokenizer files, weights).
23
+ 2. Use **Git LFS** for large weight files when pushing this Space.
24
+ 3. In Space **Settings → Repository variables**: `ASR_REALTIME_MODE` = `finetuned`.
25
+
26
+ ## Optional environment variables
27
+
28
+ | Variable | Default | Meaning |
29
+ |----------|---------|---------|
30
+ | `ASR_WHISPER_MODEL` | `openai/whisper-small` | HF Whisper id if not using local finetuned |
31
+ | `ASR_REALTIME_MODE` | `quality` | `quality` = hub Whisper; `finetuned` = load `models/whisper_finetuned_it` |
32
+ | `ASR_WHISPER_FINETUNED_DIR` | (see above) | Override path to finetuned folder |
33
+ | `ASR_TRANSLATE` | `1` | Set `0` to disable English translation |
34
+ | `ASR_MIN_RMS` | `0.005` | Silence gate |
35
+
36
+ ## Local test
37
+
38
+ ```bash
39
+ cd Italian-Speech-to-Text
40
+ python app.py
41
+ ```
42
+
43
+ Configuration reference: https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Space: speak Italian → **Italian transcription + English translation**.
3
+
4
+ Whisper (Italian) + Marian IT→EN. Run locally: ``python app.py``
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+
12
+ os.environ.setdefault("ASR_REALTIME_MODE", "quality")
13
+ os.environ.setdefault("ASR_WHISPER_LANGUAGE", "italian")
14
+ os.environ.setdefault("ASR_TRANSLATE", "1")
15
+
16
+ import numpy as np
17
+ import gradio as gr
18
+
19
+ from italian_en_pipeline import ItalianEnglishPipeline
20
+
21
+ _SPACE_ROOT = Path(__file__).resolve().parent
22
+
23
+ _pipeline: ItalianEnglishPipeline | None = None
24
+
25
+
26
+ def _get_pipeline() -> ItalianEnglishPipeline:
27
+ global _pipeline
28
+ if _pipeline is None:
29
+ _pipeline = ItalianEnglishPipeline(project_root=str(_SPACE_ROOT))
30
+ return _pipeline
31
+
32
+
33
+ def transcribe(audio: tuple[int, np.ndarray] | None) -> tuple[str, str]:
34
+ """Gradio Audio (numpy) → (italian, english)."""
35
+ if audio is None:
36
+ return "", ""
37
+ sr, data = audio
38
+ if data is None or len(data) == 0:
39
+ return "", ""
40
+ x = np.asarray(data, dtype=np.float32)
41
+ if x.ndim > 1:
42
+ x = x.mean(axis=-1)
43
+ floats = x.reshape(-1).tolist()
44
+ return _get_pipeline().transcribe_chunk(floats, int(sr))
45
+
46
+
47
+ with gr.Blocks(title="Italian speech → Italian + English") as demo:
48
+ gr.Markdown(
49
+ "### Italian → Italian + English\n"
50
+ "Speak or upload **Italian** audio. Output is **recognized Italian** and **English** translation "
51
+ "(Whisper + Marian). Optional fine-tuned Whisper in `models/whisper_finetuned_it/`."
52
+ )
53
+ audio_in = gr.Audio(
54
+ sources=["microphone", "upload"],
55
+ type="numpy",
56
+ label="Audio (Italian)",
57
+ )
58
+ run_btn = gr.Button("Transcribe", variant="primary")
59
+ out_it = gr.Textbox(label="Italian (ASR)", lines=4)
60
+ out_en = gr.Textbox(label="English (translation)", lines=4)
61
+
62
+ run_btn.click(fn=transcribe, inputs=[audio_in], outputs=[out_it, out_en])
63
+
64
+
65
+ if __name__ == "__main__":
66
+ port = int(os.environ.get("PORT", "7860"))
67
+ demo.launch(server_name="0.0.0.0", server_port=port)
italian_en_pipeline.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Single pipeline: **Italian speech → Italian text + English translation** (Whisper + Marian).
3
+
4
+ Standalone copy for Hugging Face Space (same folder as ``app.py``). ``project_root`` is this
5
+ directory; optional fine-tuned Whisper lives under ``models/whisper_finetuned_it/``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import re
12
+ from collections import Counter
13
+ from typing import Optional
14
+
15
+ import numpy as np
16
+ import torch
17
+
18
+ import editdistance
19
+
20
+ from whisper_asr import WhisperASR
21
+
22
+ SAMPLE_RATE = 16_000
23
+
24
+
25
+ def _pick_device() -> torch.device:
26
+ if torch.cuda.is_available():
27
+ return torch.device("cuda")
28
+ if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
29
+ return torch.device("mps")
30
+ return torch.device("cpu")
31
+
32
+
33
+ _HF_ONLY_MODES = frozenset({"quality", "pretrained", "base", "hf", "huggingface"})
34
+
35
+
36
+ def _default_realtime_mode(default_finetuned: str) -> str:
37
+ explicit = (os.environ.get("ASR_REALTIME_MODE") or "").strip().lower()
38
+ if explicit:
39
+ return explicit or "quality"
40
+ custom = (os.environ.get("ASR_WHISPER_FINETUNED_DIR") or "").strip() or default_finetuned
41
+ cfg = os.path.join(custom, "config.json")
42
+ vocab = os.path.join(custom, "vocab.json")
43
+ if os.path.isdir(custom) and os.path.isfile(cfg) and os.path.isfile(vocab):
44
+ return "finetuned"
45
+ return "quality"
46
+
47
+
48
+ class ItalianEnglishPipeline:
49
+ """Whisper (Italian ASR) + Marian MT (IT→EN). One call returns both strings."""
50
+
51
+ def __init__(self, project_root: Optional[str] = None) -> None:
52
+ # Space root = directory containing this module (same as app.py).
53
+ _here = os.path.dirname(os.path.abspath(__file__))
54
+ self.project_root = project_root or _here
55
+ self.default_finetuned = os.path.join(self.project_root, "models", "whisper_finetuned_it")
56
+ self.device = _pick_device()
57
+
58
+ self.realtime_mode = _default_realtime_mode(self.default_finetuned)
59
+ self.whisper_model_name = (os.environ.get("ASR_WHISPER_MODEL") or "").strip() or "openai/whisper-small"
60
+ self.max_new = max(32, int(os.environ.get("ASR_WHISPER_MAX_NEW_TOKENS", "448")))
61
+ self.num_beams = max(1, int(os.environ.get("ASR_WHISPER_NUM_BEAMS", "5")))
62
+ self.min_rms = float(os.environ.get("ASR_MIN_RMS", "0.005"))
63
+
64
+ tr = (os.environ.get("ASR_TRANSLATE") or "1").strip().lower()
65
+ self.translate_enabled = tr not in ("0", "false", "no", "off")
66
+ self.translate_model_id = (os.environ.get("ASR_TRANSLATE_MODEL") or "Helsinki-NLP/opus-mt-it-en").strip()
67
+
68
+ self.lexicon: set[str] = self._load_lexicon_from_project()
69
+ self._whisper: Optional[WhisperASR] = None
70
+ self._whisper_failed = False
71
+ self._translator = None
72
+ self._translator_failed = False
73
+
74
+ def _load_lexicon_from_project(self) -> set[str]:
75
+ env_dir = (os.environ.get("ASR_WHISPER_FINETUNED_DIR") or "").strip()
76
+ for base in (env_dir or None, self.default_finetuned):
77
+ if not base:
78
+ continue
79
+ p = os.path.join(base, "lexicon.txt")
80
+ if os.path.isfile(p):
81
+ words: set[str] = set()
82
+ with open(p, "r", encoding="utf-8") as f:
83
+ for line in f:
84
+ w = line.strip().upper()
85
+ if w:
86
+ words.add(w)
87
+ return words
88
+ return set()
89
+
90
+ def resolve_whisper_model_id(self) -> str:
91
+ if self.realtime_mode in _HF_ONLY_MODES:
92
+ return self.whisper_model_name
93
+ custom = (os.environ.get("ASR_WHISPER_FINETUNED_DIR") or "").strip() or self.default_finetuned
94
+ if os.path.isdir(custom) and os.path.isfile(os.path.join(custom, "config.json")):
95
+ return custom
96
+ print(
97
+ f"[WARN] No fine-tuned Whisper at {custom}; falling back to {self.whisper_model_name}. "
98
+ "Use ASR_REALTIME_MODE=quality for pretrained-only."
99
+ )
100
+ return self.whisper_model_name
101
+
102
+ def _whisper_pipe(self) -> Optional[WhisperASR]:
103
+ if self._whisper_failed:
104
+ return None
105
+ if self._whisper is None:
106
+ try:
107
+ mid = self.resolve_whisper_model_id()
108
+ self._whisper = WhisperASR(mid, self.device)
109
+ self._whisper.load()
110
+ except Exception as e:
111
+ print(f"[WARN] Could not load Whisper ({e}).")
112
+ self._whisper_failed = True
113
+ return None
114
+ return self._whisper
115
+
116
+ def translate_it_to_en(self, text: str) -> str:
117
+ if self._translator_failed or not self.translate_enabled:
118
+ return ""
119
+ t = (text or "").strip()
120
+ if not t:
121
+ return ""
122
+ if self._translator is None:
123
+ try:
124
+ from transformers import pipeline
125
+
126
+ self._translator = pipeline(
127
+ "translation",
128
+ model=self.translate_model_id,
129
+ device=-1,
130
+ )
131
+ print(f"[Pipeline] Translation loaded: {self.translate_model_id}")
132
+ except Exception as e:
133
+ self._translator_failed = True
134
+ print(f"[WARN] Translation pipeline failed ({e})")
135
+ return ""
136
+ try:
137
+ out = self._translator(t, max_length=512, clean_up_tokenization_spaces=True)
138
+ if isinstance(out, list) and out:
139
+ return (out[0].get("translation_text") or "").strip()
140
+ return ""
141
+ except Exception as e:
142
+ print(f"[WARN] Translation failed: {e}")
143
+ return ""
144
+
145
+ @staticmethod
146
+ def _likely_repetition_hallucination(text: str) -> bool:
147
+ words = [w for w in re.split(r"\s+", text.strip()) if w]
148
+ if len(words) < 12:
149
+ return False
150
+ stripped = [re.sub(r"^[^\w]+|[^\w]+$", "", w, flags=re.UNICODE).lower() for w in words]
151
+ stripped = [w for w in stripped if w]
152
+ if len(stripped) < 12:
153
+ return False
154
+ c = Counter(stripped)
155
+ return c.most_common(1)[0][1] / len(stripped) >= 0.55
156
+
157
+ def _correct_word(self, word: str, max_dist: int = 2) -> str:
158
+ if not self.lexicon or not word:
159
+ return word
160
+ w = word.upper()
161
+ if w in self.lexicon:
162
+ return word
163
+ if len(w) < 3:
164
+ return word
165
+ best = w
166
+ best_d = max_dist + 1
167
+ for cand in self.lexicon:
168
+ if abs(len(cand) - len(w)) > 2:
169
+ continue
170
+ if len(w) >= 5 and (not cand or cand[0] != w[0]):
171
+ continue
172
+ d = editdistance.eval(w, cand)
173
+ if d < best_d:
174
+ best_d = d
175
+ best = cand
176
+ if d == 0:
177
+ break
178
+ if best_d <= max_dist:
179
+ return best
180
+ return word
181
+
182
+ def _correct_text(self, text: str) -> str:
183
+ if not text or not self.lexicon:
184
+ return text
185
+ words = text.split()
186
+ corrected = [self._correct_word(w, max_dist=1) for w in words]
187
+ merged: list[str] = []
188
+ i = 0
189
+ while i < len(corrected):
190
+ if i + 1 < len(corrected):
191
+ w1 = corrected[i]
192
+ w2 = corrected[i + 1]
193
+ joined = (w1 + w2).upper()
194
+ if joined in self.lexicon:
195
+ merged.append(joined)
196
+ i += 2
197
+ continue
198
+ merged.append(corrected[i])
199
+ i += 1
200
+ return " ".join(merged)
201
+
202
+ def _whisper_transcribe(self, audio_16k: np.ndarray) -> str:
203
+ pipe = self._whisper_pipe()
204
+ if pipe is None:
205
+ return ""
206
+ if audio_16k.ndim != 1:
207
+ audio_16k = audio_16k.reshape(-1)
208
+ raw = pipe.transcribe(
209
+ audio_16k,
210
+ SAMPLE_RATE,
211
+ max_new_tokens=self.max_new,
212
+ num_beams=self.num_beams,
213
+ )
214
+ return (raw or "").strip()
215
+
216
+ def _preprocess_chunk(self, audio_float32: list[float], sample_rate: int) -> Optional[np.ndarray]:
217
+ import torchaudio
218
+
219
+ arr = np.array(audio_float32, dtype=np.float32)
220
+ if len(arr) < 800:
221
+ return None
222
+ if sample_rate != SAMPLE_RATE:
223
+ t = torch.from_numpy(arr).float().unsqueeze(0).unsqueeze(0)
224
+ t = torchaudio.functional.resample(t, sample_rate, SAMPLE_RATE)
225
+ arr = t.squeeze().numpy()
226
+ arr = (
227
+ torchaudio.functional.highpass_biquad(
228
+ torch.from_numpy(arr).float().unsqueeze(0), SAMPLE_RATE, 80.0
229
+ )
230
+ .squeeze(0)
231
+ .numpy()
232
+ )
233
+ rms = float(np.sqrt(np.mean(arr**2))) if arr.size > 0 else 0.0
234
+ if rms < self.min_rms:
235
+ return None
236
+ return arr
237
+
238
+ def transcribe_chunk(self, audio_float32: list[float], sample_rate: int) -> tuple[str, str]:
239
+ """
240
+ Returns ``(italian_text, english_text)``. Empty strings if silence / rejected.
241
+ """
242
+ arr = self._preprocess_chunk(audio_float32, sample_rate)
243
+ if arr is None:
244
+ return "", ""
245
+
246
+ raw = self._whisper_transcribe(arr)
247
+ text = (raw or "").strip()
248
+ if not text:
249
+ return "", ""
250
+ if self._likely_repetition_hallucination(text):
251
+ return "", ""
252
+
253
+ en = self.translate_it_to_en(text)
254
+ if self.lexicon:
255
+ text = self._correct_text(text.upper())
256
+ return text, en
257
+
258
+ def format_remote_response(self, italian_raw: str) -> tuple[str, str]:
259
+ """After a remote ASR returns Italian text: apply lexicon + translation."""
260
+ text = (italian_raw or "").strip()
261
+ if not isinstance(text, str):
262
+ text = str(text)
263
+ raw_for_tr = text
264
+ if text and self.lexicon:
265
+ text = self._correct_text(text.upper())
266
+ en = self.translate_it_to_en(raw_for_tr) if raw_for_tr else ""
267
+ return text, en
268
+
269
+
270
+ _pipeline_singleton: Optional[ItalianEnglishPipeline] = None
271
+
272
+
273
+ def get_pipeline(project_root: Optional[str] = None) -> ItalianEnglishPipeline:
274
+ global _pipeline_singleton
275
+ if _pipeline_singleton is None:
276
+ _pipeline_singleton = ItalianEnglishPipeline(project_root=project_root)
277
+ return _pipeline_singleton
models/whisper_finetuned_it/README.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Fine-tuned Whisper (optional)
2
+
3
+ Place your Italian fine-tuned Whisper checkpoint **here** (this folder), with at least:
4
+
5
+ - `config.json`
6
+ - `vocab.json` (or tokenizer files your training run produced)
7
+ - Model weights (e.g. `pytorch_model.bin` or safetensors)
8
+
9
+ Optional: `lexicon.txt` — one uppercase word per line for light post-correction.
10
+
11
+ When this folder looks like a valid Hugging Face `from_pretrained` directory, set in the Space **Settings → Variables**:
12
+
13
+ - `ASR_REALTIME_MODE=finetuned`
14
+
15
+ If the folder is empty or invalid, the app falls back to `ASR_WHISPER_MODEL` (default `openai/whisper-small`) with `ASR_REALTIME_MODE=quality`.
16
+
17
+ **Large files:** use [Git LFS](https://git-lfs.com) on the Space repo before pushing weights.
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.44.0
2
+ torch>=2.4.0
3
+ torchaudio>=2.4.0
4
+ transformers>=4.46.0
5
+ accelerate>=1.0.0
6
+ numpy>=2.1.0
7
+ sentencepiece>=0.1.99
8
+ editdistance>=0.8.0
whisper_asr.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Speech-to-text using **OpenAI Whisper** weights on Hugging Face (open weights, free to use).
3
+
4
+ Models: https://huggingface.co/openai — e.g. ``openai/whisper-tiny``, ``whisper-base``, ``whisper-small``.
5
+
6
+ Language for generation is set by the ``language`` argument or ``ASR_WHISPER_LANGUAGE``
7
+ (e.g. ``italian``, ``english``); Whisper expects the full language name in English.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ import os
14
+ from typing import Optional
15
+
16
+ import numpy as np
17
+ import torch
18
+ import torchaudio
19
+ from transformers import WhisperForConditionalGeneration, WhisperProcessor
20
+
21
+ SAMPLE_RATE = 16_000
22
+
23
+
24
+ def pick_device() -> torch.device:
25
+ if torch.cuda.is_available():
26
+ return torch.device("cuda")
27
+ if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
28
+ return torch.device("mps")
29
+ return torch.device("cpu")
30
+
31
+
32
+ class WhisperASR:
33
+ """Lazy-loadable Whisper STT (beam decode when supported)."""
34
+
35
+ def __init__(
36
+ self,
37
+ model_name: str,
38
+ device: Optional[torch.device] = None,
39
+ *,
40
+ language: Optional[str] = None,
41
+ ):
42
+ self.model_name = model_name
43
+ self.device = device or pick_device()
44
+ raw = (language or os.environ.get("ASR_WHISPER_LANGUAGE") or "italian").strip().lower()
45
+ self.language = raw or "italian"
46
+ self._model: Optional[WhisperForConditionalGeneration] = None
47
+ self._processor: Optional[WhisperProcessor] = None
48
+
49
+ def load(self) -> None:
50
+ if self._model is not None:
51
+ return
52
+ self._processor = WhisperProcessor.from_pretrained(self.model_name)
53
+ self._model = WhisperForConditionalGeneration.from_pretrained(self.model_name).to(
54
+ self.device
55
+ )
56
+ self._model.eval()
57
+ print(f"[WhisperASR] Loaded {self.model_name} on {self.device}")
58
+
59
+ @property
60
+ def model(self) -> WhisperForConditionalGeneration:
61
+ self.load()
62
+ assert self._model is not None
63
+ return self._model
64
+
65
+ @property
66
+ def processor(self) -> WhisperProcessor:
67
+ self.load()
68
+ assert self._processor is not None
69
+ return self._processor
70
+
71
+ def _cap_max_new_tokens(self, requested: int) -> int:
72
+ """Whisper caps total decoder length at ``max_target_positions``; prefix tokens count too."""
73
+ lim = int(getattr(self.model.config, "max_target_positions", 448))
74
+ # Room for language/task start tokens (transformers warns if max_new_tokens == lim exactly).
75
+ margin = 24
76
+ return max(1, min(requested, lim - margin))
77
+
78
+ @torch.no_grad()
79
+ def transcribe(
80
+ self,
81
+ waveform: np.ndarray,
82
+ sample_rate: int,
83
+ *,
84
+ max_new_tokens: int = 440,
85
+ num_beams: int = 5,
86
+ no_speech_threshold: float | None = None,
87
+ compression_ratio_threshold: float | None = None,
88
+ logprob_threshold: float | None = None,
89
+ ) -> str:
90
+ """``waveform`` mono float32; any sample rate (resampled to 16 kHz)."""
91
+ w = np.asarray(waveform, dtype=np.float32).reshape(-1)
92
+ if w.size == 0:
93
+ return ""
94
+ if not math.isfinite(float(np.max(np.abs(w)))):
95
+ return ""
96
+ sr = int(sample_rate)
97
+ t = torch.from_numpy(w).unsqueeze(0)
98
+ if sr != SAMPLE_RATE:
99
+ t = torchaudio.functional.resample(t, sr, SAMPLE_RATE)
100
+ audio_16k = t.squeeze(0).numpy()
101
+
102
+ inputs = self.processor(audio_16k, sampling_rate=SAMPLE_RATE, return_tensors="pt")
103
+ input_features = inputs["input_features"].to(self.device)
104
+ attention_mask = inputs.get("attention_mask")
105
+ if attention_mask is not None:
106
+ attention_mask = attention_mask.to(self.device)
107
+
108
+ cap = self._cap_max_new_tokens(max_new_tokens)
109
+
110
+ # Whisper-specific thresholds reduce silence/noise hallucinations (HF transformers).
111
+ # Defaults match OpenAI-ish behavior; stricter than "unset" when backends ignore None.
112
+ if no_speech_threshold is None:
113
+ no_speech_threshold = float(os.environ.get("ASR_NO_SPEECH_THRESHOLD", "0.65"))
114
+ if compression_ratio_threshold is None:
115
+ compression_ratio_threshold = float(os.environ.get("ASR_COMPRESSION_RATIO_THRESHOLD", "2.0"))
116
+ if logprob_threshold is None:
117
+ logprob_threshold = float(os.environ.get("ASR_LOGPROB_THRESHOLD", "-1.0"))
118
+
119
+ gen_common: dict = {
120
+ "max_new_tokens": cap,
121
+ "num_beams": num_beams,
122
+ "do_sample": False,
123
+ "language": self.language,
124
+ "task": "transcribe",
125
+ "no_speech_threshold": no_speech_threshold,
126
+ "compression_ratio_threshold": compression_ratio_threshold,
127
+ "logprob_threshold": logprob_threshold,
128
+ }
129
+ if attention_mask is not None:
130
+ gen_common["attention_mask"] = attention_mask
131
+
132
+ try:
133
+ ids = self.model.generate(input_features, **gen_common)
134
+ except TypeError:
135
+ gen_common.pop("no_speech_threshold", None)
136
+ gen_common.pop("compression_ratio_threshold", None)
137
+ gen_common.pop("logprob_threshold", None)
138
+ try:
139
+ ids = self.model.generate(input_features, **gen_common)
140
+ except TypeError:
141
+ ids = self.model.generate(
142
+ input_features,
143
+ max_new_tokens=cap,
144
+ num_beams=num_beams,
145
+ do_sample=False,
146
+ )
147
+
148
+ text = self.processor.batch_decode(ids, skip_special_tokens=True)[0]
149
+ return (text or "").strip()