captchakraken 3.1.0 → 3.2.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 (44) hide show
  1. package/AGENTS.md +4 -4
  2. package/LICENSE +1 -1
  3. package/README.md +10 -13
  4. package/dist/index.d.ts +1 -0
  5. package/dist/kinds.d.ts +8 -0
  6. package/dist/kinds.js +3 -1
  7. package/dist/model-name.d.ts +6 -0
  8. package/dist/model-name.js +28 -0
  9. package/dist/playwright-types.d.ts +18 -0
  10. package/dist/png.d.ts +16 -0
  11. package/dist/png.js +119 -0
  12. package/dist/puppeteer-adapter.d.ts +7 -1
  13. package/dist/puppeteer-adapter.js +6 -0
  14. package/dist/selectors.d.ts +6 -0
  15. package/dist/selectors.js +4 -0
  16. package/dist/solver.d.ts +76 -4
  17. package/dist/solver.js +486 -165
  18. package/dist/types.d.ts +10 -5
  19. package/dist/verdicts.d.ts +45 -0
  20. package/dist/verdicts.js +125 -0
  21. package/package.json +7 -7
  22. package/python/AGENTS.md +13 -8
  23. package/python/LICENSE +1 -1
  24. package/python/README.md +10 -13
  25. package/python/pyproject.toml +6 -6
  26. package/python/src/captchakraken/__init__.py +5 -1
  27. package/python/src/captchakraken/cli.py +7 -1
  28. package/python/src/captchakraken/kinds.py +9 -0
  29. package/python/src/captchakraken/page_solver.py +439 -152
  30. package/python/src/captchakraken/planner.py +52 -11
  31. package/python/src/captchakraken/prompts.py +24 -0
  32. package/python/src/captchakraken/selectors.py +10 -0
  33. package/python/src/captchakraken/tool_calls/find_checkbox.py +4 -0
  34. package/python/src/captchakraken/tool_calls/find_grid.py +22 -0
  35. package/python/src/captchakraken/verdicts.py +154 -0
  36. package/python/Dockerfile +0 -41
  37. package/python/examples/README.md +0 -114
  38. package/python/examples/_harness.py +0 -177
  39. package/python/examples/demoHcaptcha.py +0 -8
  40. package/python/examples/demoRecaptcha.py +0 -8
  41. package/python/examples/watch_playwright.py +0 -31
  42. package/python/examples/with_local_gguf.py +0 -28
  43. package/python/examples/with_playwright.py +0 -24
  44. package/scripts/copy-python.mjs +0 -29
