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
package/dist/watcher.d.ts CHANGED
@@ -1,99 +1,26 @@
1
1
  /**
2
- * Auto-solve watcher — install once, and captchas are solved as they appear.
3
- *
4
- * WHY THIS IS A POLL AND NOT AN INJECTED OBSERVER
5
- * The obvious implementation is a `MutationObserver` inside the page that calls
6
- * out through `exposeBinding`. That is faster to react, and it is also the one
7
- * design that cannot be made stealthy on every platform: `exposeBinding` puts a
8
- * function on `window` and an observer is script the page can enumerate. On
9
- * Camoufox both are sandboxed and invisible; on vanilla Playwright or Puppeteer
10
- * they are not, and a captcha vendor is exactly the party that looks.
11
- *
12
- * So the watcher injects NOTHING. It drives the same `detectCaptcha()` the
13
- * solver already uses, from the driver side, on a timer. Consequences worth
14
- * knowing:
15
- *
16
- * - Nothing is added to the page on ANY platform, so there is no new
17
- * detection surface to reason about per launcher.
18
- * - On Camoufox the probe's DOM reads run in Camoufox's isolated Juggler
19
- * world for free — that is its default for all Playwright evaluation
20
- * (`main_world_eval` / an `mw:` prefix is the opt-OUT). The watcher never
21
- * opts out, so it is isolated there by construction, not by special-casing.
22
- * - Reaction time is bounded by `intervalMs`, not by the mutation itself.
23
- * A captcha that appears just after a tick waits up to one interval.
24
- *
25
- *
26
- * ONE WATCHER COVERS THE WHOLE PAGE, FOR ITS WHOLE LIFE
27
- * A `Page` outlives navigation, and so does the watcher holding it: install it
28
- * once and it keeps probing across every `goto`, staying quiet while there is
29
- * nothing to solve and re-arming after each solve. That is deliberately the
30
- * whole API — the case that motivates a browser-wide installer is almost always
31
- * a challenge appearing on request 40 of a run, on the SAME page object the run
32
- * started with, and that is already covered by one line. Pinned in
33
- * browser-compat.test.ts.
34
- * DETECTION IS THE SOLVER'S, NOT A COPY
35
- * The trigger is `solver.detectCaptcha(page)`, the same method `solve()` calls,
36
- * so "what counts as a captcha" has exactly one definition. An earlier sketch
37
- * kept a union of the vendor selectors here for a cheaper single-roundtrip
38
- * probe; it drifted from VENDOR_WIDGET_LOCATORS the first time a vendor was
39
- * added, which is the failure this indirection exists to prevent.
40
- *
41
- * ```typescript
42
- * const solver = new CaptchaKrakenSolver();
43
- * const watcher = solver.watch(page, { onSolved: r => console.log(r.isSolved) });
44
- * // ... your automation runs; captchas are solved underneath it ...
45
- * await watcher.stop();
46
- * ```
2
+ * Polls; injects nothing. An exposed binding or a MutationObserver is script the page can enumerate under
3
+ * vanilla Playwright or Puppeteer, and a captcha vendor is exactly the party that looks. See TRIBAL_KNOWLEDGE.md.
47
4
  */
48
5
  import { PlaywrightPage as Page } from './playwright-types';
49
6
  import { SolveResult } from './types';
50
- /**
51
- * The slice of the solver the watcher drives.
52
- *
53
- * Structural rather than an import of `CaptchaKrakenSolver` so this module has
54
- * no runtime dependency on solver.ts (which imports this one for the `watch()`
55
- * method — a real cycle under CommonJS), and so tests can drive it with a fake.
56
- */
7
+ /** Structural, not the solver class: a real import here would be a CommonJS cycle. Detection reuses detectCaptcha because a copied selector union drifted the first time a vendor was added. */
57
8
  export interface WatchableSolver {
58
9
  detectCaptcha(page: Page): Promise<unknown | null>;
59
10
  solve(page: Page): Promise<SolveResult | void>;
60
11
  }
