captchakraken 2.9.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.
Files changed (77) hide show
  1. package/AGENTS.md +112 -0
  2. package/NOTICE +36 -0
  3. package/dist/cli-invocation.d.ts +7 -36
  4. package/dist/cli-invocation.js +8 -49
  5. package/dist/errors.d.ts +7 -27
  6. package/dist/errors.js +6 -25
  7. package/dist/humanize.d.ts +56 -240
  8. package/dist/humanize.js +159 -415
  9. package/dist/index.d.ts +5 -129
  10. package/dist/index.js +4 -98
  11. package/dist/kinds.d.ts +135 -0
  12. package/dist/kinds.js +70 -0
  13. package/dist/limits.d.ts +0 -48
  14. package/dist/limits.js +2 -48
  15. package/dist/model-name.d.ts +4 -19
  16. package/dist/model-name.js +32 -40
  17. package/dist/playwright-types.d.ts +25 -95
  18. package/dist/playwright-types.js +0 -23
  19. package/dist/puppeteer-adapter.d.ts +4 -43
  20. package/dist/puppeteer-adapter.js +22 -58
  21. package/dist/python-command.d.ts +0 -41
  22. package/dist/python-command.js +0 -37
  23. package/dist/selectors.d.ts +54 -0
  24. package/dist/selectors.js +117 -0
  25. package/dist/slide-geometry.d.ts +2 -29
  26. package/dist/slide-geometry.js +18 -44
  27. package/dist/solver.d.ts +94 -712
  28. package/dist/solver.js +923 -3362
  29. package/dist/timing.d.ts +7 -40
  30. package/dist/timing.js +4 -36
  31. package/dist/token-usage.d.ts +1 -7
  32. package/dist/token-usage.js +10 -95
  33. package/dist/types.d.ts +75 -493
  34. package/dist/watcher.d.ts +5 -78
  35. package/dist/watcher.js +0 -56
  36. package/package.json +11 -4
  37. package/python/AGENTS.md +122 -0
  38. package/python/NOTICE +36 -0
  39. package/python/README.md +1 -2
  40. package/python/examples/README.md +10 -0
  41. package/python/examples/_harness.py +4 -73
  42. package/python/examples/demoHcaptcha.py +0 -13
  43. package/python/examples/demoRecaptcha.py +0 -10
  44. package/python/examples/watch_playwright.py +1 -26
  45. package/python/examples/with_local_gguf.py +28 -0
  46. package/python/examples/with_playwright.py +0 -19
  47. package/python/pyproject.toml +68 -19
  48. package/python/src/captchakraken/__init__.py +37 -45
  49. package/python/src/captchakraken/action_types.py +14 -36
  50. package/python/src/captchakraken/cli.py +262 -883
  51. package/python/src/captchakraken/config.py +60 -146
  52. package/python/src/captchakraken/errors.py +17 -95
  53. package/python/src/captchakraken/humanize.py +174 -597
  54. package/python/src/captchakraken/image_processor.py +16 -235
  55. package/python/src/captchakraken/keyframes.py +46 -484
  56. package/python/src/captchakraken/kinds.py +152 -0
  57. package/python/src/captchakraken/models.json +36 -4
  58. package/python/src/captchakraken/overlay.py +65 -498
  59. package/python/src/captchakraken/page_solver.py +1052 -3098
  60. package/python/src/captchakraken/planner.py +50 -487
  61. package/python/src/captchakraken/prompts.py +45 -261
  62. package/python/src/captchakraken/selectors.py +184 -0
  63. package/python/src/captchakraken/server_manager.py +7 -58
  64. package/python/src/captchakraken/solver.py +128 -522
  65. package/python/src/captchakraken/timing.py +20 -74
  66. package/python/src/captchakraken/tool_calls/board_painted.py +47 -0
  67. package/python/src/captchakraken/tool_calls/find_checkbox.py +20 -30
  68. package/python/src/captchakraken/tool_calls/find_grid.py +186 -1339
  69. package/python/src/captchakraken/tool_calls/track_piece.py +81 -57
  70. package/python/src/captchakraken/updater.py +7 -76
  71. package/python/src/captchakraken/watcher.py +12 -101
  72. package/scripts/copy-python.mjs +10 -10
  73. package/scripts/setup-python.js +1 -21
  74. package/dist/trajectory.d.ts +0 -58
  75. package/dist/trajectory.js +0 -266
  76. package/python/src/captchakraken/tool_calls/move_indicator.py +0 -431
  77. package/python/src/captchakraken/trajectory.py +0 -284
@@ -1,244 +1,25 @@
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] = []
1
+ """The frame-diff primitive every movement check in both ports shares."""
27
2
 
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
3
+ import cv2
36
4
 
37
- # =========================================================================
38
- # Static Utility Methods (Stateless)
39
- # =========================================================================
40
5
 
6
+ class ImageProcessor:
41
7
  @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.
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 False
56
-
16
+ return 0.0
57
17
  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)
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 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
-
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