captchakraken 2.2.0 → 2.4.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.
@@ -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
+ }
@@ -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
- console.log('[CaptchaKraken] Installing bundled python package (core deps)...');
84
- run(py, ['-m', 'pip', 'install', '.'], { cwd: cliRoot });
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
  }