captchakraken 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -4
- package/LICENSE +1 -1
- package/README.md +10 -13
- package/dist/index.d.ts +1 -0
- package/dist/kinds.d.ts +8 -0
- package/dist/kinds.js +3 -1
- package/dist/model-name.d.ts +6 -0
- package/dist/model-name.js +28 -0
- package/dist/playwright-types.d.ts +18 -0
- package/dist/png.d.ts +16 -0
- package/dist/png.js +119 -0
- package/dist/puppeteer-adapter.d.ts +7 -1
- package/dist/puppeteer-adapter.js +6 -0
- package/dist/selectors.d.ts +6 -0
- package/dist/selectors.js +4 -0
- package/dist/solver.d.ts +76 -4
- package/dist/solver.js +486 -165
- package/dist/types.d.ts +10 -5
- package/dist/verdicts.d.ts +45 -0
- package/dist/verdicts.js +125 -0
- package/package.json +7 -7
- package/python/AGENTS.md +13 -8
- package/python/LICENSE +1 -1
- package/python/README.md +10 -13
- package/python/pyproject.toml +6 -6
- package/python/src/captchakraken/__init__.py +5 -1
- package/python/src/captchakraken/cli.py +7 -1
- package/python/src/captchakraken/kinds.py +9 -0
- package/python/src/captchakraken/page_solver.py +439 -152
- package/python/src/captchakraken/planner.py +52 -11
- package/python/src/captchakraken/prompts.py +24 -0
- package/python/src/captchakraken/selectors.py +10 -0
- package/python/src/captchakraken/tool_calls/find_checkbox.py +4 -0
- package/python/src/captchakraken/tool_calls/find_grid.py +22 -0
- package/python/src/captchakraken/verdicts.py +154 -0
- package/python/Dockerfile +0 -41
- package/python/examples/README.md +0 -114
- package/python/examples/_harness.py +0 -177
- package/python/examples/demoHcaptcha.py +0 -8
- package/python/examples/demoRecaptcha.py +0 -8
- package/python/examples/watch_playwright.py +0 -31
- package/python/examples/with_local_gguf.py +0 -28
- package/python/examples/with_playwright.py +0 -24
- package/scripts/copy-python.mjs +0 -29
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RoundVerdict } from './verdicts';
|
|
1
2
|
import type { Humanizer, TouchTransform } from './humanize.js';
|
|
2
3
|
import type { ActionKind, FrameRole, HumanizationMode, PromptFamily, SolveStage, Vendor } from './kinds';
|
|
3
4
|
/** One intermediate stage of a solve, handed to `CaptchaKrakenConfig.onStep`. */
|
|
@@ -43,9 +44,11 @@ export interface CaptchaKrakenConfig {
|
|
|
43
44
|
};
|
|
44
45
|
/** Solve rounds that fit the timeout. Default 6. */
|
|
45
46
|
maxSolveLoops?: number;
|
|
46
|
-
/**
|
|
47
|
+
/** How long to keep looking for a widget the page has not drawn yet, before "no captcha" is believed. Not charged to `overallSolveTimeoutMs`. Default 15000. */
|
|
48
|
+
detectionTimeoutMs?: number;
|
|
49
|
+
/** Repeats of one answer on one board before the solve ends; the first repeats are re-asked at a fresh sample. Default 2. */
|
|
47
50
|
maxNoProgressRounds?: number;
|
|
48
|
-
/**
|
|
51
|
+
/** @deprecated Ignored since 3.2.0: a round that performed nothing goes straight on to the next. */
|
|
49
52
|
postSolveDelayMs?: number;
|
|
50
53
|
/** Whole-solve time limit. Default 45000. */
|
|
51
54
|
overallSolveTimeoutMs?: number;
|
|
@@ -119,11 +122,11 @@ export interface CaptchaKrakenConfig {
|
|
|
119
122
|
elementScreenshotTimeoutMs?: number;
|
|
120
123
|
/** Timeout for an `onStep` screenshot. Default 2000. */
|
|
121
124
|
stepScreenshotTimeoutMs?: number;
|
|
122
|
-
/**
|
|
125
|
+
/** @deprecated Ignored since 3.2.0: a stale handle now counts against `maxSolveLoops`. */
|
|
123
126
|
maxStaleElementRetries?: number;
|
|
124
|
-
/**
|
|
127
|
+
/** The pause before a failed round is tried again. Default 900. */
|
|
125
128
|
staleElementBackoffMs?: number;
|
|
126
|
-
/**
|
|
129
|
+
/** @deprecated Ignored since 3.2.0: an unusable answer now counts against `maxSolveLoops`. */
|
|
127
130
|
maxUnsupportedReSolves?: number;
|
|
128
131
|
}
|
|
129
132
|
export interface BoundingBox {
|
|
@@ -190,4 +193,6 @@ export interface SolveResult {
|
|
|
190
193
|
};
|
|
191
194
|
/** Milliseconds per phase, plus `total`. */
|
|
192
195
|
phases?: Record<string, number>;
|
|
196
|
+
/** What the vendor's own server answered, in order, for the vendors whose answer is readable. */
|
|
197
|
+
verdicts: RoundVerdict[];
|
|
193
198
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The vendor's own answer to each submitted round, read off the wire. Mirrors python/src/captchakraken/verdicts.py.
|
|
3
|
+
*
|
|
4
|
+
* Every shape below was recorded against the vendor's public demo page, never inferred: a DOM done-signal can only
|
|
5
|
+
* say "something changed", while the answer-check response says whether the round was taken. A vendor with no
|
|
6
|
+
* readable answer (Turnstile, and the rest of the table) is judged by the DOM signals in the driver instead.
|
|
7
|
+
*/
|
|
8
|
+
import { Verdict, Vendor } from './kinds';
|
|
9
|
+
import type { PlaywrightPage } from './playwright-types';
|
|
10
|
+
export interface RoundVerdict {
|
|
11
|
+
vendor: Vendor;
|
|
12
|
+
verdict: Verdict;
|
|
13
|
+
}
|
|
14
|
+
export interface VerdictEndpoint {
|
|
15
|
+
/** A URL substring naming the vendor's own endpoint. */
|
|
16
|
+
marker: string;
|
|
17
|
+
read: (body: string) => Verdict | null;
|
|
18
|
+
}
|
|
19
|
+
/** Only a vendor listed here has a readable answer; the rest are judged by the DOM done-signals. */
|
|
20
|
+
export declare const VERDICT_ENDPOINTS: Partial<Record<Vendor, readonly VerdictEndpoint[]>>;
|
|
21
|
+
/** The vendor and endpoint whose marker the URL carries, so a listener reads only the bodies it can judge. */
|
|
22
|
+
export declare function endpointFor(url: string): {
|
|
23
|
+
vendor: Vendor;
|
|
24
|
+
endpoint: VerdictEndpoint;
|
|
25
|
+
} | null;
|
|
26
|
+
export declare function readVerdict(url: string, status: number, body: string): RoundVerdict | null;
|
|
27
|
+
/**
|
|
28
|
+
* Every verdict the page's network carries while a solve runs.
|
|
29
|
+
*
|
|
30
|
+
* A page-level listener sees the vendors' cross-origin frames too. It is optional: a page object with no `on`
|
|
31
|
+
* records nothing, and the driver then judges every round by the DOM, as it always has.
|
|
32
|
+
*/
|
|
33
|
+
export declare class VerdictLog {
|
|
34
|
+
private readonly page;
|
|
35
|
+
readonly verdicts: RoundVerdict[];
|
|
36
|
+
private read;
|
|
37
|
+
private pending;
|
|
38
|
+
private readonly listener;
|
|
39
|
+
constructor(page: PlaywrightPage);
|
|
40
|
+
/** The verdicts that arrived since the last call, once every body already on the wire has been read. */
|
|
41
|
+
fresh(): Promise<RoundVerdict[]>;
|
|
42
|
+
/** The last accept or reject, which is what the round came to. */
|
|
43
|
+
decisive(): Verdict | null;
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}
|
package/dist/verdicts.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VerdictLog = exports.VERDICT_ENDPOINTS = void 0;
|
|
4
|
+
exports.endpointFor = endpointFor;
|
|
5
|
+
exports.readVerdict = readVerdict;
|
|
6
|
+
/**
|
|
7
|
+
* The vendor's own answer to each submitted round, read off the wire. Mirrors python/src/captchakraken/verdicts.py.
|
|
8
|
+
*
|
|
9
|
+
* Every shape below was recorded against the vendor's public demo page, never inferred: a DOM done-signal can only
|
|
10
|
+
* say "something changed", while the answer-check response says whether the round was taken. A vendor with no
|
|
11
|
+
* readable answer (Turnstile, and the rest of the table) is judged by the DOM signals in the driver instead.
|
|
12
|
+
*/
|
|
13
|
+
const kinds_1 = require("./kinds");
|
|
14
|
+
// The vendors' own HTTP refusal: hCaptcha's frame words a 429 as "Your computer or network has sent too many requests".
|
|
15
|
+
const TOO_MANY_REQUESTS = 429;
|
|
16
|
+
const parse = (body) => {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(body);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/** A JSON object whose boolean `pass` is the verdict. */
|
|
25
|
+
const passField = (body) => {
|
|
26
|
+
const passed = parse(body)?.pass;
|
|
27
|
+
return typeof passed !== 'boolean' ? null : passed ? kinds_1.Verdict.ACCEPTED : kinds_1.Verdict.REJECTED;
|
|
28
|
+
};
|
|
29
|
+
/** A JSON array behind the `)]}'` anti-hijacking line. */
|
|
30
|
+
const xssiArray = (body) => {
|
|
31
|
+
const data = parse(body.startsWith(")]}'") && body.includes('\n') ? body.slice(body.indexOf('\n') + 1) : body);
|
|
32
|
+
return Array.isArray(data) && data.length ? data : null;
|
|
33
|
+
};
|
|
34
|
+
/** `["uvresp", token, 1, lifetime, ...]` when taken; `["uvresp", context, 0, ..., ["rresp", ...next board]]` when refused. */
|
|
35
|
+
const userVerify = (body) => {
|
|
36
|
+
const data = xssiArray(body);
|
|
37
|
+
if (!data || data[0] !== 'uvresp' || data.length < 3)
|
|
38
|
+
return null;
|
|
39
|
+
return data[2] === 1 ? kinds_1.Verdict.ACCEPTED : data[2] === 0 ? kinds_1.Verdict.REJECTED : null;
|
|
40
|
+
};
|
|
41
|
+
const newBoard = (body) => (xssiArray(body)?.[0] === 'rresp' ? kinds_1.Verdict.NEW_CHALLENGE : null);
|
|
42
|
+
/** `callback({...})` whose `data.result` is the verdict. */
|
|
43
|
+
const jsonpResult = (body) => {
|
|
44
|
+
const start = body.indexOf('(');
|
|
45
|
+
const end = body.lastIndexOf(')');
|
|
46
|
+
const data = (start >= 0 && start < end ? parse(body.slice(start + 1, end)) : null);
|
|
47
|
+
const result = data?.status === 'success' ? data.data?.result : undefined;
|
|
48
|
+
return result === 'success' ? kinds_1.Verdict.ACCEPTED : result === 'fail' ? kinds_1.Verdict.REJECTED : null;
|
|
49
|
+
};
|
|
50
|
+
const urlOnly = () => kinds_1.Verdict.NEW_CHALLENGE;
|
|
51
|
+
/** Only a vendor listed here has a readable answer; the rest are judged by the DOM done-signals. */
|
|
52
|
+
exports.VERDICT_ENDPOINTS = {
|
|
53
|
+
// The deal endpoint's body is encrypted, so its URL is the whole signal.
|
|
54
|
+
[kinds_1.Vendor.HCAPTCHA]: [
|
|
55
|
+
{ marker: 'hcaptcha.com/checkcaptcha/', read: passField },
|
|
56
|
+
{ marker: 'hcaptcha.com/getcaptcha/', read: urlOnly },
|
|
57
|
+
],
|
|
58
|
+
[kinds_1.Vendor.RECAPTCHA]: [
|
|
59
|
+
{ marker: '/recaptcha/api2/userverify', read: userVerify },
|
|
60
|
+
{ marker: '/recaptcha/api2/reload', read: newBoard },
|
|
61
|
+
],
|
|
62
|
+
[kinds_1.Vendor.GEETEST]: [
|
|
63
|
+
{ marker: 'geetest.com/verify', read: jsonpResult },
|
|
64
|
+
{ marker: 'geetest.com/load', read: urlOnly },
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
/** The vendor and endpoint whose marker the URL carries, so a listener reads only the bodies it can judge. */
|
|
68
|
+
function endpointFor(url) {
|
|
69
|
+
return Object.entries(exports.VERDICT_ENDPOINTS)
|
|
70
|
+
.flatMap(([vendor, endpoints]) => endpoints.map((endpoint) => ({ vendor, endpoint })))
|
|
71
|
+
.find(({ endpoint }) => url.includes(endpoint.marker)) ?? null;
|
|
72
|
+
}
|
|
73
|
+
function readVerdict(url, status, body) {
|
|
74
|
+
const found = endpointFor(url);
|
|
75
|
+
if (!found)
|
|
76
|
+
return null;
|
|
77
|
+
if (status === TOO_MANY_REQUESTS)
|
|
78
|
+
return { vendor: found.vendor, verdict: kinds_1.Verdict.BLOCKED };
|
|
79
|
+
const verdict = status >= 200 && status < 300 ? found.endpoint.read(body) : null;
|
|
80
|
+
return verdict ? { vendor: found.vendor, verdict } : null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Every verdict the page's network carries while a solve runs.
|
|
84
|
+
*
|
|
85
|
+
* A page-level listener sees the vendors' cross-origin frames too. It is optional: a page object with no `on`
|
|
86
|
+
* records nothing, and the driver then judges every round by the DOM, as it always has.
|
|
87
|
+
*/
|
|
88
|
+
class VerdictLog {
|
|
89
|
+
constructor(page) {
|
|
90
|
+
this.page = page;
|
|
91
|
+
this.verdicts = [];
|
|
92
|
+
this.read = 0;
|
|
93
|
+
this.pending = [];
|
|
94
|
+
this.listener = (response) => {
|
|
95
|
+
const url = response.url();
|
|
96
|
+
const status = response.status();
|
|
97
|
+
if (!endpointFor(url))
|
|
98
|
+
return;
|
|
99
|
+
const body = status === TOO_MANY_REQUESTS ? Promise.resolve('') : response.text();
|
|
100
|
+
this.pending.push(body.then((text) => {
|
|
101
|
+
const verdict = readVerdict(url, status, text);
|
|
102
|
+
if (verdict)
|
|
103
|
+
this.verdicts.push(verdict);
|
|
104
|
+
}).catch(() => { }));
|
|
105
|
+
};
|
|
106
|
+
page.on?.('response', this.listener);
|
|
107
|
+
}
|
|
108
|
+
/** The verdicts that arrived since the last call, once every body already on the wire has been read. */
|
|
109
|
+
async fresh() {
|
|
110
|
+
await Promise.all(this.pending.splice(0));
|
|
111
|
+
const fresh = this.verdicts.slice(this.read);
|
|
112
|
+
this.read = this.verdicts.length;
|
|
113
|
+
return fresh;
|
|
114
|
+
}
|
|
115
|
+
/** The last accept or reject, which is what the round came to. */
|
|
116
|
+
decisive() {
|
|
117
|
+
return [...this.verdicts].reverse().map((v) => v.verdict)
|
|
118
|
+
.find((v) => v === kinds_1.Verdict.ACCEPTED || v === kinds_1.Verdict.REJECTED) ?? null;
|
|
119
|
+
}
|
|
120
|
+
async close() {
|
|
121
|
+
this.page.off?.('response', this.listener);
|
|
122
|
+
await Promise.all(this.pending.splice(0));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.VerdictLog = VerdictLog;
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "captchakraken",
|
|
3
|
-
"version": "3.
|
|
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,
|
|
3
|
+
"version": "3.2.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, 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
9
|
"NOTICE",
|
|
10
10
|
"python",
|
|
11
|
-
"
|
|
11
|
+
"!python/**/__pycache__",
|
|
12
|
+
"scripts/setup-python.js",
|
|
12
13
|
"AGENTS.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|
|
@@ -24,14 +25,13 @@
|
|
|
24
25
|
},
|
|
25
26
|
"repository": {
|
|
26
27
|
"type": "git",
|
|
27
|
-
"url": "git+https://github.com/
|
|
28
|
+
"url": "git+https://github.com/CaptchaKraken/CaptchaKraken.git",
|
|
28
29
|
"directory": "js"
|
|
29
30
|
},
|
|
30
31
|
"keywords": [
|
|
31
32
|
"captcha",
|
|
32
33
|
"captcha-solver",
|
|
33
34
|
"recaptcha",
|
|
34
|
-
"hcaptcha",
|
|
35
35
|
"geetest",
|
|
36
36
|
"turnstile",
|
|
37
37
|
"yidun",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"license": "SEE LICENSE IN LICENSE",
|
|
50
50
|
"type": "commonjs",
|
|
51
51
|
"bugs": {
|
|
52
|
-
"url": "https://github.com/
|
|
52
|
+
"url": "https://github.com/CaptchaKraken/CaptchaKraken/issues"
|
|
53
53
|
},
|
|
54
|
-
"homepage": "https://github.com/
|
|
54
|
+
"homepage": "https://github.com/CaptchaKraken/CaptchaKraken#readme",
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"cursory-js": "2.0.1"
|
|
57
57
|
},
|
package/python/AGENTS.md
CHANGED
|
@@ -9,7 +9,7 @@ browser — bring your own — and every inference goes to a model endpoint you
|
|
|
9
9
|
point it at. The `serve` extra is what turns this box into that endpoint.
|
|
10
10
|
|
|
11
11
|
Repo, guides and the full agent guide:
|
|
12
|
-
<https://github.com/
|
|
12
|
+
<https://github.com/CaptchaKraken/CaptchaKraken>
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -71,10 +71,15 @@ module docstring of `captchakraken/cli.py` lists every mode with its arguments.
|
|
|
71
71
|
## 4. Errors
|
|
72
72
|
|
|
73
73
|
`PageSolver.solve()` raises from `captchakraken.page_solver`:
|
|
74
|
-
`NoCaptchaFoundError` (no interactive widget
|
|
75
|
-
`
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
`NoCaptchaFoundError` (no interactive widget appeared within
|
|
75
|
+
`detection_timeout_ms` — usually not a failure), `VendorBlockedError` (the
|
|
76
|
+
vendor refused to serve this client at all), `UnsupportedChallengeError`,
|
|
77
|
+
`AnimatedChallengeError` (the challenge could not be *recorded*),
|
|
78
|
+
`PageClosedError`, and `CaptchaSolveError`, which is the base class of the
|
|
79
|
+
other five — catch it last or it swallows them. Apart from a vendor refusal, a
|
|
80
|
+
closed page and the time budget, a solve only gives up once every one of
|
|
81
|
+
`max_solve_loops` has been spent. `result.verdicts` holds the vendor's own
|
|
82
|
+
answer to each round where its answer-check response is readable. The TypeScript
|
|
78
83
|
port raises none of these; it exports one error class and throws plain `Error`
|
|
79
84
|
otherwise.
|
|
80
85
|
|
|
@@ -100,7 +105,7 @@ generation and the weights drift apart.
|
|
|
100
105
|
|
|
101
106
|
- **Never invent a number.** Every accuracy, latency and price figure we publish
|
|
102
107
|
is on one page —
|
|
103
|
-
[docs/benchmarks.md](https://github.com/
|
|
108
|
+
[docs/benchmarks.md](https://github.com/CaptchaKraken/CaptchaKraken/blob/main/docs/benchmarks.md).
|
|
104
109
|
Quote it as written; do not derive, round, or convert anything, and do not
|
|
105
110
|
read a static-image rate as a promise about a browser run.
|
|
106
111
|
- **Never print an API key** into a transcript, a log, or a commit. Use
|
|
@@ -116,7 +121,7 @@ generation and the weights drift apart.
|
|
|
116
121
|
- **Respect the licence.** Building automation with this is fine; selling the
|
|
117
122
|
solve, or shipping it inside a browser or automation product, is not. See
|
|
118
123
|
[LICENSE](./LICENSE) and
|
|
119
|
-
[docs/licensing.md](https://github.com/
|
|
124
|
+
[docs/licensing.md](https://github.com/CaptchaKraken/CaptchaKraken/blob/main/docs/licensing.md).
|
|
120
125
|
- **Changing this package's own code?** Its rules are in
|
|
121
|
-
[CONTRIBUTING.md](https://github.com/
|
|
126
|
+
[CONTRIBUTING.md](https://github.com/CaptchaKraken/CaptchaKraken/blob/main/CONTRIBUTING.md),
|
|
122
127
|
not here.
|
package/python/LICENSE
CHANGED
|
@@ -141,5 +141,5 @@ This text is a license, not legal advice. If the boundary between a permitted
|
|
|
141
141
|
"value-add" product and a prohibited "thin wrapper", or between using the
|
|
142
142
|
Software with a browser and shipping it as part of one, is unclear for your use
|
|
143
143
|
case, ask by opening an issue or messaging the maintainer on GitHub:
|
|
144
|
-
https://github.com/
|
|
144
|
+
https://github.com/CaptchaKraken/CaptchaKraken
|
|
145
145
|
--------------------------------------------------------------------------------
|
package/python/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/
|
|
2
|
+
<img src="https://raw.githubusercontent.com/CaptchaKraken/CaptchaKraken/main/docs/assets/logo-card.png" alt="CaptchaKraken" width="128" height="128">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">captchakraken</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<b>A captcha solver for browser automation.</b><br>
|
|
9
|
-
The Python engine and CLI behind <a href="https://github.com/
|
|
9
|
+
The Python engine and CLI behind <a href="https://github.com/CaptchaKraken/CaptchaKraken">CaptchaKraken</a>.
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
OpenCV tile detection plus a fine-tuned **Qwen3.5-9B** vision model. Give it a
|
|
@@ -19,34 +19,31 @@ nothing at all.
|
|
|
19
19
|
|
|
20
20
|
> For demo videos, accuracy numbers, the browser driver, and the full
|
|
21
21
|
> self-hosting guide, see the main repo
|
|
22
|
-
> **[CaptchaKraken](https://github.com/
|
|
22
|
+
> **[CaptchaKraken](https://github.com/CaptchaKraken/CaptchaKraken)**.
|
|
23
23
|
|
|
24
24
|
## Watch it work
|
|
25
25
|
|
|
26
26
|
<p align="center">
|
|
27
|
-
<img src="https://raw.githubusercontent.com/
|
|
28
|
-
alt="A live hCaptcha image select challenge being solved end to end">
|
|
29
|
-
<img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/demo/recaptcha_4x4.webp" width="260"
|
|
27
|
+
<img src="https://raw.githubusercontent.com/CaptchaKraken/CaptchaKraken/main/docs/assets/demo/recaptcha_4x4.webp" width="260"
|
|
30
28
|
alt="A live reCAPTCHA 4×4 tile grid challenge being solved end to end">
|
|
31
|
-
<img src="https://raw.githubusercontent.com/
|
|
29
|
+
<img src="https://raw.githubusercontent.com/CaptchaKraken/CaptchaKraken/main/docs/assets/demo/geetest_slide.webp" width="260"
|
|
32
30
|
alt="A live GeeTest slide jigsaw challenge being solved end to end">
|
|
33
31
|
</p>
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
reCAPTCHA 4×4 tile grid 9/10 in 8.7s · GeeTest slide jigsaw 10/10 in 7.6s — median of the solved attempts, measured 2026-08-19 on **captcha-v12**
|
|
36
34
|
against each vendor's own public demo page. Counts rather than percentages
|
|
37
35
|
because ten attempts is not a percentage. Idle time is cut from the clips, so
|
|
38
36
|
they run shorter than the solves they show.
|
|
39
37
|
|
|
40
|
-
**
|
|
38
|
+
**More puzzle types**, as video and with the full method, at
|
|
41
39
|
[captchakraken.com](https://captchakraken.com) and in the
|
|
42
|
-
[main repo](https://github.com/
|
|
40
|
+
[main repo](https://github.com/CaptchaKraken/CaptchaKraken#watch-it-work).
|
|
43
41
|
|
|
44
42
|
## What it solves
|
|
45
43
|
|
|
46
44
|
| Vendor | Puzzles |
|
|
47
45
|
|---|---|
|
|
48
46
|
| **reCAPTCHA** | 3×3 and 4×4 image grids, including the dynamic re-deal |
|
|
49
|
-
| **hCaptcha** | Image grids, click, drag, connect-the-path, tetris-fit, animated |
|
|
50
47
|
| **GeeTest** v3 + v4 | Slide, icon, nine, svg, gobang, iconcrush |
|
|
51
48
|
| **NetEase Yidun** | Jigsaw, picture-click, icon-click |
|
|
52
49
|
| **Tencent, Lemin, Prosopo** | Slide, cropped-image and grid flows |
|
|
@@ -67,7 +64,7 @@ pip install "captchakraken[serve]" # + the serving stack (vLLM/torch) to self-
|
|
|
67
64
|
The base install is lightweight — everything you need to solve captchas against
|
|
68
65
|
a vLLM server (local or remote). The `[serve]` extra pulls the heavy stack only
|
|
69
66
|
if you want to run the model yourself. The one-command
|
|
70
|
-
[`setup.sh`](https://github.com/
|
|
67
|
+
[`setup.sh`](https://github.com/CaptchaKraken/CaptchaKraken) installs `[serve]`,
|
|
71
68
|
downloads the weights, and writes an env file for you.
|
|
72
69
|
|
|
73
70
|
## No GPU? Use the hosted API
|
|
@@ -100,7 +97,7 @@ captchakraken server start | stop | status | run
|
|
|
100
97
|
```bash
|
|
101
98
|
# Solve an image/video: classify → find_grid → plan. Prints the click actions.
|
|
102
99
|
captchakraken path/to/captcha.png
|
|
103
|
-
captchakraken path/to/captcha.png --puzzle-source
|
|
100
|
+
captchakraken path/to/captcha.png --puzzle-source recaptcha
|
|
104
101
|
```
|
|
105
102
|
|
|
106
103
|
```python
|
package/python/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "captchakraken"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.2.0"
|
|
8
8
|
description = "Captcha-solving engine and CLI: OpenCV grid detection plus a fine-tuned Qwen3.5-9B vision model, for image grids, click and drag puzzles, sliders, distorted text and animated challenges. Hosted API or self-hosted on vLLM."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -15,7 +15,7 @@ requires-python = ">=3.11"
|
|
|
15
15
|
license = { file = "LICENSE" }
|
|
16
16
|
authors = [{ name = "Jake Writer" }]
|
|
17
17
|
keywords = [
|
|
18
|
-
"captcha", "captcha-solver", "recaptcha", "
|
|
18
|
+
"captcha", "captcha-solver", "recaptcha", "geetest", "turnstile",
|
|
19
19
|
"yidun", "tencent-captcha", "playwright", "browser-automation", "scraping",
|
|
20
20
|
"anti-bot", "vllm", "qwen", "computer-vision", "ocr",
|
|
21
21
|
]
|
|
@@ -92,10 +92,10 @@ captchakraken = "captchakraken.cli:main"
|
|
|
92
92
|
|
|
93
93
|
[project.urls]
|
|
94
94
|
Homepage = "https://captchakraken.com"
|
|
95
|
-
Documentation = "https://github.com/
|
|
96
|
-
Source = "https://github.com/
|
|
97
|
-
Issues = "https://github.com/
|
|
98
|
-
Changelog = "https://github.com/
|
|
95
|
+
Documentation = "https://github.com/CaptchaKraken/CaptchaKraken#readme"
|
|
96
|
+
Source = "https://github.com/CaptchaKraken/CaptchaKraken"
|
|
97
|
+
Issues = "https://github.com/CaptchaKraken/CaptchaKraken/issues"
|
|
98
|
+
Changelog = "https://github.com/CaptchaKraken/CaptchaKraken/blob/main/CHANGELOG.md"
|
|
99
99
|
|
|
100
100
|
[tool.hatch.build.targets.wheel]
|
|
101
101
|
packages = ["src/captchakraken"]
|
|
@@ -30,7 +30,9 @@ from .kinds import (
|
|
|
30
30
|
PromptFamily,
|
|
31
31
|
RetryMode,
|
|
32
32
|
Vendor,
|
|
33
|
+
Verdict,
|
|
33
34
|
)
|
|
35
|
+
from .verdicts import RoundVerdict
|
|
34
36
|
|
|
35
37
|
from .humanize import (
|
|
36
38
|
AppiumTouchBackend,
|
|
@@ -96,7 +98,9 @@ __all__ = [
|
|
|
96
98
|
"PauseKind",
|
|
97
99
|
"PromptFamily",
|
|
98
100
|
"RetryMode",
|
|
101
|
+
"RoundVerdict",
|
|
99
102
|
"Vendor",
|
|
103
|
+
"Verdict",
|
|
100
104
|
]
|
|
101
105
|
|
|
102
|
-
__version__ = "3.
|
|
106
|
+
__version__ = "3.2.0"
|
|
@@ -177,7 +177,10 @@ def _report_outcome(args):
|
|
|
177
177
|
try:
|
|
178
178
|
from .planner import ActionPlanner
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
planner = ActionPlanner()
|
|
181
|
+
reported = planner.report_outcome(args[0], Outcome(args[1]) == Outcome.SOLVED)
|
|
182
|
+
# `supported` false is the self-hosted 404, which tells the driver to stop spawning this.
|
|
183
|
+
return {"reported": bool(reported), "supported": planner.outcome_supported}
|
|
181
184
|
except Exception as exc:
|
|
182
185
|
return {"reported": False, "error": str(exc)}
|
|
183
186
|
|
|
@@ -185,6 +188,7 @@ def _report_outcome(args):
|
|
|
185
188
|
def _serve(args):
|
|
186
189
|
"""Persistent CV worker: one JSON request per stdin line, one JSON reply per stdout line."""
|
|
187
190
|
from .image_processor import ImageProcessor
|
|
191
|
+
from .tool_calls.find_checkbox import find_checkbox
|
|
188
192
|
from .tool_calls.find_grid import find_grid
|
|
189
193
|
|
|
190
194
|
def handle(req):
|
|
@@ -206,6 +210,8 @@ def _serve(args):
|
|
|
206
210
|
return match_region(req["ref"], req["live"], float(req["cx"]), float(req["cy"]), req.get("tolerance"))
|
|
207
211
|
if cmd == "board-painted":
|
|
208
212
|
return board_painted(req["image"], req.get("floor"), req.get("box"))
|
|
213
|
+
if cmd == "find-checkbox":
|
|
214
|
+
return find_checkbox(req["image"])
|
|
209
215
|
if cmd == "track-piece":
|
|
210
216
|
return track_piece(req["before"], req["after"], req.get("exclude"), req.get("travel") or 0.0)
|
|
211
217
|
raise ValueError(f"unknown cmd: {cmd!r}")
|
|
@@ -104,6 +104,15 @@ class Outcome(StrEnum):
|
|
|
104
104
|
FAILED = "failed"
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
class Verdict(StrEnum):
|
|
108
|
+
"""What the vendor's own server answered a round: BLOCKED is a refusal to serve at all, not a wrong answer."""
|
|
109
|
+
|
|
110
|
+
ACCEPTED = "accepted"
|
|
111
|
+
REJECTED = "rejected"
|
|
112
|
+
NEW_CHALLENGE = "new-challenge"
|
|
113
|
+
BLOCKED = "blocked"
|
|
114
|
+
|
|
115
|
+
|
|
107
116
|
class Phase(StrEnum):
|
|
108
117
|
"""Timing phases. Only INFERENCE and MOUSE are productive; everything else is waiting."""
|
|
109
118
|
|