captchakraken 2.4.0 → 2.6.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 (51) hide show
  1. package/LICENSE +47 -7
  2. package/README.md +24 -4
  3. package/dist/cli-invocation.d.ts +46 -0
  4. package/dist/cli-invocation.js +68 -0
  5. package/dist/index.d.ts +27 -0
  6. package/dist/index.js +29 -1
  7. package/dist/limits.d.ts +51 -0
  8. package/dist/limits.js +54 -0
  9. package/dist/model-name.d.ts +23 -0
  10. package/dist/model-name.js +70 -0
  11. package/dist/playwright-types.d.ts +28 -0
  12. package/dist/puppeteer-adapter.d.ts +11 -0
  13. package/dist/puppeteer-adapter.js +23 -0
  14. package/dist/python-command.d.ts +50 -0
  15. package/dist/python-command.js +60 -0
  16. package/dist/slide-geometry.d.ts +33 -0
  17. package/dist/slide-geometry.js +64 -0
  18. package/dist/solver.d.ts +214 -12
  19. package/dist/solver.js +1285 -165
  20. package/dist/trajectory.d.ts +31 -0
  21. package/dist/trajectory.js +162 -0
  22. package/dist/types.d.ts +128 -11
  23. package/dist/watcher.d.ts +99 -0
  24. package/dist/watcher.js +129 -0
  25. package/package.json +2 -5
  26. package/python/LICENSE +47 -7
  27. package/python/README.md +25 -8
  28. package/python/examples/README.md +36 -0
  29. package/python/examples/_harness.py +182 -94
  30. package/python/examples/demoHcaptcha.py +2 -1
  31. package/python/examples/demoRecaptcha.py +2 -1
  32. package/python/examples/watch_playwright.py +56 -0
  33. package/python/examples/with_playwright.py +43 -0
  34. package/python/pyproject.toml +4 -3
  35. package/python/src/captchakraken/__init__.py +5 -3
  36. package/python/src/captchakraken/action_types.py +24 -3
  37. package/python/src/captchakraken/cli.py +212 -2
  38. package/python/src/captchakraken/config.py +39 -5
  39. package/python/src/captchakraken/keyframes.py +690 -0
  40. package/python/src/captchakraken/models.json +120 -0
  41. package/python/src/captchakraken/page_solver.py +1465 -102
  42. package/python/src/captchakraken/pinned_model.json +5 -7
  43. package/python/src/captchakraken/planner.py +253 -59
  44. package/python/src/captchakraken/prompts.py +485 -0
  45. package/python/src/captchakraken/solver.py +182 -11
  46. package/python/src/captchakraken/timing.py +70 -0
  47. package/python/src/captchakraken/tool_calls/find_grid.py +367 -52
  48. package/python/src/captchakraken/tool_calls/track_piece.py +96 -0
  49. package/python/src/captchakraken/trajectory.py +2 -7
  50. package/python/src/captchakraken/watcher.py +193 -0
  51. package/python/src/captchakraken/planner_types.py +0 -74
package/python/LICENSE CHANGED
@@ -1,5 +1,5 @@
1
- CaptchaKraken Source-Available License v1.0
2
- Copyright (c) 2026 JobHarvest / JWriter20. All rights reserved.
1
+ CaptchaKraken Source-Available License v1.1
2
+ Copyright (c) 2026 CaptchaKraken LLC. All rights reserved.
3
3
 
4
4
  This license governs use of the CaptchaKraken software in this repository and of
5
5
  the associated machine-learning model weights and LoRA adapters published by the
@@ -26,12 +26,17 @@ You MAY use the Software for:
26
26
 
27
27
  Illustrative (non-exhaustive) examples of PERMITTED commercial use:
28
28
  - Web scrapers and data-collection pipelines.
29
- - Stealth / anti-detection browsers and browser automation frameworks
30
- that use the Software as one internal capability.
29
+ - Browser automation you build and operate for your own purposes.
31
30
  - QA, testing, and accessibility tooling.
32
31
  - Any application where the captcha solve is a means to an end the user
33
32
  is actually paying for.
