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.
@@ -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 finetune repo's
33
- # scripts/train_unified.sh), so anything smaller is enlarged before the vision
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
- # geetest_v3_slide captures (277x285), predictions landed 80-105 px from the
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 haproxy front (on the reverse-proxy EC2) routes on. When a
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>`; haproxy sends anything above its threshold (5) straight
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 finetune repo exactly.
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 haproxy front on this header.
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 finetune repo's `scripts/check_prompt_parity.py`, where the shipped prompt
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 finetune repo
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 finetune repo's check_prompt_parity.py reads
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 finetune repo at PROMPT_VERSION 2.
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 — `check_prompt_parity.py` rejects an unknown value at release time,
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 (recaptcha_grid_4x4) up to 93.0 (geetest_v4_nine)
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 recaptcha_grid_4x4 alone,
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