captchakraken 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +54 -0
  2. package/dist/index.d.ts +56 -0
  3. package/dist/index.js +43 -0
  4. package/dist/playwright-types.d.ts +119 -0
  5. package/dist/playwright-types.js +25 -0
  6. package/dist/puppeteer-adapter.d.ts +70 -0
  7. package/dist/puppeteer-adapter.js +96 -0
  8. package/dist/solver.d.ts +264 -0
  9. package/dist/solver.js +1922 -0
  10. package/dist/token-usage.d.ts +15 -0
  11. package/dist/token-usage.js +102 -0
  12. package/dist/types.d.ts +248 -0
  13. package/dist/types.js +2 -0
  14. package/package.json +49 -0
  15. package/python/Dockerfile +41 -0
  16. package/python/README.md +71 -0
  17. package/python/examples/README.md +68 -0
  18. package/python/examples/_harness.py +158 -0
  19. package/python/examples/demoHcaptcha.py +20 -0
  20. package/python/examples/demoRecaptcha.py +17 -0
  21. package/python/pyproject.toml +79 -0
  22. package/python/src/captchakraken/__init__.py +61 -0
  23. package/python/src/captchakraken/action_types.py +56 -0
  24. package/python/src/captchakraken/cli.py +656 -0
  25. package/python/src/captchakraken/config.py +78 -0
  26. package/python/src/captchakraken/image_processor.py +244 -0
  27. package/python/src/captchakraken/overlay.py +520 -0
  28. package/python/src/captchakraken/planner.py +408 -0
  29. package/python/src/captchakraken/planner_types.py +74 -0
  30. package/python/src/captchakraken/server_manager.py +290 -0
  31. package/python/src/captchakraken/solver.py +434 -0
  32. package/python/src/captchakraken/timing.py +42 -0
  33. package/python/src/captchakraken/tool_calls/find_checkbox.py +72 -0
  34. package/python/src/captchakraken/tool_calls/find_grid.py +1762 -0
  35. package/python/src/captchakraken/tool_calls/move_indicator.py +431 -0
  36. package/scripts/copy-python.mjs +29 -0
  37. package/scripts/setup-python.js +104 -0
