captchakraken 3.1.0 → 3.2.1
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 +4 -4
- package/LICENSE +1 -1
- package/README.md +10 -13
- package/dist/index.d.ts +1 -0
- package/dist/kinds.d.ts +8 -0
- package/dist/kinds.js +3 -1
- package/dist/model-name.d.ts +6 -0
- package/dist/model-name.js +28 -0
- package/dist/playwright-types.d.ts +18 -0
- package/dist/png.d.ts +16 -0
- package/dist/png.js +119 -0
- package/dist/puppeteer-adapter.d.ts +7 -1
- package/dist/puppeteer-adapter.js +6 -0
- package/dist/selectors.d.ts +9 -0
- package/dist/selectors.js +11 -3
- package/dist/solver.d.ts +76 -4
- package/dist/solver.js +509 -176
- package/dist/types.d.ts +10 -5
- package/dist/verdicts.d.ts +45 -0
- package/dist/verdicts.js +125 -0
- package/package.json +7 -7
- package/python/AGENTS.md +13 -8
- package/python/LICENSE +1 -1
- package/python/README.md +10 -13
- package/python/pyproject.toml +6 -6
- package/python/src/captchakraken/__init__.py +5 -1
- package/python/src/captchakraken/cli.py +7 -1
- package/python/src/captchakraken/kinds.py +9 -0
- package/python/src/captchakraken/page_solver.py +472 -167
- package/python/src/captchakraken/planner.py +52 -11
- package/python/src/captchakraken/prompts.py +24 -0
- package/python/src/captchakraken/selectors.py +19 -2
- package/python/src/captchakraken/tool_calls/find_checkbox.py +4 -0
- package/python/src/captchakraken/tool_calls/find_grid.py +22 -0
- package/python/src/captchakraken/verdicts.py +154 -0
- package/python/Dockerfile +0 -41
- package/python/examples/README.md +0 -114
- package/python/examples/_harness.py +0 -177
- package/python/examples/demoHcaptcha.py +0 -8
- package/python/examples/demoRecaptcha.py +0 -8
- package/python/examples/watch_playwright.py +0 -31
- package/python/examples/with_local_gguf.py +0 -28
- package/python/examples/with_playwright.py +0 -24
- package/scripts/copy-python.mjs +0 -29
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
import time
|
|
6
|
-
from dataclasses import dataclass, replace
|
|
7
|
-
|
|
8
|
-
from camoufox.sync_api import Camoufox
|
|
9
|
-
|
|
10
|
-
from captchakraken.page_solver import CaptchaSolveError, PageSolver
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@dataclass
|
|
14
|
-
class DemoSpec:
|
|
15
|
-
name: str
|
|
16
|
-
url: str
|
|
17
|
-
vendor: str
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def spec_from_argv(default: DemoSpec, argv=None) -> DemoSpec:
|
|
21
|
-
ap = argparse.ArgumentParser(
|
|
22
|
-
description=f"CaptchaKraken demo — {default.name}",
|
|
23
|
-
epilog="With no URL, runs against the built-in demo page.")
|
|
24
|
-
ap.add_argument("url", nargs="?", default=default.url,
|
|
25
|
-
help=f"page to solve (default: {default.url})")
|
|
26
|
-
ap.add_argument("--vendor", default=default.vendor,
|
|
27
|
-
choices=("hcaptcha", "recaptcha"),
|
|
28
|
-
help="only affects failure wording; the solver auto-detects")
|
|
29
|
-
ap.add_argument("--name", default=None, help="label for the report")
|
|
30
|
-
ap.add_argument("--headed", action="store_true",
|
|
31
|
-
help="show the browser (same as HEADLESS=0)")
|
|
32
|
-
args = ap.parse_args(argv)
|
|
33
|
-
|
|
34
|
-
if args.headed:
|
|
35
|
-
os.environ["HEADLESS"] = "0"
|
|
36
|
-
name = args.name or (default.name if args.url == default.url else args.url)
|
|
37
|
-
return replace(default, url=args.url, vendor=args.vendor, name=name)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _launch_kwargs() -> dict:
|
|
41
|
-
kw = dict(headless=os.getenv("HEADLESS", "1") != "0",
|
|
42
|
-
humanize=os.getenv("HUMANIZE", "0") != "0", geoip=False)
|
|
43
|
-
binary = os.getenv("CAMOUFOX_BINARY") or os.getenv("CAMOUFOX_EXECUTABLE_PATH")
|
|
44
|
-
if binary:
|
|
45
|
-
kw["executable_path"] = binary
|
|
46
|
-
return kw
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _page_kwargs() -> dict:
|
|
50
|
-
out = os.getenv("CAPTCHA_DEMO_VIDEO_DIR")
|
|
51
|
-
if not out:
|
|
52
|
-
return {}
|
|
53
|
-
w, _, h = os.getenv("CAPTCHA_DEMO_VIDEO_SIZE", "1280x800").partition("x")
|
|
54
|
-
return {"record_video_dir": out,
|
|
55
|
-
"record_video_size": {"width": int(w), "height": int(h)}}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _finish_video(page):
|
|
59
|
-
video = getattr(page, "video", None)
|
|
60
|
-
if video is None:
|
|
61
|
-
return None
|
|
62
|
-
try:
|
|
63
|
-
page.context.close()
|
|
64
|
-
return video.path()
|
|
65
|
-
except Exception:
|
|
66
|
-
return None
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _tokens(usage) -> tuple[int, int]:
|
|
70
|
-
inp = out = 0
|
|
71
|
-
for u in usage or []:
|
|
72
|
-
inp += int(u.get("prompt_tokens", u.get("inputTokens", 0)) or 0)
|
|
73
|
-
out += int(u.get("completion_tokens", u.get("outputTokens", 0)) or 0)
|
|
74
|
-
return inp, out
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _explain(vendor: str, solved: bool, err) -> str:
|
|
78
|
-
msg = str(err or "").lower()
|
|
79
|
-
if "unsupported" in msg or "cannot solve" in msg:
|
|
80
|
-
if vendor == "hcaptcha":
|
|
81
|
-
return ("hCaptcha served a challenge type the solver does not handle "
|
|
82
|
-
"yet (drag / video / choose-the-card) — re-run for a grid.")
|
|
83
|
-
return "Not a supported challenge type — re-run to try again."
|
|
84
|
-
if "no captcha" in msg or "nocaptchafound" in msg:
|
|
85
|
-
return ("No captcha widget was found on that page. Check the URL, or "
|
|
86
|
-
"the widget may load only after an interaction.")
|
|
87
|
-
if "vllm" in msg or "connection" in msg or "refused" in msg or "max retries" in msg:
|
|
88
|
-
return ("Could not reach the vLLM server — is it up and is VLLM_BASE_URL "
|
|
89
|
-
"correct? (A local server auto-starts only with captchakraken[serve].)")
|
|
90
|
-
if "timeout" in msg:
|
|
91
|
-
return "Timed out — the page or challenge never became interactable."
|
|
92
|
-
if err:
|
|
93
|
-
return str(err)
|
|
94
|
-
if not solved:
|
|
95
|
-
return ("The answer was submitted but the vendor did not accept it. Most "
|
|
96
|
-
"often IP reputation or fingerprint flagging rather than a wrong "
|
|
97
|
-
"answer — try a cleaner IP or a residential proxy.")
|
|
98
|
-
return "Unknown failure."
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def run_demo(spec: DemoSpec) -> None:
|
|
102
|
-
spec = spec_from_argv(spec)
|
|
103
|
-
|
|
104
|
-
t0 = time.time()
|
|
105
|
-
ok = False
|
|
106
|
-
reason = None
|
|
107
|
-
inp = out = 0
|
|
108
|
-
solve_s = 0.0
|
|
109
|
-
video_path = None
|
|
110
|
-
err = None
|
|
111
|
-
|
|
112
|
-
try:
|
|
113
|
-
with Camoufox(**_launch_kwargs()) as browser:
|
|
114
|
-
page = browser.new_page(**_page_kwargs())
|
|
115
|
-
try:
|
|
116
|
-
page.goto(spec.url, wait_until="domcontentloaded", timeout=60_000)
|
|
117
|
-
try:
|
|
118
|
-
page.wait_for_load_state("networkidle", timeout=15_000)
|
|
119
|
-
except Exception:
|
|
120
|
-
pass
|
|
121
|
-
page.wait_for_timeout(3000)
|
|
122
|
-
|
|
123
|
-
s0 = time.time()
|
|
124
|
-
try:
|
|
125
|
-
result = PageSolver().solve(page)
|
|
126
|
-
finally:
|
|
127
|
-
solve_s = time.time() - s0
|
|
128
|
-
ok = bool(result.is_solved)
|
|
129
|
-
inp, out = _tokens(result.token_usage)
|
|
130
|
-
if not ok:
|
|
131
|
-
reason = _explain(spec.vendor, ok, None)
|
|
132
|
-
except CaptchaSolveError as e:
|
|
133
|
-
err = e
|
|
134
|
-
reason = _explain(spec.vendor, False, e)
|
|
135
|
-
finally:
|
|
136
|
-
video_path = _finish_video(page)
|
|
137
|
-
except Exception as e:
|
|
138
|
-
err = e
|
|
139
|
-
reason = _explain(spec.vendor, False, e)
|
|
140
|
-
|
|
141
|
-
tps = out / solve_s if out and solve_s > 0 else 0.0
|
|
142
|
-
_report(spec, ok=ok, total_s=time.time() - t0, solve_s=solve_s,
|
|
143
|
-
inp=inp, out=out, tps=tps, reason=reason, video=video_path)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def _fmt(s: float) -> str:
|
|
147
|
-
return f"{s:.1f}s" if s >= 1 else f"{int(s * 1000)}ms"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _report(spec, *, ok, total_s, solve_s, inp, out, tps, reason, video=None):
|
|
151
|
-
line = "─" * 52
|
|
152
|
-
print(f"\n{line}")
|
|
153
|
-
print(f" CaptchaKraken demo — {spec.name}")
|
|
154
|
-
print(f" {spec.url}")
|
|
155
|
-
print(line)
|
|
156
|
-
print(f" result : {'✓ SOLVED' if ok else '✗ not solved'}")
|
|
157
|
-
print(f" total time : {_fmt(total_s)} (solve: {_fmt(solve_s)})")
|
|
158
|
-
print(f" tokens : {inp} in / {out} out")
|
|
159
|
-
print(f" gen speed : {f'~{tps:.1f} tok/s' if tps > 0 else 'n/a'}")
|
|
160
|
-
if video:
|
|
161
|
-
print(f" video : {video}")
|
|
162
|
-
if reason:
|
|
163
|
-
print(f" reason : {reason}")
|
|
164
|
-
print(f"{line}\n")
|
|
165
|
-
|
|
166
|
-
record = os.getenv("CAPTCHA_DEMO_RECORD")
|
|
167
|
-
if record:
|
|
168
|
-
with open(record, "a", encoding="utf-8") as fh:
|
|
169
|
-
fh.write(json.dumps({
|
|
170
|
-
"demo": spec.name, "url": spec.url, "vendor": spec.vendor,
|
|
171
|
-
"ok": ok, "reason": reason,
|
|
172
|
-
"total_s": round(total_s, 1), "solve_s": round(solve_s, 1),
|
|
173
|
-
"input_tokens": inp, "output_tokens": out,
|
|
174
|
-
"tokens_per_sec": round(tps, 1), "video": video,
|
|
175
|
-
}) + "\n")
|
|
176
|
-
|
|
177
|
-
sys.exit(0 if ok else 1)
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
|
|
3
|
-
from playwright.sync_api import sync_playwright
|
|
4
|
-
|
|
5
|
-
from captchakraken import PageSolver
|
|
6
|
-
|
|
7
|
-
URL = sys.argv[1] if len(sys.argv) > 1 else "https://www.google.com/recaptcha/api2/demo"
|
|
8
|
-
SECONDS = int(sys.argv[2]) if len(sys.argv) > 2 else 30
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def main() -> int:
|
|
12
|
-
with sync_playwright() as p:
|
|
13
|
-
browser = p.chromium.launch(headless=True)
|
|
14
|
-
page = browser.new_page()
|
|
15
|
-
|
|
16
|
-
watcher = PageSolver().watch(
|
|
17
|
-
page,
|
|
18
|
-
on_solved=lambda r: print("✅ solved one:", r.is_solved),
|
|
19
|
-
on_error=lambda e: print("solve failed:", e),
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
page.goto(URL)
|
|
23
|
-
watcher.run(timeout_ms=SECONDS * 1000)
|
|
24
|
-
|
|
25
|
-
print(f"stopped after {watcher.solves} solve(s)")
|
|
26
|
-
browser.close()
|
|
27
|
-
return 0 if watcher.solves else 1
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if __name__ == "__main__":
|
|
31
|
-
raise SystemExit(main())
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
from captchakraken import solve_captcha
|
|
5
|
-
|
|
6
|
-
TEXT_CAPTCHAS = {"--text"}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def main() -> int:
|
|
10
|
-
args = [a for a in sys.argv[1:] if a not in TEXT_CAPTCHAS]
|
|
11
|
-
if not args:
|
|
12
|
-
print(__doc__)
|
|
13
|
-
return 2
|
|
14
|
-
image = args[0]
|
|
15
|
-
text_mode = bool(TEXT_CAPTCHAS & set(sys.argv[1:]))
|
|
16
|
-
|
|
17
|
-
print(f"endpoint : {os.getenv('VLLM_BASE_URL', '(default)')}")
|
|
18
|
-
print(f"model : {os.getenv('CAPTCHA_LORA_NAME', '(registry default)')}")
|
|
19
|
-
print(f"prompts : {os.getenv('CAPTCHA_LORA_ADAPTER', '(latest)')}")
|
|
20
|
-
|
|
21
|
-
result = solve_captcha(image, text_mode=text_mode)
|
|
22
|
-
|
|
23
|
-
print(f"answer : {result}")
|
|
24
|
-
return 0 if result else 1
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if __name__ == "__main__":
|
|
28
|
-
raise SystemExit(main())
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
|
|
3
|
-
from playwright.sync_api import sync_playwright
|
|
4
|
-
|
|
5
|
-
from captchakraken import PageSolver
|
|
6
|
-
|
|
7
|
-
URL = sys.argv[1] if len(sys.argv) > 1 else "https://www.google.com/recaptcha/api2/demo"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def main() -> int:
|
|
11
|
-
with sync_playwright() as p:
|
|
12
|
-
browser = p.chromium.launch(headless=True)
|
|
13
|
-
page = browser.new_page()
|
|
14
|
-
page.goto(URL)
|
|
15
|
-
|
|
16
|
-
result = PageSolver().solve(page)
|
|
17
|
-
|
|
18
|
-
print("✅ solved" if result.is_solved else "❌ not solved")
|
|
19
|
-
browser.close()
|
|
20
|
-
return 0 if result.is_solved else 1
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if __name__ == "__main__":
|
|
24
|
-
raise SystemExit(main())
|
package/scripts/copy-python.mjs
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { cpSync, rmSync, existsSync } from 'node:fs'
|
|
2
|
-
import { dirname, resolve } from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
-
|
|
5
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
6
|
-
const src = resolve(here, '../../python')
|
|
7
|
-
const dest = resolve(here, '../python')
|
|
8
|
-
|
|
9
|
-
if (!existsSync(src)) {
|
|
10
|
-
console.error(`[copy-python] source engine not found at ${src}`)
|
|
11
|
-
process.exit(1)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const SKIP = new Set([
|
|
15
|
-
'.venv', '__pycache__', 'dist', 'build', '.pytest_cache', '.ruff_cache', 'tests',
|
|
16
|
-
'coverage', 'htmlcov', '.nyc_output', 'coverage.xml', 'lcov.info',
|
|
17
|
-
|
|
18
|
-
'latestDebugRun',
|
|
19
|
-
])
|
|
20
|
-
|
|
21
|
-
rmSync(dest, { recursive: true, force: true })
|
|
22
|
-
cpSync(src, dest, {
|
|
23
|
-
recursive: true,
|
|
24
|
-
filter: (p) =>
|
|
25
|
-
!p
|
|
26
|
-
.split(/[\\/]/)
|
|
27
|
-
.some((seg) => SKIP.has(seg) || seg.endsWith('.egg-info') || seg.startsWith('.coverage')),
|
|
28
|
-
})
|
|
29
|
-
console.log('[copy-python] bundled python/ engine -> js/python/')
|