"""faster-whisper engine for audio captcha transcription. Uses CTranslate2 under the hood. CPU-friendly, very small models (tiny=75MB, base=150MB). For captcha audio (digits/phrases with light noise) 'tiny' is sufficient and 10x faster than 'base'. """ from __future__ import annotations import os import tempfile from pathlib import Path from threading import Lock from typing import Optional from captcha_solver.engines.base import BaseEngine from captcha_solver.config import get_settings class WhisperEngine(BaseEngine): name = "whisper" def __init__(self) -> None: super().__init__() self._model = None self._lock = Lock() def _do_load(self) -> None: from faster_whisper import WhisperModel s = get_settings() cache = s.cache_dir / "whisper" cache.mkdir(parents=True, exist_ok=True) os.environ.setdefault("HF_HOME", str(s.cache_dir / "hf")) self._model = WhisperModel( s.whisper_model, device=s.whisper_device, compute_type=s.whisper_compute_type, download_root=str(cache), ) def _do_unload(self) -> None: self._model = None def transcribe( self, audio_bytes: bytes, language: Optional[str] = "en", beam_size: int = 1, ) -> tuple[str, float]: """Transcribe audio bytes. Returns (text, confidence). Confidence is the average segment log-prob (mapped to 0-1). """ if not self._loaded: self.load() assert self._model is not None with tempfile.NamedTemporaryFile(suffix=".audio", delete=False) as tmp: tmp.write(audio_bytes) tmp_path = tmp.name try: segments, info = self._model.transcribe( tmp_path, language=language, beam_size=beam_size, vad_filter=False, ) texts: list[str] = [] confs: list[float] = [] for seg in segments: texts.append(seg.text.strip()) if seg.avg_logprob is not None: import math p = math.exp(seg.avg_logprob) confs.append(max(0.0, min(1.0, p))) text = " ".join(t for t in texts if t).strip() conf = (sum(confs) / len(confs)) if confs else 0.0 return text, conf finally: try: Path(tmp_path).unlink(missing_ok=True) except Exception: pass