Skip to content

Commit c66c9a4

Browse files
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/<name>.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>
1 parent 9140c0e commit c66c9a4

5 files changed

Lines changed: 65 additions & 10 deletions

File tree

skills-manifest.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"files": 138
77
},
88
"faceless-explainer": {
9-
"hash": "f37cb1dba2890b79",
9+
"hash": "7d587ff36c975d9a",
1010
"files": 24
1111
},
1212
"figma": {
@@ -62,12 +62,12 @@
6262
"files": 132
6363
},
6464
"pr-to-video": {
65-
"hash": "8ea0227e18ab5fc6",
65+
"hash": "14250b018114d26d",
6666
"files": 30
6767
},
6868
"product-launch-video": {
69-
"hash": "e89a8d11d99b4edf",
70-
"files": 28
69+
"hash": "12c0895d4963aa90",
70+
"files": 29
7171
},
7272
"remotion-to-hyperframes": {
7373
"hash": "bf184a65059b95e8",

skills/faceless-explainer/scripts/lib/assets.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export function basenamesFromCandidates(value) {
1717
}
1818

1919
// Copy each frame's asset_candidates from capture/{assets,assets/videos,
20-
// screenshots} into assets/. Already-staged files are left as is (first-wins),
21-
// so calling this twice is safe. Returns { staged, wanted, anomalies }.
20+
// assets/svgs, screenshots} into assets/. Already-staged files are left as is
21+
// (first-wins), so calling this twice is safe. Returns { staged, wanted, anomalies }.
2222
export function stageAssets({ hyperframesDir, frames }) {
2323
const wanted = new Set();
2424
for (const f of frames) {
@@ -27,6 +27,7 @@ export function stageAssets({ hyperframesDir, frames }) {
2727
const captureDirs = [
2828
join(hyperframesDir, "capture/assets"),
2929
join(hyperframesDir, "capture/assets/videos"), // videos download into a subdir
30+
join(hyperframesDir, "capture/assets/svgs"), // inline SVGs extract into a subdir
3031
join(hyperframesDir, "capture/screenshots"),
3132
];
3233
const assetsDir = join(hyperframesDir, "assets");

skills/pr-to-video/scripts/lib/assets.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export function basenamesFromCandidates(value) {
1717
}
1818

1919
// Copy each frame's asset_candidates from capture/{assets,assets/videos,
20-
// screenshots} into assets/. Already-staged files are left as is (first-wins),
21-
// so calling this twice is safe. Returns { staged, wanted, anomalies }.
20+
// assets/svgs, screenshots} into assets/. Already-staged files are left as is
21+
// (first-wins), so calling this twice is safe. Returns { staged, wanted, anomalies }.
2222
export function stageAssets({ hyperframesDir, frames }) {
2323
const wanted = new Set();
2424
for (const f of frames) {
@@ -27,6 +27,7 @@ export function stageAssets({ hyperframesDir, frames }) {
2727
const captureDirs = [
2828
join(hyperframesDir, "capture/assets"),
2929
join(hyperframesDir, "capture/assets/videos"), // videos download into a subdir
30+
join(hyperframesDir, "capture/assets/svgs"), // inline SVGs extract into a subdir
3031
join(hyperframesDir, "capture/screenshots"),
3132
];
3233
const assetsDir = join(hyperframesDir, "assets");

skills/product-launch-video/scripts/lib/assets.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export function basenamesFromCandidates(value) {
1717
}
1818

1919
// Copy each frame's asset_candidates from capture/{assets,assets/videos,
20-
// screenshots} into assets/. Already-staged files are left as is (first-wins),
21-
// so calling this twice is safe. Returns { staged, wanted, anomalies }.
20+
// assets/svgs, screenshots} into assets/. Already-staged files are left as is
21+
// (first-wins), so calling this twice is safe. Returns { staged, wanted, anomalies }.
2222
export function stageAssets({ hyperframesDir, frames }) {
2323
const wanted = new Set();
2424
for (const f of frames) {
@@ -27,6 +27,7 @@ export function stageAssets({ hyperframesDir, frames }) {
2727
const captureDirs = [
2828
join(hyperframesDir, "capture/assets"),
2929
join(hyperframesDir, "capture/assets/videos"), // videos download into a subdir
30+
join(hyperframesDir, "capture/assets/svgs"), // inline SVGs extract into a subdir
3031
join(hyperframesDir, "capture/screenshots"),
3132
];
3233
const assetsDir = join(hyperframesDir, "assets");
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import assert from "node:assert/strict";
2+
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import test from "node:test";
6+
import { stageAssets } from "./lib/assets.mjs";
7+
8+
// ── captured SVGs are stageable ──────────────────────────────────────────────
9+
// Regression: `hyperframes capture` extracts inline SVGs into capture/assets/svgs/
10+
// (assetDownloader.ts), and the capture manifest advertises them to the agent as
11+
// `assets/svgs/<name>.svg`, so a frame names one in `asset_candidates` exactly as
12+
// it names a screenshot. stageAssets only searched capture/{assets,assets/videos,
13+
// screenshots}, so every captured SVG resolved to nothing — reported as a
14+
// non-fatal anomaly, and the frame 404'd the logo it had been told to use.
15+
16+
function projectWithCapturedSvg() {
17+
const dir = mkdtempSync(join(tmpdir(), "product-launch-stage-assets-"));
18+
mkdirSync(join(dir, "capture/assets/svgs"), { recursive: true });
19+
mkdirSync(join(dir, "capture/screenshots"), { recursive: true });
20+
writeFileSync(join(dir, "capture/assets/svgs/brand-mark.svg"), "<svg/>");
21+
writeFileSync(join(dir, "capture/screenshots/hero.png"), "png");
22+
return dir;
23+
}
24+
25+
const frames = [
26+
{ extra: { asset_candidates: "assets/svgs/brand-mark.svg — the mark; assets/hero.png — hero" } },
27+
];
28+
29+
test("stages an SVG that capture wrote into capture/assets/svgs/", () => {
30+
const dir = projectWithCapturedSvg();
31+
32+
const { staged, wanted, anomalies } = stageAssets({ hyperframesDir: dir, frames });
33+
34+
assert.equal(wanted.size, 2);
35+
assert.equal(staged, 2, `expected both assets staged, got anomalies: ${anomalies.join("; ")}`);
36+
assert.deepEqual(anomalies, []);
37+
assert.ok(existsSync(join(dir, "assets/brand-mark.svg")));
38+
assert.ok(existsSync(join(dir, "assets/hero.png")));
39+
});
40+
41+
test("still reports an asset that exists nowhere under capture/", () => {
42+
const dir = projectWithCapturedSvg();
43+
44+
const { staged, anomalies } = stageAssets({
45+
hyperframesDir: dir,
46+
frames: [{ extra: { asset_candidates: "assets/svgs/absent.svg — never captured" } }],
47+
});
48+
49+
assert.equal(staged, 0);
50+
assert.equal(anomalies.length, 1);
51+
assert.match(anomalies[0], /absent\.svg/);
52+
});

0 commit comments

Comments
 (0)