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
package/dist/trajectory.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Camoufox's own `humanize` is a DIFFERENT, browser-level mechanism that
|
|
20
20
|
* re-humanises every `mouse.move()` it is handed. Running both composes them —
|
|
21
|
-
* measured 82.1s vs 13.4s on one
|
|
21
|
+
* measured 82.1s vs 13.4s on one a GeeTest v4 slider solve, because each of the 60
|
|
22
22
|
* points below became its own humanised sub-trajectory. Drive with humanize off.
|
|
23
23
|
*/
|
|
24
24
|
export type Point = [number, number];
|
package/dist/trajectory.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* Camoufox's own `humanize` is a DIFFERENT, browser-level mechanism that
|
|
21
21
|
* re-humanises every `mouse.move()` it is handed. Running both composes them —
|
|
22
|
-
* measured 82.1s vs 13.4s on one
|
|
22
|
+
* measured 82.1s vs 13.4s on one a GeeTest v4 slider solve, because each of the 60
|
|
23
23
|
* points below became its own humanised sub-trajectory. Drive with humanize off.
|
|
24
24
|
*/
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/types.d.ts
CHANGED
|
@@ -407,6 +407,18 @@ export interface CaptchaKrakenConfig {
|
|
|
407
407
|
*/
|
|
408
408
|
keyframeWaitTimeoutMs?: number;
|
|
409
409
|
keyframeWaitPollMs?: number;
|
|
410
|
+
/**
|
|
411
|
+
* Hard ceiling on a burst that never repeats a screen. The burst normally
|
|
412
|
+
* ends when the cycle closes; this bounds a continuous animation, which never
|
|
413
|
+
* closes one. Default 12000ms.
|
|
414
|
+
*/
|
|
415
|
+
videoBurstMaxMs?: number;
|
|
416
|
+
/**
|
|
417
|
+
* Record while the still screenshot is being read, so a cycling board is
|
|
418
|
+
* known before its answer is acted on. Default on. Off restores the older
|
|
419
|
+
* shape, which learned the same thing two rounds later.
|
|
420
|
+
*/
|
|
421
|
+
speculativeBurstEnabled?: boolean;
|
|
410
422
|
/**
|
|
411
423
|
* Extra wall clock (ms) granted ONCE, the first time a solve escalates to a
|
|
412
424
|
* recording. NOT a looser `overallSolveTimeoutMs`.
|
|
@@ -420,8 +432,8 @@ export interface CaptchaKrakenConfig {
|
|
|
420
432
|
* timeout — which reads as a slow model rather than as a budget with no room
|
|
421
433
|
* for what the solver had just decided to do.
|
|
422
434
|
*
|
|
423
|
-
* Measured 2026-08-22,
|
|
424
|
-
*
|
|
435
|
+
* Measured 2026-08-22, a full driver-gate run: an hCaptcha click-by-trait board,
|
|
436
|
+
* an hCaptcha connect-the-path board and an hCaptcha 3x3 property grid each failed this port
|
|
425
437
|
* with "Captcha solve timed out after 45000ms (attempt 6/6)" at 49-59s, and
|
|
426
438
|
* solve in 14-20s on the rounds the still path answers them.
|
|
427
439
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "captchakraken",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.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",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"prebuild": "node scripts/copy-python.mjs",
|
|
15
15
|
"build": "tsc",
|
|
16
|
+
"demo": "tsx examples/demoSites.ts",
|
|
16
17
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
17
18
|
"test": "tsc -p tsconfig.test.json && node --test .test-build/*.test.js",
|
|
18
19
|
"prepublishOnly": "npm run build",
|
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.9.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"
|
|
@@ -107,4 +107,4 @@ __all__ = [
|
|
|
107
107
|
# It said 2.6.0 while the wheel on PyPI said 2.6.1, so anyone gating on the
|
|
108
108
|
# runtime attribute saw a version that had not been current for weeks.
|
|
109
109
|
# tests/test_public_contract.py compares all three.
|
|
110
|
-
__version__ = "2.
|
|
110
|
+
__version__ = "2.9.0"
|
|
@@ -542,6 +542,10 @@ def _handle_solve_animated() -> bool:
|
|
|
542
542
|
# solve log rather than only inferable from the answer.
|
|
543
543
|
"keyframes": paths,
|
|
544
544
|
"keyframe_mode": kfset.mode,
|
|
545
|
+
# What the board LOOKS like, as distinct from how it was
|
|
546
|
+
# sliced. The driver gates its clicks on this; see
|
|
547
|
+
# KeyframeSet.steady_screens.
|
|
548
|
+
"steady_screens": kfset.steady_screens,
|
|
545
549
|
"source_frames": len(imgs),
|
|
546
550
|
}))
|
|
547
551
|
except UnsupportedCaptchaError as e:
|
|
@@ -14,7 +14,7 @@ three directions at once —
|
|
|
14
14
|
device farm, a model of their own users) was composing two of them. That is
|
|
15
15
|
not hypothetical — camoufox's `humanize` juggler re-humanises every
|
|
16
16
|
`mouse.move()` it is handed, and running both measured 82.1s against 13.4s
|
|
17
|
-
on one
|
|
17
|
+
on one a GeeTest v4 slider solve, because each of the 60 trajectory points
|
|
18
18
|
became its own humanised sub-trajectory.
|
|
19
19
|
- a caller on their own infrastructure, against their own fixtures, is paying
|
|
20
20
|
for texture nobody is measuring.
|
|
@@ -41,7 +41,7 @@ pointer", because that is what the next gesture starts from and what
|
|
|
41
41
|
`SolveResult.final_mouse_position` reports.
|
|
42
42
|
|
|
43
43
|
The TypeScript side is `js/src/humanize.ts`; the two are one design in two
|
|
44
|
-
languages, and `tests
|
|
44
|
+
languages, and `the training repo's humanizer parity tests` in the training repo pins
|
|
45
45
|
that the mode names and the pause vocabulary have not drifted.
|
|
46
46
|
"""
|
|
47
47
|
|
|
@@ -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:
|
|
@@ -538,7 +607,7 @@ def _drop_smeared(
|
|
|
538
607
|
six eval clips, dropping a SCREEN. A page that never reaches the model is
|
|
539
608
|
unanswerable whenever the target lived on it.
|
|
540
609
|
* *Drop when some other still is within `distinct_ratio`* cut
|
|
541
|
-
|
|
610
|
+
an hCaptcha tile-flip animation from 6 stills to 3, because two boards of that
|
|
542
611
|
puzzle differ by ONE TILE — well inside `distinct_ratio` — so a diff
|
|
543
612
|
threshold cannot tell "the same picture again" from "the next board".
|
|
544
613
|
EXPECTED_SLICING says it directly: fewer than 6 "means dedup merged two
|
|
@@ -625,6 +694,7 @@ def extract_keyframes(
|
|
|
625
694
|
reps, per_frame = cycle
|
|
626
695
|
mode = "static" if len(reps) == 1 else "cycle"
|
|
627
696
|
return KeyframeSet(
|
|
697
|
+
steady_screens=len(reps),
|
|
628
698
|
mode=mode,
|
|
629
699
|
keyframes=[
|
|
630
700
|
Keyframe(number=k + 1, source_index=idx, timestamp_ms=_ms(idx),
|
|
@@ -654,6 +724,9 @@ def extract_keyframes(
|
|
|
654
724
|
# smears the first pass had just removed.
|
|
655
725
|
indices = _drop_smeared(frames, indices, p)
|
|
656
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),
|
|
657
730
|
mode="even",
|
|
658
731
|
keyframes=[
|
|
659
732
|
Keyframe(number=k + 1, source_index=idx, timestamp_ms=_ms(idx),
|
|
@@ -679,7 +752,7 @@ def extract_keyframes_from_video(
|
|
|
679
752
|
# <media_dir>/keyframes/<stem>/keyframes.json
|
|
680
753
|
#
|
|
681
754
|
# Derived, regenerable, and ADDITIVE — the clip stays canonical next to it. That
|
|
682
|
-
# matters most under
|
|
755
|
+
# matters most under the held-out sample corpus, which is irreplaceable hand-labeled
|
|
683
756
|
# data we never rewrite (CLAUDE.md § Data trees): extraction only ever creates a
|
|
684
757
|
# new sibling directory there.
|
|
685
758
|
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
" authorised holder can do: a licensed model has nothing to",
|
|
63
63
|
" download however good your credentials are. `latest` may",
|
|
64
64
|
" never be private either — an unpinned self-hoster would get",
|
|
65
|
-
" a 401 instead of a model — and
|
|
65
|
+
" a 401 instead of a model — and the release parity gate says so.",
|
|
66
66
|
"",
|
|
67
67
|
" \"licensed\" — NOT downloadable by anyone. Reachable only through",
|
|
68
68
|
" api.captchakraken.com, or by a holder of a licence we issue.",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"place that decides what an unpinned client downloads and what prompts it",
|
|
74
74
|
"sends. A second list of 'models you may not publish' kept somewhere else",
|
|
75
75
|
"would be a list that drifts, and the drift is silent in the direction that",
|
|
76
|
-
"costs the most. The
|
|
77
|
-
"to upload a licensed model to the Hub;
|
|
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
78
|
"`latest` be one, because an unpinned self-hoster would resolve to weights",
|
|
79
79
|
"they cannot obtain.",
|
|
80
80
|
"",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"",
|
|
103
103
|
"Each expert name must resolve — through `served_aliases` or as a repo id —",
|
|
104
104
|
"to a REGISTERED entry declaring the same `prompt_version` and the same",
|
|
105
|
-
"`pixel_budget` as the model that routes to it.
|
|
105
|
+
"`pixel_budget` as the model that routes to it. The release parity gate",
|
|
106
106
|
"enforces that. It is what makes aliasing four arms onto one entry safe",
|
|
107
107
|
"rather than convenient: the four share a base, a schema and a band today",
|
|
108
108
|
"(measured, not assumed — runs 20260906-0142xx all carry prompt generation",
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"min": 518400,
|
|
133
133
|
"max": 518400
|
|
134
134
|
},
|
|
135
|
-
"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."
|
|
136
136
|
},
|
|
137
137
|
"CaptchaKraken/Twilight-v1.2-FP8": {
|
|
138
138
|
"prompt_version": "2",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"min": 518400,
|
|
143
143
|
"max": 518400
|
|
144
144
|
},
|
|
145
|
-
"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."
|
|
146
146
|
},
|
|
147
147
|
"CaptchaKraken/Sunlight-v1.2-AWQ-4bit": {
|
|
148
148
|
"prompt_version": "2",
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"base_model": "RedHatAI/Qwen3.5-9B-FP8-dynamic",
|
|
161
161
|
"lora_revision": "main",
|
|
162
162
|
"lora_name": "captcha",
|
|
163
|
-
"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."
|
|
164
164
|
},
|
|
165
165
|
"CaptchaKraken/CaptchaKrakenV1_Lora": {
|
|
166
166
|
"prompt_version": "1",
|
|
@@ -168,19 +168,19 @@
|
|
|
168
168
|
"base_model": "RedHatAI/Qwen3.5-9B-FP8-dynamic",
|
|
169
169
|
"lora_revision": "main",
|
|
170
170
|
"lora_name": "captcha",
|
|
171
|
-
"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."
|
|
172
172
|
},
|
|
173
173
|
"CaptchaKraken/Sunlight-AWQ-4bit": {
|
|
174
174
|
"prompt_version": "1",
|
|
175
175
|
"kind": "merged",
|
|
176
176
|
"lora_name": "captcha",
|
|
177
|
-
"note": "
|
|
177
|
+
"note": "Generation 1 merged into the base model at AWQ 4-bit."
|
|
178
178
|
},
|
|
179
179
|
"CaptchaKraken/Twilight-FP8": {
|
|
180
180
|
"prompt_version": "1",
|
|
181
181
|
"kind": "merged",
|
|
182
182
|
"lora_name": "captcha",
|
|
183
|
-
"note": "
|
|
183
|
+
"note": "Generation 1 merged into the base model at FP8-dynamic."
|
|
184
184
|
},
|
|
185
185
|
"CaptchaKraken/Abyss": {
|
|
186
186
|
"availability": "licensed",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"video": "abyss-video",
|
|
199
199
|
"text": "abyss-text"
|
|
200
200
|
},
|
|
201
|
-
"note": "
|
|
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
202
|
},
|
|
203
203
|
"CaptchaKraken/Abyss-27B": {
|
|
204
204
|
"availability": "private",
|
|
@@ -211,7 +211,7 @@
|
|
|
211
211
|
"min": 518400,
|
|
212
212
|
"max": 518400
|
|
213
213
|
},
|
|
214
|
-
"note": "
|
|
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
215
|
},
|
|
216
216
|
"CaptchaKraken/Abyss-grid": {
|
|
217
217
|
"availability": "private",
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
"min": 518400,
|
|
225
225
|
"max": 518400
|
|
226
226
|
},
|
|
227
|
-
"note": "
|
|
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
228
|
},
|
|
229
229
|
"CaptchaKraken/Abyss-text": {
|
|
230
230
|
"availability": "private",
|
|
@@ -237,19 +237,45 @@
|
|
|
237
237
|
"min": 518400,
|
|
238
238
|
"max": 518400
|
|
239
239
|
},
|
|
240
|
-
"note": "
|
|
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."
|
|
241
267
|
}
|
|
242
268
|
},
|
|
243
269
|
"served_aliases": {
|
|
244
|
-
"_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.
|
|
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.",
|
|
245
271
|
"captcha": "CaptchaKraken/CaptchaKraken-Lora-v1.2",
|
|
246
272
|
"captcha-v12": "CaptchaKraken/CaptchaKraken-Lora-v1.2",
|
|
247
|
-
"_comment_abyss": "
|
|
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.",
|
|
248
274
|
"abyss": "CaptchaKraken/Abyss",
|
|
249
|
-
"_comment_abyss_experts": "The four expert arms. These are the names that go
|
|
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.",
|
|
250
276
|
"abyss-grid": "CaptchaKraken/Abyss-grid",
|
|
251
|
-
"abyss-general": "CaptchaKraken/Abyss",
|
|
252
|
-
"abyss-video": "CaptchaKraken/Abyss",
|
|
277
|
+
"abyss-general": "CaptchaKraken/Abyss-general",
|
|
278
|
+
"abyss-video": "CaptchaKraken/Abyss-video",
|
|
253
279
|
"abyss-text": "CaptchaKraken/Abyss-text",
|
|
254
280
|
"abyss-27b": "CaptchaKraken/Abyss-27B"
|
|
255
281
|
}
|