implement backend service manager and speech transcription pipeline with supporting setup scripts

This commit is contained in:
techjarves committed 2026-06-21 19:23:38 +05:30
1 parent 20b81926b6
commit b71b4ecafa
17 files changed
+1680 -46

No files matched your search

+465 -10
View File
@@ -30,8 +30,10 @@ function readPort(value, fallback) {
const PORT_FRONTEND = readPort(process.env.PORT || process.env.FRONTEND_PORT, 1420);
const PREFERRED_BACKEND_PORT = readPort(process.env.BACKEND_PORT || process.env.SD_BACKEND_PORT, 8080);
const PREFERRED_LLM_PORT = readPort(process.env.LLM_PORT, 10086);
const PREFERRED_SPEECH_PORT = readPort(process.env.SPEECH_PORT, 10088);
let PORT_BACKEND = PREFERRED_BACKEND_PORT;
let PORT_LLM = PREFERRED_LLM_PORT;
let PORT_SPEECH = PREFERRED_SPEECH_PORT;
const MAX_JSON_BODY_BYTES = 64 * 1024 * 1024;
const SERVER_BUILD = "text-image-v1";
const ROOT = path.join(__dirname, "..");
@@ -105,6 +107,34 @@ const LLM_BENCHMARK_PATH = path.join(LLM_CONFIG_DIR, "llm-benchmarks.json");
if (!fs.existsSync(LLM_CONFIG_DIR)) {
fs.mkdirSync(LLM_CONFIG_DIR, { recursive: true });
}
const SPEECH_MODELS = path.join(ROOT, "app", "speech-models");
if (!fs.existsSync(SPEECH_MODELS)) {
fs.mkdirSync(SPEECH_MODELS, { recursive: true });
}
const TRANSCRIPTIONS = path.join(ROOT, "app", "transcriptions");
if (!fs.existsSync(TRANSCRIPTIONS)) {
fs.mkdirSync(TRANSCRIPTIONS, { recursive: true });
}
const SPEECH_BACKEND_PATHS = {
winCli: path.join(ROOT, "app", "speech-backend", "win", "whisper-cli.exe"),
winServer: path.join(ROOT, "app", "speech-backend", "win", "whisper-server.exe"),
linuxCli: path.join(ROOT, "app", "speech-backend", "linux", "whisper-cli"),
linuxServer: path.join(ROOT, "app", "speech-backend", "linux", "whisper-server"),
macCli: path.join(ROOT, "app", "speech-backend", "mac", "whisper-cli"),
macServer: path.join(ROOT, "app", "speech-backend", "mac", "whisper-server"),
};
const SPEECH_MODEL_CATALOG = [
{ id: "tiny.en", name: "Whisper Tiny English", filename: "ggml-tiny.en.bin", size: "75 MB", language: "English", recommended: false },
{ id: "tiny.en-q5_1", name: "Whisper Tiny English Q5", filename: "ggml-tiny.en-q5_1.bin", size: "31 MB", language: "English", recommended: false },
{ id: "base.en", name: "Whisper Base English", filename: "ggml-base.en.bin", size: "142 MB", language: "English", recommended: true },
{ id: "base.en-q5_1", name: "Whisper Base English Q5", filename: "ggml-base.en-q5_1.bin", size: "57 MB", language: "English", recommended: true },
{ id: "small.en", name: "Whisper Small English", filename: "ggml-small.en.bin", size: "466 MB", language: "English", recommended: false },
{ id: "small.en-q5_1", name: "Whisper Small English Q5", filename: "ggml-small.en-q5_1.bin", size: "181 MB", language: "English", recommended: false },
{ id: "tiny", name: "Whisper Tiny Multilingual", filename: "ggml-tiny.bin", size: "75 MB", language: "Multilingual", recommended: false },
{ id: "base", name: "Whisper Base Multilingual", filename: "ggml-base.bin", size: "142 MB", language: "Multilingual", recommended: false },
{ id: "base-q5_1", name: "Whisper Base Multilingual Q5", filename: "ggml-base-q5_1.bin", size: "57 MB", language: "Multilingual", recommended: false },
{ id: "small", name: "Whisper Small Multilingual", filename: "ggml-small.bin", size: "466 MB", language: "Multilingual", recommended: false },
];
const OPENVINO_MODELS = path.join(ROOT, "app", "openvino-models");
if (!fs.existsSync(OPENVINO_MODELS)) {
fs.mkdirSync(OPENVINO_MODELS, { recursive: true });
@@ -142,6 +172,23 @@ let llmProcSeq = 0;
let llmReady = false;
let llmError = null;
let llmOperationQueue = Promise.resolve();
let speechReady = false;
let speechError = null;
let speechOperationQueue = Promise.resolve();
let speechSettings = {
model: null,
language: "auto",
threads: Math.max(1, Math.min(8, os.cpus().length || 4)),
backendBinary: "",
backendMode: "",
};
let speechTranscriptionState = {
active: false,
phase: "",
progress: 0,
model: "",
filename: "",
};
let llmSettings = {
model: null,
threads: Math.max(1, Math.min(16, os.cpus().length || 4)),
@@ -1396,6 +1443,21 @@ async function findAvailableLlmPort() {
throw new Error(`No free LLM port found. Tried ${PREFERRED_LLM_PORT} and 28121-28160.`);
}
async function findAvailableSpeechPort() {
const preferred = await checkPort(PREFERRED_SPEECH_PORT);
if (preferred.available) return PREFERRED_SPEECH_PORT;
for (let port = 28161; port <= 28190; port += 1) {
const candidate = await checkPort(port);
if (candidate.available) {
console.log(` [speech] Preferred port ${PREFERRED_SPEECH_PORT} is busy; using ${port} instead.`);
return port;
}
}
throw new Error(`No free speech port found. Tried ${PREFERRED_SPEECH_PORT} and 28161-28190.`);
}
function getLlmBackend() {
return getLlmBackendCandidates()[0] || null;
}
@@ -1537,6 +1599,283 @@ function getLlmModels() {
}
}
function findExistingFile(candidates) {
return candidates.find((candidate) => candidate && fs.existsSync(candidate)) || "";
}
function getSpeechBackend() {
if (osPlatform === "win32") {
return {
cli: findExistingFile([SPEECH_BACKEND_PATHS.winCli, path.join(ROOT, "app", "speech-backend", "win", "main.exe")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.winServer, path.join(ROOT, "app", "speech-backend", "win", "server.exe")]),
mode: "whisper.cpp CPU",
};
}
if (osPlatform === "darwin") {
return {
cli: findExistingFile([SPEECH_BACKEND_PATHS.macCli, path.join(ROOT, "app", "speech-backend", "mac", "main")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.macServer, path.join(ROOT, "app", "speech-backend", "mac", "server")]),
mode: "whisper.cpp Metal/CPU",
};
}
return {
cli: findExistingFile([SPEECH_BACKEND_PATHS.linuxCli, path.join(ROOT, "app", "speech-backend", "linux", "main")]),
server: findExistingFile([SPEECH_BACKEND_PATHS.linuxServer, path.join(ROOT, "app", "speech-backend", "linux", "server")]),
mode: "whisper.cpp CPU",
};
}
function getSpeechModels() {
const catalog = SPEECH_MODEL_CATALOG.map((model) => {
const modelPath = path.join(SPEECH_MODELS, model.filename);
const installed = fs.existsSync(modelPath);
return {
...model,
installed,
sizeBytes: installed ? getPathSize(modelPath) : 0,
localSize: installed ? formatBytes(getPathSize(modelPath)) : "",
url: `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${model.filename}`,
};
});
const known = new Set(catalog.map((model) => model.filename.toLowerCase()));
let custom = [];
try {
custom = fs.readdirSync(SPEECH_MODELS)
.filter((filename) => filename.toLowerCase().endsWith(".bin") && !known.has(filename.toLowerCase()))
.map((filename) => {
const sizeBytes = getPathSize(path.join(SPEECH_MODELS, filename));
return {
id: filename,
name: filename,
filename,
size: formatBytes(sizeBytes),
localSize: formatBytes(sizeBytes),
sizeBytes,
language: "Custom",
installed: true,
recommended: false,
custom: true,
};
});
} catch (_) {}
return [...catalog, ...custom];
}
function resolveSpeechModel(value) {
const raw = String(value || "").trim();
const model = getSpeechModels().find((item) => item.id === raw || item.filename === raw) ||
getSpeechModels().find((item) => item.installed);
if (!model) throw new Error("Download or import a Whisper model first.");
const modelPath = path.join(SPEECH_MODELS, path.basename(model.filename));
if (!pathInside(modelPath, SPEECH_MODELS) || !fs.existsSync(modelPath)) {
throw new Error(`Speech model is not installed: ${model.filename}`);
}
return { ...model, path: modelPath };
}
function runExclusiveSpeechOperation(operation) {
const run = speechOperationQueue.catch(() => {}).then(operation);
speechOperationQueue = run.catch(() => {});
return run;
}
async function startSpeech(settings = {}) {
const backend = getSpeechBackend();
if (!backend.cli) {
throw new Error("whisper.cpp is not installed. Run the platform setup script to install the speech backend.");
}
const model = resolveSpeechModel(settings.model);
await killBackend();
await killOpenVinoWorker();
await killLlm();
PORT_SPEECH = await findAvailableSpeechPort();
speechError = null;
speechReady = true;
speechSettings = {
...speechSettings,
model: model.filename,
language: settings.language || speechSettings.language || "auto",
threads: Math.max(1, Math.min(32, Number(settings.threads) || speechSettings.threads || 4)),
backendBinary: path.basename(backend.cli),
backendMode: backend.mode,
};
}
async function stopSpeech() {
speechReady = false;
speechError = null;
speechTranscriptionState = { active: false, phase: "", progress: 0, model: "", filename: "" };
}
function readBinaryBody(req, limitBytes = 250 * 1024 * 1024) {
return new Promise((resolve, reject) => {
const chunks = [];
let bytes = 0;
req.on("data", (chunk) => {
bytes += chunk.length;
if (bytes > limitBytes) {
reject(new Error(`Audio file is too large. Limit is ${Math.round(limitBytes / (1024 * 1024))} MB.`));
req.destroy();
return;
}
chunks.push(chunk);
});
req.on("end", () => resolve(Buffer.concat(chunks)));
req.on("error", reject);
});
}
function isWaveBuffer(buffer) {
return buffer?.length > 44 &&
buffer.toString("ascii", 0, 4) === "RIFF" &&
buffer.toString("ascii", 8, 12) === "WAVE";
}
function saveTranscript(result) {
const createdAt = new Date().toISOString();
const stamp = createdAt.replace(/[:.]/g, "-");
const base = `transcript-${stamp}-${safeOutputName(result.sourceFilename || "audio")}`;
const textFilename = `${base}.txt`;
const jsonFilename = `${base}.json`;
fs.writeFileSync(path.join(TRANSCRIPTIONS, textFilename), result.text || "", "utf8");
const metadata = {
...result,
createdAt,
textFile: textFilename,
metadata: jsonFilename,
};
fs.writeFileSync(path.join(TRANSCRIPTIONS, jsonFilename), JSON.stringify(metadata, null, 2), "utf8");
return metadata;
}
function listTranscriptions() {
try {
return fs.readdirSync(TRANSCRIPTIONS)
.filter((file) => file.toLowerCase().endsWith(".json"))
.map((file) => {
const filePath = path.join(TRANSCRIPTIONS, file);
try {
const metadata = JSON.parse(fs.readFileSync(filePath, "utf8"));
const stat = fs.statSync(filePath);
return {
...metadata,
filename: file,
displayName: metadata.sourceFilename || metadata.textFile || file,
sizeBytes: stat.size,
size: formatBytes(stat.size),
modifiedAt: stat.mtime.toISOString(),
createdAt: metadata.createdAt || stat.mtime.toISOString(),
url: `/transcriptions/${encodeURIComponent(file)}`,
textUrl: metadata.textFile ? `/transcriptions/${encodeURIComponent(metadata.textFile)}` : "",
};
} catch (_) {
return null;
}
})
.filter(Boolean)
.sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")));
} catch (_) {
return [];
}
}
function transcribeWavBuffer(buffer, options = {}) {
return new Promise((resolve, reject) => {
if (!isWaveBuffer(buffer)) {
reject(new Error("Speech transcription currently accepts WAV audio only."));
return;
}
const backend = getSpeechBackend();
if (!backend.cli) {
reject(new Error("whisper.cpp CLI backend is not installed."));
return;
}
const model = resolveSpeechModel(options.model || speechSettings.model);
const language = String(options.language || speechSettings.language || "auto");
const sourceFilename = safeOutputName(options.filename || "recording.wav") || "recording.wav";
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const tempBase = path.join(TRANSCRIPTIONS, `.speech-${stamp}-${sourceFilename.replace(/\.[^.]+$/, "")}`);
const wavPath = `${tempBase}.wav`;
const outBase = `${tempBase}-out`;
fs.writeFileSync(wavPath, buffer);
speechTranscriptionState = {
active: true,
phase: "Transcribing audio...",
progress: -1,
model: model.filename,
filename: sourceFilename,
};
const args = [
"-m", model.path,
"-f", wavPath,
"-otxt",
"-oj",
"-of", outBase,
"-t", String(Math.max(1, Math.min(32, Number(options.threads) || speechSettings.threads || 4))),
];
if (language && language !== "auto") {
args.push("-l", language);
} else {
args.push("-l", "auto");
}
const startedAt = Date.now();
console.log(" [speech] Starting:", backend.cli, args.join(" "));
const proc = spawn(backend.cli, args, {
stdio: "pipe",
windowsHide: true,
env: {
...process.env,
...(osPlatform === "linux" ? { LD_LIBRARY_PATH: path.dirname(backend.cli) + (process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : "") } : {}),
...(osPlatform === "darwin" ? { DYLD_LIBRARY_PATH: path.dirname(backend.cli) + (process.env.DYLD_LIBRARY_PATH ? `:${process.env.DYLD_LIBRARY_PATH}` : "") } : {}),
},
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (data) => {
stdout += data.toString();
process.stdout.write(" [speech] " + data.toString());
});
proc.stderr.on("data", (data) => {
stderr += data.toString();
process.stderr.write(" [speech-err] " + data.toString());
});
proc.on("exit", (code) => {
try { fs.unlinkSync(wavPath); } catch (_) {}
speechTranscriptionState = { active: false, phase: code === 0 ? "Complete" : "Failed", progress: code === 0 ? 100 : 0, model: model.filename, filename: sourceFilename };
if (code !== 0) {
const message = (stderr || stdout || `whisper.cpp exited with code ${code}`).trim().slice(-1200);
speechError = message;
reject(new Error(message));
return;
}
const txtPath = `${outBase}.txt`;
const jsonPath = `${outBase}.json`;
const text = fs.existsSync(txtPath) ? fs.readFileSync(txtPath, "utf8").trim() : stdout.trim();
let raw = null;
try {
raw = fs.existsSync(jsonPath) ? JSON.parse(fs.readFileSync(jsonPath, "utf8")) : null;
} catch (_) {}
try { fs.unlinkSync(txtPath); } catch (_) {}
try { fs.unlinkSync(jsonPath); } catch (_) {}
const saved = saveTranscript({
text,
model: model.filename,
modelName: model.name,
language,
sourceFilename,
durationMs: Date.now() - startedAt,
raw,
});
speechError = null;
resolve(saved);
});
});
}
function pingLlmReady(expectedModel = "") {
return new Promise((resolve) => {
const req = http.get(`http://127.0.0.1:${PORT_LLM}/v1/models`, (res) => {
@@ -3730,7 +4069,7 @@ function cancelModelDownload() {
try { fs.unlinkSync(activeDownload.tempPath || activeDownload.destPath); } catch (_) {}
}
} else if (filename) {
const targetDir = downloadState.kind === "text" ? LLM_MODELS : MODELS;
const targetDir = downloadState.kind === "text" ? LLM_MODELS : downloadState.kind === "speech" ? SPEECH_MODELS : MODELS;
try { fs.unlinkSync(`${path.join(targetDir, filename)}.part`); } catch (_) {}
}
activeDownload = null;
@@ -3754,7 +4093,7 @@ const MIME = {
".html": "text/html", ".js": "application/javascript",
".css": "text/css", ".png": "image/png",
".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml",
".ico": "image/x-icon", ".json": "application/json",
".ico": "image/x-icon", ".json": "application/json", ".wav": "audio/wav", ".txt": "text/plain",
".woff2":"font/woff2", ".woff": "font/woff", ".ttf": "font/ttf",
};
@@ -4047,12 +4386,17 @@ function deleteGeneratedOutputs(outputs = []) {
return deleted;
}
function streamModelUpload(req, filename, targetDir = MODELS, textOnly = false) {
function streamModelUpload(req, filename, targetDir = MODELS, mode = "image") {
return new Promise((resolve, reject) => {
const safeFilename = path.basename(filename || "");
const lowerName = safeFilename.toLowerCase();
if (!safeFilename || (textOnly ? !lowerName.endsWith(".gguf") : !isModelFile(lowerName))) {
reject(new Error(textOnly ? "Filename must end with .gguf" : "Filename must end with .gguf, .safetensors, or .ckpt"));
const valid = mode === "text"
? lowerName.endsWith(".gguf")
: mode === "speech"
? lowerName.endsWith(".bin")
: isModelFile(lowerName);
if (!safeFilename || !valid) {
reject(new Error(mode === "text" ? "Filename must end with .gguf" : mode === "speech" ? "Filename must end with .bin" : "Filename must end with .gguf, .safetensors, or .ckpt"));
return;
}
@@ -4090,9 +4434,9 @@ function streamModelUpload(req, filename, targetDir = MODELS, textOnly = false)
finished = true;
fs.renameSync(tempPath, destPath);
console.log(` [api] Imported model file: ${safeFilename}`);
if (textOnly) {
if (mode === "text" || mode === "speech") {
const stats = fs.statSync(destPath);
resolve({ filename: safeFilename, name: safeFilename, sizeBytes: stats.size, size: formatBytes(stats.size), format: "GGUF" });
resolve({ filename: safeFilename, name: safeFilename, sizeBytes: stats.size, size: formatBytes(stats.size), format: mode === "speech" ? "Whisper GGML" : "GGUF" });
} else {
resolve(getModelInfo(safeFilename));
}
@@ -4139,6 +4483,13 @@ const server = http.createServer(async (req, res) => {
settings: llmSettings,
port: PORT_LLM,
},
speech: {
ready: speechReady,
error: speechError,
settings: speechSettings,
backend: getSpeechBackend(),
transcription: speechTranscriptionState,
},
hardware: getHardwareSpecs(),
telemetry: getTelemetry(),
});
@@ -4278,6 +4629,109 @@ const server = http.createServer(async (req, res) => {
}
}
if (req.url === "/api/speech/status" && req.method === "GET") {
const backend = getSpeechBackend();
return json(res, 200, {
ok: true,
ready: speechReady,
running: speechReady,
port: PORT_SPEECH,
preferredPort: PREFERRED_SPEECH_PORT,
backendInstalled: Boolean(backend.cli),
backendPath: backend.cli || "",
serverPath: backend.server || "",
backendMode: backend.mode,
error: speechError,
settings: speechSettings,
transcription: speechTranscriptionState,
});
}
if (req.url === "/api/speech/models" && req.method === "GET") {
return json(res, 200, { ok: true, models: getSpeechModels() });
}
if (req.url === "/api/speech/start" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
try {
await runExclusiveSpeechOperation(() => startSpeech(body));
return json(res, 200, { ok: true, ready: speechReady, settings: speechSettings, port: PORT_SPEECH });
} catch (err) {
speechError = err.message || String(err);
return json(res, 500, { ok: false, error: speechError });
}
}
if (req.url === "/api/speech/stop" && req.method === "POST") {
await runExclusiveSpeechOperation(() => stopSpeech());
return json(res, 200, { ok: true });
}
if (req.url.startsWith("/api/speech/transcribe") && req.method === "POST") {
const parsed = new URL(req.url, `http://${req.headers.host || "localhost"}`);
try {
const audio = await readBinaryBody(req);
const result = await runExclusiveSpeechOperation(() => transcribeWavBuffer(audio, {
model: parsed.searchParams.get("model") || speechSettings.model,
language: parsed.searchParams.get("language") || "auto",
filename: parsed.searchParams.get("filename") || req.headers["x-filename"] || "recording.wav",
threads: parsed.searchParams.get("threads") || speechSettings.threads,
}));
return json(res, 200, { ok: true, transcription: result });
} catch (err) {
speechError = err.message || String(err);
return json(res, err.message?.includes("too large") ? 413 : 500, { ok: false, error: speechError });
}
}
if (req.url === "/api/speech/download-model" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
if (downloadState.active) return json(res, 409, { ok: false, error: "Another model download is already active." });
const modelId = String(body.modelId || body.model_id || body.model || "");
const catalogModel = SPEECH_MODEL_CATALOG.find((model) => model.id === modelId || model.filename === modelId);
const url = body.url ? String(body.url) : (catalogModel ? `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${catalogModel.filename}` : "");
const filename = path.basename(String(body.filename || catalogModel?.filename || ""));
if (!url || !filename || !filename.toLowerCase().endsWith(".bin")) {
return json(res, 400, { ok: false, error: "A speech model .bin URL or catalog model id is required." });
}
startModelDownload(url, filename, SPEECH_MODELS, "speech");
return json(res, 200, { ok: true, message: "Speech model download started", filename });
}
if (req.url.startsWith("/api/speech/import-model") && req.method === "POST") {
try {
const parsed = new URL(req.url, `http://${req.headers.host || "localhost"}`);
const result = await streamModelUpload(req, parsed.searchParams.get("filename"), SPEECH_MODELS, "speech");
return json(res, 200, { ok: true, message: `Imported ${result.filename}`, model: result });
} catch (err) {
console.error(" [api] Failed to import speech model:", err);
return json(res, 500, { ok: false, error: err.message });
}
}
if (req.url === "/api/speech/delete-model" && req.method === "POST") {
const body = await readJsonBody(req, res);
if (!body) return;
const filename = path.basename(String(body.filename || ""));
const modelPath = path.join(SPEECH_MODELS, filename);
if (!filename || !pathInside(modelPath, SPEECH_MODELS)) {
return json(res, 400, { ok: false, error: "Invalid filename" });
}
if (speechSettings.model === filename) await stopSpeech();
try {
fs.unlinkSync(modelPath);
return json(res, 200, { ok: true });
} catch (err) {
return json(res, err.code === "ENOENT" ? 404 : 500, { ok: false, error: err.code === "ENOENT" ? "Speech model not found" : err.message });
}
}
if (req.url === "/api/speech/transcriptions" && req.method === "GET") {
return json(res, 200, { ok: true, transcriptions: listTranscriptions() });
}
function isOomError(msg) {
if (!msg) return false;
const lower = String(msg).toLowerCase();
@@ -4855,7 +5309,7 @@ async function getLlmfitRecommendations(useCase = "chat", limit = 10) {
if (req.url.startsWith("/api/llm/import-model") && req.method === "POST") {
try {
const parsed = new URL(req.url, `http://${req.headers.host || "localhost"}`);
const result = await streamModelUpload(req, parsed.searchParams.get("filename"), LLM_MODELS, true);
const result = await streamModelUpload(req, parsed.searchParams.get("filename"), LLM_MODELS, "text");
return json(res, 200, { ok: true, message: `Imported ${result.filename}`, model: result });
} catch (err) {
console.error(" [api] Failed to import text model:", err);
@@ -4942,6 +5396,7 @@ server.listen(PORT_FRONTEND, "0.0.0.0", () => {
console.log(" Frontend : http://localhost:" + PORT_FRONTEND);
console.log(" Image API: http://127.0.0.1:" + PORT_BACKEND);
console.log(" Text API : starts on http://127.0.0.1:" + PREFERRED_LLM_PORT);
console.log(" Speech : managed locally, API on frontend port");
console.log(" ============================================================");
console.log("");
@@ -4950,5 +5405,5 @@ server.listen(PORT_FRONTEND, "0.0.0.0", () => {
});
// Graceful shutdown
process.on("SIGINT", async () => { await killBackend(); await killOpenVinoWorker(); await killLlm(); process.exit(0); });
process.on("SIGTERM", async () => { await killBackend(); await killOpenVinoWorker(); await killLlm(); process.exit(0); });
process.on("SIGINT", async () => { await killBackend(); await killOpenVinoWorker(); await killLlm(); await stopSpeech(); process.exit(0); });
process.on("SIGTERM", async () => { await killBackend(); await killOpenVinoWorker(); await killLlm(); await stopSpeech(); process.exit(0); });