captchakraken 2.8.0 → 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/dist/cli-invocation.d.ts +14 -16
- package/dist/cli-invocation.js +28 -20
- package/dist/humanize.d.ts +3 -3
- package/dist/humanize.js +3 -3
- package/dist/model-name.d.ts +1 -1
- package/dist/model-name.js +1 -1
- package/dist/solver.d.ts +202 -11
- package/dist/solver.js +690 -113
- package/dist/trajectory.d.ts +1 -1
- package/dist/trajectory.js +1 -1
- package/dist/types.d.ts +14 -2
- package/package.json +2 -1
- package/python/pyproject.toml +1 -1
- package/python/src/captchakraken/__init__.py +1 -1
- package/python/src/captchakraken/cli.py +4 -0
- package/python/src/captchakraken/humanize.py +2 -2
- package/python/src/captchakraken/keyframes.py +86 -13
- package/python/src/captchakraken/models.json +45 -19
- package/python/src/captchakraken/page_solver.py +657 -52
- package/python/src/captchakraken/planner.py +86 -7
- package/python/src/captchakraken/prompts.py +5 -5
- package/python/src/captchakraken/solver.py +2 -2
|
@@ -29,13 +29,13 @@ DEBUG = os.getenv("CAPTCHA_DEBUG", "0") == "1"
|
|
|
29
29
|
# The pixel floor the adapters are TRAINED at, and therefore the floor an image
|
|
30
30
|
# has to clear before it is worth sending.
|
|
31
31
|
#
|
|
32
|
-
# Training exports `MIN_PIXELS=200704` (448², in the
|
|
33
|
-
#
|
|
32
|
+
# Training exports `MIN_PIXELS=200704` (448², in the training repo's
|
|
33
|
+
# the training pipeline), so anything smaller is enlarged before the vision
|
|
34
34
|
# encoder sees it. Serving did none of that: vLLM runs with no
|
|
35
35
|
# `--mm-processor-kwargs` and this client used to re-encode the file unchanged,
|
|
36
36
|
# so a small captcha arrived at a geometry the model was never tuned on. It
|
|
37
37
|
# fails as plausible-but-wrong coordinates, never as an error — measured on real
|
|
38
|
-
#
|
|
38
|
+
# a GeeTest v3 slider captures (277x285), predictions landed 80-105 px from the
|
|
39
39
|
# hand label at native size and 1-4 px away once upscaled.
|
|
40
40
|
#
|
|
41
41
|
# A FLOOR, NOT A RESIZE. Images already above it are passed through byte-for-byte;
|
|
@@ -92,9 +92,9 @@ def _encode_image(path: str,
|
|
|
92
92
|
im.save(buf, "PNG")
|
|
93
93
|
return "image/png", base64.b64encode(buf.getvalue()).decode()
|
|
94
94
|
|
|
95
|
-
# Header the fleet's
|
|
95
|
+
# Header the fleet's the gateway front (on the reverse-proxy EC2) routes on. When a
|
|
96
96
|
# caller sets CAPTCHA_REQUEST_PRIORITY to a positive int, every request carries
|
|
97
|
-
# `X-JH-Priority: <n>`;
|
|
97
|
+
# `X-JH-Priority: <n>`; the gateway sends anything above its threshold (5) straight
|
|
98
98
|
# to the backup GPUs so it never competes with production traffic on the main
|
|
99
99
|
# 5090. The tier-2 CI gate sets this — its traffic is throwaway and must stay
|
|
100
100
|
# off the primary.
|
|
@@ -274,7 +274,7 @@ def video_action_prompt(n_keyframes: int) -> str:
|
|
|
274
274
|
|
|
275
275
|
The count is in the text because the model has no other way to know how many
|
|
276
276
|
images arrived — frame identities live in the prompt, not in the image payload.
|
|
277
|
-
Mirrors `instructions.video_instruction` in the
|
|
277
|
+
Mirrors `instructions.video_instruction` in the training repo exactly.
|
|
278
278
|
"""
|
|
279
279
|
n = int(n_keyframes)
|
|
280
280
|
if n < 1:
|
|
@@ -283,6 +283,66 @@ def video_action_prompt(n_keyframes: int) -> str:
|
|
|
283
283
|
return VIDEO_ACTION_PROMPT_TEMPLATE.format(n=n, listing=listing)
|
|
284
284
|
|
|
285
285
|
|
|
286
|
+
#: Sampling temperature per RE-ASK of a board the vendor refused, first entry
|
|
287
|
+
#: first. THE ONE COPY: the JS port reaches this through `CAPTCHA_RESAMPLE_LEVEL`
|
|
288
|
+
#: rather than carrying its own numbers, because a schedule written down twice is
|
|
289
|
+
#: a schedule the two ports can disagree about — and the driver gate drives both
|
|
290
|
+
#: and averages them.
|
|
291
|
+
#:
|
|
292
|
+
#: GREEDY ONLY, AND THAT IS A MEASUREMENT, NOT AN OVERSIGHT. Heating the re-ask
|
|
293
|
+
#: was tried: `(0.0, 0.35, 0.7)`, escalating each time a board came back with the
|
|
294
|
+
#: answer it had already given. On five puzzle types that a greedy driver passes,
|
|
295
|
+
#: the escalating one scored 0/5 against 4/5, and the median FAILED attempt cost
|
|
296
|
+
#: 24.0s against 13.9s. Two things went wrong and they compound:
|
|
297
|
+
#:
|
|
298
|
+
#: 1. A hotter sample is a WORSE answer. The greedy read is the best single
|
|
299
|
+
#: guess this model has; the schedule spends rounds walking away from it.
|
|
300
|
+
#: 2. It defeats the give-up rule. `_no_progress_rounds` counts IDENTICAL
|
|
301
|
+
#: answers, so making each re-ask differ resets the counter — the attempt
|
|
302
|
+
#: stops bailing at round 3 and runs to the round ceiling instead. Slower
|
|
303
|
+
#: failures, not fewer.
|
|
304
|
+
#:
|
|
305
|
+
#: The plumbing stays because the LEVEL is the right thing to travel; what was
|
|
306
|
+
#: wrong was the trigger. A re-ask should be driven by the vendor actually
|
|
307
|
+
#: refusing the answer — read off the network response — and not by the model
|
|
308
|
+
#: agreeing with itself, which is only ever evidence that decoding is
|
|
309
|
+
#: deterministic. Add temperatures here when that signal exists to gate them.
|
|
310
|
+
RESAMPLE_TEMPERATURES = (0.0,)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def sampling_for_level(level: int) -> Dict[str, Any]:
|
|
314
|
+
"""Decode overrides for the `level`-th re-ask of one board.
|
|
315
|
+
|
|
316
|
+
Level 0 is greedy and returns {}, so a first look is byte-identical to what
|
|
317
|
+
this client has always sent. Past that it carries a temperature AND a seed:
|
|
318
|
+
the temperature is what makes a second sample possible at all, and the seed
|
|
319
|
+
is what makes two re-asks at one temperature two samples rather than one
|
|
320
|
+
sample twice.
|
|
321
|
+
"""
|
|
322
|
+
temps = RESAMPLE_TEMPERATURES or (0.0,)
|
|
323
|
+
temp = temps[min(max(level, 0), len(temps) - 1)]
|
|
324
|
+
if not temp:
|
|
325
|
+
return {}
|
|
326
|
+
return {"temperature": temp, "seed": 1000 + level}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _sampling_from_env() -> Dict[str, Any]:
|
|
330
|
+
"""The JS port's re-ask level, which arrives as an env var.
|
|
331
|
+
|
|
332
|
+
That port drives its own round loop and spawns this CLI once per round, so
|
|
333
|
+
a fresh process has no memory of how many times the board has already been
|
|
334
|
+
read. Passing the LEVEL rather than the temperature keeps the schedule in
|
|
335
|
+
one place — see `RESAMPLE_TEMPERATURES`.
|
|
336
|
+
"""
|
|
337
|
+
raw = os.environ.get("CAPTCHA_RESAMPLE_LEVEL", "").strip()
|
|
338
|
+
if not raw:
|
|
339
|
+
return {}
|
|
340
|
+
try:
|
|
341
|
+
return sampling_for_level(int(raw))
|
|
342
|
+
except ValueError:
|
|
343
|
+
return {}
|
|
344
|
+
|
|
345
|
+
|
|
286
346
|
class ActionPlanner:
|
|
287
347
|
"""Thin client for the vLLM `captcha` LoRA."""
|
|
288
348
|
|
|
@@ -303,6 +363,15 @@ class ActionPlanner:
|
|
|
303
363
|
self.model = model or config.lora_name()
|
|
304
364
|
self.base_url = base_url or config.base_url()
|
|
305
365
|
self.api_key = api_key or config.api_key()
|
|
366
|
+
#: Decode overrides merged into every request payload, or empty.
|
|
367
|
+
#:
|
|
368
|
+
#: The page driver sets this while RE-ASKING a board the vendor already
|
|
369
|
+
#: refused, so that the second look is a second SAMPLE rather than the
|
|
370
|
+
#: same arithmetic run twice — see
|
|
371
|
+
#: `PageSolverConfig.resample_temperatures`. Empty by default and
|
|
372
|
+
#: cleared the moment a fresh board arrives, because greedy is the best
|
|
373
|
+
#: single guess and every solve that works today works on it.
|
|
374
|
+
self.sampling: Dict[str, Any] = {}
|
|
306
375
|
# Prompts follow the MODEL, not this client's release. Resolve by the
|
|
307
376
|
# served name first (a hosted endpoint serves `captcha`, not a repo id,
|
|
308
377
|
# and models.json maps the alias back), falling back to the adapter repo
|
|
@@ -461,6 +530,10 @@ class ActionPlanner:
|
|
|
461
530
|
]
|
|
462
531
|
|
|
463
532
|
model = self._model_for(family)
|
|
533
|
+
# `sampling` is set by the page driver when it is RE-ASKING a board the
|
|
534
|
+
# vendor already refused — see `PageSolverConfig.resample_temperatures`.
|
|
535
|
+
# Absent (the normal case, and every first look) it is greedy exactly as
|
|
536
|
+
# before, so nothing about a solve that works today changes.
|
|
464
537
|
payload = {
|
|
465
538
|
"model": model,
|
|
466
539
|
"messages": messages,
|
|
@@ -471,13 +544,19 @@ class ActionPlanner:
|
|
|
471
544
|
# level is the documented way.
|
|
472
545
|
"chat_template_kwargs": {"enable_thinking": False},
|
|
473
546
|
}
|
|
547
|
+
# In-process the driver sets `sampling` directly; the JS port drives
|
|
548
|
+
# its own loop and spawns this as a CLI, so its level arrives in the
|
|
549
|
+
# environment. Explicit beats inherited when both are present.
|
|
550
|
+
sampling = self.sampling or _sampling_from_env()
|
|
551
|
+
if sampling:
|
|
552
|
+
payload.update(sampling)
|
|
474
553
|
|
|
475
554
|
headers = {
|
|
476
555
|
"Authorization": f"Bearer {self.api_key}",
|
|
477
556
|
"Content-Type": "application/json",
|
|
478
557
|
# Fleet routing: absent unless CAPTCHA_REQUEST_PRIORITY is set (see
|
|
479
558
|
# routing_headers). Low-priority batch traffic is steered to the
|
|
480
|
-
# backup GPUs by the
|
|
559
|
+
# backup GPUs by the gateway front on this header.
|
|
481
560
|
**routing_headers(),
|
|
482
561
|
}
|
|
483
562
|
|
|
@@ -5,7 +5,7 @@ Why this exists
|
|
|
5
5
|
The prompt a captcha model is sent has to be the prompt it was trained on. When
|
|
6
6
|
they drift, nothing errors — the model answers in a schema the client does not
|
|
7
7
|
recognise and puzzles silently fail. That has happened: see failure-mode 3 in
|
|
8
|
-
the
|
|
8
|
+
the training repo's release parity gate, where the shipped prompt
|
|
9
9
|
kept asking for the legacy `output`/`simulate_drag`/PascalCase schema long after
|
|
10
10
|
the LoRA had been retrained on `action: drag`/`drags[]`.
|
|
11
11
|
|
|
@@ -48,14 +48,14 @@ _PROMPTS_FILE_ENV = "CAPTCHA_PROMPTS_FILE"
|
|
|
48
48
|
_DISABLE_FETCH_ENV = "CAPTCHA_PROMPTS_NO_FETCH"
|
|
49
49
|
|
|
50
50
|
# The newest generation this client ships built-ins for. NOT "the version to
|
|
51
|
-
# send" — that is per model, resolved below. This exists so the
|
|
51
|
+
# send" — that is per model, resolved below. This exists so the training repo
|
|
52
52
|
# can ask "do you speak the generation I am training?" without importing us.
|
|
53
53
|
LATEST_PROMPT_VERSION = "2"
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
# ── built-in templates, one entry per generation in service ─────────────────
|
|
57
57
|
#
|
|
58
|
-
# A PURE LITERAL, deliberately: the
|
|
58
|
+
# A PURE LITERAL, deliberately: the training repo's release parity gate reads
|
|
59
59
|
# it by AST without importing this package (which pulls in pydantic/cv2 that the
|
|
60
60
|
# training venv need not have). Keep it literal — an f-string, a .join(), or a
|
|
61
61
|
# reference to another constant makes it unreadable to that gate, and the gate
|
|
@@ -68,7 +68,7 @@ LATEST_PROMPT_VERSION = "2"
|
|
|
68
68
|
# 2 — adds the PUZZLE PIECE SLIDER clause and the animated + text families.
|
|
69
69
|
# Byte-identical to src/synthetic/reasoning/instructions.py
|
|
70
70
|
# ::PIXEL_INSTRUCTION_TEMPLATE, ::VIDEO_INSTRUCTION_TEMPLATE and
|
|
71
|
-
# ::TEXT_INSTRUCTION in the
|
|
71
|
+
# ::TEXT_INSTRUCTION in the training repo at PROMPT_VERSION 2.
|
|
72
72
|
#
|
|
73
73
|
# Do not edit a published generation's text. Ever. Those models are frozen and
|
|
74
74
|
# so are their prompts; a change here is a change to what an already-shipped
|
|
@@ -258,7 +258,7 @@ PRIVATE = "private"
|
|
|
258
258
|
LICENSED = "licensed"
|
|
259
259
|
#: Anything outside this set is a typo, and a typo must not read as "public".
|
|
260
260
|
#: The whole class of bug this file exists to prevent is a field that fails
|
|
261
|
-
#: open —
|
|
261
|
+
#: open — the release parity gate rejects an unknown value at release time,
|
|
262
262
|
#: and `is_licensed` below treats one as licensed rather than guessing.
|
|
263
263
|
#:
|
|
264
264
|
#: Spelled out as literals rather than `(PUBLIC, PRIVATE, LICENSED)`: the
|
|
@@ -480,7 +480,7 @@ class CaptchaSolver:
|
|
|
480
480
|
# colour is its own; a sprite board shares one board colour behind
|
|
481
481
|
# every cell, so the medians collapse onto a single value.
|
|
482
482
|
# Measured over the real captures: iconcrush and gobang 0.0-0.8,
|
|
483
|
-
# against 14.9 (
|
|
483
|
+
# against 14.9 (a reCAPTCHA 4x4 grid) up to 93.0 (a GeeTest v4 icon board)
|
|
484
484
|
# for the five true grid families. The threshold sits in an 18x
|
|
485
485
|
# gap, which is why it is a fixed number rather than a tuned one.
|
|
486
486
|
medians = []
|
|
@@ -529,7 +529,7 @@ class CaptchaSolver:
|
|
|
529
529
|
# ...plus contiguity 40/1748 2.29% 0/269 0.00%
|
|
530
530
|
#
|
|
531
531
|
# It cost 38 real grids and caught nothing the two checks above did
|
|
532
|
-
# not already catch — 19 of those were
|
|
532
|
+
# not already catch — 19 of those were a reCAPTCHA 4x4 grid alone,
|
|
533
533
|
# 5.5% of that family, every one of them sent to the click path to
|
|
534
534
|
# be asked for coordinates on a board answered with cell numbers.
|
|
535
535
|
# Nothing errors when that happens; the reply is simply graded
|