| """ |
| hCaptcha Solver β Hugging Face Spaces |
| ====================================== |
| Required env secret: GEMINI_API_KEY (Google AI Studio key) |
| Optional env vars: |
| GEMINI_MODEL (default: gemini-2.0-flash) |
| PROXY_SERVER e.g. 1.2.3.4:5188 |
| PROXY_USERNAME |
| PROXY_PASSWORD |
| |
| Endpoints: |
| GET / β status check |
| POST /solve/captcha β solve hCaptcha |
| body: { "url": "https://...", "sitekey": "..." } |
| resp: { "success": true/false, "token": "P1_...", "error": "..." } |
| """ |
|
|
| import os |
| from contextlib import asynccontextmanager |
| from pathlib import Path |
| from typing import List |
|
|
| from fastapi import FastAPI |
| from fastapi.middleware.cors import CORSMiddleware |
| from pydantic import BaseModel |
|
|
| PORT = int(os.environ.get("PORT", 7860)) |
| GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "") |
| GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite-preview") |
|
|
| PROXY_SERVER = os.environ.get("PROXY_SERVER", "") |
| PROXY_USERNAME = os.environ.get("PROXY_USERNAME", "") |
| PROXY_PASSWORD = os.environ.get("PROXY_PASSWORD", "") |
|
|
| if not GEMINI_API_KEY: |
| print("[WARNING] GEMINI_API_KEY is not set β solver will fail!") |
|
|
|
|
| |
|
|
| def _patch_gemini_provider(): |
| from google import genai as _genai |
| from google.genai import types as _types |
| from hcaptcha_challenger.tools.internal.providers.gemini import GeminiProvider |
|
|
| MIME = {".jpg": "image/jpeg", ".jpeg": "image/jpeg", |
| ".png": "image/png", ".webp": "image/webp"} |
|
|
| @property |
| def _patched_client(self): |
| if self._client is None: |
| self._client = _genai.Client(api_key=GEMINI_API_KEY) |
| return self._client |
|
|
| GeminiProvider.client = _patched_client |
|
|
| async def _patched_upload_files(self, files: List[Path]): |
| parts = [] |
| for f in files: |
| if f and Path(f).exists(): |
| data = Path(f).read_bytes() |
| mime = MIME.get(Path(f).suffix.lower(), "image/png") |
| parts.append(_types.Part.from_bytes(data=data, mime_type=mime)) |
| return parts |
|
|
| GeminiProvider._upload_files = _patched_upload_files |
|
|
| @staticmethod |
| def _patched_files_to_parts(files): |
| return files |
|
|
| GeminiProvider._files_to_parts = _patched_files_to_parts |
|
|
| print("[patch] GeminiProvider patched β inline images") |
|
|
|
|
| _patch_gemini_provider() |
|
|
|
|
| |
|
|
| _cv_patch_applied = False |
|
|
| def _patch_robotic_arm_cv(): |
| global _cv_patch_applied |
| if _cv_patch_applied: |
| return |
| _cv_patch_applied = True |
|
|
| from contextlib import suppress |
| from playwright.async_api import TimeoutError as PWTimeout |
| from hcaptcha_challenger.agent.challenger import RoboticArm |
| from hcaptcha_challenger.models import SpatialPath, PointCoordinate |
| import cv_challenge as cv_mod |
|
|
| async def _cv_challenge_image_drag_drop(self, job_type): |
| frame_challenge = await self.get_challenge_frame_locator() |
| crumb_count = await self.check_crumb_count() |
| cache_key = self.config.create_cache_key(self.captcha_payload) |
|
|
| challenge_view = frame_challenge.locator("//div[@class='challenge-view']") |
| bbox = await challenge_view.bounding_box() |
|
|
| for cid in range(crumb_count): |
| await self.page.wait_for_timeout( |
| self.config.WAIT_FOR_CHALLENGE_VIEW_TO_RENDER_MS |
| ) |
|
|
| raw, projection = await self._capture_spatial_mapping( |
| frame_challenge, cache_key, cid |
| ) |
| user_prompt = self._match_user_prompt(job_type) |
|
|
| try: |
| import shutil as _sh |
| _sh.copy(str(raw), f"/tmp/challenge_{job_type.value}_{cid}.png") |
| if bbox: |
| _bstr = (f"x={bbox['x']:.0f} y={bbox['y']:.0f} " |
| f"w={bbox['width']:.0f} h={bbox['height']:.0f}") |
| print(f"[cv2][debug] screenshot β " |
| f"/tmp/challenge_{job_type.value}_{cid}.png bbox={_bstr}") |
| except Exception: |
| pass |
|
|
| cv_result = None |
| if bbox: |
| try: |
| cv_result = cv_mod.dispatch_by_type( |
| job_type.value, raw, bbox |
| ) |
| if cv_result: |
| if isinstance(cv_result, list): |
| cv_mod.annotate(raw, cv_result, bbox, |
| f"/tmp/cv2_annotated_{job_type.value}_{cid}.png") |
| print(f"[cv2][crumb {cid+1}] type={job_type.value} " |
| f"result={cv_result}") |
| else: |
| print(f"[cv2][crumb {cid+1}] type={job_type.value} " |
| f"β no CV result, falling back to Gemini") |
| except Exception as e: |
| print(f"[cv2][crumb {cid+1}] dispatch error: {e}") |
|
|
| if cv_result is not None: |
| if isinstance(cv_result, list): |
| for (fx, fy), (tx, ty) in cv_result: |
| path = SpatialPath( |
| start_point=PointCoordinate(x=int(fx), y=int(fy)), |
| end_point=PointCoordinate(x=int(tx), y=int(ty)), |
| ) |
| await self._perform_drag_drop(path) |
|
|
| elif isinstance(cv_result, tuple) and len(cv_result) == 2: |
| first = cv_result[0] |
| if isinstance(first, tuple): |
| (fx, fy), (tx, ty) = cv_result |
| path = SpatialPath( |
| start_point=PointCoordinate(x=int(fx), y=int(fy)), |
| end_point=PointCoordinate(x=int(tx), y=int(ty)), |
| ) |
| await self._perform_drag_drop(path) |
| else: |
| cx, cy = cv_result |
| await self.page.mouse.click(int(cx), int(cy), delay=180) |
| await self.page.wait_for_timeout(500) |
|
|
| with suppress(PWTimeout): |
| submit_btn = frame_challenge.locator( |
| "//div[@class='button-submit button']" |
| ) |
| await self.click_by_mouse(submit_btn) |
|
|
| else: |
| print(f"[cv2][crumb {cid+1}] no CV result β using Gemini") |
| response = await self._spatial_path_reasoner( |
| challenge_screenshot=raw, |
| grid_divisions=projection, |
| auxiliary_information=user_prompt, |
| ) |
| import logging |
| logging.getLogger(__name__).debug( |
| f"[{cid+1}/{crumb_count}]ToolInvokeMessage: {response.log_message}" |
| ) |
| self._spatial_path_reasoner.cache_response( |
| path=cache_key.joinpath(f"{cache_key.name}_{cid}_model_answer.json") |
| ) |
| for path in response.paths: |
| await self._perform_drag_drop(path) |
|
|
| with suppress(PWTimeout): |
| submit_btn = frame_challenge.locator( |
| "//div[@class='button-submit button']" |
| ) |
| await self.click_by_mouse(submit_btn) |
|
|
| RoboticArm.challenge_image_drag_drop = _cv_challenge_image_drag_drop |
| print("[patch] RoboticArm.challenge_image_drag_drop β cv2-first + Gemini fallback") |
|
|
|
|
| |
|
|
| class SolveRequest(BaseModel): |
| url: str |
| sitekey: str |
|
|
| class SolveResponse(BaseModel): |
| success: bool |
| token: str | None = None |
| error: str | None = None |
|
|
|
|
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| print("=" * 50) |
| print("hCaptcha Solver β Hugging Face Space") |
| print(f"Model : {GEMINI_MODEL}") |
| print(f"Port : {PORT}") |
| print(f"Key : {'set' if GEMINI_API_KEY else 'NOT SET'}") |
| print(f"Proxy : {PROXY_SERVER}" if PROXY_SERVER else "Proxy : none") |
| print("=" * 50) |
| yield |
|
|
|
|
| app = FastAPI(title="hCaptcha Solver", lifespan=lifespan) |
| app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) |
|
|
|
|
| async def run_solver(url: str, sitekey: str) -> dict | None: |
| from playwright.async_api import async_playwright |
| from hcaptcha_challenger.agent import AgentV, AgentConfig |
| from hcaptcha_challenger.models import CaptchaResponse |
|
|
| _patch_robotic_arm_cv() |
|
|
| mock_html = f"""<!DOCTYPE html> |
| <html> |
| <head> |
| <script src="https://js.hcaptcha.com/1/api.js" async defer></script> |
| <style>body{{background:#111;display:flex;justify-content:center;align-items:center;height:100vh;margin:0}}</style> |
| </head> |
| <body> |
| <div class="h-captcha" data-sitekey="{sitekey}"></div> |
| </body> |
| </html>""" |
|
|
| proxy_config = None |
| if PROXY_SERVER: |
| proxy_config = { |
| "server": f"http://{PROXY_SERVER}", |
| "username": PROXY_USERNAME, |
| "password": PROXY_PASSWORD, |
| } |
| print(f"[proxy] Using {PROXY_SERVER}") |
|
|
| async with async_playwright() as p: |
| browser = await p.chromium.launch( |
| headless=True, |
| args=["--no-sandbox", "--disable-setuid-sandbox", |
| "--disable-dev-shm-usage", "--disable-gpu"], |
| proxy=proxy_config, |
| ) |
| context = await browser.new_context( |
| viewport={"width": 1280, "height": 720}, |
| user_agent=( |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " |
| "AppleWebKit/537.36 (KHTML, like Gecko) " |
| "Chrome/122.0.0.0 Safari/537.36" |
| ), |
| ) |
| page = await context.new_page() |
|
|
| await page.route(url, lambda route: route.fulfill( |
| status=200, content_type="text/html", body=mock_html |
| )) |
|
|
| try: |
| await page.goto(url, wait_until="load") |
| await page.wait_for_selector( |
| "//iframe[contains(@src, 'frame=checkbox')]", timeout=15000 |
| ) |
|
|
| agent_config = AgentConfig( |
| GEMINI_API_KEY=GEMINI_API_KEY, |
| CHALLENGE_CLASSIFIER_MODEL=GEMINI_MODEL, |
| IMAGE_CLASSIFIER_MODEL=GEMINI_MODEL, |
| SPATIAL_POINT_REASONER_MODEL=GEMINI_MODEL, |
| SPATIAL_PATH_REASONER_MODEL=GEMINI_MODEL, |
| ) |
|
|
| agent = AgentV(page=page, agent_config=agent_config) |
| print(f"[*] Solving hCaptcha for {url}...") |
|
|
| await agent.robotic_arm.click_checkbox() |
| await agent.wait_for_challenge() |
|
|
| if agent.cr_list: |
| cr: CaptchaResponse = agent.cr_list[-1] |
| return cr.model_dump(by_alias=True) |
|
|
| return None |
| finally: |
| await browser.close() |
|
|
|
|
| @app.get("/") |
| async def root(): |
| return { |
| "status": "online", |
| "model": GEMINI_MODEL, |
| "endpoints": {"POST /solve/captcha": "Solve hCaptcha"}, |
| } |
|
|
|
|
| @app.post("/solve/captcha", response_model=SolveResponse) |
| async def solve_captcha(request: SolveRequest): |
| try: |
| print("[solver] start") |
| result = await run_solver(request.url, request.sitekey) |
| print(f"[solver] done β pass={result.get('pass') if result else False}") |
| except Exception as e: |
| print(f"[solver] error: {e}") |
| return SolveResponse(success=False, error=str(e)) |
|
|
| if result and result.get("pass"): |
| token = result.get("generated_pass_UUID") or result.get("token") |
| return SolveResponse(success=True, token=token) |
| return SolveResponse(success=False, error="Challenge not solved") |
|
|
|
|
| if __name__ == "__main__": |
| import uvicorn |
| uvicorn.run(app, host="0.0.0.0", port=PORT) |
|
|