captchakraken 2.0.0__tar.gz
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.
- captchakraken-2.0.0/.gitignore +44 -0
- captchakraken-2.0.0/Dockerfile +41 -0
- captchakraken-2.0.0/PKG-INFO +102 -0
- captchakraken-2.0.0/README.md +71 -0
- captchakraken-2.0.0/examples/README.md +68 -0
- captchakraken-2.0.0/examples/_harness.py +158 -0
- captchakraken-2.0.0/examples/demoHcaptcha.py +20 -0
- captchakraken-2.0.0/examples/demoRecaptcha.py +17 -0
- captchakraken-2.0.0/pyproject.toml +79 -0
- captchakraken-2.0.0/src/captchakraken/__init__.py +61 -0
- captchakraken-2.0.0/src/captchakraken/action_types.py +56 -0
- captchakraken-2.0.0/src/captchakraken/cli.py +656 -0
- captchakraken-2.0.0/src/captchakraken/config.py +78 -0
- captchakraken-2.0.0/src/captchakraken/image_processor.py +244 -0
- captchakraken-2.0.0/src/captchakraken/overlay.py +520 -0
- captchakraken-2.0.0/src/captchakraken/planner.py +408 -0
- captchakraken-2.0.0/src/captchakraken/planner_types.py +74 -0
- captchakraken-2.0.0/src/captchakraken/server_manager.py +290 -0
- captchakraken-2.0.0/src/captchakraken/solver.py +434 -0
- captchakraken-2.0.0/src/captchakraken/timing.py +42 -0
- captchakraken-2.0.0/src/captchakraken/tool_calls/find_checkbox.py +72 -0
- captchakraken-2.0.0/src/captchakraken/tool_calls/find_grid.py +1762 -0
- captchakraken-2.0.0/src/captchakraken/tool_calls/move_indicator.py +431 -0
- captchakraken-2.0.0/tests/conftest.py +25 -0
- captchakraken-2.0.0/tests/grid_diag.py +203 -0
- captchakraken-2.0.0/tests/grid_fp.py +55 -0
- captchakraken-2.0.0/tests/grid_regression.py +85 -0
- captchakraken-2.0.0/tests/grid_trace_reject.py +92 -0
- captchakraken-2.0.0/tests/test_checkbox_captcha.py +92 -0
- captchakraken-2.0.0/tests/test_find_grid_corpus.py +213 -0
- captchakraken-2.0.0/tests/test_grid_detection.py +91 -0
- captchakraken-2.0.0/tests/test_grid_detection_ci.py +98 -0
- captchakraken-2.0.0/tests/test_move_indicator.py +174 -0
- captchakraken-2.0.0/tests/test_object_detection.py +142 -0
- captchakraken-2.0.0/tests/test_selected_fields.py +122 -0
- captchakraken-2.0.0/tests/test_solver.py +208 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
node_modules
|
|
2
|
+
dist
|
|
3
|
+
*.log
|
|
4
|
+
.DS_Store
|
|
5
|
+
.env
|
|
6
|
+
.npmrc
|
|
7
|
+
test-results
|
|
8
|
+
playwright-report
|
|
9
|
+
debug_runs/
|
|
10
|
+
test_logs/
|
|
11
|
+
|
|
12
|
+
# generated by setup.sh
|
|
13
|
+
captchakraken.env
|
|
14
|
+
|
|
15
|
+
# python port
|
|
16
|
+
python/.venv
|
|
17
|
+
python/build/
|
|
18
|
+
python/dist/
|
|
19
|
+
**/__pycache__/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
|
|
24
|
+
# js port: the python engine is copied in at build time from the sibling
|
|
25
|
+
# repo-root python/ (single source of truth) — never commit the copy.
|
|
26
|
+
js/python/
|
|
27
|
+
js/dist/
|
|
28
|
+
js/node_modules/
|
|
29
|
+
|
|
30
|
+
# test-run artifacts
|
|
31
|
+
hcaptcha_5x_attempt_*.png
|
|
32
|
+
hcaptcha_5x_summary.json
|
|
33
|
+
captcha_video_summary.json
|
|
34
|
+
server_log.txt
|
|
35
|
+
|
|
36
|
+
# demo recorder (tests/record_demos.spec.ts) transients. Curated clips that the
|
|
37
|
+
# README embeds live in docs/demos/ (committed); everything else is throwaway.
|
|
38
|
+
record_demos_summary.json
|
|
39
|
+
record_demos_*_summary.json
|
|
40
|
+
record_demos*.log
|
|
41
|
+
run_full_record.out
|
|
42
|
+
run_full_record.sh
|
|
43
|
+
*_video_attempt_*.png
|
|
44
|
+
captcha_videos/
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# CaptchaKraken vLLM server image.
|
|
2
|
+
#
|
|
3
|
+
# Serves the base model + captcha LoRA over an OpenAI-compatible endpoint on
|
|
4
|
+
# :8000. The image bakes in the `captchakraken[serve]` stack and (optionally)
|
|
5
|
+
# the weights, then hands off to `captchakraken server run`, which execs
|
|
6
|
+
# `vllm serve` assembled entirely from the env-overridable config — so you can
|
|
7
|
+
# repoint it at any base model / adapter without editing this file.
|
|
8
|
+
#
|
|
9
|
+
# docker build -t captchakraken-vllm .
|
|
10
|
+
# docker run --gpus all -p 8000:8000 --ipc=host \
|
|
11
|
+
# -e VLLM_API_KEY=your_key captchakraken-vllm
|
|
12
|
+
FROM pytorch/pytorch:2.7.0-cuda12.8-cudnn9-devel
|
|
13
|
+
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
|
|
16
|
+
RUN apt-get update && apt-get install -y \
|
|
17
|
+
git ninja-build libgl1 libglib2.0-0 wget ffmpeg \
|
|
18
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
19
|
+
|
|
20
|
+
# Install the package + serving extra (vllm/torch/transformers/accelerate/hf).
|
|
21
|
+
COPY . /app/captchakraken
|
|
22
|
+
RUN pip install --upgrade pip && pip install "/app/captchakraken[serve]"
|
|
23
|
+
|
|
24
|
+
# Model identity — all overridable at build/run time (model-agnostic image).
|
|
25
|
+
ARG HF_TOKEN
|
|
26
|
+
ENV HF_TOKEN=${HF_TOKEN}
|
|
27
|
+
ENV CAPTCHA_BASE_MODEL="Qwen/Qwen3.5-9B"
|
|
28
|
+
ENV CAPTCHA_LORA_ADAPTER="CaptchaKraken/CaptchaKraken_v1"
|
|
29
|
+
ENV CAPTCHA_LORA_NAME="captcha"
|
|
30
|
+
ENV VLLM_GPU_MEMORY_UTILIZATION="0.80"
|
|
31
|
+
ENV VLLM_PORT=8000
|
|
32
|
+
|
|
33
|
+
# Bake weights into the image (best-effort; falls back to runtime download).
|
|
34
|
+
RUN python3 -c "import os; from huggingface_hub import snapshot_download; \
|
|
35
|
+
token = os.getenv('HF_TOKEN'); \
|
|
36
|
+
snapshot_download(os.getenv('CAPTCHA_BASE_MODEL'), token=token); \
|
|
37
|
+
snapshot_download(os.getenv('CAPTCHA_LORA_ADAPTER'), token=token)" \
|
|
38
|
+
|| echo "Warning: model prefetch failed; weights download at first run."
|
|
39
|
+
|
|
40
|
+
EXPOSE 8000
|
|
41
|
+
CMD ["captchakraken", "server", "run"]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: captchakraken
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Self-hosted captcha solver: OpenCV grid detection + a fine-tuned Qwen3.5-9B vision LoRA served on vLLM.
|
|
5
|
+
Project-URL: Homepage, https://github.com/JWriter20/CaptchaKraken
|
|
6
|
+
Project-URL: Issues, https://github.com/JWriter20/CaptchaKraken/issues
|
|
7
|
+
Author: Jake Writer
|
|
8
|
+
License: GPL-3.0-or-later
|
|
9
|
+
Keywords: automation,captcha,computer-vision,hcaptcha,qwen,recaptcha,vllm
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: numpy>=1.24.0
|
|
15
|
+
Requires-Dist: opencv-python-headless>=4.10.0
|
|
16
|
+
Requires-Dist: pillow>=10.0.0
|
|
17
|
+
Requires-Dist: pydantic>=2.0.0
|
|
18
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
19
|
+
Requires-Dist: requests>=2.31.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
24
|
+
Provides-Extra: serve
|
|
25
|
+
Requires-Dist: accelerate>=0.27.0; extra == 'serve'
|
|
26
|
+
Requires-Dist: huggingface-hub>=0.23.0; extra == 'serve'
|
|
27
|
+
Requires-Dist: torch>=2.0.0; extra == 'serve'
|
|
28
|
+
Requires-Dist: transformers>=4.40.0; extra == 'serve'
|
|
29
|
+
Requires-Dist: vllm>=0.6.3; extra == 'serve'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# captchakraken
|
|
33
|
+
|
|
34
|
+
The Python engine + CLI behind [CaptchaKraken](https://github.com/JWriter20/CaptchaKraken):
|
|
35
|
+
OpenCV grid detection + a fine-tuned **Qwen3.5-9B** vision LoRA served on
|
|
36
|
+
**vLLM**. Given a screenshot of a captcha grid, it locates the tiles and returns
|
|
37
|
+
the click plan. Ships the `captchakraken` command.
|
|
38
|
+
|
|
39
|
+
> For demo videos, accuracy numbers, the browser driver, and the full
|
|
40
|
+
> self-hosting guide, see the main repo
|
|
41
|
+
> **[CaptchaKraken](https://github.com/JWriter20/CaptchaKraken)**.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install captchakraken # client: OpenCV detection + vLLM HTTP planner
|
|
47
|
+
pip install "captchakraken[serve]" # + the serving stack (vLLM/torch) to self-host
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The base install is lightweight — everything you need to solve captchas against
|
|
51
|
+
a vLLM server (local or remote). The `[serve]` extra pulls the heavy stack only
|
|
52
|
+
if you want to run the model yourself. The one-command
|
|
53
|
+
[`setup.sh`](https://github.com/JWriter20/CaptchaKraken) installs `[serve]`,
|
|
54
|
+
downloads the weights, and writes an env file for you.
|
|
55
|
+
|
|
56
|
+
## Hands-off server
|
|
57
|
+
|
|
58
|
+
The vLLM server is managed for you. On your first solve, if the configured
|
|
59
|
+
endpoint is **local** and nothing is listening, a server is started
|
|
60
|
+
automatically and reused. Point `VLLM_BASE_URL` at a server you already run to
|
|
61
|
+
skip local management entirely.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
captchakraken server start | stop | status | run
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Solve an image/video: classify → find_grid → plan. Prints the click actions.
|
|
71
|
+
captchakraken path/to/captcha.png
|
|
72
|
+
captchakraken path/to/captcha.png --puzzle-source hcaptcha
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from captchakraken import CaptchaSolver
|
|
77
|
+
|
|
78
|
+
solver = CaptchaSolver() # connects to / auto-starts a local vLLM
|
|
79
|
+
actions = solver.solve("captcha.png")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Pure-OpenCV tool subcommands (no model): `find-grid`, `find-checkbox`,
|
|
83
|
+
`detect-selected`, `grid-cell-states`, `find-move`, `find-movable`, and a
|
|
84
|
+
persistent `serve` worker the browser driver polls.
|
|
85
|
+
|
|
86
|
+
## Configuration (model-agnostic)
|
|
87
|
+
|
|
88
|
+
Everything model-specific lives in `captchakraken.config` and is env-overridable
|
|
89
|
+
— the solver never hard-codes a model.
|
|
90
|
+
|
|
91
|
+
| Variable | Meaning | Default |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `VLLM_BASE_URL` | Inference endpoint | `http://localhost:8000/v1` |
|
|
94
|
+
| `CAPTCHA_KRAKEN_API_KEY` | Bearer token (`VLLM_API_KEY` also accepted) | `EMPTY` |
|
|
95
|
+
| `CAPTCHA_BASE_MODEL` | Base weights vLLM loads | `Qwen/Qwen3.5-9B` |
|
|
96
|
+
| `CAPTCHA_LORA_ADAPTER` | Captcha adapter (HF id or path) | `CaptchaKraken/CaptchaKraken_v1` |
|
|
97
|
+
| `CAPTCHA_LORA_NAME` | Served adapter name the client requests | `captcha` |
|
|
98
|
+
| `CAPTCHA_KRAKEN_AUTOSTART` | `0` disables local auto-start | `1` |
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
GPL-3.0-or-later.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# captchakraken
|
|
2
|
+
|
|
3
|
+
The Python engine + CLI behind [CaptchaKraken](https://github.com/JWriter20/CaptchaKraken):
|
|
4
|
+
OpenCV grid detection + a fine-tuned **Qwen3.5-9B** vision LoRA served on
|
|
5
|
+
**vLLM**. Given a screenshot of a captcha grid, it locates the tiles and returns
|
|
6
|
+
the click plan. Ships the `captchakraken` command.
|
|
7
|
+
|
|
8
|
+
> For demo videos, accuracy numbers, the browser driver, and the full
|
|
9
|
+
> self-hosting guide, see the main repo
|
|
10
|
+
> **[CaptchaKraken](https://github.com/JWriter20/CaptchaKraken)**.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install captchakraken # client: OpenCV detection + vLLM HTTP planner
|
|
16
|
+
pip install "captchakraken[serve]" # + the serving stack (vLLM/torch) to self-host
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The base install is lightweight — everything you need to solve captchas against
|
|
20
|
+
a vLLM server (local or remote). The `[serve]` extra pulls the heavy stack only
|
|
21
|
+
if you want to run the model yourself. The one-command
|
|
22
|
+
[`setup.sh`](https://github.com/JWriter20/CaptchaKraken) installs `[serve]`,
|
|
23
|
+
downloads the weights, and writes an env file for you.
|
|
24
|
+
|
|
25
|
+
## Hands-off server
|
|
26
|
+
|
|
27
|
+
The vLLM server is managed for you. On your first solve, if the configured
|
|
28
|
+
endpoint is **local** and nothing is listening, a server is started
|
|
29
|
+
automatically and reused. Point `VLLM_BASE_URL` at a server you already run to
|
|
30
|
+
skip local management entirely.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
captchakraken server start | stop | status | run
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Solve an image/video: classify → find_grid → plan. Prints the click actions.
|
|
40
|
+
captchakraken path/to/captcha.png
|
|
41
|
+
captchakraken path/to/captcha.png --puzzle-source hcaptcha
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from captchakraken import CaptchaSolver
|
|
46
|
+
|
|
47
|
+
solver = CaptchaSolver() # connects to / auto-starts a local vLLM
|
|
48
|
+
actions = solver.solve("captcha.png")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Pure-OpenCV tool subcommands (no model): `find-grid`, `find-checkbox`,
|
|
52
|
+
`detect-selected`, `grid-cell-states`, `find-move`, `find-movable`, and a
|
|
53
|
+
persistent `serve` worker the browser driver polls.
|
|
54
|
+
|
|
55
|
+
## Configuration (model-agnostic)
|
|
56
|
+
|
|
57
|
+
Everything model-specific lives in `captchakraken.config` and is env-overridable
|
|
58
|
+
— the solver never hard-codes a model.
|
|
59
|
+
|
|
60
|
+
| Variable | Meaning | Default |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `VLLM_BASE_URL` | Inference endpoint | `http://localhost:8000/v1` |
|
|
63
|
+
| `CAPTCHA_KRAKEN_API_KEY` | Bearer token (`VLLM_API_KEY` also accepted) | `EMPTY` |
|
|
64
|
+
| `CAPTCHA_BASE_MODEL` | Base weights vLLM loads | `Qwen/Qwen3.5-9B` |
|
|
65
|
+
| `CAPTCHA_LORA_ADAPTER` | Captcha adapter (HF id or path) | `CaptchaKraken/CaptchaKraken_v1` |
|
|
66
|
+
| `CAPTCHA_LORA_NAME` | Served adapter name the client requests | `captcha` |
|
|
67
|
+
| `CAPTCHA_KRAKEN_AUTOSTART` | `0` disables local auto-start | `1` |
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
GPL-3.0-or-later.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Examples (Python)
|
|
2
|
+
|
|
3
|
+
Two runnable demos that drive a real stealth browser
|
|
4
|
+
([camoufox](https://github.com/JWriter20/camoufox)) to a live captcha demo site,
|
|
5
|
+
screenshot the challenge, run the **engine** on it, and print token speed /
|
|
6
|
+
total time / outcome:
|
|
7
|
+
|
|
8
|
+
| File | Site |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `demoRecaptcha.py` | Google reCAPTCHA v2 demo |
|
|
11
|
+
| `demoHcaptcha.py` | hCaptcha demo |
|
|
12
|
+
|
|
13
|
+
> The Python port is the engine (detection + planner). These demos validate the
|
|
14
|
+
> engine + model + server on a real challenge frame. Full click-replay and
|
|
15
|
+
> multi-round verification in a live page are what the TypeScript port
|
|
16
|
+
> (`captchakraken`) does end-to-end.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd python
|
|
22
|
+
pip install -e ".[serve]" # engine + serving stack (use ".[]" for a remote server)
|
|
23
|
+
pip install camoufox # example-only dep
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### The camoufox binary (from your fork)
|
|
27
|
+
|
|
28
|
+
Uses the **camoufox binary from the fork's releases**:
|
|
29
|
+
[JWriter20/camoufox → Releases](https://github.com/JWriter20/camoufox/releases).
|
|
30
|
+
|
|
31
|
+
1. Download the latest release asset for your OS/arch and extract it.
|
|
32
|
+
2. Point the demo at the extracted `camoufox` executable:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
export CAMOUFOX_BINARY=/path/to/camoufox/camoufox # your fork binary
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If `CAMOUFOX_BINARY` is unset, camoufox falls back to its default binary
|
|
39
|
+
(`python -m camoufox fetch`).
|
|
40
|
+
|
|
41
|
+
### Point at a model
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Run
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python examples/demoRecaptcha.py
|
|
51
|
+
python examples/demoHcaptcha.py
|
|
52
|
+
HEADLESS=0 python examples/demoRecaptcha.py # watch the browser
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Reading the report
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
result : ✓ engine produced a solution
|
|
59
|
+
click plan: 4 tile(s)/target(s)
|
|
60
|
+
total time : 6.1s (solve: 3.4s)
|
|
61
|
+
tokens : 812 in / 34 out
|
|
62
|
+
gen speed : ~10.0 tok/s
|
|
63
|
+
reason : <only on failure — unsupported puzzle, unreachable server, …>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`gen speed` = model output tokens ÷ solve seconds. Failure reasons the harness
|
|
67
|
+
reports: unreachable vLLM server, an unsupported hCaptcha puzzle (drag/video),
|
|
68
|
+
the challenge iframe never appearing, or the model returning no matching tiles.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared runner for the CaptchaKraken Python demos.
|
|
3
|
+
|
|
4
|
+
The Python port is the *engine* (OpenCV detection + the vLLM planner). These demos
|
|
5
|
+
launch a real stealth browser (camoufox, using the binary from your fork —
|
|
6
|
+
JWriter20/camoufox releases; see README.md), navigate to a captcha demo site,
|
|
7
|
+
open the challenge, screenshot it, and run the engine on that frame. They report
|
|
8
|
+
token-generation speed, total time, and whether the engine produced a valid
|
|
9
|
+
solution — plus a best-effort reason when it didn't.
|
|
10
|
+
|
|
11
|
+
Note: full click-replay + multi-round verification in a live page is what the
|
|
12
|
+
TypeScript port (`captchakraken`) does end-to-end. Here we validate the
|
|
13
|
+
engine + model + server on a real challenge frame.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import tempfile
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
from camoufox.sync_api import Camoufox
|
|
22
|
+
|
|
23
|
+
from captchakraken import CaptchaSolver
|
|
24
|
+
from captchakraken.action_types import ClickAction
|
|
25
|
+
from captchakraken.solver import UnsupportedCaptchaError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class DemoSpec:
|
|
30
|
+
name: str
|
|
31
|
+
url: str
|
|
32
|
+
vendor: str # "recaptcha" | "hcaptcha"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _launch_kwargs() -> dict:
|
|
36
|
+
kw = dict(headless=os.getenv("HEADLESS", "1") != "0", humanize=True, geoip=False)
|
|
37
|
+
# Point camoufox at YOUR fork's binary. If unset, camoufox uses its default
|
|
38
|
+
# cached binary (`python -m camoufox fetch`).
|
|
39
|
+
binary = os.getenv("CAMOUFOX_BINARY") or os.getenv("CAMOUFOX_EXECUTABLE_PATH")
|
|
40
|
+
if binary:
|
|
41
|
+
kw["executable_path"] = binary
|
|
42
|
+
return kw
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _capture_challenge(page, spec: DemoSpec) -> str:
|
|
46
|
+
"""Reach the challenge iframe and screenshot it to a temp PNG. Returns the path.
|
|
47
|
+
|
|
48
|
+
Raises RuntimeError with a human message if the challenge never appears.
|
|
49
|
+
"""
|
|
50
|
+
out = tempfile.NamedTemporaryFile(suffix=".png", delete=False).name
|
|
51
|
+
|
|
52
|
+
if spec.vendor == "recaptcha":
|
|
53
|
+
anchor = page.wait_for_selector('iframe[src*="/recaptcha/api2/anchor"]', timeout=30_000)
|
|
54
|
+
frame = anchor.content_frame()
|
|
55
|
+
frame.click("#recaptcha-anchor", timeout=15_000)
|
|
56
|
+
challenge = page.wait_for_selector('iframe[src*="/recaptcha/api2/bframe"]', timeout=30_000)
|
|
57
|
+
# Let the tiles paint.
|
|
58
|
+
page.wait_for_timeout(2500)
|
|
59
|
+
challenge.screenshot(path=out)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
if spec.vendor == "hcaptcha":
|
|
63
|
+
box = page.wait_for_selector('iframe[src*="hcaptcha.com"][src*="checkbox"], iframe[title*="checkbox"]',
|
|
64
|
+
timeout=30_000)
|
|
65
|
+
box.content_frame().click("#checkbox, div[role=checkbox]", timeout=15_000)
|
|
66
|
+
challenge = page.wait_for_selector('iframe[title*="hCaptcha challenge"], iframe[src*="hcaptcha.com"][src*="frame=challenge"]',
|
|
67
|
+
timeout=30_000)
|
|
68
|
+
page.wait_for_timeout(2500)
|
|
69
|
+
challenge.screenshot(path=out)
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
raise RuntimeError(f"unknown vendor {spec.vendor!r}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _tokens_per_sec(solver: CaptchaSolver, elapsed_s: float) -> tuple[int, int, float]:
|
|
76
|
+
"""(input_tokens, output_tokens, tokens/sec) from the planner's usage log."""
|
|
77
|
+
inp = out = 0
|
|
78
|
+
for u in getattr(solver.planner, "token_usage", []) or []:
|
|
79
|
+
inp += int(u.get("prompt_tokens", 0) or 0)
|
|
80
|
+
out += int(u.get("completion_tokens", 0) or 0)
|
|
81
|
+
tps = out / elapsed_s if out and elapsed_s > 0 else 0.0
|
|
82
|
+
return inp, out, tps
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _explain(vendor: str, err: Exception) -> str:
|
|
86
|
+
msg = str(err).lower()
|
|
87
|
+
if isinstance(err, UnsupportedCaptchaError) or "cannot solve" in msg or "unsupported" in msg:
|
|
88
|
+
if vendor == "hcaptcha":
|
|
89
|
+
return ("hCaptcha served a non-grid challenge (drag / video / choose-the-card). "
|
|
90
|
+
"The engine handles image grids + checkboxes only — re-run to try for a grid.")
|
|
91
|
+
return "Not a supported grid/checkbox challenge — re-run to try again."
|
|
92
|
+
if "vllm" in msg or "connection" in msg or "refused" in msg or "max retries" in msg:
|
|
93
|
+
return ("Could not reach the vLLM server — is it up and is VLLM_BASE_URL correct? "
|
|
94
|
+
"(A local server auto-starts only if captchakraken[serve] is installed.)")
|
|
95
|
+
if "timeout" in msg or "wait_for_selector" in msg:
|
|
96
|
+
return "The challenge iframe never appeared (slow network, blocked widget, or the checkbox auto-passed)."
|
|
97
|
+
return str(err) or "Unknown failure."
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run_demo(spec: DemoSpec) -> None:
|
|
101
|
+
t0 = time.time()
|
|
102
|
+
ok = False
|
|
103
|
+
reason = None
|
|
104
|
+
inp = out = 0
|
|
105
|
+
tps = 0.0
|
|
106
|
+
solve_s = 0.0
|
|
107
|
+
plan_desc = "-"
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
with Camoufox(**_launch_kwargs()) as browser:
|
|
111
|
+
page = browser.new_page()
|
|
112
|
+
page.goto(spec.url, wait_until="domcontentloaded", timeout=60_000)
|
|
113
|
+
shot = _capture_challenge(page, spec)
|
|
114
|
+
|
|
115
|
+
solver = CaptchaSolver()
|
|
116
|
+
s0 = time.time()
|
|
117
|
+
try:
|
|
118
|
+
actions = solver.solve(shot, puzzle_source=spec.vendor)
|
|
119
|
+
finally:
|
|
120
|
+
solve_s = time.time() - s0
|
|
121
|
+
inp, out, tps = _tokens_per_sec(solver, solve_s)
|
|
122
|
+
|
|
123
|
+
acts = actions if isinstance(actions, list) else [actions]
|
|
124
|
+
clicks = [a for a in acts if isinstance(a, ClickAction) and (a.target_bounding_boxes or [])]
|
|
125
|
+
n_tiles = sum(len(a.target_bounding_boxes or []) for a in clicks)
|
|
126
|
+
if n_tiles > 0:
|
|
127
|
+
ok = True
|
|
128
|
+
plan_desc = f"click plan: {n_tiles} tile(s)/target(s)"
|
|
129
|
+
else:
|
|
130
|
+
plan_desc = f"actions: {[type(a).__name__ for a in acts]}"
|
|
131
|
+
reason = ("The model returned no tiles to click — either none matched the prompt, "
|
|
132
|
+
"or the challenge frame wasn't a clean grid. Re-run to try again.")
|
|
133
|
+
except Exception as err: # noqa: BLE001 — demo: report, don't traceback
|
|
134
|
+
reason = _explain(spec.vendor, err)
|
|
135
|
+
|
|
136
|
+
_report(spec, ok=ok, total_s=time.time() - t0, solve_s=solve_s,
|
|
137
|
+
inp=inp, out=out, tps=tps, plan=plan_desc, reason=reason)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _fmt(s: float) -> str:
|
|
141
|
+
return f"{s:.1f}s" if s >= 1 else f"{int(s * 1000)}ms"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _report(spec, *, ok, total_s, solve_s, inp, out, tps, plan, reason):
|
|
145
|
+
line = "─" * 52
|
|
146
|
+
print(f"\n{line}")
|
|
147
|
+
print(f" CaptchaKraken demo (engine) — {spec.name}")
|
|
148
|
+
print(f" {spec.url}")
|
|
149
|
+
print(line)
|
|
150
|
+
print(f" result : {'✓ engine produced a solution' if ok else '✗ no solution'}")
|
|
151
|
+
print(f" {plan}")
|
|
152
|
+
print(f" total time : {_fmt(total_s)} (solve: {_fmt(solve_s)})")
|
|
153
|
+
print(f" tokens : {inp} in / {out} out")
|
|
154
|
+
print(f" gen speed : {f'~{tps:.1f} tok/s' if tps > 0 else 'n/a'}")
|
|
155
|
+
if reason:
|
|
156
|
+
print(f" reason : {reason}")
|
|
157
|
+
print(f"{line}\n")
|
|
158
|
+
raise SystemExit(0 if ok else 1)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Engine demo: run CaptchaKraken on the standard hCaptcha demo page.
|
|
3
|
+
|
|
4
|
+
pip install -e ".[serve]" # engine + serving stack (or [.] against a remote server)
|
|
5
|
+
pip install camoufox && python -m camoufox fetch # or set CAMOUFOX_BINARY to your fork binary
|
|
6
|
+
source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
|
|
7
|
+
python examples/demoHcaptcha.py
|
|
8
|
+
|
|
9
|
+
Note: hCaptcha randomly serves non-grid puzzles (drag / video / choose-the-card),
|
|
10
|
+
which the engine does not handle yet — the report says so; re-run for a grid.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from _harness import DemoSpec, run_demo
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
run_demo(DemoSpec(
|
|
17
|
+
name="hCaptcha",
|
|
18
|
+
url="https://accounts.hcaptcha.com/demo",
|
|
19
|
+
vendor="hcaptcha",
|
|
20
|
+
))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Engine demo: run CaptchaKraken on Google's standard reCAPTCHA v2 demo page.
|
|
3
|
+
|
|
4
|
+
pip install -e ".[serve]" # engine + serving stack (or [.] against a remote server)
|
|
5
|
+
pip install camoufox && python -m camoufox fetch # or set CAMOUFOX_BINARY to your fork binary
|
|
6
|
+
source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
|
|
7
|
+
python examples/demoRecaptcha.py
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from _harness import DemoSpec, run_demo
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
run_demo(DemoSpec(
|
|
14
|
+
name="reCAPTCHA v2",
|
|
15
|
+
url="https://www.google.com/recaptcha/api2/demo",
|
|
16
|
+
vendor="recaptcha",
|
|
17
|
+
))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "captchakraken"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Self-hosted captcha solver: OpenCV grid detection + a fine-tuned Qwen3.5-9B vision LoRA served on vLLM."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "GPL-3.0-or-later" }
|
|
12
|
+
authors = [{ name = "Jake Writer" }]
|
|
13
|
+
keywords = ["captcha", "recaptcha", "hcaptcha", "vllm", "qwen", "computer-vision", "automation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# Core = the lightweight CLIENT: OpenCV grid detection + the HTTP planner that
|
|
21
|
+
# talks to a vLLM server (local or remote). This is all you need to SOLVE
|
|
22
|
+
# captchas against an existing endpoint. The heavy serving stack (vllm/torch)
|
|
23
|
+
# is deliberately NOT here — see the `serve` extra.
|
|
24
|
+
dependencies = [
|
|
25
|
+
"pydantic>=2.0.0",
|
|
26
|
+
"pillow>=10.0.0",
|
|
27
|
+
"numpy>=1.24.0",
|
|
28
|
+
"opencv-python-headless>=4.10.0",
|
|
29
|
+
"requests>=2.31.0",
|
|
30
|
+
"python-dotenv>=1.0.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
# Self-hosting: install this to RUN a local vLLM server (the setup script does
|
|
35
|
+
# `pip install "captchakraken[serve]"`). Not needed to call a remote server.
|
|
36
|
+
serve = [
|
|
37
|
+
"vllm>=0.6.3",
|
|
38
|
+
"torch>=2.0.0",
|
|
39
|
+
"transformers>=4.40.0",
|
|
40
|
+
"accelerate>=0.27.0",
|
|
41
|
+
"huggingface_hub>=0.23.0",
|
|
42
|
+
]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=7.0.0",
|
|
45
|
+
"mypy>=1.0.0",
|
|
46
|
+
"ruff>=0.1.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.scripts]
|
|
50
|
+
captchakraken = "captchakraken.cli:main"
|
|
51
|
+
|
|
52
|
+
[project.urls]
|
|
53
|
+
Homepage = "https://github.com/JWriter20/CaptchaKraken"
|
|
54
|
+
Issues = "https://github.com/JWriter20/CaptchaKraken/issues"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/captchakraken"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
pythonpath = ["src"]
|
|
62
|
+
|
|
63
|
+
[tool.mypy]
|
|
64
|
+
python_version = "3.10"
|
|
65
|
+
warn_return_any = false
|
|
66
|
+
warn_unused_configs = true
|
|
67
|
+
disallow_untyped_defs = false
|
|
68
|
+
check_untyped_defs = false
|
|
69
|
+
ignore_missing_imports = true
|
|
70
|
+
no_implicit_optional = false
|
|
71
|
+
exclude = ["build/", "dist/"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
line-length = 120
|
|
75
|
+
target-version = "py310"
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = ["E", "F", "W", "I"]
|
|
79
|
+
ignore = ["E501"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CaptchaKraken — OpenCV grid detection + a fine-tuned Qwen3.5-9B vision LoRA
|
|
3
|
+
served on vLLM.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from captchakraken import CaptchaSolver
|
|
7
|
+
solver = CaptchaSolver() # auto-starts / connects to a local vLLM server
|
|
8
|
+
actions = solver.solve("captcha.png")
|
|
9
|
+
|
|
10
|
+
Model/endpoint defaults live in `captchakraken.config` and are fully
|
|
11
|
+
env-overridable (VLLM_BASE_URL, CAPTCHA_LORA_ADAPTER, …); the solver itself is
|
|
12
|
+
model-agnostic. The legacy v1 stack (SAM3 grounding, multi-provider planner,
|
|
13
|
+
detect/segment/drag-refine) lives on the `v1-old-architecture` branch.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
try: # pragma: no cover
|
|
19
|
+
from dotenv import load_dotenv
|
|
20
|
+
|
|
21
|
+
project_root = Path(__file__).resolve().parent.parent
|
|
22
|
+
load_dotenv(project_root / ".env")
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
from .action_types import (
|
|
27
|
+
CaptchaAction,
|
|
28
|
+
ClickAction,
|
|
29
|
+
DragAction,
|
|
30
|
+
TypeAction,
|
|
31
|
+
WaitAction,
|
|
32
|
+
)
|
|
33
|
+
from .image_processor import ImageProcessor
|
|
34
|
+
from .overlay import add_overlays_to_image
|
|
35
|
+
|
|
36
|
+
# The planner (requests) and solver (torch/vllm/transformers) pull in the heavy
|
|
37
|
+
# serving stack. Keep them optional so leaf modules — e.g. tool_calls.find_grid,
|
|
38
|
+
# which needs only cv2 + numpy + pillow — can be imported in a minimal env (CI's
|
|
39
|
+
# hermetic grid-detection test) without the full GPU dependency set installed.
|
|
40
|
+
try: # pragma: no cover - exercised only when the serving stack is installed
|
|
41
|
+
from .planner import ActionPlanner
|
|
42
|
+
from .solver import CaptchaSolver, solve_captcha
|
|
43
|
+
except ModuleNotFoundError:
|
|
44
|
+
ActionPlanner = None # type: ignore[assignment,misc]
|
|
45
|
+
CaptchaSolver = None # type: ignore[assignment,misc]
|
|
46
|
+
solve_captcha = None # type: ignore[assignment]
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"CaptchaSolver",
|
|
50
|
+
"solve_captcha",
|
|
51
|
+
"ActionPlanner",
|
|
52
|
+
"ImageProcessor",
|
|
53
|
+
"CaptchaAction",
|
|
54
|
+
"ClickAction",
|
|
55
|
+
"DragAction",
|
|
56
|
+
"TypeAction",
|
|
57
|
+
"WaitAction",
|
|
58
|
+
"add_overlays_to_image",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
__version__ = "2.0.0"
|