@@ -1,177 +0,0 @@
1
- import argparse
2
- import json
3
- import os
4
- import sys
5
- import time
6
- from dataclasses import dataclass, replace
7
-
8
- from camoufox.sync_api import Camoufox
9
-
10
- from captchakraken.page_solver import CaptchaSolveError, PageSolver
11
-
12
-
13
- @dataclass
14
- class DemoSpec:
15
- name: str
16
- url: str
17
- vendor: str
18
-
19
-
20
- def spec_from_argv(default: DemoSpec, argv=None) -> DemoSpec:
21
- ap = argparse.ArgumentParser(
22
- description=f"CaptchaKraken demo — {default.name}",
23
- epilog="With no URL, runs against the built-in demo page.")
24
- ap.add_argument("url", nargs="?", default=default.url,
25
- help=f"page to solve (default: {default.url})")
26
- ap.add_argument("--vendor", default=default.vendor,
27
- choices=("hcaptcha", "recaptcha"),
28
- help="only affects failure wording; the solver auto-detects")
29
- ap.add_argument("--name", default=None, help="label for the report")
30
- ap.add_argument("--headed", action="store_true",
31
- help="show the browser (same as HEADLESS=0)")
32
- args = ap.parse_args(argv)
33
-
34
- if args.headed:
35
- os.environ["HEADLESS"] = "0"
36
- name = args.name or (default.name if args.url == default.url else args.url)
37
- return replace(default, url=args.url, vendor=args.vendor, name=name)
38
-
39
-
40
- def _launch_kwargs() -> dict:
41
- kw = dict(headless=os.getenv("HEADLESS", "1") != "0",
42
- humanize=os.getenv("HUMANIZE", "0") != "0", geoip=False)
43
- binary = os.getenv("CAMOUFOX_BINARY") or os.getenv("CAMOUFOX_EXECUTABLE_PATH")
44
- if binary:
45
- kw["executable_path"] = binary
46
- return kw
47
-
48
-
49
- def _page_kwargs() -> dict:
50
- out = os.getenv("CAPTCHA_DEMO_VIDEO_DIR")
51
- if not out:
52
- return {}
53
- w, _, h = os.getenv("CAPTCHA_DEMO_VIDEO_SIZE", "1280x800").partition("x")
54
- return {"record_video_dir": out,
55
- "record_video_size": {"width": int(w), "height": int(h)}}
56
-
57
-
58
- def _finish_video(page):
59
- video = getattr(page, "video", None)
60
- if video is None:
61
- return None
62
- try:
63
- page.context.close()
64
- return video.path()
65
- except Exception:
66
- return None
67
-
68
-
69
- def _tokens(usage) -> tuple[int, int]:
70
- inp = out = 0
71
- for u in usage or []:
72
- inp += int(u.get("prompt_tokens", u.get("inputTokens", 0)) or 0)
73
- out += int(u.get("completion_tokens", u.get("outputTokens", 0)) or 0)
74
- return inp, out
75
-
76
-
77
- def _explain(vendor: str, solved: bool, err) -> str:
78
- msg = str(err or "").lower()
79
- if "unsupported" in msg or "cannot solve" in msg:
80
- if vendor == "hcaptcha":
81
- return ("hCaptcha served a challenge type the solver does not handle "
82
- "yet (drag / video / choose-the-card) — re-run for a grid.")
83
- return "Not a supported challenge type — re-run to try again."
84
- if "no captcha" in msg or "nocaptchafound" in msg:
85
- return ("No captcha widget was found on that page. Check the URL, or "
86
- "the widget may load only after an interaction.")
87
- if "vllm" in msg or "connection" in msg or "refused" in msg or "max retries" in msg:
88
- return ("Could not reach the vLLM server — is it up and is VLLM_BASE_URL "
89
- "correct? (A local server auto-starts only with captchakraken[serve].)")
90
- if "timeout" in msg:
91
- return "Timed out — the page or challenge never became interactable."
92
- if err:
93
- return str(err)
94
- if not solved:
95
- return ("The answer was submitted but the vendor did not accept it. Most "
96
- "often IP reputation or fingerprint flagging rather than a wrong "
97
- "answer — try a cleaner IP or a residential proxy.")
98
- return "Unknown failure."
99
-
100
-
101
- def run_demo(spec: DemoSpec) -> None:
102
- spec = spec_from_argv(spec)
103
-
104
- t0 = time.time()
105
- ok = False
106
- reason = None
107
- inp = out = 0
108
- solve_s = 0.0
109
- video_path = None
110
- err = None
111
-
112
- try:
113
- with Camoufox(**_launch_kwargs()) as browser:
114
- page = browser.new_page(**_page_kwargs())
115
- try:
116
- page.goto(spec.url, wait_until="domcontentloaded", timeout=60_000)
117
- try:
118
- page.wait_for_load_state("networkidle", timeout=15_000)
119
- except Exception:
120
- pass
121
- page.wait_for_timeout(3000)
122
-
123
- s0 = time.time()
124
- try:
125
- result = PageSolver().solve(page)
126
- finally:
127
- solve_s = time.time() - s0
128
- ok = bool(result.is_solved)
129
- inp, out = _tokens(result.token_usage)
130
- if not ok:
131
- reason = _explain(spec.vendor, ok, None)
132
- except CaptchaSolveError as e:
133
- err = e
134
- reason = _explain(spec.vendor, False, e)
135
- finally:
136
- video_path = _finish_video(page)
137
- except Exception as e:
138
- err = e
139
- reason = _explain(spec.vendor, False, e)
140
-
141
- tps = out / solve_s if out and solve_s > 0 else 0.0
142
- _report(spec, ok=ok, total_s=time.time() - t0, solve_s=solve_s,
143
- inp=inp, out=out, tps=tps, reason=reason, video=video_path)
144
-
145
-
146
- def _fmt(s: float) -> str:
147
- return f"{s:.1f}s" if s >= 1 else f"{int(s * 1000)}ms"
148
-
149
-
150
- def _report(spec, *, ok, total_s, solve_s, inp, out, tps, reason, video=None):
151
- line = "─" * 52
152
- print(f"\n{line}")
153
- print(f" CaptchaKraken demo — {spec.name}")
154
- print(f" {spec.url}")
155
- print(line)
156
- print(f" result : {'✓ SOLVED' if ok else '✗ not solved'}")
157
- print(f" total time : {_fmt(total_s)} (solve: {_fmt(solve_s)})")
158
- print(f" tokens : {inp} in / {out} out")
159
- print(f" gen speed : {f'~{tps:.1f} tok/s' if tps > 0 else 'n/a'}")
160
- if video:
161
- print(f" video : {video}")
162
- if reason:
163
- print(f" reason : {reason}")
164
- print(f"{line}\n")
165
-
166
- record = os.getenv("CAPTCHA_DEMO_RECORD")
167
- if record:
168
- with open(record, "a", encoding="utf-8") as fh:
169
- fh.write(json.dumps({
170
- "demo": spec.name, "url": spec.url, "vendor": spec.vendor,
171
- "ok": ok, "reason": reason,
172
- "total_s": round(total_s, 1), "solve_s": round(solve_s, 1),
173
- "input_tokens": inp, "output_tokens": out,
174
- "tokens_per_sec": round(tps, 1), "video": video,
175
- }) + "\n")
176
-
177
- sys.exit(0 if ok else 1)
@@ -1,8 +0,0 @@
1
- from _harness import DemoSpec, run_demo
2
-
3
- if __name__ == "__main__":
4
- run_demo(DemoSpec(
5
- name="hCaptcha",
6
- url="https://accounts.hcaptcha.com/demo",
7
- vendor="hcaptcha",
8
- ))
@@ -1,8 +0,0 @@
1
- from _harness import DemoSpec, run_demo
2
-
3
- if __name__ == "__main__":
4
- run_demo(DemoSpec(
5
- name="reCAPTCHA v2",
6
- url="https://www.google.com/recaptcha/api2/demo",
7
- vendor="recaptcha",
8
- ))
@@ -1,31 +0,0 @@
1
- import sys
2
-
3
- from playwright.sync_api import sync_playwright
4
-
5
- from captchakraken import PageSolver
6
-
7
- URL = sys.argv[1] if len(sys.argv) > 1 else "https://www.google.com/recaptcha/api2/demo"
8
- SECONDS = int(sys.argv[2]) if len(sys.argv) > 2 else 30
9
-
10
-
11
- def main() -> int:
12
- with sync_playwright() as p:
13
- browser = p.chromium.launch(headless=True)
14
- page = browser.new_page()
15
-
16
- watcher = PageSolver().watch(
17
- page,
18
- on_solved=lambda r: print("✅ solved one:", r.is_solved),
19
- on_error=lambda e: print("solve failed:", e),
20
- )
21
-
22
- page.goto(URL)
23
- watcher.run(timeout_ms=SECONDS * 1000)
24
-
25
- print(f"stopped after {watcher.solves} solve(s)")
26
- browser.close()
27
- return 0 if watcher.solves else 1
28
-
29
-
30
- if __name__ == "__main__":
31
- raise SystemExit(main())
@@ -1,28 +0,0 @@
1
- import os
2
- import sys
3
-
4
- from captchakraken import solve_captcha
5
-
6
- TEXT_CAPTCHAS = {"--text"}
7
-
8
-
9
- def main() -> int:
10
- args = [a for a in sys.argv[1:] if a not in TEXT_CAPTCHAS]
11
- if not args:
12
- print(__doc__)
13
- return 2
14
- image = args[0]
15
- text_mode = bool(TEXT_CAPTCHAS & set(sys.argv[1:]))
16
-
17
- print(f"endpoint : {os.getenv('VLLM_BASE_URL', '(default)')}")
18
- print(f"model : {os.getenv('CAPTCHA_LORA_NAME', '(registry default)')}")
19
- print(f"prompts : {os.getenv('CAPTCHA_LORA_ADAPTER', '(latest)')}")
20
-
21
- result = solve_captcha(image, text_mode=text_mode)
22
-
23
- print(f"answer : {result}")
24
- return 0 if result else 1
25
-
26
-
27
- if __name__ == "__main__":
28
- raise SystemExit(main())
@@ -1,24 +0,0 @@
1
- import sys
2
-
3
- from playwright.sync_api import sync_playwright
4
-
5
- from captchakraken import PageSolver
6
-
7
- URL = sys.argv[1] if len(sys.argv) > 1 else "https://www.google.com/recaptcha/api2/demo"
8
-
9
-
10
- def main() -> int:
11
- with sync_playwright() as p:
12
- browser = p.chromium.launch(headless=True)
13
- page = browser.new_page()
14
- page.goto(URL)
15
-
16
- result = PageSolver().solve(page)
17
-
18
- print("✅ solved" if result.is_solved else "❌ not solved")
19
- browser.close()
20
- return 0 if result.is_solved else 1
21
-
22
-
23
- if __name__ == "__main__":
24
- raise SystemExit(main())
@@ -1,29 +0,0 @@
1
- import { cpSync, rmSync, existsSync } from 'node:fs'
2
- import { dirname, resolve } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
-
5
- const here = dirname(fileURLToPath(import.meta.url))
6
- const src = resolve(here, '../../python')
7
- const dest = resolve(here, '../python')
8
-
9
- if (!existsSync(src)) {
10
- console.error(`[copy-python] source engine not found at ${src}`)
11
- process.exit(1)
12
- }
13
-
14
- const SKIP = new Set([
15
- '.venv', '__pycache__', 'dist', 'build', '.pytest_cache', '.ruff_cache', 'tests',
16
- 'coverage', 'htmlcov', '.nyc_output', 'coverage.xml', 'lcov.info',
17
-
18
- 'latestDebugRun',
19
- ])
20
-
21
- rmSync(dest, { recursive: true, force: true })
22
- cpSync(src, dest, {
23
- recursive: true,
24
- filter: (p) =>
25
- !p
26
- .split(/[\\/]/)
27
- .some((seg) => SKIP.has(seg) || seg.endsWith('.egg-info') || seg.startsWith('.coverage')),
28
- })
29
- console.log('[copy-python] bundled python/ engine -> js/python/')