@exulu/backend 1.59.0 → 1.61.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/dist/{catalog-EOKGOHTY.js → catalog-BWE6SLE2.js} +1 -1
- package/dist/chunk-IDHS2BZO.js +210 -0
- package/dist/{chunk-YS27XOXI.js → chunk-ILAHW4UT.js} +5 -1
- package/dist/{chunk-U36VJDZ7.js → chunk-MPV7HBV6.js} +66 -4
- package/dist/cli/start-whisper.cjs +240 -0
- package/dist/cli/start-whisper.d.cts +1 -0
- package/dist/cli/start-whisper.d.ts +1 -0
- package/dist/cli/start-whisper.js +204 -0
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-ZEECMX43.js → convert-exulu-tools-to-ai-sdk-tools-CULC37U6.js} +1 -1
- package/dist/index.cjs +2110 -412
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1647 -237
- package/ee/python/requirements.txt +18 -0
- package/ee/python/setup.sh +44 -0
- package/ee/python/transcription/__init__.py +0 -0
- package/ee/python/transcription/pipeline.py +232 -0
- package/ee/python/transcription/server.py +151 -0
- package/ee/python/transcription/tests/__init__.py +0 -0
- package/ee/python/transcription/tests/test_server.py +111 -0
- package/ee/python/transcription/worker.py +135 -0
- package/package.json +4 -2
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// src/utils/python-setup.ts
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import { resolve, join, dirname } from "path";
|
|
5
|
+
import { existsSync, readFileSync } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
var execAsync = promisify(exec);
|
|
8
|
+
function getPackageRoot() {
|
|
9
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
10
|
+
let currentDir = dirname(currentFile);
|
|
11
|
+
let attempts = 0;
|
|
12
|
+
const maxAttempts = 10;
|
|
13
|
+
while (attempts < maxAttempts) {
|
|
14
|
+
const packageJsonPath = join(currentDir, "package.json");
|
|
15
|
+
if (existsSync(packageJsonPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
18
|
+
if (packageJson.name === "@exulu/backend") {
|
|
19
|
+
return currentDir;
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const parentDir = resolve(currentDir, "..");
|
|
25
|
+
if (parentDir === currentDir) {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
currentDir = parentDir;
|
|
29
|
+
attempts++;
|
|
30
|
+
}
|
|
31
|
+
const fallback = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
function getSetupScriptPath(packageRoot) {
|
|
35
|
+
return resolve(packageRoot, "ee/python/setup.sh");
|
|
36
|
+
}
|
|
37
|
+
function getVenvPath(packageRoot) {
|
|
38
|
+
return resolve(packageRoot, "ee/python/.venv");
|
|
39
|
+
}
|
|
40
|
+
function isPythonEnvironmentSetup(packageRoot) {
|
|
41
|
+
const root = packageRoot ?? getPackageRoot();
|
|
42
|
+
const venvPath = getVenvPath(root);
|
|
43
|
+
const pythonPath = join(venvPath, "bin", "python");
|
|
44
|
+
return existsSync(venvPath) && existsSync(pythonPath);
|
|
45
|
+
}
|
|
46
|
+
async function setupPythonEnvironment(options = {}) {
|
|
47
|
+
const {
|
|
48
|
+
packageRoot = getPackageRoot(),
|
|
49
|
+
force = false,
|
|
50
|
+
verbose = false,
|
|
51
|
+
timeout = 6e5
|
|
52
|
+
// 10 minutes
|
|
53
|
+
} = options;
|
|
54
|
+
if (!force && isPythonEnvironmentSetup(packageRoot)) {
|
|
55
|
+
if (verbose) {
|
|
56
|
+
console.log("\u2713 Python environment already set up");
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
message: "Python environment already exists",
|
|
61
|
+
alreadyExists: true
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const setupScriptPath = getSetupScriptPath(packageRoot);
|
|
65
|
+
if (!existsSync(setupScriptPath)) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
message: `Setup script not found at: ${setupScriptPath}`,
|
|
69
|
+
alreadyExists: false
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
if (verbose) {
|
|
74
|
+
console.log("Setting up Python environment...");
|
|
75
|
+
}
|
|
76
|
+
const { stdout, stderr } = await execAsync(`bash "${setupScriptPath}"`, {
|
|
77
|
+
cwd: packageRoot,
|
|
78
|
+
timeout,
|
|
79
|
+
env: {
|
|
80
|
+
...process.env,
|
|
81
|
+
// Ensure script can write to the directory
|
|
82
|
+
PYTHONDONTWRITEBYTECODE: "1"
|
|
83
|
+
},
|
|
84
|
+
maxBuffer: 10 * 1024 * 1024
|
|
85
|
+
// 10MB buffer
|
|
86
|
+
});
|
|
87
|
+
const output = stdout + stderr;
|
|
88
|
+
const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
|
|
89
|
+
const pythonVersion = versionMatch ? versionMatch[1] : void 0;
|
|
90
|
+
if (verbose) {
|
|
91
|
+
console.log(output);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
message: "Python environment set up successfully",
|
|
96
|
+
alreadyExists: false,
|
|
97
|
+
pythonVersion,
|
|
98
|
+
output
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const errorOutput = error.stdout + error.stderr;
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
message: `Setup failed: ${error.message}`,
|
|
105
|
+
alreadyExists: false,
|
|
106
|
+
output: errorOutput
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function getPythonSetupInstructions() {
|
|
111
|
+
return `
|
|
112
|
+
Python environment not set up. Please run one of the following commands:
|
|
113
|
+
|
|
114
|
+
Option 1 (Automatic):
|
|
115
|
+
import { setupPythonEnvironment } from '@exulu/backend';
|
|
116
|
+
await setupPythonEnvironment();
|
|
117
|
+
|
|
118
|
+
Option 2 (Manual - for package consumers):
|
|
119
|
+
npx @exulu/backend setup-python
|
|
120
|
+
|
|
121
|
+
Option 3 (Manual - for contributors):
|
|
122
|
+
npm run python:setup
|
|
123
|
+
|
|
124
|
+
These commands will automatically create a Python virtual environment (.venv)
|
|
125
|
+
in the @exulu/backend package and install all required dependencies.
|
|
126
|
+
|
|
127
|
+
Requirements:
|
|
128
|
+
- Python 3.10 or higher must be installed
|
|
129
|
+
- pip must be available
|
|
130
|
+
- venv module must be available (for creating virtual environments)
|
|
131
|
+
|
|
132
|
+
If Python dependencies are not installed, install them first, then run one of the commands above:
|
|
133
|
+
- macOS: brew install python@3.12
|
|
134
|
+
- Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
|
|
135
|
+
- Alpine Linux: apk add python3 py3-pip python3-dev
|
|
136
|
+
- Windows: Download from https://www.python.org/downloads/
|
|
137
|
+
|
|
138
|
+
Note: In Docker containers, ensure you install all three components:
|
|
139
|
+
Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
|
|
140
|
+
Alpine: apk add python3 py3-pip python3-dev
|
|
141
|
+
`.trim();
|
|
142
|
+
}
|
|
143
|
+
async function validatePythonEnvironment(packageRoot, checkPackages = true) {
|
|
144
|
+
const root = packageRoot ?? getPackageRoot();
|
|
145
|
+
const venvPath = getVenvPath(root);
|
|
146
|
+
const pythonPath = join(venvPath, "bin", "python");
|
|
147
|
+
if (!existsSync(venvPath)) {
|
|
148
|
+
return {
|
|
149
|
+
valid: false,
|
|
150
|
+
message: getPythonSetupInstructions()
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (!existsSync(pythonPath)) {
|
|
154
|
+
return {
|
|
155
|
+
valid: false,
|
|
156
|
+
message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
await execAsync(`"${pythonPath}" --version`, { cwd: root });
|
|
161
|
+
} catch {
|
|
162
|
+
return {
|
|
163
|
+
valid: false,
|
|
164
|
+
message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (checkPackages) {
|
|
168
|
+
const criticalPackages = ["docling", "transformers"];
|
|
169
|
+
const missingPackages = [];
|
|
170
|
+
for (const pkg of criticalPackages) {
|
|
171
|
+
try {
|
|
172
|
+
await execAsync(`"${pythonPath}" -c "import ${pkg}"`, {
|
|
173
|
+
cwd: root,
|
|
174
|
+
timeout: 1e4
|
|
175
|
+
// 10 second timeout per import check
|
|
176
|
+
});
|
|
177
|
+
} catch {
|
|
178
|
+
missingPackages.push(pkg);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (missingPackages.length > 0) {
|
|
182
|
+
return {
|
|
183
|
+
valid: false,
|
|
184
|
+
message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
|
|
185
|
+
|
|
186
|
+
This usually happens when:
|
|
187
|
+
1. The .venv folder was copied but dependencies were not installed
|
|
188
|
+
2. The package was installed via npm but setup script was not run
|
|
189
|
+
|
|
190
|
+
Please run:
|
|
191
|
+
await setupPythonEnvironment({ force: true })
|
|
192
|
+
|
|
193
|
+
Or manually run the setup script:
|
|
194
|
+
bash ` + getSetupScriptPath(root)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
valid: true,
|
|
200
|
+
message: "Python environment is valid"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export {
|
|
205
|
+
getPackageRoot,
|
|
206
|
+
isPythonEnvironmentSetup,
|
|
207
|
+
setupPythonEnvironment,
|
|
208
|
+
getPythonSetupInstructions,
|
|
209
|
+
validatePythonEnvironment
|
|
210
|
+
};
|
|
@@ -40,7 +40,11 @@ var fetchLiteLLMCatalog = async () => {
|
|
|
40
40
|
supports_vision: !!m.model_info?.supports_vision,
|
|
41
41
|
supports_function_calling: !!m.model_info?.supports_function_calling,
|
|
42
42
|
supports_pdf_input: !!m.model_info?.supports_pdf_input,
|
|
43
|
-
supports_audio_input: !!m.model_info?.supports_audio_input
|
|
43
|
+
supports_audio_input: !!m.model_info?.supports_audio_input,
|
|
44
|
+
sizes: Array.isArray(m.model_info?.sizes) ? m.model_info.sizes : null,
|
|
45
|
+
qualities: Array.isArray(m.model_info?.qualities) ? m.model_info.qualities : null,
|
|
46
|
+
supports_edit: !!m.model_info?.supports_edit,
|
|
47
|
+
max_n: typeof m.model_info?.max_n === "number" ? m.model_info.max_n : null
|
|
44
48
|
}));
|
|
45
49
|
_cache = { expiresAt: Date.now() + CACHE_TTL_MS, items };
|
|
46
50
|
return items.filter((m) => m.type !== "speech_to_text" && m.type !== "text_to_speech");
|
|
@@ -262,7 +262,8 @@ var spawnLiteLLM = (cfg) => {
|
|
|
262
262
|
log(
|
|
263
263
|
`Spawning LiteLLM: ${cfg.litellmBin} --config ${cfg.configPath} --port ${cfg.port} --host ${cfg.host}`
|
|
264
264
|
);
|
|
265
|
-
const { DEBUG: _debug, ...
|
|
265
|
+
const { DEBUG: _debug, ...rest } = process.env;
|
|
266
|
+
const childEnv = { ...rest, DEBUG: "false" };
|
|
266
267
|
const child = spawn(
|
|
267
268
|
cfg.litellmBin,
|
|
268
269
|
[
|
|
@@ -275,7 +276,7 @@ var spawnLiteLLM = (cfg) => {
|
|
|
275
276
|
],
|
|
276
277
|
{
|
|
277
278
|
stdio: ["ignore", "pipe", "pipe"],
|
|
278
|
-
env:
|
|
279
|
+
env: childEnv
|
|
279
280
|
}
|
|
280
281
|
);
|
|
281
282
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -743,7 +744,7 @@ var ExuluTool = class {
|
|
|
743
744
|
});
|
|
744
745
|
providerapikey = resolved.apiKey;
|
|
745
746
|
}
|
|
746
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-
|
|
747
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-CULC37U6.js");
|
|
747
748
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
748
749
|
[this],
|
|
749
750
|
[],
|
|
@@ -3167,6 +3168,10 @@ var usersSchema = {
|
|
|
3167
3168
|
name: "anthropic_token",
|
|
3168
3169
|
type: "text"
|
|
3169
3170
|
},
|
|
3171
|
+
{
|
|
3172
|
+
name: "personal_system_prompt",
|
|
3173
|
+
type: "longText"
|
|
3174
|
+
},
|
|
3170
3175
|
{
|
|
3171
3176
|
name: "role",
|
|
3172
3177
|
type: "uuid"
|
|
@@ -3175,6 +3180,7 @@ var usersSchema = {
|
|
|
3175
3180
|
};
|
|
3176
3181
|
var platformConfigurationsSchema = {
|
|
3177
3182
|
type: "platform_configurations",
|
|
3183
|
+
RBAC: true,
|
|
3178
3184
|
name: {
|
|
3179
3185
|
plural: "platform_configurations",
|
|
3180
3186
|
singular: "platform_configuration"
|
|
@@ -3294,6 +3300,60 @@ var promptFavoritesSchema = {
|
|
|
3294
3300
|
}
|
|
3295
3301
|
]
|
|
3296
3302
|
};
|
|
3303
|
+
var transcriptionJobsSchema = {
|
|
3304
|
+
type: "transcription_jobs",
|
|
3305
|
+
name: {
|
|
3306
|
+
plural: "transcription_jobs",
|
|
3307
|
+
singular: "transcription_job"
|
|
3308
|
+
},
|
|
3309
|
+
RBAC: true,
|
|
3310
|
+
fields: [
|
|
3311
|
+
{ name: "audio", type: "file" },
|
|
3312
|
+
{ name: "title", type: "text" },
|
|
3313
|
+
{ name: "status", type: "text", index: true },
|
|
3314
|
+
{ name: "whisper_job_id", type: "text" },
|
|
3315
|
+
{ name: "raw_segments", type: "json" },
|
|
3316
|
+
{ name: "speakers", type: "json" },
|
|
3317
|
+
{ name: "language", type: "text" },
|
|
3318
|
+
{ name: "duration_seconds", type: "number" },
|
|
3319
|
+
{ name: "project_id", type: "uuid", required: false },
|
|
3320
|
+
{ name: "target_rights_mode", type: "text", default: "private" },
|
|
3321
|
+
{ name: "target_rbac_users", type: "json" },
|
|
3322
|
+
{ name: "target_rbac_roles", type: "json" },
|
|
3323
|
+
{ name: "saved_item_id", type: "uuid", required: false },
|
|
3324
|
+
{ name: "error", type: "text" }
|
|
3325
|
+
]
|
|
3326
|
+
};
|
|
3327
|
+
var imageGenerationsSchema = {
|
|
3328
|
+
type: "image_generations",
|
|
3329
|
+
name: {
|
|
3330
|
+
plural: "image_generations",
|
|
3331
|
+
singular: "image_generation"
|
|
3332
|
+
},
|
|
3333
|
+
// Access is gated by the parent agent_sessions RBAC — rows have no
|
|
3334
|
+
// independent visibility, so no row-level RBAC fields are needed here.
|
|
3335
|
+
RBAC: false,
|
|
3336
|
+
fields: [
|
|
3337
|
+
{ name: "session_id", type: "uuid", required: true, index: true },
|
|
3338
|
+
{ name: "tool_call_id", type: "text", required: true, index: true },
|
|
3339
|
+
{ name: "user_id", type: "number", required: true, index: true },
|
|
3340
|
+
{ name: "operation", type: "text", required: true },
|
|
3341
|
+
// 'generate' | 'edit'
|
|
3342
|
+
{ name: "model", type: "text", required: true },
|
|
3343
|
+
{ name: "prompt", type: "longText", required: true },
|
|
3344
|
+
{ name: "applied_style_id", type: "uuid", required: false },
|
|
3345
|
+
{ name: "applied_style_markdown", type: "longText", required: false },
|
|
3346
|
+
{ name: "size", type: "text", required: false },
|
|
3347
|
+
{ name: "quality", type: "text", required: false },
|
|
3348
|
+
{ name: "n", type: "number", default: 1 },
|
|
3349
|
+
{ name: "reference_image_keys", type: "json", required: false },
|
|
3350
|
+
{ name: "mask_image_key", type: "text", required: false },
|
|
3351
|
+
{ name: "image_keys", type: "json", required: true },
|
|
3352
|
+
{ name: "revised_prompts", type: "json", required: false },
|
|
3353
|
+
{ name: "selected", type: "boolean", default: false },
|
|
3354
|
+
{ name: "error", type: "text", required: false }
|
|
3355
|
+
]
|
|
3356
|
+
};
|
|
3297
3357
|
var contextPresetsSchema = {
|
|
3298
3358
|
type: "context_presets",
|
|
3299
3359
|
name: {
|
|
@@ -3384,7 +3444,9 @@ var coreSchemas = {
|
|
|
3384
3444
|
promptLibrarySchema: () => addCoreFields(promptLibrarySchema),
|
|
3385
3445
|
embedderSettingsSchema: () => addCoreFields(embedderSettingsSchema),
|
|
3386
3446
|
promptFavoritesSchema: () => addCoreFields(promptFavoritesSchema),
|
|
3387
|
-
contextPresetsSchema: () => addCoreFields(contextPresetsSchema)
|
|
3447
|
+
contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
|
|
3448
|
+
transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
|
|
3449
|
+
imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
|
|
3388
3450
|
};
|
|
3389
3451
|
if (license["agent-feedback"]) {
|
|
3390
3452
|
schemas.feedbackSchema = () => addCoreFields(feedbackSchema);
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
5
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href;
|
|
6
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
7
|
+
|
|
8
|
+
// src/cli/start-whisper.ts
|
|
9
|
+
var import_config = require("dotenv/config");
|
|
10
|
+
|
|
11
|
+
// src/utils/python-setup.ts
|
|
12
|
+
var import_child_process = require("child_process");
|
|
13
|
+
var import_util = require("util");
|
|
14
|
+
var import_path = require("path");
|
|
15
|
+
var import_fs = require("fs");
|
|
16
|
+
var import_url = require("url");
|
|
17
|
+
var execAsync = (0, import_util.promisify)(import_child_process.exec);
|
|
18
|
+
function getPackageRoot() {
|
|
19
|
+
const currentFile = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
20
|
+
let currentDir = (0, import_path.dirname)(currentFile);
|
|
21
|
+
let attempts = 0;
|
|
22
|
+
const maxAttempts = 10;
|
|
23
|
+
while (attempts < maxAttempts) {
|
|
24
|
+
const packageJsonPath = (0, import_path.join)(currentDir, "package.json");
|
|
25
|
+
if ((0, import_fs.existsSync)(packageJsonPath)) {
|
|
26
|
+
try {
|
|
27
|
+
const packageJson = JSON.parse((0, import_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
28
|
+
if (packageJson.name === "@exulu/backend") {
|
|
29
|
+
return currentDir;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const parentDir = (0, import_path.resolve)(currentDir, "..");
|
|
35
|
+
if (parentDir === currentDir) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
currentDir = parentDir;
|
|
39
|
+
attempts++;
|
|
40
|
+
}
|
|
41
|
+
const fallback = (0, import_path.resolve)((0, import_path.dirname)((0, import_url.fileURLToPath)(importMetaUrl)), "../..");
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/exulu/transcription/supervisor.ts
|
|
46
|
+
var import_node_child_process = require("child_process");
|
|
47
|
+
var import_node_fs = require("fs");
|
|
48
|
+
var import_node_path = require("path");
|
|
49
|
+
var MAX_CRASHES = 5;
|
|
50
|
+
var INITIAL_BACKOFF_MS = 1e3;
|
|
51
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
52
|
+
var READY_TIMEOUT_MS = 30 * 6e4;
|
|
53
|
+
var READY_POLL_INTERVAL_MS = 500;
|
|
54
|
+
var SHUTDOWN_GRACE_MS = 5e3;
|
|
55
|
+
var internal = {
|
|
56
|
+
child: void 0,
|
|
57
|
+
state: "idle",
|
|
58
|
+
crashCount: 0,
|
|
59
|
+
backoffMs: INITIAL_BACKOFF_MS,
|
|
60
|
+
readyPromise: void 0,
|
|
61
|
+
shutdownRequested: false
|
|
62
|
+
};
|
|
63
|
+
var resolveConfig = (packageRoot) => {
|
|
64
|
+
const host = process.env.WHISPER_HOST ?? "127.0.0.1";
|
|
65
|
+
const port = process.env.WHISPER_PORT ?? "9876";
|
|
66
|
+
const venvBin = (0, import_node_path.resolve)(packageRoot, "ee/python/.venv/bin");
|
|
67
|
+
const venvPython = (0, import_node_path.resolve)(venvBin, "python");
|
|
68
|
+
const cwd = (0, import_node_path.resolve)(packageRoot, "ee/python/transcription");
|
|
69
|
+
return { host, port, venvBin, venvPython, cwd };
|
|
70
|
+
};
|
|
71
|
+
var log = (line) => {
|
|
72
|
+
const text = line.startsWith("[EXULU-WHISPER]") ? line : `[EXULU-WHISPER] ${line}`;
|
|
73
|
+
console.log(text);
|
|
74
|
+
};
|
|
75
|
+
var pollHealth = async (host, port) => {
|
|
76
|
+
const url = `http://${host}:${port}/healthz`;
|
|
77
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
78
|
+
while (Date.now() < deadline) {
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(url, { method: "GET" });
|
|
81
|
+
if (res.ok) return;
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
|
|
85
|
+
}
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Whisper server did not become ready at ${url} within ${READY_TIMEOUT_MS}ms`
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
var spawnWhisper = (cfg) => {
|
|
91
|
+
log(
|
|
92
|
+
`Spawning: ${cfg.venvPython} -m uvicorn server:app --host ${cfg.host} --port ${cfg.port}`
|
|
93
|
+
);
|
|
94
|
+
const child = (0, import_node_child_process.spawn)(
|
|
95
|
+
cfg.venvPython,
|
|
96
|
+
[
|
|
97
|
+
"-m",
|
|
98
|
+
"uvicorn",
|
|
99
|
+
"server:app",
|
|
100
|
+
"--host",
|
|
101
|
+
cfg.host,
|
|
102
|
+
"--port",
|
|
103
|
+
cfg.port,
|
|
104
|
+
"--log-level",
|
|
105
|
+
"info"
|
|
106
|
+
],
|
|
107
|
+
{
|
|
108
|
+
cwd: cfg.cwd,
|
|
109
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
110
|
+
env: process.env
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
child.stdout?.on("data", (chunk) => {
|
|
114
|
+
chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(l));
|
|
115
|
+
});
|
|
116
|
+
child.stderr?.on("data", (chunk) => {
|
|
117
|
+
chunk.toString().split("\n").filter((l) => l.length > 0).forEach((l) => log(`stderr: ${l}`));
|
|
118
|
+
});
|
|
119
|
+
return child;
|
|
120
|
+
};
|
|
121
|
+
var supervise = async (cfg) => {
|
|
122
|
+
while (!internal.shutdownRequested && internal.crashCount < MAX_CRASHES) {
|
|
123
|
+
internal.state = internal.crashCount === 0 ? "starting" : "respawning";
|
|
124
|
+
internal.child = spawnWhisper(cfg);
|
|
125
|
+
const exitPromise = new Promise((resolveFn) => {
|
|
126
|
+
internal.child.on("exit", (code2) => resolveFn(code2));
|
|
127
|
+
});
|
|
128
|
+
try {
|
|
129
|
+
await Promise.race([
|
|
130
|
+
pollHealth(cfg.host, cfg.port).then(() => "ready"),
|
|
131
|
+
exitPromise.then((code2) => ({ exited: code2 }))
|
|
132
|
+
]);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
log(`Readiness probe failed: ${err.message}`);
|
|
135
|
+
try {
|
|
136
|
+
internal.child?.kill("SIGTERM");
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!internal.child?.killed && internal.child?.exitCode === null) {
|
|
141
|
+
internal.state = "ready";
|
|
142
|
+
internal.crashCount = 0;
|
|
143
|
+
internal.backoffMs = INITIAL_BACKOFF_MS;
|
|
144
|
+
log("Whisper server is ready.");
|
|
145
|
+
}
|
|
146
|
+
const code = await exitPromise;
|
|
147
|
+
internal.state = "respawning";
|
|
148
|
+
internal.child = void 0;
|
|
149
|
+
if (internal.shutdownRequested) {
|
|
150
|
+
log("Child exited during shutdown; supervisor stopping.");
|
|
151
|
+
internal.state = "stopped";
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
internal.crashCount += 1;
|
|
155
|
+
log(
|
|
156
|
+
`Whisper server exited (code=${code}). Crash ${internal.crashCount}/${MAX_CRASHES}. Respawning in ${internal.backoffMs}ms.`
|
|
157
|
+
);
|
|
158
|
+
if (internal.crashCount >= MAX_CRASHES) {
|
|
159
|
+
log(
|
|
160
|
+
"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."
|
|
161
|
+
);
|
|
162
|
+
internal.state = "given_up";
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
await new Promise((r) => setTimeout(r, internal.backoffMs));
|
|
166
|
+
internal.backoffMs = Math.min(internal.backoffMs * 2, MAX_BACKOFF_MS);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var startWhisperSupervisor = async (options) => {
|
|
170
|
+
if (internal.readyPromise) {
|
|
171
|
+
return internal.readyPromise;
|
|
172
|
+
}
|
|
173
|
+
const cfg = resolveConfig(options.packageRoot);
|
|
174
|
+
if (!(0, import_node_fs.existsSync)(cfg.venvPython)) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Whisper supervisor: Python venv not found at ${cfg.venvPython}. Run \`npx @exulu/backend setup-python\` first.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (!(0, import_node_fs.existsSync)(cfg.cwd)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Whisper supervisor: transcription scripts not found at ${cfg.cwd}. The @exulu/backend package may be corrupt; reinstall it.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
internal.readyPromise = (async () => {
|
|
185
|
+
supervise(cfg);
|
|
186
|
+
const deadline = Date.now() + READY_TIMEOUT_MS + 5e3;
|
|
187
|
+
while (Date.now() < deadline) {
|
|
188
|
+
if (internal.state === "ready") return;
|
|
189
|
+
if (internal.state === "given_up") {
|
|
190
|
+
throw new Error("Whisper supervisor gave up before becoming ready.");
|
|
191
|
+
}
|
|
192
|
+
await new Promise((r) => setTimeout(r, READY_POLL_INTERVAL_MS));
|
|
193
|
+
}
|
|
194
|
+
throw new Error("Timed out waiting for whisper supervisor readiness.");
|
|
195
|
+
})();
|
|
196
|
+
registerShutdownHandlers();
|
|
197
|
+
return internal.readyPromise;
|
|
198
|
+
};
|
|
199
|
+
var stopWhisper = (signal = "SIGTERM") => {
|
|
200
|
+
internal.shutdownRequested = true;
|
|
201
|
+
const child = internal.child;
|
|
202
|
+
if (!child) return;
|
|
203
|
+
try {
|
|
204
|
+
child.kill(signal);
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
setTimeout(() => {
|
|
208
|
+
try {
|
|
209
|
+
if (!child.killed && child.exitCode === null) {
|
|
210
|
+
child.kill("SIGKILL");
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}, SHUTDOWN_GRACE_MS).unref();
|
|
215
|
+
};
|
|
216
|
+
var shutdownHandlersRegistered = false;
|
|
217
|
+
var registerShutdownHandlers = () => {
|
|
218
|
+
if (shutdownHandlersRegistered) return;
|
|
219
|
+
shutdownHandlersRegistered = true;
|
|
220
|
+
process.on("SIGINT", () => stopWhisper("SIGTERM"));
|
|
221
|
+
process.on("SIGTERM", () => stopWhisper("SIGTERM"));
|
|
222
|
+
process.on("exit", () => stopWhisper("SIGTERM"));
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// src/cli/start-whisper.ts
|
|
226
|
+
var main = async () => {
|
|
227
|
+
const packageRoot = getPackageRoot();
|
|
228
|
+
try {
|
|
229
|
+
await startWhisperSupervisor({ packageRoot });
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error(`[EXULU-WHISPER] ${err.message}`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
await new Promise(() => {
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
main().catch((err) => {
|
|
238
|
+
console.error(`[EXULU-WHISPER] Unexpected error: ${err.stack ?? err}`);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|