61
12
  export interface WatchOptions {
62
- /**
63
- * Milliseconds between detection probes. Default 1000.
64
- *
65
- * This is the latency floor for noticing a captcha, and each tick costs one
66
- * `detectCaptcha()` — a handful of selector queries. Below ~250ms you are
67
- * paying real CPU for latency the solve itself (seconds) makes irrelevant.
68
- */
13
+ /** Default 1000; below ~250 is wasted CPU. */
69
14
  intervalMs?: number;
70
- /** Stop automatically after this many successful solves. Default: unlimited. */
71
15
  maxSolves?: number;
72
- /**
73
- * Wait this long after a solve THROWS before probing again. Default 5000.
74
- *
75
- * Without it a permanently unsupported challenge — an invisible reCAPTCHA v3,
76
- * say — turns the watcher into a hot loop that re-attempts and re-bills every
77
- * `intervalMs` forever.
78
- */
16
+ /** Default 5000. Without it a permanently unsupported challenge is a hot loop that re-attempts and re-bills every interval forever. */
79
17
  errorBackoffMs?: number;
80
- /** Called after each solve that returns a result. Exceptions here are swallowed. */
81
18
  onSolved?: (result: SolveResult) => void | Promise<void>;
82
- /**
83
- * Called when detection or solving throws. Exceptions here are swallowed.
84
- *
85
- * Errors are reported, never fatal: `NoCaptchaFoundError` fires routinely
86
- * when a widget disappears between the probe and the solve, and a watcher
87
- * that died on it would be useless.
88
- */
89
19
  onError?: (error: unknown) => void | Promise<void>;
90
20
  }
91
21
  export interface CaptchaWatcher {
92
- /** Stop probing and resolve once any in-flight solve has finished. */
93
22
  stop(): Promise<void>;
94
- /** False once stopped, the page closed, or `maxSolves` was reached. */
95
23
  readonly running: boolean;
96
- /** Successful solves so far. */
97
24
  readonly solves: number;
98
25
  }
99
26
  export declare function watchPage(solver: WatchableSolver, page: Page, options?: WatchOptions): CaptchaWatcher;
package/dist/watcher.js CHANGED
@@ -1,58 +1,9 @@
1
1
  "use strict";
2
- /**
3
- * Auto-solve watcher — install once, and captchas are solved as they appear.
4
- *
5
- * WHY THIS IS A POLL AND NOT AN INJECTED OBSERVER
6
- * The obvious implementation is a `MutationObserver` inside the page that calls
7
- * out through `exposeBinding`. That is faster to react, and it is also the one
8
- * design that cannot be made stealthy on every platform: `exposeBinding` puts a
9
- * function on `window` and an observer is script the page can enumerate. On
10
- * Camoufox both are sandboxed and invisible; on vanilla Playwright or Puppeteer
11
- * they are not, and a captcha vendor is exactly the party that looks.
12
- *
13
- * So the watcher injects NOTHING. It drives the same `detectCaptcha()` the
14
- * solver already uses, from the driver side, on a timer. Consequences worth
15
- * knowing:
16
- *
17
- * - Nothing is added to the page on ANY platform, so there is no new
18
- * detection surface to reason about per launcher.
19
- * - On Camoufox the probe's DOM reads run in Camoufox's isolated Juggler
20
- * world for free — that is its default for all Playwright evaluation
21
- * (`main_world_eval` / an `mw:` prefix is the opt-OUT). The watcher never
22
- * opts out, so it is isolated there by construction, not by special-casing.
23
- * - Reaction time is bounded by `intervalMs`, not by the mutation itself.
24
- * A captcha that appears just after a tick waits up to one interval.
25
- *
26
- *
27
- * ONE WATCHER COVERS THE WHOLE PAGE, FOR ITS WHOLE LIFE
28
- * A `Page` outlives navigation, and so does the watcher holding it: install it
29
- * once and it keeps probing across every `goto`, staying quiet while there is
30
- * nothing to solve and re-arming after each solve. That is deliberately the
31
- * whole API — the case that motivates a browser-wide installer is almost always
32
- * a challenge appearing on request 40 of a run, on the SAME page object the run
33
- * started with, and that is already covered by one line. Pinned in
34
- * browser-compat.test.ts.
35
- * DETECTION IS THE SOLVER'S, NOT A COPY
36
- * The trigger is `solver.detectCaptcha(page)`, the same method `solve()` calls,
37
- * so "what counts as a captcha" has exactly one definition. An earlier sketch
38
- * kept a union of the vendor selectors here for a cheaper single-roundtrip
39
- * probe; it drifted from VENDOR_WIDGET_LOCATORS the first time a vendor was
40
- * added, which is the failure this indirection exists to prevent.
41
- *
42
- * ```typescript
43
- * const solver = new CaptchaKrakenSolver();
44
- * const watcher = solver.watch(page, { onSolved: r => console.log(r.isSolved) });
45
- * // ... your automation runs; captchas are solved underneath it ...
46
- * await watcher.stop();
47
- * ```
48
- */
49
2
  Object.defineProperty(exports, "__esModule", { value: true });
