ballagb19 commited on
Commit
b048fc9
·
verified ·
1 Parent(s): 42c7b5a

Upload captcha_solver/solvers/hcaptcha_solver.py with huggingface_hub

Browse files
captcha_solver/solvers/hcaptcha_solver.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """hCaptcha tile classifier.
2
+
3
+ Accepts a tile image + instruction text, returns yes/no classification.
4
+ Uses Florence-2 for phrase grounding / visual question answering.
5
+
6
+ This solver is designed for the POST /classify endpoint which the
7
+ Playwright bot calls for each tile in the hCaptcha grid.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from typing import Optional
14
+
15
+ from captcha_solver.solvers.base import BaseSolver, SolveAttempt
16
+ from captcha_solver.utils.image import decode_base64_image, image_to_pil
17
+
18
+
19
+ class HCaptchaSolver(BaseSolver):
20
+ name = "hcaptcha"
21
+ captcha_type = "hcaptcha"
22
+
23
+ def __init__(self, ctx) -> None:
24
+ super().__init__(ctx)
25
+ self._img = None
26
+ self._hint: str = ""
27
+
28
+ def prepare(self, image_b64: Optional[str], audio_b64: Optional[str], hint: Optional[str]) -> None:
29
+ if not image_b64:
30
+ self._img = None
31
+ return
32
+ try:
33
+ data = decode_base64_image(image_b64)
34
+ self._img = image_to_pil(data)
35
+ except Exception as exc:
36
+ self._img = None
37
+ self._last_error = f"decode: {exc}"
38
+ return
39
+ self._hint = (hint or "").strip()
40
+
41
+ def attempts(self):
42
+ return [
43
+ self._florence2_classify,
44
+ self._moondream_classify,
45
+ ]
46
+
47
+ def _florence2_classify(self) -> SolveAttempt:
48
+ """Classify tile using Florence-2."""
49
+ if self._img is None:
50
+ return SolveAttempt(
51
+ answer="no",
52
+ confidence=0.0,
53
+ solver_name="hcaptcha.florence2",
54
+ error="no image",
55
+ )
56
+ if not self.ctx.florence._loaded:
57
+ try:
58
+ self.ctx.florence.load()
59
+ except Exception as exc:
60
+ return SolveAttempt(
61
+ answer="no",
62
+ confidence=0.0,
63
+ solver_name="hcaptcha.florence2",
64
+ error=f"load failed: {exc}",
65
+ )
66
+
67
+ try:
68
+ import torch
69
+
70
+ # Use Florence-2's caption + phrase grounding to classify
71
+ # First, get a caption of the image
72
+ prompt = "<CAPTION>"
73
+ inputs = self.ctx.florence._processor(
74
+ text=prompt, images=self._img, return_tensors="pt"
75
+ ).to(self.ctx.florence._model.device)
76
+
77
+ with torch.no_grad():
78
+ gen = self.ctx.florence._model.generate(
79
+ input_ids=inputs["input_ids"],
80
+ pixel_values=inputs["pixel_values"].to(self.ctx.florence._model.dtype),
81
+ max_new_tokens=64,
82
+ num_beams=3,
83
+ do_sample=False,
84
+ )
85
+ caption = self.ctx.florence._processor.batch_decode(
86
+ gen, skip_special_tokens=False
87
+ )[0]
88
+ caption_parsed = self.ctx.florence._processor.post_process_generation(
89
+ caption, task="<CAPTION>", image_size=(self._img.width, self._img.height)
90
+ )
91
+ caption_text = str(caption_parsed.get("<CAPTION>", "")).lower()
92
+
93
+ # Now ask if the hint matches the caption
94
+ hint_lower = self._hint.lower()
95
+ if not hint_lower:
96
+ # No hint - return the caption as answer
97
+ return SolveAttempt(
98
+ answer=caption_text,
99
+ confidence=0.4,
100
+ solver_name="hcaptcha.florence2",
101
+ metadata={"caption": caption_text},
102
+ )
103
+
104
+ # Check if the hint words appear in the caption
105
+ hint_words = hint_lower.split()
106
+ matches = sum(1 for w in hint_words if w in caption_text)
107
+ ratio = matches / len(hint_words) if hint_words else 0
108
+
109
+ is_match = ratio >= 0.5 # At least half the hint words match
110
+ return SolveAttempt(
111
+ answer="yes" if is_match else "no",
112
+ confidence=0.75 if is_match else 0.65,
113
+ solver_name="hcaptcha.florence2",
114
+ metadata={"caption": caption_text, "match_ratio": ratio},
115
+ )
116
+
117
+ except Exception as exc:
118
+ return SolveAttempt(
119
+ answer="no",
120
+ confidence=0.0,
121
+ solver_name="hcaptcha.florence2",
122
+ error=str(exc),
123
+ )
124
+
125
+ def _moondream_classify(self) -> SolveAttempt:
126
+ """Classify tile using Moondream2 VQA."""
127
+ if self._img is None:
128
+ return SolveAttempt(
129
+ answer="no",
130
+ confidence=0.0,
131
+ solver_name="hcaptcha.moondream",
132
+ error="no image",
133
+ )
134
+
135
+ try:
136
+ hint = self._hint or "the main object"
137
+ question = f"Does this image contain {hint}? Answer yes or no only."
138
+ out = self.ctx.moondream.query(self._img, question, max_tokens=10)
139
+
140
+ is_yes = out.strip().lower().startswith("yes")
141
+ return SolveAttempt(
142
+ answer="yes" if is_yes else "no",
143
+ confidence=0.70 if is_yes else 0.60,
144
+ solver_name="hcaptcha.moondream",
145
+ metadata={"raw_answer": out},
146
+ )
147
+ except Exception as exc:
148
+ return SolveAttempt(
149
+ answer="no",
150
+ confidence=0.0,
151
+ solver_name="hcaptcha.moondream",
152
+ error=str(exc),
153
+ )
154
+
155
+
156
+ def classify_tile(image_b64: str, instruction: str, ctx) -> dict:
157
+ """Quick classifier for a single tile. Used by POST /classify.
158
+
159
+ Args:
160
+ image_b64: Base64-encoded tile image.
161
+ instruction: hCaptcha instruction (e.g. "Find all items that were made by people").
162
+ ctx: SolveContext with loaded engines.
163
+
164
+ Returns:
165
+ dict with "match" (bool), "confidence" (float), "caption" (str).
166
+ """
167
+ solver = HCaptchaSolver(ctx)
168
+ solver.prepare(image_b64, None, instruction)
169
+
170
+ # Try Florence-2 first, then Moondream
171
+ for attempt_fn in solver.attempts():
172
+ result = attempt_fn()
173
+ if result.confidence >= 0.5:
174
+ return {
175
+ "match": result.answer.lower() == "yes",
176
+ "confidence": result.confidence,
177
+ "caption": result.metadata.get("caption", result.answer),
178
+ "solver": result.solver_name,
179
+ }
180
+
181
+ return {
182
+ "match": False,
183
+ "confidence": 0.0,
184
+ "caption": "",
185
+ "solver": "hcaptcha.none",
186
+ }