var express = require('express'); var router = express.Router(); const sessionChecker = require("../middleware/sessionChecker.js"); const MAX_BUFFER_SIZE = 300; const DEFAULT_INITIAL_LIMIT = 40; const MAX_INITIAL_LIMIT = 120; const CAPTION_META_KEYS = new Set(["sequence", "createdAt", "original"]); const liveCaptionState = { startedAt: Date.now(), latestSequence: 0, captions: [], }; const normalizeLang = (lang = "") => { const value = String(lang || "").trim().toLowerCase(); if (!value) return ""; const base = value.split(",")[0].split("-")[0].trim(); return base || value; }; const normalizeTranslations = (translations) => { if (!translations || typeof translations !== "object" || Array.isArray(translations)) return {}; const normalized = {}; for (const [langKey, translatedText] of Object.entries(translations)) { const lang = normalizeLang(langKey); const text = typeof translatedText === "string" ? translatedText.trim() : ""; if (!lang || !text) continue; normalized[lang] = text; } return normalized; }; const buildTranslationsFromFlatPayload = (payload) => { const ignoredKeys = new Set(["original", "sourceLang", "translations"]); const normalized = {}; for (const [key, value] of Object.entries(payload || {})) { if (ignoredKeys.has(key)) continue; const lang = normalizeLang(key); const text = typeof value === "string" ? value.trim() : ""; if (!lang || !text) continue; normalized[lang] = text; } return normalized; }; const inferSourceLangFromTranslations = (original, translations) => { const normalizedOriginal = String(original || "").trim(); if (!normalizedOriginal) return "original"; for (const [lang, text] of Object.entries(translations || {})) { if (String(text || "").trim() === normalizedOriginal) return normalizeLang(lang); } return "original"; }; const getAvailableLanguages = () => { const langs = new Set(); for (const caption of liveCaptionState.captions) { Object.keys(caption || {}).forEach((key) => { if (CAPTION_META_KEYS.has(key)) return; const lang = normalizeLang(key); if (lang) langs.add(lang); }); } return Array.from(langs).filter(Boolean).sort(); }; router.get("/stream", sessionChecker, async (req, res) => { try { const sinceSequence = Number.parseInt(req.query?.sinceSequence, 10); const requestedLimit = Number.parseInt(req.query?.limit, 10); const initialLimit = Number.isFinite(requestedLimit) ? Math.max(1, Math.min(requestedLimit, MAX_INITIAL_LIMIT)) : DEFAULT_INITIAL_LIMIT; let captions = []; if (Number.isFinite(sinceSequence) && sinceSequence >= 0) { captions = liveCaptionState.captions.filter((item) => item.sequence > sinceSequence); } else { captions = liveCaptionState.captions.slice(-initialLimit); } return res.json({ status: "ok", latestSequence: liveCaptionState.latestSequence, startedAt: new Date(liveCaptionState.startedAt).toISOString(), availableLanguages: getAvailableLanguages(), captions, }); } catch (error) { console.error("Error getting live captions stream", error); return res.status(500).json({ status: "Internal server error", latestSequence: liveCaptionState.latestSequence, captions: [], availableLanguages: [], }); } }); router.post("/ingest", async (req, res) => { try { // TODO: Add basic auth/API key validation before production roll-out. const original = typeof req.body?.original === "string" ? req.body.original.trim() : ""; const mapFromNested = normalizeTranslations(req.body?.translations); const mapFromFlat = buildTranslationsFromFlatPayload(req.body); const translations = { ...mapFromNested, ...mapFromFlat }; const sourceLang = normalizeLang(req.body?.sourceLang || inferSourceLangFromTranslations(original, translations)); if (!original) { return res.status(400).json({ status: "Original text is required" }); } if (sourceLang && sourceLang !== "original" && !translations[sourceLang]) { translations[sourceLang] = original; } const sequence = liveCaptionState.latestSequence + 1; const caption = { sequence, createdAt: new Date().toISOString(), original, ...translations, }; liveCaptionState.latestSequence = sequence; liveCaptionState.captions.push(caption); if (liveCaptionState.captions.length > MAX_BUFFER_SIZE) { liveCaptionState.captions.splice(0, liveCaptionState.captions.length - MAX_BUFFER_SIZE); } return res.json({ status: "ok", caption, latestSequence: liveCaptionState.latestSequence, availableLanguages: getAvailableLanguages(), }); } catch (error) { console.error("Error ingesting live captions", error); return res.status(500).json({ status: "Internal server error" }); } }); router.post("/reset", async (_, res) => { try { // TODO: Add admin authorization before exposing this endpoint. liveCaptionState.startedAt = Date.now(); liveCaptionState.latestSequence = 0; liveCaptionState.captions = []; return res.json({ status: "ok" }); } catch (error) { console.error("Error resetting live captions state", error); return res.status(500).json({ status: "Internal server error" }); } }); module.exports = router;