captchakraken 2.9.0 → 3.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 (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 +131 -708
  28. package/dist/solver.js +1319 -3382
  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 +77 -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 +1282 -3101
  60. package/python/src/captchakraken/planner.py +65 -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
@@ -0,0 +1,28 @@
1
+ import os
2
+ import sys
3
+
4
+ from captchakraken import solve_captcha
5
+
6
+ TEXT_CAPTCHAS = {"--text"}
7
+
8
+
9
+ def main() -> int:
10
+ args = [a for a in sys.argv[1:] if a not in TEXT_CAPTCHAS]
11
+ if not args:
12
+ print(__doc__)
13
+ return 2
14
+ image = args[0]
15
+ text_mode = bool(TEXT_CAPTCHAS & set(sys.argv[1:]))
16
+
17
+ print(f"endpoint : {os.getenv('VLLM_BASE_URL', '(default)')}")
18
+ print(f"model : {os.getenv('CAPTCHA_LORA_NAME', '(registry default)')}")
19
+ print(f"prompts : {os.getenv('CAPTCHA_LORA_ADAPTER', '(latest)')}")
20
+
21
+ result = solve_captcha(image, text_mode=text_mode)
22
+
23
+ print(f"answer : {result}")
24
+ return 0 if result else 1
25
+
26
+
27
+ if __name__ == "__main__":
28
+ raise SystemExit(main())
@@ -1,20 +1,3 @@
1
- """
2
- Vanilla Playwright + CaptchaKraken.
3
-
4
- No adapter, and no browser dependency in the package: the driver duck-types the
5
- Playwright surface, so a real `sync_playwright` page satisfies it as-is — as do
6
- patchright's and camoufox's.
7
-
8
- pip install captchakraken playwright && playwright install chromium
9
- export VLLM_BASE_URL=https://api.captchakraken.com/v1
10
- export CAPTCHA_KRAKEN_API_KEY=ck_live_...
11
- python examples/with_playwright.py [url]
12
-
13
- SYNC, NOT ASYNC. `PageSolver` mirrors the synchronous Playwright API. A sync
14
- handle cannot be driven from inside an event loop, so `async_playwright` pages
15
- cannot be solved — use `sync_playwright`, as here.
16
- """
17
-
18
1
  import sys
19
2
 
20
3
  from playwright.sync_api import sync_playwright
@@ -30,9 +13,7 @@ def main() -> int:
30
13
  page = browser.new_page()
31
14
  page.goto(URL)
32
15
 
33
- # ── the whole integration: construct, solve ──────────────────────────
34
16
  result = PageSolver().solve(page)
35
- # ─────────────────────────────────────────────────────────────────────
36
17
 
37
18
  print("✅ solved" if result.is_solved else "❌ not solved")
38
19
  browser.close()
@@ -1,13 +1,13 @@
1
1
  [build-system]
2
- requires = ["hatchling"]
2
+ requires = ["hatchling==1.32.0"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "captchakraken"
7
- version = "2.9.0"
7
+ version = "3.1.0"
8
8
  description = "Captcha-solving engine and CLI: OpenCV grid detection plus a fine-tuned Qwen3.5-9B vision model, for image grids, click and drag puzzles, sliders, distorted text and animated challenges. Hosted API or self-hosted on vLLM."
9
9
  readme = "README.md"
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.11"
11
11
  # Source-available, NOT GPL. The CaptchaKraken Source-Available License permits
12
12
  # commercial use as an internal component but prohibits selling the solve itself,
13
13
  # thin-wrapper resale, and bundling it into a stealth browser someone else ships
@@ -23,7 +23,6 @@ classifiers = [
23
23
  "Development Status :: 5 - Production/Stable",
24
24
  "Intended Audience :: Developers",
25
25
  "Programming Language :: Python :: 3",
26
- "Programming Language :: Python :: 3.10",
27
26
  "Programming Language :: Python :: 3.11",
28
27
  "Programming Language :: Python :: 3.12",
29
28
  "Programming Language :: Python :: 3.13",
@@ -40,29 +39,52 @@ classifiers = [
40
39
  # talks to a vLLM server (local or remote). This is all you need to SOLVE
41
40
  # captchas against an existing endpoint. The heavy serving stack (vllm/torch)
42
41
  # is deliberately NOT here — see the `serve` extra.
42
+ # Pinned exactly, never ranged: a dependency changes when we decide it does.
43
+ #
44
+ # EVERY PIN MUST INSTALL ON `requires-python`'s FLOOR, not on the newest
45
+ # interpreter that happens to be on your machine. numpy 2.5.3 was pinned here
46
+ # from a 3.14 box and broke the then-3.10 leg outright — it declares
47
+ # `Requires-Python >=3.12`, so pip could not find it at all.
48
+ #
49
+ # The floor is 3.11 as of 3.0.0, and it moved because the MOUSE moved: `cursory`
50
+ # declares `Requires-Python >=3.11` and drags numpy 2.3.x with it, which declares
51
+ # the same. Neither is a pin we could have chosen differently — 2.2.6 was the
52
+ # newest numpy covering 3.10, and cursory does not run there at all. Resolve
53
+ # against the floor before changing any of these, and see TRIBAL_KNOWLEDGE.md.
43
54
  dependencies = [
44
- "pydantic>=2.0.0",
45
- "pillow>=10.0.0",
46
- "numpy>=1.24.0",
47
- "opencv-python-headless>=4.10.0",
48
- "requests>=2.31.0",
49
- "python-dotenv>=1.0.0",
55
+ "pydantic==2.13.5",
56
+ "pillow==12.3.0",
57
+ # 2.3.x rather than 2.2.6 because `cursory` requires `numpy~=2.3.3` and the
58
+ # old pin was ours, not opencv's — opencv-python-headless asks only for
59
+ # `numpy>=2`. Moved deliberately as part of the major bump.
60
+ "numpy==2.3.5",
61
+ "opencv-python-headless==5.0.0.93",
62
+ "requests==2.34.2",
63
+ "python-dotenv==1.2.3",
64
+ # The mouse. LGPL-3.0-or-later, used as an ordinary installed dependency and
65
+ # never vendored or bundled, which is the arrangement the licence is written
66
+ # for — see NOTICE. Its TypeScript port `cursory-js` is the JS driver's
67
+ # dependency, and the two agree bit for bit on a seed.
68
+ "cursory==2.0.0",
50
69
  ]
51
70
 
52
71
  [project.optional-dependencies]
53
72
  # Self-hosting: install this to RUN a local vLLM server (the setup script does
54
73
  # `pip install "captchakraken[serve]"`). Not needed to call a remote server.
74
+ # torch is the version vllm 0.29.0 itself pins; any other value makes this
75
+ # extra unresolvable rather than merely different.
55
76
  serve = [
56
- "vllm>=0.6.3",
57
- "torch>=2.0.0",
58
- "transformers>=4.40.0",
59
- "accelerate>=0.27.0",
60
- "huggingface_hub>=0.23.0",
77
+ "vllm==0.29.0",
78
+ "torch==2.13.0",
79
+ "transformers==5.17.0",
80
+ "accelerate==1.15.0",
81
+ "huggingface_hub==1.30.0",
61
82
  ]
62
83
  dev = [
63
- "pytest>=7.0.0",
64
- "mypy>=1.0.0",
65
- "ruff>=0.1.0",
84
+ "pytest==9.1.1",
85
+ "pytest-cov==7.1.0",
86
+ "mypy==2.3.1",
87
+ "ruff==0.16.6",
66
88
  ]
67
89
 
68
90
  [project.scripts]
@@ -78,12 +100,39 @@ Changelog = "https://github.com/JWriter20/CaptchaKraken/blob/main/CHANGELOG.md"
78
100
  [tool.hatch.build.targets.wheel]
79
101
  packages = ["src/captchakraken"]
80
102
 
103
+ # AGENTS.md lives beside pyproject.toml, not inside the package, so the wheel
104
+ # would not carry it. An agent that only ever sees `site-packages/captchakraken`
105
+ # is exactly the reader it is written for, so it is copied in.
106
+ [tool.hatch.build.targets.wheel.force-include]
107
+ "AGENTS.md" = "captchakraken/AGENTS.md"
108
+
81
109
  [tool.pytest.ini_options]
82
110
  testpaths = ["tests"]
83
111
  pythonpath = ["src"]
84
112
 
113
+ # Coverage is NOT in addopts on purpose: the rule here is to run the failing
114
+ # test rather than the suite, and a threshold measured over one test would fail
115
+ # every time. CI asks for it explicitly.
116
+ [tool.coverage.run]
117
+ source = ["captchakraken"]
118
+ branch = true
119
+
120
+ [tool.coverage.report]
121
+ precision = 2
122
+ # The floor is the value measured over the suite as it stands, rounded down to a
123
+ # whole percent — not the 90% the spec asks for. A gate that is red the day it
124
+ # lands blocks every pull request and teaches people to route around it; the
125
+ # real distance to 90% is recorded in TRIBAL_KNOWLEDGE.md so it cannot be
126
+ # forgotten. Raise this as coverage rises, never lower it.
127
+ #
128
+ # 53 -> 58: dead code removed (the denominator shrank) plus behaviour tests on
129
+ # the overlay, the checkbox detector, the wait-gate metric, the CLI wire format
130
+ # and the worker protocol. Measured 58.33% on 3.11 and 58.31% on 3.12; the floor
131
+ # is the lower, rounded down.
132
+ fail_under = 69
133
+
85
134
  [tool.mypy]
86
- python_version = "3.10"
135
+ python_version = "3.11"
87
136
  warn_return_any = false
88
137
  warn_unused_configs = true
89
138
  disallow_untyped_defs = false
@@ -1,20 +1,6 @@
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.
13
- """
14
-
15
1
  from pathlib import Path
16
2
 
17
- try: # pragma: no cover
3
+ try:
18
4
  from dotenv import load_dotenv
19
5
 
20
6
  project_root = Path(__file__).resolve().parent.parent
@@ -32,16 +18,20 @@ from .action_types import (
32
18
  from .image_processor import ImageProcessor
33
19
  from .overlay import add_overlays_to_image
34
20
 
35
- # The hosted API's refusal type. Outside the try below on purpose: `errors`
36
- # imports nothing but `typing`, and docs/hosted-api.md tells every caller to
37
- # branch on its `.code` — a recipe that has to work in both ports, and in a
38
- # minimal install that has no serving stack.
39
21
  from .errors import CaptchaKrakenAPIError
22
+ from .kinds import (
23
+ ActionKind,
24
+ Availability,
25
+ ErrorCode,
26
+ HumanizationMode,
27
+ KeyframeMode,
28
+ Outcome,
29
+ PauseKind,
30
+ PromptFamily,
31
+ RetryMode,
32
+ Vendor,
33
+ )
40
34
 
41
- # How gestures are PERFORMED — mouse, touch, or nothing. Deliberately outside
42
- # the try below: `humanize` imports only `trajectory`, so it has no dependency
43
- # floor at all, and a caller writing a custom Humanizer must be able to import
44
- # the base class without the serving stack installed.
45
35
  from .humanize import (
46
36
  AppiumTouchBackend,
47
37
  CdpTouchBackend,
@@ -53,33 +43,30 @@ from .humanize import (
53
43
  TouchscreenTouchBackend,
54
44
  )
55
45
 
56
- # The planner (requests) and solver (torch/vllm/transformers) pull in the heavy
57
- # serving stack. Keep them optional so leaf modules — e.g. tool_calls.find_grid,
58
- # which needs only cv2 + numpy + pillow — can be imported in a minimal env (CI's
59
- # hermetic grid-detection test) without the full GPU dependency set installed.
60
- try: # pragma: no cover - exercised only when the serving stack is installed
46
+ try:
61
47
  from .planner import ActionPlanner
62
48
  from .solver import CaptchaSolver, solve_captcha
63
49
 
64
- # The page driver sits behind the same guard: it imports `solver`, so it has
65
- # the same dependency floor. It needs NO browser package of its own — the
66
- # caller supplies the Playwright-compatible page (see page_solver's module
67
- # docstring on why we duck-type rather than import one).
68
50
  from .page_solver import PageSolver, SolveResult, solve_captcha_on_page
51
+ from .selectors import SELECTORS, VendorSelectors
69
52
  from .watcher import CaptchaWatcher
70
53
  except ModuleNotFoundError:
71
- ActionPlanner = None # type: ignore[assignment,misc]
72
- CaptchaSolver = None # type: ignore[assignment,misc]
73
- solve_captcha = None # type: ignore[assignment]
74
- PageSolver = None # type: ignore[assignment,misc]
75
- CaptchaWatcher = None # type: ignore[assignment,misc]
76
- SolveResult = None # type: ignore[assignment,misc]
77
- solve_captcha_on_page = None # type: ignore[assignment]
54
+ ActionPlanner = None
55
+ CaptchaSolver = None
56
+ solve_captcha = None
57
+ PageSolver = None
58
+ SELECTORS = None
59
+ VendorSelectors = None
60
+ CaptchaWatcher = None
61
+ SolveResult = None
62
+ solve_captcha_on_page = None
78
63
 
79
64
  __all__ = [
80
65
  "CaptchaSolver",
81
66
  "solve_captcha",
82
67
  "PageSolver",
68
+ "SELECTORS",
69
+ "VendorSelectors",
83
70
  "CaptchaWatcher",
84
71
  "SolveResult",
85
72
  "solve_captcha_on_page",
@@ -100,11 +87,16 @@ __all__ = [
100
87
  "CdpTouchBackend",
101
88
  "AppiumTouchBackend",
102
89
  "TouchscreenTouchBackend",
90
+ "ActionKind",
91
+ "Availability",
92
+ "ErrorCode",
93
+ "HumanizationMode",
94
+ "KeyframeMode",
95
+ "Outcome",
96
+ "PauseKind",
97
+ "PromptFamily",
98
+ "RetryMode",
99
+ "Vendor",
103
100
  ]
104
101
 
105
- # KEEP IN STEP with python/pyproject.toml and js/package.json — the two ports
106
- # ship together (rule 1c) and this is the only one importable code can read.
107
- # It said 2.6.0 while the wheel on PyPI said 2.6.1, so anyone gating on the
108
- # runtime attribute saw a version that had not been current for weeks.
109
- # tests/test_public_contract.py compares all three.
110
- __version__ = "2.9.0"
102
+ __version__ = "3.1.0"
@@ -2,12 +2,10 @@ from typing import List, Literal, Optional, Union, Tuple
2
2
 
3
3
  from pydantic import BaseModel, RootModel
4
4
 
5
+ from .kinds import ActionKind
6
+
5
7
 
6
8
  class BoundingBox(RootModel):
7
- """
8
- Strongly typed bounding box: [x1, y1, x2, y2] in percentages (0.0 to 1.0).
9
- Acts like a list for convenience but ensures exactly 4 float elements.
10
- """
11
9
  root: Tuple[float, float, float, float]
12
10
 
13
11
  def __iter__(self):
@@ -21,57 +19,37 @@ class BoundingBox(RootModel):
21
19
 
22
20
 
23
21
  class Action(BaseModel):
24
- action: str
25
- # ANIMATED CHALLENGES ONLY. Path to the keyframe the model chose to act on, and
26
- # its 1-based number in the set that was sent.
27
- #
28
- # Why an action carries a picture: on an animated puzzle the target is only
29
- # present part of the time, so these coordinates are correct only while the
30
- # widget looks the way it did in that keyframe. The driver therefore holds the
31
- # mouse until the live neighbourhood around the click point matches the same
32
- # neighbourhood of this file, then clicks. Without it the click fires on
33
- # whatever happens to be on screen, which for a fading sprite is usually
34
- # background.
35
- #
36
- # Absent on every still puzzle, where there is no moment to wait for. Both are
37
- # set together or not at all — a number with no image cannot be waited on, and
38
- # an image with no number cannot be reported.
22
+ action: ActionKind
39
23
  await_keyframe: Optional[str] = None
40
24
  frame: Optional[int] = None
41
25
 
42
26
 
43
27
  class ClickAction(Action):
44
- action: Literal["click"]
45
- target_bounding_boxes: List[BoundingBox] # List of [x1, y1, x2, y2] in percentages
28
+ action: Literal[ActionKind.CLICK]
29
+ target_bounding_boxes: List[BoundingBox]
46
30
 
47
31
 
48
32
  class DragAction(Action):
49
- action: Literal["drag"]
50
- # Optional, not merely defaulted: a PUZZLE-PIECE SLIDER is carried as a drag
51
- # with source_bounding_box explicitly None, and pydantic validates a value
52
- # that is passed even when it equals the default. Declaring these as bare
53
- # `BoundingBox = None` made constructing a slide raise ValidationError —
54
- # every slide puzzle, at the moment of acting on a correct answer.
55
- source_bounding_box: Optional[BoundingBox] = None # [x1, y1, x2, y2] in percentages
56
- target_bounding_box: Optional[BoundingBox] = None # [x1, y1, x2, y2] in percentages
33
+ action: Literal[ActionKind.DRAG]
34
+ # `Optional[...] = None`, never a bare `BoundingBox = None`: pydantic validates a passed value even when it
35
+ # equals the default, and the bare form made every slide (a drag with no source) raise ValidationError.
36
+ source_bounding_box: Optional[BoundingBox] = None
37
+ target_bounding_box: Optional[BoundingBox] = None
57
38
 
58
39
 
59
40
  class TypeAction(Action):
60
- """Type text into an input."""
61
- action: Literal["type"]
41
+ action: Literal[ActionKind.TYPE]
62
42
  text: str
63
- target_bounding_box: Optional[BoundingBox] = None # [x1, y1, x2, y2] in percentages
43
+ target_bounding_box: Optional[BoundingBox] = None
64
44
 
65
45
 
66
46
  class WaitAction(Action):
67
- """Wait for a specified duration."""
68
- action: Literal["wait"]
47
+ action: Literal[ActionKind.WAIT]
69
48
  duration_ms: int
70
49
 
71
50
 
72
51
  class DoneAction(Action):
73
- """Signal that the captcha is solved or no further actions are needed."""
74
- action: Literal["done"]
52
+ action: Literal[ActionKind.DONE]
75
53
 
76
54
 
77
55
  CaptchaAction = Union[ClickAction, DragAction, TypeAction, WaitAction, DoneAction]