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.
Files changed (37) hide show
  1. package/README.md +54 -0
  2. package/dist/index.d.ts +56 -0
  3. package/dist/index.js +43 -0
  4. package/dist/playwright-types.d.ts +119 -0
  5. package/dist/playwright-types.js +25 -0
  6. package/dist/puppeteer-adapter.d.ts +70 -0
  7. package/dist/puppeteer-adapter.js +96 -0
  8. package/dist/solver.d.ts +264 -0
  9. package/dist/solver.js +1922 -0
  10. package/dist/token-usage.d.ts +15 -0
  11. package/dist/token-usage.js +102 -0
  12. package/dist/types.d.ts +248 -0
  13. package/dist/types.js +2 -0
  14. package/package.json +49 -0
  15. package/python/Dockerfile +41 -0
  16. package/python/README.md +71 -0
  17. package/python/examples/README.md +68 -0
  18. package/python/examples/_harness.py +158 -0
  19. package/python/examples/demoHcaptcha.py +20 -0
  20. package/python/examples/demoRecaptcha.py +17 -0
  21. package/python/pyproject.toml +79 -0
  22. package/python/src/captchakraken/__init__.py +61 -0
  23. package/python/src/captchakraken/action_types.py +56 -0
  24. package/python/src/captchakraken/cli.py +656 -0
  25. package/python/src/captchakraken/config.py +78 -0
  26. package/python/src/captchakraken/image_processor.py +244 -0
  27. package/python/src/captchakraken/overlay.py +520 -0
  28. package/python/src/captchakraken/planner.py +408 -0
  29. package/python/src/captchakraken/planner_types.py +74 -0
  30. package/python/src/captchakraken/server_manager.py +290 -0
  31. package/python/src/captchakraken/solver.py +434 -0
  32. package/python/src/captchakraken/timing.py +42 -0
  33. package/python/src/captchakraken/tool_calls/find_checkbox.py +72 -0
  34. package/python/src/captchakraken/tool_calls/find_grid.py +1762 -0
  35. package/python/src/captchakraken/tool_calls/move_indicator.py +431 -0
  36. package/scripts/copy-python.mjs +29 -0
  37. 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
+