@gorlitzer-labs/seldon 0.1.13 → 0.1.14

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 +31 -3
  2. package/package.json +1 -1
package/bin/seldon.js CHANGED
@@ -172,8 +172,14 @@ function installPython(m, dev) {
172
172
  run(py, ["-m", "venv", venv]);
173
173
  ok = run(path.join(venv, "bin", "pip"), ["install", "-r", req]);
174
174
  }
175
+ // demerzel needs spaCy's en_core_web_sm (kokoro G2P) in the venv — install it
176
+ // with the venv (a bare fetch-models skip would leave the voice unable to start).
177
+ if (ok && m.id === "demerzel") {
178
+ console.log(C.dim(" installing spaCy model en_core_web_sm (voice text processing)…"));
179
+ run(path.join(venv, "bin", "python"), ["-m", "spacy", "download", "en_core_web_sm"]);
180
+ }
175
181
  // demerzel has no standalone binary — it runs as a module from its venv.
176
- if (ok && m.id === "demerzel") console.log(C.dim(` run it: seldon up (starts the voice on http://localhost:8770; first run fetches models)`));
182
+ if (ok && m.id === "demerzel") console.log(C.dim(` run it: seldon up (voice on http://localhost:8770; add --tailnet for your phone)`));
177
183
  else if (ok) console.log(C.dim(` run it: ${path.join(dest, m.bin)} (isolated venv at ${venv})`));
178
184
  return ok;
179
185
  }
@@ -320,6 +326,18 @@ function demerzelModelsReady() {
320
326
  }
321
327
  const demerzelInstalled = () => fs.existsSync(path.join(SELDON_HOME, "demerzel", ".venv", "bin", "python"));
322
328
 
329
+ // This machine's Tailscale IPv4 (100.64.0.0/10), or null. Tries PATH then the
330
+ // macOS app location. Used to expose the voice to your phone over the tailnet.
331
+ function tailnetIp() {
332
+ for (const bin of ["tailscale", "/Applications/Tailscale.app/Contents/MacOS/tailscale"]) {
333
+ try {
334
+ const ip = execSync(`${bin} ip -4`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim().split(/\s+/)[0];
335
+ if (/^100\./.test(ip)) return ip;
336
+ } catch {}
337
+ }
338
+ return null;
339
+ }
340
+
323
341
  // Fetch demerzel's model set (~25 GB) — offered, never silent. Respects the
324
342
  // chosen LLM (DEMERZEL_LLM) so `seldon install demerzel` / `seldon up` pull the
325
343
  // model you'll actually run. --models forces yes, --no-models forces skip.
@@ -352,8 +370,17 @@ async function up() {
352
370
  else {
353
371
  if (!demerzelModelsReady()) await ensureDemerzelModels({ ask: true }); // offer the fetch inline
354
372
  if (demerzelModelsReady()) {
355
- const r = startDaemon("demerzel", path.join(home, ".venv/bin/python"), ["-m", "demerzel.server"], { cwd: home, env: { ...process.env } });
356
- go.push(`${C.bold("Voice")} ${C.cyan("http://localhost:8770")} ${C.dim(r.already ? "(already up)" : "(starting — models load, ~30s)")}`);
373
+ // --tailnet exposes the voice on this machine's tailnet IP (reachable
374
+ // from your phone). Default stays loopback (this machine only).
375
+ const wantTailnet = process.argv.includes("--tailnet") || process.argv.includes("--phone");
376
+ let host = null;
377
+ if (wantTailnet) { host = tailnetIp(); if (!host) later.push("--tailnet: no Tailscale IP found (is Tailscale running?) — started the voice on localhost instead."); }
378
+ const env = { ...process.env };
379
+ if (host) { env.DEMERZEL_HOST = host; stopDaemon("demerzel"); } // rebind: restart if it was on loopback
380
+ const r = startDaemon("demerzel", path.join(home, ".venv/bin/python"), ["-m", "demerzel.server"], { cwd: home, env });
381
+ const url = host ? `http://${host}:8770` : "http://localhost:8770";
382
+ go.push(`${C.bold("Voice")} ${C.cyan(url)} ${C.dim(host ? "(open it on your phone — on your tailnet)" : (r.already ? "(already up)" : "(starting — models load, ~30s)"))}`);
383
+ if (host) later.push(C.dim("Voice is on your tailnet — anyone on it can talk to Demerzel (and it can act on this Mac). Run with DEMERZEL_READONLY=1 to share it safely."));
357
384
  } else {
358
385
  later.push("Voice (demerzel): models not fetched yet — run `seldon up` again when you're ready to pull them.");
359
386
  }
@@ -510,6 +537,7 @@ ${C.bold("seldon")} — install the Seldon stack, pick what you use.
510
537
 
511
538
  ${C.bold("seldon")} open the checklist (space to pick, enter to install)
512
539
  ${C.bold("seldon up")} start the stack (voice + supervisor) and print where to go
540
+ ${C.bold("seldon up --tailnet")} also expose the voice on your tailnet (reach it from your phone)
513
541
  ${C.bold("seldon status")} what's running · ${C.bold("seldon down")} stop it
514
542
  ${C.bold("seldon install")} [ids…] install everything picked, or the named modules
515
543
  ${C.bold("seldon uninstall")} ids… remove the named modules (global CLI / isolated venv)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gorlitzer-labs/seldon",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
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": {