captchakraken 3.2.0 → 4.0.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/AGENTS.md +5 -9
- package/LICENSE +165 -145
- package/NOTICE +4 -4
- package/README.md +10 -11
- package/dist/cli-invocation.d.ts +6 -3
- package/dist/cli-invocation.js +26 -10
- package/dist/humanize.d.ts +37 -1
- package/dist/humanize.js +143 -9
- package/dist/index.d.ts +13 -12
- package/dist/index.js +25 -33
- package/dist/kinds.d.ts +15 -2
- package/dist/kinds.js +12 -4
- package/dist/playwright-types.d.ts +3 -0
- package/dist/selectors.d.ts +96 -11
- package/dist/selectors.js +226 -112
- package/dist/solver.d.ts +93 -13
- package/dist/solver.js +579 -208
- package/dist/types.d.ts +53 -8
- package/dist/verdicts.d.ts +3 -0
- package/dist/verdicts.js +6 -0
- package/package.json +4 -4
- package/python/AGENTS.md +7 -11
- package/python/LICENSE +165 -145
- package/python/NOTICE +4 -4
- package/python/README.md +10 -9
- package/python/pyproject.toml +27 -13
- package/python/src/captchakraken/__init__.py +43 -46
- package/python/src/captchakraken/action_types.py +32 -1
- package/python/src/captchakraken/captchakraken.config.json +416 -0
- package/python/src/captchakraken/cli.py +66 -30
- package/python/src/captchakraken/clipvideo.py +258 -0
- package/python/src/captchakraken/humanize.py +141 -10
- package/python/src/captchakraken/image_processor.py +3 -7
- package/python/src/captchakraken/keyframes.py +96 -14
- package/python/src/captchakraken/kinds.py +14 -2
- package/python/src/captchakraken/models.json +28 -3
- package/python/src/captchakraken/page_solver.py +621 -259
- package/python/src/captchakraken/planner.py +298 -47
- package/python/src/captchakraken/pointer/__init__.py +20 -0
- package/python/src/captchakraken/pointer/detector.py +333 -0
- package/python/src/captchakraken/pointer/elements.py +273 -0
- package/python/src/captchakraken/pointer/heads.py +163 -0
- package/python/src/captchakraken/pointer/layout.py +319 -0
- package/python/src/captchakraken/pointer/marks.py +184 -0
- package/python/src/captchakraken/pointer/server.py +146 -0
- package/python/src/captchakraken/pointer/service.py +553 -0
- package/python/src/captchakraken/pointer/vllm_plugin.py +109 -0
- package/python/src/captchakraken/prompts.py +111 -34
- package/python/src/captchakraken/selectors.py +224 -131
- package/python/src/captchakraken/server_manager.py +18 -0
- package/python/src/captchakraken/solver.py +169 -88
- package/python/src/captchakraken/tool_calls/board_painted.py +11 -6
- package/python/src/captchakraken/tool_calls/fill_tracker.py +152 -0
- package/python/src/captchakraken/tool_calls/find_checkbox.py +14 -59
- package/python/src/captchakraken/tool_calls/find_grid.py +273 -1191
- package/python/src/captchakraken/tool_calls/slide_tracker.py +1102 -0
- package/python/src/captchakraken/verdicts.py +16 -0
- package/dist/slide-geometry.d.ts +0 -6
- package/dist/slide-geometry.js +0 -38
- package/python/src/captchakraken/tool_calls/track_piece.py +0 -120
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": [
|
|
3
|
+
"Every URL pattern and selector the solver detects and drives a captcha with.",
|
|
4
|
+
"",
|
|
5
|
+
"This file is the default: PageSolver (Python) and CaptchaKraken (JS) load it",
|
|
6
|
+
"when no other is given. To add your own, write a file of the same shape and",
|
|
7
|
+
"pass its path (or the parsed object) as `selectors` in PageSolverConfig",
|
|
8
|
+
"(Python) or CaptchaKrakenConfig (JS). A file of your own may hold only what",
|
|
9
|
+
"it changes: `custom`, any vendor under `vendors` (it replaces that vendor's",
|
|
10
|
+
"whole entry), or any list under `generic`; the rest comes from this file.",
|
|
11
|
+
"",
|
|
12
|
+
"custom: your own entry, for a captcha the built-in vendors do not know. It",
|
|
13
|
+
"is detected before every built-in vendor and reported as `unknown`, and its",
|
|
14
|
+
"submit, text box, refresh and slider controls are tried before the generic",
|
|
15
|
+
"ones. It must name a `widget`, `challenge` or `checkbox`; `hosts` is optional.",
|
|
16
|
+
"",
|
|
17
|
+
"detector: the element detector that finds grids, tick boxes and pieces on the",
|
|
18
|
+
"CPU before any model is asked. `standard` (ResNet-18, 124 MB), `lite`",
|
|
19
|
+
"(MobileNetV3, 24 MB, ~6x faster on one CPU) or `auto`: lite on two usable",
|
|
20
|
+
"CPUs or fewer, and above that standard only when a one-off timing predicts",
|
|
21
|
+
"it inside 200 ms a board. Only the chosen model is downloaded.",
|
|
22
|
+
"CAPTCHA_KRAKEN_DETECTOR_MODEL overrides this.",
|
|
23
|
+
"",
|
|
24
|
+
"vendors: one entry per vendor, in detection order. Order matters: the driver",
|
|
25
|
+
"takes the first visible match in table order. `hosts` are URL substrings of",
|
|
26
|
+
"the vendor's code on the wire; every other field is a list of CSS (or",
|
|
27
|
+
"Playwright `xpath=`) selectors, except `response`, `checked`, `accepted` and",
|
|
28
|
+
"`fresh` (one selector each), `banners` ([selector, verdict] pairs) and",
|
|
29
|
+
"`hold_completes` (true or false). `note` is for people, not the solver.",
|
|
30
|
+
"",
|
|
31
|
+
"generic: the fallbacks tried after every vendor's own. `submit_text` is the",
|
|
32
|
+
"button text probed for a submit control, in order.",
|
|
33
|
+
"",
|
|
34
|
+
"Keys starting with `_` are comments. Field reference: `_fields`."
|
|
35
|
+
],
|
|
36
|
+
"_fields": {
|
|
37
|
+
"hosts": "URL substrings of the vendor's code on the wire. A tripwire, not a detector: the host stays on the wire when a vendor renames its markup (see TRIBAL_KNOWLEDGE.md). BotDetect is self-hosted, so it has none.",
|
|
38
|
+
"challenge": "On the host page: the open challenge. Detected first.",
|
|
39
|
+
"checkbox": "On the host page: the checkbox. Detected while `response` is empty and `checked` is not showing.",
|
|
40
|
+
"enclave": "On the host page: a frame the vendor draws its other frames inside. Every host-page selector also reads the inside of a visible one.",
|
|
41
|
+
"widget": "On the host page: inline widget shapes, detected last. Iframe selectors go first; a class that lives inside the frame document (.CheckboxCaptcha, .mtcap) only ever matches an inline embed.",
|
|
42
|
+
"response": "On the host page: the field the vendor fills on success. Read first and unconditionally, because hCaptcha's overlay hides the anchor.",
|
|
43
|
+
"checked": "Inside the checkbox frame: the box shows accepted. Demo pages do not always fill `response`.",
|
|
44
|
+
"box": "Inside the checkbox frame: the box itself, clicked from the DOM. A vendor without one is found on a photograph. An in-page checkbox that names one is a button with no tick box to find, and is clicked where it is.",
|
|
45
|
+
"accepted": "On the host page: the vendor painted its success state. Visibility is part of the test (see TRIBAL_KNOWLEDGE.md).",
|
|
46
|
+
"fresh": "Inside the challenge frame: a new round has painted, with text.",
|
|
47
|
+
"images": "Inside the challenge frame: the pictures a board is made of; the driver waits for each to have painted.",
|
|
48
|
+
"submit": "Inside the widget: the vendor's own submit control, tried after the generic text probes.",
|
|
49
|
+
"banners": "Inside the challenge frame: verdict banners and what each means.",
|
|
50
|
+
"blocked": "Inside the challenge frame: the vendor refusing to serve this client at all. Nothing after it can succeed.",
|
|
51
|
+
"passive": "On the host page: the vendor's frames that are never interactive, so a page showing only these has no widget.",
|
|
52
|
+
"text_input": "Inside the widget, and its enclosing fieldset/form: the answer box of a typed captcha.",
|
|
53
|
+
"slider_handle": "Inside the widget: the knob a slide must start on; a drag from the piece moves nothing.",
|
|
54
|
+
"piece": "Inside the widget: the piece, measured to steer the slide and dragged directly when there is no track.",
|
|
55
|
+
"refresh": "Inside the widget: the control that deals a new puzzle, pressed when the model answers `refresh`.",
|
|
56
|
+
"hold_completes": "A press-and-hold that completes on its own once full, and resets when let go before it has: the fill reaching the answer's `to` is not a release, and the hold lasts until the widget says it is done."
|
|
57
|
+
},
|
|
58
|
+
"detector": "auto",
|
|
59
|
+
"custom": null,
|
|
60
|
+
"vendors": {
|
|
61
|
+
"recaptcha": {
|
|
62
|
+
"note": "An Enterprise sitekey serves the same widget from /recaptcha/enterprise/ instead of /recaptcha/api2/.",
|
|
63
|
+
"hosts": [
|
|
64
|
+
"google.com/recaptcha",
|
|
65
|
+
"recaptcha.net"
|
|
66
|
+
],
|
|
67
|
+
"challenge": [
|
|
68
|
+
"iframe[src*=\"recaptcha/api2/bframe\"]",
|
|
69
|
+
"iframe[src*=\"recaptcha/enterprise/bframe\"]"
|
|
70
|
+
],
|
|
71
|
+
"checkbox": [
|
|
72
|
+
"iframe[src*=\"recaptcha/api2/anchor\"]:not([src*=\"size=invisible\"])",
|
|
73
|
+
"iframe[src*=\"recaptcha/enterprise/anchor\"]:not([src*=\"size=invisible\"])"
|
|
74
|
+
],
|
|
75
|
+
"response": "[name=\"g-recaptcha-response\"]",
|
|
76
|
+
"checked": ".recaptcha-checkbox-checked",
|
|
77
|
+
"box": [
|
|
78
|
+
"#recaptcha-anchor"
|
|
79
|
+
],
|
|
80
|
+
"fresh": ".rc-imageselect-instructions, #rc-imageselect",
|
|
81
|
+
"submit": [
|
|
82
|
+
"#recaptcha-verify-button"
|
|
83
|
+
],
|
|
84
|
+
"banners": [
|
|
85
|
+
[
|
|
86
|
+
".rc-imageselect-error-select-more",
|
|
87
|
+
"select-more"
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
".rc-imageselect-error-dynamic-more",
|
|
91
|
+
"dynamic-more"
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
".rc-imageselect-incorrect-response",
|
|
95
|
+
"rejected"
|
|
96
|
+
]
|
|
97
|
+
],
|
|
98
|
+
"blocked": [
|
|
99
|
+
".rc-doscaptcha-header-text"
|
|
100
|
+
],
|
|
101
|
+
"passive": [
|
|
102
|
+
"iframe[src*=\"size=invisible\"]"
|
|
103
|
+
],
|
|
104
|
+
"refresh": [
|
|
105
|
+
"#recaptcha-reload-button"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"hcaptcha": {
|
|
109
|
+
"note": "Keyed on the `hcaptcha` substring, not the apex host: challenges are served off newassets.hcaptcha.com. secure-api.js draws every frame inside a full-viewport enclave, one per render, and only a visible one holds an open challenge.",
|
|
110
|
+
"hosts": [
|
|
111
|
+
"hcaptcha.com"
|
|
112
|
+
],
|
|
113
|
+
"challenge": [
|
|
114
|
+
"iframe[src*=\"hcaptcha\"][src*=\"frame=challenge\"]"
|
|
115
|
+
],
|
|
116
|
+
"checkbox": [
|
|
117
|
+
"iframe[src*=\"hcaptcha\"][src*=\"frame=checkbox\"]:not([src*=\"frame=checkbox-invisible\"])"
|
|
118
|
+
],
|
|
119
|
+
"enclave": [
|
|
120
|
+
"iframe[src*=\"hcaptcha\"][src*=\"frame=enclave\"]"
|
|
121
|
+
],
|
|
122
|
+
"response": "[name=\"h-captcha-response\"]",
|
|
123
|
+
"checked": "#checkbox[aria-checked=\"true\"]",
|
|
124
|
+
"box": [
|
|
125
|
+
"#checkbox"
|
|
126
|
+
],
|
|
127
|
+
"fresh": ".prompt-text",
|
|
128
|
+
"images": [
|
|
129
|
+
".task-image .image",
|
|
130
|
+
".task .image",
|
|
131
|
+
".challenge-example img",
|
|
132
|
+
".image-wrapper img"
|
|
133
|
+
],
|
|
134
|
+
"submit": [
|
|
135
|
+
".button-submit"
|
|
136
|
+
],
|
|
137
|
+
"passive": [
|
|
138
|
+
"iframe[src*=\"hcaptcha\"][src*=\"frame=enclave\"]",
|
|
139
|
+
"iframe[src*=\"hcaptcha\"][src*=\"frame=checkbox-invisible\"]"
|
|
140
|
+
],
|
|
141
|
+
"refresh": [
|
|
142
|
+
".refresh.button"
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
"turnstile": {
|
|
146
|
+
"hosts": [
|
|
147
|
+
"challenges.cloudflare.com"
|
|
148
|
+
],
|
|
149
|
+
"checkbox": [
|
|
150
|
+
"iframe[src*=\"challenges.cloudflare.com\"]",
|
|
151
|
+
".cf-turnstile"
|
|
152
|
+
],
|
|
153
|
+
"response": "[name=\"cf-turnstile-response\"]"
|
|
154
|
+
},
|
|
155
|
+
"geetest": {
|
|
156
|
+
"note": "v4's \"Click to verify\" bar opens the puzzle. Its container wears `geetest_freeze_wait` while the puzzle is open (`geetest_boxShow` only until a first wrong answer, after which the box stays up dealing a new one) and `geetest_success` once passed, so the checkbox matches only a bar that has not been clicked yet.",
|
|
157
|
+
"hosts": [
|
|
158
|
+
"geetest.com"
|
|
159
|
+
],
|
|
160
|
+
"checkbox": [
|
|
161
|
+
".geetest_captcha:not(.geetest_boxShow):not(.geetest_freeze_wait):not(.geetest_success):not(.geetest_lock_success) .geetest_btn_click"
|
|
162
|
+
],
|
|
163
|
+
"widget": [
|
|
164
|
+
".geetest_box",
|
|
165
|
+
".geetest_panel_box",
|
|
166
|
+
".geetest_popup_window",
|
|
167
|
+
".geetest_widget"
|
|
168
|
+
],
|
|
169
|
+
"box": [
|
|
170
|
+
".geetest_btn_click"
|
|
171
|
+
],
|
|
172
|
+
"accepted": ".geetest_result_tips.geetest_success, .geetest_captcha.geetest_success, .geetest_captcha.geetest_lock_success",
|
|
173
|
+
"submit": [
|
|
174
|
+
".geetest_submit"
|
|
175
|
+
],
|
|
176
|
+
"slider_handle": [
|
|
177
|
+
".geetest_slider_button",
|
|
178
|
+
".geetest_btn",
|
|
179
|
+
".geetest_slider .geetest_arrow"
|
|
180
|
+
],
|
|
181
|
+
"piece": [
|
|
182
|
+
".geetest_slice"
|
|
183
|
+
],
|
|
184
|
+
"refresh": [
|
|
185
|
+
".geetest_refresh",
|
|
186
|
+
".geetest_refresh_1"
|
|
187
|
+
]
|
|
188
|
+
},
|
|
189
|
+
"tencent": {
|
|
190
|
+
"note": "Both in-page and iframe shapes stay (Tencent moved in-host on 2026-08-11; see TRIBAL_KNOWLEDGE.md), and the iframe id is prefix-anchored: `[id*=tcaptcha]` also matched MTCaptcha's iframe and hid that `.mtcap` matched nothing.",
|
|
191
|
+
"hosts": [
|
|
192
|
+
"captcha.gtimg.com",
|
|
193
|
+
"captcha.qcloud.com"
|
|
194
|
+
],
|
|
195
|
+
"widget": [
|
|
196
|
+
"#tcaptcha_transform_dy",
|
|
197
|
+
"#tCaptchaDyContent",
|
|
198
|
+
".tencent-captcha-dy__content",
|
|
199
|
+
"iframe#tcaptcha_iframe_dy",
|
|
200
|
+
"iframe[id^=\"tcaptcha\"]",
|
|
201
|
+
"iframe[src*=\"captcha.gtimg.com\"]",
|
|
202
|
+
"iframe[src*=\"captcha.qq.com\"]"
|
|
203
|
+
],
|
|
204
|
+
"slider_handle": [
|
|
205
|
+
".tencent-captcha-dy__slider-block",
|
|
206
|
+
"#tcaptcha_drag_thumb",
|
|
207
|
+
".tc-slider-normal",
|
|
208
|
+
"[id*=slideBlock]"
|
|
209
|
+
],
|
|
210
|
+
"piece": [
|
|
211
|
+
".tencent-captcha-dy__fg-item"
|
|
212
|
+
],
|
|
213
|
+
"refresh": [
|
|
214
|
+
".tencent-captcha-dy__footer-icon--refresh"
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
"yidun": {
|
|
218
|
+
"hosts": [
|
|
219
|
+
"dun.163.com",
|
|
220
|
+
"cstaticdun.126.net",
|
|
221
|
+
"necaptcha.nosdn.127.net"
|
|
222
|
+
],
|
|
223
|
+
"widget": [
|
|
224
|
+
".yidun_panel",
|
|
225
|
+
".yidun"
|
|
226
|
+
],
|
|
227
|
+
"slider_handle": [
|
|
228
|
+
".yidun_slider",
|
|
229
|
+
".yidun_jigsaw"
|
|
230
|
+
],
|
|
231
|
+
"piece": [
|
|
232
|
+
".yidun_jigsaw"
|
|
233
|
+
],
|
|
234
|
+
"refresh": [
|
|
235
|
+
".yidun_refresh"
|
|
236
|
+
]
|
|
237
|
+
},
|
|
238
|
+
"yandex": {
|
|
239
|
+
"hosts": [
|
|
240
|
+
"smartcaptcha.yandexcloud.net"
|
|
241
|
+
],
|
|
242
|
+
"widget": [
|
|
243
|
+
"iframe[src*=\"smartcaptcha.yandexcloud.net/advanced\"]",
|
|
244
|
+
"iframe[src*=\"smartcaptcha.yandexcloud.net\"]",
|
|
245
|
+
".CheckboxCaptcha"
|
|
246
|
+
],
|
|
247
|
+
"text_input": [
|
|
248
|
+
".AdvancedCaptcha-Input input",
|
|
249
|
+
"input.Textinput-Control",
|
|
250
|
+
"input[name=\"rep\"]"
|
|
251
|
+
]
|
|
252
|
+
},
|
|
253
|
+
"lemin": {
|
|
254
|
+
"hosts": [
|
|
255
|
+
"leminnow.com"
|
|
256
|
+
],
|
|
257
|
+
"widget": [
|
|
258
|
+
"#lemin-cropped-captcha",
|
|
259
|
+
".lemin-captcha-popup"
|
|
260
|
+
],
|
|
261
|
+
"slider_handle": [
|
|
262
|
+
".lemin-slider-handle",
|
|
263
|
+
"#lemin-cropped-captcha .slider"
|
|
264
|
+
],
|
|
265
|
+
"piece": [
|
|
266
|
+
".lemin-cropped-puzzle-piece",
|
|
267
|
+
"#lemin-cropped-captcha canvas + canvas"
|
|
268
|
+
]
|
|
269
|
+
},
|
|
270
|
+
"prosopo": {
|
|
271
|
+
"hosts": [
|
|
272
|
+
"prosopo.io"
|
|
273
|
+
],
|
|
274
|
+
"widget": [
|
|
275
|
+
".prosopo-modalInner",
|
|
276
|
+
".procaptcha-checkbox"
|
|
277
|
+
]
|
|
278
|
+
},
|
|
279
|
+
"mtcaptcha": {
|
|
280
|
+
"hosts": [
|
|
281
|
+
"mtcaptcha.com"
|
|
282
|
+
],
|
|
283
|
+
"widget": [
|
|
284
|
+
"iframe[src*=\"service.mtcaptcha.com\"]",
|
|
285
|
+
"iframe[id^=\"mtcaptcha-iframe\"]",
|
|
286
|
+
".mtcaptcha",
|
|
287
|
+
".mtcap"
|
|
288
|
+
],
|
|
289
|
+
"text_input": [
|
|
290
|
+
"input.mtcap-inputtext",
|
|
291
|
+
".mtcap input[type=text]"
|
|
292
|
+
],
|
|
293
|
+
"refresh": [
|
|
294
|
+
".mtcap-refresh"
|
|
295
|
+
]
|
|
296
|
+
},
|
|
297
|
+
"botdetect": {
|
|
298
|
+
"hosts": [],
|
|
299
|
+
"widget": [
|
|
300
|
+
".BDC_CaptchaDiv"
|
|
301
|
+
],
|
|
302
|
+
"text_input": [
|
|
303
|
+
"input[id*=captchaCode]",
|
|
304
|
+
"input#captchaCode",
|
|
305
|
+
"input[id*=validateCaptcha]",
|
|
306
|
+
".BDC_CaptchaDiv input[type=text]"
|
|
307
|
+
],
|
|
308
|
+
"refresh": [
|
|
309
|
+
"a[id*=\"ReloadLink\"]",
|
|
310
|
+
".BDC_ReloadIcon"
|
|
311
|
+
]
|
|
312
|
+
},
|
|
313
|
+
"human": {
|
|
314
|
+
"note": "The press-and-hold widget. Found by its host-page container whether the button is drawn inline or in the vendor's iframe inside it: the hold is performed on a photograph of the container, so it never needs the frame's DOM.",
|
|
315
|
+
"hosts": [],
|
|
316
|
+
"widget": [
|
|
317
|
+
"#px-captcha"
|
|
318
|
+
],
|
|
319
|
+
"hold_completes": true
|
|
320
|
+
},
|
|
321
|
+
"awswaf": {
|
|
322
|
+
"note": "The card is drawn in an open shadow root, which the locator engine pierces; its Confirm is found by the text probe.",
|
|
323
|
+
"hosts": [
|
|
324
|
+
"awswaf.com"
|
|
325
|
+
],
|
|
326
|
+
"widget": [
|
|
327
|
+
".amzn-captcha-modal"
|
|
328
|
+
]
|
|
329
|
+
},
|
|
330
|
+
"captchafox": {
|
|
331
|
+
"note": "The puzzle opens in a modal over the \"I am human\" box, which stays on screen, so the modal is a challenge: a widget would be outranked by the box and the box clicked again.",
|
|
332
|
+
"hosts": [
|
|
333
|
+
"captchafox.com"
|
|
334
|
+
],
|
|
335
|
+
"challenge": [
|
|
336
|
+
".cf-modal__challenge"
|
|
337
|
+
],
|
|
338
|
+
"checkbox": [
|
|
339
|
+
"div[id^=\"cf-widget-\"]"
|
|
340
|
+
],
|
|
341
|
+
"response": "[name=\"cf-captcha-response\"]",
|
|
342
|
+
"slider_handle": [
|
|
343
|
+
".cf-slider__button"
|
|
344
|
+
]
|
|
345
|
+
},
|
|
346
|
+
"dingxiang": {
|
|
347
|
+
"note": "Every mode opens the same popup card. The trigger bar under it has no box to tick and is not a widget.",
|
|
348
|
+
"hosts": [
|
|
349
|
+
"cdn.dingxiang-inc.com/ctu-group/captcha",
|
|
350
|
+
"cap.dingxiang-inc.com"
|
|
351
|
+
],
|
|
352
|
+
"widget": [
|
|
353
|
+
".dx_captcha_oneclick_one-step"
|
|
354
|
+
],
|
|
355
|
+
"slider_handle": [
|
|
356
|
+
".dx_captcha_basic_slider",
|
|
357
|
+
".dx_captcha_rotate_slider"
|
|
358
|
+
]
|
|
359
|
+
},
|
|
360
|
+
"funcaptcha": {
|
|
361
|
+
"note": "The host page carries the outer frame (iframe.arkoselabs.com/<key>/index.html) and the game is two frames inside it; the game frame's own path is listed so a page that frames the game directly is found too.",
|
|
362
|
+
"hosts": [
|
|
363
|
+
"arkoselabs.com"
|
|
364
|
+
],
|
|
365
|
+
"widget": [
|
|
366
|
+
"iframe[src*=\"arkoselabs.com\"]",
|
|
367
|
+
"iframe[src*=\"ec-game-core/game-core\"]"
|
|
368
|
+
],
|
|
369
|
+
"hold_completes": true
|
|
370
|
+
},
|
|
371
|
+
"proton": {
|
|
372
|
+
"note": "Proton frames its own captcha at /core/v4/captcha, on account-api.proton.me or under the app's own /api/.",
|
|
373
|
+
"hosts": [
|
|
374
|
+
"/core/v4/captcha"
|
|
375
|
+
],
|
|
376
|
+
"widget": [
|
|
377
|
+
"iframe[src*=\"/core/v4/captcha\"]"
|
|
378
|
+
]
|
|
379
|
+
},
|
|
380
|
+
"unknown": {
|
|
381
|
+
"hosts": []
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
"generic": {
|
|
385
|
+
"submit_text": [
|
|
386
|
+
"verify",
|
|
387
|
+
"next",
|
|
388
|
+
"submit",
|
|
389
|
+
"skip",
|
|
390
|
+
"confirm"
|
|
391
|
+
],
|
|
392
|
+
"text_input": [
|
|
393
|
+
"input[name*=\"captcha\" i]",
|
|
394
|
+
"input[id*=\"captcha\" i]",
|
|
395
|
+
"input[aria-label*=\"captcha\" i]",
|
|
396
|
+
"input[placeholder*=\"code\" i]",
|
|
397
|
+
"input[autocomplete=\"off\"][type=text]",
|
|
398
|
+
"input[type=text]",
|
|
399
|
+
"input:not([type])",
|
|
400
|
+
"input[type=tel]",
|
|
401
|
+
"textarea"
|
|
402
|
+
],
|
|
403
|
+
"slider_handle": [
|
|
404
|
+
"[role=\"slider\"]",
|
|
405
|
+
"[aria-valuenow]",
|
|
406
|
+
"[class*=\"slider\"][class*=\"btn\"]",
|
|
407
|
+
"[class*=\"slider\"][class*=\"button\"]",
|
|
408
|
+
"[class*=\"slide\"][class*=\"handle\"]",
|
|
409
|
+
"[class*=\"drag\"][class*=\"thumb\"]"
|
|
410
|
+
],
|
|
411
|
+
"piece": [
|
|
412
|
+
"[class*=\"puzzle\"][class*=\"piece\"]",
|
|
413
|
+
"[class*=\"jigsaw\"]"
|
|
414
|
+
]
|
|
415
|
+
}
|
|
416
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""The `captchakraken` CLI: one-shot solves, the
|
|
1
|
+
"""The `captchakraken` CLI: one-shot solves, the detector tool calls, the CV worker, and server management.
|
|
2
2
|
|
|
3
3
|
`captchakraken image.png [model] [api_provider] [api_key] [--puzzle-source ...]` solves one still.
|
|
4
4
|
Exit codes: 1 failure, 2 unsupported puzzle, 3 the hosted API refused.
|
|
@@ -16,7 +16,6 @@ from .errors import CaptchaKrakenAPIError
|
|
|
16
16
|
from .kinds import Outcome, PromptFamily, RetryMode, Vendor
|
|
17
17
|
from .solver import CaptchaSolver, UnsupportedCaptchaError
|
|
18
18
|
|
|
19
|
-
|
|
20
19
|
_SELF_PRINTED = object()
|
|
21
20
|
|
|
22
21
|
|
|
@@ -59,12 +58,10 @@ def match_region(ref: str, live: str, cx: float, cy: float, tolerance: Optional[
|
|
|
59
58
|
|
|
60
59
|
The neighbourhood only: a whole-frame match would need every unrelated sprite to align and would essentially never open.
|
|
61
60
|
"""
|
|
62
|
-
import
|
|
63
|
-
|
|
64
|
-
from .keyframes import MATCH_REGION_TOLERANCE, region_box, region_diff_ratio
|
|
61
|
+
from .keyframes import MATCH_REGION_TOLERANCE, read_frame, region_box, region_diff_ratio
|
|
65
62
|
|
|
66
63
|
tol = MATCH_REGION_TOLERANCE if tolerance is None else float(tolerance)
|
|
67
|
-
a, b =
|
|
64
|
+
a, b = read_frame(ref), read_frame(live)
|
|
68
65
|
if a is None or b is None:
|
|
69
66
|
return {"match": False, "diff": 1.0, "error": "unreadable image"}
|
|
70
67
|
d = region_diff_ratio(a, b, region_box(a.shape[1::-1], (cx, cy)))
|
|
@@ -77,12 +74,36 @@ def board_painted(image: str, floor=None, box=None) -> dict:
|
|
|
77
74
|
return board_is_painted(image, floor, box)
|
|
78
75
|
|
|
79
76
|
|
|
80
|
-
def
|
|
81
|
-
|
|
77
|
+
def slide_step(req: dict) -> dict:
|
|
78
|
+
"""One look of the slide loop (`tool_calls/slide_tracker.py`); a lost or unseen object is `{"error"}`, never a
|
|
79
|
+
position, so the driver releases rather than steering by a guess."""
|
|
80
|
+
from .tool_calls.slide_tracker import TrackingError
|
|
81
|
+
from .tool_calls.slide_tracker import slide_step as step
|
|
82
|
+
|
|
83
|
+
def pt(v):
|
|
84
|
+
return None if v is None else (float(v[0]), float(v[1]))
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
return step(req["first"], req["probe"], req["current"], probe_s=req["probe_s"], s=req["s"],
|
|
88
|
+
goal=pt(req["goal"]), seed=pt(req.get("seed")), handle=pt(req.get("handle")),
|
|
89
|
+
bounds=tuple(req["bounds"]) if req.get("bounds") else None,
|
|
90
|
+
history=req.get("history") or (), tol=float(req.get("tol") or 2.0))
|
|
91
|
+
except TrackingError as e:
|
|
92
|
+
return {"error": str(e)}
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
|
|
95
|
+
def fill_step(req: dict) -> dict:
|
|
96
|
+
"""One look at a held widget's fill (`tool_calls/fill_tracker.py`); an unreadable line is `{"error"}`, never a
|
|
97
|
+
release, so the driver keeps holding on the widget's own signals."""
|
|
98
|
+
from .tool_calls.fill_tracker import FillError
|
|
99
|
+
from .tool_calls.fill_tracker import fill_step as step
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
return step(req["first"], req["current"], start=tuple(req["start"]), end=tuple(req["end"]), t=float(req["t"]),
|
|
103
|
+
history=req.get("history") or (), poll_s=float(req.get("poll_s") or 0.25),
|
|
104
|
+
tol=float(req.get("tol") or 2.0), first_t=req.get("first_t"))
|
|
105
|
+
except FillError as e:
|
|
106
|
+
return {"error": str(e)}
|
|
86
107
|
|
|
87
108
|
|
|
88
109
|
def _find_grid(args):
|
|
@@ -164,10 +185,16 @@ def _board_painted(args):
|
|
|
164
185
|
return board_painted(args[0], float(args[1]) if len(args) > 1 else None)
|
|
165
186
|
|
|
166
187
|
|
|
167
|
-
def
|
|
168
|
-
_need(args,
|
|
169
|
-
|
|
170
|
-
|
|
188
|
+
def _slide_step(args):
|
|
189
|
+
_need(args, 1, "slide-step request.json (or the JSON itself)")
|
|
190
|
+
raw = args[0]
|
|
191
|
+
return slide_step(json.loads(open(raw).read() if os.path.exists(raw) else raw))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _fill_step(args):
|
|
195
|
+
_need(args, 1, "fill-step request.json (or the JSON itself)")
|
|
196
|
+
raw = args[0]
|
|
197
|
+
return fill_step(json.loads(open(raw).read() if os.path.exists(raw) else raw))
|
|
171
198
|
|
|
172
199
|
|
|
173
200
|
def _report_outcome(args):
|
|
@@ -212,8 +239,10 @@ def _serve(args):
|
|
|
212
239
|
return board_painted(req["image"], req.get("floor"), req.get("box"))
|
|
213
240
|
if cmd == "find-checkbox":
|
|
214
241
|
return find_checkbox(req["image"])
|
|
215
|
-
if cmd == "
|
|
216
|
-
return
|
|
242
|
+
if cmd == "slide-step":
|
|
243
|
+
return slide_step(req)
|
|
244
|
+
if cmd == "fill-step":
|
|
245
|
+
return fill_step(req)
|
|
217
246
|
raise ValueError(f"unknown cmd: {cmd!r}")
|
|
218
247
|
|
|
219
248
|
sys.stdout.write(json.dumps({"ready": True}) + "\n")
|
|
@@ -249,7 +278,7 @@ def _run_solve(query: Callable[[], dict]) -> None:
|
|
|
249
278
|
|
|
250
279
|
|
|
251
280
|
def _solve_animated(args):
|
|
252
|
-
"""
|
|
281
|
+
"""Solve a recorded burst (zero-padded PNGs in `--frames-dir`) as the model's prompt generation asks it."""
|
|
253
282
|
import glob
|
|
254
283
|
|
|
255
284
|
parser = argparse.ArgumentParser(prog="captchakraken solve-animated")
|
|
@@ -264,20 +293,21 @@ def _solve_animated(args):
|
|
|
264
293
|
_fail(f"no frames in {opts.frames_dir}")
|
|
265
294
|
|
|
266
295
|
def query():
|
|
267
|
-
import
|
|
268
|
-
|
|
269
|
-
from .keyframes import extract_keyframes, write_keyframes
|
|
296
|
+
from .keyframes import read_frame
|
|
297
|
+
from .solver import cut_recording
|
|
270
298
|
|
|
271
|
-
imgs = [im for im in (
|
|
299
|
+
imgs = [im for im in (read_frame(p) for p in frames) if im is not None]
|
|
272
300
|
if not imgs:
|
|
273
301
|
_fail("no frame decoded")
|
|
274
|
-
|
|
275
|
-
paths = [os.path.abspath(p) for p in write_keyframes(kfset, os.path.join(opts.frames_dir, "keyframes"), stem="challenge")]
|
|
302
|
+
recording = cut_recording(imgs, float(opts.fps), os.path.abspath(os.path.join(opts.frames_dir, "keyframes")))
|
|
276
303
|
solver = CaptchaSolver(model=opts.model, api_key=opts.api_key, expert=opts.expert)
|
|
277
|
-
result = solver.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
"
|
|
304
|
+
result = solver.solve_animated(recording)
|
|
305
|
+
kfset = recording.keyframes
|
|
306
|
+
return {"actions": [a.model_dump() for a in result],
|
|
307
|
+
"fallbacks": [[a.model_dump() for a in plan] for plan in solver.pointer_fallback_actions()],
|
|
308
|
+
"token_usage": solver.planner.token_usage,
|
|
309
|
+
"keyframes": recording.keyframe_paths, "keyframe_mode": kfset.mode,
|
|
310
|
+
"steady_screens": kfset.steady_screens, "source_frames": len(imgs)}
|
|
281
311
|
|
|
282
312
|
_run_solve(query)
|
|
283
313
|
return _SELF_PRINTED
|
|
@@ -332,7 +362,8 @@ COMMANDS: Dict[str, Callable[[List[str]], Any]] = {
|
|
|
332
362
|
"grid-cell-states-fixed": _grid_cell_states_fixed,
|
|
333
363
|
"match-region": _match_region,
|
|
334
364
|
"board-painted": _board_painted,
|
|
335
|
-
"
|
|
365
|
+
"slide-step": _slide_step,
|
|
366
|
+
"fill-step": _fill_step,
|
|
336
367
|
"report-outcome": _report_outcome,
|
|
337
368
|
"serve": _serve,
|
|
338
369
|
"solve-animated": _solve_animated,
|
|
@@ -371,15 +402,20 @@ def main():
|
|
|
371
402
|
help="Force one expert of a routed model. Also CAPTCHA_EXPERT.")
|
|
372
403
|
parser.add_argument("--text-mode", action="store_true",
|
|
373
404
|
help="The widget has a text box, so the answer is typed rather than clicked.")
|
|
405
|
+
parser.add_argument("--history", action="append", default=[],
|
|
406
|
+
help="An earlier screen of the same paged puzzle, oldest first (repeatable).")
|
|
374
407
|
args = parser.parse_args()
|
|
375
408
|
_image(args.image_path)
|
|
376
409
|
|
|
377
410
|
def query():
|
|
378
411
|
solver = CaptchaSolver(model=args.model, api_key=args.api_key, expert=args.expert)
|
|
412
|
+
solver.planner.pointer_history = list(args.history)
|
|
379
413
|
result = solver.solve(args.image_path, puzzle_source=args.puzzle_source,
|
|
380
414
|
retry_mode=args.retry_mode, text_mode=args.text_mode)
|
|
381
415
|
actions = [a.model_dump() for a in result] if isinstance(result, list) else result.model_dump()
|
|
382
|
-
return {"actions": actions,
|
|
416
|
+
return {"actions": actions,
|
|
417
|
+
"fallbacks": [[a.model_dump() for a in plan] for plan in solver.pointer_fallback_actions()],
|
|
418
|
+
"token_usage": solver.planner.token_usage}
|
|
383
419
|
|
|
384
420
|
_run_solve(query)
|
|
385
421
|
|