@gorlitzer-labs/seldon 0.1.20 → 0.1.22

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 (2) hide show
  1. package/bin/seldon.js +57 -13
  2. package/package.json +1 -1
package/bin/seldon.js CHANGED
@@ -158,7 +158,11 @@ function installPython(m, dev) {
158
158
  const venvArgs = ["venv", venv, "--seed"]; // --seed puts pip in the venv:
159
159
  // kokoro/misaki/spaCy shells out to pip at runtime to load its model, and a
160
160
  // bare uv venv has none (crashes the voice on start otherwise).
161
- if (m.pyVersion) venvArgs.push("--python", m.pyVersion); // uv fetches a standalone CPython
161
+ // uv fetches a standalone CPython — but only if told to. By default it PREFERS
162
+ // a matching system interpreter, and a python.org framework build ships with
163
+ // no CA bundle, so the venv's urllib failed every HTTPS download (the CAM++
164
+ // voiceprint model silently never arrived). only-managed makes the comment true.
165
+ if (m.pyVersion) venvArgs.push("--python", m.pyVersion, "--python-preference", "only-managed");
162
166
  if (!run(uv, venvArgs)) return false;
163
167
  ok = run(uv, ["pip", "install", "--python", path.join(venv, "bin", "python"), "-r", req]);
164
168
  } else {
@@ -324,6 +328,7 @@ const RUN_DIR = path.join(SELDON_HOME, "run");
324
328
  const pidFile = (name) => path.join(RUN_DIR, name + ".pid");
325
329
  const readPid = (name) => { try { return parseInt(fs.readFileSync(pidFile(name), "utf8").trim(), 10) || 0; } catch { return 0; } };
326
330
  const isAlive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } };
331
+ const portListening = (port) => { try { return !!execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim(); } catch { return false; } };
327
332
  const daemonUp = (name) => { const p = readPid(name); return p && isAlive(p) ? p : 0; };
328
333
 
