Spaces:
Running
Running
File size: 19,623 Bytes
a2b450c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 | import whisper
import os
import torch
import warnings
import gc # Garbage Collector for memory management
import tempfile
import re
import time
from pydub import AudioSegment # 🟢 NEW: Add pydub
# Suppress FP16 warnings on CPU
warnings.filterwarnings("ignore", message="FP16 is not supported on CPU; using FP32 instead")
class AgentInput:
TURBO_MODEL_ID = "openai/whisper-large-v3-turbo"
DISTIL_MODEL_ID = "distil-whisper/distil-large-v3"
MMS_MODEL_ID = "facebook/mms-1b-all"
QWEN_ASR_MODEL_ID = "Qwen/Qwen3-ASR-1.7B"
def __init__(self, device="cpu"):
print(f"👂 Agent 1 (Input) Online: Preparing Whisper on {device}...")
self.device = device
self.loaded_models = {}
self.loaded_pipelines = {}
self.turbo_disabled = False
self.turbo_slow_threshold_s = float(os.environ.get("ASR_TURBO_SLOW_THRESHOLD_S", "18"))
# USE 'tiny' FOR CLOUD TO PREVENT EXIT CODE 137 (OOM)
# Use 'base' only if you are running on a machine with 16GB+ RAM
self.model_name = "tiny"
try:
# Load the model and immediately collect garbage to free RAM
self.model = whisper.load_model(self.model_name, device=device)
self.loaded_models[self.model_name] = self.model
gc.collect()
print(f"✅ Whisper '{self.model_name}' model loaded. RAM optimized.")
except Exception as e:
print(f"⚠️ Load failed: {e}. Attempting emergency load...")
# Emergency fallback to tiny if not already tried
self.model = whisper.load_model("tiny", device=device)
self.loaded_models["tiny"] = self.model
self.model_name = "tiny"
def _audio_duration_s(self, audio_path):
try:
return float(AudioSegment.from_file(audio_path).duration_seconds)
except Exception:
return None
def _hint_text(self, language=None, dialect_hint=""):
return f"{language or ''} {dialect_hint or ''}".strip().lower()
def _is_zero_gpu_available(self):
return torch.cuda.is_available()
def _is_underrepresented_language(self, language=None, dialect_hint=""):
hint = self._hint_text(language, dialect_hint)
common_tokens = {
"english", " en", "en ", "korean", " ko", "ko ", "arabic", " ar", "ar ",
"chinese", "mandarin", "cantonese", " zh", "zh ", "french", " fr", "fr ",
"spanish", " es", "es ", "german", "deutsch", " de", "de ", "italian",
" it", "it ", "portuguese", " pt", "pt ", "hindi", " hi", "hi ",
"japanese", " ja", "ja ", "russian", " ru", "ru "
}
if any(token in f" {hint} " for token in common_tokens):
return False
return bool(hint)
def _expected_script(self, language=None, dialect_hint=""):
lang = str(language or "").strip().lower()
padded_lang = f" {lang} "
if lang in {"en", "eng", "fr", "fra", "es", "spa", "de", "deu", "it", "ita", "pt", "por", "pcm", "tl", "tgl", "fil"}:
return "latin"
if lang in {"ko", "kor"}:
return "hangul"
if lang in {"ar", "ara", "ur", "urd", "fa", "fas", "prs", "ps", "pus"}:
return "arabic"
if lang in {"zh", "cmn", "yue", "zho"}:
return "cjk"
if lang in {"ja", "jpn"}:
return "japanese"
if lang in {"hi", "hin", "mr", "mar", "ne", "nep"}:
return "devanagari"
if lang in {"th", "tha"}:
return "thai"
hint = self._hint_text(language, dialect_hint)
padded_hint = f" {hint} "
if any(token in padded_hint for token in ["korean", "hangul", "satoori", "jeju", "gyeongsang", "chungcheong", "jeolla", "busan", " ko "]):
return "hangul"
if any(token in padded_hint for token in ["arabic", "urdu", "persian", "farsi", "dari", "pashto", " ar "]):
return "arabic"
if any(token in padded_hint for token in ["chinese", "mandarin", "cantonese", "yue", " zh "]):
return "cjk"
if any(token in padded_hint for token in ["japanese", " ja "]):
return "japanese"
if any(token in padded_hint for token in ["hindi", "marathi", "nepali", " hi "]):
return "devanagari"
if "thai" in padded_hint:
return "thai"
if any(token in padded_hint for token in ["english", "french", "spanish", "german", "italian", "portuguese", "tagalog", "filipino", "pidgin", "patois", " en ", " fr ", " es ", " tl "]):
return "latin"
if any(token in padded_lang for token in [" en ", " eng ", " fr ", " fra ", " es ", " spa "]):
return "latin"
return None
def _looks_wrong_script(self, text, language=None, dialect_hint=""):
text = str(text or "").strip()
if not text:
return True
script = self._expected_script(language, dialect_hint)
if not script:
return False
patterns = {
"hangul": r"[\uac00-\ud7a3]",
"arabic": r"[\u0600-\u06ff]",
"cjk": r"[\u3400-\u9fff]",
"japanese": r"[\u3040-\u30ff\u3400-\u9fff]",
"devanagari": r"[\u0900-\u097f]",
"thai": r"[\u0e00-\u0e7f]",
"latin": r"[A-Za-z]",
}
expected_hits = len(re.findall(patterns[script], text))
if script == "latin":
other_hits = len(re.findall(r"[\u0600-\u06ff\u0900-\u097f\u0e00-\u0e7f\u3040-\u30ff\u3400-\u9fff\uac00-\ud7a3]", text))
return expected_hits == 0 or other_hits > expected_hits
return expected_hits == 0
def _mms_language_code(self, language=None, dialect_hint=""):
hint = self._hint_text(language, dialect_hint)
mappings = [
(["nigerian pidgin", "pidgin"], "pcm"),
(["tagalog", "filipino", " tl ", " tgl ", " fil "], "tgl"),
(["english", " en "], "eng"),
(["korean", " ko "], "kor"),
(["arabic", " ar "], "ara"),
(["mandarin", "chinese", " zh "], "cmn"),
(["cantonese", "yue"], "yue"),
(["french", " fr "], "fra"),
(["spanish", " es "], "spa"),
(["german", "deutsch", " de "], "deu"),
(["italian", " it "], "ita"),
(["portuguese", " pt "], "por"),
(["hindi", " hi "], "hin"),
(["japanese", " ja "], "jpn"),
(["russian", " ru "], "rus"),
(["thai"], "tha"),
(["vietnamese"], "vie"),
(["swahili"], "swh"),
(["yoruba"], "yor"),
(["igbo"], "ibo"),
(["hausa"], "hau"),
]
padded = f" {hint} "
for tokens, code in mappings:
if any(token in padded for token in tokens):
return code
return "eng"
def _normalize_manual_route(self, model_choice):
choice = str(model_choice or "auto").strip().lower()
aliases = {
"tiny": {"engine": "whisper", "model": "tiny", "label": "Whisper tiny (Edge fastest)"},
"whisper tiny": {"engine": "whisper", "model": "tiny", "label": "Whisper tiny (Edge fastest)"},
"base": {"engine": "whisper", "model": "base", "label": "Whisper base (Edge balanced)"},
"whisper base": {"engine": "whisper", "model": "base", "label": "Whisper base (Edge balanced)"},
"small": {"engine": "whisper", "model": "small", "label": "Whisper small (Fallback)"},
"whisper-small": {"engine": "whisper", "model": "small", "label": "Whisper small (Fallback)"},
"whisper small": {"engine": "whisper", "model": "small", "label": "Whisper small (Fallback)"},
"turbo": {"engine": "hf-whisper", "model": self.TURBO_MODEL_ID, "label": "Whisper large-v3 turbo (HF Pro)"},
"large-v3-turbo": {"engine": "hf-whisper", "model": self.TURBO_MODEL_ID, "label": "Whisper large-v3 turbo (HF Pro)"},
"whisper-large-v3-turbo": {"engine": "hf-whisper", "model": self.TURBO_MODEL_ID, "label": "Whisper large-v3 turbo (HF Pro)"},
self.TURBO_MODEL_ID: {"engine": "hf-whisper", "model": self.TURBO_MODEL_ID, "label": "Whisper large-v3 turbo (HF Pro)"},
"distil": {"engine": "hf-whisper", "model": self.DISTIL_MODEL_ID, "label": "Distil-Whisper (English fast)"},
"distil-large-v3": {"engine": "hf-whisper", "model": self.DISTIL_MODEL_ID, "label": "Distil-Whisper (English fast)"},
self.DISTIL_MODEL_ID: {"engine": "hf-whisper", "model": self.DISTIL_MODEL_ID, "label": "Distil-Whisper (English fast)"},
"qwen": {"engine": "hf-whisper", "model": self.QWEN_ASR_MODEL_ID, "label": "Qwen3-ASR 1.7B (Multilingual)"},
"qwen3-asr": {"engine": "hf-whisper", "model": self.QWEN_ASR_MODEL_ID, "label": "Qwen3-ASR 1.7B (Multilingual)"},
"qwen/qwen3-asr-1.7b": {"engine": "hf-whisper", "model": self.QWEN_ASR_MODEL_ID, "label": "Qwen3-ASR 1.7B (Multilingual)"},
self.QWEN_ASR_MODEL_ID.lower(): {"engine": "hf-whisper", "model": self.QWEN_ASR_MODEL_ID, "label": "Qwen3-ASR 1.7B (Multilingual)"},
"mms": {"engine": "mms", "model": self.MMS_MODEL_ID, "label": "MMS 1B (Low-resource)"},
self.MMS_MODEL_ID: {"engine": "mms", "model": self.MMS_MODEL_ID, "label": "MMS 1B (Low-resource)"},
}
return aliases.get(choice)
def _auto_routes(self, language=None, dialect_hint="", audio_path=None):
base = {"engine": "whisper", "model": "base", "label": "Whisper base (Edge balanced)"}
tiny = {"engine": "whisper", "model": "tiny", "label": "Whisper tiny (Edge fastest)"}
small = {"engine": "whisper", "model": "small", "label": "Whisper small (Fallback)"}
mms = {"engine": "mms", "model": self.MMS_MODEL_ID, "label": "MMS 1B (Low-resource)"}
turbo = {"engine": "hf-whisper", "model": self.TURBO_MODEL_ID, "label": "Whisper large-v3 turbo (HF Pro)"}
duration_s = self._audio_duration_s(audio_path) if audio_path else None
if self._is_zero_gpu_available() and not self.turbo_disabled and not (duration_s and duration_s > 18):
return [turbo, base, tiny]
if self._expected_script(language, dialect_hint) == "latin":
return [base, tiny]
if self._is_underrepresented_language(language, dialect_hint):
return [base, mms, small]
return [small]
def resolve_model_name(self, model_choice="auto", language=None, dialect_hint="", audio_path=None):
manual = self._normalize_manual_route(model_choice)
if manual:
return manual["model"]
return self._auto_routes(language=language, dialect_hint=dialect_hint, audio_path=audio_path)[0]["model"]
def _get_model(self, model_name):
model_name = model_name if model_name in {"tiny", "base", "small"} else "small"
if model_name in self.loaded_models:
self.model_name = model_name
self.model = self.loaded_models[model_name]
return self.model
try:
print(f"🔄 Loading Whisper '{model_name}' on demand...")
self.loaded_models[model_name] = whisper.load_model(model_name, device=self.device)
self.model_name = model_name
self.model = self.loaded_models[model_name]
gc.collect()
return self.model
except Exception as e:
print(f"⚠️ Whisper '{model_name}' unavailable ({e}); falling back to tiny.")
self.model_name = "tiny"
self.model = self.loaded_models.get("tiny") or whisper.load_model("tiny", device=self.device)
self.loaded_models["tiny"] = self.model
gc.collect()
return self.model
def _get_hf_pipeline(self, model_id):
if model_id in self.loaded_pipelines:
return self.loaded_pipelines[model_id]
try:
from transformers import pipeline
device_index = 0 if torch.cuda.is_available() else -1
kwargs = {"model": model_id, "device": device_index}
if torch.cuda.is_available():
kwargs["torch_dtype"] = torch.float16
print(f"🔄 Loading ASR pipeline '{model_id}'...")
self.loaded_pipelines[model_id] = pipeline("automatic-speech-recognition", **kwargs)
gc.collect()
return self.loaded_pipelines[model_id]
except Exception as e:
raise RuntimeError(f"ASR pipeline '{model_id}' unavailable: {e}") from e
def _get_mms_pipeline(self, language=None, dialect_hint=""):
lang_code = self._mms_language_code(language, dialect_hint)
cache_key = f"{self.MMS_MODEL_ID}:{lang_code}"
if cache_key in self.loaded_pipelines:
return self.loaded_pipelines[cache_key]
try:
from transformers import AutoProcessor, Wav2Vec2ForCTC, pipeline
device_index = 0 if torch.cuda.is_available() else -1
processor = AutoProcessor.from_pretrained(self.MMS_MODEL_ID)
model = Wav2Vec2ForCTC.from_pretrained(self.MMS_MODEL_ID)
try:
processor.tokenizer.set_target_lang(lang_code)
model.load_adapter(lang_code)
except Exception as e:
print(f"⚠️ MMS adapter '{lang_code}' unavailable ({e}); trying English adapter.")
lang_code = "eng"
processor.tokenizer.set_target_lang(lang_code)
model.load_adapter(lang_code)
cache_key = f"{self.MMS_MODEL_ID}:{lang_code}"
if torch.cuda.is_available():
model = model.to("cuda")
self.loaded_pipelines[cache_key] = pipeline(
"automatic-speech-recognition",
model=model,
tokenizer=processor.tokenizer,
feature_extractor=processor.feature_extractor,
device=device_index,
)
gc.collect()
return self.loaded_pipelines[cache_key]
except Exception as e:
raise RuntimeError(f"MMS ASR unavailable: {e}") from e
def _transcribe_with_route(self, route, clean_path, language=None, dialect_hint=""):
start = time.perf_counter()
if route["engine"] == "whisper":
model = self._get_model(route["model"])
result = model.transcribe(clean_path, language=language, fp16=False)
elif route["engine"] == "mms":
pipe = self._get_mms_pipeline(language, dialect_hint)
result = pipe(clean_path)
else:
pipe = self._get_hf_pipeline(route["model"])
generate_kwargs = {"task": "transcribe"}
if language:
generate_kwargs["language"] = language
result = pipe(clean_path, generate_kwargs=generate_kwargs)
elapsed_s = time.perf_counter() - start
if isinstance(result, dict):
text = str(result.get("text", "")).strip()
else:
text = str(result or "").strip()
if route["model"] == self.TURBO_MODEL_ID and elapsed_s > self.turbo_slow_threshold_s:
self.turbo_disabled = True
print(f"⚠️ Turbo route was slow ({elapsed_s:.2f}s); future Auto calls will use Whisper small.")
return text, elapsed_s
# 🟢 NEW HELPER: Sanitizes corrupted browser audio
def _sanitize_audio(self, audio_path):
try:
# Try to load it regardless of format
audio = AudioSegment.from_file(audio_path)
# Export it to a clean, standard WAV in a temp file
temp_path = os.path.join(tempfile.gettempdir(), f"clean_audio_{os.path.basename(audio_path)}.wav")
audio.export(temp_path, format="wav")
return temp_path
except Exception as e:
print(f"⚠️ Audio Sanitization Warning: {e}")
return audio_path # Fallback to original if pydub fails
def _should_sanitize_audio(self, sanitize_audio=True):
if isinstance(sanitize_audio, str):
return sanitize_audio.strip().lower() not in {"off", "false", "0", "no", "raw", "none"}
return bool(sanitize_audio)
def transcribe(self, audio_path, language=None, model_choice="auto", dialect_hint="", sanitize_audio=True):
if not audio_path:
return [{"text": "", "speaker": "SYSTEM"}]
try:
clean_path = self._sanitize_audio(audio_path) if self._should_sanitize_audio(sanitize_audio) else audio_path
manual_route = self._normalize_manual_route(model_choice)
routes = [manual_route] if manual_route else self._auto_routes(language=language, dialect_hint=dialect_hint, audio_path=clean_path)
if manual_route and manual_route["engine"] != "whisper":
routes.append({"engine": "whisper", "model": "small", "label": "Whisper small (Fallback)"})
transcription_text = ""
used_label = "Unknown"
underrepresented = self._is_underrepresented_language(language, dialect_hint)
tried_mms_retry = False
for route in routes:
try:
text, elapsed_s = self._transcribe_with_route(route, clean_path, language=language, dialect_hint=dialect_hint)
used_label = route["label"]
print(f"🎙️ Speech model route: requested={model_choice or 'auto'} resolved={used_label} time={elapsed_s:.2f}s")
transcription_text = text
if not manual_route and route["model"] == self.TURBO_MODEL_ID and elapsed_s > self.turbo_slow_threshold_s:
transcription_text = ""
continue
needs_mms_retry = (
not manual_route
and route["engine"] != "mms"
and not tried_mms_retry
and (underrepresented or self._looks_wrong_script(text, language, dialect_hint))
)
if needs_mms_retry:
tried_mms_retry = True
mms_route = {"engine": "mms", "model": self.MMS_MODEL_ID, "label": "MMS 1B (Low-resource)"}
print("🔁 ASR output needs low-resource validation; retrying with MMS 1B.")
mms_text, mms_elapsed = self._transcribe_with_route(mms_route, clean_path, language=language, dialect_hint=dialect_hint)
if mms_text and not self._looks_wrong_script(mms_text, language, dialect_hint):
transcription_text = mms_text
used_label = mms_route["label"]
print(f"🎙️ Speech model route: retry resolved={used_label} time={mms_elapsed:.2f}s")
if transcription_text:
break
except Exception as route_error:
print(f"⚠️ ASR route '{route['label']}' failed: {route_error}")
continue
gc.collect() # Force memory release
# Clean up temp file
if clean_path != audio_path and os.path.exists(clean_path):
os.remove(clean_path)
return [{"text": transcription_text, "speaker": "Speaker 1", "model": used_label}]
except Exception as e:
print(f"❌ Transcription Error: {e}")
return [{"text": "", "speaker": "ERROR"}]
|