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 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 getAvailableLanguages = () => { const langs = new Set(); for (const caption of liveCaptionState.captions) { if (caption?.sourceLang) langs.add(caption.sourceLang); const translationMap = caption?.translations || {}; Object.keys(translationMap).forEach((lang) => langs.add(normalizeLang(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 sourceLang = normalizeLang(req.body?.sourceLang || "original"); const translations = normalizeTranslations(req.body?.translations); if (!original) { return res.status(400).json({ status: "Original text is required" }); } const sequence = liveCaptionState.latestSequence + 1; const caption = { sequence, createdAt: new Date().toISOString(), sourceLang, 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;