34
33
 
34
+ (c) RUNNING the Software alongside a stealth, anti-detection, or antidetect
35
+ browser — Camoufox, Puppeteer, Playwright, or any similar tool — to
36
+ automate your own work, commercially or otherwise. Section 3(d) restricts
37
+ SHIPPING the Software as a feature of such a browser to other people. It
38
+ does not restrict USING it with one.
39
+
35
40
  ================================================================================
36
41
  3. PROHIBITED USES
37
42
  ================================================================================
@@ -51,7 +56,23 @@ holder:
51
56
  models (e.g. bounding boxes, tile selections, click plans) through a paid
52
57
  or public API as a captcha-solving service.
53
58
 
54
- In short: you may BUILD WITH this Software, but you may not SELL THE SOLVE.
59
+ (d) Embed, bundle, preinstall, or otherwise ship the Software — or a client,
60
+ wrapper, plugin, or extension for it — as a captcha-solving capability of
61
+ any stealth browser, anti-detection or antidetect browser, browser
62
+ profile or identity manager, or browser-automation product or platform
63
+ that you distribute, host, or otherwise offer to third parties. This
64
+ applies whether the Software ships inside your distribution, is fetched
65
+ on demand, or is merely wired up and advertised as a supported
66
+ integration, and it applies whether or not you charge for it.
67
+
68
+ This restriction is about DISTRIBUTION, not use. Nothing in it limits
69
+ Section 2(c): a person who uses such a browser may install and run the
70
+ Software with it freely.
71
+
72
+ In short: you may BUILD WITH this Software, and you may USE it with whatever
73
+ browser you like — but you may not SELL THE SOLVE, and you may not SHIP THE
74
+ SOLVE as a feature of a stealth browser or automation product you hand to
75
+ other people.
55
76
 
56
77
  ================================================================================
57
78
  4. MODEL OUTPUTS
@@ -65,6 +86,7 @@ outputs as a captcha-solving service is prohibited under Section 3(c).
65
86
  ================================================================================
66
87
  If you redistribute the Software (modified or not), you must retain this license
67
88
  and this notice, and you must not remove or alter the restrictions in Section 3.
89
+ The right to redistribute granted by Section 1 is subject to Section 3(d).
68
90
 
69
91
  ================================================================================
70
92
  6. TRADEMARKS
@@ -95,11 +117,29 @@ not endorse using the Software to violate any third party's terms or rights.
95
117
  9. COMMERCIAL LICENSING
96
118
  ================================================================================
97
119
  For uses prohibited under Section 3 — including selling captcha-solving as a
98
- service — contact the copyright holder to discuss a separate commercial license.
120
+ service, and shipping the Software as a built-in capability of a stealth browser
121
+ or automation product — contact the copyright holder to discuss a separate
122
+ commercial license. These uses are licensable; they are not categorically
123
+ refused.
124
+
125
+ ================================================================================
126
+ 10. VERSION HISTORY
127
+ ================================================================================
128
+ v1.1 — Section 3(d) added: shipping the Software as a captcha-solving feature of
129
+ a stealth/anti-detection browser or automation product distributed to
130
+ third parties now requires a commercial license. Section 2(c) added to
131
+ make clear that USING the Software with such a browser remains permitted.
132
+ The v1.0 text listed "stealth / anti-detection browsers" as an
133
+ illustrative PERMITTED commercial use; that example is removed.
134
+ v1.0 — Initial release.
135
+
136
+ Each version applies to the releases distributed under it. Copies obtained under
137
+ v1.0 remain governed by v1.0.
99
138
 
100
139
  --------------------------------------------------------------------------------
101
140
  This text is a license, not legal advice. If the boundary between a permitted
102
- "value-add" product and a prohibited "thin wrapper" is unclear for your use
141
+ "value-add" product and a prohibited "thin wrapper", or between using the
142
+ Software with a browser and shipping it as part of one, is unclear for your use
103
143
  case, ask by opening an issue or messaging the maintainer on GitHub:
104
144
  https://github.com/JWriter20/CaptchaKraken
