captchakraken 2.4.0 → 2.6.1
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.
- package/LICENSE +47 -7
- package/README.md +60 -13
- package/dist/cli-invocation.d.ts +46 -0
- package/dist/cli-invocation.js +68 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +29 -1
- package/dist/limits.d.ts +51 -0
- package/dist/limits.js +54 -0
- package/dist/model-name.d.ts +23 -0
- package/dist/model-name.js +70 -0
- package/dist/playwright-types.d.ts +28 -0
- package/dist/puppeteer-adapter.d.ts +11 -0
- package/dist/puppeteer-adapter.js +23 -0
- package/dist/python-command.d.ts +50 -0
- package/dist/python-command.js +60 -0
- package/dist/slide-geometry.d.ts +33 -0
- package/dist/slide-geometry.js +64 -0
- package/dist/solver.d.ts +217 -12
- package/dist/solver.js +1332 -172
- package/dist/trajectory.d.ts +31 -0
- package/dist/trajectory.js +162 -0
- package/dist/types.d.ts +158 -11
- package/dist/watcher.d.ts +99 -0
- package/dist/watcher.js +129 -0
- package/package.json +12 -7
- package/python/LICENSE +47 -7
- package/python/README.md +58 -13
- package/python/examples/README.md +36 -0
- package/python/examples/_harness.py +182 -94
- package/python/examples/demoHcaptcha.py +2 -1
- package/python/examples/demoRecaptcha.py +2 -1
- package/python/examples/watch_playwright.py +56 -0
- package/python/examples/with_playwright.py +43 -0
- package/python/pyproject.toml +24 -6
- package/python/src/captchakraken/__init__.py +5 -3
- package/python/src/captchakraken/action_types.py +24 -3
- package/python/src/captchakraken/cli.py +212 -2
- package/python/src/captchakraken/config.py +39 -5
- package/python/src/captchakraken/keyframes.py +690 -0
- package/python/src/captchakraken/models.json +120 -0
- package/python/src/captchakraken/page_solver.py +1559 -105
- package/python/src/captchakraken/pinned_model.json +5 -7
- package/python/src/captchakraken/planner.py +253 -59
- package/python/src/captchakraken/prompts.py +485 -0
- package/python/src/captchakraken/solver.py +182 -11
- package/python/src/captchakraken/timing.py +70 -0
- package/python/src/captchakraken/tool_calls/find_grid.py +367 -52
- package/python/src/captchakraken/tool_calls/track_piece.py +96 -0
- package/python/src/captchakraken/trajectory.py +2 -7
- package/python/src/captchakraken/watcher.py +193 -0
- package/python/src/captchakraken/planner_types.py +0 -74
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "captchakraken",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.6.1",
|
|
4
|
+
"description": "Captcha solver for Playwright and Puppeteer. Finds the captcha, reads the puzzle with a fine-tuned Qwen3.5-9B vision model, and clicks, drags, slides or types through to a token — reCAPTCHA, hCaptcha, GeeTest, NetEase Yidun, Tencent, Lemin, Prosopo, Turnstile and distorted text. Hosted API or self-hosted.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"prebuild": "node scripts/copy-python.mjs",
|
|
15
15
|
"build": "tsc",
|
|
16
16
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
17
|
-
"test": "tsc
|
|
17
|
+
"test": "tsc -p tsconfig.test.json && node --test .test-build/*.test.js",
|
|
18
18
|
"prepublishOnly": "npm run build",
|
|
19
19
|
"postinstall": "node scripts/setup-python.js"
|
|
20
20
|
},
|
|
@@ -25,11 +25,19 @@
|
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"captcha",
|
|
28
|
+
"captcha-solver",
|
|
28
29
|
"recaptcha",
|
|
29
30
|
"hcaptcha",
|
|
31
|
+
"geetest",
|
|
32
|
+
"turnstile",
|
|
33
|
+
"yidun",
|
|
34
|
+
"tencent-captcha",
|
|
30
35
|
"playwright",
|
|
31
36
|
"puppeteer",
|
|
32
|
-
"
|
|
37
|
+
"camoufox",
|
|
38
|
+
"browser-automation",
|
|
39
|
+
"scraping",
|
|
40
|
+
"anti-bot",
|
|
33
41
|
"vllm",
|
|
34
42
|
"qwen"
|
|
35
43
|
],
|
|
@@ -43,8 +51,5 @@
|
|
|
43
51
|
"devDependencies": {
|
|
44
52
|
"@types/node": "^24.10.2",
|
|
45
53
|
"typescript": "^5.9.3"
|
|
46
|
-
},
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"cursory-ts": "^1.0.0"
|
|
49
54
|
}
|
|
50
55
|
}
|
package/python/LICENSE
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
CaptchaKraken Source-Available License v1.
|
|
2
|
-
Copyright (c) 2026
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
@@ -1,14 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/logo-card.png" alt="CaptchaKraken" width="128" height="128">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
<h1 align="center">captchakraken</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>A captcha solver for browser automation.</b><br>
|
|
9
|
+
The Python engine and CLI behind <a href="https://github.com/JWriter20/CaptchaKraken">CaptchaKraken</a>.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
OpenCV tile detection plus a fine-tuned **Qwen3.5-9B** vision model. Give it a
|
|
13
|
+
screenshot of a captcha and it returns the plan to solve it — which tiles to
|
|
14
|
+
select, where to click, what to drag, how far to slide, or what text to type.
|
|
15
|
+
Ships the `captchakraken` command.
|
|
16
|
+
|
|
17
|
+
Run the model on **your own hardware**, or point it at the **hosted API** and run
|
|
18
|
+
nothing at all.
|
|
7
19
|
|
|
8
20
|
> For demo videos, accuracy numbers, the browser driver, and the full
|
|
9
21
|
> self-hosting guide, see the main repo
|
|
10
22
|
> **[CaptchaKraken](https://github.com/JWriter20/CaptchaKraken)**.
|
|
11
23
|
|
|
24
|
+
## What it solves
|
|
25
|
+
|
|
26
|
+
| Vendor | Puzzles |
|
|
27
|
+
|---|---|
|
|
28
|
+
| **reCAPTCHA** | 3×3 and 4×4 image grids, including the dynamic re-deal |
|
|
29
|
+
| **hCaptcha** | Image grids, click, drag, connect-the-path, tetris-fit, animated |
|
|
30
|
+
| **GeeTest** v3 + v4 | Slide, icon, nine, svg, gobang, iconcrush |
|
|
31
|
+
| **NetEase Yidun** | Jigsaw, picture-click, icon-click |
|
|
32
|
+
| **Tencent, Lemin, Prosopo** | Slide, cropped-image and grid flows |
|
|
33
|
+
| **BotDetect, MTCaptcha, Yandex** | Distorted text — read and typed, not clicked |
|
|
34
|
+
| **Cloudflare Turnstile** | Via the checkbox flow (free on the hosted API) |
|
|
35
|
+
|
|
36
|
+
**44 puzzle types**, driven end to end in CI against generated fixtures on both
|
|
37
|
+
the TypeScript and Python ports. Animated challenges are recorded, sliced into
|
|
38
|
+
keyframes and answered with the frame the action belongs to.
|
|
39
|
+
|
|
12
40
|
## Install
|
|
13
41
|
|
|
14
42
|
```bash
|
|
@@ -22,6 +50,20 @@ if you want to run the model yourself. The one-command
|
|
|
22
50
|
[`setup.sh`](https://github.com/JWriter20/CaptchaKraken) installs `[serve]`,
|
|
23
51
|
downloads the weights, and writes an env file for you.
|
|
24
52
|
|
|
53
|
+
## No GPU? Use the hosted API
|
|
54
|
+
|
|
55
|
+
Point the client at `https://api.captchakraken.com/v1` and run no model at all.
|
|
56
|
+
Sign in at [captchakraken.com/signin](https://captchakraken.com/signin) for a
|
|
57
|
+
`ck_live_…` key, or let the MCP server write one for you:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
claude mcp add captchakraken -- npx -y captchakraken-mcp
|
|
61
|
+
# then call sign_in, then create_api_key
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`create_api_key` writes the key and the endpoint to `~/.captchakraken/credentials`,
|
|
65
|
+
which the client reads on its own — **no environment variables needed**.
|
|
66
|
+
|
|
25
67
|
## Hands-off server
|
|
26
68
|
|
|
27
69
|
The vLLM server is managed for you. On your first solve, if the configured
|
|
@@ -59,15 +101,18 @@ Everything model-specific lives in `captchakraken.config` and is env-overridable
|
|
|
59
101
|
|
|
60
102
|
| Variable | Meaning | Default |
|
|
61
103
|
|---|---|---|
|
|
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 | `
|
|
65
|
-
| `CAPTCHA_LORA_ADAPTER` | Captcha adapter (HF id or path) | `CaptchaKraken/
|
|
66
|
-
| `CAPTCHA_LORA_NAME` | Served adapter name the client requests | `captcha` |
|
|
104
|
+
| `VLLM_BASE_URL` | Inference endpoint | `~/.captchakraken/credentials`, else `http://localhost:8000/v1` |
|
|
105
|
+
| `CAPTCHA_KRAKEN_API_KEY` | Bearer token (`VLLM_API_KEY` also accepted) | `~/.captchakraken/credentials`, else `EMPTY` |
|
|
106
|
+
| `CAPTCHA_BASE_MODEL` | Base weights vLLM loads | `RedHatAI/Qwen3.5-9B-FP8-dynamic` |
|
|
107
|
+
| `CAPTCHA_LORA_ADAPTER` | Captcha adapter (HF id or path) | `CaptchaKraken/CaptchaKraken-Lora-v1.2` |
|
|
108
|
+
| `CAPTCHA_LORA_NAME` | Served adapter name the client requests | `captcha-v12` |
|
|
67
109
|
| `CAPTCHA_KRAKEN_AUTOSTART` | `0` disables local auto-start | `1` |
|
|
68
110
|
|
|
69
111
|
## License
|
|
70
112
|
|
|
71
|
-
**CaptchaKraken Source-Available License v1.
|
|
72
|
-
Build *with* it (scrapers,
|
|
73
|
-
|
|
113
|
+
**CaptchaKraken Source-Available License v1.1** — see [LICENSE](./LICENSE).
|
|
114
|
+
Build *with* it (scrapers, QA tooling) and run it against **any** browser you
|
|
115
|
+
like, stealth or not. You may **not sell the solve itself**, ship a thin wrapper
|
|
116
|
+
(browser extension, hosted solving API), or **bundle it as a built-in feature of
|
|
117
|
+
a stealth/antidetect browser you distribute** — using it with one is fine. Those
|
|
118
|
+
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
token-generation speed, total time, and whether the
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
46
|
-
"""
|
|
93
|
+
def _page_kwargs() -> dict:
|
|
94
|
+
"""Record the session when CAPTCHA_DEMO_VIDEO_DIR is set.
|
|
47
95
|
|
|
48
|
-
|
|
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
|
|
79
|
-
inp += int(u.get("prompt_tokens", 0) or 0)
|
|
80
|
-
out += int(u.get("completion_tokens", 0) or 0)
|
|
81
|
-
|
|
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,
|
|
86
|
-
msg = str(err).lower()
|
|
87
|
-
if
|
|
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
|
|
90
|
-
"
|
|
91
|
-
return "Not a supported
|
|
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
|
|
94
|
-
"(A local server auto-starts only
|
|
95
|
-
if "timeout" in msg
|
|
96
|
-
return "
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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,
|
|
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,
|
|
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
|
|
218
|
+
print(f" CaptchaKraken demo — {spec.name}")
|
|
148
219
|
print(f" {spec.url}")
|
|
149
220
|
print(line)
|
|
150
|
-
print(f" result : {'✓
|
|
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
|
-
|
|
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
|