captchakraken 2.9.0 → 3.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.
- package/AGENTS.md +112 -0
- package/NOTICE +36 -0
- package/dist/cli-invocation.d.ts +7 -36
- package/dist/cli-invocation.js +8 -49
- package/dist/errors.d.ts +7 -27
- package/dist/errors.js +6 -25
- package/dist/humanize.d.ts +56 -240
- package/dist/humanize.js +159 -415
- package/dist/index.d.ts +5 -129
- package/dist/index.js +4 -98
- package/dist/kinds.d.ts +135 -0
- package/dist/kinds.js +70 -0
- package/dist/limits.d.ts +0 -48
- package/dist/limits.js +2 -48
- package/dist/model-name.d.ts +4 -19
- package/dist/model-name.js +32 -40
- package/dist/playwright-types.d.ts +25 -95
- package/dist/playwright-types.js +0 -23
- package/dist/puppeteer-adapter.d.ts +4 -43
- package/dist/puppeteer-adapter.js +22 -58
- package/dist/python-command.d.ts +0 -41
- package/dist/python-command.js +0 -37
- package/dist/selectors.d.ts +54 -0
- package/dist/selectors.js +117 -0
- package/dist/slide-geometry.d.ts +2 -29
- package/dist/slide-geometry.js +18 -44
- package/dist/solver.d.ts +131 -708
- package/dist/solver.js +1319 -3382
- package/dist/timing.d.ts +7 -40
- package/dist/timing.js +4 -36
- package/dist/token-usage.d.ts +1 -7
- package/dist/token-usage.js +10 -95
- package/dist/types.d.ts +77 -493
- package/dist/watcher.d.ts +5 -78
- package/dist/watcher.js +0 -56
- package/package.json +11 -4
- package/python/AGENTS.md +122 -0
- package/python/NOTICE +36 -0
- package/python/README.md +1 -2
- package/python/examples/README.md +10 -0
- package/python/examples/_harness.py +4 -73
- package/python/examples/demoHcaptcha.py +0 -13
- package/python/examples/demoRecaptcha.py +0 -10
- package/python/examples/watch_playwright.py +1 -26
- package/python/examples/with_local_gguf.py +28 -0
- package/python/examples/with_playwright.py +0 -19
- package/python/pyproject.toml +68 -19
- package/python/src/captchakraken/__init__.py +37 -45
- package/python/src/captchakraken/action_types.py +14 -36
- package/python/src/captchakraken/cli.py +262 -883
- package/python/src/captchakraken/config.py +60 -146
- package/python/src/captchakraken/errors.py +17 -95
- package/python/src/captchakraken/humanize.py +174 -597
- package/python/src/captchakraken/image_processor.py +16 -235
- package/python/src/captchakraken/keyframes.py +46 -484
- package/python/src/captchakraken/kinds.py +152 -0
- package/python/src/captchakraken/models.json +36 -4
- package/python/src/captchakraken/overlay.py +65 -498
- package/python/src/captchakraken/page_solver.py +1282 -3101
- package/python/src/captchakraken/planner.py +65 -487
- package/python/src/captchakraken/prompts.py +45 -261
- package/python/src/captchakraken/selectors.py +184 -0
- package/python/src/captchakraken/server_manager.py +7 -58
- package/python/src/captchakraken/solver.py +128 -522
- package/python/src/captchakraken/timing.py +20 -74
- package/python/src/captchakraken/tool_calls/board_painted.py +47 -0
- package/python/src/captchakraken/tool_calls/find_checkbox.py +20 -30
- package/python/src/captchakraken/tool_calls/find_grid.py +186 -1339
- package/python/src/captchakraken/tool_calls/track_piece.py +81 -57
- package/python/src/captchakraken/updater.py +7 -76
- package/python/src/captchakraken/watcher.py +12 -101
- package/scripts/copy-python.mjs +10 -10
- package/scripts/setup-python.js +1 -21
- package/dist/trajectory.d.ts +0 -58
- package/dist/trajectory.js +0 -266
- package/python/src/captchakraken/tool_calls/move_indicator.py +0 -431
- package/python/src/captchakraken/trajectory.py +0 -284
package/dist/watcher.d.ts
CHANGED
|
@@ -1,99 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* WHY THIS IS A POLL AND NOT AN INJECTED OBSERVER
|
|
5
|
-
* The obvious implementation is a `MutationObserver` inside the page that calls
|
|
6
|
-
* out through `exposeBinding`. That is faster to react, and it is also the one
|
|
7
|
-
* design that cannot be made stealthy on every platform: `exposeBinding` puts a
|
|
8
|
-
* function on `window` and an observer is script the page can enumerate. On
|
|
9
|
-
* Camoufox both are sandboxed and invisible; on vanilla Playwright or Puppeteer
|
|
10
|
-
* they are not, and a captcha vendor is exactly the party that looks.
|
|
11
|
-
*
|
|
12
|
-
* So the watcher injects NOTHING. It drives the same `detectCaptcha()` the
|
|
13
|
-
* solver already uses, from the driver side, on a timer. Consequences worth
|
|
14
|
-
* knowing:
|
|
15
|
-
*
|
|
16
|
-
* - Nothing is added to the page on ANY platform, so there is no new
|
|
17
|
-
* detection surface to reason about per launcher.
|
|
18
|
-
* - On Camoufox the probe's DOM reads run in Camoufox's isolated Juggler
|
|
19
|
-
* world for free — that is its default for all Playwright evaluation
|
|
20
|
-
* (`main_world_eval` / an `mw:` prefix is the opt-OUT). The watcher never
|
|
21
|
-
* opts out, so it is isolated there by construction, not by special-casing.
|
|
22
|
-
* - Reaction time is bounded by `intervalMs`, not by the mutation itself.
|
|
23
|
-
* A captcha that appears just after a tick waits up to one interval.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* ONE WATCHER COVERS THE WHOLE PAGE, FOR ITS WHOLE LIFE
|
|
27
|
-
* A `Page` outlives navigation, and so does the watcher holding it: install it
|
|
28
|
-
* once and it keeps probing across every `goto`, staying quiet while there is
|
|
29
|
-
* nothing to solve and re-arming after each solve. That is deliberately the
|
|
30
|
-
* whole API — the case that motivates a browser-wide installer is almost always
|
|
31
|
-
* a challenge appearing on request 40 of a run, on the SAME page object the run
|
|
32
|
-
* started with, and that is already covered by one line. Pinned in
|
|
33
|
-
* browser-compat.test.ts.
|
|
34
|
-
* DETECTION IS THE SOLVER'S, NOT A COPY
|
|
35
|
-
* The trigger is `solver.detectCaptcha(page)`, the same method `solve()` calls,
|
|
36
|
-
* so "what counts as a captcha" has exactly one definition. An earlier sketch
|
|
37
|
-
* kept a union of the vendor selectors here for a cheaper single-roundtrip
|
|
38
|
-
* probe; it drifted from VENDOR_WIDGET_LOCATORS the first time a vendor was
|
|
39
|
-
* added, which is the failure this indirection exists to prevent.
|
|
40
|
-
*
|
|
41
|
-
* ```typescript
|
|
42
|
-
* const solver = new CaptchaKrakenSolver();
|
|
43
|
-
* const watcher = solver.watch(page, { onSolved: r => console.log(r.isSolved) });
|
|
44
|
-
* // ... your automation runs; captchas are solved underneath it ...
|
|
45
|
-
* await watcher.stop();
|
|
46
|
-
* ```
|
|
2
|
+
* Polls; injects nothing. An exposed binding or a MutationObserver is script the page can enumerate under
|
|
3
|
+
* vanilla Playwright or Puppeteer, and a captcha vendor is exactly the party that looks. See TRIBAL_KNOWLEDGE.md.
|
|
47
4
|
*/
|
|
48
5
|
import { PlaywrightPage as Page } from './playwright-types';
|
|
49
6
|
import { SolveResult } from './types';
|
|
50
|
-
/**
|
|
51
|
-
* The slice of the solver the watcher drives.
|
|
52
|
-
*
|
|
53
|
-
* Structural rather than an import of `CaptchaKrakenSolver` so this module has
|
|
54
|
-
* no runtime dependency on solver.ts (which imports this one for the `watch()`
|
|
55
|
-
* method — a real cycle under CommonJS), and so tests can drive it with a fake.
|
|
56
|
-
*/
|
|
7
|
+
/** Structural, not the solver class: a real import here would be a CommonJS cycle. Detection reuses detectCaptcha because a copied selector union drifted the first time a vendor was added. */
|
|
57
8
|
export interface WatchableSolver {
|
|
58
9
|
detectCaptcha(page: Page): Promise<unknown | null>;
|
|
59
10
|
solve(page: Page): Promise<SolveResult | void>;
|
|
60
11
|
}
|
|
61
12
|
export interface WatchOptions {
|
|
62
|
-
/**
|
|
63
|
-
* Milliseconds between detection probes. Default 1000.
|
|
64
|
-
*
|
|
65
|
-
* This is the latency floor for noticing a captcha, and each tick costs one
|
|
66
|
-
* `detectCaptcha()` — a handful of selector queries. Below ~250ms you are
|
|
67
|
-
* paying real CPU for latency the solve itself (seconds) makes irrelevant.
|
|
68
|
-
*/
|
|
13
|
+
/** Default 1000; below ~250 is wasted CPU. */
|
|
69
14
|
intervalMs?: number;
|
|
70
|
-
/** Stop automatically after this many successful solves. Default: unlimited. */
|
|
71
15
|
maxSolves?: number;
|
|
72
|
-
/**
|
|
73
|
-
* Wait this long after a solve THROWS before probing again. Default 5000.
|
|
74
|
-
*
|
|
75
|
-
* Without it a permanently unsupported challenge — an invisible reCAPTCHA v3,
|
|
76
|
-
* say — turns the watcher into a hot loop that re-attempts and re-bills every
|
|
77
|
-
* `intervalMs` forever.
|
|
78
|
-
*/
|
|
16
|
+
/** Default 5000. Without it a permanently unsupported challenge is a hot loop that re-attempts and re-bills every interval forever. */
|
|
79
17
|
errorBackoffMs?: number;
|
|
80
|
-
/** Called after each solve that returns a result. Exceptions here are swallowed. */
|
|
81
18
|
onSolved?: (result: SolveResult) => void | Promise<void>;
|
|
82
|
-
/**
|
|
83
|
-
* Called when detection or solving throws. Exceptions here are swallowed.
|
|
84
|
-
*
|
|
85
|
-
* Errors are reported, never fatal: `NoCaptchaFoundError` fires routinely
|
|
86
|
-
* when a widget disappears between the probe and the solve, and a watcher
|
|
87
|
-
* that died on it would be useless.
|
|
88
|
-
*/
|
|
89
19
|
onError?: (error: unknown) => void | Promise<void>;
|
|
90
20
|
}
|
|
91
21
|
export interface CaptchaWatcher {
|
|
92
|
-
/** Stop probing and resolve once any in-flight solve has finished. */
|
|
93
22
|
stop(): Promise<void>;
|
|
94
|
-
/** False once stopped, the page closed, or `maxSolves` was reached. */
|
|
95
23
|
readonly running: boolean;
|
|
96
|
-
/** Successful solves so far. */
|
|
97
24
|
readonly solves: number;
|
|
98
25
|
}
|
|
99
26
|
export declare function watchPage(solver: WatchableSolver, page: Page, options?: WatchOptions): CaptchaWatcher;
|
package/dist/watcher.js
CHANGED
|
@@ -1,58 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Auto-solve watcher — install once, and captchas are solved as they appear.
|
|
4
|
-
*
|
|
5
|
-
* WHY THIS IS A POLL AND NOT AN INJECTED OBSERVER
|
|
6
|
-
* The obvious implementation is a `MutationObserver` inside the page that calls
|
|
7
|
-
* out through `exposeBinding`. That is faster to react, and it is also the one
|
|
8
|
-
* design that cannot be made stealthy on every platform: `exposeBinding` puts a
|
|
9
|
-
* function on `window` and an observer is script the page can enumerate. On
|
|
10
|
-
* Camoufox both are sandboxed and invisible; on vanilla Playwright or Puppeteer
|
|
11
|
-
* they are not, and a captcha vendor is exactly the party that looks.
|
|
12
|
-
*
|
|
13
|
-
* So the watcher injects NOTHING. It drives the same `detectCaptcha()` the
|
|
14
|
-
* solver already uses, from the driver side, on a timer. Consequences worth
|
|
15
|
-
* knowing:
|
|
16
|
-
*
|
|
17
|
-
* - Nothing is added to the page on ANY platform, so there is no new
|
|
18
|
-
* detection surface to reason about per launcher.
|
|
19
|
-
* - On Camoufox the probe's DOM reads run in Camoufox's isolated Juggler
|
|
20
|
-
* world for free — that is its default for all Playwright evaluation
|
|
21
|
-
* (`main_world_eval` / an `mw:` prefix is the opt-OUT). The watcher never
|
|
22
|
-
* opts out, so it is isolated there by construction, not by special-casing.
|
|
23
|
-
* - Reaction time is bounded by `intervalMs`, not by the mutation itself.
|
|
24
|
-
* A captcha that appears just after a tick waits up to one interval.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* ONE WATCHER COVERS THE WHOLE PAGE, FOR ITS WHOLE LIFE
|
|
28
|
-
* A `Page` outlives navigation, and so does the watcher holding it: install it
|
|
29
|
-
* once and it keeps probing across every `goto`, staying quiet while there is
|
|
30
|
-
* nothing to solve and re-arming after each solve. That is deliberately the
|
|
31
|
-
* whole API — the case that motivates a browser-wide installer is almost always
|
|
32
|
-
* a challenge appearing on request 40 of a run, on the SAME page object the run
|
|
33
|
-
* started with, and that is already covered by one line. Pinned in
|
|
34
|
-
* browser-compat.test.ts.
|
|
35
|
-
* DETECTION IS THE SOLVER'S, NOT A COPY
|
|
36
|
-
* The trigger is `solver.detectCaptcha(page)`, the same method `solve()` calls,
|
|
37
|
-
* so "what counts as a captcha" has exactly one definition. An earlier sketch
|
|
38
|
-
* kept a union of the vendor selectors here for a cheaper single-roundtrip
|
|
39
|
-
* probe; it drifted from VENDOR_WIDGET_LOCATORS the first time a vendor was
|
|
40
|
-
* added, which is the failure this indirection exists to prevent.
|
|
41
|
-
*
|
|
42
|
-
* ```typescript
|
|
43
|
-
* const solver = new CaptchaKrakenSolver();
|
|
44
|
-
* const watcher = solver.watch(page, { onSolved: r => console.log(r.isSolved) });
|
|
45
|
-
* // ... your automation runs; captchas are solved underneath it ...
|
|
46
|
-
* await watcher.stop();
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
3
|
exports.watchPage = watchPage;
|
|
51
|
-
/** A page that has closed, where the launcher exposes that (Playwright and Puppeteer both do). */
|
|
52
4
|
function isPageClosed(page) {
|
|
53
5
|
return typeof page.isClosed === 'function' ? page.isClosed() : false;
|
|
54
6
|
}
|
|
55
|
-
/** Report to a user callback without letting its failure reach the loop. */
|
|
56
7
|
async function report(fn, value) {
|
|
57
8
|
if (!fn)
|
|
58
9
|
return;
|
|
@@ -60,7 +11,6 @@ async function report(fn, value) {
|
|
|
60
11
|
await fn(value);
|
|
61
12
|
}
|
|
62
13
|
catch {
|
|
63
|
-
// A throwing callback is the caller's bug and must not stop the watcher.
|
|
64
14
|
}
|
|
65
15
|
}
|
|
66
16
|
function watchPage(solver, page, options = {}) {
|
|
@@ -69,12 +19,8 @@ function watchPage(solver, page, options = {}) {
|
|
|
69
19
|
const maxSolves = options.maxSolves ?? Infinity;
|
|
70
20
|
let running = true;
|
|
71
21
|
let solves = 0;
|
|
72
|
-
// Resolved by the loop on its way out, so stop() can await a solve in flight.
|
|
73
|
-
// Definite assignment: the Promise executor runs synchronously, so `finished`
|
|
74
|
-
// is set before any code below can reach it — TS cannot prove that itself.
|
|
75
22
|
let finished;
|
|
76
23
|
const done = new Promise((resolve) => { finished = resolve; });
|
|
77
|
-
// Held so stop() can cut a pending sleep short instead of waiting it out.
|
|
78
24
|
let wake = null;
|
|
79
25
|
const sleep = (ms) => new Promise((resolve) => {
|
|
80
26
|
const timer = setTimeout(() => { wake = null; resolve(); }, ms);
|
|
@@ -91,8 +37,6 @@ function watchPage(solver, page, options = {}) {
|
|
|
91
37
|
try {
|
|
92
38
|
if (!(await solver.detectCaptcha(page)))
|
|
93
39
|
continue;
|
|
94
|
-
// Re-check: detectCaptcha can take a moment, and stop() during it
|
|
95
|
-
// should not be followed by a fresh multi-second solve.
|
|
96
40
|
if (!running)
|
|
97
41
|
break;
|
|
98
42
|
const result = await solver.solve(page);
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "captchakraken",
|
|
3
|
-
"version": "
|
|
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
|
|
3
|
+
"version": "3.1.0",
|
|
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 \u2014 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": [
|
|
8
8
|
"dist",
|
|
9
|
+
"NOTICE",
|
|
9
10
|
"python",
|
|
10
11
|
"scripts",
|
|
12
|
+
"AGENTS.md",
|
|
11
13
|
"LICENSE"
|
|
12
14
|
],
|
|
13
15
|
"scripts": {
|
|
@@ -16,6 +18,7 @@
|
|
|
16
18
|
"demo": "tsx examples/demoSites.ts",
|
|
17
19
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
18
20
|
"test": "tsc -p tsconfig.test.json && node --test .test-build/*.test.js",
|
|
21
|
+
"coverage": "tsc -p tsconfig.test.json && c8 node --test .test-build/*.test.js",
|
|
19
22
|
"prepublishOnly": "npm run build",
|
|
20
23
|
"postinstall": "node scripts/setup-python.js"
|
|
21
24
|
},
|
|
@@ -49,8 +52,12 @@
|
|
|
49
52
|
"url": "https://github.com/JWriter20/CaptchaKraken/issues"
|
|
50
53
|
},
|
|
51
54
|
"homepage": "https://github.com/JWriter20/CaptchaKraken#readme",
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"cursory-js": "2.0.1"
|
|
57
|
+
},
|
|
52
58
|
"devDependencies": {
|
|
53
|
-
"@types/node": "
|
|
54
|
-
"
|
|
59
|
+
"@types/node": "24.10.2",
|
|
60
|
+
"c8": "12.0.0",
|
|
61
|
+
"typescript": "5.9.3"
|
|
55
62
|
}
|
|
56
63
|
}
|
package/python/AGENTS.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# captchakraken (PyPI)
|
|
2
|
+
|
|
3
|
+
The Python **engine and CLI**: OpenCV grid detection plus a planner that asks a
|
|
4
|
+
fine-tuned vision model what to do, and a page driver that performs it in a
|
|
5
|
+
Playwright browser.
|
|
6
|
+
|
|
7
|
+
It is not a browser and, by default, not the model either. It installs no
|
|
8
|
+
browser — bring your own — and every inference goes to a model endpoint you
|
|
9
|
+
point it at. The `serve` extra is what turns this box into that endpoint.
|
|
10
|
+
|
|
11
|
+
Repo, guides and the full agent guide:
|
|
12
|
+
<https://github.com/JWriter20/CaptchaKraken>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Point it at a model, first
|
|
17
|
+
|
|
18
|
+
| You have | Do this |
|
|
19
|
+
|---|---|
|
|
20
|
+
| No GPU | Use the hosted API — add the account MCP server, `npx -y captchakraken-mcp`, call `sign_in` then `create_api_key` |
|
|
21
|
+
| A GPU, and want it local | `pip install "captchakraken[serve]"`, then `captchakraken server start` |
|
|
22
|
+
| Your own vLLM server | `export VLLM_BASE_URL=…/v1` and `export CAPTCHA_KRAKEN_API_KEY=…` |
|
|
23
|
+
|
|
24
|
+
`create_api_key` writes the key and endpoint to `~/.captchakraken/credentials`
|
|
25
|
+
(mode 0600) and the client reads that file by itself, so the MCP path needs **no
|
|
26
|
+
environment variables at all**. The key is never printed into the conversation.
|
|
27
|
+
|
|
28
|
+
A local endpoint auto-starts on the first solve; `CAPTCHA_KRAKEN_AUTOSTART=0`
|
|
29
|
+
turns that off. A non-localhost `VLLM_BASE_URL` is never started or managed for
|
|
30
|
+
you.
|
|
31
|
+
|
|
32
|
+
## 2. Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install captchakraken # the client: grid detection + planner + CLI
|
|
36
|
+
pip install "captchakraken[serve]" # ...and the vLLM serving stack, to host it
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Python 3.11+.
|
|
40
|
+
|
|
41
|
+
## 3. Solve
|
|
42
|
+
|
|
43
|
+
Synchronous Playwright:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from playwright.sync_api import sync_playwright
|
|
47
|
+
from captchakraken import PageSolver
|
|
48
|
+
|
|
49
|
+
with sync_playwright() as p:
|
|
50
|
+
page = p.chromium.launch().new_page()
|
|
51
|
+
page.goto("https://www.google.com/recaptcha/api2/demo")
|
|
52
|
+
result = PageSolver().solve(page)
|
|
53
|
+
print(result.is_solved)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`PageSolver().watch(page)` installs a background watcher instead, for when you
|
|
57
|
+
do not know if or when a captcha will appear; stop it with `.stop()`.
|
|
58
|
+
|
|
59
|
+
No browser, just an image:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
captchakraken path/to/captcha.png # prints a JSON click plan
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Other CLI modes, all printing JSON: `captchakraken server start|stop|status|run`
|
|
66
|
+
manages a local vLLM server, `captchakraken fetch` updates weights and the
|
|
67
|
+
serving stack together, and the OpenCV tool calls (`find-grid`, `find-checkbox`,
|
|
68
|
+
`get-numbered-grid`, `detect-selected`, …) are exposed one per subcommand. The
|
|
69
|
+
module docstring of `captchakraken/cli.py` lists every mode with its arguments.
|
|
70
|
+
|
|
71
|
+
## 4. Errors
|
|
72
|
+
|
|
73
|
+
`PageSolver.solve()` raises from `captchakraken.page_solver`:
|
|
74
|
+
`NoCaptchaFoundError` (no interactive widget — usually not a failure),
|
|
75
|
+
`UnsupportedChallengeError`, `AnimatedChallengeError` (the challenge could not
|
|
76
|
+
be *recorded*), `PageClosedError`, and `CaptchaSolveError`, which is the base
|
|
77
|
+
class of the other four — catch it last or it swallows them. The TypeScript
|
|
78
|
+
port raises none of these; it exports one error class and throws plain `Error`
|
|
79
|
+
otherwise.
|
|
80
|
+
|
|
81
|
+
Refusals from the hosted API raise `CaptchaKrakenAPIError` (exported from the
|
|
82
|
+
package root). **Branch on `.code`, never on the message text** — the codes are
|
|
83
|
+
the contract, the wording is not: `insufficient_credits`, `missing_api_key`,
|
|
84
|
+
`invalid_api_key`, `rate_limited`, `request_too_large`, `account_suspended`,
|
|
85
|
+
`upstream_unavailable`. Retrying a `403` that means "no licence" will never
|
|
86
|
+
succeed.
|
|
87
|
+
|
|
88
|
+
## 5. If you drive the model yourself
|
|
89
|
+
|
|
90
|
+
Grid screenshots must be sent with **cell numbers drawn on them** — the client
|
|
91
|
+
runs `find_grid`, renders the numbered overlay, and sends that image, because
|
|
92
|
+
the model reads those labels and was never trained to invent a numbering.
|
|
93
|
+
Without the overlay reCAPTCHA 4×4 scores zero without erroring. Image area is
|
|
94
|
+
also clamped into the `pixel_budget` the model declares in
|
|
95
|
+
`captchakraken/models.json`. Use the shipped client unless you have a reason not
|
|
96
|
+
to, and never hardcode a model name — read it from the config, or the prompt
|
|
97
|
+
generation and the weights drift apart.
|
|
98
|
+
|
|
99
|
+
## Rules for agents
|
|
100
|
+
|
|
101
|
+
- **Never invent a number.** Every accuracy, latency and price figure we publish
|
|
102
|
+
is on one page —
|
|
103
|
+
[docs/benchmarks.md](https://github.com/JWriter20/CaptchaKraken/blob/main/docs/benchmarks.md).
|
|
104
|
+
Quote it as written; do not derive, round, or convert anything, and do not
|
|
105
|
+
read a static-image rate as a promise about a browser run.
|
|
106
|
+
- **Never print an API key** into a transcript, a log, or a commit. Use
|
|
107
|
+
`create_api_key`, which writes it to disk instead. `captchakraken.env` and
|
|
108
|
+
`.env` hold keys and are never committed.
|
|
109
|
+
- **Abyss is what our hosted API serves, and it is never downloadable.** The
|
|
110
|
+
current client names an Abyss expert on every request to our endpoint and is
|
|
111
|
+
answered by Abyss; an older client, or a request that names no model, gets
|
|
112
|
+
Twilight v1.2, because its prompts belong to that generation. Nothing about it
|
|
113
|
+
is gated on the account. Do not suggest weights, a download or a workaround
|
|
114
|
+
for it, and do not quote accuracy figures for it — none are published. Verify
|
|
115
|
+
what is serving with `GET /v1/models`.
|
|
116
|
+
- **Respect the licence.** Building automation with this is fine; selling the
|
|
117
|
+
solve, or shipping it inside a browser or automation product, is not. See
|
|
118
|
+
[LICENSE](./LICENSE) and
|
|
119
|
+
[docs/licensing.md](https://github.com/JWriter20/CaptchaKraken/blob/main/docs/licensing.md).
|
|
120
|
+
- **Changing this package's own code?** Its rules are in
|
|
121
|
+
[CONTRIBUTING.md](https://github.com/JWriter20/CaptchaKraken/blob/main/CONTRIBUTING.md),
|
|
122
|
+
not here.
|
package/python/NOTICE
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
CaptchaKraken
|
|
2
|
+
Copyright (c) 2026 CaptchaKraken LLC. All rights reserved.
|
|
3
|
+
|
|
4
|
+
This product includes third-party software. The notices below are required by
|
|
5
|
+
those components' licences and do not change the terms of the CaptchaKraken
|
|
6
|
+
Source-Available License, which governs everything in this repository that is
|
|
7
|
+
not listed here.
|
|
8
|
+
|
|
9
|
+
────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
Cursory — mouse trajectory generation
|
|
11
|
+
────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
cursory https://github.com/Vinyzu/cursory (Python, PyPI: cursory)
|
|
14
|
+
cursory-js https://github.com/JWriter20/cursory-js (TypeScript, npm: cursory-js)
|
|
15
|
+
|
|
16
|
+
Copyright (c) Vinyzu and the cursory-js contributors.
|
|
17
|
+
Licensed under the GNU Lesser General Public License, version 3 or later
|
|
18
|
+
(LGPL-3.0-or-later). A copy is available at https://www.gnu.org/licenses/lgpl-3.0.html
|
|
19
|
+
|
|
20
|
+
HOW WE USE IT, AND WHY THAT MATTERS. Both are ORDINARY INSTALLED
|
|
21
|
+
DEPENDENCIES — `cursory` is declared in python/pyproject.toml and `cursory-js`
|
|
22
|
+
in js/package.json, and each is resolved by the user's own package manager at
|
|
23
|
+
install time. Neither is vendored into this repository, copied into our
|
|
24
|
+
sources, or bundled into a build artifact: the TypeScript build is `tsc`, which
|
|
25
|
+
leaves `require('cursory-js')` as a runtime import rather than inlining it, and
|
|
26
|
+
`js/scripts/copy-python.mjs` copies only our own `python/` tree.
|
|
27
|
+
|
|
28
|
+
That arrangement is the one the LGPL is written for, and it is what lets this
|
|
29
|
+
product remain under its own licence. It also means a user may replace either
|
|
30
|
+
library with a modified version of their own — reinstall the package, and the
|
|
31
|
+
driver picks it up — which is the freedom section 4 of the LGPL requires be
|
|
32
|
+
preserved. Do not vendor, inline, or bundle either one without taking legal
|
|
33
|
+
advice first; doing so is the change that would put this obligation somewhere
|
|
34
|
+
other than where it now sits.
|
|
35
|
+
|
|
36
|
+
We have not modified either library.
|
package/python/README.md
CHANGED
|
@@ -111,8 +111,7 @@ actions = solver.solve("captcha.png")
|
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
Pure-OpenCV tool subcommands (no model): `find-grid`, `find-checkbox`,
|
|
114
|
-
`detect-selected`, `grid-cell-states`,
|
|
115
|
-
persistent `serve` worker the browser driver polls.
|
|
114
|
+
`detect-selected`, `grid-cell-states`, and a persistent `serve` worker the browser driver polls.
|
|
116
115
|
|
|
117
116
|
## Configuration (model-agnostic)
|
|
118
117
|
|
|
@@ -44,6 +44,16 @@ If `CAMOUFOX_BINARY` is unset, camoufox falls back to its default binary
|
|
|
44
44
|
source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Running the model locally on **Ollama** or **LM Studio** instead? Those load
|
|
48
|
+
GGUF, so they need the GGUF build rather than the AWQ/FP8 downloads — see
|
|
49
|
+
[Self-hosting → GGUF](../../docs/self-hosting.md#gguf-for-ollama-lm-studio-and-llamacpp),
|
|
50
|
+
then check the wiring with one image and no browser:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python examples/with_local_gguf.py path/to/captcha.png
|
|
54
|
+
python examples/with_local_gguf.py path/to/text-captcha.png --text
|
|
55
|
+
```
|
|
56
|
+
|
|
47
57
|
## Run
|
|
48
58
|
|
|
49
59
|
```bash
|
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Shared runner for the CaptchaKraken Python demos.
|
|
3
|
-
|
|
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.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
1
|
import argparse
|
|
26
2
|
import json
|
|
27
3
|
import os
|
|
@@ -38,17 +14,10 @@ from captchakraken.page_solver import CaptchaSolveError, PageSolver
|
|
|
38
14
|
class DemoSpec:
|
|
39
15
|
name: str
|
|
40
16
|
url: str
|
|
41
|
-
vendor: str
|
|
17
|
+
vendor: str
|
|
42
18
|
|
|
43
19
|
|
|
44
20
|
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
21
|
ap = argparse.ArgumentParser(
|
|
53
22
|
description=f"CaptchaKraken demo — {default.name}",
|
|
54
23
|
epilog="With no URL, runs against the built-in demo page.")
|
|
@@ -69,21 +38,8 @@ def spec_from_argv(default: DemoSpec, argv=None) -> DemoSpec:
|
|
|
69
38
|
|
|
70
39
|
|
|
71
40
|
def _launch_kwargs() -> dict:
|
|
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
41
|
kw = dict(headless=os.getenv("HEADLESS", "1") != "0",
|
|
84
42
|
humanize=os.getenv("HUMANIZE", "0") != "0", geoip=False)
|
|
85
|
-
# Point camoufox at YOUR fork's binary. If unset, camoufox uses its default
|
|
86
|
-
# cached binary (`python -m camoufox fetch`).
|
|
87
43
|
binary = os.getenv("CAMOUFOX_BINARY") or os.getenv("CAMOUFOX_EXECUTABLE_PATH")
|
|
88
44
|
if binary:
|
|
89
45
|
kw["executable_path"] = binary
|
|
@@ -91,13 +47,6 @@ def _launch_kwargs() -> dict:
|
|
|
91
47
|
|
|
92
48
|
|
|
93
49
|
def _page_kwargs() -> dict:
|
|
94
|
-
"""Record the session when CAPTCHA_DEMO_VIDEO_DIR is set.
|
|
95
|
-
|
|
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
50
|
out = os.getenv("CAPTCHA_DEMO_VIDEO_DIR")
|
|
102
51
|
if not out:
|
|
103
52
|
return {}
|
|
@@ -107,27 +56,17 @@ def _page_kwargs() -> dict:
|
|
|
107
56
|
|
|
108
57
|
|
|
109
58
|
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
59
|
video = getattr(page, "video", None)
|
|
116
60
|
if video is None:
|
|
117
61
|
return None
|
|
118
62
|
try:
|
|
119
63
|
page.context.close()
|
|
120
64
|
return video.path()
|
|
121
|
-
except Exception:
|
|
65
|
+
except Exception:
|
|
122
66
|
return None
|
|
123
67
|
|
|
124
68
|
|
|
125
69
|
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.
|
|
130
|
-
"""
|
|
131
70
|
inp = out = 0
|
|
132
71
|
for u in usage or []:
|
|
133
72
|
inp += int(u.get("prompt_tokens", u.get("inputTokens", 0)) or 0)
|
|
@@ -175,11 +114,9 @@ def run_demo(spec: DemoSpec) -> None:
|
|
|
175
114
|
page = browser.new_page(**_page_kwargs())
|
|
176
115
|
try:
|
|
177
116
|
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
117
|
try:
|
|
181
118
|
page.wait_for_load_state("networkidle", timeout=15_000)
|
|
182
|
-
except Exception:
|
|
119
|
+
except Exception:
|
|
183
120
|
pass
|
|
184
121
|
page.wait_for_timeout(3000)
|
|
185
122
|
|
|
@@ -196,10 +133,8 @@ def run_demo(spec: DemoSpec) -> None:
|
|
|
196
133
|
err = e
|
|
197
134
|
reason = _explain(spec.vendor, False, e)
|
|
198
135
|
finally:
|
|
199
|
-
# In the finally so a failed run still yields its recording —
|
|
200
|
-
# that is the run you most want to watch back.
|
|
201
136
|
video_path = _finish_video(page)
|
|
202
|
-
except Exception as e:
|
|
137
|
+
except Exception as e:
|
|
203
138
|
err = e
|
|
204
139
|
reason = _explain(spec.vendor, False, e)
|
|
205
140
|
|
|
@@ -228,10 +163,6 @@ def _report(spec, *, ok, total_s, solve_s, inp, out, tps, reason, video=None):
|
|
|
228
163
|
print(f" reason : {reason}")
|
|
229
164
|
print(f"{line}\n")
|
|
230
165
|
|
|
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
166
|
record = os.getenv("CAPTCHA_DEMO_RECORD")
|
|
236
167
|
if record:
|
|
237
168
|
with open(record, "a", encoding="utf-8") as fh:
|
|
@@ -1,16 +1,3 @@
|
|
|
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 # this demo page
|
|
8
|
-
python examples/demoHcaptcha.py https://your.site/ # any URL
|
|
9
|
-
|
|
10
|
-
Note: hCaptcha randomly serves non-grid puzzles (drag / video / choose-the-card),
|
|
11
|
-
which the engine does not handle yet — the report says so; re-run for a grid.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
1
|
from _harness import DemoSpec, run_demo
|
|
15
2
|
|
|
16
3
|
if __name__ == "__main__":
|
|
@@ -1,13 +1,3 @@
|
|
|
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 # this demo page
|
|
8
|
-
python examples/demoRecaptcha.py https://your.site/ # any URL
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
1
|
from _harness import DemoSpec, run_demo
|
|
12
2
|
|
|
13
3
|
if __name__ == "__main__":
|
|
@@ -1,26 +1,3 @@
|
|
|
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
1
|
import sys
|
|
25
2
|
|
|
26
3
|
from playwright.sync_api import sync_playwright
|
|
@@ -36,16 +13,14 @@ def main() -> int:
|
|
|
36
13
|
browser = p.chromium.launch(headless=True)
|
|
37
14
|
page = browser.new_page()
|
|
38
15
|
|
|
39
|
-
# ── install once, then let it work ───────────────────────────────────
|
|
40
16
|
watcher = PageSolver().watch(
|
|
41
17
|
page,
|
|
42
18
|
on_solved=lambda r: print("✅ solved one:", r.is_solved),
|
|
43
19
|
on_error=lambda e: print("solve failed:", e),
|
|
44
20
|
)
|
|
45
|
-
# ─────────────────────────────────────────────────────────────────────
|
|
46
21
|
|
|
47
22
|
page.goto(URL)
|
|
48
|
-
watcher.run(timeout_ms=SECONDS * 1000)
|
|
23
|
+
watcher.run(timeout_ms=SECONDS * 1000)
|
|
49
24
|
|
|
50
25
|
print(f"stopped after {watcher.solves} solve(s)")
|
|
51
26
|
browser.close()
|