ballagb19's picture
Upload captcha_solver/solvers/audio_solver.py with huggingface_hub
ba18154 verified
Raw
History Blame Contribute Delete
3.2 kB
"""Audio captcha solver.
Strategy:
1. faster-whisper (tiny) with beam_size=1, language='en'
2. If ollama enabled, ask it to clean the digits
3. Local LLM (Qwen2.5-1.5B) as a digit-extraction fallback
We accept:
- MP3, WAV, OGG (any ffmpeg-decodable audio)
- Tries to extract ONLY digits if the text is clearly digit-only audio
- Returns the full transcript otherwise
"""
from __future__ import annotations
import re
from typing import Optional
from captcha_solver.solvers.base import BaseSolver, SolveAttempt
from captcha_solver.utils.image import decode_base64_audio
class AudioSolver(BaseSolver):
name = "audio"
captcha_type = "audio"
def __init__(self, ctx) -> None:
super().__init__(ctx)
self._audio: bytes = b""
def prepare(self, image_b64: Optional[str], audio_b64: Optional[str], hint: Optional[str]) -> None:
if not audio_b64:
self._audio = b""
return
try:
self._audio = decode_base64_audio(audio_b64)
except Exception as exc:
self._audio = b""
self._last_error = f"decode: {exc}"
def attempts(self):
return [
self._whisper_only,
self._whisper_plus_llm,
]
def _whisper_only(self) -> SolveAttempt:
if not self._audio:
return SolveAttempt(answer="", confidence=0.0, solver_name="audio.whisper", error="no audio")
text, conf = self.ctx.whisper.transcribe(self._audio, language="en", beam_size=1)
digits = _extract_digits(text)
if digits and len(digits) >= 3:
return SolveAttempt(answer=digits, confidence=max(conf, 0.6), solver_name="audio.whisper.digits")
if text:
return SolveAttempt(answer=text, confidence=conf, solver_name="audio.whisper.text")
return SolveAttempt(answer="", confidence=0.0, solver_name="audio.whisper", error="empty transcript")
def _whisper_plus_llm(self) -> SolveAttempt:
if not self._audio:
return SolveAttempt(answer="", confidence=0.0, solver_name="audio.whisper_llm", error="no audio")
text, conf = self.ctx.whisper.transcribe(self._audio, language="en", beam_size=5)
if not text:
return SolveAttempt(answer="", confidence=0.0, solver_name="audio.whisper_llm", error="empty")
try:
cleaned = self.ctx.qwen.generate(
f"Extract only the digits/words from this audio transcript. "
f"Return them in the order spoken, space-separated. No commentary.\n\nTranscript: {text}",
max_new_tokens=40,
)
except Exception as exc:
return SolveAttempt(answer="", confidence=0.0, solver_name="audio.whisper_llm", error=str(exc))
cleaned = cleaned.strip()
if not cleaned:
return SolveAttempt(answer="", confidence=0.0, solver_name="audio.whisper_llm", error="llm empty")
return SolveAttempt(answer=cleaned, confidence=min(conf + 0.1, 0.95), solver_name="audio.whisper_llm")
def _extract_digits(text: str) -> str:
digits = re.findall(r"\d", text)
if len(digits) >= 3:
return " ".join(digits)
return ""