captchakraken 2.6.1 → 2.8.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/README.md +90 -7
- package/dist/cli-invocation.d.ts +2 -0
- package/dist/cli-invocation.js +5 -1
- package/dist/errors.d.ts +1 -1
- package/dist/humanize.d.ts +377 -0
- package/dist/humanize.js +716 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +44 -1
- package/dist/playwright-types.d.ts +18 -0
- package/dist/solver.d.ts +112 -5
- package/dist/solver.js +527 -174
- package/dist/timing.d.ts +48 -0
- package/dist/timing.js +103 -0
- package/dist/trajectory.d.ts +27 -0
- package/dist/trajectory.js +104 -0
- package/dist/types.d.ts +99 -0
- package/package.json +1 -1
- package/python/README.md +43 -0
- package/python/pyproject.toml +1 -1
- package/python/src/captchakraken/__init__.py +36 -1
- package/python/src/captchakraken/cli.py +62 -4
- package/python/src/captchakraken/config.py +28 -2
- package/python/src/captchakraken/errors.py +24 -0
- package/python/src/captchakraken/humanize.py +929 -0
- package/python/src/captchakraken/keyframes.py +99 -0
- package/python/src/captchakraken/models.json +149 -13
- package/python/src/captchakraken/page_solver.py +476 -270
- package/python/src/captchakraken/pinned_model.json +3 -2
- package/python/src/captchakraken/planner.py +118 -9
- package/python/src/captchakraken/prompts.py +182 -0
- package/python/src/captchakraken/server_manager.py +9 -4
- package/python/src/captchakraken/solver.py +97 -57
- package/python/src/captchakraken/timing.py +8 -3
- package/python/src/captchakraken/tool_calls/find_grid.py +357 -20
- package/python/src/captchakraken/trajectory.py +117 -0
- package/python/src/captchakraken/updater.py +62 -1
package/dist/timing.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where one solve's wall-clock went, by phase.
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript half of `python/src/captchakraken/timing.py`; the two are one
|
|
5
|
+
* implementation in two languages and `test_timing_parity.py` pins that they
|
|
6
|
+
* name the same phases. That parity is the point: without it the ports can only
|
|
7
|
+
* be compared on their totals, and "the JS port is four seconds slower" is not
|
|
8
|
+
* a bug report — "the JS port spends four seconds starting a Python
|
|
9
|
+
* interpreter it did not need to start" is.
|
|
10
|
+
*
|
|
11
|
+
* Always on. It is a few map updates per phase against multi-second waits, and
|
|
12
|
+
* a budget you have to opt into is one nobody has when the slow solve happens.
|
|
13
|
+
* Only PRINTED under `CAPTCHA_TIMINGS=1`; always RETURNED on `SolveResult`.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* What a phase's time COUNTS AS, when asking how much of a solve was useful.
|
|
17
|
+
*
|
|
18
|
+
* Only two kinds of second are worth spending: one the model is thinking in,
|
|
19
|
+
* and one the pointer is travelling in (which has to look human, so it cannot
|
|
20
|
+
* be rushed). Everything else is the driver waiting on a clock, and every such
|
|
21
|
+
* wait is a candidate for deletion or for overlapping with something real.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PRODUCTIVE: Set<string>;
|
|
24
|
+
export declare function timingsEnabled(): boolean;
|
|
25
|
+
export declare class PhaseBudget {
|
|
26
|
+
readonly totals: Map<string, number>;
|
|
27
|
+
readonly counts: Map<string, number>;
|
|
28
|
+
private readonly open;
|
|
29
|
+
private readonly t0;
|
|
30
|
+
/**
|
|
31
|
+
* Attribute `fn`'s wall-clock to `name`.
|
|
32
|
+
*
|
|
33
|
+
* Phases may nest (a burst contains its screenshots). Only the OUTERMOST of a
|
|
34
|
+
* given NAME accumulates, so re-entering one cannot double-count it — but a
|
|
35
|
+
* phase nested inside a differently-named one counts under both, deliberately:
|
|
36
|
+
* the cursor drifting over the widget WHILE the model generates is genuinely
|
|
37
|
+
* both `mouse` and `inference`, and hiding either would misreport what the
|
|
38
|
+
* solve was doing. The totals are therefore an attribution, not a partition,
|
|
39
|
+
* and can exceed the elapsed time.
|
|
40
|
+
*/
|
|
41
|
+
phase<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
42
|
+
/** Record a measured span directly, for blocks that would need re-indenting. */
|
|
43
|
+
add(name: string, ms: number): void;
|
|
44
|
+
elapsedMs(): number;
|
|
45
|
+
/** Plain object for `SolveResult.phases`, with the solve total folded in. */
|
|
46
|
+
toObject(): Record<string, number>;
|
|
47
|
+
report(): string;
|
|
48
|
+
}
|
package/dist/timing.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Where one solve's wall-clock went, by phase.
|
|
4
|
+
*
|
|
5
|
+
* The TypeScript half of `python/src/captchakraken/timing.py`; the two are one
|
|
6
|
+
* implementation in two languages and `test_timing_parity.py` pins that they
|
|
7
|
+
* name the same phases. That parity is the point: without it the ports can only
|
|
8
|
+
* be compared on their totals, and "the JS port is four seconds slower" is not
|
|
9
|
+
* a bug report — "the JS port spends four seconds starting a Python
|
|
10
|
+
* interpreter it did not need to start" is.
|
|
11
|
+
*
|
|
12
|
+
* Always on. It is a few map updates per phase against multi-second waits, and
|
|
13
|
+
* a budget you have to opt into is one nobody has when the slow solve happens.
|
|
14
|
+
* Only PRINTED under `CAPTCHA_TIMINGS=1`; always RETURNED on `SolveResult`.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.PhaseBudget = exports.PRODUCTIVE = void 0;
|
|
18
|
+
exports.timingsEnabled = timingsEnabled;
|
|
19
|
+
/**
|
|
20
|
+
* What a phase's time COUNTS AS, when asking how much of a solve was useful.
|
|
21
|
+
*
|
|
22
|
+
* Only two kinds of second are worth spending: one the model is thinking in,
|
|
23
|
+
* and one the pointer is travelling in (which has to look human, so it cannot
|
|
24
|
+
* be rushed). Everything else is the driver waiting on a clock, and every such
|
|
25
|
+
* wait is a candidate for deletion or for overlapping with something real.
|
|
26
|
+
*/
|
|
27
|
+
exports.PRODUCTIVE = new Set(['inference', 'mouse']);
|
|
28
|
+
function timingsEnabled() {
|
|
29
|
+
return process.env.CAPTCHA_TIMINGS === '1';
|
|
30
|
+
}
|
|
31
|
+
class PhaseBudget {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.totals = new Map();
|
|
34
|
+
this.counts = new Map();
|
|
35
|
+
this.open = [];
|
|
36
|
+
this.t0 = Date.now();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Attribute `fn`'s wall-clock to `name`.
|
|
40
|
+
*
|
|
41
|
+
* Phases may nest (a burst contains its screenshots). Only the OUTERMOST of a
|
|
42
|
+
* given NAME accumulates, so re-entering one cannot double-count it — but a
|
|
43
|
+
* phase nested inside a differently-named one counts under both, deliberately:
|
|
44
|
+
* the cursor drifting over the widget WHILE the model generates is genuinely
|
|
45
|
+
* both `mouse` and `inference`, and hiding either would misreport what the
|
|
46
|
+
* solve was doing. The totals are therefore an attribution, not a partition,
|
|
47
|
+
* and can exceed the elapsed time.
|
|
48
|
+
*/
|
|
49
|
+
async phase(name, fn) {
|
|
50
|
+
if (this.open.includes(name))
|
|
51
|
+
return fn();
|
|
52
|
+
this.open.push(name);
|
|
53
|
+
const t0 = Date.now();
|
|
54
|
+
try {
|
|
55
|
+
return await fn();
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
this.open.splice(this.open.indexOf(name), 1);
|
|
59
|
+
this.add(name, Date.now() - t0);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Record a measured span directly, for blocks that would need re-indenting. */
|
|
63
|
+
add(name, ms) {
|
|
64
|
+
this.totals.set(name, (this.totals.get(name) ?? 0) + ms);
|
|
65
|
+
this.counts.set(name, (this.counts.get(name) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
elapsedMs() {
|
|
68
|
+
return Date.now() - this.t0;
|
|
69
|
+
}
|
|
70
|
+
/** Plain object for `SolveResult.phases`, with the solve total folded in. */
|
|
71
|
+
toObject() {
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const [k, v] of this.totals)
|
|
74
|
+
out[k] = v;
|
|
75
|
+
out.total = this.elapsedMs();
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
report() {
|
|
79
|
+
const total = this.elapsedMs();
|
|
80
|
+
let useful = 0;
|
|
81
|
+
for (const [k, v] of this.totals)
|
|
82
|
+
if (exports.PRODUCTIVE.has(k))
|
|
83
|
+
useful += v;
|
|
84
|
+
const rows = [...this.totals.entries()].sort((a, b) => b[1] - a[1]);
|
|
85
|
+
const lines = [
|
|
86
|
+
`[BUDGET] solve ${(total / 1000).toFixed(1)}s — ` +
|
|
87
|
+
`${(useful / 1000).toFixed(1)}s useful (${total ? Math.round((100 * useful) / total) : 0}%), ` +
|
|
88
|
+
`${((total - useful) / 1000).toFixed(1)}s waiting`,
|
|
89
|
+
];
|
|
90
|
+
for (const [name, ms] of rows) {
|
|
91
|
+
const tag = exports.PRODUCTIVE.has(name) ? '*' : ' ';
|
|
92
|
+
lines.push(`[BUDGET] ${tag} ${name.padEnd(22)} ${(ms / 1000).toFixed(2).padStart(6)}s x${this.counts.get(name)}`);
|
|
93
|
+
}
|
|
94
|
+
let attributed = 0;
|
|
95
|
+
for (const v of this.totals.values())
|
|
96
|
+
attributed += v;
|
|
97
|
+
if (total - attributed > 50) {
|
|
98
|
+
lines.push(`[BUDGET] ${'(unattributed)'.padEnd(22)} ${((total - attributed) / 1000).toFixed(2).padStart(6)}s`);
|
|
99
|
+
}
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.PhaseBudget = PhaseBudget;
|
package/dist/trajectory.d.ts
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* - a small overshoot-and-correct on longer moves, which is the single most
|
|
13
13
|
* recognisable human tell and the one a linear interpolation never produces
|
|
14
14
|
*
|
|
15
|
+
* `generate_swipe` is the SAME contract for a FINGER, and it is a different
|
|
16
|
+
* model rather than the mouse model with different constants — see its
|
|
17
|
+
* docstring.
|
|
18
|
+
*
|
|
15
19
|
* Camoufox's own `humanize` is a DIFFERENT, browser-level mechanism that
|
|
16
20
|
* re-humanises every `mouse.move()` it is handed. Running both composes them —
|
|
17
21
|
* measured 82.1s vs 13.4s on one geetest_v4_slide solve, because each of the 60
|
|
@@ -29,3 +33,26 @@ export type Point = [number, number];
|
|
|
29
33
|
* detectable.
|
|
30
34
|
*/
|
|
31
35
|
export declare function generate_trajectory(target_start: readonly number[], target_end: readonly number[], frequency?: number, frequencyRandomizer?: number): [Point[], number[]];
|
|
36
|
+
/**
|
|
37
|
+
* A FINGER travelling from `target_start` to `target_end`, same contract as
|
|
38
|
+
* `generate_trajectory`: `[points, timings]`, timings cumulative from 0, last
|
|
39
|
+
* point exactly `target_end`.
|
|
40
|
+
*
|
|
41
|
+
* A separate model rather than `generate_trajectory` with other constants,
|
|
42
|
+
* because the three things that make the mouse output human are each WRONG
|
|
43
|
+
* here:
|
|
44
|
+
*
|
|
45
|
+
* - **No overshoot-and-correct.** That tell is a hand arriving past a target
|
|
46
|
+
* it cannot see under the cursor. A finger occludes its own target and
|
|
47
|
+
* commits; the correction, when there is one, is a second gesture.
|
|
48
|
+
* - **A different velocity profile.** `easeTouch`, not `easeInOut` — above.
|
|
49
|
+
* - **Different jitter.** The mouse model adds white noise scaled by speed (a
|
|
50
|
+
* hand tracking a path imprecisely). A digitizer instead reports a wandering
|
|
51
|
+
* centroid, which is low-frequency and roughly speed INDEPENDENT, so this is
|
|
52
|
+
* a smoothed random walk.
|
|
53
|
+
*
|
|
54
|
+
* `frequency` defaults to 90: digitizers sample at 120Hz+, but touchmove is
|
|
55
|
+
* coalesced to the compositor, so more samples than this buys nothing and costs
|
|
56
|
+
* a dispatch round-trip each.
|
|
57
|
+
*/
|
|
58
|
+
export declare function generate_swipe(target_start: readonly number[], target_end: readonly number[], frequency?: number, frequencyRandomizer?: number): [Point[], number[]];
|
package/dist/trajectory.js
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* - a small overshoot-and-correct on longer moves, which is the single most
|
|
14
14
|
* recognisable human tell and the one a linear interpolation never produces
|
|
15
15
|
*
|
|
16
|
+
* `generate_swipe` is the SAME contract for a FINGER, and it is a different
|
|
17
|
+
* model rather than the mouse model with different constants — see its
|
|
18
|
+
* docstring.
|
|
19
|
+
*
|
|
16
20
|
* Camoufox's own `humanize` is a DIFFERENT, browser-level mechanism that
|
|
17
21
|
* re-humanises every `mouse.move()` it is handed. Running both composes them —
|
|
18
22
|
* measured 82.1s vs 13.4s on one geetest_v4_slide solve, because each of the 60
|
|
@@ -20,6 +24,7 @@
|
|
|
20
24
|
*/
|
|
21
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
26
|
exports.generate_trajectory = generate_trajectory;
|
|
27
|
+
exports.generate_swipe = generate_swipe;
|
|
23
28
|
// Fitts's law: MT = a + b * log2(distance / width + 1). The constants are the
|
|
24
29
|
// usual empirical range for a mouse; `WIDTH` stands in for target precision.
|
|
25
30
|
const FITTS_A_MS = 90.0;
|
|
@@ -160,3 +165,102 @@ function generate_trajectory(target_start, target_end, frequency = 60, frequency
|
|
|
160
165
|
points[points.length - 1] = end;
|
|
161
166
|
return [points, timings];
|
|
162
167
|
}
|
|
168
|
+
// ── Touch ───────────────────────────────────────────────────────────────────
|
|
169
|
+
//
|
|
170
|
+
// Fitts's law holds for direct touch too, but with a slower intercept and a much
|
|
171
|
+
// wider effective target: a fingertip contact patch is ~9mm across, which is the
|
|
172
|
+
// 44pt / 48dp minimum both platform guidelines are built around. So the same
|
|
173
|
+
// distance takes longer AND is aimed less precisely than with a mouse.
|
|
174
|
+
const TOUCH_FITTS_A_MS = 160.0;
|
|
175
|
+
const TOUCH_FITTS_B_MS = 135.0;
|
|
176
|
+
const TOUCH_WIDTH_PX = 44.0;
|
|
177
|
+
// How flick-like the launch is. 0 would be the mouse's symmetric bell; 1 would
|
|
178
|
+
// be an instantaneous jump at t=0. See `easeTouch`.
|
|
179
|
+
const TOUCH_LAUNCH = 0.7;
|
|
180
|
+
// A finger drags across glass on a short wrist/thumb pivot, so it bows far less
|
|
181
|
+
// than a hand moving a mouse across a desk.
|
|
182
|
+
const TOUCH_BOW_CAP_PX = 40.0;
|
|
183
|
+
// The reported contact point wanders, because it is the CENTROID of a soft patch
|
|
184
|
+
// rolling under pressure rather than a rigid sensor. `TOUCH_WOBBLE_DECAY` is the
|
|
185
|
+
// AR(1) coefficient that makes that wander low-frequency: white per-sample noise
|
|
186
|
+
// would show up in a spectrum as nothing a finger produces.
|
|
187
|
+
const TOUCH_WOBBLE_PX = 0.55;
|
|
188
|
+
const TOUCH_WOBBLE_DECAY = 0.82;
|
|
189
|
+
/**
|
|
190
|
+
* Asymmetric ease: a finger leaves fast and brakes late.
|
|
191
|
+
*
|
|
192
|
+
* A mouse hand is symmetric (`easeInOut` — accelerate, decelerate, equally). A
|
|
193
|
+
* finger is not: the launch is a flick off the contact point and the arrival is
|
|
194
|
+
* a brake. Blending the two rather than using a pure ease-out is what keeps the
|
|
195
|
+
* launch from being an instantaneous jump at t=0, which no digitizer would ever
|
|
196
|
+
* report.
|
|
197
|
+
*/
|
|
198
|
+
function easeTouch(t) {
|
|
199
|
+
return (1.0 - TOUCH_LAUNCH) * easeInOut(t) + TOUCH_LAUNCH * (1.0 - Math.pow(1.0 - t, 2.4));
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* A FINGER travelling from `target_start` to `target_end`, same contract as
|
|
203
|
+
* `generate_trajectory`: `[points, timings]`, timings cumulative from 0, last
|
|
204
|
+
* point exactly `target_end`.
|
|
205
|
+
*
|
|
206
|
+
* A separate model rather than `generate_trajectory` with other constants,
|
|
207
|
+
* because the three things that make the mouse output human are each WRONG
|
|
208
|
+
* here:
|
|
209
|
+
*
|
|
210
|
+
* - **No overshoot-and-correct.** That tell is a hand arriving past a target
|
|
211
|
+
* it cannot see under the cursor. A finger occludes its own target and
|
|
212
|
+
* commits; the correction, when there is one, is a second gesture.
|
|
213
|
+
* - **A different velocity profile.** `easeTouch`, not `easeInOut` — above.
|
|
214
|
+
* - **Different jitter.** The mouse model adds white noise scaled by speed (a
|
|
215
|
+
* hand tracking a path imprecisely). A digitizer instead reports a wandering
|
|
216
|
+
* centroid, which is low-frequency and roughly speed INDEPENDENT, so this is
|
|
217
|
+
* a smoothed random walk.
|
|
218
|
+
*
|
|
219
|
+
* `frequency` defaults to 90: digitizers sample at 120Hz+, but touchmove is
|
|
220
|
+
* coalesced to the compositor, so more samples than this buys nothing and costs
|
|
221
|
+
* a dispatch round-trip each.
|
|
222
|
+
*/
|
|
223
|
+
function generate_swipe(target_start, target_end, frequency = 90, frequencyRandomizer = 0.1) {
|
|
224
|
+
const start = [Number(target_start[0]), Number(target_start[1])];
|
|
225
|
+
const end = [Number(target_end[0]), Number(target_end[1])];
|
|
226
|
+
const dx = end[0] - start[0];
|
|
227
|
+
const dy = end[1] - start[1];
|
|
228
|
+
const distance = Math.hypot(dx, dy);
|
|
229
|
+
if (distance < 1e-6)
|
|
230
|
+
return [[end], [0.0]];
|
|
231
|
+
let durationMs = TOUCH_FITTS_A_MS + TOUCH_FITTS_B_MS * Math.log2(distance / TOUCH_WIDTH_PX + 1.0);
|
|
232
|
+
durationMs *= uniform(0.85, 1.2);
|
|
233
|
+
const steps = Math.max(2, Math.round((durationMs / 1000.0) * Math.max(1, frequency)));
|
|
234
|
+
let c1;
|
|
235
|
+
let c2;
|
|
236
|
+
if (distance < SHORT_MOVE_PX) {
|
|
237
|
+
c1 = start;
|
|
238
|
+
c2 = end;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
[c1, c2] = controlPoints(start, end, distance);
|
|
242
|
+
// Pull the mouse model's bow in: `controlPoints` is shared, and its arc is
|
|
243
|
+
// sized for a hand crossing a desk.
|
|
244
|
+
const shrink = Math.min(1.0, TOUCH_BOW_CAP_PX / Math.max(TOUCH_BOW_CAP_PX, distance * 0.16));
|
|
245
|
+
c1 = [start[0] + (c1[0] - start[0]) * shrink, start[1] + (c1[1] - start[1]) * shrink];
|
|
246
|
+
c2 = [start[0] + (c2[0] - start[0]) * shrink, start[1] + (c2[1] - start[1]) * shrink];
|
|
247
|
+
}
|
|
248
|
+
const points = [];
|
|
249
|
+
const timings = [];
|
|
250
|
+
let elapsed = 0.0;
|
|
251
|
+
const perStep = durationMs / steps;
|
|
252
|
+
let wobX = 0.0;
|
|
253
|
+
let wobY = 0.0;
|
|
254
|
+
for (let i = 0; i <= steps; i++) {
|
|
255
|
+
const t = easeTouch(i / steps);
|
|
256
|
+
const [px, py] = cubicBezier(start, c1, c2, end, t);
|
|
257
|
+
wobX = wobX * TOUCH_WOBBLE_DECAY + gauss(0.0, TOUCH_WOBBLE_PX);
|
|
258
|
+
wobY = wobY * TOUCH_WOBBLE_DECAY + gauss(0.0, TOUCH_WOBBLE_PX);
|
|
259
|
+
points.push([px + wobX, py + wobY]);
|
|
260
|
+
timings.push(elapsed);
|
|
261
|
+
elapsed += perStep * uniform(1.0 - frequencyRandomizer, 1.0 + frequencyRandomizer);
|
|
262
|
+
}
|
|
263
|
+
// Land exactly where the caller asked, same reason as the mouse model.
|
|
264
|
+
points[points.length - 1] = end;
|
|
265
|
+
return [points, timings];
|
|
266
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Humanizer, HumanizationMode, TouchTransform } from './humanize.js';
|
|
1
2
|
/**
|
|
2
3
|
* Lifecycle event emitted by {@link CaptchaKrakenConfig.onStep}. One is fired
|
|
3
4
|
* before any interaction (`stage: 'initial'`) and one after every executed
|
|
@@ -46,6 +47,65 @@ export interface SolveStepEvent {
|
|
|
46
47
|
meta?: Record<string, any>;
|
|
47
48
|
}
|
|
48
49
|
export interface CaptchaKrakenConfig {
|
|
50
|
+
/**
|
|
51
|
+
* How the driver MOVES — a choice of input DEVICE, not a realism dial.
|
|
52
|
+
*
|
|
53
|
+
* - `'mouse'` (default) Bezier arcs, Fitts's-law durations, overshoot.
|
|
54
|
+
* - `'mobile'` touch events with finger kinematics. On a touch-only widget
|
|
55
|
+
* this is the difference between the page's handlers firing and
|
|
56
|
+
* not; a mousemove there is the wrong event, not a weaker one.
|
|
57
|
+
* Needs a Chromium-family page launched with `hasTouch: true`,
|
|
58
|
+
* or an Appium/WebdriverIO driver in {@link touchDriver}.
|
|
59
|
+
* - `'none'` the shortest legal path to the same DOM effect. Much faster,
|
|
60
|
+
* and detectable by anything scoring pointer telemetry — for
|
|
61
|
+
* fixtures, self-hosted targets, and stacks that humanise
|
|
62
|
+
* elsewhere.
|
|
63
|
+
*
|
|
64
|
+
* Unset falls back to `CAPTCHA_HUMANIZATION`, then `'mouse'`. Set
|
|
65
|
+
* {@link humanizer} instead to supply your own implementation.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const context = await browser.newContext({ ...devices['Pixel 7'], hasTouch: true });
|
|
70
|
+
* const solver = new CaptchaKrakenSolver({ humanization: 'mobile' });
|
|
71
|
+
* await solver.solve(await context.newPage());
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
humanization?: HumanizationMode;
|
|
75
|
+
/**
|
|
76
|
+
* Your own {@link Humanizer}. Overrides {@link humanization} entirely — the
|
|
77
|
+
* driver then makes no decisions about pointer motion at all.
|
|
78
|
+
*
|
|
79
|
+
* Use it when you already model your users' input (a hardware pointer, a
|
|
80
|
+
* device farm, a recorded trace), or when the browser humanises for you:
|
|
81
|
+
* camoufox's `humanize` juggler re-humanises every `mouse.move()` it is
|
|
82
|
+
* handed, and composing both measured 82.1s against 13.4s on one solve.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* import { NullHumanizer, type Humanizer } from 'captchakraken';
|
|
87
|
+
*
|
|
88
|
+
* class MyPointer extends NullHumanizer implements Humanizer {
|
|
89
|
+
* async move(page, to) { await myHardwareMouse.glideTo(to[0], to[1]); this.at = to; }
|
|
90
|
+
* }
|
|
91
|
+
* new CaptchaKrakenSolver({ humanizer: new MyPointer() });
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
humanizer?: Humanizer;
|
|
95
|
+
/**
|
|
96
|
+
* `humanization: 'mobile'` only. The thing that is actually TOUCHED, when it
|
|
97
|
+
* is not the page object — an Appium / WebdriverIO / Selenium driver on a real
|
|
98
|
+
* handset. Left unset, the mode dispatches CDP touch events at the page it
|
|
99
|
+
* was given, which is what browser mobile emulation wants.
|
|
100
|
+
*/
|
|
101
|
+
touchDriver?: any;
|
|
102
|
+
/**
|
|
103
|
+
* `humanization: 'mobile'` only. CSS-pixel → device-pixel transform for
|
|
104
|
+
* {@link touchDriver}. `scale` is usually `window.devicePixelRatio`; `origin`
|
|
105
|
+
* is the top-left of the webview in screen coordinates. The default identity
|
|
106
|
+
* is right for emulation and for callers who map coordinates themselves.
|
|
107
|
+
*/
|
|
108
|
+
touchTransform?: TouchTransform;
|
|
49
109
|
/**
|
|
50
110
|
* Optional observer fired at each intermediate solve stage. Receives a
|
|
51
111
|
* baseline screenshot before any action and one after every executed action.
|
|
@@ -78,6 +138,23 @@ export interface CaptchaKrakenConfig {
|
|
|
78
138
|
* with the vLLM server; see model-name.ts.
|
|
79
139
|
*/
|
|
80
140
|
model?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Force ONE expert of a ROUTED model — 'pixel' | 'grid' | 'video' | 'text'.
|
|
143
|
+
*
|
|
144
|
+
* A routed model (Abyss) is four LoRA adapters behind one endpoint, and the
|
|
145
|
+
* router is the prompt family each request is about to send — so leaving this
|
|
146
|
+
* unset already reaches the right expert. This is the override: serve one
|
|
147
|
+
* arm, drive only the puzzles it owns, which is what a per-arm benchmark
|
|
148
|
+
* needs and what a licence holder pinning a single expert wants.
|
|
149
|
+
*
|
|
150
|
+
* Refused against a model that serves a single adapter — every model
|
|
151
|
+
* published so far — rather than ignored, because a run that quietly measured
|
|
152
|
+
* the generalist while reporting an expert is a number nobody can catch.
|
|
153
|
+
*
|
|
154
|
+
* Mirrors the Python `PageSolverConfig.expert`; also settable as
|
|
155
|
+
* CAPTCHA_EXPERT, which this forwards to the CLI ahead of.
|
|
156
|
+
*/
|
|
157
|
+
expert?: string;
|
|
81
158
|
/**
|
|
82
159
|
* Bearer token for the vLLM server (also picked up from VLLM_API_KEY env).
|
|
83
160
|
*/
|
|
@@ -379,6 +456,18 @@ export interface CaptchaKrakenConfig {
|
|
|
379
456
|
* Default: 8000
|
|
380
457
|
*/
|
|
381
458
|
elementScreenshotTimeoutMs?: number;
|
|
459
|
+
/**
|
|
460
|
+
* How long an `onStep` observer's screenshot may take, ms (default 2000).
|
|
461
|
+
*
|
|
462
|
+
* Deliberately separate from `elementScreenshotTimeoutMs`, which is sized for
|
|
463
|
+
* the picture the MODEL reads. This one is a trace snapshot: it is taken with
|
|
464
|
+
* `animations: 'disabled'`, which makes Playwright wait for the element to
|
|
465
|
+
* stop moving first, and on a still-animating widget that ran to the full 8s
|
|
466
|
+
* on every step — 8.0s of a measured 12.0s solve. An observer must never cost
|
|
467
|
+
* more than the action it observes, and a missed frame in a trace costs
|
|
468
|
+
* nothing. Ignored entirely when no `onStep` is set.
|
|
469
|
+
*/
|
|
470
|
+
stepScreenshotTimeoutMs?: number;
|
|
382
471
|
/**
|
|
383
472
|
* After a submit, hCaptcha may replace the challenge iframe for the next
|
|
384
473
|
* round, detaching the handle we just detected ("element is not visible").
|
|
@@ -495,4 +584,14 @@ export interface SolveResult {
|
|
|
495
584
|
cachedInputTokens: number;
|
|
496
585
|
estimatedCost: number;
|
|
497
586
|
};
|
|
587
|
+
/**
|
|
588
|
+
* Where this solve's wall-clock went, in milliseconds per phase — the same
|
|
589
|
+
* partition `CAPTCHA_TIMINGS=1` prints, handed to the caller instead of to
|
|
590
|
+
* stderr. Mirrors `SolveResult.phases` on the Python port.
|
|
591
|
+
*
|
|
592
|
+
* Returned rather than only logged because "the solve took 12s" is not
|
|
593
|
+
* actionable and "the settle monitor spent 4s of it" is, and a caller
|
|
594
|
+
* measuring a fleet cannot scrape another process's stderr.
|
|
595
|
+
*/
|
|
596
|
+
phases?: Record<string, number>;
|
|
498
597
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "captchakraken",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
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",
|
package/python/README.md
CHANGED
|
@@ -21,6 +21,26 @@ nothing at all.
|
|
|
21
21
|
> self-hosting guide, see the main repo
|
|
22
22
|
> **[CaptchaKraken](https://github.com/JWriter20/CaptchaKraken)**.
|
|
23
23
|
|
|
24
|
+
## Watch it work
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/demo/hcaptcha_grid.webp" width="260"
|
|
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"
|
|
30
|
+
alt="A live reCAPTCHA 4×4 tile grid challenge being solved end to end">
|
|
31
|
+
<img src="https://raw.githubusercontent.com/JWriter20/CaptchaKraken/main/docs/assets/demo/geetest_slide.webp" width="260"
|
|
32
|
+
alt="A live GeeTest slide jigsaw challenge being solved end to end">
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
hCaptcha image select 12/12 in 10.5s · 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
|
+
against each vendor's own public demo page. Counts rather than percentages
|
|
37
|
+
because ten attempts is not a percentage. Idle time is cut from the clips, so
|
|
38
|
+
they run shorter than the solves they show.
|
|
39
|
+
|
|
40
|
+
**Ten more puzzle types**, as video and with the full method, at
|
|
41
|
+
[captchakraken.com](https://captchakraken.com) and in the
|
|
42
|
+
[main repo](https://github.com/JWriter20/CaptchaKraken#watch-it-work).
|
|
43
|
+
|
|
24
44
|
## What it solves
|
|
25
45
|
|
|
26
46
|
| Vendor | Puzzles |
|
|
@@ -107,6 +127,29 @@ Everything model-specific lives in `captchakraken.config` and is env-overridable
|
|
|
107
127
|
| `CAPTCHA_LORA_ADAPTER` | Captcha adapter (HF id or path) | `CaptchaKraken/CaptchaKraken-Lora-v1.2` |
|
|
108
128
|
| `CAPTCHA_LORA_NAME` | Served adapter name the client requests | `captcha-v12` |
|
|
109
129
|
| `CAPTCHA_KRAKEN_AUTOSTART` | `0` disables local auto-start | `1` |
|
|
130
|
+
| `CAPTCHA_HUMANIZATION` | How gestures are performed: `mouse`, `mobile` or `none` | `mouse` |
|
|
131
|
+
|
|
132
|
+
**How it moves** is a choice of input device, not a realism dial. `mobile`
|
|
133
|
+
dispatches real touch events with finger kinematics and never touches
|
|
134
|
+
`page.mouse`; `none` goes straight to the DOM effect. Set it in code (which wins
|
|
135
|
+
over the env var, because the right mode is a property of the page you are
|
|
136
|
+
driving), and pass your own object to override ours entirely:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from captchakraken import PageSolver
|
|
140
|
+
from captchakraken.page_solver import PageSolverConfig
|
|
141
|
+
|
|
142
|
+
PageSolver(config=PageSolverConfig(humanization="mobile"))
|
|
143
|
+
PageSolver(config=PageSolverConfig(humanization="none"))
|
|
144
|
+
PageSolver(config=PageSolverConfig(humanizer=my_own))
|
|
145
|
+
|
|
146
|
+
# A real handset over Appium / Selenium — W3C touch pointer actions.
|
|
147
|
+
PageSolver(config=PageSolverConfig(
|
|
148
|
+
humanization="mobile",
|
|
149
|
+
touch_driver=driver,
|
|
150
|
+
touch_transform={"scale": 3.0, "origin": (0, 132)}, # CSS px -> screen px
|
|
151
|
+
))
|
|
152
|
+
```
|
|
110
153
|
|
|
111
154
|
## License
|
|
112
155
|
|
package/python/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "captchakraken"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.8.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.10"
|
|
@@ -32,6 +32,27 @@ from .action_types import (
|
|
|
32
32
|
from .image_processor import ImageProcessor
|
|
33
33
|
from .overlay import add_overlays_to_image
|
|
34
34
|
|
|
35
|
+
# The hosted API's refusal type. Outside the try below on purpose: `errors`
|
|
36
|
+
# imports nothing but `typing`, and docs/hosted-api.md tells every caller to
|
|
37
|
+
# branch on its `.code` — a recipe that has to work in both ports, and in a
|
|
38
|
+
# minimal install that has no serving stack.
|
|
39
|
+
from .errors import CaptchaKrakenAPIError
|
|
40
|
+
|
|
41
|
+
# How gestures are PERFORMED — mouse, touch, or nothing. Deliberately outside
|
|
42
|
+
# the try below: `humanize` imports only `trajectory`, so it has no dependency
|
|
43
|
+
# floor at all, and a caller writing a custom Humanizer must be able to import
|
|
44
|
+
# the base class without the serving stack installed.
|
|
45
|
+
from .humanize import (
|
|
46
|
+
AppiumTouchBackend,
|
|
47
|
+
CdpTouchBackend,
|
|
48
|
+
Humanizer,
|
|
49
|
+
MobileHumanizer,
|
|
50
|
+
MouseHumanizer,
|
|
51
|
+
NullHumanizer,
|
|
52
|
+
TouchBackend,
|
|
53
|
+
TouchscreenTouchBackend,
|
|
54
|
+
)
|
|
55
|
+
|
|
35
56
|
# The planner (requests) and solver (torch/vllm/transformers) pull in the heavy
|
|
36
57
|
# serving stack. Keep them optional so leaf modules — e.g. tool_calls.find_grid,
|
|
37
58
|
# which needs only cv2 + numpy + pillow — can be imported in a minimal env (CI's
|
|
@@ -70,6 +91,20 @@ __all__ = [
|
|
|
70
91
|
"TypeAction",
|
|
71
92
|
"WaitAction",
|
|
72
93
|
"add_overlays_to_image",
|
|
94
|
+
"CaptchaKrakenAPIError",
|
|
95
|
+
"Humanizer",
|
|
96
|
+
"MouseHumanizer",
|
|
97
|
+
"MobileHumanizer",
|
|
98
|
+
"NullHumanizer",
|
|
99
|
+
"TouchBackend",
|
|
100
|
+
"CdpTouchBackend",
|
|
101
|
+
"AppiumTouchBackend",
|
|
102
|
+
"TouchscreenTouchBackend",
|
|
73
103
|
]
|
|
74
104
|
|
|
75
|
-
|
|
105
|
+
# KEEP IN STEP with python/pyproject.toml and js/package.json — the two ports
|
|
106
|
+
# ship together (rule 1c) and this is the only one importable code can read.
|
|
107
|
+
# It said 2.6.0 while the wheel on PyPI said 2.6.1, so anyone gating on the
|
|
108
|
+
# runtime attribute saw a version that had not been current for weeks.
|
|
109
|
+
# tests/test_public_contract.py compares all three.
|
|
110
|
+
__version__ = "2.8.0"
|