105
145
  --------------------------------------------------------------------------------
package/python/README.md CHANGED
@@ -22,6 +22,20 @@ if you want to run the model yourself. The one-command
22
22
  [`setup.sh`](https://github.com/JWriter20/CaptchaKraken) installs `[serve]`,
23
23
  downloads the weights, and writes an env file for you.
24
24
 
25
+ ## No GPU? Use the hosted API
26
+
27
+ Point the client at `https://api.captchakraken.com/v1` and run no model at all.
28
+ Sign in at [captchakraken.com/signin](https://captchakraken.com/signin) for a
29
+ `ck_live_…` key, or let the MCP server write one for you:
30
+
31
+ ```bash
32
+ claude mcp add captchakraken -- npx -y captchakraken-mcp
33
+ # then call sign_in, then create_api_key
34
+ ```
35
+
36
+ `create_api_key` writes the key and the endpoint to `~/.captchakraken/credentials`,
37
+ which the client reads on its own — **no environment variables needed**.
38
+
25
39
  ## Hands-off server
26
40
 
27
41
  The vLLM server is managed for you. On your first solve, if the configured
@@ -59,15 +73,18 @@ Everything model-specific lives in `captchakraken.config` and is env-overridable
59
73
 
60
74
  | Variable | Meaning | Default |
61
75
  |---|---|---|
62
- | `VLLM_BASE_URL` | Inference endpoint | `http://localhost:8000/v1` |
63
- | `CAPTCHA_KRAKEN_API_KEY` | Bearer token (`VLLM_API_KEY` also accepted) | `EMPTY` |
64
- | `CAPTCHA_BASE_MODEL` | Base weights vLLM loads | `Qwen/Qwen3.5-9B` |
65
- | `CAPTCHA_LORA_ADAPTER` | Captcha adapter (HF id or path) | `CaptchaKraken/CaptchaKraken_v1` |
66
- | `CAPTCHA_LORA_NAME` | Served adapter name the client requests | `captcha` |
76
+ | `VLLM_BASE_URL` | Inference endpoint | `~/.captchakraken/credentials`, else `http://localhost:8000/v1` |
77
+ | `CAPTCHA_KRAKEN_API_KEY` | Bearer token (`VLLM_API_KEY` also accepted) | `~/.captchakraken/credentials`, else `EMPTY` |
78
+ | `CAPTCHA_BASE_MODEL` | Base weights vLLM loads | `RedHatAI/Qwen3.5-9B-FP8-dynamic` |
79
+ | `CAPTCHA_LORA_ADAPTER` | Captcha adapter (HF id or path) | `CaptchaKraken/CaptchaKraken-Lora-v1.2` |
80
+ | `CAPTCHA_LORA_NAME` | Served adapter name the client requests | `captcha-v12` |
67
81
  | `CAPTCHA_KRAKEN_AUTOSTART` | `0` disables local auto-start | `1` |
68
82
 
69
83
  ## License
70
84
 
71
- **CaptchaKraken Source-Available License v1.0** — see [LICENSE](./LICENSE).
72
- Build *with* it (scrapers, stealth browsers, QA tooling); you may **not sell the
73
- solve itself** or ship a thin wrapper (browser extension, hosted solving API).
85
+ **CaptchaKraken Source-Available License v1.1** — see [LICENSE](./LICENSE).
86
+ Build *with* it (scrapers, QA tooling) and run it against **any** browser you
87
+ like, stealth or not. You may **not sell the solve itself**, ship a thin wrapper
88
+ (browser extension, hosted solving API), or **bundle it as a built-in feature of
89
+ a stealth/antidetect browser you distribute** — using it with one is fine. Those
90
+ three are licensable, not categorically refused: open an issue to ask.
@@ -66,3 +66,39 @@ HEADLESS=0 python examples/demoRecaptcha.py # watch the browser
66
66
  `gen speed` = model output tokens ÷ solve seconds. Failure reasons the harness
67
67
  reports: unreachable vLLM server, an unsupported hCaptcha puzzle (drag/video),
68
68
  the challenge iframe never appearing, or the model returning no matching tiles.
69
+
70
+ ## Playwright, and the auto-solver
71
+
72
+ | File | What it shows |
73
+ |---|---|
74
+ | `with_playwright.py` | Vanilla Playwright — no adapter, 2 added lines |
75
+ | `watch_playwright.py` | The auto-solver: install once, solves as they appear |
76
+
77
+ ```bash
78
+ pip install playwright && playwright install chromium
79
+
80
+ export VLLM_BASE_URL=https://api.captchakraken.com/v1
81
+ export CAPTCHA_KRAKEN_API_KEY=ck_live_...
82
+
83
+ python examples/with_playwright.py
84
+ python examples/watch_playwright.py https://your.site/page 60
85
+ ```
86
+
87
+ Use `sync_playwright`, not `async_playwright`: `PageSolver` mirrors the
88
+ synchronous API, and a sync handle cannot be driven from inside an event loop.
89
+
90
+ ## There is no Puppeteer path in Python
91
+
92
+ The TypeScript port ships `fromPuppeteer()` because Puppeteer is a first-class
93
+ Node library. Python has no equivalent worth pointing you at:
94
+
95
+ - **pyppeteer** last released 2.0.0 in **February 2024**, and pins
96
+ `urllib3 <2.0.0` and `websockets <11.0` — which conflicts directly with
97
+ CaptchaKraken's own `requests` dependency, so the two cannot co-install
98
+ cleanly.
99
+ - It is **async-only**, and this driver is synchronous. Even resolving the
100
+ dependency conflict, a sync `PageSolver` cannot be driven from asyncio.
101
+
102
+ Playwright *is* the Python answer, and `patchright` and `camoufox` are the
103
+ stealth-patched builds of it — all three duck-type identically, so the examples
104
+ above work unchanged with any of them.
@@ -1,39 +1,87 @@
1
1
  """
2
2
  Shared runner for the CaptchaKraken Python demos.
3
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.
4
+ Launches a real stealth browser (camoufox, using the binary from your fork —
5
+ JWriter20/camoufox releases; see README.md), navigates to a page with a captcha
6
+ on it, and solves it THE WAY PRODUCTION DOES: `PageSolver` finds the widget,
7
+ opens the challenge, clicks, submits, and decides whether the vendor accepted.
8
+ It reports token-generation speed, total time, and whether the solve succeeded,
9
+ plus a best-effort reason when it didn't.
10
+
11
+ Point it at any URL:
12
+
13
+ python examples/demoHcaptcha.py # the built-in demo page
14
+ python examples/demoHcaptcha.py https://your.site/ # anything else
15
+ python examples/demoHcaptcha.py https://your.site/ --headed
16
+
17
+ This used to be image-in / actions-out — screenshot the challenge, ask the
18
+ engine for a click plan, print how many tiles it would have clicked, and stop.
19
+ That measured the model but never the driver, and a recording of it showed a
20
+ captcha nobody touched. `captchakraken.page_solver` is the Python mirror of the
21
+ TypeScript driver, so both ports now demo the same end-to-end path, which is
22
+ what CLAUDE.md rule 1c asks for.
14
23
  """
15
24
 
25
+ import argparse
26
+ import json
16
27
  import os
17
- import tempfile
28
+ import sys
18
29
  import time
19
- from dataclasses import dataclass
30
+ from dataclasses import dataclass, replace
20
31
 
21
32
  from camoufox.sync_api import Camoufox
22
33
 
23
- from captchakraken import CaptchaSolver
24
- from captchakraken.action_types import ClickAction
25
- from captchakraken.solver import UnsupportedCaptchaError
34
+ from captchakraken.page_solver import CaptchaSolveError, PageSolver
26
35
 
27
36
 
28
37
  @dataclass
29
38
  class DemoSpec:
30
39
  name: str
31
40
  url: str
32
- vendor: str # "recaptcha" | "hcaptcha"
41
+ vendor: str # "recaptcha" | "hcaptcha" — advisory; PageSolver auto-detects
42
+
43
+
44
+ def spec_from_argv(default: DemoSpec, argv=None) -> DemoSpec:
45
+ """Let any demo point at an arbitrary page.
46
+
47
+ The vendor stays a default rather than being inferred from the URL: it only
48
+ selects the wording of a failure explanation, and PageSolver detects the
49
+ actual widget itself. Guessing it from a hostname would be wrong exactly on
50
+ the pages worth demoing — your own site, embedding someone else's captcha.
51
+ """
52
+ ap = argparse.ArgumentParser(
53
+ description=f"CaptchaKraken demo — {default.name}",
54
+ epilog="With no URL, runs against the built-in demo page.")
55
+ ap.add_argument("url", nargs="?", default=default.url,
56
+ help=f"page to solve (default: {default.url})")
57
+ ap.add_argument("--vendor", default=default.vendor,
58
+ choices=("hcaptcha", "recaptcha"),
59
+ help="only affects failure wording; the solver auto-detects")
60
+ ap.add_argument("--name", default=None, help="label for the report")
61
+ ap.add_argument("--headed", action="store_true",
62
+ help="show the browser (same as HEADLESS=0)")
63
+ args = ap.parse_args(argv)
64
+
65
+ if args.headed:
66
+ os.environ["HEADLESS"] = "0"
67
+ name = args.name or (default.name if args.url == default.url else args.url)
68
+ return replace(default, url=args.url, vendor=args.vendor, name=name)
33
69
 
34
70
 
35
71
  def _launch_kwargs() -> dict:
36
- kw = dict(headless=os.getenv("HEADLESS", "1") != "0", humanize=True, geoip=False)
72
+ # HUMANIZE defaults OFF, same as tests/live-solve/solve_fixture.py, and for
73
+ # the same measured reason. The solver already moves the mouse along its own
74
+ # 60-point trajectory (`_smooth_move` -> `generate_trajectory`); camoufox's
75
+ # humanize juggler then expands EACH of those 60 micro-moves into its own
76
+ # humanised sub-trajectory — 60 nested traversals to cover one straight
77
+ # line. On this demo page that is 25-52s per click round instead of ~5s,
78
+ # which does not merely look slow: it blows the 120s overall_solve_timeout
79
+ # and reports a solvable captcha as "not solved". What a vendor scores is
80
+ # the trajectory SHAPE, and that comes from generate_trajectory either way.
81
+ # Set HUMANIZE=1 to drive it the other way against a vendor that
82
+ # fingerprints motion.
83
+ kw = dict(headless=os.getenv("HEADLESS", "1") != "0",
84
+ humanize=os.getenv("HUMANIZE", "0") != "0", geoip=False)
37
85
  # Point camoufox at YOUR fork's binary. If unset, camoufox uses its default
38
86
  # cached binary (`python -m camoufox fetch`).
39
87
  binary = os.getenv("CAMOUFOX_BINARY") or os.getenv("CAMOUFOX_EXECUTABLE_PATH")
@@ -42,117 +90,157 @@ def _launch_kwargs() -> dict:
42
90
  return kw
43
91
 
44
92
 
45
- def _capture_challenge(page, spec: DemoSpec) -> str:
46
- """Reach the challenge iframe and screenshot it to a temp PNG. Returns the path.
93
+ def _page_kwargs() -> dict:
94
+ """Record the session when CAPTCHA_DEMO_VIDEO_DIR is set.
47
95
 
48
- Raises RuntimeError with a human message if the challenge never appears.
96
+ Plain Playwright options, but they only produce a video on a build whose
97
+ screencast actually emits frames. Stock camoufox accepts every one of them
98
+ and writes either nothing or a blank 0.96s file — no error anywhere — so a
99
+ caller that cares should check the size of what it gets back.
100
+ """
101
+ out = os.getenv("CAPTCHA_DEMO_VIDEO_DIR")
102
+ if not out:
103
+ return {}
104
+ w, _, h = os.getenv("CAPTCHA_DEMO_VIDEO_SIZE", "1280x800").partition("x")
105
+ return {"record_video_dir": out,
106
+ "record_video_size": {"width": int(w), "height": int(h)}}
107
+
108
+
109
+ def _finish_video(page):
110
+ """Close the context so the container is finalised; return the file path.
111
+
112
+ Playwright writes the video on CONTEXT close, not page close — reading
113
+ video.path() before that names a file which may never appear.
114
+ """
115
+ video = getattr(page, "video", None)
116
+ if video is None:
117
+ return None
118
+ try:
119
+ page.context.close()
120
+ return video.path()
121
+ except Exception: # noqa: BLE001 — a missing video must not fail the demo
122
+ return None
123
+
124
+
125
+ def _tokens(usage) -> tuple[int, int]:
126
+ """(input, output) over a SolveResult's usage log.
127
+
128
+ Accepts both spellings because the two ports report different ones and this
129
+ is a demo, not a place to discover a key mismatch as a zero.
49
130
  """
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
131
  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
132
+ for u in usage or []:
133
+ inp += int(u.get("prompt_tokens", u.get("inputTokens", 0)) or 0)
134
+ out += int(u.get("completion_tokens", u.get("outputTokens", 0)) or 0)
135
+ return inp, out
83
136
 
84
137
 
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:
138
+ def _explain(vendor: str, solved: bool, err) -> str:
139
+ msg = str(err or "").lower()
140
+ if "unsupported" in msg or "cannot solve" in msg:
88
141
  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."
142
+ return ("hCaptcha served a challenge type the solver does not handle "
143
+ "yet (drag / video / choose-the-card) — re-run for a grid.")
144
+ return "Not a supported challenge type — re-run to try again."
145
+ if "no captcha" in msg or "nocaptchafound" in msg:
146
+ return ("No captcha widget was found on that page. Check the URL, or "
147
+ "the widget may load only after an interaction.")
92
148
  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."
149
+ return ("Could not reach the vLLM server — is it up and is VLLM_BASE_URL "
150
+ "correct? (A local server auto-starts only with captchakraken[serve].)")
151
+ if "timeout" in msg:
152
+ return "Timed out — the page or challenge never became interactable."
153
+ if err:
154
+ return str(err)
155
+ if not solved:
156
+ return ("The answer was submitted but the vendor did not accept it. Most "
157
+ "often IP reputation or fingerprint flagging rather than a wrong "
158
+ "answer — try a cleaner IP or a residential proxy.")
159
+ return "Unknown failure."
98
160
 
99
161
 
100
162
  def run_demo(spec: DemoSpec) -> None:
163
+ spec = spec_from_argv(spec)
164
+
101
165
  t0 = time.time()
102
166
  ok = False
103
167
  reason = None
104
168
  inp = out = 0
105
- tps = 0.0
106
169
  solve_s = 0.0
107
- plan_desc = "-"
170
+ video_path = None
171
+ err = None
108
172
 
109
173
  try:
110
174
  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()
175
+ page = browser.new_page(**_page_kwargs())
117
176
  try:
118
- actions = solver.solve(shot, puzzle_source=spec.vendor)
177
+ page.goto(spec.url, wait_until="domcontentloaded", timeout=60_000)
178
+ # The widget's iframe injects after DOMContentLoaded on every
179
+ # vendor; the TS harness waits the same way.
180
+ try:
181
+ page.wait_for_load_state("networkidle", timeout=15_000)
182
+ except Exception: # noqa: BLE001 — a busy page is still solvable
183
+ pass
184
+ page.wait_for_timeout(3000)
185
+
186
+ s0 = time.time()
187
+ try:
188
+ result = PageSolver().solve(page)
189
+ finally:
190
+ solve_s = time.time() - s0
191
+ ok = bool(result.is_solved)
192
+ inp, out = _tokens(result.token_usage)
193
+ if not ok:
194
+ reason = _explain(spec.vendor, ok, None)
195
+ except CaptchaSolveError as e:
196
+ err = e
197
+ reason = _explain(spec.vendor, False, e)
119
198
  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
-
199
+ # In the finally so a failed run still yields its recording —
200
+ # that is the run you most want to watch back.
201
+ video_path = _finish_video(page)
202
+ except Exception as e: # noqa: BLE001 — demo: report, don't traceback
203
+ err = e
204
+ reason = _explain(spec.vendor, False, e)
205
+
206
+ tps = out / solve_s if out and solve_s > 0 else 0.0
136
207
  _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)
208
+ inp=inp, out=out, tps=tps, reason=reason, video=video_path)
138
209
 
139
210
 
140
211
  def _fmt(s: float) -> str:
141
212
  return f"{s:.1f}s" if s >= 1 else f"{int(s * 1000)}ms"
142
213
 
143
214
 
144
- def _report(spec, *, ok, total_s, solve_s, inp, out, tps, plan, reason):
215
+ def _report(spec, *, ok, total_s, solve_s, inp, out, tps, reason, video=None):
145
216
  line = "─" * 52
146
217
  print(f"\n{line}")
147
- print(f" CaptchaKraken demo (engine) — {spec.name}")
218
+ print(f" CaptchaKraken demo — {spec.name}")
148
219
  print(f" {spec.url}")
149
220
  print(line)
150
- print(f" result : {'✓ engine produced a solution' if ok else '✗ no solution'}")
151
- print(f" {plan}")
221
+ print(f" result : {'✓ SOLVED' if ok else '✗ not solved'}")
152
222
  print(f" total time : {_fmt(total_s)} (solve: {_fmt(solve_s)})")
153
223
  print(f" tokens : {inp} in / {out} out")
154
224
  print(f" gen speed : {f'~{tps:.1f} tok/s' if tps > 0 else 'n/a'}")
225
+ if video:
226
+ print(f" video : {video}")
155
227
  if reason:
156
228
  print(f" reason : {reason}")
157
229
  print(f"{line}\n")
158
- raise SystemExit(0 if ok else 1)
230
+
231
+ # Optional machine-readable copy of the same report, appended as one JSON
232
+ # line. The printed block is for a human reading a terminal; anything that
233
+ # wants to keep the result — compare two adapters, chart tok/s over a
234
+ # week — should not have to scrape it back out of that text.
235
+ record = os.getenv("CAPTCHA_DEMO_RECORD")
236
+ if record:
237
+ with open(record, "a", encoding="utf-8") as fh:
238
+ fh.write(json.dumps({
239
+ "demo": spec.name, "url": spec.url, "vendor": spec.vendor,
240
+ "ok": ok, "reason": reason,
241
+ "total_s": round(total_s, 1), "solve_s": round(solve_s, 1),
242
+ "input_tokens": inp, "output_tokens": out,
243
+ "tokens_per_sec": round(tps, 1), "video": video,
244
+ }) + "\n")
245
+
246
+ sys.exit(0 if ok else 1)
@@ -4,7 +4,8 @@ Engine demo: run CaptchaKraken on the standard hCaptcha demo page.
4
4
  pip install -e ".[serve]" # engine + serving stack (or [.] against a remote server)
5
5
  pip install camoufox && python -m camoufox fetch # or set CAMOUFOX_BINARY to your fork binary
6
6
  source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
7
- python examples/demoHcaptcha.py
7
+ python examples/demoHcaptcha.py # this demo page
8
+ python examples/demoHcaptcha.py https://your.site/ # any URL
8
9
 
9
10
  Note: hCaptcha randomly serves non-grid puzzles (drag / video / choose-the-card),
10
11
  which the engine does not handle yet — the report says so; re-run for a grid.
@@ -4,7 +4,8 @@ Engine demo: run CaptchaKraken on Google's standard reCAPTCHA v2 demo page.
4
4
  pip install -e ".[serve]" # engine + serving stack (or [.] against a remote server)
5
5
  pip install camoufox && python -m camoufox fetch # or set CAMOUFOX_BINARY to your fork binary
6
6
  source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
7
- python examples/demoRecaptcha.py
7
+ python examples/demoRecaptcha.py # this demo page
8
+ python examples/demoRecaptcha.py https://your.site/ # any URL
8
9
  """
9
10
 
10
11
  from _harness import DemoSpec, run_demo
@@ -0,0 +1,56 @@
1
+ """
2
+ The auto-solver: install once, and captchas are solved as they appear.
3
+
4
+ `solve(page)` handles whatever is on the page right now. `watch(page)` is for
5
+ when you do not know WHERE in a script a challenge will interrupt you.
6
+
7
+ python examples/watch_playwright.py [url] [seconds]
8
+
9
+ TWO SHAPES, because this port is synchronous:
10
+
11
+ solver.watch(page).run() # blocking — hold this page clean
12
+ w = solver.watch(page) # cooperative — you own the cadence
13
+ while working():
14
+ w.poll_once()
15
+
16
+ The TypeScript twin returns a background handle instead. That difference is
17
+ Playwright's, not ours: a sync handle is bound to the greenlet that created it,
18
+ so a worker thread cannot drive the page.
19
+
20
+ It injects nothing into the page on any launcher. Under camoufox its DOM reads
21
+ run in the isolated Juggler world by default.
22
+ """
23
+
24
+ import sys
25
+
26
+ from playwright.sync_api import sync_playwright
27
+
28
+ from captchakraken import PageSolver
29
+
30
+ URL = sys.argv[1] if len(sys.argv) > 1 else "https://www.google.com/recaptcha/api2/demo"
31
+ SECONDS = int(sys.argv[2]) if len(sys.argv) > 2 else 30
32
+
33
+
34
+ def main() -> int:
35
+ with sync_playwright() as p:
36
+ browser = p.chromium.launch(headless=True)
37
+ page = browser.new_page()
38
+
39
+ # ── install once, then let it work ───────────────────────────────────
40
+ watcher = PageSolver().watch(
41
+ page,
42
+ on_solved=lambda r: print("✅ solved one:", r.is_solved),
43
+ on_error=lambda e: print("solve failed:", e),
44
+ )
45
+ # ─────────────────────────────────────────────────────────────────────
46
+
47
+ page.goto(URL)
48
+ watcher.run(timeout_ms=SECONDS * 1000) # blocks; solves as they appear
49
+
50
+ print(f"stopped after {watcher.solves} solve(s)")
51
+ browser.close()
52
+ return 0 if watcher.solves else 1
53
+
54
+
55
+ if __name__ == "__main__":
56
+ raise SystemExit(main())
@@ -0,0 +1,43 @@
1
+ """
2
+ Vanilla Playwright + CaptchaKraken.
3
+
4
+ No adapter, and no browser dependency in the package: the driver duck-types the
5
+ Playwright surface, so a real `sync_playwright` page satisfies it as-is — as do
6
+ patchright's and camoufox's.
7
+
8
+ pip install captchakraken playwright && playwright install chromium
9
+ export VLLM_BASE_URL=https://api.captchakraken.com/v1
10
+ export CAPTCHA_KRAKEN_API_KEY=ck_live_...
11
+ python examples/with_playwright.py [url]
12
+
13
+ SYNC, NOT ASYNC. `PageSolver` mirrors the synchronous Playwright API. A sync
14
+ handle cannot be driven from inside an event loop, so `async_playwright` pages
15
+ cannot be solved — use `sync_playwright`, as here.
16
+ """
17
+
18
+ import sys
19
+
20
+ from playwright.sync_api import sync_playwright
21
+
22
+ from captchakraken import PageSolver
23
+
24
+ URL = sys.argv[1] if len(sys.argv) > 1 else "https://www.google.com/recaptcha/api2/demo"
25
+
26
+
27
+ def main() -> int:
28
+ with sync_playwright() as p:
29
+ browser = p.chromium.launch(headless=True)
30
+ page = browser.new_page()
31
+ page.goto(URL)
32
+
33
+ # ── the whole integration: construct, solve ──────────────────────────
34
+ result = PageSolver().solve(page)
35
+ # ─────────────────────────────────────────────────────────────────────
36
+
37
+ print("✅ solved" if result.is_solved else "❌ not solved")
38
+ browser.close()
39
+ return 0 if result.is_solved else 1
40
+
41
+
42
+ if __name__ == "__main__":
43
+ raise SystemExit(main())