From c66c9a4c76ff0681cadc8007a8e02744bbcdab10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 20 Aug 2026 16:22:20 -0400 Subject: [PATCH] fix(skills): stage SVGs that capture wrote into capture/assets/svgs/ (#3336) `hyperframes capture` extracts inline SVGs into capture/assets/svgs/, and the capture manifest advertises them to the agent as `assets/svgs/.svg`, so a frame names one in `asset_candidates` exactly the way it names a screenshot. stageAssets searched only capture/{assets,assets/videos,screenshots}, so every captured SVG resolved to nothing: logged as a non-fatal anomaly, and the frame 404'd the brand mark it had been told to use. Add the directory to the search list, and cover it with a test that fails without the fix. lib/assets.mjs is byte-identical across product-launch-video, faceless-explainer and pr-to-video, so the fix lands in all three. Folding it into hyperframes-core/scripts/lib/, where frame-packets-core.mjs already lives, is a separate change. Co-authored-by: anikam13 <22992075+anikam13@users.noreply.github.com> --- skills-manifest.json | 8 +-- .../faceless-explainer/scripts/lib/assets.mjs | 5 +- skills/pr-to-video/scripts/lib/assets.mjs | 5 +- .../scripts/lib/assets.mjs | 5 +- .../scripts/stage-assets.test.mjs | 52 +++++++++++++++++++ 5 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 skills/product-launch-video/scripts/stage-assets.test.mjs diff --git a/skills-manifest.json b/skills-manifest.json index e3860f309..b30803fbc 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -6,7 +6,7 @@ "files": 138 }, "faceless-explainer": { - "hash": "f37cb1dba2890b79", + "hash": "7d587ff36c975d9a", "files": 24 }, "figma": { @@ -62,12 +62,12 @@ "files": 132 }, "pr-to-video": { - "hash": "8ea0227e18ab5fc6", + "hash": "14250b018114d26d", "files": 30 }, "product-launch-video": { - "hash": "e89a8d11d99b4edf", - "files": 28 + "hash": "12c0895d4963aa90", + "files": 29 }, "remotion-to-hyperframes": { "hash": "bf184a65059b95e8", diff --git a/skills/faceless-explainer/scripts/lib/assets.mjs b/skills/faceless-explainer/scripts/lib/assets.mjs index 0f06ac5da..3515bf7c7 100644 --- a/skills/faceless-explainer/scripts/lib/assets.mjs +++ b/skills/faceless-explainer/scripts/lib/assets.mjs @@ -17,8 +17,8 @@ export function basenamesFromCandidates(value) { } // Copy each frame's asset_candidates from capture/{assets,assets/videos, -// screenshots} into assets/. Already-staged files are left as is (first-wins), -// so calling this twice is safe. Returns { staged, wanted, anomalies }. +// assets/svgs, screenshots} into assets/. Already-staged files are left as is +// (first-wins), so calling this twice is safe. Returns { staged, wanted, anomalies }. export function stageAssets({ hyperframesDir, frames }) { const wanted = new Set(); for (const f of frames) { @@ -27,6 +27,7 @@ export function stageAssets({ hyperframesDir, frames }) { const captureDirs = [ join(hyperframesDir, "capture/assets"), join(hyperframesDir, "capture/assets/videos"), // videos download into a subdir + join(hyperframesDir, "capture/assets/svgs"), // inline SVGs extract into a subdir join(hyperframesDir, "capture/screenshots"), ]; const assetsDir = join(hyperframesDir, "assets"); diff --git a/skills/pr-to-video/scripts/lib/assets.mjs b/skills/pr-to-video/scripts/lib/assets.mjs index 0f06ac5da..3515bf7c7 100644 --- a/skills/pr-to-video/scripts/lib/assets.mjs +++ b/skills/pr-to-video/scripts/lib/assets.mjs @@ -17,8 +17,8 @@ export function basenamesFromCandidates(value) { } // Copy each frame's asset_candidates from capture/{assets,assets/videos, -// screenshots} into assets/. Already-staged files are left as is (first-wins), -// so calling this twice is safe. Returns { staged, wanted, anomalies }. +// assets/svgs, screenshots} into assets/. Already-staged files are left as is +// (first-wins), so calling this twice is safe. Returns { staged, wanted, anomalies }. export function stageAssets({ hyperframesDir, frames }) { const wanted = new Set(); for (const f of frames) { @@ -27,6 +27,7 @@ export function stageAssets({ hyperframesDir, frames }) { const captureDirs = [ join(hyperframesDir, "capture/assets"), join(hyperframesDir, "capture/assets/videos"), // videos download into a subdir + join(hyperframesDir, "capture/assets/svgs"), // inline SVGs extract into a subdir join(hyperframesDir, "capture/screenshots"), ]; const assetsDir = join(hyperframesDir, "assets"); diff --git a/skills/product-launch-video/scripts/lib/assets.mjs b/skills/product-launch-video/scripts/lib/assets.mjs index 0f06ac5da..3515bf7c7 100644 --- a/skills/product-launch-video/scripts/lib/assets.mjs +++ b/skills/product-launch-video/scripts/lib/assets.mjs @@ -17,8 +17,8 @@ export function basenamesFromCandidates(value) { } // Copy each frame's asset_candidates from capture/{assets,assets/videos, -// screenshots} into assets/. Already-staged files are left as is (first-wins), -// so calling this twice is safe. Returns { staged, wanted, anomalies }. +// assets/svgs, screenshots} into assets/. Already-staged files are left as is +// (first-wins), so calling this twice is safe. Returns { staged, wanted, anomalies }. export function stageAssets({ hyperframesDir, frames }) { const wanted = new Set(); for (const f of frames) { @@ -27,6 +27,7 @@ export function stageAssets({ hyperframesDir, frames }) { const captureDirs = [ join(hyperframesDir, "capture/assets"), join(hyperframesDir, "capture/assets/videos"), // videos download into a subdir + join(hyperframesDir, "capture/assets/svgs"), // inline SVGs extract into a subdir join(hyperframesDir, "capture/screenshots"), ]; const assetsDir = join(hyperframesDir, "assets"); diff --git a/skills/product-launch-video/scripts/stage-assets.test.mjs b/skills/product-launch-video/scripts/stage-assets.test.mjs new file mode 100644 index 000000000..6aca45a7a --- /dev/null +++ b/skills/product-launch-video/scripts/stage-assets.test.mjs @@ -0,0 +1,52 @@ +import assert from "node:assert/strict"; +import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { stageAssets } from "./lib/assets.mjs"; + +// ── captured SVGs are stageable ────────────────────────────────────────────── +// Regression: `hyperframes capture` extracts inline SVGs into capture/assets/svgs/ +// (assetDownloader.ts), and the capture manifest advertises them to the agent as +// `assets/svgs/.svg`, so a frame names one in `asset_candidates` exactly as +// it names a screenshot. stageAssets only searched capture/{assets,assets/videos, +// screenshots}, so every captured SVG resolved to nothing — reported as a +// non-fatal anomaly, and the frame 404'd the logo it had been told to use. + +function projectWithCapturedSvg() { + const dir = mkdtempSync(join(tmpdir(), "product-launch-stage-assets-")); + mkdirSync(join(dir, "capture/assets/svgs"), { recursive: true }); + mkdirSync(join(dir, "capture/screenshots"), { recursive: true }); + writeFileSync(join(dir, "capture/assets/svgs/brand-mark.svg"), ""); + writeFileSync(join(dir, "capture/screenshots/hero.png"), "png"); + return dir; +} + +const frames = [ + { extra: { asset_candidates: "assets/svgs/brand-mark.svg — the mark; assets/hero.png — hero" } }, +]; + +test("stages an SVG that capture wrote into capture/assets/svgs/", () => { + const dir = projectWithCapturedSvg(); + + const { staged, wanted, anomalies } = stageAssets({ hyperframesDir: dir, frames }); + + assert.equal(wanted.size, 2); + assert.equal(staged, 2, `expected both assets staged, got anomalies: ${anomalies.join("; ")}`); + assert.deepEqual(anomalies, []); + assert.ok(existsSync(join(dir, "assets/brand-mark.svg"))); + assert.ok(existsSync(join(dir, "assets/hero.png"))); +}); + +test("still reports an asset that exists nowhere under capture/", () => { + const dir = projectWithCapturedSvg(); + + const { staged, anomalies } = stageAssets({ + hyperframesDir: dir, + frames: [{ extra: { asset_candidates: "assets/svgs/absent.svg — never captured" } }], + }); + + assert.equal(staged, 0); + assert.equal(anomalies.length, 1); + assert.match(anomalies[0], /absent\.svg/); +});