ballagb19 commited on
Commit
c3d7bde
·
verified ·
1 Parent(s): 0834343

Upload captcha_solver/api/routes.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. captcha_solver/api/routes.py +249 -0
captcha_solver/api/routes.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI routes for the captcha solver API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from threading import Lock
7
+
8
+ from fastapi import APIRouter, HTTPException
9
+
10
+ from captcha_solver.config import get_settings
11
+ from captcha_solver.models import (
12
+ HealthResponse,
13
+ ModelsResponse,
14
+ SolveRequest,
15
+ SolveResponse,
16
+ StatsResponse,
17
+ )
18
+ from captcha_solver.solvers.router import SolverRouter
19
+ from captcha_solver.utils.cache import SolveCache
20
+ from captcha_solver.utils.image import audio_size_hash, perceptual_hash
21
+
22
+
23
+ router = APIRouter()
24
+
25
+ _state: dict = {
26
+ "router": None,
27
+ "cache": None,
28
+ "stats": {
29
+ "total": 0,
30
+ "by_type": {},
31
+ "by_solver": {},
32
+ "success": 0,
33
+ "elapsed_sum_ms": 0,
34
+ "cache_hits": 0,
35
+ "started_at": time.time(),
36
+ },
37
+ "_stats_lock": Lock(),
38
+ }
39
+
40
+
41
+ def get_state() -> dict:
42
+ if _state["router"] is None:
43
+ s = get_settings()
44
+ r = SolverRouter()
45
+ r.init()
46
+ _state["router"] = r
47
+ _state["cache"] = SolveCache(
48
+ ttl_seconds=s.cache_ttl_seconds,
49
+ max_entries=s.cache_max_entries,
50
+ )
51
+ return _state
52
+
53
+
54
+ @router.post("/solve", response_model=SolveResponse)
55
+ def solve(req: SolveRequest) -> SolveResponse:
56
+ s = get_settings()
57
+ state = get_state()
58
+ t0 = time.time()
59
+ captcha_type = req.type or "auto"
60
+
61
+ if not req.image_base64 and not req.audio_base64 and not req.hint:
62
+ raise HTTPException(status_code=400, detail="must provide image_base64, audio_base64, or hint")
63
+
64
+ cache_key = None
65
+ if req.use_cache and s.cache_enabled:
66
+ if req.image_base64:
67
+ try:
68
+ from captcha_solver.utils.image import decode_base64_image
69
+ cache_key = "img:" + perceptual_hash(decode_base64_image(req.image_base64))
70
+ except Exception:
71
+ cache_key = None
72
+ elif req.audio_base64:
73
+ try:
74
+ from captcha_solver.utils.image import decode_base64_audio
75
+ cache_key = "aud:" + audio_size_hash(decode_base64_audio(req.audio_base64))
76
+ except Exception:
77
+ cache_key = None
78
+ if cache_key:
79
+ hit = state["cache"].get(cache_key)
80
+ if hit is not None:
81
+ with state["_stats_lock"]:
82
+ state["stats"]["total"] += 1
83
+ state["stats"]["by_type"][captcha_type] = state["stats"]["by_type"].get(captcha_type, 0) + 1
84
+ state["stats"]["by_solver"][hit.solver] = state["stats"]["by_solver"].get(hit.solver, 0) + 1
85
+ state["stats"]["success"] += 1
86
+ state["stats"]["cache_hits"] += 1
87
+ return SolveResponse(
88
+ success=True,
89
+ answer=hit.answer,
90
+ confidence=hit.confidence,
91
+ solver=hit.solver,
92
+ elapsed_ms=int((time.time() - t0) * 1000),
93
+ cache_hit=True,
94
+ )
95
+
96
+ router_obj: SolverRouter = state["router"]
97
+ attempt = router_obj.solve(
98
+ captcha_type=captcha_type,
99
+ image_b64=req.image_base64,
100
+ audio_b64=req.audio_base64,
101
+ hint=req.hint,
102
+ )
103
+ elapsed_ms = int((time.time() - t0) * 1000)
104
+ success = bool(attempt.answer) and attempt.confidence > 0
105
+
106
+ with state["_stats_lock"]:
107
+ st = state["stats"]
108
+ st["total"] += 1
109
+ st["by_type"][captcha_type] = st["by_type"].get(captcha_type, 0) + 1
110
+ st["by_solver"][attempt.solver_name] = st["by_solver"].get(attempt.solver_name, 0) + 1
111
+ if success:
112
+ st["success"] += 1
113
+ st["elapsed_sum_ms"] += elapsed_ms
114
+
115
+ if success and cache_key and s.cache_enabled:
116
+ state["cache"].set(cache_key, attempt.answer, attempt.solver_name, attempt.confidence)
117
+
118
+ return SolveResponse(
119
+ success=success,
120
+ answer=attempt.answer or None,
121
+ confidence=attempt.confidence,
122
+ solver=attempt.solver_name,
123
+ elapsed_ms=elapsed_ms,
124
+ cache_hit=False,
125
+ attempts=len(attempt.metadata) + 1,
126
+ error=attempt.error,
127
+ )
128
+
129
+
130
+ @router.get("/health", response_model=HealthResponse)
131
+ def health() -> HealthResponse:
132
+ state = get_state()
133
+ engines = state["router"].engine_status() if state["router"] else {}
134
+ status = "ok"
135
+ if any(v == "not_loaded" for v in engines.values()):
136
+ status = "degraded"
137
+ if all(v == "not_loaded" for v in engines.values()):
138
+ status = "down"
139
+ return HealthResponse(
140
+ status=status,
141
+ version="0.1.0",
142
+ engines=engines,
143
+ uptime_s=time.time() - state["stats"]["started_at"],
144
+ )
145
+
146
+
147
+ @router.get("/stats", response_model=StatsResponse)
148
+ def stats() -> StatsResponse:
149
+ state = get_state()
150
+ st = state["stats"]
151
+ total = st["total"]
152
+ return StatsResponse(
153
+ total_requests=total,
154
+ by_type=dict(st["by_type"]),
155
+ by_solver=dict(st["by_solver"]),
156
+ success_rate=(st["success"] / total) if total else 0.0,
157
+ avg_elapsed_ms=(st["elapsed_sum_ms"] / total) if total else 0.0,
158
+ cache_hits=st["cache_hits"],
159
+ )
160
+
161
+
162
+ @router.get("/models", response_model=ModelsResponse)
163
+ def models() -> ModelsResponse:
164
+ state = get_state()
165
+ engines = state["router"].engine_status() if state["router"] else {}
166
+ loaded = [k for k, v in engines.items() if v == "loaded"]
167
+ available = [
168
+ {"name": "faster-whisper (tiny)", "size_mb": 75, "engine": "whisper", "purpose": "audio captcha"},
169
+ {"name": "faster-whisper (base)", "size_mb": 150, "engine": "whisper", "purpose": "audio captcha (better)"},
170
+ {"name": "Florence-2-base", "size_mb": 1200, "engine": "florence2", "purpose": "OCR + detection"},
171
+ {"name": "Moondream2", "size_mb": 1700, "engine": "moondream2", "purpose": "image grid VQA"},
172
+ {"name": "Qwen2.5-1.5B-Instruct", "size_mb": 1500, "engine": "qwen", "purpose": "math / text reasoning"},
173
+ {"name": "Qwen2-VL 7B (via ollama)", "size_mb": 4500, "engine": "ollama", "purpose": "best vision (optional)"},
174
+ ]
175
+ return ModelsResponse(
176
+ loaded=loaded,
177
+ available=available,
178
+ ollama_enabled=engines.get("ollama") == "loaded",
179
+ )
180
+
181
+
182
+ @router.get("/cache/clear")
183
+ def clear_cache() -> dict:
184
+ state = get_state()
185
+ if state["cache"]:
186
+ state["cache"].clear()
187
+ return {"cleared": True}
188
+
189
+
190
+ # --- hCaptcha tile classification ---
191
+
192
+ @router.post("/classify")
193
+ def classify_tile(req: dict) -> dict:
194
+ """Classify a single hCaptcha tile against an instruction.
195
+
196
+ Request body:
197
+ image_base64: str - base64-encoded tile image
198
+ instruction: str - hCaptcha instruction (e.g. "Find all items made by people")
199
+
200
+ Response:
201
+ match: bool - whether the tile matches the instruction
202
+ confidence: float - classification confidence
203
+ caption: str - Florence-2 caption of the tile
204
+ solver: str - which solver was used
205
+ """
206
+ from captcha_solver.solvers.hcaptcha_solver import classify_tile as _classify
207
+
208
+ image_b64 = req.get("image_base64", "")
209
+ instruction = req.get("instruction", "")
210
+
211
+ if not image_b64:
212
+ raise HTTPException(status_code=400, detail="image_base64 is required")
213
+ if not instruction:
214
+ raise HTTPException(status_code=400, detail="instruction is required")
215
+
216
+ state = get_state()
217
+ router_obj = state["router"]
218
+ # Build SolveContext from router's engines
219
+ from captcha_solver.solvers.base import SolveContext
220
+ ctx = SolveContext(
221
+ whisper=router_obj._engines.get("whisper"),
222
+ florence=router_obj._engines.get("florence2"),
223
+ moondream=router_obj._engines.get("moondream2"),
224
+ qwen=router_obj._engines.get("qwen"),
225
+ ollama=router_obj._engines.get("ollama"),
226
+ )
227
+ return _classify(image_b64, instruction, ctx)
228
+
229
+
230
+ # --- Learning stats ---
231
+ try:
232
+ from captcha_solver.learning.db import LearningDB
233
+ _learning_db = LearningDB()
234
+ except ImportError:
235
+ _learning_db = None
236
+
237
+
238
+ @router.get("/stats")
239
+ def get_learning_stats() -> dict:
240
+ if not _learning_db:
241
+ return {"enabled": False, "error": "learning module not available"}
242
+ summary = _learning_db.summary()
243
+ rankings = _learning_db.get_solver_ranking()
244
+ return {
245
+ "enabled": True,
246
+ "summary": summary,
247
+ "rankings": rankings,
248
+ "recent_failures": _learning_db.get_recent_failures(5),
249
+ }