50
3
  exports.watchPage = watchPage;
51
- /** A page that has closed, where the launcher exposes that (Playwright and Puppeteer both do). */
52
4
  function isPageClosed(page) {
53
5
  return typeof page.isClosed === 'function' ? page.isClosed() : false;
54
6
  }
55
- /** Report to a user callback without letting its failure reach the loop. */
56
7
  async function report(fn, value) {
57
8
  if (!fn)
58
9
  return;
@@ -60,7 +11,6 @@ async function report(fn, value) {
60
11
  await fn(value);
61
12
  }
62
13
  catch {
63
- // A throwing callback is the caller's bug and must not stop the watcher.
64
14
  }
65
15
  }
66
16
  function watchPage(solver, page, options = {}) {
@@ -69,12 +19,8 @@ function watchPage(solver, page, options = {}) {
69
19
  const maxSolves = options.maxSolves ?? Infinity;
70
20
  let running = true;
71
21
  let solves = 0;
72
- // Resolved by the loop on its way out, so stop() can await a solve in flight.
73
- // Definite assignment: the Promise executor runs synchronously, so `finished`
74
- // is set before any code below can reach it — TS cannot prove that itself.
75
22
  let finished;
76
23
  const done = new Promise((resolve) => { finished = resolve; });
77
- // Held so stop() can cut a pending sleep short instead of waiting it out.
78
24
  let wake = null;
79
25
  const sleep = (ms) => new Promise((resolve) => {
80
26
  const timer = setTimeout(() => { wake = null; resolve(); }, ms);
@@ -91,8 +37,6 @@ function watchPage(solver, page, options = {}) {
91
37
  try {
92
38
  if (!(await solver.detectCaptcha(page)))
93
39
  continue;
94
- // Re-check: detectCaptcha can take a moment, and stop() during it
95
- // should not be followed by a fresh multi-second solve.
96
40
  if (!running)
97
41
  break;
98
42
  const result = await solver.solve(page);
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "captchakraken",
3
- "version": "2.9.0",
4
- "description": "Captcha solver for Playwright and Puppeteer. Finds the captcha, reads the puzzle with a fine-tuned Qwen3.5-9B vision model, and clicks, drags, slides or types through to a token — reCAPTCHA, hCaptcha, GeeTest, NetEase Yidun, Tencent, Lemin, Prosopo, Turnstile and distorted text. Hosted API or self-hosted.",
3
+ "version": "3.1.0",
4
+ "description": "Captcha solver for Playwright and Puppeteer. Finds the captcha, reads the puzzle with a fine-tuned Qwen3.5-9B vision model, and clicks, drags, slides or types through to a token \u2014 reCAPTCHA, hCaptcha, GeeTest, NetEase Yidun, Tencent, Lemin, Prosopo, Turnstile and distorted text. Hosted API or self-hosted.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
8
  "dist",
9
+ "NOTICE",
9
10
  "python",
10
11
  "scripts",
12
+ "AGENTS.md",
11
13
  "LICENSE"
12
14
  ],
13
15
  "scripts": {
@@ -16,6 +18,7 @@
16
18
  "demo": "tsx examples/demoSites.ts",
17
19
  "typecheck": "tsc --noEmit -p tsconfig.json",
18
20
  "test": "tsc -p tsconfig.test.json && node --test .test-build/*.test.js",
21
+ "coverage": "tsc -p tsconfig.test.json && c8 node --test .test-build/*.test.js",
19
22
  "prepublishOnly": "npm run build",
20
23
  "postinstall": "node scripts/setup-python.js"
21
24
  },
@@ -49,8 +52,12 @@
49
52
  "url": "https://github.com/JWriter20/CaptchaKraken/issues"
50
53
  },
51
54
  "homepage": "https://github.com/JWriter20/CaptchaKraken#readme",
55
+ "dependencies": {
56
+ "cursory-js": "2.0.1"
57
+ },
52
58
  "devDependencies": {
53
- "@types/node": "^24.10.2",
54
- "typescript": "^5.9.3"
59
+ "@types/node": "24.10.2",
60
+ "c8": "12.0.0",
61
+ "typescript": "5.9.3"
55
62
  }
56
63
  }
@@ -0,0 +1,122 @@
1
+ # captchakraken (PyPI)
2
+
3
+ The Python **engine and CLI**: OpenCV grid detection plus a planner that asks a
4
+ fine-tuned vision model what to do, and a page driver that performs it in a
5
+ Playwright browser.
6
+
7
+ It is not a browser and, by default, not the model either. It installs no
8
+ browser — bring your own — and every inference goes to a model endpoint you
9
+ point it at. The `serve` extra is what turns this box into that endpoint.
10
+
11
+ Repo, guides and the full agent guide:
12
+ <https://github.com/JWriter20/CaptchaKraken>
13
+
14
+ ---
15
+
16
+ ## 1. Point it at a model, first
17
+
18
+ | You have | Do this |
19
+ |---|---|
20
+ | No GPU | Use the hosted API — add the account MCP server, `npx -y captchakraken-mcp`, call `sign_in` then `create_api_key` |
21
+ | A GPU, and want it local | `pip install "captchakraken[serve]"`, then `captchakraken server start` |
22
+ | Your own vLLM server | `export VLLM_BASE_URL=…/v1` and `export CAPTCHA_KRAKEN_API_KEY=…` |
23
+
24
+ `create_api_key` writes the key and endpoint to `~/.captchakraken/credentials`
25
+ (mode 0600) and the client reads that file by itself, so the MCP path needs **no
26
+ environment variables at all**. The key is never printed into the conversation.
27
+
28
+ A local endpoint auto-starts on the first solve; `CAPTCHA_KRAKEN_AUTOSTART=0`
29
+ turns that off. A non-localhost `VLLM_BASE_URL` is never started or managed for
30
+ you.
31
+
32
+ ## 2. Install
33
+
34
+ ```bash
35
+ pip install captchakraken # the client: grid detection + planner + CLI
36
+ pip install "captchakraken[serve]" # ...and the vLLM serving stack, to host it
37
+ ```
38
+
39
+ Python 3.11+.
40
+
41
+ ## 3. Solve
42
+
43
+ Synchronous Playwright:
44
+
45
+ ```python
46
+ from playwright.sync_api import sync_playwright
47
+ from captchakraken import PageSolver
48
+
49
+ with sync_playwright() as p:
50
+ page = p.chromium.launch().new_page()
51
+ page.goto("https://www.google.com/recaptcha/api2/demo")
52
+ result = PageSolver().solve(page)
53
+ print(result.is_solved)
54
+ ```
55
+
56
+ `PageSolver().watch(page)` installs a background watcher instead, for when you
57
+ do not know if or when a captcha will appear; stop it with `.stop()`.
58
+
59
+ No browser, just an image:
60
+
61
+ ```bash
62
+ captchakraken path/to/captcha.png # prints a JSON click plan
63
+ ```
64
+
65
+ Other CLI modes, all printing JSON: `captchakraken server start|stop|status|run`
66
+ manages a local vLLM server, `captchakraken fetch` updates weights and the
67
+ serving stack together, and the OpenCV tool calls (`find-grid`, `find-checkbox`,
68
+ `get-numbered-grid`, `detect-selected`, …) are exposed one per subcommand. The
69
+ module docstring of `captchakraken/cli.py` lists every mode with its arguments.
70
+
71
+ ## 4. Errors
72
+
73
+ `PageSolver.solve()` raises from `captchakraken.page_solver`:
74
+ `NoCaptchaFoundError` (no interactive widget — usually not a failure),
75
+ `UnsupportedChallengeError`, `AnimatedChallengeError` (the challenge could not
76
+ be *recorded*), `PageClosedError`, and `CaptchaSolveError`, which is the base
77
+ class of the other four — catch it last or it swallows them. The TypeScript
78
+ port raises none of these; it exports one error class and throws plain `Error`
79
+ otherwise.
80
+
81
+ Refusals from the hosted API raise `CaptchaKrakenAPIError` (exported from the
82
+ package root). **Branch on `.code`, never on the message text** — the codes are
83
+ the contract, the wording is not: `insufficient_credits`, `missing_api_key`,
84
+ `invalid_api_key`, `rate_limited`, `request_too_large`, `account_suspended`,
85
+ `upstream_unavailable`. Retrying a `403` that means "no licence" will never
86
+ succeed.
87
+
88
+ ## 5. If you drive the model yourself
89
+
90
+ Grid screenshots must be sent with **cell numbers drawn on them** — the client
91
+ runs `find_grid`, renders the numbered overlay, and sends that image, because
92
+ the model reads those labels and was never trained to invent a numbering.
93
+ Without the overlay reCAPTCHA 4×4 scores zero without erroring. Image area is
94
+ also clamped into the `pixel_budget` the model declares in
95
+ `captchakraken/models.json`. Use the shipped client unless you have a reason not
96
+ to, and never hardcode a model name — read it from the config, or the prompt
97
+ generation and the weights drift apart.
98
+
99
+ ## Rules for agents
100
+
101
+ - **Never invent a number.** Every accuracy, latency and price figure we publish
102
+ is on one page —
103
+ [docs/benchmarks.md](https://github.com/JWriter20/CaptchaKraken/blob/main/docs/benchmarks.md).
104
+ Quote it as written; do not derive, round, or convert anything, and do not
105
+ read a static-image rate as a promise about a browser run.
106
+ - **Never print an API key** into a transcript, a log, or a commit. Use
107
+ `create_api_key`, which writes it to disk instead. `captchakraken.env` and
108
+ `.env` hold keys and are never committed.
109
+ - **Abyss is what our hosted API serves, and it is never downloadable.** The
110
+ current client names an Abyss expert on every request to our endpoint and is
111
+ answered by Abyss; an older client, or a request that names no model, gets
112
+ Twilight v1.2, because its prompts belong to that generation. Nothing about it
113
+ is gated on the account. Do not suggest weights, a download or a workaround
114
+ for it, and do not quote accuracy figures for it — none are published. Verify
115
+ what is serving with `GET /v1/models`.
116
+ - **Respect the licence.** Building automation with this is fine; selling the
117
+ solve, or shipping it inside a browser or automation product, is not. See
118
+ [LICENSE](./LICENSE) and
119
+ [docs/licensing.md](https://github.com/JWriter20/CaptchaKraken/blob/main/docs/licensing.md).
120
+ - **Changing this package's own code?** Its rules are in
121
+ [CONTRIBUTING.md](https://github.com/JWriter20/CaptchaKraken/blob/main/CONTRIBUTING.md),
122
+ not here.
package/python/NOTICE ADDED
@@ -0,0 +1,36 @@
1
+ CaptchaKraken
2
+ Copyright (c) 2026 CaptchaKraken LLC. All rights reserved.
3
+
4
+ This product includes third-party software. The notices below are required by
5
+ those components' licences and do not change the terms of the CaptchaKraken
6
+ Source-Available License, which governs everything in this repository that is
7
+ not listed here.
8
+
9
+ ────────────────────────────────────────────────────────────────────────────
10
+ Cursory — mouse trajectory generation
11
+ ────────────────────────────────────────────────────────────────────────────
12
+
13
+ cursory https://github.com/Vinyzu/cursory (Python, PyPI: cursory)
14
+ cursory-js https://github.com/JWriter20/cursory-js (TypeScript, npm: cursory-js)
15
+
16
+ Copyright (c) Vinyzu and the cursory-js contributors.
17
+ Licensed under the GNU Lesser General Public License, version 3 or later
18
+ (LGPL-3.0-or-later). A copy is available at https://www.gnu.org/licenses/lgpl-3.0.html
19
+
20
+ HOW WE USE IT, AND WHY THAT MATTERS. Both are ORDINARY INSTALLED
21
+ DEPENDENCIES — `cursory` is declared in python/pyproject.toml and `cursory-js`
22
+ in js/package.json, and each is resolved by the user's own package manager at
23
+ install time. Neither is vendored into this repository, copied into our
24
+ sources, or bundled into a build artifact: the TypeScript build is `tsc`, which
25
+ leaves `require('cursory-js')` as a runtime import rather than inlining it, and
26
+ `js/scripts/copy-python.mjs` copies only our own `python/` tree.
27
+
28
+ That arrangement is the one the LGPL is written for, and it is what lets this
29
+ product remain under its own licence. It also means a user may replace either
30
+ library with a modified version of their own — reinstall the package, and the
31
+ driver picks it up — which is the freedom section 4 of the LGPL requires be
32
+ preserved. Do not vendor, inline, or bundle either one without taking legal
33
+ advice first; doing so is the change that would put this obligation somewhere
34
+ other than where it now sits.
35
+
36
+ We have not modified either library.
package/python/README.md CHANGED
@@ -111,8 +111,7 @@ actions = solver.solve("captcha.png")
111
111
  ```
112
112
 
113
113
  Pure-OpenCV tool subcommands (no model): `find-grid`, `find-checkbox`,
114
- `detect-selected`, `grid-cell-states`, `find-move`, `find-movable`, and a
115
- persistent `serve` worker the browser driver polls.
114
+ `detect-selected`, `grid-cell-states`, and a persistent `serve` worker the browser driver polls.
116
115
 
117
116
  ## Configuration (model-agnostic)
118
117
 
@@ -44,6 +44,16 @@ If `CAMOUFOX_BINARY` is unset, camoufox falls back to its default binary
44
44
  source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
45
45
  ```
46
46
 
47
+ Running the model locally on **Ollama** or **LM Studio** instead? Those load
48
+ GGUF, so they need the GGUF build rather than the AWQ/FP8 downloads — see
49
+ [Self-hosting → GGUF](../../docs/self-hosting.md#gguf-for-ollama-lm-studio-and-llamacpp),
50
+ then check the wiring with one image and no browser:
51
+
52
+ ```bash
53
+ python examples/with_local_gguf.py path/to/captcha.png
54
+ python examples/with_local_gguf.py path/to/text-captcha.png --text
55
+ ```
56
+
47
57
  ## Run
48
58
 
49
59
  ```bash
@@ -1,27 +1,3 @@
1
- """
2
- Shared runner for the CaptchaKraken Python demos.
3
-
4
- Launches a real stealth browser (camoufox, using the binary from your fork —
5
- JWriter20/camoufox releases; see README.md), navigates to a page with a captcha
6
- on it, and solves it THE WAY PRODUCTION DOES: `PageSolver` finds the widget,
7
- opens the challenge, clicks, submits, and decides whether the vendor accepted.
8
- It reports token-generation speed, total time, and whether the solve succeeded,
9
- plus a best-effort reason when it didn't.
10
-
11
- Point it at any URL:
12
-
13
- python examples/demoHcaptcha.py # the built-in demo page
14
- python examples/demoHcaptcha.py https://your.site/ # anything else
15
- python examples/demoHcaptcha.py https://your.site/ --headed
16
-
17
- This used to be image-in / actions-out — screenshot the challenge, ask the
18
- engine for a click plan, print how many tiles it would have clicked, and stop.
19
- That measured the model but never the driver, and a recording of it showed a
20
- captcha nobody touched. `captchakraken.page_solver` is the Python mirror of the
21
- TypeScript driver, so both ports now demo the same end-to-end path, which is
22
- what CLAUDE.md rule 1c asks for.
23
- """
24
-
25
1
  import argparse
26
2
  import json
27
3
  import os
@@ -38,17 +14,10 @@ from captchakraken.page_solver import CaptchaSolveError, PageSolver
38
14
  class DemoSpec:
39
15
  name: str
40
16
  url: str
41
- vendor: str # "recaptcha" | "hcaptcha" — advisory; PageSolver auto-detects
17
+ vendor: str
42
18
 
43
19
 
44
20
  def spec_from_argv(default: DemoSpec, argv=None) -> DemoSpec:
45
- """Let any demo point at an arbitrary page.
46
-
47
- The vendor stays a default rather than being inferred from the URL: it only
48
- selects the wording of a failure explanation, and PageSolver detects the
49
- actual widget itself. Guessing it from a hostname would be wrong exactly on
50
- the pages worth demoing — your own site, embedding someone else's captcha.
51
- """
52
21
  ap = argparse.ArgumentParser(
53
22
  description=f"CaptchaKraken demo — {default.name}",
54
23
  epilog="With no URL, runs against the built-in demo page.")
@@ -69,21 +38,8 @@ def spec_from_argv(default: DemoSpec, argv=None) -> DemoSpec:
69
38
 
70
39
 
71
40
  def _launch_kwargs() -> dict:
72
- # HUMANIZE defaults OFF, same as tests/live-solve/solve_fixture.py, and for
73
- # the same measured reason. The solver already moves the mouse along its own
74
- # 60-point trajectory (`_smooth_move` -> `generate_trajectory`); camoufox's
75
- # humanize juggler then expands EACH of those 60 micro-moves into its own
76
- # humanised sub-trajectory — 60 nested traversals to cover one straight
77
- # line. On this demo page that is 25-52s per click round instead of ~5s,
78
- # which does not merely look slow: it blows the 120s overall_solve_timeout
79
- # and reports a solvable captcha as "not solved". What a vendor scores is
80
- # the trajectory SHAPE, and that comes from generate_trajectory either way.
81
- # Set HUMANIZE=1 to drive it the other way against a vendor that
82
- # fingerprints motion.
83
41
  kw = dict(headless=os.getenv("HEADLESS", "1") != "0",
84
42
  humanize=os.getenv("HUMANIZE", "0") != "0", geoip=False)
85
- # Point camoufox at YOUR fork's binary. If unset, camoufox uses its default
86
- # cached binary (`python -m camoufox fetch`).
87
43
  binary = os.getenv("CAMOUFOX_BINARY") or os.getenv("CAMOUFOX_EXECUTABLE_PATH")
88
44
  if binary:
89
45
  kw["executable_path"] = binary
@@ -91,13 +47,6 @@ def _launch_kwargs() -> dict:
91
47
 
92
48
 
93
49
  def _page_kwargs() -> dict:
94
- """Record the session when CAPTCHA_DEMO_VIDEO_DIR is set.
95
-
96
- Plain Playwright options, but they only produce a video on a build whose
97
- screencast actually emits frames. Stock camoufox accepts every one of them
98
- and writes either nothing or a blank 0.96s file — no error anywhere — so a
99
- caller that cares should check the size of what it gets back.
100
- """
101
50
  out = os.getenv("CAPTCHA_DEMO_VIDEO_DIR")
102
51
  if not out:
103
52
  return {}
@@ -107,27 +56,17 @@ def _page_kwargs() -> dict:
107
56
 
108
57
 
109
58
  def _finish_video(page):
110
- """Close the context so the container is finalised; return the file path.
111
-
112
- Playwright writes the video on CONTEXT close, not page close — reading
113
- video.path() before that names a file which may never appear.
114
- """
115
59
  video = getattr(page, "video", None)
116
60
  if video is None:
117
61
  return None
118
62
  try:
119
63
  page.context.close()
120
64
  return video.path()
121
- except Exception: # noqa: BLE001 — a missing video must not fail the demo
65
+ except Exception:
122
66
  return None
123
67
 
124
68
 
125
69
  def _tokens(usage) -> tuple[int, int]:
126
- """(input, output) over a SolveResult's usage log.
127
-
128
- Accepts both spellings because the two ports report different ones and this
129
- is a demo, not a place to discover a key mismatch as a zero.
130
- """
131
70
  inp = out = 0
132
71
  for u in usage or []:
133
72
  inp += int(u.get("prompt_tokens", u.get("inputTokens", 0)) or 0)
@@ -175,11 +114,9 @@ def run_demo(spec: DemoSpec) -> None:
175
114
  page = browser.new_page(**_page_kwargs())
176
115
  try:
177
116
  page.goto(spec.url, wait_until="domcontentloaded", timeout=60_000)
178
- # The widget's iframe injects after DOMContentLoaded on every
179
- # vendor; the TS harness waits the same way.
180
117
  try:
181
118
  page.wait_for_load_state("networkidle", timeout=15_000)
182
- except Exception: # noqa: BLE001 — a busy page is still solvable
119
+ except Exception:
183
120
  pass
184
121
  page.wait_for_timeout(3000)
185
122
 
@@ -196,10 +133,8 @@ def run_demo(spec: DemoSpec) -> None:
196
133
  err = e
197
134
  reason = _explain(spec.vendor, False, e)
198
135
  finally:
199
- # In the finally so a failed run still yields its recording —
200
- # that is the run you most want to watch back.
201
136
  video_path = _finish_video(page)
202
- except Exception as e: # noqa: BLE001 — demo: report, don't traceback
137
+ except Exception as e:
203
138
  err = e
204
139
  reason = _explain(spec.vendor, False, e)
205
140
 
@@ -228,10 +163,6 @@ def _report(spec, *, ok, total_s, solve_s, inp, out, tps, reason, video=None):
228
163
  print(f" reason : {reason}")
229
164
  print(f"{line}\n")
230
165
 
231
- # Optional machine-readable copy of the same report, appended as one JSON
232
- # line. The printed block is for a human reading a terminal; anything that
233
- # wants to keep the result — compare two adapters, chart tok/s over a
234
- # week — should not have to scrape it back out of that text.
235
166
  record = os.getenv("CAPTCHA_DEMO_RECORD")
236
167
  if record:
237
168
  with open(record, "a", encoding="utf-8") as fh:
@@ -1,16 +1,3 @@
1
- """
2
- Engine demo: run CaptchaKraken on the standard hCaptcha demo page.
3
-
4
- pip install -e ".[serve]" # engine + serving stack (or [.] against a remote server)
5
- pip install camoufox && python -m camoufox fetch # or set CAMOUFOX_BINARY to your fork binary
6
- source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
7
- python examples/demoHcaptcha.py # this demo page
8
- python examples/demoHcaptcha.py https://your.site/ # any URL
9
-
10
- Note: hCaptcha randomly serves non-grid puzzles (drag / video / choose-the-card),
11
- which the engine does not handle yet — the report says so; re-run for a grid.
12
- """
13
-
14
1
  from _harness import DemoSpec, run_demo
15
2
 
16
3
  if __name__ == "__main__":
@@ -1,13 +1,3 @@
1
- """
2
- Engine demo: run CaptchaKraken on Google's standard reCAPTCHA v2 demo page.
3
-
4
- pip install -e ".[serve]" # engine + serving stack (or [.] against a remote server)
5
- pip install camoufox && python -m camoufox fetch # or set CAMOUFOX_BINARY to your fork binary
6
- source ../captchakraken.env # VLLM_BASE_URL + CAPTCHA_KRAKEN_API_KEY
7
- python examples/demoRecaptcha.py # this demo page
8
- python examples/demoRecaptcha.py https://your.site/ # any URL
9
- """
10
-
11
1
  from _harness import DemoSpec, run_demo
12
2
 
13
3
  if __name__ == "__main__":
@@ -1,26 +1,3 @@
1
- """
2
- The auto-solver: install once, and captchas are solved as they appear.
3
-
4
- `solve(page)` handles whatever is on the page right now. `watch(page)` is for
5
- when you do not know WHERE in a script a challenge will interrupt you.
6
-
7
- python examples/watch_playwright.py [url] [seconds]
8
-
9
- TWO SHAPES, because this port is synchronous:
10
-
11
- solver.watch(page).run() # blocking — hold this page clean
12
- w = solver.watch(page) # cooperative — you own the cadence
13
- while working():
14
- w.poll_once()
15
-
16
- The TypeScript twin returns a background handle instead. That difference is
17
- Playwright's, not ours: a sync handle is bound to the greenlet that created it,
18
- so a worker thread cannot drive the page.
19
-
20
- It injects nothing into the page on any launcher. Under camoufox its DOM reads
21
- run in the isolated Juggler world by default.
22
- """
23
-
24
1
  import sys
25
2
 
26
3
  from playwright.sync_api import sync_playwright
@@ -36,16 +13,14 @@ def main() -> int:
36
13
  browser = p.chromium.launch(headless=True)
37
14
  page = browser.new_page()
38
15
 
39
- # ── install once, then let it work ───────────────────────────────────
40
16
  watcher = PageSolver().watch(
41
17
  page,
42
18
  on_solved=lambda r: print("✅ solved one:", r.is_solved),
43
19
  on_error=lambda e: print("solve failed:", e),
44
20
  )
45
- # ─────────────────────────────────────────────────────────────────────
46
21
 
47
22
  page.goto(URL)
48
- watcher.run(timeout_ms=SECONDS * 1000) # blocks; solves as they appear
23
+ watcher.run(timeout_ms=SECONDS * 1000)
49
24
 
50
25
  print(f"stopped after {watcher.solves} solve(s)")
51
26
  browser.close()