captchakraken 2.2.0 → 2.3.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/LICENSE +105 -0
- package/README.md +3 -1
- package/dist/playwright-types.d.ts +2 -0
- package/dist/solver.d.ts +67 -0
- package/dist/solver.js +377 -15
- package/dist/types.d.ts +103 -0
- package/package.json +4 -3
- package/python/LICENSE +105 -0
- package/python/README.md +3 -1
- package/python/pyproject.toml +6 -2
- package/python/src/captchakraken/__init__.py +13 -1
- package/python/src/captchakraken/cli.py +63 -0
- package/python/src/captchakraken/config.py +86 -4
- package/python/src/captchakraken/page_solver.py +1579 -0
- package/python/src/captchakraken/pinned_model.json +28 -0
- package/python/src/captchakraken/planner.py +248 -17
- package/python/src/captchakraken/server_manager.py +3 -2
- package/python/src/captchakraken/trajectory.py +172 -0
- package/python/src/captchakraken/updater.py +168 -0
- package/scripts/setup-python.js +16 -2
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified fetch/update for self-hosters.
|
|
3
|
+
|
|
4
|
+
One command pulls the latest published CaptchaKraken model from the HuggingFace
|
|
5
|
+
org (https://huggingface.co/CaptchaKraken) AND refreshes the local vLLM serving
|
|
6
|
+
stack, so upgrading picks up new model revisions + engine fixes without
|
|
7
|
+
re-running the whole installer:
|
|
8
|
+
|
|
9
|
+
captchakraken fetch
|
|
10
|
+
|
|
11
|
+
What it does (all model-agnostic — repo ids come from `config`, so overriding
|
|
12
|
+
CAPTCHA_LORA_ADAPTER / CAPTCHA_BASE_MODEL retargets it):
|
|
13
|
+
|
|
14
|
+
1. Re-downloads the configured captcha LoRA adapter + base model from HF. A
|
|
15
|
+
no-op when you're already on the latest revision; pulls the new commit
|
|
16
|
+
otherwise. (Downloads are cached under HF_HOME, same as the installer.)
|
|
17
|
+
2. Upgrades the vLLM serving stack in the current environment
|
|
18
|
+
(``pip install -U vllm huggingface_hub``).
|
|
19
|
+
3. Restarts a locally-managed vLLM server if one is running, so the freshly
|
|
20
|
+
pulled weights + engine actually take effect. Remote endpoints are left
|
|
21
|
+
alone (you manage those).
|
|
22
|
+
|
|
23
|
+
Flags handled by the CLI wrapper:
|
|
24
|
+
--weights-only just re-pull weights (skip the pip upgrade)
|
|
25
|
+
--engine-only just upgrade vLLM (skip the weight download)
|
|
26
|
+
--no-restart don't bounce a running local server
|
|
27
|
+
--dry-run print the plan as JSON and do nothing (no network, no pip)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
import shutil
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
|
|
35
|
+
from . import config
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# The vLLM serving stack `fetch` keeps current. Mirrors the `serve` extra in
|
|
39
|
+
# pyproject; upgraded in-place so a self-hoster gets engine fixes without a full
|
|
40
|
+
# reinstall. Kept deliberately small — torch/transformers ride along with vllm.
|
|
41
|
+
ENGINE_PACKAGES = ["vllm", "huggingface_hub"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _hf_bin() -> "str | None":
|
|
45
|
+
"""Locate the HuggingFace CLI. Prefer the one next to the CURRENT interpreter
|
|
46
|
+
(the venv the CLI runs in) — the JS driver invokes us via the venv python
|
|
47
|
+
without its bin dir on PATH — then fall back to PATH. Accepts either the new
|
|
48
|
+
`hf` entrypoint or the legacy `huggingface-cli`."""
|
|
49
|
+
here = os.path.dirname(sys.executable)
|
|
50
|
+
for name in ("hf", "huggingface-cli"):
|
|
51
|
+
sibling = os.path.join(here, name)
|
|
52
|
+
if os.path.exists(sibling) and os.access(sibling, os.X_OK):
|
|
53
|
+
return sibling
|
|
54
|
+
return shutil.which("hf") or shutil.which("huggingface-cli")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _download_cmd(repo_id: str) -> "list[str]":
|
|
58
|
+
"""The argv that pulls (or refreshes) one HF repo into the local cache. Uses
|
|
59
|
+
the `hf`/`huggingface-cli` binary when present; otherwise drives
|
|
60
|
+
`huggingface_hub.snapshot_download` through the current interpreter so a
|
|
61
|
+
fetch still works from a bare `pip install captchakraken[serve]` (no console
|
|
62
|
+
script on PATH)."""
|
|
63
|
+
hf = _hf_bin()
|
|
64
|
+
if hf:
|
|
65
|
+
return [hf, "download", repo_id]
|
|
66
|
+
return [
|
|
67
|
+
sys.executable, "-c",
|
|
68
|
+
"import sys; from huggingface_hub import snapshot_download; "
|
|
69
|
+
"snapshot_download(sys.argv[1])",
|
|
70
|
+
repo_id,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _pip_upgrade_cmd() -> "list[str]":
|
|
75
|
+
return [sys.executable, "-m", "pip", "install", "--upgrade", *ENGINE_PACKAGES]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def plan(
|
|
79
|
+
*,
|
|
80
|
+
weights: bool = True,
|
|
81
|
+
engine: bool = True,
|
|
82
|
+
restart: bool = True,
|
|
83
|
+
base: "str | None" = None,
|
|
84
|
+
lora: "str | None" = None,
|
|
85
|
+
) -> dict:
|
|
86
|
+
"""Assemble the fetch plan WITHOUT running anything. Pure + side-effect-free
|
|
87
|
+
so `--dry-run` and the tests can assert exactly what a real run would do."""
|
|
88
|
+
base_model = base or config.base_model()
|
|
89
|
+
lora_adapter = lora or config.lora_adapter()
|
|
90
|
+
base_url = config.base_url()
|
|
91
|
+
|
|
92
|
+
repos = [lora_adapter, base_model] if weights else []
|
|
93
|
+
return {
|
|
94
|
+
"weights": weights,
|
|
95
|
+
"engine": engine,
|
|
96
|
+
"restart": restart,
|
|
97
|
+
"base_model": base_model,
|
|
98
|
+
"lora_adapter": lora_adapter,
|
|
99
|
+
"hf_org": "https://huggingface.co/CaptchaKraken",
|
|
100
|
+
"downloads": [_download_cmd(r) for r in repos],
|
|
101
|
+
"engine_upgrade": _pip_upgrade_cmd() if engine else None,
|
|
102
|
+
"server": {"base_url": base_url, "local": _is_local(base_url)},
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _is_local(base_url: str) -> bool:
|
|
107
|
+
# Imported lazily so a plan/dry-run doesn't drag in requests via server_manager.
|
|
108
|
+
from .server_manager import is_local
|
|
109
|
+
|
|
110
|
+
return is_local(base_url)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _log(msg: str) -> None:
|
|
114
|
+
print(f"[fetch] {msg}", file=sys.stderr)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _run(cmd: "list[str]") -> None:
|
|
118
|
+
_log("$ " + " ".join(cmd))
|
|
119
|
+
subprocess.run(cmd, check=True)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def fetch(
|
|
123
|
+
*,
|
|
124
|
+
weights: bool = True,
|
|
125
|
+
engine: bool = True,
|
|
126
|
+
restart: bool = True,
|
|
127
|
+
dry_run: bool = False,
|
|
128
|
+
base: "str | None" = None,
|
|
129
|
+
lora: "str | None" = None,
|
|
130
|
+
) -> dict:
|
|
131
|
+
"""Pull the latest weights + engine and (optionally) restart a local server.
|
|
132
|
+
|
|
133
|
+
Returns a JSON-serializable summary of what was done. On --dry-run, returns
|
|
134
|
+
the plan with ``"dry_run": true`` and performs no I/O.
|
|
135
|
+
"""
|
|
136
|
+
p = plan(weights=weights, engine=engine, restart=restart, base=base, lora=lora)
|
|
137
|
+
if dry_run:
|
|
138
|
+
return {**p, "dry_run": True}
|
|
139
|
+
|
|
140
|
+
if weights:
|
|
141
|
+
_log(f"Pulling latest weights from {p['hf_org']} (+ base) …")
|
|
142
|
+
for cmd in p["downloads"]:
|
|
143
|
+
_run(cmd)
|
|
144
|
+
|
|
145
|
+
if engine:
|
|
146
|
+
_log("Upgrading the vLLM serving stack …")
|
|
147
|
+
_run(p["engine_upgrade"])
|
|
148
|
+
|
|
149
|
+
server_result = None
|
|
150
|
+
if restart and p["server"]["local"]:
|
|
151
|
+
from . import server_manager
|
|
152
|
+
|
|
153
|
+
if server_manager._read_pid() is not None:
|
|
154
|
+
_log("Restarting the local vLLM server so new weights/engine load …")
|
|
155
|
+
server_manager.stop()
|
|
156
|
+
server_result = server_manager.start(background=True)
|
|
157
|
+
else:
|
|
158
|
+
_log("No local server running — it will pick up the update on next start.")
|
|
159
|
+
elif restart:
|
|
160
|
+
_log("Endpoint is remote — leaving the server you manage untouched.")
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
"fetched_weights": bool(weights),
|
|
164
|
+
"upgraded_engine": bool(engine),
|
|
165
|
+
"server": server_result,
|
|
166
|
+
"base_model": p["base_model"],
|
|
167
|
+
"lora_adapter": p["lora_adapter"],
|
|
168
|
+
}
|
package/scripts/setup-python.js
CHANGED
|
@@ -80,8 +80,22 @@ function main() {
|
|
|
80
80
|
// OpenCV grid detection + the HTTP planner that talks to a vLLM server. The
|
|
81
81
|
// heavy serving stack (vllm/torch, the `[serve]` extra) is NOT installed here
|
|
82
82
|
// — that's what setup.sh installs for people self-hosting the model.
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
//
|
|
84
|
+
// Prefer the PUBLISHED package from PyPI, pinned to THIS npm package's version
|
|
85
|
+
// so the JS driver and the Python engine stay in lockstep. Only fall back to
|
|
86
|
+
// the bundled source tree when PyPI can't satisfy that pin — offline installs,
|
|
87
|
+
// or a dev/CI/from-git build whose version isn't on PyPI yet.
|
|
88
|
+
const pkgVersion = require(path.join(pkgRoot, 'package.json')).version;
|
|
89
|
+
try {
|
|
90
|
+
console.log(`[CaptchaKraken] Installing captchakraken==${pkgVersion} from PyPI...`);
|
|
91
|
+
run(py, ['-m', 'pip', 'install', `captchakraken==${pkgVersion}`]);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.warn(
|
|
94
|
+
`[CaptchaKraken] PyPI install failed (${err && err.message ? err.message : err}); ` +
|
|
95
|
+
'falling back to the bundled source tree.',
|
|
96
|
+
);
|
|
97
|
+
run(py, ['-m', 'pip', 'install', '.'], { cwd: cliRoot });
|
|
98
|
+
}
|
|
85
99
|
|
|
86
100
|
console.log('[CaptchaKraken] Python environment ready.');
|
|
87
101
|
}
|