@@ -0,0 +1,158 @@
1
+ """
2
+ Shared runner for the CaptchaKraken Python demos.
3
+
4
+ The Python port is the *engine* (OpenCV detection + the vLLM planner). These demos
5
+ launch a real stealth browser (camoufox, using the binary from your fork —
6
+ JWriter20/camoufox releases; see README.md), navigate to a captcha demo site,
7
+ open the challenge, screenshot it, and run the engine on that frame. They report
8
+ token-generation speed, total time, and whether the engine produced a valid
9
+ solution — plus a best-effort reason when it didn't.
10
+
11
+ Note: full click-replay + multi-round verification in a live page is what the
12
+ TypeScript port (`captchakraken`) does end-to-end. Here we validate the
13
+ engine + model + server on a real challenge frame.
14
+ """
15
+
16
+ import os
17
+ import tempfile
18
+ import time
19
+ from dataclasses import dataclass
20
+
21
+ from camoufox.sync_api import Camoufox
22
+
23
+ from captchakraken import CaptchaSolver
24
+ from captchakraken.action_types import ClickAction
25
+ from captchakraken.solver import UnsupportedCaptchaError
26
+
27
+
28
+ @dataclass
29
+ class DemoSpec:
30
+ name: str
31
+ url: str
32
+ vendor: str # "recaptcha" | "hcaptcha"
33
+
34
+
35
+ def _launch_kwargs() -> dict:
36
+ kw = dict(headless=os.getenv("HEADLESS", "1") != "0", humanize=True, geoip=False)
37
+ # Point camoufox at YOUR fork's binary. If unset, camoufox uses its default
38
+ # cached binary (`python -m camoufox fetch`).
39
+ binary = os.getenv("CAMOUFOX_BINARY") or os.getenv("CAMOUFOX_EXECUTABLE_PATH")
40
+ if binary:
41
+ kw["executable_path"] = binary
42
+ return kw
43
+
44
+
45
+ def _capture_challenge(page, spec: DemoSpec) -> str:
46
+ """Reach the challenge iframe and screenshot it to a temp PNG. Returns the path.
47
+
48
+ Raises RuntimeError with a human message if the challenge never appears.
49
+ """
50
+ out = tempfile.NamedTemporaryFile(suffix=".png", delete=False).name
51
+
52
+ if spec.vendor == "recaptcha":
53
+ anchor = page.wait_for_selector('iframe[src*="/recaptcha/api2/anchor"]', timeout=30_000)
54
+ frame = anchor.content_frame()
55
+ frame.click("#recaptcha-anchor", timeout=15_000)
56
+ challenge = page.wait_for_selector('iframe[src*="/recaptcha/api2/bframe"]', timeout=30_000)
57
+ # Let the tiles paint.
58
+ page.wait_for_timeout(2500)
59
+ challenge.screenshot(path=out)
60
+ return out
61
+
62
+ if spec.vendor == "hcaptcha":
63
+ box = page.wait_for_selector('iframe[src*="hcaptcha.com"][src*="checkbox"], iframe[title*="checkbox"]',
64
+ timeout=30_000)
65
+ box.content_frame().click("#checkbox, div[role=checkbox]", timeout=15_000)
66
+ challenge = page.wait_for_selector('iframe[title*="hCaptcha challenge"], iframe[src*="hcaptcha.com"][src*="frame=challenge"]',
67
+ timeout=30_000)
68
+ page.wait_for_timeout(2500)
69
+ challenge.screenshot(path=out)
70
+ return out
71
+
72
+ raise RuntimeError(f"unknown vendor {spec.vendor!r}")
73
+
74
+
75
+ def _tokens_per_sec(solver: CaptchaSolver, elapsed_s: float) -> tuple[int, int, float]:
76
+ """(input_tokens, output_tokens, tokens/sec) from the planner's usage log."""
77
+ inp = out = 0
78
+ for u in getattr(solver.planner, "token_usage", []) or []:
79
+ inp += int(u.get("prompt_tokens", 0) or 0)
80
+ out += int(u.get("completion_tokens", 0) or 0)
81
+ tps = out / elapsed_s if out and elapsed_s > 0 else 0.0
82
+ return inp, out, tps
83
+
84
+
85
+ def _explain(vendor: str, err: Exception) -> str:
86
+ msg = str(err).lower()
87
+ if isinstance(err, UnsupportedCaptchaError) or "cannot solve" in msg or "unsupported" in msg:
88
+ if vendor == "hcaptcha":
89
+ return ("hCaptcha served a non-grid challenge (drag / video / choose-the-card). "
90
+ "The engine handles image grids + checkboxes only — re-run to try for a grid.")
91
+ return "Not a supported grid/checkbox challenge — re-run to try again."
92
+ if "vllm" in msg or "connection" in msg or "refused" in msg or "max retries" in msg:
93
+ return ("Could not reach the vLLM server — is it up and is VLLM_BASE_URL correct? "
94
+ "(A local server auto-starts only if captchakraken[serve] is installed.)")
95
+ if "timeout" in msg or "wait_for_selector" in msg:
96
+ return "The challenge iframe never appeared (slow network, blocked widget, or the checkbox auto-passed)."
97
+ return str(err) or "Unknown failure."
98
+
99
+
100
+ def run_demo(spec: DemoSpec) -> None:
101
+ t0 = time.time()
102
+ ok = False
103
+ reason = None
104
+ inp = out = 0
105
+ tps = 0.0
106
+ solve_s = 0.0
107
+ plan_desc = "-"
108
+
109
+ try:
110
+ with Camoufox(**_launch_kwargs()) as browser:
111
+ page = browser.new_page()
112
+ page.goto(spec.url, wait_until="domcontentloaded", timeout=60_000)
113
+ shot = _capture_challenge(page, spec)
114
+
115
+ solver = CaptchaSolver()
116
+ s0 = time.time()
117
+ try:
118
+ actions = solver.solve(shot, puzzle_source=spec.vendor)
119
+ finally:
120
+ solve_s = time.time() - s0
121
+ inp, out, tps = _tokens_per_sec(solver, solve_s)
122
+
123
+ acts = actions if isinstance(actions, list) else [actions]
124
+ clicks = [a for a in acts if isinstance(a, ClickAction) and (a.target_bounding_boxes or [])]
125
+ n_tiles = sum(len(a.target_bounding_boxes or []) for a in clicks)
126
+ if n_tiles > 0:
127
+ ok = True
128
+ plan_desc = f"click plan: {n_tiles} tile(s)/target(s)"
129
+ else:
130
+ plan_desc = f"actions: {[type(a).__name__ for a in acts]}"
131
+ reason = ("The model returned no tiles to click — either none matched the prompt, "
132
+ "or the challenge frame wasn't a clean grid. Re-run to try again.")
133
+ except Exception as err: # noqa: BLE001 — demo: report, don't traceback
134
+ reason = _explain(spec.vendor, err)
135
+
136
+ _report(spec, ok=ok, total_s=time.time() - t0, solve_s=solve_s,
137
+ inp=inp, out=out, tps=tps, plan=plan_desc, reason=reason)
138
+
139
+
140
+ def _fmt(s: float) -> str:
141
+ return f"{s:.1f}s" if s >= 1 else f"{int(s * 1000)}ms"
142
+
143
+
144
+ def _report(spec, *, ok, total_s, solve_s, inp, out, tps, plan, reason):
145
+ line = "─" * 52
146
+ print(f"\n{line}")
147
+ print(f" CaptchaKraken demo (engine) — {spec.name}")
148
+ print(f" {spec.url}")
149
+ print(line)
150
+ print(f" result : {'✓ engine produced a solution' if ok else '✗ no solution'}")
151
+ print(f" {plan}")
152
+ print(f" total time : {_fmt(total_s)} (solve: {_fmt(solve_s)})")
153
+ print(f" tokens : {inp} in / {out} out")
154
+ print(f" gen speed : {f'~{tps:.1f} tok/s' if tps > 0 else 'n/a'}")
155
+ if reason:
156
+ print(f" reason : {reason}")
157
+ print(f"{line}\n")
158
+ raise SystemExit(0 if ok else 1)
@@ -0,0 +1,20 @@
1
+ """
2
+ Engine demo: run CaptchaKraken on the standard hCaptcha demo page.
3
+
4
+ pip install -e ".[serve]" # engine + serving stack (or [.] against a remote server)
5
+ pip install camoufox && python -m camoufox fetch # or set CAMOUFOX_BINARY to your fork binary
6
+ source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
7
+ python examples/demoHcaptcha.py
8
+
9
+ Note: hCaptcha randomly serves non-grid puzzles (drag / video / choose-the-card),
10
+ which the engine does not handle yet — the report says so; re-run for a grid.
11
+ """
12
+
13
+ from _harness import DemoSpec, run_demo
14
+
15
+ if __name__ == "__main__":
16
+ run_demo(DemoSpec(
17
+ name="hCaptcha",
18
+ url="https://accounts.hcaptcha.com/demo",
19
+ vendor="hcaptcha",
20
+ ))
@@ -0,0 +1,17 @@
1
+ """
2
+ Engine demo: run CaptchaKraken on Google's standard reCAPTCHA v2 demo page.
3
+
4
+ pip install -e ".[serve]" # engine + serving stack (or [.] against a remote server)
5
+ pip install camoufox && python -m camoufox fetch # or set CAMOUFOX_BINARY to your fork binary
6
+ source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
7
+ python examples/demoRecaptcha.py
8
+ """
9
+
10
+ from _harness import DemoSpec, run_demo
11
+
12
+ if __name__ == "__main__":
13
+ run_demo(DemoSpec(
14
+ name="reCAPTCHA v2",
15
+ url="https://www.google.com/recaptcha/api2/demo",
16
+ vendor="recaptcha",
17
+ ))
@@ -0,0 +1,79 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "captchakraken"
7
+ version = "2.0.0"
8
+ description = "Self-hosted captcha solver: OpenCV grid detection + a fine-tuned Qwen3.5-9B vision LoRA served on vLLM."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "GPL-3.0-or-later" }
12
+ authors = [{ name = "Jake Writer" }]
13
+ keywords = ["captcha", "recaptcha", "hcaptcha", "vllm", "qwen", "computer-vision", "automation"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: OS Independent",
17
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
18
+ ]
19
+
20
+ # Core = the lightweight CLIENT: OpenCV grid detection + the HTTP planner that
21
+ # talks to a vLLM server (local or remote). This is all you need to SOLVE
22
+ # captchas against an existing endpoint. The heavy serving stack (vllm/torch)
23
+ # is deliberately NOT here — see the `serve` extra.
24
+ dependencies = [
25
+ "pydantic>=2.0.0",
26
+ "pillow>=10.0.0",
27
+ "numpy>=1.24.0",
28
+ "opencv-python-headless>=4.10.0",
29
+ "requests>=2.31.0",
30
+ "python-dotenv>=1.0.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ # Self-hosting: install this to RUN a local vLLM server (the setup script does
35
+ # `pip install "captchakraken[serve]"`). Not needed to call a remote server.
36
+ serve = [
37
+ "vllm>=0.6.3",
38
+ "torch>=2.0.0",
39
+ "transformers>=4.40.0",
40
+ "accelerate>=0.27.0",
41
+ "huggingface_hub>=0.23.0",
42
+ ]
43
+ dev = [
44
+ "pytest>=7.0.0",
45
+ "mypy>=1.0.0",
46
+ "ruff>=0.1.0",
47
+ ]
48
+
49
+ [project.scripts]
50
+ captchakraken = "captchakraken.cli:main"
51
+
52
+ [project.urls]
53
+ Homepage = "https://github.com/JWriter20/CaptchaKraken"
54
+ Issues = "https://github.com/JWriter20/CaptchaKraken/issues"
55
+
56
+ [tool.hatch.build.targets.wheel]
57
+ packages = ["src/captchakraken"]
58
+
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["tests"]
61
+ pythonpath = ["src"]
62
+
63
+ [tool.mypy]
64
+ python_version = "3.10"
65
+ warn_return_any = false
66
+ warn_unused_configs = true
67
+ disallow_untyped_defs = false
68
+ check_untyped_defs = false
69
+ ignore_missing_imports = true
70
+ no_implicit_optional = false
71
+ exclude = ["build/", "dist/"]
72
+
73
+ [tool.ruff]
74
+ line-length = 120
75
+ target-version = "py310"
76
+
77
+ [tool.ruff.lint]
78
+ select = ["E", "F", "W", "I"]
79
+ ignore = ["E501"]
@@ -0,0 +1,61 @@
1
+ """
2
+ CaptchaKraken — OpenCV grid detection + a fine-tuned Qwen3.5-9B vision LoRA
3
+ served on vLLM.
4
+
5
+ Usage:
6
+ from captchakraken import CaptchaSolver
7
+ solver = CaptchaSolver() # auto-starts / connects to a local vLLM server
8
+ actions = solver.solve("captcha.png")
9
+
10
+ Model/endpoint defaults live in `captchakraken.config` and are fully
11
+ env-overridable (VLLM_BASE_URL, CAPTCHA_LORA_ADAPTER, …); the solver itself is
12
+ model-agnostic. The legacy v1 stack (SAM3 grounding, multi-provider planner,
13
+ detect/segment/drag-refine) lives on the `v1-old-architecture` branch.
14
+ """
15
+
16
+ from pathlib import Path
17
+
18
+ try: # pragma: no cover
19
+ from dotenv import load_dotenv
20
+
21
+ project_root = Path(__file__).resolve().parent.parent
22
+ load_dotenv(project_root / ".env")
23
+ except Exception:
24
+ pass
25
+
26
+ from .action_types import (
27
+ CaptchaAction,
28
+ ClickAction,
29
+ DragAction,
30
+ TypeAction,
31
+ WaitAction,
32
+ )
33
+ from .image_processor import ImageProcessor
34
+ from .overlay import add_overlays_to_image
35
+
36
+ # The planner (requests) and solver (torch/vllm/transformers) pull in the heavy
37
+ # serving stack. Keep them optional so leaf modules — e.g. tool_calls.find_grid,
38
+ # which needs only cv2 + numpy + pillow — can be imported in a minimal env (CI's
39
+ # hermetic grid-detection test) without the full GPU dependency set installed.
40
+ try: # pragma: no cover - exercised only when the serving stack is installed
41
+ from .planner import ActionPlanner
42
+ from .solver import CaptchaSolver, solve_captcha
43
+ except ModuleNotFoundError:
44
+ ActionPlanner = None # type: ignore[assignment,misc]
45
+ CaptchaSolver = None # type: ignore[assignment,misc]
46
+ solve_captcha = None # type: ignore[assignment]
47
+
48
+ __all__ = [
49
+ "CaptchaSolver",
50
+ "solve_captcha",
51
+ "ActionPlanner",
52
+ "ImageProcessor",
53
+ "CaptchaAction",
54
+ "ClickAction",
55
+ "DragAction",
56
+ "TypeAction",
57
+ "WaitAction",
58
+ "add_overlays_to_image",
59
+ ]
60
+
61
+ __version__ = "2.0.0"
@@ -0,0 +1,56 @@
1
+ from typing import List, Literal, Optional, Union, Tuple
2
+
3
+ from pydantic import BaseModel, RootModel
4
+
5
+
6
+ class BoundingBox(RootModel):
7
+ """
8
+ Strongly typed bounding box: [x1, y1, x2, y2] in percentages (0.0 to 1.0).
9
+ Acts like a list for convenience but ensures exactly 4 float elements.
10
+ """
11
+ root: Tuple[float, float, float, float]
12
+
13
+ def __iter__(self):
14
+ return iter(self.root)
15
+
16
+ def __getitem__(self, item):
17
+ return self.root[item]
18
+
19
+ def __len__(self):
20
+ return len(self.root)
21
+
22
+
23
+ class Action(BaseModel):
24
+ action: str
25
+
26
+
27
+ class ClickAction(Action):
28
+ action: Literal["click"]
29
+ target_bounding_boxes: List[BoundingBox] # List of [x1, y1, x2, y2] in percentages
30
+
31
+
32
+ class DragAction(Action):
33
+ action: Literal["drag"]
34
+ source_bounding_box: BoundingBox = None # [x1, y1, x2, y2] in percentages
35
+ target_bounding_box: BoundingBox = None # [x1, y1, x2, y2] in percentages
36
+
37
+
38
+ class TypeAction(Action):
39
+ """Type text into an input."""
40
+ action: Literal["type"]
41
+ text: str
42
+ target_bounding_box: BoundingBox = None # [x1, y1, x2, y2] in percentages
43
+
44
+
45
+ class WaitAction(Action):
46
+ """Wait for a specified duration."""
47
+ action: Literal["wait"]
48
+ duration_ms: int
49
+
50
+
51
+ class DoneAction(Action):
52
+ """Signal that the captcha is solved or no further actions are needed."""
53
+ action: Literal["done"]
54
+
55
+
56
+ CaptchaAction = Union[ClickAction, DragAction, TypeAction, WaitAction, DoneAction]