reframe-video 0.6.33 → 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 +248 -36
- package/dist/browserEntry.js +62 -3
- package/dist/cli.js +162 -18
- 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 +1537 -1111
- 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
|
|
|
@@ -1263,6 +1350,33 @@ var init_gradient = __esm({
|
|
|
1263
1350
|
});
|
|
1264
1351
|
|
|
1265
1352
|
// ../core/src/evaluate.ts
|
|
1353
|
+
function multiply(m, n) {
|
|
1354
|
+
return [
|
|
1355
|
+
m[0] * n[0] + m[2] * n[1],
|
|
1356
|
+
m[1] * n[0] + m[3] * n[1],
|
|
1357
|
+
m[0] * n[2] + m[2] * n[3],
|
|
1358
|
+
m[1] * n[2] + m[3] * n[3],
|
|
1359
|
+
m[0] * n[4] + m[2] * n[5] + m[4],
|
|
1360
|
+
m[1] * n[4] + m[3] * n[5] + m[5]
|
|
1361
|
+
];
|
|
1362
|
+
}
|
|
1363
|
+
function localMatrix(x, y, rotationDeg, scale, scaleX = 1, scaleY = 1, skewXDeg = 0, skewYDeg = 0) {
|
|
1364
|
+
const r = rotationDeg * Math.PI / 180;
|
|
1365
|
+
if (scaleX === 1 && scaleY === 1 && skewXDeg === 0 && skewYDeg === 0) {
|
|
1366
|
+
const cos = Math.cos(r) * scale;
|
|
1367
|
+
const sin = Math.sin(r) * scale;
|
|
1368
|
+
return [cos, sin, -sin, cos, x, y];
|
|
1369
|
+
}
|
|
1370
|
+
const c = Math.cos(r);
|
|
1371
|
+
const s = Math.sin(r);
|
|
1372
|
+
const tx = Math.tan(skewXDeg * Math.PI / 180);
|
|
1373
|
+
const ty = Math.tan(skewYDeg * Math.PI / 180);
|
|
1374
|
+
const R = [c, s, -s, c, 0, 0];
|
|
1375
|
+
const K = [1, ty, tx, 1, 0, 0];
|
|
1376
|
+
const S = [scale * scaleX, 0, 0, scale * scaleY, 0, 0];
|
|
1377
|
+
const m = multiply(R, multiply(K, S));
|
|
1378
|
+
return [m[0], m[1], m[2], m[3], x, y];
|
|
1379
|
+
}
|
|
1266
1380
|
function behaviorEnvelope(b, t) {
|
|
1267
1381
|
const from = b.from ?? Number.NEGATIVE_INFINITY;
|
|
1268
1382
|
const until = b.until ?? Number.POSITIVE_INFINITY;
|
|
@@ -1318,7 +1432,39 @@ function sampleProp(compiled, t, target, prop, fallback) {
|
|
|
1318
1432
|
}
|
|
1319
1433
|
return value;
|
|
1320
1434
|
}
|
|
1321
|
-
|
|
1435
|
+
function nodeParentMatrix(compiled, id, t) {
|
|
1436
|
+
const num2 = (target, prop, fallback) => {
|
|
1437
|
+
const v = sampleProp(compiled, t, target, prop, fallback);
|
|
1438
|
+
return typeof v === "number" ? v : fallback;
|
|
1439
|
+
};
|
|
1440
|
+
let result = null;
|
|
1441
|
+
const walk = (node, parent) => {
|
|
1442
|
+
if (node.id === id) {
|
|
1443
|
+
result = parent;
|
|
1444
|
+
return true;
|
|
1445
|
+
}
|
|
1446
|
+
if (node.type === "group") {
|
|
1447
|
+
const m = multiply(
|
|
1448
|
+
parent,
|
|
1449
|
+
localMatrix(
|
|
1450
|
+
num2(node.id, "x", node.props.x),
|
|
1451
|
+
num2(node.id, "y", node.props.y),
|
|
1452
|
+
num2(node.id, "rotation", node.props.rotation ?? 0),
|
|
1453
|
+
num2(node.id, "scale", node.props.scale ?? 1),
|
|
1454
|
+
num2(node.id, "scaleX", node.props.scaleX ?? 1),
|
|
1455
|
+
num2(node.id, "scaleY", node.props.scaleY ?? 1),
|
|
1456
|
+
num2(node.id, "skewX", node.props.skewX ?? 0),
|
|
1457
|
+
num2(node.id, "skewY", node.props.skewY ?? 0)
|
|
1458
|
+
)
|
|
1459
|
+
);
|
|
1460
|
+
for (const child of node.children) if (walk(child, m)) return true;
|
|
1461
|
+
}
|
|
1462
|
+
return false;
|
|
1463
|
+
};
|
|
1464
|
+
for (const node of compiled.ir.nodes) if (walk(node, IDENTITY)) break;
|
|
1465
|
+
return result;
|
|
1466
|
+
}
|
|
1467
|
+
var IDENTITY, DEG;
|
|
1322
1468
|
var init_evaluate = __esm({
|
|
1323
1469
|
"../core/src/evaluate.ts"() {
|
|
1324
1470
|
"use strict";
|
|
@@ -1327,6 +1473,7 @@ var init_evaluate = __esm({
|
|
|
1327
1473
|
init_gradient();
|
|
1328
1474
|
init_interpolate();
|
|
1329
1475
|
init_path();
|
|
1476
|
+
IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
1330
1477
|
DEG = Math.PI / 180;
|
|
1331
1478
|
}
|
|
1332
1479
|
});
|
|
@@ -1364,6 +1511,10 @@ function autoFoley(compiled, opts = {}) {
|
|
|
1364
1511
|
os.push(num(compiled, t, id, "opacity", node.props.opacity ?? 1));
|
|
1365
1512
|
}
|
|
1366
1513
|
const speed = (i2) => i2 <= 0 ? 0 : Math.hypot(xs[i2] - xs[i2 - 1], ys[i2] - ys[i2 - 1]) * fps;
|
|
1514
|
+
const worldX = (frame) => {
|
|
1515
|
+
const m = nodeParentMatrix(compiled, id, frame / fps);
|
|
1516
|
+
return m ? m[0] * xs[frame] + m[2] * ys[frame] + m[4] : xs[frame];
|
|
1517
|
+
};
|
|
1367
1518
|
let i = 1;
|
|
1368
1519
|
while (i <= N) {
|
|
1369
1520
|
if (speed(i) <= vMin) {
|
|
@@ -1386,19 +1537,20 @@ function autoFoley(compiled, opts = {}) {
|
|
|
1386
1537
|
if (peakV > vMin && durS >= MIN_DUR && visible) {
|
|
1387
1538
|
if (wantWhoosh) {
|
|
1388
1539
|
const quickFlick = durS < 0.25;
|
|
1389
|
-
cands.push({ t: peak / fps, sfx: quickFlick ? "swish" : "whoosh", gain: master * loud(peakV), pan: panOf(
|
|
1540
|
+
cands.push({ t: peak / fps, sfx: quickFlick ? "swish" : "whoosh", gain: master * loud(peakV), pan: panOf(worldX(peak)), rank: peakV });
|
|
1390
1541
|
}
|
|
1391
1542
|
const stopped = b >= N || speed(b + 1) < vStop && speed(Math.min(N, b + 2)) < vStop;
|
|
1392
|
-
const
|
|
1543
|
+
const wxb = worldX(b);
|
|
1544
|
+
const landsOnScreen = wxb >= 0 && wxb <= W && os[b] > 0.1;
|
|
1393
1545
|
if (wantImpact && peakV > vDecel && stopped && landsOnScreen && b < N) {
|
|
1394
|
-
cands.push({ t: (b + 1) / fps, sfx: size > 220 ? "thud" : "knock", gain: master * loud(peakV), pan: panOf(
|
|
1546
|
+
cands.push({ t: (b + 1) / fps, sfx: size > 220 ? "thud" : "knock", gain: master * loud(peakV), pan: panOf(worldX(b)), rank: peakV * 1.1 });
|
|
1395
1547
|
}
|
|
1396
1548
|
}
|
|
1397
1549
|
}
|
|
1398
1550
|
if (wantPop && ss[0] < 0.25) {
|
|
1399
1551
|
for (let k = 1; k <= N; k++) {
|
|
1400
1552
|
if (ss[k - 1] < 0.5 && ss[k] >= 0.5 && os[k] > 0.05) {
|
|
1401
|
-
cands.push({ t: k / fps, sfx: "pop", gain: master * 0.7, pan: panOf(
|
|
1553
|
+
cands.push({ t: k / fps, sfx: "pop", gain: master * 0.7, pan: panOf(worldX(k)), rank: 600 });
|
|
1402
1554
|
break;
|
|
1403
1555
|
}
|
|
1404
1556
|
}
|
|
@@ -1461,6 +1613,31 @@ var init_montage = __esm({
|
|
|
1461
1613
|
}
|
|
1462
1614
|
});
|
|
1463
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
|
+
|
|
1464
1641
|
// ../core/src/presets.ts
|
|
1465
1642
|
function makeRng(seed) {
|
|
1466
1643
|
let a = seed >>> 0 || 2654435769;
|
|
@@ -1645,6 +1822,8 @@ var init_devicePreset = __esm({
|
|
|
1645
1822
|
"../core/src/devicePreset.ts"() {
|
|
1646
1823
|
"use strict";
|
|
1647
1824
|
init_dsl();
|
|
1825
|
+
init_effects();
|
|
1826
|
+
init_gradient();
|
|
1648
1827
|
}
|
|
1649
1828
|
});
|
|
1650
1829
|
|
|
@@ -1682,22 +1861,6 @@ var init_figure = __esm({
|
|
|
1682
1861
|
}
|
|
1683
1862
|
});
|
|
1684
1863
|
|
|
1685
|
-
// ../core/src/textMetrics.ts
|
|
1686
|
-
var init_textMetrics = __esm({
|
|
1687
|
-
"../core/src/textMetrics.ts"() {
|
|
1688
|
-
"use strict";
|
|
1689
|
-
}
|
|
1690
|
-
});
|
|
1691
|
-
|
|
1692
|
-
// ../core/src/textFx.ts
|
|
1693
|
-
var init_textFx = __esm({
|
|
1694
|
-
"../core/src/textFx.ts"() {
|
|
1695
|
-
"use strict";
|
|
1696
|
-
init_dsl();
|
|
1697
|
-
init_textMetrics();
|
|
1698
|
-
}
|
|
1699
|
-
});
|
|
1700
|
-
|
|
1701
1864
|
// ../core/src/motionOps.ts
|
|
1702
1865
|
var init_motionOps = __esm({
|
|
1703
1866
|
"../core/src/motionOps.ts"() {
|
|
@@ -1707,13 +1870,14 @@ var init_motionOps = __esm({
|
|
|
1707
1870
|
});
|
|
1708
1871
|
|
|
1709
1872
|
// ../core/src/audio.ts
|
|
1710
|
-
function collectClipAudio(ir, duration, warnings) {
|
|
1873
|
+
function collectClipAudio(ir, duration, labelTimes, warnings) {
|
|
1711
1874
|
const out = [];
|
|
1712
1875
|
const walk = (nodes) => {
|
|
1713
1876
|
for (const node of nodes) {
|
|
1714
1877
|
if (node.type === "video") {
|
|
1715
1878
|
const gain = node.props.volume ?? 1;
|
|
1716
|
-
const
|
|
1879
|
+
const startRaw = node.props.start;
|
|
1880
|
+
const start = typeof startRaw === "string" ? labelTimes.get(startRaw)?.t0 ?? 0 : startRaw ?? 0;
|
|
1717
1881
|
if (gain <= 0) continue;
|
|
1718
1882
|
if (start >= duration) {
|
|
1719
1883
|
warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
|
|
@@ -1731,7 +1895,7 @@ function resolveAudioPlan(compiled) {
|
|
|
1731
1895
|
const audio = compiled.ir.audio;
|
|
1732
1896
|
const warnings = [];
|
|
1733
1897
|
const duration = compiled.duration;
|
|
1734
|
-
const clipAudio = collectClipAudio(compiled.ir, duration, warnings);
|
|
1898
|
+
const clipAudio = collectClipAudio(compiled.ir, duration, compiled.labelTimes, warnings);
|
|
1735
1899
|
const autoCues = audio?.autoFoley ? autoFoley(compiled, audio.autoFoley === true ? {} : audio.autoFoley) : [];
|
|
1736
1900
|
const manualCues = [...audio?.cues ?? [], ...autoCues];
|
|
1737
1901
|
if (!audio || !audio.bgm && manualCues.length === 0) {
|
|
@@ -1919,6 +2083,7 @@ var init_src = __esm({
|
|
|
1919
2083
|
init_validate();
|
|
1920
2084
|
init_composeComposition();
|
|
1921
2085
|
init_compose();
|
|
2086
|
+
init_manifest();
|
|
1922
2087
|
init_compile();
|
|
1923
2088
|
init_path();
|
|
1924
2089
|
init_camera();
|
|
@@ -1927,6 +2092,7 @@ var init_src = __esm({
|
|
|
1927
2092
|
init_effects();
|
|
1928
2093
|
init_layout();
|
|
1929
2094
|
init_montage();
|
|
2095
|
+
init_titles();
|
|
1930
2096
|
init_presets();
|
|
1931
2097
|
init_devicePreset();
|
|
1932
2098
|
init_cursor();
|
|
@@ -3154,7 +3320,7 @@ ${stderr.slice(-2e3)}`))
|
|
|
3154
3320
|
});
|
|
3155
3321
|
}
|
|
3156
3322
|
function neededSeconds(node, duration) {
|
|
3157
|
-
const start = node.props.start ?? 0;
|
|
3323
|
+
const start = typeof node.props.start === "string" ? 0 : node.props.start ?? 0;
|
|
3158
3324
|
const rate = node.props.rate ?? 1;
|
|
3159
3325
|
const clipStart = node.props.clipStart ?? 0;
|
|
3160
3326
|
return clipStart + Math.max(0, duration - start) * Math.max(0, rate) + 1 / 30;
|
|
@@ -3678,6 +3844,10 @@ var USER_CWD = process.env.INIT_CWD ?? process.cwd();
|
|
|
3678
3844
|
var RENDER_CLI = PACKAGED ? join9(ROOT2, "dist", "cli.js") : join9(ROOT2, "packages", "render-cli", "src", "cli.ts");
|
|
3679
3845
|
var LABELS = PACKAGED ? join9(ROOT2, "dist", "labels.js") : join9(ROOT2, "packages", "render-cli", "src", "labels.ts");
|
|
3680
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");
|
|
3681
3851
|
var DIFF = PACKAGED ? join9(ROOT2, "dist", "diff.js") : join9(ROOT2, "packages", "render-cli", "src", "diff.ts");
|
|
3682
3852
|
var FRAME = PACKAGED ? join9(ROOT2, "dist", "frame.js") : join9(ROOT2, "packages", "render-cli", "src", "frame.ts");
|
|
3683
3853
|
var PLAYER = PACKAGED ? join9(ROOT2, "dist", "player.js") : join9(ROOT2, "packages", "render-cli", "src", "player.ts");
|
|
@@ -3708,7 +3878,12 @@ usage:
|
|
|
3708
3878
|
player (plays live in any browser or a Claude.ai artifact; visual only)
|
|
3709
3879
|
${CMD} preview open the scrub/edit UI (lists scenes in your directory)
|
|
3710
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)
|
|
3711
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)
|
|
3712
3887
|
${CMD} compile <scene.ts|.json> [-o out.json] [--stdin] [--code "<src>"] [--json]
|
|
3713
3888
|
bundle + validate a scene to SceneIR JSON, no render (fast; no ffmpeg/chromium)
|
|
3714
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)
|
|
@@ -3861,6 +4036,43 @@ ${USAGE}`);
|
|
|
3861
4036
|
await (PACKAGED ? run2(process.execPath, [LABELS, inputPath]) : run2("npx", ["tsx", LABELS, inputPath]))
|
|
3862
4037
|
);
|
|
3863
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
|
+
}
|
|
3864
4076
|
case "compile": {
|
|
3865
4077
|
const hasInlineSource = rest.includes("--stdin") || rest.includes("--code");
|
|
3866
4078
|
const fileArg = rest.find((a, i) => !a.startsWith("-") && !["-o", "--code", "--timeout"].includes(rest[i - 1] ?? ""));
|