From 619836227820a1eebd27d749490fbd3051216ab5 Mon Sep 17 00:00:00 2001 From: Miao Yang Date: Tue, 30 Jun 2026 09:58:55 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(skills):=20storyboard=20review=20?= =?UTF-8?q?=E2=80=94=20bg-on-clip=20rule,=20slideshow=20output,=20parser?= =?UTF-8?q?=20parity=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the storyboard-angle review (jrusso1020): - M1 (invisible text): frame-worker.md (x3) + SKILL.md Step 5 (x3) now require a frame's full-bleed background on a class=clip layer, never the #root / data-composition-id element (the root is clip-gated to its scene window, so a background on it is not a dependable ground and dark text can land on the black host body). The assembler already paints frame.md's canvas onto index #root as the base ground; the per-frame clip rides on top. - B3 (slideshow truncates to slide 1): slideshow/SKILL.md gains an Output section (decks render via 'present'; 'render index.html' captures only the first composition; linear main-line MP4 export is deferred). - Parser drift: vendoredParity.test.ts guards the three vendored storyboard.mjs copies (byte-identical + parse-parity with @hyperframes/core). - skills-manifest.json regenerated for the edited SKILL.md files. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/storyboard/vendoredParity.test.ts | 71 +++++++++++++++++++ skills-manifest.json | 8 +-- skills/faceless-explainer/SKILL.md | 2 + .../sub-agents/frame-worker.md | 1 + skills/pr-to-video/SKILL.md | 2 + skills/pr-to-video/sub-agents/frame-worker.md | 1 + skills/product-launch-video/SKILL.md | 2 + .../sub-agents/frame-worker.md | 1 + skills/slideshow/SKILL.md | 6 ++ 9 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/storyboard/vendoredParity.test.ts diff --git a/packages/core/src/storyboard/vendoredParity.test.ts b/packages/core/src/storyboard/vendoredParity.test.ts new file mode 100644 index 0000000000..15413042b2 --- /dev/null +++ b/packages/core/src/storyboard/vendoredParity.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { parseStoryboard as coreParse } from "./parseStoryboard.js"; + +// Sync guard for the hand-vendored parser. +// +// Each standalone skill ships a plain-JS copy of this parser at +// scripts/lib/storyboard.mjs — skills install via `npx skills add`, where a +// script can't reach @hyperframes/core (and the core export is .ts that node +// can't load). The copies MUST stay in lockstep with core. This test fails the +// moment they drift, so a parser change can't silently leave the skills behind: +// 1. every vendored copy is byte-identical to the others, and +// 2. a vendored copy parses a representative storyboard identically to core. +// If you change the parser, update every path below in the same commit. +const VENDORED_RELATIVE = [ + "../../../../skills/faceless-explainer/scripts/lib/storyboard.mjs", + "../../../../skills/pr-to-video/scripts/lib/storyboard.mjs", + "../../../../skills/product-launch-video/scripts/lib/storyboard.mjs", +]; + +const vendoredUrls = VENDORED_RELATIVE.map((rel) => new URL(rel, import.meta.url)); + +// Exercises frontmatter (known keys + an unknown global), H2/H3 frame headings, +// every recognized meta key + an alias (transition / vo / description), an unknown +// status (warning + stash under extra), an unparseable duration (warning), unknown +// extra keys, and free-form narrative — the surfaces most likely to drift. +const SAMPLE = `--- +format: 1080x1920 +message: "Ship it" +arc: Hook → Proof → CTA +audience: builders +campaign: spring +--- + +## Frame 1 — Hook +- duration: 3s +- status: animated +- transition: cut +- vo: "Open cold." +- src: compositions/frames/01-hook.html +- poster: 2s +- effect: shimmer + +Open on the promise. + +### Beat 2 — Detail +- duration: four +- status: glowing +- description: a quiet hold +- voiceover: "The payoff." + +Land it. +`; + +describe("vendored storyboard parser parity", () => { + it("every vendored copy is byte-identical", () => { + const bodies = vendoredUrls.map((u) => readFileSync(fileURLToPath(u), "utf8")); + for (let i = 1; i < bodies.length; i++) { + expect( + bodies[i], + `${VENDORED_RELATIVE[i]} drifted from ${VENDORED_RELATIVE[0]} — re-vendor it`, + ).toBe(bodies[0]); + } + }); + + it("a vendored copy parses identically to core", async () => { + const vendored = await import(vendoredUrls[0].href); + expect(vendored.parseStoryboard(SAMPLE)).toEqual(coreParse(SAMPLE)); + }); +}); diff --git a/skills-manifest.json b/skills-manifest.json index acb94dd4a1..12e5474423 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -6,7 +6,7 @@ "files": 144 }, "faceless-explainer": { - "hash": "b94e52c77d04c1ea", + "hash": "edbe47dd738d14d8", "files": 17 }, "general-video": { @@ -54,11 +54,11 @@ "files": 132 }, "pr-to-video": { - "hash": "85a7577aee41bb82", + "hash": "ef4a3aa5a943aeec", "files": 21 }, "product-launch-video": { - "hash": "5341508d185c091d", + "hash": "b7bee220096f2ae2", "files": 18 }, "remotion-to-hyperframes": { @@ -66,7 +66,7 @@ "files": 70 }, "slideshow": { - "hash": "ae8d8f3093ef0c04", + "hash": "2f006c93745fcc32", "files": 2 }, "talking-head-recut": { diff --git a/skills/faceless-explainer/SKILL.md b/skills/faceless-explainer/SKILL.md index 30667133a6..6a80d44e9e 100644 --- a/skills/faceless-explainer/SKILL.md +++ b/skills/faceless-explainer/SKILL.md @@ -128,6 +128,8 @@ Before dispatch, read `sub-agents/frame-worker.md` and `../hyperframes-core/refe Each worker context must include `PROJECT_DIR`, `frame_id`, canvas size, caption status and keep-out band if captions are enabled, and `RULES_DIR` as the absolute path to this skill's `../hyperframes-animation/rules/`. Each worker reads `frame.md`, its own `## Frame N` block from `STORYBOARD.md`, the local rule recipe (`../hyperframes-animation/rules/.md`) for each cited motion, and the frame's blueprint template (`../hyperframes-animation/blueprints/.md`). Each worker writes only `compositions/frames/NN-*.html`. Workers must never edit `STORYBOARD.md`. +**Full-bleed backgrounds ride on a `class="clip"` layer, never the `#root`.** A frame's ground (color field / gradient / grid) is its own full-duration background clip — a `background` set on the `#root` / `data-composition-id` element is clip-gated to the frame's window and is not a dependable ground, so dark content can land on the black host `body` and render invisible. The video's base ground is painted by the assembler from `frame.md`'s `canvas` color onto the index `#root`. (Full rule + self-check: `sub-agents/frame-worker.md`.) + As each worker returns, the orchestrator marks that frame as `animated` in `STORYBOARD.md`. After audio timings exist, build captions in the background and assemble the index: diff --git a/skills/faceless-explainer/sub-agents/frame-worker.md b/skills/faceless-explainer/sub-agents/frame-worker.md index ac7fa0bb93..1528410b67 100644 --- a/skills/faceless-explainer/sub-agents/frame-worker.md +++ b/skills/faceless-explainer/sub-agents/frame-worker.md @@ -62,6 +62,7 @@ You **can't** meaningfully run `hyperframes lint` / `validate` / `inspect` here: - `missing_template_wrapper` / `missing_composition_id` — root is `