reframe-video 0.6.34 → 0.6.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reframe",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "description": "Create and iterate motion-graphics videos as addressable data: deterministic mp4 renders, human edits that survive AI regeneration, label-anchored audio, data-driven batch rendering.",
5
5
  "author": {
6
6
  "name": "Kiyeon Jeon",
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ // ../render-cli/src/assemble.ts
4
+ import { writeFile } from "node:fs/promises";
5
+ import { existsSync } from "node:fs";
6
+ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
7
+
8
+ // ../render-cli/src/media/probe.ts
9
+ import { spawn } from "node:child_process";
10
+ var VIDEO_EXT = /\.(mp4|mov|webm|m4v|mkv)$/i;
11
+ function run(cmd, args) {
12
+ return new Promise((res, reject) => {
13
+ const proc = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
14
+ let stdout = "", stderr = "";
15
+ proc.stdout.on("data", (d) => stdout += d.toString());
16
+ proc.stderr.on("data", (d) => stderr += d.toString());
17
+ proc.on("close", (code) => res({ code: code ?? 1, stdout, stderr }));
18
+ proc.on("error", reject);
19
+ });
20
+ }
21
+ async function probeMedia(file) {
22
+ const isVideo = VIDEO_EXT.test(file);
23
+ let out;
24
+ try {
25
+ out = await run("ffprobe", [
26
+ "-v",
27
+ "error",
28
+ "-show_entries",
29
+ "format=duration:stream=width,height",
30
+ "-of",
31
+ "json",
32
+ file
33
+ ]);
34
+ } catch {
35
+ throw new Error("ffprobe not found on PATH \u2014 install ffmpeg (macOS: brew install ffmpeg, debian: apt install ffmpeg)");
36
+ }
37
+ if (out.code !== 0) return { isVideo };
38
+ try {
39
+ const j = JSON.parse(out.stdout);
40
+ const d = j.format?.duration ? Number(j.format.duration) : NaN;
41
+ const stream = (j.streams ?? []).find((s) => s.width && s.height);
42
+ return {
43
+ isVideo,
44
+ ...Number.isFinite(d) && d > 0 ? { duration: d } : {},
45
+ ...stream ? { width: Number(stream.width), height: Number(stream.height) } : {}
46
+ };
47
+ } catch {
48
+ return { isVideo };
49
+ }
50
+ }
51
+
52
+ // ../render-cli/src/assemble.ts
53
+ var BGM_SYNTHS = ["ambient-pad", "lofi", "pulse", "tension", "uplift"];
54
+ function parseArgs(argv) {
55
+ const media = [];
56
+ const a = { media, hold: 3.5, seed: 0 };
57
+ for (let i = 0; i < argv.length; i++) {
58
+ const arg = argv[i];
59
+ const next = () => argv[++i] ?? fail(`${arg} needs a value`);
60
+ if (arg === "-o" || arg === "--out") a.out = next();
61
+ else if (arg === "--title") a.title = next();
62
+ else if (arg === "--bgm") a.bgm = next();
63
+ else if (arg === "--hold") a.hold = Number(next());
64
+ else if (arg === "--seed") a.seed = Number(next());
65
+ else if (arg.startsWith("-")) fail(`unknown flag "${arg}"`);
66
+ else media.push(arg);
67
+ }
68
+ return a;
69
+ }
70
+ function fail(msg) {
71
+ console.error(`error: ${msg}`);
72
+ process.exit(1);
73
+ }
74
+ var CWD = process.env.INIT_CWD ?? process.cwd();
75
+ var userPath = (p) => isAbsolute(p) ? p : resolve(CWD, p);
76
+ async function main() {
77
+ const args = parseArgs(process.argv.slice(2));
78
+ if (args.media.length === 0) {
79
+ fail('assemble needs at least one media file\nusage: reframe assemble <media...> [-o name] [--title "\u2026"] [--bgm <synth>] [--hold s] [--seed N]');
80
+ }
81
+ if (args.bgm && !BGM_SYNTHS.includes(args.bgm)) fail(`unknown --bgm "${args.bgm}" \u2014 valid: ${BGM_SYNTHS.join(", ")}`);
82
+ const outArg = (args.out ?? "media-story").replace(/\.ts$/, "");
83
+ const outPath = userPath(`${outArg}.ts`);
84
+ const name = basename(outPath, ".ts");
85
+ if (existsSync(outPath)) fail(`${outArg}.ts already exists`);
86
+ const outDir = dirname(outPath);
87
+ const shots = [];
88
+ for (const m of args.media) {
89
+ const abs = userPath(m);
90
+ if (!existsSync(abs)) fail(`no such file: ${abs}`);
91
+ const info = await probeMedia(abs);
92
+ const rel = relative(outDir, abs) || basename(abs);
93
+ const src = rel.split("\\").join("/");
94
+ const hold = info.isVideo && info.duration ? Math.min(10, Math.max(0.5, info.duration)) : args.hold;
95
+ const portrait = !!(info.width && info.height && info.height > info.width);
96
+ shots.push({ src, hold: Number(hold.toFixed(3)), isVideo: info.isVideo, ...portrait ? { ken: "pan" } : {} });
97
+ }
98
+ await writeFile(outPath, sceneSource(name, shots, args));
99
+ const vids = shots.filter((s) => s.isVideo).length;
100
+ console.log(`created ${name}.ts \u2014 ${shots.length} shots (${vids} video, ${shots.length - vids} image)`);
101
+ console.log(` next: reframe render ${name}.ts`);
102
+ }
103
+ function sceneSource(name, shots, args) {
104
+ const imports = ["scene", "photoMontage", "seq", "par"];
105
+ if (args.title) imports.push("title");
106
+ const shotLines = shots.map((s) => ` { src: ${JSON.stringify(s.src)}, hold: ${s.hold}${s.ken ? `, ken: ${JSON.stringify(s.ken)}` : ""} },`).join("\n");
107
+ const titleBlock = args.title ? `
108
+ const ttl = title({ text: ${JSON.stringify(args.title)}, id: "ttl", x: W / 2, y: H / 2, fontSize: 110, entrance: "cascade", exit: "dissolve", hold: 1.6 });
109
+ ` : "";
110
+ const nodes = args.title ? "[...m.nodes, ...ttl.nodes]" : "[...m.nodes]";
111
+ const timeline = args.title ? "par(m.timeline, ttl.timeline)" : "seq(m.timeline)";
112
+ const audio = args.bgm ? `
113
+ audio: { bgm: { synth: ${JSON.stringify(args.bgm)}, gain: 0.18, fadeIn: 1.2, fadeOut: 2 } },` : "";
114
+ return `import { ${imports.join(", ")} } from "@reframe/core";
115
+
116
+ // Assembled by \`reframe assemble\`. Clip holds were probed from the media (baked
117
+ // in, so the render is deterministic). Edit freely \u2014 change order/holds, the
118
+ // title, swap a src; node ids \`shot-0\`, \`shot-1\`\u2026 and \`ttl-*\` are stable.
119
+ const W = 1920, H = 1080;
120
+
121
+ const m = photoMontage([
122
+ ${shotLines}
123
+ ], { id: "shot", size: { width: W, height: H }, transition: 0.6, seed: ${args.seed} });
124
+ ${titleBlock}
125
+ export default scene({
126
+ id: ${JSON.stringify(name)},
127
+ size: { width: W, height: H },
128
+ fps: 30,
129
+ background: "#000000",
130
+ nodes: ${nodes},
131
+ timeline: ${timeline},${audio}
132
+ });
133
+ `;
134
+ }
135
+ main().catch((err) => {
136
+ console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
137
+ process.exit(1);
138
+ });
package/dist/bin.js CHANGED
@@ -258,11 +258,68 @@ function compileScene(ir) {
258
258
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
259
259
  const natural = durationOf(grouping, 0);
260
260
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
261
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
261
+ const at = typeof tl.at === "number" ? tl.at : void 0;
262
+ const beatStart = at ?? start + (tl.gap ?? 0);
262
263
  return beatStart + k * natural;
263
264
  }
264
265
  }
