From f7c743d82a0ccba960460cab1798146da4576bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Wed, 29 Jul 2026 00:53:40 +0000 Subject: [PATCH 1/2] fix(skills): materialize frame root durations --- .../scripts/assemble-index.mjs | 28 ++++-- .../scripts/assemble-index.test.mjs | 94 +++++++++++++++++++ .../references/frame-worker-core.md | 4 +- 3 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 skills/faceless-explainer/scripts/assemble-index.test.mjs diff --git a/skills/faceless-explainer/scripts/assemble-index.mjs b/skills/faceless-explainer/scripts/assemble-index.mjs index 8a7554500a..90d6eef6ad 100644 --- a/skills/faceless-explainer/scripts/assemble-index.mjs +++ b/skills/faceless-explainer/scripts/assemble-index.mjs @@ -32,12 +32,14 @@ // [--audio-meta audio_meta.json]. On disk: each built frame's src html, // capture/{assets,assets/videos,screenshots}/ for staging, compositions/captions.html. // Writes: /index.html + stages assets/ + (guard ① below) -// repairs a frame file in place when its root is missing data-width/height. +// repairs a frame file in place when its root is missing +// data-width/data-height/data-duration. // // Pre-assembly frame guards (run in the same pass that reads each frame, so common // `lint` failures surface HERE instead of after assembly + a wasted render): -// ① AUTO-REPAIR — a sub-comp root missing data-width/data-height: inject the canvas -// dims (the renderer needs them on the cloned root; else lint root_missing_dimensions). +// ① AUTO-REPAIR — a sub-comp root missing data-width/data-height/data-duration: +// inject the canvas dims and storyboard duration. The transition injector +// extends that static duration when it pads the outgoing frame tail. // ② HARD FAIL — a timed element (data-start+duration+track-index) that is not the root // and lacks class="clip" (shows the whole frame), or two same-track clips that overlap. // (Media inside a sub-comp is NOT a violation: the runtime seeks + decodes nested @@ -162,7 +164,7 @@ function findRootTag(html) { } // Returns { errors: string[], repairedHtml: string|null, repairNote: string|null }. -function guardFrame(html, label) { +function guardFrame(html, label, durationSeconds) { const errors = []; // Scan a copy with comments + + +`; +} + +test("assembly materializes frame root duration before transition injection", () => { + const project = mkdtempSync(join(tmpdir(), "frame-root-duration-")); + try { + write( + join(project, "STORYBOARD.md"), + `--- +format: 1920x1080 +--- + +## Frame 1 — First + +- duration: 2s +- transition_in: cut +- status: animated +- src: compositions/frames/01-first.html + +## Frame 2 — Second + +- duration: 2s +- transition_in: crossfade 0.5s +- status: animated +- src: compositions/frames/02-second.html +`, + ); + const firstPath = join(project, "compositions", "frames", "01-first.html"); + const secondPath = join(project, "compositions", "frames", "02-second.html"); + write(firstPath, frameWithoutRootDuration("01-first", 2)); + write(secondPath, frameWithoutRootDuration("02-second", 2)); + + execFileSync( + process.execPath, + [assembleScript, "--storyboard", join(project, "STORYBOARD.md"), "--hyperframes", project], + { encoding: "utf8" }, + ); + + assert.match( + readFileSync(firstPath, "utf8"), + /data-composition-id="01-first"[^>]*data-duration="2"/, + ); + assert.match( + readFileSync(secondPath, "utf8"), + /data-composition-id="02-second"[^>]*data-duration="2"/, + ); + + assert.doesNotThrow(() => + execFileSync( + process.execPath, + [ + transitionsScript, + "inject", + "--storyboard", + join(project, "STORYBOARD.md"), + "--hyperframes", + project, + ], + { encoding: "utf8", stdio: "pipe" }, + ), + ); + assert.match( + readFileSync(firstPath, "utf8"), + /data-composition-id="01-first"[^>]*data-duration="2.5"/, + ); + } finally { + rmSync(project, { recursive: true, force: true }); + } +}); diff --git a/skills/hyperframes-core/references/frame-worker-core.md b/skills/hyperframes-core/references/frame-worker-core.md index 6b1da927f2..3dbf5ade58 100644 --- a/skills/hyperframes-core/references/frame-worker-core.md +++ b/skills/hyperframes-core/references/frame-worker-core.md @@ -56,7 +56,7 @@ Shared law for every narrative frame, each load-bearing; your workflow's delta a 1. **Read** — your packet top to bottom (your frame block, the inlined blueprint, the inlined rule recipes), then `frame.md` (the look). Internalize the self-check codes below before you write — most lethal is **template transport**: every `