const express = require("express"); const fs = require("fs"); const path = require("path"); const { DeviceStore } = require("./backend/companion/device_store"); const { CompanionGateway } = require("./backend/companion/gateway"); const { RevisionStore } = require("./backend/config/revision_store"); const { CompanionCaptionDeliveryAdapter } = require("./backend/delivery/caption_delivery"); const { JsonlDiagnosticLog } = require("./backend/logs/jsonl_log"); const { ArtifactManager } = require("./backend/models/artifact_manager"); const { SessionCoordinator } = require("./backend/sessions/session_coordinator"); const { WhisperWorkerSupervisor, WhisperCppServerProvider } = require("./backend/transcription/provider"); const { ensureDataDirs, dataPath } = require("./backend/paths"); const modelManifest = require("./models_manifest.json"); const runtimeManifest = require("./runtime_manifest.json"); const manifest = require("./plugin.json"); const PLUGIN_ID = "lumi_transcription"; module.exports = { id: PLUGIN_ID, init({ web, db, logger }) { ensureDataDirs(); const allowInsecure = process.env.LUMI_COMPANION_DEV_ALLOW_INSECURE === "1"; const devices = new DeviceStore(db, { allowInsecure }); const revisions = new RevisionStore(db); const diagnosticLog = new JsonlDiagnosticLog(dataPath("logs")); diagnosticLog.cleanup(); const cleanupTimer = setInterval(() => diagnosticLog.cleanup(), 60 * 60 * 1000); cleanupTimer.unref?.(); const supervisor = new WhisperWorkerSupervisor({ executable: process.env.LUMI_TRANSCRIPTION_WORKER || "", args: process.env.LUMI_TRANSCRIPTION_WORKER_ARGS ? JSON.parse(process.env.LUMI_TRANSCRIPTION_WORKER_ARGS) : [] }); supervisor.on("diagnostic", (entry) => diagnosticLog.append({ kind: "worker", ...entry })); supervisor.on("error", (error) => diagnosticLog.append({ kind: "worker", state: "error", message: error.message })); const provider = new WhisperCppServerProvider(supervisor); const sessions = new SessionCoordinator({ provider, deliveryFactory: (send) => new CompanionCaptionDeliveryAdapter(send), log: diagnosticLog }); const gateway = new CompanionGateway({ devices, sessions, log: diagnosticLog, allowInsecure }); const unregisterUpgrade = web.addUpgradeHandler("/plugins/lumi_transcription/live", (request, socket, head) => gateway.upgrade(request, socket, head)); const models = new ArtifactManager(dataPath("models")); const runtimeArchives = new ArtifactManager(dataPath("tmp")); const runtimes = new ArtifactManager(dataPath("runtime")); const selectedModelId = revisions.list().selected_model_id?.value; const selectedModel = modelManifest.models.find((entry) => entry.id === selectedModelId); if (supervisor.executable && selectedModel) { const selectedStatus = models.status(selectedModel); if (selectedStatus.valid) provider.loadModel({ ...selectedModel, path: selectedStatus.path }) .catch((error) => diagnosticLog.append({ kind: "worker", state: "model_restore_failed", model_id: selectedModel.id, message: error.message })); } const router = web.createRouter(); router.use("/assets", express.static(path.join(__dirname, "public"))); router.get("/", requireAdmin, async (_req, res) => { res.render(path.join(__dirname, "views", "settings.ejs"), { title: "Lumi Transcription", pluginVersion: manifest.version, providerHealth: await provider.health(), devices: devices.list(), settings: revisions.list(), models: modelManifest.models.map((entry) => ({ ...entry, status: models.status(entry) })), runtimeManifest, logs: diagnosticLog.files() }); }); router.get("/api/status", requireAdmin, async (_req, res) => res.json({ ok: true, plugin: { id: PLUGIN_ID, version: manifest.version }, protocol_version: 1, provider: await provider.health(), devices: devices.list(), settings: revisions.list(), models: modelManifest.models.map((entry) => ({ id: entry.id, ...models.status(entry) })), runtime: runtimeManifest })); router.post("/api/pairing-package", requireAdmin, (req, res) => { try { const host = requestHost(req); const pairing = devices.issuePairing({ userId: req.session.user.id, host }); const bootstrap = { format: "lumi-companion-bootstrap-v1", ...pairing, exchange_url: `${host}/plugins/${PLUGIN_ID}/api/pair` }; res.set("Cache-Control", "no-store"); res.attachment(`lumi-companion-${pairing.pairing_id}.lumi-pairing.json`); res.send(`${JSON.stringify(bootstrap, null, 2)}\n`); } catch (error) { res.status(400).json({ ok: false, error: error.message }); } }); router.post("/api/pair", requireSecureRequest(allowInsecure), (req, res) => { try { res.set("Cache-Control", "no-store"); res.status(201).json({ ok: true, ...devices.exchange(req.body || {}) }); } catch (error) { res.status(error.code === "PAIRING_ALREADY_USED" ? 409 : 400).json({ ok: false, code: error.code, error: error.message }); } }); router.get("/api/devices", requireAdmin, (_req, res) => res.json({ devices: devices.list() })); router.post("/api/devices/:id/revoke", requireAdmin, (req, res) => res.json({ ok: devices.revoke(req.params.id) })); router.post("/api/devices/:id/capabilities", requireAdmin, (req, res) => { const capabilities = devices.setCapabilities(req.params.id, req.body.capabilities); if (!capabilities) return res.status(404).json({ ok: false, error: "Device was not found or is revoked." }); res.json({ ok: true, capabilities }); }); router.get("/api/settings", requireSettingsAccess(devices, allowInsecure), (_req, res) => res.json({ fields: revisions.list() })); router.patch("/api/settings", requireSettingsAccess(devices, allowInsecure), (req, res) => { try { const actor = req.session?.user?.id || req.lumiDevice?.id; const result = revisions.apply(req.body.changes, actor); if (result.applied.length) web.emitEvent?.("transcription:settings_changed", { fields: result.applied.map((entry) => entry.key) }, { role: "admin" }); res.status(result.conflicts.length ? 409 : 200).json({ ok: !result.conflicts.length, ...result }); } catch (error) { res.status(400).json({ ok: false, error: error.message }); } }); router.post("/api/models/:id/download", requireAdmin, async (req, res) => { const entry = modelManifest.models.find((candidate) => candidate.id === req.params.id); if (!entry) return res.status(404).json({ ok: false, error: "Model was not found." }); try { res.status(201).json({ ok: true, status: await models.download(entry, { confirmed: req.body.confirmed === true }) }); } catch (error) { res.status(400).json({ ok: false, error: error.message }); } }); router.post("/api/models/:id/load", requireAdmin, async (req, res) => { const entry = modelManifest.models.find((candidate) => candidate.id === req.params.id); if (!entry) return res.status(404).json({ ok: false, error: "Model was not found." }); const status = models.status(entry); if (!status.valid) return res.status(409).json({ ok: false, error: "Download and verify this model before loading it." }); try { const providerStatus = await provider.loadModel({ ...entry, path: status.path }); const current = revisions.list().selected_model_id; revisions.apply([{ key: "selected_model_id", value: entry.id, base_revision: current?.revision || 0 }], req.session.user.id); res.json({ ok: true, status: providerStatus }); } catch (error) { res.status(500).json({ ok: false, error: error.message }); } }); router.post("/api/runtime/:id/install", requireAdmin, async (req, res) => { const entry = runtimeManifest.artifacts.find((candidate) => candidate.id === req.params.id); if (!entry) return res.status(404).json({ ok: false, error: "Runtime was not found." }); try { const filename = `${entry.id}.zip`; const archive = await runtimeArchives.download({ ...entry, filename }, { confirmed: req.body.confirmed === true }); res.status(201).json({ ok: true, status: runtimes.installZip(entry, archive.path) }); } catch (error) { res.status(400).json({ ok: false, error: error.message }); } }); router.get("/api/logs", requireAdmin, (_req, res) => res.json({ files: diagnosticLog.files().map(({ path: _path, ...entry }) => entry) })); web.mount(`/plugins/${PLUGIN_ID}`, router, { label: "Transcription", role: "admin", section: "plugins" }); global.lumiFrameworks = global.lumiFrameworks || {}; global.lumiFrameworks.transcription = { version: manifest.version, protocol_version: 1, health: () => provider.health() }; return async () => { clearInterval(cleanupTimer); unregisterUpgrade(); await gateway.close(); await sessions.close(); await provider.stop(); if (global.lumiFrameworks?.transcription?.version === manifest.version) delete global.lumiFrameworks.transcription; logger?.info?.("Lumi transcription stopped", {}, { event: "transcription_stopped" }); }; } }; function requireAdmin(req, res, next) { if (req.session?.user?.isAdmin) return next(); return res.status(403).json({ error: "Administrator access is required." }); } function requireSettingsAccess(devices, allowInsecure) { return (req, res, next) => { if (req.session?.user?.isAdmin) return next(); const forwarded = String(req.headers["x-forwarded-proto"] || "").split(",")[0].trim(); const local = ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(req.socket.remoteAddress); if (!req.secure && forwarded !== "https" && !(allowInsecure && local)) return res.status(426).json({ error: "Device settings synchronization requires HTTPS." }); const auth = devices.authenticate(req.headers.authorization, "transcription.settings.v1"); if (!auth.allowed) return res.status(401).json({ error: "A paired device credential is required." }); req.lumiDevice = auth.device; next(); }; } function requestHost(req) { const forwarded = String(req.headers["x-forwarded-proto"] || "").split(",")[0].trim(); const protocol = forwarded === "https" ? "https" : req.protocol; return `${protocol}://${req.get("host")}`; } function requireSecureRequest(allowInsecure) { return (req, res, next) => { const forwarded = String(req.headers["x-forwarded-proto"] || "").split(",")[0].trim(); const local = ["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(req.socket.remoteAddress); if (req.secure || forwarded === "https" || (allowInsecure && local)) return next(); return res.status(426).json({ error: "Lumi Companion pairing requires HTTPS." }); }; }