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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/assemble.js +138 -0
- package/dist/bin.js +178 -31
- package/dist/browserEntry.js +62 -3
- package/dist/cli.js +93 -14
- package/dist/compile-api.js +20 -0
- package/dist/compile.js +20 -0
- package/dist/diff.js +81 -3
- package/dist/frame.js +81 -3
- package/dist/index.js +1528 -1107
- package/dist/labels.js +80 -2
- package/dist/lint.js +1060 -0
- package/dist/manifest.js +1065 -0
- package/dist/types/compose.d.ts +6 -0
- package/dist/types/devicePreset.d.ts +36 -3
- package/dist/types/dsl.d.ts +4 -3
- package/dist/types/index.d.ts +4 -2
- package/dist/types/ir.d.ts +13 -5
- package/dist/types/manifest.d.ts +92 -0
- package/dist/types/titles.d.ts +71 -0
- package/dist/verifyOverlay.js +1152 -0
- package/guides/edsl-guide.md +58 -0
- package/guides/regen-contract.md +14 -0
- package/package.json +1 -1
- package/skills/reframe/SKILL.md +26 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
package/dist/assemble.js
ADDED
|
@@ -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
|
|
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
|
|
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 =
|
|
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
|
|
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] ?? ""));
|
package/dist/browserEntry.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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;
|