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
|
@@ -505,6 +505,8 @@ def _handle_solve_animated() -> bool:
|
|
|
505
505
|
"rate the training data was collected at).")
|
|
506
506
|
parser.add_argument("--model", default=None)
|
|
507
507
|
parser.add_argument("--api-key", default=None)
|
|
508
|
+
parser.add_argument("--expert", default=None,
|
|
509
|
+
choices=["pixel", "grid", "video", "text"])
|
|
508
510
|
args = parser.parse_args(sys.argv[2:])
|
|
509
511
|
|
|
510
512
|
frames = _frames_in(args.frames_dir)
|
|
@@ -527,7 +529,8 @@ def _handle_solve_animated() -> bool:
|
|
|
527
529
|
paths = [str(os.path.abspath(p))
|
|
528
530
|
for p in write_keyframes(kfset, kf_dir, stem="challenge")]
|
|
529
531
|
|
|
530
|
-
solver = CaptchaSolver(model=args.model, api_key=args.api_key
|
|
532
|
+
solver = CaptchaSolver(model=args.model, api_key=args.api_key,
|
|
533
|
+
expert=args.expert)
|
|
531
534
|
result = solver.solve_keyframes(paths)
|
|
532
535
|
|
|
533
536
|
action_data = [a.model_dump() for a in result]
|
|
@@ -539,6 +542,10 @@ def _handle_solve_animated() -> bool:
|
|
|
539
542
|
# solve log rather than only inferable from the answer.
|
|
540
543
|
"keyframes": paths,
|
|
541
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,
|
|
542
549
|
"source_frames": len(imgs),
|
|
543
550
|
}))
|
|
544
551
|
except UnsupportedCaptchaError as e:
|
|
@@ -630,6 +637,44 @@ def _handle_match_region() -> bool:
|
|
|
630
637
|
return True
|
|
631
638
|
|
|
632
639
|
|
|
640
|
+
def _handle_report_outcome() -> bool:
|
|
641
|
+
"""`captchakraken report-outcome <session-id> solved|failed`
|
|
642
|
+
|
|
643
|
+
Tell the hosted API whether the widget accepted this solve. The one fact the
|
|
644
|
+
server cannot see for itself: a wrong answer arrives there as a 200 with
|
|
645
|
+
well-formed JSON in it, and only the driver watched the widget.
|
|
646
|
+
|
|
647
|
+
THIS EXISTS FOR THE TypeScript PORT. The Python driver calls
|
|
648
|
+
`planner.report_outcome` in-process at the end of `PageSolver.solve`; the TS
|
|
649
|
+
driver spawns the CLI, which is the same subprocess boundary every other
|
|
650
|
+
model call already crosses. One implementation of the endpoint, the
|
|
651
|
+
credential and the extra headers, rather than a second copy in TypeScript
|
|
652
|
+
that agrees with this one until one of them is edited — the drift
|
|
653
|
+
`model-name.ts` already exists to prevent, on the same seam.
|
|
654
|
+
|
|
655
|
+
ALWAYS EXITS 0. The caller is a `finally` block in a driver whose solve has
|
|
656
|
+
already finished, so there is no outcome here worth failing over, and a
|
|
657
|
+
non-zero exit would make a self-hosted user's logs report an error on every
|
|
658
|
+
solve. What happened is on stdout for anyone who wants it.
|
|
659
|
+
"""
|
|
660
|
+
if len(sys.argv) <= 1 or sys.argv[1] != "report-outcome":
|
|
661
|
+
return False
|
|
662
|
+
if len(sys.argv) < 4 or sys.argv[3] not in ("solved", "failed"):
|
|
663
|
+
print(json.dumps({
|
|
664
|
+
"error": "Usage: captchakraken report-outcome <session-id> solved|failed"
|
|
665
|
+
}), file=sys.stderr)
|
|
666
|
+
sys.exit(1)
|
|
667
|
+
delivered = False
|
|
668
|
+
try:
|
|
669
|
+
from .planner import ActionPlanner
|
|
670
|
+
delivered = ActionPlanner().report_outcome(sys.argv[2], sys.argv[3] == "solved")
|
|
671
|
+
except Exception as exc: # noqa: BLE001 — see the docstring
|
|
672
|
+
print(json.dumps({"reported": False, "error": str(exc)}))
|
|
673
|
+
return True
|
|
674
|
+
print(json.dumps({"reported": bool(delivered)}))
|
|
675
|
+
return True
|
|
676
|
+
|
|
677
|
+
|
|
633
678
|
def _handle_tool_commands() -> bool:
|
|
634
679
|
if len(sys.argv) <= 1:
|
|
635
680
|
return False
|
|
@@ -841,6 +886,8 @@ def main():
|
|
|
841
886
|
return
|
|
842
887
|
if _handle_track_piece():
|
|
843
888
|
return
|
|
889
|
+
if _handle_report_outcome():
|
|
890
|
+
return
|
|
844
891
|
|
|
845
892
|
parser = argparse.ArgumentParser(description="CaptchaKraken v2 (vLLM)")
|
|
846
893
|
parser.add_argument("image_path", help="Path to the captcha image or video")
|
|
@@ -867,8 +914,11 @@ def main():
|
|
|
867
914
|
"--puzzle-source",
|
|
868
915
|
default="unknown",
|
|
869
916
|
choices=["hcaptcha", "recaptcha", "unknown"],
|
|
870
|
-
help="Vendor hint from the Playwright wrapper.
|
|
871
|
-
"
|
|
917
|
+
help="Vendor hint from the Playwright wrapper. Constrains which grid "
|
|
918
|
+
"shapes a detection may be solved as: hCaptcha ships only a 3x3, so a "
|
|
919
|
+
"16-cell lattice on one is a false positive (find_grid latches onto the "
|
|
920
|
+
"header/footer bands of click puzzles). An unrecognised or absent hint "
|
|
921
|
+
"allows every shape, which is what GeeTest and Prosopo boards report.",
|
|
872
922
|
)
|
|
873
923
|
parser.add_argument(
|
|
874
924
|
"--retry-mode",
|
|
@@ -879,6 +929,17 @@ def main():
|
|
|
879
929
|
"images'). Switches the grid prompt to a more aggressive variant that "
|
|
880
930
|
"instructs the LoRA to look at the full grid for tiles it missed.",
|
|
881
931
|
)
|
|
932
|
+
parser.add_argument(
|
|
933
|
+
"--expert",
|
|
934
|
+
default=None,
|
|
935
|
+
choices=["pixel", "grid", "video", "text"],
|
|
936
|
+
help="Force one expert of a ROUTED model (Abyss) instead of letting the "
|
|
937
|
+
"prompt family choose. Routing is automatic and this is the override a "
|
|
938
|
+
"benchmark needs: serve one arm, grade the types it owns, serve the "
|
|
939
|
+
"next. Refused against a model that serves a single adapter, because "
|
|
940
|
+
"quietly measuring the generalist and reporting it as the expert is a "
|
|
941
|
+
"number nobody can catch. Also settable as CAPTCHA_EXPERT.",
|
|
942
|
+
)
|
|
882
943
|
parser.add_argument(
|
|
883
944
|
"--text-mode",
|
|
884
945
|
action="store_true",
|
|
@@ -897,7 +958,8 @@ def main():
|
|
|
897
958
|
|
|
898
959
|
try:
|
|
899
960
|
with timed("cli.total"):
|
|
900
|
-
solver = CaptchaSolver(model=args.model, api_key=args.api_key
|
|
961
|
+
solver = CaptchaSolver(model=args.model, api_key=args.api_key,
|
|
962
|
+
expert=args.expert)
|
|
901
963
|
result = solver.solve(
|
|
902
964
|
args.image_path,
|
|
903
965
|
puzzle_source=args.puzzle_source,
|
|
@@ -193,11 +193,37 @@ def api_key() -> str:
|
|
|
193
193
|
# CaptchaKraken_v1.1, so nothing moves until a newer model is registered — at
|
|
194
194
|
# which point that IS the intent.
|
|
195
195
|
def _registry_default(field: str) -> Optional[str]:
|
|
196
|
-
"""`field` from the
|
|
196
|
+
"""`field` from the entry of the model this process will actually serve.
|
|
197
|
+
|
|
198
|
+
THE PIN DECIDES THE ENTRY, not just the adapter. This read `latest`
|
|
199
|
+
unconditionally, so `CAPTCHA_LORA_ADAPTER` moved the adapter and nothing
|
|
200
|
+
else: the base weights to load it onto, the served `lora_name` to put in
|
|
201
|
+
the request, and the revision all still came from whichever model happened
|
|
202
|
+
to be `latest`.
|
|
203
|
+
|
|
204
|
+
That was survivable while every registered model was a 9B on one base
|
|
205
|
+
answering to one served name. It stopped being survivable the day the
|
|
206
|
+
registry gained `CaptchaKraken/Abyss-27B` — a Qwen3.8-**27B** — because
|
|
207
|
+
pinning it downloaded a 9B base, tried to load a 27B adapter onto it, and
|
|
208
|
+
sent `captcha-v12` as the model name. Nothing in the client errors on that;
|
|
209
|
+
it errors deep inside vLLM as a shape mismatch, or not at all if the
|
|
210
|
+
endpoint happens to serve something by that name. Same shape for every
|
|
211
|
+
expert arm, whose whole point is a `lora_name` of its own.
|
|
212
|
+
|
|
213
|
+
An UNREGISTERED pin still falls back to `latest`. A self-hoster's own
|
|
214
|
+
adapter is not in our registry and never will be; changing what they
|
|
215
|
+
resolve to would be a break with no benefit.
|
|
216
|
+
"""
|
|
197
217
|
try:
|
|
198
218
|
from . import prompts # local: prompts imports nothing from config
|
|
199
219
|
|
|
200
|
-
|
|
220
|
+
models = prompts.registered_models()
|
|
221
|
+
entry = None
|
|
222
|
+
pin = os.getenv("CAPTCHA_LORA_ADAPTER") or os.getenv("CAPTCHA_LORA_NAME")
|
|
223
|
+
if pin:
|
|
224
|
+
entry = models.get(prompts.canonical_model_id(pin) or "")
|
|
225
|
+
if entry is None:
|
|
226
|
+
entry = models.get(prompts.latest_model() or "")
|
|
201
227
|
value = (entry or {}).get(field)
|
|
202
228
|
return value if isinstance(value, str) else None
|
|
203
229
|
except Exception: # noqa: BLE001 — a broken registry falls back, never raises
|
|
@@ -149,6 +149,30 @@ def _sentence(code: str, message: str, url: Optional[str], retry: Optional[float
|
|
|
149
149
|
"our side, not yours — retry shortly."
|
|
150
150
|
)
|
|
151
151
|
|
|
152
|
+
# The two LICENSED-MODEL refusals. Both are 'you named a model', and they
|
|
153
|
+
# are separate codes because the fix is completely different: one is a
|
|
154
|
+
# licence to obtain, the other is a fleet that has not started serving yet
|
|
155
|
+
# and nothing for the caller to do. Generic prose collapses them into "the
|
|
156
|
+
# solve failed", which is the reading that sends someone to buy a licence
|
|
157
|
+
# they already hold.
|
|
158
|
+
if code == "model_not_licensed":
|
|
159
|
+
return (
|
|
160
|
+
"CaptchaKraken: the model this request named is licensed, and this "
|
|
161
|
+
"account is not licensed for it. The request was refused rather than "
|
|
162
|
+
"answered by a different model — a silent substitution would be a "
|
|
163
|
+
"score you could not explain. Unset CAPTCHA_LORA_NAME (or the "
|
|
164
|
+
f"client's `model`) to use the standard hosted model. {message} "
|
|
165
|
+
f"Licensing: {url or _SUPPORT}."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if code == "model_not_serving":
|
|
169
|
+
return (
|
|
170
|
+
"CaptchaKraken: this account IS licensed for the model it named, but "
|
|
171
|
+
"the fleet is not serving it yet. Nothing is wrong with your account "
|
|
172
|
+
"and there is nothing to buy. Unset CAPTCHA_LORA_NAME (or the "
|
|
173
|
+
f"client's `model`) to use the standard hosted model meanwhile. {message}"
|
|
174
|
+
)
|
|
175
|
+
|
|
152
176
|
# `unrecognized_prompt`, `invalid_request`, and anything added after this was
|
|
153
177
|
# written. The server's own message is the best thing available, so it is
|
|
154
178
|
# carried through verbatim rather than replaced with a guess.
|