diff --git a/skills-manifest.json b/skills-manifest.json index 12d4925207..704d8d4d26 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -6,8 +6,8 @@ "files": 140 }, "faceless-explainer": { - "hash": "72acdcb31403531d", - "files": 23 + "hash": "261a9740ec1378b0", + "files": 24 }, "figma": { "hash": "517e4dc53c13ea05", diff --git a/skills/faceless-explainer/scripts/transitions.mjs b/skills/faceless-explainer/scripts/transitions.mjs index 2c2c568eab..adfd893309 100644 --- a/skills/faceless-explainer/scripts/transitions.mjs +++ b/skills/faceless-explainer/scripts/transitions.mjs @@ -117,16 +117,18 @@ function extendFrameTail(hyperframesDir, frame, baseDuration, targetDuration, di let extended = 0; const rewritten = html.replace(/<([A-Za-z][\w:-]*)\b([^>]*)>/g, (tag, name, attrs) => { const durationMatch = attrs.match(/\bdata-duration="([\d.]+)"/); - if (!durationMatch) return tag; - const duration = Number(durationMatch[1]); - if (!Number.isFinite(duration)) return tag; - const compositionMatch = attrs.match(/\bdata-composition-id="([^"]+)"/); if (compositionMatch?.[1] === compId && !foundRoot) { foundRoot = true; - return tag.replace(/\bdata-duration="[\d.]+"/, `data-duration="${targetDuration}"`); + return durationMatch + ? tag.replace(/\bdata-duration="[\d.]+"/, `data-duration="${targetDuration}"`) + : tag.replace(/(\s*\/?>)$/, ` data-duration="${targetDuration}"$1`); } + if (!durationMatch) return tag; + const duration = Number(durationMatch[1]); + if (!Number.isFinite(duration)) return tag; + if (name.toLowerCase() === "audio") return tag; const startMatch = attrs.match(/\bdata-start="([\d.]+)"/); if (!startMatch) return tag; diff --git a/skills/faceless-explainer/scripts/transitions.test.mjs b/skills/faceless-explainer/scripts/transitions.test.mjs new file mode 100644 index 0000000000..5cb3a14518 --- /dev/null +++ b/skills/faceless-explainer/scripts/transitions.test.mjs @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { spawnSync } from "node:child_process"; +import test from "node:test"; + +const script = new URL("./transitions.mjs", import.meta.url).pathname; + +function write(filePath, contents) { + mkdirSync(dirname(filePath), { recursive: true }); + writeFileSync(filePath, contents); +} + +test("inject extends a documented frame root that omits data-duration", (t) => { + const project = mkdtempSync(join(tmpdir(), "faceless-transitions-")); + t.after(() => rmSync(project, { force: true, recursive: true })); + + write( + join(project, "STORYBOARD.md"), + `--- +format: 1920x1080 +--- + +## Frame 1 — First + +- duration: 2s +- transition_in: cut +- status: animated +- src: compositions/frames/01-a.html + +## Frame 2 — Second + +- duration: 2s +- transition_in: crossfade +- status: animated +- src: compositions/frames/02-b.html +`, + ); + write( + join(project, "index.html"), + ` +
+
+
+ +`, + ); + write( + join(project, "compositions/frames/01-a.html"), + ``, + ); + write( + join(project, "compositions/frames/02-b.html"), + ``, + ); + + const result = spawnSync( + process.execPath, + [script, "inject", "--storyboard", join(project, "STORYBOARD.md"), "--hyperframes", project], + { encoding: "utf8" }, + ); + + assert.equal(result.status, 0, result.stderr); + const outgoing = readFileSync(join(project, "compositions/frames/01-a.html"), "utf8"); + assert.match(outgoing, /data-composition-id="01-a" data-duration="2.5"/); + assert.match(outgoing, /class="clip"[^>]*data-duration="2.5"/); +});