captchakraken 2.6.1 → 2.9.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 +16 -16
- package/dist/cli-invocation.js +33 -21
- 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/model-name.d.ts +1 -1
- package/dist/model-name.js +1 -1
- package/dist/playwright-types.d.ts +18 -0
- package/dist/solver.d.ts +314 -16
- package/dist/solver.js +1208 -278
- package/dist/timing.d.ts +48 -0
- package/dist/timing.js +103 -0
- package/dist/trajectory.d.ts +28 -1
- package/dist/trajectory.js +105 -1
- package/dist/types.d.ts +113 -2
- package/package.json +2 -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 +66 -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 +184 -12
- package/python/src/captchakraken/models.json +180 -18
- package/python/src/captchakraken/page_solver.py +1132 -321
- package/python/src/captchakraken/pinned_model.json +3 -2
- package/python/src/captchakraken/planner.py +204 -16
- package/python/src/captchakraken/prompts.py +186 -4
- package/python/src/captchakraken/server_manager.py +9 -4
- package/python/src/captchakraken/solver.py +98 -58
- 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
|
@@ -24,7 +24,6 @@ v1 had a SAM3-backed tool-using planner with detect/segment/drag-refine; it
|
|
|
24
24
|
lives on the `v1-old-architecture` branch.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
-
import math
|
|
28
27
|
import os
|
|
29
28
|
import shutil
|
|
30
29
|
import sys
|
|
@@ -112,6 +111,47 @@ class DebugManager:
|
|
|
112
111
|
return None
|
|
113
112
|
|
|
114
113
|
|
|
114
|
+
#: The only cell counts a registered grid puzzle has. `find_grid` is a lattice
|
|
115
|
+
#: detector, not a catalogue — it allows rectangles and any dimension from
|
|
116
|
+
#: MIN_GRID_DIM up — so it will happily report 12 or 20 cells on a click puzzle's
|
|
117
|
+
#: header/footer bands. Anything not in here is a false positive by construction,
|
|
118
|
+
#: and it is also a board the model has never seen: the training pipeline's own
|
|
119
|
+
#: `grid_overlay.detect_cells` refuses every count but these two.
|
|
120
|
+
GRID_CELL_SHAPES = {9: (3, 3), 16: (4, 4)}
|
|
121
|
+
|
|
122
|
+
#: Cell counts each vendor actually ships, for the vendors the driver can name.
|
|
123
|
+
#: Only reCAPTCHA has a 4x4; hCaptcha's single grid puzzle is 3x3, so 16 cells on
|
|
124
|
+
#: an hCaptcha board is a contradiction rather than a close call. Measured on real
|
|
125
|
+
#: captures, that one line removes four of the five non-grid boards that clear
|
|
126
|
+
#: both find_grid and _is_real_grid today.
|
|
127
|
+
#:
|
|
128
|
+
#: A vendor NOT in this table — `unknown`, or anything new — gets every shape.
|
|
129
|
+
#: `unknown` is what the driver reports for GeeTest and Prosopo, both of which
|
|
130
|
+
#: ship real 3x3 grids, and it is what the one-shot image API defaults to, which
|
|
131
|
+
#: is the path the offline grader uses to score reCAPTCHA 4x4. Narrowing it would
|
|
132
|
+
#: stop scoring a whole puzzle type while looking like a tightening.
|
|
133
|
+
VENDOR_GRID_CELLS = {
|
|
134
|
+
"hcaptcha": frozenset({9}),
|
|
135
|
+
"recaptcha": frozenset({9, 16}),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _grid_dims(n_cells, puzzle_source="unknown"):
|
|
140
|
+
"""(rows, cols) if this detection could be a grid we ship, else None.
|
|
141
|
+
|
|
142
|
+
None means "do not solve this as a grid" — the caller falls through to the
|
|
143
|
+
click/drag path, which is what the board actually is. It is deliberately not
|
|
144
|
+
an exception: a false positive here is an ordinary event, not an error.
|
|
145
|
+
"""
|
|
146
|
+
shape = GRID_CELL_SHAPES.get(n_cells)
|
|
147
|
+
if shape is None:
|
|
148
|
+
return None
|
|
149
|
+
allowed = VENDOR_GRID_CELLS.get(puzzle_source)
|
|
150
|
+
if allowed is not None and n_cells not in allowed:
|
|
151
|
+
return None
|
|
152
|
+
return shape
|
|
153
|
+
|
|
154
|
+
|
|
115
155
|
class CaptchaSolver:
|
|
116
156
|
"""v2 solver: OpenCV grid detection + vLLM `captcha` LoRA."""
|
|
117
157
|
|
|
@@ -120,13 +160,15 @@ class CaptchaSolver:
|
|
|
120
160
|
model: Optional[str] = None,
|
|
121
161
|
provider: str = "captchaKrakenApi",
|
|
122
162
|
api_key: Optional[str] = None,
|
|
163
|
+
expert: Optional[str] = None,
|
|
123
164
|
):
|
|
124
165
|
self.debug = DebugManager(DEBUG)
|
|
125
166
|
# `provider` kept for argv compatibility with the v1 CLI signature.
|
|
126
167
|
if provider not in {"captchaKrakenApi"}:
|
|
127
168
|
self.debug.log(f"Provider {provider!r} ignored; v2 only supports captchaKrakenApi.")
|
|
128
169
|
self.planner = ActionPlanner(
|
|
129
|
-
model=model, api_key=api_key, debug_callback=self.debug.log
|
|
170
|
+
model=model, api_key=api_key, debug_callback=self.debug.log,
|
|
171
|
+
expert=expert,
|
|
130
172
|
)
|
|
131
173
|
self.image_processor = ImageProcessor(None, self.planner, self.debug)
|
|
132
174
|
self._image_size: Optional[Tuple[int, int]] = None
|
|
@@ -183,9 +225,22 @@ class CaptchaSolver:
|
|
|
183
225
|
|
|
184
226
|
with timed("solver.find_grid"):
|
|
185
227
|
grid_boxes = find_grid(cv_image_path)
|
|
186
|
-
|
|
228
|
+
# The SHAPE gate runs before the content gate: a lattice no vendor ships
|
|
229
|
+
# is not a grid whatever its cells look like, and saying so here keeps
|
|
230
|
+
# `_solve_grid` from ever being handed dimensions it would have to invent.
|
|
231
|
+
dims = _grid_dims(len(grid_boxes), puzzle_source) if grid_boxes else None
|
|
232
|
+
if grid_boxes and dims and self._is_real_grid(cv_image_path, grid_boxes):
|
|
187
233
|
self.debug.log(f"Detected grid with {len(grid_boxes)} cells")
|
|
188
|
-
return self._solve_grid(cv_image_path, grid_boxes,
|
|
234
|
+
return self._solve_grid(cv_image_path, grid_boxes, dims[0], dims[1],
|
|
235
|
+
retry_mode=retry_mode)
|
|
236
|
+
elif grid_boxes and not dims:
|
|
237
|
+
# e.g. a 16-cell lattice on an hCaptcha board (it ships no 4x4), or a
|
|
238
|
+
# 12-cell one on anything. Fall through to the click path, which is
|
|
239
|
+
# what this puzzle is.
|
|
240
|
+
self.debug.log(
|
|
241
|
+
f"find_grid returned {len(grid_boxes)} cells, which is not a shape "
|
|
242
|
+
f"{puzzle_source} ships — not solving as a grid."
|
|
243
|
+
)
|
|
189
244
|
elif grid_boxes:
|
|
190
245
|
# find_grid latched onto e.g. an hCaptcha click-puzzle's
|
|
191
246
|
# header/footer bands. Reject — only true grids are supported.
|
|
@@ -403,11 +458,6 @@ class CaptchaSolver:
|
|
|
403
458
|
if not stds:
|
|
404
459
|
return False
|
|
405
460
|
|
|
406
|
-
n = len(grid_boxes)
|
|
407
|
-
side = 3 if n == 9 else 4 if n == 16 else int(round(n ** 0.5))
|
|
408
|
-
rows = [stds[i * side:(i + 1) * side] for i in range(side)]
|
|
409
|
-
cols = [stds[i::side] for i in range(side)]
|
|
410
|
-
|
|
411
461
|
self.debug.log(
|
|
412
462
|
f"_is_real_grid: per-cell stddev = {[round(s, 1) for s in stds]}"
|
|
413
463
|
)
|
|
@@ -430,7 +480,7 @@ class CaptchaSolver:
|
|
|
430
480
|
# colour is its own; a sprite board shares one board colour behind
|
|
431
481
|
# every cell, so the medians collapse onto a single value.
|
|
432
482
|
# Measured over the real captures: iconcrush and gobang 0.0-0.8,
|
|
433
|
-
# against 14.9 (
|
|
483
|
+
# against 14.9 (a reCAPTCHA 4x4 grid) up to 93.0 (a GeeTest v4 icon board)
|
|
434
484
|
# for the five true grid families. The threshold sits in an 18x
|
|
435
485
|
# gap, which is why it is a fixed number rather than a tuned one.
|
|
436
486
|
medians = []
|
|
@@ -459,46 +509,35 @@ class CaptchaSolver:
|
|
|
459
509
|
)
|
|
460
510
|
return False
|
|
461
511
|
|
|
462
|
-
FLAT
|
|
463
|
-
#
|
|
464
|
-
flat_mask = [[1 if stds[r * side + c] < FLAT else 0
|
|
465
|
-
for c in range(side)] for r in range(side)]
|
|
466
|
-
flat_count = sum(sum(row) for row in flat_mask)
|
|
467
|
-
|
|
468
|
-
# Heuristic: in a *real* captcha grid, flat tiles (sky, asphalt)
|
|
469
|
-
# always form a spatially contiguous blob (top-left sky stack,
|
|
470
|
-
# bottom road row, etc.) because the underlying image is a single
|
|
471
|
-
# photo split into tiles. In a click-puzzle false grid, the flat
|
|
472
|
-
# cells are the **left + right margins** of the middle row,
|
|
473
|
-
# separated by the rich photo cell — non-contiguous.
|
|
512
|
+
# THE FLAT-CELL CONTIGUITY HEURISTIC IS GONE, and it was the only
|
|
513
|
+
# thing rejecting real grids.
|
|
474
514
|
#
|
|
475
|
-
#
|
|
476
|
-
#
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
return False
|
|
515
|
+
# It assumed flat tiles in a genuine photo grid always form ONE
|
|
516
|
+
# contiguous blob (a top-left sky stack, a bottom road row), because
|
|
517
|
+
# the board is a single photo cut up; a click-puzzle false grid puts
|
|
518
|
+
# its flat cells in the left and right margins of the middle row,
|
|
519
|
+
# which is two components. True of the boards it was written for,
|
|
520
|
+
# and false of plenty of photographs — a subject centred against
|
|
521
|
+
# plain sky leaves two flat corners and nothing between them.
|
|
522
|
+
#
|
|
523
|
+
# MEASURED over every real capture we hold, restricted to lattices
|
|
524
|
+
# `_grid_dims` actually accepts (9 or 16 cells — a 25-cell gobang
|
|
525
|
+
# board never reaches this function as a grid decision):
|
|
526
|
+
#
|
|
527
|
+
# rule grid false neg board false pos
|
|
528
|
+
# spread + mean-stddev 2/1748 0.11% 0/269 0.00%
|
|
529
|
+
# ...plus contiguity 40/1748 2.29% 0/269 0.00%
|
|
530
|
+
#
|
|
531
|
+
# It cost 38 real grids and caught nothing the two checks above did
|
|
532
|
+
# not already catch — 19 of those were a reCAPTCHA 4x4 grid alone,
|
|
533
|
+
# 5.5% of that family, every one of them sent to the click path to
|
|
534
|
+
# be asked for coordinates on a board answered with cell numbers.
|
|
535
|
+
# Nothing errors when that happens; the reply is simply graded
|
|
536
|
+
# against a gold it cannot match.
|
|
537
|
+
#
|
|
538
|
+
# The median-colour spread check above supersedes it and is a much
|
|
539
|
+
# cleaner separation (an 18x gap rather than a topological guess),
|
|
540
|
+
# which is why this is a deletion and not a retuning.
|
|
502
541
|
return True
|
|
503
542
|
except Exception as e:
|
|
504
543
|
self.debug.log(f"_is_real_grid check errored ({e}); accepting grid.")
|
|
@@ -508,17 +547,18 @@ class CaptchaSolver:
|
|
|
508
547
|
self,
|
|
509
548
|
image_path: str,
|
|
510
549
|
grid_boxes: List[Tuple[int, int, int, int]],
|
|
550
|
+
rows: int,
|
|
551
|
+
cols: int,
|
|
511
552
|
retry_mode: Optional[str] = None,
|
|
512
553
|
) -> Union[ClickAction, DoneAction, WaitAction]:
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
self.debug.log(f"grid {rows}x{cols} ({n} cells)")
|
|
554
|
+
# The shape is PASSED IN, already cleared by `_grid_dims`. It used to be
|
|
555
|
+
# derived here, and the derivation had an `else` branch that turned any
|
|
556
|
+
# unrecognised count into `cols = int(sqrt(n)); rows = ceil(n / cols)` —
|
|
557
|
+
# so a 12-cell false positive became a 4x3 board, got stamped with twelve
|
|
558
|
+
# numbers, and was answered with cell ids. No such board can exist in the
|
|
559
|
+
# training corpus, because the generator's own detector refuses every
|
|
560
|
+
# count but 9 and 16.
|
|
561
|
+
self.debug.log(f"grid {rows}x{cols} ({len(grid_boxes)} cells)")
|
|
522
562
|
|
|
523
563
|
cv_selected: List[int] = []
|
|
524
564
|
cv_loading: List[int] = []
|
|
@@ -56,9 +56,14 @@ class PhaseBudget:
|
|
|
56
56
|
monitor spent 31s of it" is. Always on — it is a few dict updates per phase
|
|
57
57
|
against multi-second waits — but only PRINTED under CAPTCHA_TIMINGS=1.
|
|
58
58
|
|
|
59
|
-
Phases may nest (a burst contains its screenshots)
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
Phases may nest (a burst contains its screenshots). Only the outermost of a
|
|
60
|
+
GIVEN NAME accumulates, so re-entering one cannot double-count it — but a
|
|
61
|
+
phase nested inside a differently-named one counts under both, deliberately:
|
|
62
|
+
the cursor drifting over the widget WHILE the model generates is genuinely
|
|
63
|
+
both `mouse` and `inference`, and hiding either would misreport what the
|
|
64
|
+
solve was doing. The totals are therefore an attribution, not a partition,
|
|
65
|
+
and can exceed the elapsed time. `report()` prints the residual as
|
|
66
|
+
`(unattributed)` and it can go negative when they do.
|
|
62
67
|
"""
|
|
63
68
|
|
|
64
69
|
def __init__(self) -> None:
|