265
266
  };
267
+ let labelClock;
268
+ const anyAnchor = (tl) => tl.kind === "beat" && typeof tl.at === "string" || "children" in tl && tl.children.some(anyAnchor);
269
+ if (ir.timeline && anyAnchor(ir.timeline)) {
270
+ const clock = /* @__PURE__ */ new Map();
271
+ const clockWalk = (tl, start) => {
272
+ let end = start;
273
+ switch (tl.kind) {
274
+ case "seq": {
275
+ let t = start;
276
+ for (const c of orderBeats(tl.children)) t = clockWalk(c, t);
277
+ end = t;
278
+ break;
279
+ }
280
+ case "par": {
281
+ for (const c of tl.children) end = Math.max(end, clockWalk(c, start));
282
+ break;
283
+ }
284
+ case "stagger": {
285
+ tl.children.forEach((c, i) => {
286
+ end = Math.max(end, clockWalk(c, start + i * tl.interval));
287
+ });
288
+ break;
289
+ }
290
+ case "wait":
291
+ end = start + tl.duration;
292
+ break;
293
+ case "tween":
294
+ end = start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
295
+ break;
296
+ case "motionPath":
297
+ end = start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
298
+ break;
299
+ case "to": {
300
+ const override = ir.states?.[tl.state] ?? {};
301
+ const si = tl.stagger ?? 0;
302
+ const targets = nodeOrder.filter((id) => id in override && (tl.filter === void 0 || tl.filter.includes(id)));
303
+ end = start + (tl.duration ?? DEFAULT_TO_DURATION) + Math.max(0, targets.length - 1) * si;
304
+ break;
305
+ }
306
+ case "beat": {
307
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
308
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
309
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
310
+ const at = typeof tl.at === "number" ? tl.at : void 0;
311
+ const beatStart = at ?? start + (tl.gap ?? 0);
312
+ end = clockWalk(inner, beatStart);
313
+ clock.set(tl.name, { t0: beatStart, t1: end });
314
+ break;
315
+ }
316
+ }
317
+ if ("label" in tl && tl.label !== void 0) clock.set(tl.label, { t0: start, t1: end });
318
+ return end;
319
+ };
320
+ clockWalk(ir.timeline, 0);
321
+ labelClock = clock;
322
+ }
266
323
  const walk = (tl, start) => {
267
324
  const end = walkInner(tl, start);
268
325
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -279,7 +336,8 @@ function compileScene(ir) {
279
336
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
280
337
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
281
338
  const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
282
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
339
+ const anchored = typeof tl.at === "string" ? labelClock?.get(tl.at)?.t0 : tl.at;
340
+ const beatStart = anchored !== void 0 ? anchored + (typeof tl.at === "string" ? tl.gap ?? 0 : 0) : start + (tl.gap ?? 0);
283
341
  const end = walk(inner, beatStart);
284
342
  beatTimes.set(tl.name, { t0: beatStart, t1: end });
285
343
  labelTimes.set(tl.name, { t0: beatStart, t1: end });
@@ -577,6 +635,7 @@ var init_interpolate = __esm({
577
635
  function validateScene(ir) {
578
636
  const problems = [];
579
637
  const nodeById = /* @__PURE__ */ new Map();
638
+ const startAnchors = [];
580
639
  const checkPaint = (where, value) => {
581
640
  if (typeof value !== "object" || value === null) return;
582
641
  const g = value;
@@ -609,6 +668,7 @@ function validateScene(ir) {
609
668
  if (typeof props.shadowBlur === "number" && props.shadowBlur < 0) problems.push(`node "${node.id}": shadowBlur must be >= 0`);
610
669
  if (typeof props.blend === "string" && !BLEND_MODES.has(props.blend)) problems.push(`node "${node.id}": unknown blend "${props.blend}" \u2014 use ${[...BLEND_MODES].join(", ")}`);
611
670
  if (typeof props.fit === "string" && !IMAGE_FITS.has(props.fit)) problems.push(`node "${node.id}": unknown fit "${props.fit}" \u2014 use ${[...IMAGE_FITS].join(", ")}`);
671
+ if (node.type === "video" && typeof node.props.start === "string") startAnchors.push({ id: node.id, at: node.props.start });
612
672
  if (node.type === "group") {
613
673
  const clip = node.props.clip;
614
674
  if (clip) {
@@ -669,6 +729,7 @@ function validateScene(ir) {
669
729
  );
670
730
  }
671
731
  const labels = /* @__PURE__ */ new Set();
732
+ const beatAnchors = [];
672
733
  const checkEase = (path2, ease) => {
673
734
  if (ease === void 0) return;
674
735
  if (typeof ease === "string") {
@@ -760,6 +821,7 @@ function validateScene(ir) {
760
821
  );
761
822
  }
762
823
  labels.add(tl.name);
824
+ if (typeof tl.at === "string") beatAnchors.push({ name: tl.name, at: tl.at, path: path2 });
763
825
  if (tl.duration !== void 0 && tl.duration <= 0) {
764
826
  problems.push(`${path2}: beat "${tl.name}" duration must be > 0`);
765
827
  }
@@ -778,6 +840,22 @@ function validateScene(ir) {
778
840
  }
779
841
  };
780
842
  if (ir.timeline) checkTimeline(ir.timeline, "timeline");
843
+ for (const a of beatAnchors) {
844
+ if (a.at === a.name) {
845
+ problems.push(`${a.path}: beat "${a.name}" at: "${a.at}" cannot anchor to itself`);
846
+ } else if (!labels.has(a.at)) {
847
+ problems.push(
848
+ `${a.path}: beat "${a.name}" at: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
849
+ );
850
+ }
851
+ }
852
+ for (const a of startAnchors) {
853
+ if (!labels.has(a.at)) {
854
+ problems.push(
855
+ `video "${a.id}" start: "${a.at}" \u2014 unknown timeline label \u2014 known labels: ${[...labels].join(", ") || "(none)"}`
856
+ );
857
+ }
858
+ }
781
859
  for (const [i, b] of (ir.behaviors ?? []).entries()) {
782
860
  checkProps(`behaviors[${i}]`, b.target, { [b.prop]: 0 });
783
861
  }
@@ -1117,13 +1195,6 @@ function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
1117
1195
  if ("children" in tl) tl.children.forEach(walkTimeline);
1118
1196
  };
1119
1197
  if (ir.timeline) walkTimeline(ir.timeline);
1120
- const PATCHABLE = {
1121
- to: ["duration", "ease", "stagger"],
1122
- tween: ["duration", "ease"],
1123
- wait: ["duration"],
1124
- motionPath: ["points", "duration", "ease", "curviness", "autoRotate"],
1125
- beat: ["at", "gap", "scale", "duration", "order"]
1126
- };
1127
1198
  let timingPatched = false;
1128
1199
  for (const [label, patch] of Object.entries(overlay.timeline)) {
1129
1200
  const step = byLabel.get(label);
@@ -1134,7 +1205,7 @@ function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
1134
1205
  );
1135
1206
  continue;
1136
1207
  }
1137
- const allowed = PATCHABLE[step.kind] ?? [];
1208
+ const allowed = TIMELINE_PATCHABLE[step.kind] ?? [];
1138
1209
  for (const [key2, value] of Object.entries(patch)) {
1139
1210
  if (value === void 0) continue;
1140
1211
  if (!allowed.includes(key2)) {
@@ -1203,13 +1274,29 @@ function applyOverlay(ir, overlay, layer, report, baseNodeIds) {
1203
1274
  }
1204
1275
  }
1205
1276
  }
1206
- var SCENE_PATCHABLE;
1277
+ var SCENE_PATCHABLE, TIMELINE_PATCHABLE;
1207
1278
  var init_compose = __esm({
1208
1279
  "../core/src/compose.ts"() {
1209
1280
  "use strict";
1210
1281
  init_compile();
1211
1282
  init_validate();
1212
1283
  SCENE_PATCHABLE = ["background", "duration", "fps"];
1284
+ TIMELINE_PATCHABLE = {
1285
+ to: ["duration", "ease", "stagger"],
1286
+ tween: ["duration", "ease"],
1287
+ wait: ["duration"],
1288
+ motionPath: ["points", "duration", "ease", "curviness", "autoRotate"],
1289
+ beat: ["at", "gap", "scale", "duration", "order"]
1290
+ };
1291
+ }
1292
+ });
1293
+
1294
+ // ../core/src/manifest.ts
1295
+ var init_manifest = __esm({
1296
+ "../core/src/manifest.ts"() {
1297
+ "use strict";
1298
+ init_compose();
1299
+ init_validate();
1213
1300
  }
1214
1301
  });
1215
1302
 
@@ -1526,6 +1613,31 @@ var init_montage = __esm({
1526
1613
  }
1527
1614
  });
1528
1615
 
1616
+ // ../core/src/textMetrics.ts
1617
+ var init_textMetrics = __esm({
1618
+ "../core/src/textMetrics.ts"() {
1619
+ "use strict";
1620
+ }
1621
+ });
1622
+
1623
+ // ../core/src/textFx.ts
1624
+ var init_textFx = __esm({
1625
+ "../core/src/textFx.ts"() {
1626
+ "use strict";
1627
+ init_dsl();
1628
+ init_textMetrics();
1629
+ }
1630
+ });
1631
+
1632
+ // ../core/src/titles.ts
1633
+ var init_titles = __esm({
1634
+ "../core/src/titles.ts"() {
1635
+ "use strict";
1636
+ init_dsl();
1637
+ init_textFx();
1638
+ }
1639
+ });
1640
+
1529
1641
  // ../core/src/presets.ts
1530
1642
  function makeRng(seed) {
1531
1643
  let a = seed >>> 0 || 2654435769;
@@ -1710,6 +1822,8 @@ var init_devicePreset = __esm({
1710
1822
  "../core/src/devicePreset.ts"() {
1711
1823
  "use strict";
1712
1824
  init_dsl();
1825
+ init_effects();
1826
+ init_gradient();
1713
1827
  }
1714
1828
  });
1715
1829
 
@@ -1747,22 +1861,6 @@ var init_figure = __esm({
1747
1861
  }
1748
1862
  });
1749
1863
 
1750
- // ../core/src/textMetrics.ts
1751
- var init_textMetrics = __esm({
1752
- "../core/src/textMetrics.ts"() {
1753
- "use strict";
1754
- }
1755
- });
1756
-
1757
- // ../core/src/textFx.ts
1758
- var init_textFx = __esm({
1759
- "../core/src/textFx.ts"() {
1760
- "use strict";
1761
- init_dsl();
1762
- init_textMetrics();
1763
- }
1764
- });
1765
-
1766
1864
  // ../core/src/motionOps.ts
1767
1865
  var init_motionOps = __esm({
1768
1866
  "../core/src/motionOps.ts"() {
@@ -1772,13 +1870,14 @@ var init_motionOps = __esm({
1772
1870
  });
1773
1871
 
1774
1872
  // ../core/src/audio.ts
1775
- function collectClipAudio(ir, duration, warnings) {
1873
+ function collectClipAudio(ir, duration, labelTimes, warnings) {
1776
1874
  const out = [];
1777
1875
  const walk = (nodes) => {
1778
1876
  for (const node of nodes) {
1779
1877
  if (node.type === "video") {
1780
1878
  const gain = node.props.volume ?? 1;
1781
- const start = node.props.start ?? 0;
1879
+ const startRaw = node.props.start;
1880
+ const start = typeof startRaw === "string" ? labelTimes.get(startRaw)?.t0 ?? 0 : startRaw ?? 0;
1782
1881
  if (gain <= 0) continue;
1783
1882
  if (start >= duration) {
1784
1883
  warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
@@ -1796,7 +1895,7 @@ function resolveAudioPlan(compiled) {
1796
1895
  const audio = compiled.ir.audio;
1797
1896
  const warnings = [];
1798
1897
  const duration = compiled.duration;
1799
- const clipAudio = collectClipAudio(compiled.ir, duration, warnings);
1898
+ const clipAudio = collectClipAudio(compiled.ir, duration, compiled.labelTimes, warnings);
1800
1899
  const autoCues = audio?.autoFoley ? autoFoley(compiled, audio.autoFoley === true ? {} : audio.autoFoley) : [];
1801
1900
  const manualCues = [...audio?.cues ?? [], ...autoCues];
1802
1901
  if (!audio || !audio.bgm && manualCues.length === 0) {
@@ -1984,6 +2083,7 @@ var init_src = __esm({
1984
2083
  init_validate();
1985
2084
  init_composeComposition();
1986
2085
  init_compose();
2086
+ init_manifest();
1987
2087
  init_compile();
1988
2088
  init_path();
1989
2089
  init_camera();
@@ -1992,6 +2092,7 @@ var init_src = __esm({
1992
2092
  init_effects();
1993
2093
  init_layout();
1994
2094
  init_montage();
2095
+ init_titles();
1995
2096
  init_presets();
1996
2097
  init_devicePreset();
1997
2098
  init_cursor();
@@ -3219,7 +3320,7 @@ ${stderr.slice(-2e3)}`))
3219
3320
  });
3220
3321
  }
3221
3322
  function neededSeconds(node, duration) {
3222
- const start = node.props.start ?? 0;
3323
+ const start = typeof node.props.start === "string" ? 0 : node.props.start ?? 0;
3223
3324
  const rate = node.props.rate ?? 1;
3224
3325
  const clipStart = node.props.clipStart ?? 0;
3225
3326
  return clipStart + Math.max(0, duration - start) * Math.max(0, rate) + 1 / 30;
@@ -3743,6 +3844,10 @@ var USER_CWD = process.env.INIT_CWD ?? process.cwd();
3743
3844
  var RENDER_CLI = PACKAGED ? join9(ROOT2, "dist", "cli.js") : join9(ROOT2, "packages", "render-cli", "src", "cli.ts");
3744
3845
  var LABELS = PACKAGED ? join9(ROOT2, "dist", "labels.js") : join9(ROOT2, "packages", "render-cli", "src", "labels.ts");
3745
3846
  var COMPILE = PACKAGED ? join9(ROOT2, "dist", "compile.js") : join9(ROOT2, "packages", "render-cli", "src", "compile.ts");
3847
+ var ASSEMBLE = PACKAGED ? join9(ROOT2, "dist", "assemble.js") : join9(ROOT2, "packages", "render-cli", "src", "assemble.ts");
3848
+ var MANIFEST = PACKAGED ? join9(ROOT2, "dist", "manifest.js") : join9(ROOT2, "packages", "render-cli", "src", "manifest.ts");
3849
+ var LINT = PACKAGED ? join9(ROOT2, "dist", "lint.js") : join9(ROOT2, "packages", "render-cli", "src", "lint.ts");
3850
+ var VERIFY_OVERLAY = PACKAGED ? join9(ROOT2, "dist", "verifyOverlay.js") : join9(ROOT2, "packages", "render-cli", "src", "verifyOverlay.ts");
3746
3851
  var DIFF = PACKAGED ? join9(ROOT2, "dist", "diff.js") : join9(ROOT2, "packages", "render-cli", "src", "diff.ts");
3747
3852
  var FRAME = PACKAGED ? join9(ROOT2, "dist", "frame.js") : join9(ROOT2, "packages", "render-cli", "src", "frame.ts");
3748
3853
  var PLAYER = PACKAGED ? join9(ROOT2, "dist", "player.js") : join9(ROOT2, "packages", "render-cli", "src", "player.ts");
@@ -3773,7 +3878,12 @@ usage:
3773
3878
  player (plays live in any browser or a Claude.ai artifact; visual only)
3774
3879
  ${CMD} preview open the scrub/edit UI (lists scenes in your directory)
3775
3880
  ${CMD} new <scene-name> scaffold <scene-name>.ts in your directory
3881
+ ${CMD} assemble <media...> [-o name] [--title "\u2026"] [--bgm <synth>] [--hold s] [--seed N]
3882
+ probe images/videos \u2192 scaffold a clip-aware montage scene .ts (then render it)
3776
3883
  ${CMD} labels <scene.ts|.json> print the event clock (label \u2192 exact seconds; for sound design / timing)
3884
+ ${CMD} manifest <scene.ts|.json> [--json] list the editable surface (node/state/label/beat/behavior addresses + patchable props)
3885
+ ${CMD} lint <scene.ts|.json> [--json] [--strict] flag un-addressable motion (regen-unsafe) + an addressability summary
3886
+ ${CMD} verify-overlay <base.ts|.json> <overlay.json>... [--json] compose an overlay onto a base, report survival (no render; non-zero exit on orphans)
3777
3887
  ${CMD} compile <scene.ts|.json> [-o out.json] [--stdin] [--code "<src>"] [--json]
3778
3888
  bundle + validate a scene to SceneIR JSON, no render (fast; no ffmpeg/chromium)
3779
3889
  ${CMD} frame <scene.ts|.json> [--t <sec>] [-o out.png] render ONE frame at time t to a PNG (no mp4; for a render-and-look loop)
@@ -3926,6 +4036,43 @@ ${USAGE}`);
3926
4036
  await (PACKAGED ? run2(process.execPath, [LABELS, inputPath]) : run2("npx", ["tsx", LABELS, inputPath]))
3927
4037
  );
3928
4038
  }
4039
+ case "assemble": {
4040
+ if (rest.length === 0) fail(`assemble needs at least one media file
4041
+
4042
+ ${USAGE}`);
4043
+ process.exit(
4044
+ await (PACKAGED ? run2(process.execPath, [ASSEMBLE, ...rest]) : run2("npx", ["tsx", ASSEMBLE, ...rest]))
4045
+ );
4046
+ }
4047
+ case "manifest":
4048
+ case "lint": {
4049
+ const input = rest.find((a) => !a.startsWith("-"));
4050
+ if (!input) fail(`${command} needs a scene file
4051
+
4052
+ ${USAGE}`);
4053
+ const inputPath = userPath(input);
4054
+ if (!existsSync6(inputPath)) fail(`no such file: ${inputPath}`);
4055
+ const flags = rest.filter((a) => a.startsWith("-"));
4056
+ const entry = command === "manifest" ? MANIFEST : LINT;
4057
+ process.exit(
4058
+ await (PACKAGED ? run2(process.execPath, [entry, inputPath, ...flags]) : run2("npx", ["tsx", entry, inputPath, ...flags]))
4059
+ );
4060
+ }
4061
+ case "verify-overlay": {
4062
+ const fileArgs = rest.filter((a) => !a.startsWith("-"));
4063
+ const flags = rest.filter((a) => a.startsWith("-"));
4064
+ if (fileArgs.length < 2) fail(`verify-overlay needs a base scene and at least one overlay
4065
+
4066
+ ${USAGE}`);
4067
+ const paths = fileArgs.map((p) => {
4068
+ const ap = userPath(p);
4069
+ if (!existsSync6(ap)) fail(`no such file: ${ap}`);
4070
+ return ap;
4071
+ });
4072
+ process.exit(
4073
+ await (PACKAGED ? run2(process.execPath, [VERIFY_OVERLAY, ...paths, ...flags]) : run2("npx", ["tsx", VERIFY_OVERLAY, ...paths, ...flags]))
4074
+ );
4075
+ }
3929
4076
  case "compile": {
3930
4077
  const hasInlineSource = rest.includes("--stdin") || rest.includes("--code");
3931
4078
  const fileArg = rest.find((a, i) => !a.startsWith("-") && !["-o", "--code", "--timeout"].includes(rest[i - 1] ?? ""));
@@ -219,11 +219,68 @@
219
219
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
220
220
  const natural = durationOf(grouping, 0);
221
221
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, natural) : 1);
222
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
222
+ const at = typeof tl.at === "number" ? tl.at : void 0;
223
+ const beatStart = at ?? start + (tl.gap ?? 0);
223
224
  return beatStart + k * natural;
224
225
  }
225
226
  }
226
227
  };
228
+ let labelClock;
229
+ const anyAnchor = (tl) => tl.kind === "beat" && typeof tl.at === "string" || "children" in tl && tl.children.some(anyAnchor);
230
+ if (ir.timeline && anyAnchor(ir.timeline)) {
231
+ const clock = /* @__PURE__ */ new Map();
232
+ const clockWalk = (tl, start) => {
233
+ let end = start;
234
+ switch (tl.kind) {
235
+ case "seq": {
236
+ let t = start;
237
+ for (const c of orderBeats(tl.children)) t = clockWalk(c, t);
238
+ end = t;
239
+ break;
240
+ }
241
+ case "par": {
242
+ for (const c of tl.children) end = Math.max(end, clockWalk(c, start));
243
+ break;
244
+ }
245
+ case "stagger": {
246
+ tl.children.forEach((c, i) => {
247
+ end = Math.max(end, clockWalk(c, start + i * tl.interval));
248
+ });
249
+ break;
250
+ }
251
+ case "wait":
252
+ end = start + tl.duration;
253
+ break;
254
+ case "tween":
255
+ end = start + (tl.duration ?? DEFAULT_TWEEN_DURATION);
256
+ break;
257
+ case "motionPath":
258
+ end = start + (tl.duration ?? DEFAULT_MOTIONPATH_DURATION);
259
+ break;
260
+ case "to": {
261
+ const override = ir.states?.[tl.state] ?? {};
262
+ const si = tl.stagger ?? 0;
263
+ const targets = nodeOrder.filter((id) => id in override && (tl.filter === void 0 || tl.filter.includes(id)));
264
+ end = start + (tl.duration ?? DEFAULT_TO_DURATION) + Math.max(0, targets.length - 1) * si;
265
+ break;
266
+ }
267
+ case "beat": {
268
+ const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
269
+ const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
270
+ const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
271
+ const at = typeof tl.at === "number" ? tl.at : void 0;
272
+ const beatStart = at ?? start + (tl.gap ?? 0);
273
+ end = clockWalk(inner, beatStart);
274
+ clock.set(tl.name, { t0: beatStart, t1: end });
275
+ break;
276
+ }
277
+ }
278
+ if ("label" in tl && tl.label !== void 0) clock.set(tl.label, { t0: start, t1: end });
279
+ return end;
280
+ };
281
+ clockWalk(ir.timeline, 0);
282
+ labelClock = clock;
283
+ }
227
284
  const walk = (tl, start) => {
228
285
  const end = walkInner(tl, start);
229
286
  if ("label" in tl && tl.label !== void 0) labelTimes.set(tl.label, { t0: start, t1: end });
@@ -240,7 +297,8 @@
240
297
  const grouping = { kind: tl.parallel ? "par" : "seq", children: tl.children };
241
298
  const k = tl.scale ?? (tl.duration !== void 0 ? tl.duration / Math.max(1e-9, durationOf(grouping, 0)) : 1);
242
299
  const inner = k === 1 ? grouping : scaleTimeline(grouping, k);
243
- const beatStart = tl.at ?? start + (tl.gap ?? 0);
300
+ const anchored = typeof tl.at === "string" ? labelClock?.get(tl.at)?.t0 : tl.at;
301
+ const beatStart = anchored !== void 0 ? anchored + (typeof tl.at === "string" ? tl.gap ?? 0 : 0) : start + (tl.gap ?? 0);
244
302
  const end = walk(inner, beatStart);
245
303
  beatTimes.set(tl.name, { t0: beatStart, t1: end });
246
304
  labelTimes.set(tl.name, { t0: beatStart, t1: end });
@@ -885,7 +943,8 @@
885
943
  const height = num(id, "height", node.props.height);
886
944
  const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
887
945
  const fps = compiled2.ir.fps ?? 30;
888
- const start = node.props.start ?? 0;
946
+ const startRaw = node.props.start;
947
+ const start = typeof startRaw === "string" ? compiled2.labelTimes.get(startRaw)?.t0 ?? 0 : startRaw ?? 0;
889
948
  const rate = node.props.rate ?? 1;
890
949
  const clipStart = node.props.clipStart ?? 0;
891
950
  const srcT = clipStart + Math.max(0, t - start) * rate;