@exulu/backend 1.60.0 → 1.61.1

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,204 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getPackageRoot
4
+ } from "../chunk-IDHS2BZO.js";
5
+
6
+ // src/cli/start-whisper.ts
7
+ import "dotenv/config";
8
+
9
+ // src/exulu/transcription/supervisor.ts
10
+ import { spawn } from "child_process";
11
+ import { existsSync } from "fs";
12
+ import { resolve } from "path";
13
+ var MAX_CRASHES = 5;
14
+ var INITIAL_BACKOFF_MS = 1e3;
15
+ var MAX_BACKOFF_MS = 3e4;
16
+ var READY_TIMEOUT_MS = 30 * 6e4;
17
+ var READY_POLL_INTERVAL_MS = 500;
18
+ var SHUTDOWN_GRACE_MS = 5e3;
19
+ var internal = {
20
+ child: void 0,
21
+ state: "idle",
22
+ crashCount: 0,
23
+ backoffMs: INITIAL_BACKOFF_MS,
24
+ readyPromise: void 0,
25
+ shutdownRequested: false
26
+ };
27
+ var resolveConfig = (packageRoot) => {
28
+ const host = process.env.WHISPER_HOST ?? "127.0.0.1";
29
+ const port = process.env.WHISPER_PORT ?? "9876";
30
+ const venvBin = resolve(packageRoot, "ee/python/.venv/bin");
31
+ const venvPython = resolve(venvBin, "python");
32
+ const cwd = resolve(packageRoot, "ee/python/transcription");
33
+ return { host, port, venvBin, venvPython, cwd };
34
+ };
35
+ var log = (line) => {
36
+ const text = line.startsWith("[EXULU-WHISPER]") ? line : `[EXULU-WHISPER] ${line}`;
37
+ console.log(text);
38
+ };
39
+ var pollHealth = async (host, port) => {
40
+ const url = `http://${host}:${port}/healthz`;
41
+ const deadline = Date.now() + READY_TIMEOUT_MS;
42
+ while (Date.now() < deadline) {
43
+ try {
44
+ const res = await fetch(url, { method: "GET" });
45
+ if (res.ok) return;
46
+ } catch {
47
+ }
48
+ await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
49
+ }
50
+ throw new Error(
51
+ `Whisper server did not become ready at ${url} within ${READY_TIMEOUT_MS}ms`
52
+ );
53
+ };
54
+ var spawnWhisper = (cfg) => {
55
+ log(
56
+ `Spawning: ${cfg.venvPython} -m uvicorn server:app --host ${cfg.host} --port ${cfg.port}`
57
+ );
58
+ const child = spawn(
59
+ cfg.venvPython,
60
+ [
61
+ "-m",
62
+ "uvicorn",
63
+ "server:app",
64
+ "--host",
65
+ cfg.host,
66
+ "--port",
67
+ cfg.port,
68
+ "--log-level",
69
+ "info"
70
+ ],
71
+ {
72
+ cwd: cfg.cwd,
73
+ stdio: ["ignore", "pipe", "pipe"],
74
+ env: process.env
75
+ }
76
+ );
77
+ child.stdout?.on("data", (chunk) => {
78
+ chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(l));
79
+ });
80
+ child.stderr?.on("data", (chunk) => {
81
+ chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(`stderr: ${l}`));
82
+ });
83
+ return child;
84
+ };
85
+ var supervise = async (cfg) => {
86
+ while (!internal.shutdownRequested && internal.crashCount < MAX_CRASHES) {
87
+ internal.state = internal.crashCount === 0 ? "starting" : "respawning";
88
+ internal.child = spawnWhisper(cfg);
89
+ const exitPromise = new Promise((resolveFn) => {
90
+ internal.child.on("exit", (code2) => resolveFn(code2));
91
+ });
92
+ try {
93
+ await Promise.race([
94
+ pollHealth(cfg.host, cfg.port).then(() => "ready"),
95
+ exitPromise.then((code2) => ({ exited: code2 }))
96
+ ]);
97
+ } catch (err) {
98
+ log(`Readiness probe failed: ${err.message}`);
99
+ try {
100
+ internal.child?.kill("SIGTERM");
101
+ } catch {
102
+ }
103
+ }
104
+ if (!internal.child?.killed && internal.child?.exitCode === null) {
105
+ internal.state = "ready";
106
+ internal.crashCount = 0;
107
+ internal.backoffMs = INITIAL_BACKOFF_MS;
108
+ log("Whisper server is ready.");
109
+ }
110
+ const code = await exitPromise;
111
+ internal.state = "respawning";
112
+ internal.child = void 0;
113
+ if (internal.shutdownRequested) {
114
+ log("Child exited during shutdown; supervisor stopping.");
115
+ internal.state = "stopped";
116
+ return;
117
+ }
118
+ internal.crashCount += 1;
119
+ log(
120
+ `Whisper server exited (code=${code}). Crash ${internal.crashCount}/${MAX_CRASHES}. Respawning in ${internal.backoffMs}ms.`
121
+ );
122
+ if (internal.crashCount >= MAX_CRASHES) {
123
+ log(
124
+ "Whisper server keeps crashing \u2014 fix the install (try `npx @exulu/backend setup-python --force`) and re-run `npx @exulu/backend exulu-start-whisper`. Giving up."
125
+ );
126
+ internal.state = "given_up";
127
+ return;
128
+ }
129
+ await new Promise((r) => setTimeout(r, internal.backoffMs));
130
+ internal.backoffMs = Math.min(internal.backoffMs * 2, MAX_BACKOFF_MS);
131
+ }
132
+ };
133
+ var startWhisperSupervisor = async (options) => {
134
+ if (internal.readyPromise) {
135
+ return internal.readyPromise;
136
+ }
137
+ const cfg = resolveConfig(options.packageRoot);
138
+ if (!existsSync(cfg.venvPython)) {
139
+ throw new Error(
140
+ `Whisper supervisor: Python venv not found at ${cfg.venvPython}. Run \`npx @exulu/backend setup-python\` first.`
141
+ );
142
+ }
143
+ if (!existsSync(cfg.cwd)) {
144
+ throw new Error(
145
+ `Whisper supervisor: transcription scripts not found at ${cfg.cwd}. The @exulu/backend package may be corrupt; reinstall it.`
146
+ );
147
+ }
148
+ internal.readyPromise = (async () => {
149
+ supervise(cfg);
150
+ const deadline = Date.now() + READY_TIMEOUT_MS + 5e3;
151
+ while (Date.now() < deadline) {
152
+ if (internal.state === "ready") return;
153
+ if (internal.state === "given_up") {
154
+ throw new Error("Whisper supervisor gave up before becoming ready.");
155
+ }
156
+ await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
157
+ }
158
+ throw new Error("Timed out waiting for whisper supervisor readiness.");
159
+ })();
160
+ registerShutdownHandlers();
161
+ return internal.readyPromise;
162
+ };
163
+ var stopWhisper = (signal = "SIGTERM") => {
164
+ internal.shutdownRequested = true;
165
+ const child = internal.child;
166
+ if (!child) return;
167
+ try {
168
+ child.kill(signal);
169
+ } catch {
170
+ }
171
+ setTimeout(() => {
172
+ try {
173
+ if (!child.killed && child.exitCode === null) {
174
+ child.kill("SIGKILL");
175
+ }
176
+ } catch {
177
+ }
178
+ }, SHUTDOWN_GRACE_MS).unref();
179
+ };
180
+ var shutdownHandlersRegistered = false;
181
+ var registerShutdownHandlers = () => {
182
+ if (shutdownHandlersRegistered) return;
183
+ shutdownHandlersRegistered = true;
184
+ process.on("SIGINT", () => stopWhisper("SIGTERM"));
185
+ process.on("SIGTERM", () => stopWhisper("SIGTERM"));
186
+ process.on("exit", () => stopWhisper("SIGTERM"));
187
+ };
188
+
189
+ // src/cli/start-whisper.ts
190
+ var main = async () => {
191
+ const packageRoot = getPackageRoot();
192
+ try {
193
+ await startWhisperSupervisor({ packageRoot });
194
+ } catch (err) {
195
+ console.error(`[EXULU-WHISPER] ${err.message}`);
196
+ process.exit(1);
197
+ }
198
+ await new Promise(() => {
199
+ });
200
+ };
201
+ main().catch((err) => {
202
+ console.error(`[EXULU-WHISPER] Unexpected error: ${err.stack ?? err}`);
203
+ process.exit(1);
204
+ });
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  convertExuluToolsToAiSdkTools
3
- } from "./chunk-23YNGK3V.js";
3
+ } from "./chunk-MPV7HBV6.js";
4
4
  export {
5
5
  convertExuluToolsToAiSdkTools
6
6
  };