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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Reduce a recorded captcha clip to the few frames the model is shown.
|
|
2
2
|
|
|
3
3
|
VERBATIM PORT. Everything below the docstring is byte-identical to
|
|
4
|
-
|
|
5
|
-
`scripts/
|
|
4
|
+
the training-side extractor in the training repo, and
|
|
5
|
+
`scripts/the parity gate` there fails the build if the two ever diverge.
|
|
6
6
|
|
|
7
7
|
That is not tidiness, it is a correctness requirement. The model is trained on
|
|
8
8
|
keyframes cut by that code and answers with a frame NUMBER indexing into them. If
|
|
@@ -41,7 +41,7 @@ DEFAULT_MAX_KEYFRAMES = 6
|
|
|
41
41
|
# picture". A false "same" merges two distinct states, which loses an answer, so
|
|
42
42
|
# this is the most consequential number in the file.
|
|
43
43
|
#
|
|
44
|
-
# MEASURED, not guessed. Over all 20 real clips in
|
|
44
|
+
# MEASURED, not guessed. Over all 20 real clips in the sample corpus, the
|
|
45
45
|
# consecutive-frame differences fall into two clusters with a 64x gap:
|
|
46
46
|
#
|
|
47
47
|
# same picture (compression + antialiasing) <= 0.000067 (0.067 per mille)
|
|
@@ -162,6 +162,15 @@ class KeyframeSet:
|
|
|
162
162
|
# `keyframes`). Empty for the other modes. Lets a caller answer "which
|
|
163
163
|
# keyframe was the clip showing at source frame i?" without re-diffing.
|
|
164
164
|
frame_states: List[int] = field(default_factory=list)
|
|
165
|
+
# How many distinct steady screens the clip sits on; 0 if it is not that
|
|
166
|
+
# kind of clip. INDEPENDENT OF `mode`, and the difference is the whole
|
|
167
|
+
# point: `mode` says what the slicer could PROVE about recurrence from one
|
|
168
|
+
# burst, this says what the board looks like. A three-screen captcha whose
|
|
169
|
+
# loop is longer than the recording is `even` with `steady_screens == 3`.
|
|
170
|
+
# The live driver reads this to decide whether waiting for a named screen
|
|
171
|
+
# is meaningful; reading `mode` for that put the wait off on 100% of real
|
|
172
|
+
# animated captchas. See `steady_screens()`.
|
|
173
|
+
steady_screens: int = 0
|
|
165
174
|
|
|
166
175
|
def __len__(self) -> int:
|
|
167
176
|
return len(self.keyframes)
|
|
@@ -226,6 +235,7 @@ class KeyframeSet:
|
|
|
226
235
|
return {
|
|
227
236
|
"stem": stem,
|
|
228
237
|
"mode": self.mode,
|
|
238
|
+
"steady_screens": self.steady_screens,
|
|
229
239
|
"fps": self.fps,
|
|
230
240
|
"source_frames": self.source_frames,
|
|
231
241
|
"params": self.params.as_dict(),
|
|
@@ -440,14 +450,22 @@ def _distinct_indices(
|
|
|
440
450
|
return sorted(kept)
|
|
441
451
|
|
|
442
452
|
|
|
443
|
-
def
|
|
453
|
+
def _steady_screens(
|
|
444
454
|
frames: Sequence[np.ndarray], params: KeyframeParams
|
|
445
|
-
) -> Optional[Tuple[List[int], List[int]]]:
|
|
446
|
-
"""`(state_medoid_indices,
|
|
447
|
-
pictures, else None.
|
|
455
|
+
) -> Optional[Tuple[List[Tuple[int, int]], List[int], List[int]]]:
|
|
456
|
+
"""`(holds, state_medoid_indices, state_per_hold)` if the clip is a set of
|
|
457
|
+
steady held pictures, else None.
|
|
448
458
|
|
|
449
|
-
`
|
|
450
|
-
|
|
459
|
+
Split out of `_detect_cycle` because the answer is wanted TWICE and the two
|
|
460
|
+
questions are not the same one:
|
|
461
|
+
|
|
462
|
+
- "is this a cycle?" — needs a state to be seen coming BACK, which takes
|
|
463
|
+
more than one pass through the loop.
|
|
464
|
+
- "does this board sit on a few stable screens?" — needs only one pass,
|
|
465
|
+
and is what the live driver has to know in order to decide whether
|
|
466
|
+
waiting for a particular screen is meaningful.
|
|
467
|
+
|
|
468
|
+
Answering only the first left the second unasked. See `steady_screens`.
|
|
451
469
|
"""
|
|
452
470
|
n = len(frames)
|
|
453
471
|
runs = _anchor_runs(frames, params.steady_ratio)
|
|
@@ -479,6 +497,55 @@ def _detect_cycle(
|
|
|
479
497
|
if len(state_reps) < 1:
|
|
480
498
|
return None
|
|
481
499
|
|
|
500
|
+
# NO `distinct_ratio` HERE, deliberately. The merge above already separated
|
|
501
|
+
# these states by more than `steady_ratio`, which is the measured noise
|
|
502
|
+
# floor — that is the whole of "are these different pictures".
|
|
503
|
+
#
|
|
504
|
+
# `distinct_ratio` (0.02) is twenty times coarser and exists to license
|
|
505
|
+
# `cycle`'s COLLAPSE to one frame per state, where being too generous throws
|
|
506
|
+
# away the middle of a clip. Applying it here instead answers a question
|
|
507
|
+
# nobody asked and answers it wrongly: measured on a live GeeTest svg burst,
|
|
508
|
+
# the three screens are separated by 0.0056 and 0.0050 — thin line art, a
|
|
509
|
+
# few glyph strokes change — so a 0.02 bar reports a board with three
|
|
510
|
+
# obvious screens as having none. keyframes.py's own comment already said
|
|
511
|
+
# so ("real state separations top out around 0.007, well under this") and
|
|
512
|
+
# read it as evidence the check was harmless.
|
|
513
|
+
return holds, state_reps, hold_state
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def steady_screens(frames: Sequence[np.ndarray], params: KeyframeParams) -> int:
|
|
517
|
+
"""How many distinct steady screens this clip sits on. 0 if it does not.
|
|
518
|
+
|
|
519
|
+
THE LIVE DRIVER'S QUESTION, and it is not `mode`. A board that advances
|
|
520
|
+
through three held pictures gives a burst that contains ONE pass, so
|
|
521
|
+
`_detect_cycle` cannot prove it recurs and the clip is sliced `even` — which
|
|
522
|
+
is right for slicing (dedup reaches the same stills) and was being read by
|
|
523
|
+
the driver as "nothing recurs, do not wait for a screen". Every real
|
|
524
|
+
geetest_v4_svg clip is exactly this shape: 2-3 holds covering 95-100% of the
|
|
525
|
+
burst, mutually distinct, and the page absolutely does come back around.
|
|
526
|
+
|
|
527
|
+
A one-way animation — the case the no-wait path exists for — is nearly all
|
|
528
|
+
transition, so it has no qualifying holds and this returns 0.
|
|
529
|
+
"""
|
|
530
|
+
got = _steady_screens(frames, params)
|
|
531
|
+
return len(got[1]) if got else 0
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _detect_cycle(
|
|
535
|
+
frames: Sequence[np.ndarray], params: KeyframeParams
|
|
536
|
+
) -> Optional[Tuple[List[int], List[int]]]:
|
|
537
|
+
"""`(state_medoid_indices, per_frame_state)` if the clip is a cycle of steady
|
|
538
|
+
pictures, else None.
|
|
539
|
+
|
|
540
|
+
`per_frame_state[i]` is the 0-based state of source frame i, or -1 for a
|
|
541
|
+
transition frame that belongs to no hold.
|
|
542
|
+
"""
|
|
543
|
+
n = len(frames)
|
|
544
|
+
got = _steady_screens(frames, params)
|
|
545
|
+
if got is None:
|
|
546
|
+
return None
|
|
547
|
+
holds, state_reps, hold_state = got
|
|
548
|
+
|
|
482
549
|
# A cycle REVISITS. More holds than states means some picture came back, which
|
|
483
550
|
# is what "the video is just a shift between N steady images" describes. Equal
|
|
484
551
|
# counts mean every hold was a new picture — a one-way progression (a slow
|
|
@@ -497,8 +564,10 @@ def _detect_cycle(
|
|
|
497
564
|
if len(holds) <= len(state_reps) and len(state_reps) > 1:
|
|
498
565
|
return None
|
|
499
566
|
|
|
500
|
-
# Every pair of states must be clearly different
|
|
501
|
-
#
|
|
567
|
+
# Every pair of states must be clearly different before the clip is
|
|
568
|
+
# COLLAPSED to one frame per state — that collapse is what this threshold
|
|
569
|
+
# licenses, and being too generous with it discards the middle of a clip.
|
|
570
|
+
# `steady_screens` deliberately does not apply it; see `_steady_screens`.
|
|
502
571
|
for a in range(len(state_reps)):
|
|
503
572
|
for b in range(a + 1, len(state_reps)):
|
|
504
573
|
if frame_diff_ratio(frames[state_reps[a]], frames[state_reps[b]]) < params.distinct_ratio:
|
|
@@ -511,6 +580,96 @@ def _detect_cycle(
|
|
|
511
580
|
return state_reps, per_frame
|
|
512
581
|
|
|
513
582
|
|
|
583
|
+
def _drop_smeared(
|
|
584
|
+
frames: Sequence[np.ndarray], indices: Sequence[int], params: KeyframeParams
|
|
585
|
+
) -> List[int]:
|
|
586
|
+
"""Drop an index caught BETWEEN two holds, when both of them are already
|
|
587
|
+
represented in the cut.
|
|
588
|
+
|
|
589
|
+
These clips hold a picture, swap, hold the next. An evenly spaced sample can
|
|
590
|
+
land on the instant of the swap — a board part-way between two states, which
|
|
591
|
+
is a picture the puzzle never actually shows. If the model already has the
|
|
592
|
+
board before the swap AND the board after it, that frame adds nothing but a
|
|
593
|
+
smear; if either endpoint is missing, it is the only thing standing in for a
|
|
594
|
+
state and must stay.
|
|
595
|
+
|
|
596
|
+
THE MACHINERY ALREADY EXISTED AND THIS PATH NEVER ASKED IT. `_anchor_runs`
|
|
597
|
+
plus `min_hold_frames` is how `_detect_cycle` separates holds from
|
|
598
|
+
transitions (it labels the latter -1 in `frame_states`), and
|
|
599
|
+
`min_hold_frames` had exactly one reader — inside `_detect_cycle`. On the
|
|
600
|
+
`even` path the parameter was declared, serialised into every manifest, and
|
|
601
|
+
inert.
|
|
602
|
+
|
|
603
|
+
WHY THE TEST IS "BOTH NEIGHBOURING HOLDS ARE KEPT" AND NOT A PIXEL DIFF.
|
|
604
|
+
Two earlier versions were measured against both trees and both were wrong:
|
|
605
|
+
|
|
606
|
+
* *Snap to the nearest held frame* turned `[0, 16, 39]` into `[0, 16]` on
|
|
607
|
+
six eval clips, dropping a SCREEN. A page that never reaches the model is
|
|
608
|
+
unanswerable whenever the target lived on it.
|
|
609
|
+
* *Drop when some other still is within `distinct_ratio`* cut
|
|
610
|
+
an hCaptcha tile-flip animation from 6 stills to 3, because two boards of that
|
|
611
|
+
puzzle differ by ONE TILE — well inside `distinct_ratio` — so a diff
|
|
612
|
+
threshold cannot tell "the same picture again" from "the next board".
|
|
613
|
+
EXPECTED_SLICING says it directly: fewer than 6 "means dedup merged two
|
|
614
|
+
boards that differ only by the tile mid-flip, and that tile is frequently
|
|
615
|
+
the answer".
|
|
616
|
+
|
|
617
|
+
Anchoring on the HOLDS either side asks the question that actually matters —
|
|
618
|
+
does the model already see both ends of this swap — and needs no threshold of
|
|
619
|
+
its own.
|
|
620
|
+
|
|
621
|
+
A clip with no holds at all (a rotation, a continuous fade, anything in
|
|
622
|
+
permanent motion) has no transitions to speak of and is returned untouched.
|
|
623
|
+
"""
|
|
624
|
+
runs = _anchor_runs(frames, params.steady_ratio)
|
|
625
|
+
holds = [(s, e) for (s, e) in runs if (e - s + 1) >= params.min_hold_frames]
|
|
626
|
+
if not holds:
|
|
627
|
+
return list(indices) # nothing holds; nothing is a smear
|
|
628
|
+
|
|
629
|
+
# Holds showing the same picture are ONE state — the same merge
|
|
630
|
+
# `_detect_cycle` does, at the same threshold. It matters here because a
|
|
631
|
+
# cycle revisits: on a real `icon_2x2` clip the hold at 53..74 is the
|
|
632
|
+
# opening board coming back, so the still at index 0 already covers it.
|
|
633
|
+
# Counting holds instead of states kept a smear whose neighbouring picture
|
|
634
|
+
# was on screen the whole time.
|
|
635
|
+
state_of_hold: List[int] = []
|
|
636
|
+
reps: List[int] = []
|
|
637
|
+
for start, end in holds:
|
|
638
|
+
rep = _medoid(frames, list(range(start, end + 1)))
|
|
639
|
+
for k, existing in enumerate(reps):
|
|
640
|
+
if frame_diff_ratio(frames[rep], frames[existing]) <= params.steady_ratio:
|
|
641
|
+
state_of_hold.append(k)
|
|
642
|
+
break
|
|
643
|
+
else:
|
|
644
|
+
reps.append(rep)
|
|
645
|
+
state_of_hold.append(len(reps) - 1)
|
|
646
|
+
|
|
647
|
+
def hold_of(i: int) -> Optional[int]:
|
|
648
|
+
for k, (s, e) in enumerate(holds):
|
|
649
|
+
if s <= i <= e:
|
|
650
|
+
return k
|
|
651
|
+
return None
|
|
652
|
+
|
|
653
|
+
covered = {state_of_hold[h] for h in (hold_of(i) for i in indices)
|
|
654
|
+
if h is not None}
|
|
655
|
+
kept: List[int] = []
|
|
656
|
+
for i in indices:
|
|
657
|
+
if hold_of(i) is not None:
|
|
658
|
+
kept.append(i)
|
|
659
|
+
continue
|
|
660
|
+
before = max((k for k, (s, e) in enumerate(holds) if e < i), default=None)
|
|
661
|
+
after = min((k for k, (s, e) in enumerate(holds) if s > i), default=None)
|
|
662
|
+
# Only a swap BETWEEN two kept holds is redundant. A transition at the
|
|
663
|
+
# very start or end of a clip has an endpoint that was never recorded,
|
|
664
|
+
# so it is evidence of a state and stays.
|
|
665
|
+
if before is not None and after is not None \
|
|
666
|
+
and state_of_hold[before] in covered \
|
|
667
|
+
and state_of_hold[after] in covered:
|
|
668
|
+
continue
|
|
669
|
+
kept.append(i)
|
|
670
|
+
return kept
|
|
671
|
+
|
|
672
|
+
|
|
514
673
|
def extract_keyframes(
|
|
515
674
|
frames: Sequence[np.ndarray],
|
|
516
675
|
*,
|
|
@@ -535,6 +694,7 @@ def extract_keyframes(
|
|
|
535
694
|
reps, per_frame = cycle
|
|
536
695
|
mode = "static" if len(reps) == 1 else "cycle"
|
|
537
696
|
return KeyframeSet(
|
|
697
|
+
steady_screens=len(reps),
|
|
538
698
|
mode=mode,
|
|
539
699
|
keyframes=[
|
|
540
700
|
Keyframe(number=k + 1, source_index=idx, timestamp_ms=_ms(idx),
|
|
@@ -548,13 +708,25 @@ def extract_keyframes(
|
|
|
548
708
|
)
|
|
549
709
|
|
|
550
710
|
indices = _even_indices(n, p.max_keyframes)
|
|
711
|
+
# BEFORE dedup, so a smear does not spend budget a real state could use...
|
|
712
|
+
indices = _drop_smeared(frames, indices, p)
|
|
551
713
|
if p.dedupe:
|
|
552
714
|
# Applied to `even` only. `cycle` states are distinct by construction (the
|
|
553
715
|
# merge in `_detect_cycle` uses this same threshold) and `static` is one
|
|
554
716
|
# frame, so there is nothing to remove; and dropping a cycle keyframe here
|
|
555
717
|
# would desynchronise `frame_states`, which indexes keyframes by position.
|
|
556
718
|
indices = _distinct_indices(frames, indices, p)
|
|
719
|
+
# ...AND AFTER, because `_distinct_indices` BACKFILLS. Dedup frees
|
|
720
|
+
# budget and the backfill walks the clip for anything not already
|
|
721
|
+
# represented — which is exactly what a smeared frame is, so it gets
|
|
722
|
+
# picked straight back up. Measured on a 3-hold fixture: filtering only
|
|
723
|
+
# before dedup returned [0, 10, 12, 21, 25] with 10 and 21 the two
|
|
724
|
+
# smears the first pass had just removed.
|
|
725
|
+
indices = _drop_smeared(frames, indices, p)
|
|
557
726
|
return KeyframeSet(
|
|
727
|
+
# Asked even though the cycle test refused: the two questions differ,
|
|
728
|
+
# and this is the one the driver needs answered.
|
|
729
|
+
steady_screens=steady_screens(frames, p),
|
|
558
730
|
mode="even",
|
|
559
731
|
keyframes=[
|
|
560
732
|
Keyframe(number=k + 1, source_index=idx, timestamp_ms=_ms(idx),
|
|
@@ -580,7 +752,7 @@ def extract_keyframes_from_video(
|
|
|
580
752
|
# <media_dir>/keyframes/<stem>/keyframes.json
|
|
581
753
|
#
|
|
582
754
|
# Derived, regenerable, and ADDITIVE — the clip stays canonical next to it. That
|
|
583
|
-
# matters most under
|
|
755
|
+
# matters most under the held-out sample corpus, which is irreplaceable hand-labeled
|
|
584
756
|
# data we never rewrite (CLAUDE.md § Data trees): extraction only ever creates a
|
|
585
757
|
# new sibling directory there.
|
|
586
758
|
|
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
"Why a map and not a constant: a model answers in whatever schema its prompt",
|
|
7
7
|
"asks for. Send a version-1 model the version-2 prompt and nothing errors —",
|
|
8
8
|
"it just answers worse, on every puzzle, silently. That is exactly the",
|
|
9
|
-
"2026-07-18 regression
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"been retrained on action: drag / drags[].",
|
|
9
|
+
"2026-07-18 regression, where the shipped prompt kept asking for the legacy",
|
|
10
|
+
"output/simulate_drag/PascalCase schema long after the LoRA had been",
|
|
11
|
+
"retrained on action: drag / drags[].",
|
|
13
12
|
"",
|
|
14
13
|
"Hardcoding one generation makes that inevitable the moment a new model",
|
|
15
14
|
"trains on different prompts: updating the constants breaks every already-",
|
|
@@ -27,10 +26,9 @@
|
|
|
27
26
|
"never see `latest`, so they are not silently moved onto prompts they cannot",
|
|
28
27
|
"produce.",
|
|
29
28
|
"",
|
|
30
|
-
"Adding a model: add its entry here IN THE SAME COMMIT as publishing it.
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"cannot reach a release.",
|
|
29
|
+
"Adding a model: add its entry here IN THE SAME COMMIT as publishing it. A",
|
|
30
|
+
"release gate fails if any registered model names a prompt_version this",
|
|
31
|
+
"client ships no built-ins for, so an unmapped model cannot reach a release.",
|
|
34
32
|
"",
|
|
35
33
|
"`pixel_budget` is the same idea for RESOLUTION. Qwen reads MIN_PIXELS /",
|
|
36
34
|
"MAX_PIXELS from the environment and clamps each image's AREA into that band,",
|
|
@@ -47,7 +45,80 @@
|
|
|
47
45
|
"",
|
|
48
46
|
"Omit it and the client keeps its historical behaviour (floor at 448 squared,",
|
|
49
47
|
"no ceiling). Absent means UNKNOWN, not zero — set it only where it has been",
|
|
50
|
-
"measured or the training run is known."
|
|
48
|
+
"measured or the training run is known.",
|
|
49
|
+
"",
|
|
50
|
+
"`availability` says whether the WEIGHTS are obtainable. Two values:",
|
|
51
|
+
" \"public\" — on the Hub; anyone may download and self-host it. This is",
|
|
52
|
+
" the default when the field is absent, so every entry that",
|
|
53
|
+
" predates the field keeps its meaning exactly.",
|
|
54
|
+
" \"private\" — ON THE HUB, in a PRIVATE repo. The bytes exist and",
|
|
55
|
+
" `captchakraken fetch` pulls them; the Hub answers 401 to",
|
|
56
|
+
" everyone whose token this account has not authorised. That",
|
|
57
|
+
" refusal belongs to the Hub, not to us, so the client does",
|
|
58
|
+
" NOT pre-empt it — doing so would stop our own admin token",
|
|
59
|
+
" from fetching weights it is entitled to.",
|
|
60
|
+
"",
|
|
61
|
+
" It is NOT a softer `licensed`, and the difference is what an",
|
|
62
|
+
" authorised holder can do: a licensed model has nothing to",
|
|
63
|
+
" download however good your credentials are. `latest` may",
|
|
64
|
+
" never be private either — an unpinned self-hoster would get",
|
|
65
|
+
" a 401 instead of a model — and the release parity gate says so.",
|
|
66
|
+
"",
|
|
67
|
+
" \"licensed\" — NOT downloadable by anyone. Reachable only through",
|
|
68
|
+
" api.captchakraken.com, or by a holder of a licence we issue.",
|
|
69
|
+
" There is no Hub repo to point CAPTCHA_LORA_ADAPTER at, and",
|
|
70
|
+
" `captchakraken fetch` refuses rather than 404ing at the Hub.",
|
|
71
|
+
"",
|
|
72
|
+
"Why it lives HERE and not only in a policy doc: this file is already the one",
|
|
73
|
+
"place that decides what an unpinned client downloads and what prompts it",
|
|
74
|
+
"sends. A second list of 'models you may not publish' kept somewhere else",
|
|
75
|
+
"would be a list that drifts, and the drift is silent in the direction that",
|
|
76
|
+
"costs the most. The publisher reads this field and REFUSES",
|
|
77
|
+
"to upload a licensed model to the Hub; the release parity gate refuses to let",
|
|
78
|
+
"`latest` be one, because an unpinned self-hoster would resolve to weights",
|
|
79
|
+
"they cannot obtain.",
|
|
80
|
+
"",
|
|
81
|
+
"`experts` is the fourth axis, and the newest. A ROUTED model is not one",
|
|
82
|
+
"adapter: Abyss is four LoRAs behind one endpoint, and the thing that picks",
|
|
83
|
+
"between them is the PROMPT FAMILY the request is about to send — grid,",
|
|
84
|
+
"pixel, video, text. See docs/MOE_LORA_DESIGN.md §11: the router is the",
|
|
85
|
+
"prompt family and it already exists, so no DOM access, no image classifier",
|
|
86
|
+
"and no cooperation from the caller is needed to reach the right expert.",
|
|
87
|
+
"",
|
|
88
|
+
"It lives here for the same reason prompt_version does. WHICH EXPERTS EXIST",
|
|
89
|
+
"IS A FACT ABOUT THE MODEL. A client that hardcoded four names would send",
|
|
90
|
+
"them to v1.2 — which serves one adapter under one name — and every request",
|
|
91
|
+
"would 404 on a model the endpoint has never heard of. ABSENT MEANS NOT",
|
|
92
|
+
"ROUTED, and that is the whole backwards-compatibility story: every model",
|
|
93
|
+
"published so far declares no `experts`, so the client sends the single",
|
|
94
|
+
"`lora_name` on every request exactly as it did before this field existed.",
|
|
95
|
+
"",
|
|
96
|
+
"An unmapped or unrecognised family degrades to the model's own",
|
|
97
|
+
"`lora_name` — the GENERALIST — never to another expert and never to an",
|
|
98
|
+
"error. The day the generation-2 `text` family reached ckgate without a",
|
|
99
|
+
"marker, refusing it cost every distorted-text solve in production for a",
|
|
100
|
+
"day; a caller prompting in their own words is the expected case for a",
|
|
101
|
+
"shipped model, not a 400.",
|
|
102
|
+
"",
|
|
103
|
+
"Each expert name must resolve — through `served_aliases` or as a repo id —",
|
|
104
|
+
"to a REGISTERED entry declaring the same `prompt_version` and the same",
|
|
105
|
+
"`pixel_budget` as the model that routes to it. The release parity gate",
|
|
106
|
+
"enforces that. It is what makes aliasing four arms onto one entry safe",
|
|
107
|
+
"rather than convenient: the four share a base, a schema and a band today",
|
|
108
|
+
"(measured, not assumed — runs 20260906-0142xx all carry prompt generation",
|
|
109
|
+
"2 and a flat 518400 band), and the day one of them trains at a different",
|
|
110
|
+
"generation or a different band the gate refuses the alias and that arm has",
|
|
111
|
+
"to graduate to an entry of its own. A divergence that resolved silently",
|
|
112
|
+
"through a shared entry would be the 2026-07-18 regression with four models",
|
|
113
|
+
"in it instead of one.",
|
|
114
|
+
"",
|
|
115
|
+
"The four arms are on the wire training as this is written (runs",
|
|
116
|
+
"20260906-0142xx) and none of them is published yet, so the `experts` map",
|
|
117
|
+
"below names weights that do not exist. That is the same deliberate",
|
|
118
|
+
"arming as `Abyss` itself — registered before it existed so the licence",
|
|
119
|
+
"gates were armed rather than retrofitted — and it costs nothing, because",
|
|
120
|
+
"an expert name only ever reaches the wire for a caller who asked for a",
|
|
121
|
+
"routed model in the first place."
|
|
51
122
|
],
|
|
52
123
|
"latest": "CaptchaKraken/CaptchaKraken-Lora-v1.2",
|
|
53
124
|
"models": {
|
|
@@ -61,7 +132,7 @@
|
|
|
61
132
|
"min": 518400,
|
|
62
133
|
"max": 518400
|
|
63
134
|
},
|
|
64
|
-
"note": "
|
|
135
|
+
"note": "Generation 2, and the first published one. Trained over a range of input sizes but served at a flat 720^2: floor == ceiling, so every image arrives at exactly one size and the band cannot drift. The band was chosen by sweeping the serving floor and scoring each setting; the plateau is 704-736^2 and it falls off above 768^2, where a portrait capture runs well past the vision tower's position grid. Note that the vision tower's native grid is NOT the best setting — mild position-embedding extrapolation over real pixels beats staying inside the grid."
|
|
65
136
|
},
|
|
66
137
|
"CaptchaKraken/Twilight-v1.2-FP8": {
|
|
67
138
|
"prompt_version": "2",
|
|
@@ -71,7 +142,7 @@
|
|
|
71
142
|
"min": 518400,
|
|
72
143
|
"max": 518400
|
|
73
144
|
},
|
|
74
|
-
"note": "CaptchaKraken-Lora-v1.2 merged into Qwen/Qwen3.5-9B at FP8-dynamic.
|
|
145
|
+
"note": "CaptchaKraken-Lora-v1.2 merged into Qwen/Qwen3.5-9B at FP8-dynamic. Generation 2, same pixel band as the adapter it is made from — a merge that resolved to a different prompt generation or a different band than its own LoRA is exactly the mispairing this file exists to prevent."
|
|
75
146
|
},
|
|
76
147
|
"CaptchaKraken/Sunlight-v1.2-AWQ-4bit": {
|
|
77
148
|
"prompt_version": "2",
|
|
@@ -89,7 +160,7 @@
|
|
|
89
160
|
"base_model": "RedHatAI/Qwen3.5-9B-FP8-dynamic",
|
|
90
161
|
"lora_revision": "main",
|
|
91
162
|
"lora_name": "captcha",
|
|
92
|
-
"note": "
|
|
163
|
+
"note": "Generation 1. Private on the Hub, so prompts.json cannot be fetched for it — this entry is the only thing that maps it to a prompt generation."
|
|
93
164
|
},
|
|
94
165
|
"CaptchaKraken/CaptchaKrakenV1_Lora": {
|
|
95
166
|
"prompt_version": "1",
|
|
@@ -97,24 +168,115 @@
|
|
|
97
168
|
"base_model": "RedHatAI/Qwen3.5-9B-FP8-dynamic",
|
|
98
169
|
"lora_revision": "main",
|
|
99
170
|
"lora_name": "captcha",
|
|
100
|
-
"note": "
|
|
171
|
+
"note": "Generation-1 LoRA on Qwen3.5-9B. Superseded by v1.2; kept registered so an existing pin keeps resolving to the prompts it was trained on."
|
|
101
172
|
},
|
|
102
173
|
"CaptchaKraken/Sunlight-AWQ-4bit": {
|
|
103
174
|
"prompt_version": "1",
|
|
104
175
|
"kind": "merged",
|
|
105
176
|
"lora_name": "captcha",
|
|
106
|
-
"note": "
|
|
177
|
+
"note": "Generation 1 merged into the base model at AWQ 4-bit."
|
|
107
178
|
},
|
|
108
179
|
"CaptchaKraken/Twilight-FP8": {
|
|
109
180
|
"prompt_version": "1",
|
|
110
181
|
"kind": "merged",
|
|
111
182
|
"lora_name": "captcha",
|
|
112
|
-
"note": "
|
|
183
|
+
"note": "Generation 1 merged into the base model at FP8-dynamic."
|
|
184
|
+
},
|
|
185
|
+
"CaptchaKraken/Abyss": {
|
|
186
|
+
"availability": "licensed",
|
|
187
|
+
"prompt_version": "2",
|
|
188
|
+
"kind": "lora",
|
|
189
|
+
"base_model": "Qwen/Qwen3.5-9B",
|
|
190
|
+
"lora_name": "abyss-general",
|
|
191
|
+
"pixel_budget": {
|
|
192
|
+
"min": 518400,
|
|
193
|
+
"max": 518400
|
|
194
|
+
},
|
|
195
|
+
"experts": {
|
|
196
|
+
"grid": "abyss-grid",
|
|
197
|
+
"pixel": "abyss-general",
|
|
198
|
+
"video": "abyss-video",
|
|
199
|
+
"text": "abyss-text"
|
|
200
|
+
},
|
|
201
|
+
"note": "A routed mixture of four expert LoRAs on Qwen3.5-9B — grid, pixel, video and text — selected by prompt family. Hosted only: `availability` refuses the weights, and being nameable is not being downloadable. A licence holder does name this model, which is why it and each of its arms has a served_aliases entry. `lora_name` is `abyss-general` rather than a fifth name because the generalist IS the fallback for any family `experts` does not map, so the two point at one adapter and the mixture needs one fewer resident LoRA slot."
|
|
202
|
+
},
|
|
203
|
+
"CaptchaKraken/Abyss-27B": {
|
|
204
|
+
"availability": "private",
|
|
205
|
+
"prompt_version": "2",
|
|
206
|
+
"kind": "lora",
|
|
207
|
+
"base_model": "Qwen/Qwen3.8-27B",
|
|
208
|
+
"lora_revision": "main",
|
|
209
|
+
"lora_name": "abyss-27b",
|
|
210
|
+
"pixel_budget": {
|
|
211
|
+
"min": 518400,
|
|
212
|
+
"max": 518400
|
|
213
|
+
},
|
|
214
|
+
"note": "A single-adapter Qwen3.8-27B. NOT the routed Abyss and not one of its arms — the base model is what a self-hoster loads, so it could never share that entry. Archived rather than served."
|
|
215
|
+
},
|
|
216
|
+
"CaptchaKraken/Abyss-grid": {
|
|
217
|
+
"availability": "private",
|
|
218
|
+
"prompt_version": "2",
|
|
219
|
+
"kind": "lora",
|
|
220
|
+
"base_model": "Qwen/Qwen3.5-9B",
|
|
221
|
+
"lora_revision": "main",
|
|
222
|
+
"lora_name": "abyss-grid",
|
|
223
|
+
"pixel_budget": {
|
|
224
|
+
"min": 518400,
|
|
225
|
+
"max": 518400
|
|
226
|
+
},
|
|
227
|
+
"note": "The 'grid' expert arm of the routed CaptchaKraken/Abyss. It has its own entry rather than an alias onto the router because it has its own weights at its own repo id, and `experts` names what goes on the wire. It declares the router's prompt generation and pixel band, which is what the parity gate requires of every arm and what makes the mixture coherent."
|
|
228
|
+
},
|
|
229
|
+
"CaptchaKraken/Abyss-text": {
|
|
230
|
+
"availability": "private",
|
|
231
|
+
"prompt_version": "2",
|
|
232
|
+
"kind": "lora",
|
|
233
|
+
"base_model": "Qwen/Qwen3.5-9B",
|
|
234
|
+
"lora_revision": "main",
|
|
235
|
+
"lora_name": "abyss-text",
|
|
236
|
+
"pixel_budget": {
|
|
237
|
+
"min": 518400,
|
|
238
|
+
"max": 518400
|
|
239
|
+
},
|
|
240
|
+
"note": "The 'text' expert arm of the routed CaptchaKraken/Abyss. Own entry, own weights, and it declares the router's prompt generation and pixel band — see CaptchaKraken/Abyss-grid."
|
|
241
|
+
},
|
|
242
|
+
"CaptchaKraken/Abyss-video": {
|
|
243
|
+
"availability": "private",
|
|
244
|
+
"prompt_version": "2",
|
|
245
|
+
"kind": "lora",
|
|
246
|
+
"base_model": "Qwen/Qwen3.5-9B",
|
|
247
|
+
"lora_revision": "main",
|
|
248
|
+
"lora_name": "abyss-video",
|
|
249
|
+
"pixel_budget": {
|
|
250
|
+
"min": 518400,
|
|
251
|
+
"max": 518400
|
|
252
|
+
},
|
|
253
|
+
"note": "The 'video' expert arm of the routed CaptchaKraken/Abyss. Own entry, own weights, and it declares the router's prompt generation and pixel band — see CaptchaKraken/Abyss-grid."
|
|
254
|
+
},
|
|
255
|
+
"CaptchaKraken/Abyss-general": {
|
|
256
|
+
"availability": "private",
|
|
257
|
+
"prompt_version": "2",
|
|
258
|
+
"kind": "lora",
|
|
259
|
+
"base_model": "Qwen/Qwen3.5-9B",
|
|
260
|
+
"lora_revision": "main",
|
|
261
|
+
"lora_name": "abyss-general",
|
|
262
|
+
"pixel_budget": {
|
|
263
|
+
"min": 518400,
|
|
264
|
+
"max": 518400
|
|
265
|
+
},
|
|
266
|
+
"note": "The 'pixel' expert arm of the routed CaptchaKraken/Abyss and its generalist fallback: the router's own `lora_name` names this entry. Own entry, own weights, and it declares the router's prompt generation and pixel band — see CaptchaKraken/Abyss-grid."
|
|
113
267
|
}
|
|
114
268
|
},
|
|
115
269
|
"served_aliases": {
|
|
116
|
-
"_comment": "A hosted endpoint serves a bare name, not a repo id. Map it back so prompt resolution works for callers who only ever see the alias.",
|
|
117
|
-
"captcha": "CaptchaKraken/
|
|
118
|
-
"captcha-v12": "CaptchaKraken/CaptchaKraken-Lora-v1.2"
|
|
270
|
+
"_comment": "A hosted endpoint serves a bare name, not a repo id. Map it back so prompt resolution works for callers who only ever see the alias. The hosted API serves one model behind the permanent public name `captcha`, whatever the caller sends, so `captcha` means whichever adapter is current there. Self-hosters resolve by repo id via CAPTCHA_LORA_ADAPTER and are unaffected. Move this in the same change as the gateway's upstream model, or the hosted model answers a prompt generation it was not trained on.",
|
|
271
|
+
"captcha": "CaptchaKraken/CaptchaKraken-Lora-v1.2",
|
|
272
|
+
"captcha-v12": "CaptchaKraken/CaptchaKraken-Lora-v1.2",
|
|
273
|
+
"_comment_abyss": "An alias says which model a served NAME means; `availability` says whether its weights can be downloaded. The two are independent. A licence holder names this model — the hosted API accepts it from a licensed key and lists it on /v1/models — and without this line prompt resolution falls through to the `latest` fallback, which is the model/generation mispairing this file exists to prevent. It does not make the weights fetchable.",
|
|
274
|
+
"abyss": "CaptchaKraken/Abyss",
|
|
275
|
+
"_comment_abyss_experts": "The four expert arms. These are the names that go on the wire — a routed request for a grid names `abyss-grid`, never `abyss` — so without these lines prompt resolution falls through to `latest` on every routed solve. Aliasing the arms onto one entry is deliberate and checked: they share a base, a prompt generation and a pixel band, and the parity gate requires every expert name to resolve to a registered entry declaring the SAME prompt_version and pixel_budget as the router — so the day an arm trains at another generation or band, the gate refuses the alias and that arm has to graduate to an entry of its own.",
|
|
276
|
+
"abyss-grid": "CaptchaKraken/Abyss-grid",
|
|
277
|
+
"abyss-general": "CaptchaKraken/Abyss-general",
|
|
278
|
+
"abyss-video": "CaptchaKraken/Abyss-video",
|
|
279
|
+
"abyss-text": "CaptchaKraken/Abyss-text",
|
|
280
|
+
"abyss-27b": "CaptchaKraken/Abyss-27B"
|
|
119
281
|
}
|
|
120
282
|
}
|