#!/usr/bin/env node // transitions.mjs — inter-scene transition injector behind one subcommand dispatcher. // // inject — wrapper overlap + GSAP stamp on the index.html clip wrappers // verify — deterministic gate over the injector's output // // Each subcommand reads its args from process.argv AFTER the subcommand token // (i.e. process.argv.slice(3)): // node transitions.mjs inject --group-spec ./group_spec.json --hyperframes . // node transitions.mjs verify --group-spec ./group_spec.json --index ./index.html import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { transitionsByName } from "./lib/transition-registry.mjs"; import { readDims } from "./lib/dimensions.mjs"; // =========================================================================== // inject ← inject-transitions.mjs async function runInject(argv) { const flag = (name, def) => { const i = argv.indexOf(`--${name}`); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def; }; function die(msg) { console.error(`✗ inject-transitions.mjs: ${msg}`); process.exit(1); } const groupSpecPath = resolve(flag("group-spec", "./group_spec.json")); const hyperframesDir = resolve(flag("hyperframes", ".")); const indexPath = join(hyperframesDir, "index.html"); if (!existsSync(groupSpecPath)) die(`group_spec.json not found at ${groupSpecPath}`); if (!existsSync(indexPath)) die(`index.html not found at ${indexPath} — run assemble-index.mjs first`); let spec; try { spec = JSON.parse(readFileSync(groupSpecPath, "utf8")); } catch (e) { die(`group_spec.json parse: ${e.message}`); } // Slide transitions push a wrapper fully off-canvas, so the travel distance is // the canvas dimension along the slide axis (read from group_spec; landscape // default for pre-dims specs). const { width: CANVAS_W, height: CANVAS_H } = readDims(spec); const transitions = Array.isArray(spec.transitions) ? spec.transitions : []; const injectable = transitions.filter((t) => t && t.type); let html = readFileSync(indexPath, "utf8"); if (injectable.length === 0) { console.log(`✓ inject-transitions: 0 transitions to inject — index.html unchanged`); process.exit(0); } let txByName; try { txByName = transitionsByName(); } catch (e) { die(`transition registry: ${e.message}`); } // The authoritative SCENE id set — from group_spec.groups[].scene_ids. The // captions / voice / bgm / sfx clips also use id="el-…" + (for captions) // data-composition-src, but they are NOT scenes: they must keep their own tracks // (10/11/12/20+) and must NOT join the scene ping-pong. Driving off the known // scene list (not a data-composition-src regex) is what keeps captions out. const sceneIds = new Set(); for (const g of spec.groups || []) for (const sid of g.scene_ids || []) sceneIds.add(sid); if (sceneIds.size === 0) die(`group_spec.groups[].scene_ids is empty — cannot identify scene clips`); // ---------- parse SCENE clip wrappers out of index.html ---------- // assemble-index emits each scene clip as a stable multi-line block: //
// We only keep clips whose sid is a known scene (sceneIds) — captions (el-captions) // and audio (el-