#!/usr/bin/env node /* * preview-frames.cjs — APPROXIMATE final frames in seconds, without rendering. * * node preview-frames.cjs [t1 t2 ...] * * For each sample time t: screenshot the caption layer(s) in headless Chromium * (index.html seeked to t; rail.html too if present), then composite in sharp: * frames_bg[t] → index captions (embed layer) → matte frames_fg[t] (subject * occludes the embed) → rail captions (in front, like the final alpha-overlay) * = a faithful preview of the composite for THAT moment, ~2s per frame instead of * a multi-minute render. Writes /preview/t.png + a contact sheet * preview/sheet.png. * * Use it BEFORE rendering: eyeball placement, occlusion, washout, text-on-text — * the failure classes the geometric gates can't judge. The QA checklist lives in * SKILL.md § Visual QA. (Video elements show poster/first-frame in the screenshot; * the REAL a-roll pixels come from frames_bg, so previews stay accurate.) * * Sample-time default: climax/groups midpoints from standard.json or plan.json, * else 25/50/75% of the clip. */ const path = require("path"); const fs = require("fs"); const os = require("os"); const HF_ROOTS = [ process.env.HYPERFRAMES_ROOT, path.resolve(__dirname, "../../.."), path.join(os.homedir(), "Downloads", "hyperframes"), ].filter(Boolean); function findInBun(root, pkg, sub) { const cands = [path.join(root, "node_modules", pkg)]; const bunDir = path.join(root, "node_modules", ".bun"); try { if (fs.existsSync(bunDir)) for (const d of fs.readdirSync(bunDir)) if (d.startsWith(pkg + "@")) cands.push(path.join(bunDir, d, "node_modules", pkg)); } catch {} for (const c of cands) { const p = sub ? path.join(c, sub) : c; if (fs.existsSync(p)) return p; } return null; } let puppeteer = null, sharp = null, gsapSource = null; for (const r of HF_ROOTS) { if (!puppeteer) { const p = findInBun(r, "puppeteer"); if (p) try { puppeteer = require(p); } catch {} } if (!sharp) { const p = findInBun(r, "sharp"); if (p) try { sharp = require(p); } catch {} } if (!gsapSource) { const g = findInBun(r, "gsap", path.join("dist", "gsap.min.js")); if (g) gsapSource = fs.readFileSync(g, "utf8"); } } if (!puppeteer || !sharp) { console.error("[preview] need puppeteer+sharp — set HYPERFRAMES_ROOT"); process.exit(0); } async function shotAt(browser, file, W, H, t) { const page = await browser.newPage(); try { await page.setViewport({ width: W, height: H, deviceScaleFactor: 1 }); // Serve the page's own CDN