329
334
  function startDaemon(name, cmd, args, opts = {}) {
@@ -344,11 +349,24 @@ function stopDaemon(name) {
344
349
  try { fs.rmSync(pidFile(name), { force: true }); } catch {}
345
350
  return true;
346
351
  }
347
- // demerzel needs its model set in the HF cache before it can serve.
348
- function demerzelModelsReady() {
352
+ // demerzel needs its model set before it can serve. Check each model it loads by
353
+ // name — "any models-- dir in the HF cache" was true after a fetch died halfway,
354
+ // so the one it died on (the CAM++ voiceprint, the last download) was never
355
+ // offered again. The LLM is not listed: which one depends on the brain, and
356
+ // bonsai brings its own. Returns the missing labels; [] means ready.
357
+ function demerzelModelsMissing() {
349
358
  const hub = path.join(process.env.HOME, ".cache", "huggingface", "hub");
350
- try { return fs.existsSync(hub) && fs.readdirSync(hub).some((d) => d.startsWith("models--")); } catch { return false; }
359
+ const snapshotOk = (repo) => {
360
+ const snaps = path.join(hub, "models--" + repo.replace("/", "--"), "snapshots");
361
+ try { return fs.readdirSync(snaps).some((d) => fs.readdirSync(path.join(snaps, d)).length > 0); } catch { return false; }
362
+ };
363
+ const missing = [];
364
+ if (!snapshotOk("mlx-community/Qwen3-ASR-1.7B-8bit")) missing.push("ears (Qwen3-ASR)");
365
+ if (!snapshotOk("hexgrad/Kokoro-82M")) missing.push("voice (Kokoro)");
366
+ if (!fs.existsSync(path.join(SELDON_HOME, "demerzel", "models", "campplus_en.onnx"))) missing.push("voiceprint (CAM++)");
367
+ return missing;
351
368
  }
369
+ const demerzelModelsReady = () => demerzelModelsMissing().length === 0;
352
370
  const demerzelInstalled = () => fs.existsSync(path.join(SELDON_HOME, "demerzel", ".venv", "bin", "python"));
353
371
 
354
372
  // This machine's Tailscale IPv4 (100.64.0.0/10), or null. Tries PATH then the
@@ -667,32 +685,58 @@ function moduleInstalled(m) {
667
685
 
668
686
  function statusCmd() {
669
687
  console.log(C.bold("\n seldon — stack status\n"));
688
+ // The dot answers "is it working?". For a CLI tool that is "installed"; for a
689
+ // SERVICE it is "running" — a green dot next to "voice down" read as up, and
690
+ // `seldon down` saying "nothing was running" then looked like a lie.
691
+ const up = [], starting = [], stopped = [];
692
+ const svcState = (label, pid, upText) => {
693
+ (pid ? up : stopped).push(label);
694
+ return pid ? " " + C.green(upText) : " " + C.gold(label + " down");
695
+ };
696
+ const brain = demerzelInstalled() ? (savedBrain() || "qwen") : null;
670
697
  for (const m of MODULES) {
671
698
  const inst = moduleInstalled(m);
672
- const dot = inst ? C.green("●") : C.dim("○");
673
699
  let detail;
674
700
  if (!inst) detail = platformOk(m) ? C.dim(`not installed ${C.dim("· seldon install " + m.id)}`) : C.dim("not supported on this machine");
675
701
  else if (m.method === "npm") detail = C.dim("v" + (npmGlobalVersion(m.pkg) || "?"));
676
702
  else if (m.id === "bifrost") detail = C.dim("v" + (bifrostVersion() || "?"));
677
- else if (m.id === "demerzel") detail = demerzelModelsReady() ? C.dim("venv + models ✓") : C.gold("venv ok · models not fetched (seldon up)");
703
+ else if (m.id === "demerzel") detail = demerzelModelsReady() ? C.dim("venv + models ✓") : C.gold(`venv ok · missing ${demerzelModelsMissing().join(", ")} (seldon up)`);
678
704
  else detail = C.dim("installed");
679
705
 
680
706
  // service state for the two daemons
681
- let svc = "";
682
- if (m.id === "factory") { const p = daemonUp("factory-watch"); svc = p ? " " + C.green("supervisor up") + C.dim(` (pid ${p})`) : " " + C.dim("supervisor down"); }
683
- if (m.id === "demerzel" && inst) { const p = daemonUp("demerzel"); svc = p ? " " + C.green("voice up") + " " + C.cyan("http://localhost:8770") : " " + C.dim("voice down"); }
684
-
707
+ let svc = "", running = true;
708
+ if (m.id === "factory" && inst) {
709
+ const p = daemonUp("factory-watch"); running = !!p;
710
+ svc = svcState("supervisor", p, "supervisor up") + (p ? C.dim(` (pid ${p})`) : "");
711
+ }
712
+ if (m.id === "demerzel" && inst) {
713
+ const p = daemonUp("demerzel"), b = brain !== "bonsai" || daemonUp("bonsai") || bonsaiHealthy();
714
+ // A live pid is not a working voice: models load for ~30-90s before :8770
715
+ // listens, and "voice up" there sent you to a page that would not open.
716
+ const serving = p && portListening(8770);
717
+ running = !!serving && !!b; // the voice without its brain server cannot answer
718
+ if (p && !serving) { starting.push("voice"); svc = " " + C.gold("voice starting") + C.dim(" — models loading, give it a minute"); }
719
+ else svc = svcState("voice", p, "voice up") + (p ? " " + C.cyan("http://localhost:8770") : "");
720
+ }
721
+ const dot = !inst ? C.dim("○") : running ? C.green("●") : C.gold("●");
685
722
  console.log(` ${dot} ${C.bold(m.id.padEnd(11))} ${detail}${svc}`);
686
723
  }
687
- if (demerzelInstalled()) {
688
- const brain = savedBrain() || "qwen";
724
+ if (brain) {
689
725
  const bstate = brain === "bonsai"
690
- ? (daemonUp("bonsai") || bonsaiHealthy() ? C.green("server up") : C.dim("server down"))
726
+ ? ((daemonUp("bonsai") || bonsaiHealthy()) ? (up.push("brain"), C.green("server up")) : (stopped.push("brain"), C.gold("server down")))
691
727
  : C.dim("in-process");
692
728
  console.log(C.dim(`\n voice brain: `) + C.bold(brain) + " " + bstate + C.dim(" · switch: seldon up --brain=qwen|bonsai"));
693
729
  const phone = tailscaleServeUrl();
694
730
  if (phone) console.log(C.dim(" phone (HTTPS): ") + C.cyan(phone));
695
731
  }
732
+ if (up.length || starting.length || stopped.length) {
733
+ const parts = [];
734
+ if (up.length) parts.push(C.green("● running: " + up.join(", ")));
735
+ if (starting.length) parts.push(C.gold("● starting: " + starting.join(", ")));
736
+ if (stopped.length) parts.push(C.gold("● stopped: " + stopped.join(", ")) + C.dim(" — start: ") + C.cyan("seldon up"));
737
+ if (!up.length && !starting.length) parts[0] = C.gold("● nothing is running") + C.dim(" — start: ") + C.cyan("seldon up");
738
+ console.log("\n " + parts.join(" "));
739
+ }
696
740
  const dk = dockerState();
697
741
  if (dk === "down") console.log(C.gold("\n ⚠ docker: installed but the daemon is not running") + C.dim(" — factory box and containerised gates need it (open Docker Desktop)"));
698
742
  else if (dk === "up") console.log(C.dim("\n docker: ") + C.green("up"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gorlitzer-labs/seldon",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Install the Seldon stack — pick your tools from a checklist; each installs via its native method.",
5
5
  "type": "module",
6
6
  "bin": {