captchakraken 2.8.0 → 3.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 +112 -0
- package/NOTICE +36 -0
- package/dist/cli-invocation.d.ts +9 -40
- package/dist/cli-invocation.js +16 -49
- package/dist/errors.d.ts +7 -27
- package/dist/errors.js +6 -25
- package/dist/humanize.d.ts +56 -240
- package/dist/humanize.js +159 -415
- package/dist/index.d.ts +5 -129
- package/dist/index.js +4 -98
- package/dist/kinds.d.ts +135 -0
- package/dist/kinds.js +70 -0
- package/dist/limits.d.ts +0 -48
- package/dist/limits.js +2 -48
- package/dist/model-name.d.ts +4 -19
- package/dist/model-name.js +32 -40
- package/dist/playwright-types.d.ts +25 -95
- package/dist/playwright-types.js +0 -23
- package/dist/puppeteer-adapter.d.ts +4 -43
- package/dist/puppeteer-adapter.js +22 -58
- package/dist/python-command.d.ts +0 -41
- package/dist/python-command.js +0 -37
- package/dist/selectors.d.ts +54 -0
- package/dist/selectors.js +117 -0
- package/dist/slide-geometry.d.ts +2 -29
- package/dist/slide-geometry.js +18 -44
- package/dist/solver.d.ts +112 -539
- package/dist/solver.js +1088 -2950
- package/dist/timing.d.ts +7 -40
- package/dist/timing.js +4 -36
- package/dist/token-usage.d.ts +1 -7
- package/dist/token-usage.js +10 -95
- package/dist/types.d.ts +77 -483
- package/dist/watcher.d.ts +5 -78
- package/dist/watcher.js +0 -56
- package/package.json +12 -4
- package/python/AGENTS.md +122 -0
- package/python/NOTICE +36 -0
- package/python/README.md +1 -2
- package/python/examples/README.md +10 -0
- package/python/examples/_harness.py +4 -73
- package/python/examples/demoHcaptcha.py +0 -13
- package/python/examples/demoRecaptcha.py +0 -10
- package/python/examples/watch_playwright.py +1 -26
- package/python/examples/with_local_gguf.py +28 -0
- package/python/examples/with_playwright.py +0 -19
- package/python/pyproject.toml +68 -19
- package/python/src/captchakraken/__init__.py +37 -45
- package/python/src/captchakraken/action_types.py +14 -36
- package/python/src/captchakraken/cli.py +262 -879
- package/python/src/captchakraken/config.py +60 -146
- package/python/src/captchakraken/errors.py +17 -95
- package/python/src/captchakraken/humanize.py +174 -597
- package/python/src/captchakraken/image_processor.py +16 -235
- package/python/src/captchakraken/keyframes.py +69 -434
- package/python/src/captchakraken/kinds.py +152 -0
- package/python/src/captchakraken/models.json +79 -21
- package/python/src/captchakraken/overlay.py +65 -498
- package/python/src/captchakraken/page_solver.py +1123 -2564
- package/python/src/captchakraken/planner.py +70 -428
- package/python/src/captchakraken/prompts.py +45 -261
- package/python/src/captchakraken/selectors.py +184 -0
- package/python/src/captchakraken/server_manager.py +7 -58
- package/python/src/captchakraken/solver.py +128 -522
- package/python/src/captchakraken/timing.py +20 -74
- package/python/src/captchakraken/tool_calls/board_painted.py +47 -0
- package/python/src/captchakraken/tool_calls/find_checkbox.py +20 -30
- package/python/src/captchakraken/tool_calls/find_grid.py +186 -1339
- package/python/src/captchakraken/tool_calls/track_piece.py +81 -57
- package/python/src/captchakraken/updater.py +7 -76
- package/python/src/captchakraken/watcher.py +12 -101
- package/scripts/copy-python.mjs +10 -10
- package/scripts/setup-python.js +1 -21
- package/dist/trajectory.d.ts +0 -58
- package/dist/trajectory.js +0 -266
- package/python/src/captchakraken/tool_calls/move_indicator.py +0 -431
- package/python/src/captchakraken/trajectory.py +0 -284
|
@@ -1,244 +1,25 @@
|
|
|
1
|
-
|
|
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] = []
|
|
1
|
+
"""The frame-diff primitive every movement check in both ports shares."""
|
|
27
2
|
|
|
28
|
-
|
|
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
|
|
3
|
+
import cv2
|
|
36
4
|
|
|
37
|
-
# =========================================================================
|
|
38
|
-
# Static Utility Methods (Stateless)
|
|
39
|
-
# =========================================================================
|
|
40
5
|
|
|
6
|
+
class ImageProcessor:
|
|
41
7
|
@staticmethod
|
|
42
|
-
def
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
8
|
+
def movement_ratio(image1_path: str, image2_path: str) -> float:
|
|
9
|
+
"""Share of pixels differing by more than 30 grey levels; 0 if unreadable, 1 if the sizes differ.
|
|
10
|
+
|
|
11
|
+
Split from `detect_movement` so callers can report the number: GeeTest's permanent background shimmer
|
|
12
|
+
sits close enough to the default threshold to fire the freshness re-solve on nearly every inference.
|
|
50
13
|
"""
|
|
51
|
-
img1 = cv2.imread(image1_path)
|
|
52
|
-
img2 = cv2.imread(image2_path)
|
|
53
|
-
|
|
14
|
+
img1, img2 = cv2.imread(image1_path), cv2.imread(image2_path)
|
|
54
15
|
if img1 is None or img2 is None:
|
|
55
|
-
return
|
|
56
|
-
|
|
16
|
+
return 0.0
|
|
57
17
|
if img1.shape != img2.shape:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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)
|
|
18
|
+
return 1.0
|
|
19
|
+
gray = cv2.cvtColor(cv2.absdiff(img1, img2), cv2.COLOR_BGR2GRAY)
|
|
20
|
+
_, thresh = cv2.threshold(gray, 30, 255, cv2.THRESH_BINARY)
|
|
21
|
+
return cv2.countNonZero(thresh) / float(thresh.shape[0] * thresh.shape[1])
|
|
159
22
|
|
|
160
23
|
@staticmethod
|
|
161
|
-
def
|
|
162
|
-
|
|
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
|
-
|
|
24
|
+
def detect_movement(image1_path: str, image2_path: str, threshold: float = 0.005) -> bool:
|
|
25
|
+
return ImageProcessor.movement_ratio(image1_path, image2_path) > threshold
|