captchakraken 2.6.0 → 2.8.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 +126 -16
- package/dist/cli-invocation.d.ts +2 -0
- package/dist/cli-invocation.js +5 -1
- 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/playwright-types.d.ts +18 -0
- package/dist/solver.d.ts +115 -5
- package/dist/solver.js +583 -190
- package/dist/timing.d.ts +48 -0
- package/dist/timing.js +103 -0
- package/dist/trajectory.d.ts +27 -0
- package/dist/trajectory.js +104 -0
- package/dist/types.d.ts +129 -0
- package/package.json +11 -3
- package/python/README.md +76 -5
- package/python/pyproject.toml +21 -4
- package/python/src/captchakraken/__init__.py +36 -1
- package/python/src/captchakraken/cli.py +62 -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 +99 -0
- package/python/src/captchakraken/models.json +149 -13
- package/python/src/captchakraken/page_solver.py +588 -291
- package/python/src/captchakraken/pinned_model.json +3 -2
- package/python/src/captchakraken/planner.py +118 -9
- package/python/src/captchakraken/prompts.py +182 -0
- package/python/src/captchakraken/server_manager.py +9 -4
- package/python/src/captchakraken/solver.py +97 -57
- 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
|
@@ -32,6 +32,27 @@ from .action_types import (
|
|
|
32
32
|
from .image_processor import ImageProcessor
|
|
33
33
|
from .overlay import add_overlays_to_image
|
|
34
34
|
|
|
35
|
+
# The hosted API's refusal type. Outside the try below on purpose: `errors`
|
|
36
|
+
# imports nothing but `typing`, and docs/hosted-api.md tells every caller to
|
|
37
|
+
# branch on its `.code` — a recipe that has to work in both ports, and in a
|
|
38
|
+
# minimal install that has no serving stack.
|
|
39
|
+
from .errors import CaptchaKrakenAPIError
|
|
40
|
+
|
|
41
|
+
# How gestures are PERFORMED — mouse, touch, or nothing. Deliberately outside
|
|
42
|
+
# the try below: `humanize` imports only `trajectory`, so it has no dependency
|
|
43
|
+
# floor at all, and a caller writing a custom Humanizer must be able to import
|
|
44
|
+
# the base class without the serving stack installed.
|
|
45
|
+
from .humanize import (
|
|
46
|
+
AppiumTouchBackend,
|
|
47
|
+
CdpTouchBackend,
|
|
48
|
+
Humanizer,
|
|
49
|
+
MobileHumanizer,
|
|
50
|
+
MouseHumanizer,
|
|
51
|
+
NullHumanizer,
|
|
52
|
+
TouchBackend,
|
|
53
|
+
TouchscreenTouchBackend,
|
|
54
|
+
)
|
|
55
|
+
|
|
35
56
|
# The planner (requests) and solver (torch/vllm/transformers) pull in the heavy
|
|
36
57
|
# serving stack. Keep them optional so leaf modules — e.g. tool_calls.find_grid,
|
|
37
58
|
# which needs only cv2 + numpy + pillow — can be imported in a minimal env (CI's
|
|
@@ -70,6 +91,20 @@ __all__ = [
|
|
|
70
91
|
"TypeAction",
|
|
71
92
|
"WaitAction",
|
|
72
93
|
"add_overlays_to_image",
|
|
94
|
+
"CaptchaKrakenAPIError",
|
|
95
|
+
"Humanizer",
|
|
96
|
+
"MouseHumanizer",
|
|
97
|
+
"MobileHumanizer",
|
|
98
|
+
"NullHumanizer",
|
|
99
|
+
"TouchBackend",
|
|
100
|
+
"CdpTouchBackend",
|
|
101
|
+
"AppiumTouchBackend",
|
|
102
|
+
"TouchscreenTouchBackend",
|
|
73
103
|
]
|
|
74
104
|
|
|
75
|
-
|
|
105
|
+
# KEEP IN STEP with python/pyproject.toml and js/package.json — the two ports
|
|
106
|
+
# ship together (rule 1c) and this is the only one importable code can read.
|
|
107
|
+
# It said 2.6.0 while the wheel on PyPI said 2.6.1, so anyone gating on the
|
|
108
|
+
# runtime attribute saw a version that had not been current for weeks.
|
|
109
|
+
# tests/test_public_contract.py compares all three.
|
|
110
|
+
__version__ = "2.8.0"
|
|
@@ -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]
|
|
@@ -630,6 +633,44 @@ def _handle_match_region() -> bool:
|
|
|
630
633
|
return True
|
|
631
634
|
|
|
632
635
|
|
|
636
|
+
def _handle_report_outcome() -> bool:
|
|
637
|
+
"""`captchakraken report-outcome <session-id> solved|failed`
|
|
638
|
+
|
|
639
|
+
Tell the hosted API whether the widget accepted this solve. The one fact the
|
|
640
|
+
server cannot see for itself: a wrong answer arrives there as a 200 with
|
|
641
|
+
well-formed JSON in it, and only the driver watched the widget.
|
|
642
|
+
|
|
643
|
+
THIS EXISTS FOR THE TypeScript PORT. The Python driver calls
|
|
644
|
+
`planner.report_outcome` in-process at the end of `PageSolver.solve`; the TS
|
|
645
|
+
driver spawns the CLI, which is the same subprocess boundary every other
|
|
646
|
+
model call already crosses. One implementation of the endpoint, the
|
|
647
|
+
credential and the extra headers, rather than a second copy in TypeScript
|
|
648
|
+
that agrees with this one until one of them is edited — the drift
|
|
649
|
+
`model-name.ts` already exists to prevent, on the same seam.
|
|
650
|
+
|
|
651
|
+
ALWAYS EXITS 0. The caller is a `finally` block in a driver whose solve has
|
|
652
|
+
already finished, so there is no outcome here worth failing over, and a
|
|
653
|
+
non-zero exit would make a self-hosted user's logs report an error on every
|
|
654
|
+
solve. What happened is on stdout for anyone who wants it.
|
|
655
|
+
"""
|
|
656
|
+
if len(sys.argv) <= 1 or sys.argv[1] != "report-outcome":
|
|
657
|
+
return False
|
|
658
|
+
if len(sys.argv) < 4 or sys.argv[3] not in ("solved", "failed"):
|
|
659
|
+
print(json.dumps({
|
|
660
|
+
"error": "Usage: captchakraken report-outcome <session-id> solved|failed"
|
|
661
|
+
}), file=sys.stderr)
|
|
662
|
+
sys.exit(1)
|
|
663
|
+
delivered = False
|
|
664
|
+
try:
|
|
665
|
+
from .planner import ActionPlanner
|
|
666
|
+
delivered = ActionPlanner().report_outcome(sys.argv[2], sys.argv[3] == "solved")
|
|
667
|
+
except Exception as exc: # noqa: BLE001 — see the docstring
|
|
668
|
+
print(json.dumps({"reported": False, "error": str(exc)}))
|
|
669
|
+
return True
|
|
670
|
+
print(json.dumps({"reported": bool(delivered)}))
|
|
671
|
+
return True
|
|
672
|
+
|
|
673
|
+
|
|
633
674
|
def _handle_tool_commands() -> bool:
|
|
634
675
|
if len(sys.argv) <= 1:
|
|
635
676
|
return False
|
|
@@ -841,6 +882,8 @@ def main():
|
|
|
841
882
|
return
|
|
842
883
|
if _handle_track_piece():
|
|
843
884
|
return
|
|
885
|
+
if _handle_report_outcome():
|
|
886
|
+
return
|
|
844
887
|
|
|
845
888
|
parser = argparse.ArgumentParser(description="CaptchaKraken v2 (vLLM)")
|
|
846
889
|
parser.add_argument("image_path", help="Path to the captcha image or video")
|
|
@@ -867,8 +910,11 @@ def main():
|
|
|
867
910
|
"--puzzle-source",
|
|
868
911
|
default="unknown",
|
|
869
912
|
choices=["hcaptcha", "recaptcha", "unknown"],
|
|
870
|
-
help="Vendor hint from the Playwright wrapper.
|
|
871
|
-
"
|
|
913
|
+
help="Vendor hint from the Playwright wrapper. Constrains which grid "
|
|
914
|
+
"shapes a detection may be solved as: hCaptcha ships only a 3x3, so a "
|
|
915
|
+
"16-cell lattice on one is a false positive (find_grid latches onto the "
|
|
916
|
+
"header/footer bands of click puzzles). An unrecognised or absent hint "
|
|
917
|
+
"allows every shape, which is what GeeTest and Prosopo boards report.",
|
|
872
918
|
)
|
|
873
919
|
parser.add_argument(
|
|
874
920
|
"--retry-mode",
|
|
@@ -879,6 +925,17 @@ def main():
|
|
|
879
925
|
"images'). Switches the grid prompt to a more aggressive variant that "
|
|
880
926
|
"instructs the LoRA to look at the full grid for tiles it missed.",
|
|
881
927
|
)
|
|
928
|
+
parser.add_argument(
|
|
929
|
+
"--expert",
|
|
930
|
+
default=None,
|
|
931
|
+
choices=["pixel", "grid", "video", "text"],
|
|
932
|
+
help="Force one expert of a ROUTED model (Abyss) instead of letting the "
|
|
933
|
+
"prompt family choose. Routing is automatic and this is the override a "
|
|
934
|
+
"benchmark needs: serve one arm, grade the types it owns, serve the "
|
|
935
|
+
"next. Refused against a model that serves a single adapter, because "
|
|
936
|
+
"quietly measuring the generalist and reporting it as the expert is a "
|
|
937
|
+
"number nobody can catch. Also settable as CAPTCHA_EXPERT.",
|
|
938
|
+
)
|
|
882
939
|
parser.add_argument(
|
|
883
940
|
"--text-mode",
|
|
884
941
|
action="store_true",
|
|
@@ -897,7 +954,8 @@ def main():
|
|
|
897
954
|
|
|
898
955
|
try:
|
|
899
956
|
with timed("cli.total"):
|
|
900
|
-
solver = CaptchaSolver(model=args.model, api_key=args.api_key
|
|
957
|
+
solver = CaptchaSolver(model=args.model, api_key=args.api_key,
|
|
958
|
+
expert=args.expert)
|
|
901
959
|
result = solver.solve(
|
|
902
960
|
args.image_path,
|
|
903
961
|
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.
|