@exulu/backend 1.60.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-23YNGK3V.js → chunk-MPV7HBV6.js} +63 -2
- 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-PLLM2CJL.js → convert-exulu-tools-to-ai-sdk-tools-CULC37U6.js} +1 -1
- package/dist/index.cjs +1827 -346
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1447 -249
- 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");
|
|
@@ -744,7 +744,7 @@ var ExuluTool = class {
|
|
|
744
744
|
});
|
|
745
745
|
providerapikey = resolved.apiKey;
|
|
746
746
|
}
|
|
747
|
-
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");
|
|
748
748
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
749
749
|
[this],
|
|
750
750
|
[],
|
|
@@ -3168,6 +3168,10 @@ var usersSchema = {
|
|
|
3168
3168
|
name: "anthropic_token",
|
|
3169
3169
|
type: "text"
|
|
3170
3170
|
},
|
|
3171
|
+
{
|
|
3172
|
+
name: "personal_system_prompt",
|
|
3173
|
+
type: "longText"
|
|
3174
|
+
},
|
|
3171
3175
|
{
|
|
3172
3176
|
name: "role",
|
|
3173
3177
|
type: "uuid"
|
|
@@ -3176,6 +3180,7 @@ var usersSchema = {
|
|
|
3176
3180
|
};
|
|
3177
3181
|
var platformConfigurationsSchema = {
|
|
3178
3182
|
type: "platform_configurations",
|
|
3183
|
+
RBAC: true,
|
|
3179
3184
|
name: {
|
|
3180
3185
|
plural: "platform_configurations",
|
|
3181
3186
|
singular: "platform_configuration"
|
|
@@ -3295,6 +3300,60 @@ var promptFavoritesSchema = {
|
|
|
3295
3300
|
}
|
|
3296
3301
|
]
|
|
3297
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
|
+
};
|
|
3298
3357
|
var contextPresetsSchema = {
|
|
3299
3358
|
type: "context_presets",
|
|
3300
3359
|
name: {
|
|
@@ -3385,7 +3444,9 @@ var coreSchemas = {
|
|
|
3385
3444
|
promptLibrarySchema: () => addCoreFields(promptLibrarySchema),
|
|
3386
3445
|
embedderSettingsSchema: () => addCoreFields(embedderSettingsSchema),
|
|
3387
3446
|
promptFavoritesSchema: () => addCoreFields(promptFavoritesSchema),
|
|
3388
|
-
contextPresetsSchema: () => addCoreFields(contextPresetsSchema)
|
|
3447
|
+
contextPresetsSchema: () => addCoreFields(contextPresetsSchema),
|
|
3448
|
+
transcriptionJobsSchema: () => addCoreFields(transcriptionJobsSchema),
|
|
3449
|
+
imageGenerationsSchema: () => addCoreFields(imageGenerationsSchema)
|
|
3389
3450
|
};
|
|
3390
3451
|
if (license["agent-feedback"]) {
|
|
3391
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
|