captchakraken 2.1.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 +54 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +43 -0
- package/dist/playwright-types.d.ts +119 -0
- package/dist/playwright-types.js +25 -0
- package/dist/puppeteer-adapter.d.ts +70 -0
- package/dist/puppeteer-adapter.js +96 -0
- package/dist/solver.d.ts +264 -0
- package/dist/solver.js +1922 -0
- package/dist/token-usage.d.ts +15 -0
- package/dist/token-usage.js +102 -0
- package/dist/types.d.ts +248 -0
- package/dist/types.js +2 -0
- package/package.json +49 -0
- package/python/Dockerfile +41 -0
- package/python/README.md +71 -0
- package/python/examples/README.md +68 -0
- package/python/examples/_harness.py +158 -0
- package/python/examples/demoHcaptcha.py +20 -0
- package/python/examples/demoRecaptcha.py +17 -0
- package/python/pyproject.toml +79 -0
- package/python/src/captchakraken/__init__.py +61 -0
- package/python/src/captchakraken/action_types.py +56 -0
- package/python/src/captchakraken/cli.py +656 -0
- package/python/src/captchakraken/config.py +78 -0
- package/python/src/captchakraken/image_processor.py +244 -0
- package/python/src/captchakraken/overlay.py +520 -0
- package/python/src/captchakraken/planner.py +408 -0
- package/python/src/captchakraken/planner_types.py +74 -0
- package/python/src/captchakraken/server_manager.py +290 -0
- package/python/src/captchakraken/solver.py +434 -0
- package/python/src/captchakraken/timing.py +42 -0
- package/python/src/captchakraken/tool_calls/find_checkbox.py +72 -0
- package/python/src/captchakraken/tool_calls/find_grid.py +1762 -0
- package/python/src/captchakraken/tool_calls/move_indicator.py +431 -0
- package/scripts/copy-python.mjs +29 -0
- package/scripts/setup-python.js +104 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Central, model-agnostic configuration for CaptchaKraken.
|
|
3
|
+
|
|
4
|
+
Every model-specific value lives here and is overridable via environment
|
|
5
|
+
variables, so swapping the underlying model never requires touching the solver,
|
|
6
|
+
planner, or CLI. The defaults point at the published unified captcha LoRA, but
|
|
7
|
+
nothing in the codebase hard-codes a model name outside this module.
|
|
8
|
+
|
|
9
|
+
The two variables MOST users ever set:
|
|
10
|
+
VLLM_BASE_URL where inference requests go (local or a remote server)
|
|
11
|
+
CAPTCHA_KRAKEN_API_KEY bearer token for that server
|
|
12
|
+
|
|
13
|
+
Advanced / self-hosting overrides (only needed to change WHICH model is served):
|
|
14
|
+
CAPTCHA_BASE_MODEL base weights vLLM loads (HF id or local path)
|
|
15
|
+
CAPTCHA_LORA_ADAPTER LoRA applied on top (HF id or local path)
|
|
16
|
+
CAPTCHA_LORA_NAME served adapter name the client asks for as `model`
|
|
17
|
+
VLLM_PORT, VLLM_GPU_MEMORY_UTILIZATION, VLLM_MAX_MODEL_LEN, VLLM_MAX_LORA_RANK
|
|
18
|
+
CAPTCHA_KRAKEN_AUTOSTART=0 disable auto-starting a local server on first use
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ── Inference endpoint ──────────────────────────────────────────────────────
|
|
25
|
+
def base_url() -> str:
|
|
26
|
+
"""OpenAI-compatible endpoint, e.g. http://localhost:8000/v1."""
|
|
27
|
+
return os.getenv("VLLM_BASE_URL", f"http://localhost:{port()}/v1")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def api_key() -> str:
|
|
31
|
+
return os.getenv("CAPTCHA_KRAKEN_API_KEY") or os.getenv("VLLM_API_KEY") or "EMPTY"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── Model identity (all overridable — the CLI itself is model-agnostic) ──────
|
|
35
|
+
def base_model() -> str:
|
|
36
|
+
return os.getenv("CAPTCHA_BASE_MODEL", "Qwen/Qwen3.5-9B")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def lora_adapter() -> str:
|
|
40
|
+
"""HF repo id or local path of the captcha adapter served on top of the base."""
|
|
41
|
+
return os.getenv("CAPTCHA_LORA_ADAPTER", "CaptchaKraken/CaptchaKraken_v1")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def lora_name() -> str:
|
|
45
|
+
"""The served LoRA name the client sends as the `model` field."""
|
|
46
|
+
return os.getenv("CAPTCHA_LORA_NAME", "captcha")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── Local vLLM server knobs ─────────────────────────────────────────────────
|
|
50
|
+
def port() -> int:
|
|
51
|
+
return int(os.getenv("VLLM_PORT", "8000"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def gpu_memory_utilization() -> float:
|
|
55
|
+
return float(os.getenv("VLLM_GPU_MEMORY_UTILIZATION", "0.80"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def max_model_len() -> int:
|
|
59
|
+
return int(os.getenv("VLLM_MAX_MODEL_LEN", "65536"))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def max_lora_rank() -> int:
|
|
63
|
+
return int(os.getenv("VLLM_MAX_LORA_RANK", "64"))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def autostart_enabled() -> bool:
|
|
67
|
+
"""Whether to auto-launch a local server on first use (default on)."""
|
|
68
|
+
return os.getenv("CAPTCHA_KRAKEN_AUTOSTART", "1") != "0"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def extra_serve_args() -> list:
|
|
72
|
+
"""Extra flags appended verbatim to `vllm serve` (space-separated).
|
|
73
|
+
|
|
74
|
+
Escape hatch for serving tweaks the config doesn't model directly — e.g.
|
|
75
|
+
`--enforce-eager` or a smaller `--max-num-seqs` to squeeze onto a tight GPU.
|
|
76
|
+
"""
|
|
77
|
+
raw = os.getenv("VLLM_EXTRA_ARGS", "").strip()
|
|
78
|
+
return raw.split() if raw else []
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import cv2
|
|
4
|
+
import numpy as np
|
|
5
|
+
from PIL import Image
|
|
6
|
+
from typing import Optional, List, Tuple, Dict, Any
|
|
7
|
+
|
|
8
|
+
class ImageProcessor:
|
|
9
|
+
"""
|
|
10
|
+
Unified image processing class handling:
|
|
11
|
+
- Basic image manipulation (sharpen, blur, contrast, etc.)
|
|
12
|
+
- Grid detection
|
|
13
|
+
- Background removal (using segmentation + LLM)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, attention_extractor=None, planner=None, debug_manager=None):
|
|
17
|
+
"""
|
|
18
|
+
Args:
|
|
19
|
+
attention_extractor: AttentionExtractor instance (required for background removal)
|
|
20
|
+
planner: ActionPlanner instance (required for background removal)
|
|
21
|
+
debug_manager: DebugManager instance (optional)
|
|
22
|
+
"""
|
|
23
|
+
self.attention = attention_extractor
|
|
24
|
+
self.planner = planner
|
|
25
|
+
self.debug = debug_manager
|
|
26
|
+
self._temp_files: List[str] = []
|
|
27
|
+
|
|
28
|
+
def __del__(self):
|
|
29
|
+
# Cleanup temp files
|
|
30
|
+
for f in self._temp_files:
|
|
31
|
+
if os.path.exists(f):
|
|
32
|
+
try:
|
|
33
|
+
os.unlink(f)
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
# =========================================================================
|
|
38
|
+
# Static Utility Methods (Stateless)
|
|
39
|
+
# =========================================================================
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def detect_movement(image1_path: str, image2_path: str, threshold: float = 0.005) -> bool:
|
|
43
|
+
"""
|
|
44
|
+
Compare two images and return True if they are significantly different.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
image1_path: Path to the first image.
|
|
48
|
+
image2_path: Path to the second image.
|
|
49
|
+
threshold: Percentage of pixels that must change to be considered movement.
|
|
50
|
+
"""
|
|
51
|
+
img1 = cv2.imread(image1_path)
|
|
52
|
+
img2 = cv2.imread(image2_path)
|
|
53
|
+
|
|
54
|
+
if img1 is None or img2 is None:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
if img1.shape != img2.shape:
|
|
58
|
+
# If resolution changed, something definitely changed
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
# Compute absolute difference
|
|
62
|
+
diff = cv2.absdiff(img1, img2)
|
|
63
|
+
# Convert to grayscale to simplify
|
|
64
|
+
gray_diff = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
|
|
65
|
+
|
|
66
|
+
# Threshold the difference to ignore minor noise (compression artifacts, etc.)
|
|
67
|
+
# 30 is a reasonable threshold for significant pixel change
|
|
68
|
+
_, thresh = cv2.threshold(gray_diff, 30, 255, cv2.THRESH_BINARY)
|
|
69
|
+
|
|
70
|
+
# Calculate percentage of changed pixels
|
|
71
|
+
changed_pixels = cv2.countNonZero(thresh)
|
|
72
|
+
total_pixels = thresh.shape[0] * thresh.shape[1]
|
|
73
|
+
change_ratio = changed_pixels / total_pixels
|
|
74
|
+
|
|
75
|
+
return change_ratio > threshold
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def to_greyscale(image_path: str, output_path: str) -> None:
|
|
79
|
+
"""Convert an image to greyscale and save it."""
|
|
80
|
+
img = cv2.imread(image_path)
|
|
81
|
+
if img is None:
|
|
82
|
+
raise ValueError(f"Could not read image from {image_path}")
|
|
83
|
+
|
|
84
|
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
85
|
+
cv2.imwrite(output_path, gray)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def to_edge_outline(image_path: str, output_path: str, low_threshold: int = 15, high_threshold: int = 60) -> None:
|
|
89
|
+
"""Detect and isolate edges in an image using Canny edge detection."""
|
|
90
|
+
img = cv2.imread(image_path)
|
|
91
|
+
if img is None:
|
|
92
|
+
raise ValueError(f"Could not read image from {image_path}")
|
|
93
|
+
|
|
94
|
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
95
|
+
blurred = cv2.GaussianBlur(gray, (15, 15), 0)
|
|
96
|
+
edges = cv2.Canny(blurred, low_threshold, high_threshold)
|
|
97
|
+
cv2.imwrite(output_path, edges)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def sharpen_image(image_path: str, output_path: str) -> None:
|
|
101
|
+
"""Sharpen an image using a generic kernel."""
|
|
102
|
+
img = cv2.imread(image_path)
|
|
103
|
+
if img is None:
|
|
104
|
+
raise ValueError(f"Could not read image from {image_path}")
|
|
105
|
+
|
|
106
|
+
kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
|
|
107
|
+
sharpened = cv2.filter2D(img, -1, kernel)
|
|
108
|
+
cv2.imwrite(output_path, sharpened)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def sharpen_edges(image_path: str, output_path: str) -> str:
|
|
112
|
+
"""Apply unsharp masking to sharpen edges (alternative to sharpen_image)."""
|
|
113
|
+
img = cv2.imread(image_path)
|
|
114
|
+
if img is None:
|
|
115
|
+
raise ValueError(f"Could not load image: {image_path}")
|
|
116
|
+
|
|
117
|
+
gaussian = cv2.GaussianBlur(img, (9, 9), 10.0)
|
|
118
|
+
unsharp = cv2.addWeighted(img, 1.5, gaussian, -0.5, 0, img)
|
|
119
|
+
|
|
120
|
+
cv2.imwrite(output_path, unsharp)
|
|
121
|
+
return output_path
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def apply_clahe(image_path: str, output_path: str, clip_limit: float = 2.0, tile_grid_size: Tuple[int, int] = (8, 8)) -> None:
|
|
125
|
+
"""Apply CLAHE (Contrast Limited Adaptive Histogram Equalization)."""
|
|
126
|
+
img = cv2.imread(image_path)
|
|
127
|
+
if img is None:
|
|
128
|
+
raise ValueError(f"Could not read image from {image_path}")
|
|
129
|
+
|
|
130
|
+
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
|
|
131
|
+
l, a, b = cv2.split(lab)
|
|
132
|
+
|
|
133
|
+
clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)
|
|
134
|
+
cl = clahe.apply(l)
|
|
135
|
+
|
|
136
|
+
limg = cv2.merge((cl, a, b))
|
|
137
|
+
final = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)
|
|
138
|
+
cv2.imwrite(output_path, final)
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def apply_contrast_enhancement(image_path: str, output_path: str, alpha: float = 1.5, beta: int = 0) -> None:
|
|
142
|
+
"""Apply contrast enhancement using linear stretching."""
|
|
143
|
+
img = cv2.imread(image_path)
|
|
144
|
+
if img is None:
|
|
145
|
+
raise ValueError(f"Could not read image from {image_path}")
|
|
146
|
+
|
|
147
|
+
enhanced = cv2.convertScaleAbs(img, alpha=alpha, beta=beta)
|
|
148
|
+
cv2.imwrite(output_path, enhanced)
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def apply_gaussian_blur(image_path: str, output_path: str, kernel_size: Tuple[int, int] = (5, 5), sigma_x: float = 0) -> None:
|
|
152
|
+
"""Apply Gaussian Blur to an image."""
|
|
153
|
+
img = cv2.imread(image_path)
|
|
154
|
+
if img is None:
|
|
155
|
+
raise ValueError(f"Could not read image from {image_path}")
|
|
156
|
+
|
|
157
|
+
blurred = cv2.GaussianBlur(img, kernel_size, sigma_x)
|
|
158
|
+
cv2.imwrite(output_path, blurred)
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def blend_colors(image_path: str, output_path: str, spatial_radius: int = 10, color_radius: int = 20) -> str:
|
|
162
|
+
"""Apply mean shift filtering to blend similar colors (cartoon effect)."""
|
|
163
|
+
img = cv2.imread(image_path)
|
|
164
|
+
if img is None:
|
|
165
|
+
raise ValueError(f"Could not load image: {image_path}")
|
|
166
|
+
|
|
167
|
+
filtered = cv2.pyrMeanShiftFiltering(img, sp=spatial_radius, sr=color_radius)
|
|
168
|
+
cv2.imwrite(output_path, filtered)
|
|
169
|
+
return output_path
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def merge_similar_colors(image_path: str, output_path: str, k: int = 8) -> None:
|
|
173
|
+
"""Quantize image colors to K clusters using K-Means."""
|
|
174
|
+
img = cv2.imread(image_path)
|
|
175
|
+
if img is None:
|
|
176
|
+
raise ValueError(f"Could not read image from {image_path}")
|
|
177
|
+
|
|
178
|
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
|
179
|
+
pixels = img_rgb.reshape((-1, 3))
|
|
180
|
+
pixels = np.float32(pixels)
|
|
181
|
+
|
|
182
|
+
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
|
|
183
|
+
_, labels, centers = cv2.kmeans(pixels, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
|
|
184
|
+
|
|
185
|
+
centers = np.uint8(centers)
|
|
186
|
+
quantized = centers[labels.flatten()]
|
|
187
|
+
quantized_img = quantized.reshape(img.shape)
|
|
188
|
+
|
|
189
|
+
quantized_bgr = cv2.cvtColor(quantized_img, cv2.COLOR_RGB2BGR)
|
|
190
|
+
cv2.imwrite(output_path, quantized_bgr)
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def enhance_for_detection(image: Image.Image) -> Image.Image:
|
|
194
|
+
"""
|
|
195
|
+
Enhance color distinction (saturation/contrast) for better detection.
|
|
196
|
+
Works on PIL Image.
|
|
197
|
+
"""
|
|
198
|
+
# Convert PIL to BGR for OpenCV
|
|
199
|
+
img_np = np.array(image)
|
|
200
|
+
if len(img_np.shape) == 2: # Greyscale
|
|
201
|
+
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_GRAY2BGR)
|
|
202
|
+
elif img_np.shape[2] == 4: # RGBA
|
|
203
|
+
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGBA2BGR)
|
|
204
|
+
else:
|
|
205
|
+
img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR)
|
|
206
|
+
|
|
207
|
+
# 1. Color Distinction (Saturation boost)
|
|
208
|
+
# Convert to HSV to easily manipulate saturation
|
|
209
|
+
hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
|
|
210
|
+
h, s, v = cv2.split(hsv)
|
|
211
|
+
|
|
212
|
+
# Increase saturation significantly to make colors distinct
|
|
213
|
+
# Adding 50 is quite a strong boost
|
|
214
|
+
s = cv2.add(s, 50)
|
|
215
|
+
|
|
216
|
+
# Optionally boost Value/Brightness contrast slightly as well
|
|
217
|
+
v = cv2.addWeighted(v, 1.1, np.zeros_like(v), 0, -10)
|
|
218
|
+
|
|
219
|
+
hsv_enhanced = cv2.merge((h, s, v))
|
|
220
|
+
bgr_enhanced = cv2.cvtColor(hsv_enhanced, cv2.COLOR_HSV2BGR)
|
|
221
|
+
|
|
222
|
+
# 2. Local Contrast Enhancement (CLAHE)
|
|
223
|
+
# This helps with local structure and making boundaries more distinct
|
|
224
|
+
lab = cv2.cvtColor(bgr_enhanced, cv2.COLOR_BGR2LAB)
|
|
225
|
+
l, a, b = cv2.split(lab)
|
|
226
|
+
|
|
227
|
+
# Use a slightly higher clipLimit for more contrast
|
|
228
|
+
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
|
|
229
|
+
l_enhanced = clahe.apply(l)
|
|
230
|
+
|
|
231
|
+
lab_enhanced = cv2.merge((l_enhanced, a, b))
|
|
232
|
+
final_bgr = cv2.cvtColor(lab_enhanced, cv2.COLOR_LAB2BGR)
|
|
233
|
+
|
|
234
|
+
# Convert back to PIL RGB
|
|
235
|
+
return Image.fromarray(cv2.cvtColor(final_bgr, cv2.COLOR_BGR2RGB))
|
|
236
|
+
|
|
237
|
+
# =========================================================================
|
|
238
|
+
# Background Removal Logic (Stateful)
|
|
239
|
+
# =========================================================================
|
|
240
|
+
|
|
241
|
+
# Note: remove_background() (SAM 3-based) was removed in v2. See the
|
|
242
|
+
# `v1-old-architecture` branch if you need to revive segmentation-based
|
|
243
|
+
# background isolation for drag-piece verification.
|
|
244
|
+
|