reframe-video 0.6.17 → 0.6.19
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/README.md +45 -0
- package/dist/bin.js +120 -34
- package/dist/browserEntry.js +1 -1
- package/dist/cli.js +98 -28
- package/dist/compile-api.d.ts +6 -0
- package/dist/compile-api.js +477 -0
- package/dist/compile.js +477 -0
- package/dist/diff.js +77 -26
- package/dist/index.js +17 -2
- package/dist/labels.js +81 -32
- package/dist/renderer-canvas.d.ts +1 -0
- package/dist/renderer-canvas.js +1 -1
- package/dist/trace-cli.js +1 -1
- package/dist/types/audio.d.ts +10 -0
- package/dist/types/ir.d.ts +10 -0
- package/dist/types-renderer/index.d.ts +34 -0
- package/guides/edsl-guide.md +7 -3
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ npx reframe-video render hello.ts # → out/hello.mp4
|
|
|
22
22
|
|---|---|
|
|
23
23
|
| `reframe render <scene.ts> [--overlay edits.json] [-o out.mp4]` | deterministic mp4 |
|
|
24
24
|
| `reframe batch <scene.ts> <data.json\|csv>` | one mp4 per data row (row keys are overlay addresses) |
|
|
25
|
+
| `reframe compile <scene.ts> [-o out.json] [--json]` | bundle + validate a scene to SceneIR JSON, no render (fast; no ffmpeg/chromium) |
|
|
25
26
|
| `reframe preview` | scrub/play/edit UI for scenes in the current directory; edits export as overlay JSON |
|
|
26
27
|
| `reframe new <name>` | scaffold a documented starter scene |
|
|
27
28
|
| `reframe motion <mp4>` | calibrated motion profile of a rendered clip |
|
|
@@ -63,6 +64,50 @@ Audio is label-anchored (`audio: { cues: [{ at: "enter", sfx: "whoosh" }] }`)
|
|
|
63
64
|
so sound design follows retiming and regeneration. Full syntax:
|
|
64
65
|
`npx reframe-video guide`.
|
|
65
66
|
|
|
67
|
+
## Rendering to a canvas (live preview)
|
|
68
|
+
|
|
69
|
+
The same renderer that produces the mp4 is exported as a subpath for drawing
|
|
70
|
+
frames to a 2D canvas in the browser — so an editor or preview can render a
|
|
71
|
+
scene live and match the export. Compile once, draw any time `t`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { compileScene, evaluate } from "reframe-video";
|
|
75
|
+
import { renderFrame, drawDisplayList } from "reframe-video/renderer";
|
|
76
|
+
|
|
77
|
+
const compiled = compileScene(myScene); // myScene: SceneIR
|
|
78
|
+
const ctx = canvas.getContext("2d")!;
|
|
79
|
+
|
|
80
|
+
renderFrame(ctx, compiled, t); // clears + paints the frame at time t
|
|
81
|
+
// or drive the DisplayList yourself (you own the clear/background):
|
|
82
|
+
drawDisplayList(ctx, evaluate(compiled, t));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Camera, clips, track mattes, group effects, gradients and text are handled
|
|
86
|
+
exactly like the mp4 path — `renderFrame` bakes the scene camera in, so don't
|
|
87
|
+
apply one yourself. Images and video need registries: pass `images`
|
|
88
|
+
(`{ get(src) }`) and `videos` (`{ frame(src, i) }`) returning decoded
|
|
89
|
+
`CanvasImageSource`s. The default entry (scene authoring + `compileScene` /
|
|
90
|
+
`evaluate`) is unchanged.
|
|
91
|
+
|
|
92
|
+
## Compiling source to IR in-process (server)
|
|
93
|
+
|
|
94
|
+
For a backend that has an LLM author eDSL source and needs the **SceneIR** back
|
|
95
|
+
(to preview, diff, or self-correct) without rendering, the loader is exported as
|
|
96
|
+
a Node-only subpath:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { loadSceneFromCode, loadScene, SceneLoadError } from "reframe-video/compile";
|
|
100
|
+
|
|
101
|
+
const ir = await loadSceneFromCode(generatedSource); // bundle + validate, no ffmpeg/chromium
|
|
102
|
+
// errors are classified + sanitized (no base64 bundle dump):
|
|
103
|
+
try { await loadSceneFromCode(badSource); }
|
|
104
|
+
catch (e) { if (e instanceof SceneLoadError) console.log(e.kind, e.message); } // "eval" | "bundle" | "validation"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This runs the scene module in-process — bound untrusted/model-authored source
|
|
108
|
+
(a timeout) and run it where it can't do harm. The same thing on the CLI is
|
|
109
|
+
`reframe compile … --json`.
|
|
110
|
+
|
|
66
111
|
## Why this instead of generating Remotion/HTML?
|
|
67
112
|
|
|
68
113
|
One-shot generation quality is a wash (we measured it). The difference is the
|
package/dist/bin.js
CHANGED
|
@@ -578,6 +578,15 @@ function validateScene(ir) {
|
|
|
578
578
|
if (cue.gain !== void 0 && cue.gain < 0) {
|
|
579
579
|
problems.push(`audio.cues[${i}]: gain must be >= 0`);
|
|
580
580
|
}
|
|
581
|
+
if (cue.fadeIn !== void 0 && cue.fadeIn < 0) {
|
|
582
|
+
problems.push(`audio.cues[${i}]: fadeIn must be >= 0`);
|
|
583
|
+
}
|
|
584
|
+
if (cue.fadeOut !== void 0 && cue.fadeOut < 0) {
|
|
585
|
+
problems.push(`audio.cues[${i}]: fadeOut must be >= 0`);
|
|
586
|
+
}
|
|
587
|
+
if (cue.pan !== void 0 && (cue.pan < -1 || cue.pan > 1)) {
|
|
588
|
+
problems.push(`audio.cues[${i}]: pan must be in [-1, 1] (-1 left \u2026 +1 right)`);
|
|
589
|
+
}
|
|
581
590
|
}
|
|
582
591
|
const duck = ir.audio?.bgm?.duck;
|
|
583
592
|
if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
|
|
@@ -644,7 +653,7 @@ var init_validate = __esm({
|
|
|
644
653
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
645
654
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
646
655
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
647
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
656
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
648
657
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
649
658
|
group: COMMON_PROPS
|
|
650
659
|
};
|
|
@@ -1251,7 +1260,7 @@ function collectClipAudio(ir, duration, warnings) {
|
|
|
1251
1260
|
warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
|
|
1252
1261
|
continue;
|
|
1253
1262
|
}
|
|
1254
|
-
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain });
|
|
1263
|
+
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain, fadeIn: node.props.fadeIn ?? 0, pan: node.props.pan ?? 0 });
|
|
1255
1264
|
}
|
|
1256
1265
|
if (node.type === "group") walk(node.children);
|
|
1257
1266
|
}
|
|
@@ -1293,6 +1302,9 @@ function resolveAudioPlan(compiled) {
|
|
|
1293
1302
|
t,
|
|
1294
1303
|
gain: cue.gain ?? 1,
|
|
1295
1304
|
duration: cueDuration,
|
|
1305
|
+
fadeIn: cue.fadeIn ?? 0,
|
|
1306
|
+
fadeOut: cue.fadeOut ?? 0,
|
|
1307
|
+
pan: cue.pan ?? 0,
|
|
1296
1308
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
1297
1309
|
});
|
|
1298
1310
|
}
|
|
@@ -1985,6 +1997,10 @@ function atempoChain(rate) {
|
|
|
1985
1997
|
out.push(`atempo=${r.toFixed(4)}`);
|
|
1986
1998
|
return out;
|
|
1987
1999
|
}
|
|
2000
|
+
function panFilter(pan) {
|
|
2001
|
+
const clamp = (v) => Math.max(0, Math.min(1, v)).toFixed(4);
|
|
2002
|
+
return `pan=stereo|c0=${clamp(1 - pan)}*c0|c1=${clamp(1 + pan)}*c1`;
|
|
2003
|
+
}
|
|
1988
2004
|
function buildFilterGraph(plan, inputs) {
|
|
1989
2005
|
const lines = [];
|
|
1990
2006
|
const mixIn = ["[anchor]"];
|
|
@@ -2013,7 +2029,14 @@ function buildFilterGraph(plan, inputs) {
|
|
|
2013
2029
|
}
|
|
2014
2030
|
plan.cues.forEach((cue, i) => {
|
|
2015
2031
|
const delayMs = Math.round(cue.t * 1e3);
|
|
2016
|
-
|
|
2032
|
+
const chain = [FORMAT, `volume=${cue.gain}`];
|
|
2033
|
+
if (cue.fadeIn > 0) chain.push(`afade=t=in:st=0:d=${cue.fadeIn}`);
|
|
2034
|
+
if (cue.fadeOut > 0) {
|
|
2035
|
+
chain.push(`afade=t=out:st=${Math.max(0, cue.duration - cue.fadeOut).toFixed(3)}:d=${cue.fadeOut}`);
|
|
2036
|
+
}
|
|
2037
|
+
if (cue.pan !== 0) chain.push(panFilter(cue.pan));
|
|
2038
|
+
chain.push(`adelay=${delayMs}:all=1`);
|
|
2039
|
+
lines.push(`[${inputIndex}:a]${chain.join(",")}[c${i}]`);
|
|
2017
2040
|
mixIn.push(`[c${i}]`);
|
|
2018
2041
|
inputIndex++;
|
|
2019
2042
|
});
|
|
@@ -2021,6 +2044,8 @@ function buildFilterGraph(plan, inputs) {
|
|
|
2021
2044
|
const chain = [];
|
|
2022
2045
|
if (audio.clipStart > 0) chain.push(`atrim=start=${audio.clipStart.toFixed(3)}`, "asetpts=PTS-STARTPTS");
|
|
2023
2046
|
chain.push(...atempoChain(audio.rate), FORMAT, `volume=${audio.gain}`);
|
|
2047
|
+
if (audio.fadeIn > 0) chain.push(`afade=t=in:st=0:d=${audio.fadeIn}`);
|
|
2048
|
+
if (audio.pan !== 0) chain.push(panFilter(audio.pan));
|
|
2024
2049
|
const delayMs = Math.round(audio.start * 1e3);
|
|
2025
2050
|
if (delayMs > 0) chain.push(`adelay=${delayMs}:all=1`);
|
|
2026
2051
|
lines.push(`[${inputIndex}:a]${chain.join(",")}[k${i}]`);
|
|
@@ -2455,13 +2480,13 @@ async function captureIr(ir, opts) {
|
|
|
2455
2480
|
const assets = await buildImageAssets(ir, sceneDir);
|
|
2456
2481
|
const { fps, duration } = resolveTiming(ir, opts);
|
|
2457
2482
|
const videoAssets = await buildVideoFrameAssets(ir, sceneDir, fps, duration);
|
|
2458
|
-
const
|
|
2483
|
+
const bundle2 = await browserBundle();
|
|
2459
2484
|
return withPage(ir.size, async (page) => {
|
|
2460
2485
|
await page.setContent(
|
|
2461
2486
|
`<!DOCTYPE html><html><body style="margin:0;background:#000"></body></html>`
|
|
2462
2487
|
);
|
|
2463
2488
|
await injectFonts(page);
|
|
2464
|
-
await page.addScriptTag({ content:
|
|
2489
|
+
await page.addScriptTag({ content: bundle2 });
|
|
2465
2490
|
await page.evaluate(
|
|
2466
2491
|
([sceneIr, imageAssets, vAssets]) => window.__reframe.init(sceneIr, imageAssets, vAssets),
|
|
2467
2492
|
[ir, assets, videoAssets]
|
|
@@ -2654,66 +2679,108 @@ var init_batch = __esm({
|
|
|
2654
2679
|
// ../render-cli/src/loadScene.ts
|
|
2655
2680
|
var loadScene_exports = {};
|
|
2656
2681
|
__export(loadScene_exports, {
|
|
2682
|
+
SceneLoadError: () => SceneLoadError,
|
|
2657
2683
|
isComposition: () => isComposition,
|
|
2658
2684
|
loadModule: () => loadModule,
|
|
2659
|
-
loadScene: () => loadScene
|
|
2685
|
+
loadScene: () => loadScene,
|
|
2686
|
+
loadSceneFromCode: () => loadSceneFromCode
|
|
2660
2687
|
});
|
|
2661
2688
|
import { build as build2 } from "esbuild";
|
|
2662
2689
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
2663
2690
|
import { dirname as dirname6, resolve as resolve5 } from "node:path";
|
|
2664
2691
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2665
|
-
async function
|
|
2666
|
-
|
|
2667
|
-
|
|
2692
|
+
async function bundle(input) {
|
|
2693
|
+
const common = {
|
|
2694
|
+
bundle: true,
|
|
2695
|
+
format: "esm",
|
|
2696
|
+
platform: "neutral",
|
|
2697
|
+
write: false,
|
|
2698
|
+
logLevel: "silent",
|
|
2699
|
+
sourcemap: "inline",
|
|
2700
|
+
alias: ALIAS
|
|
2701
|
+
};
|
|
2668
2702
|
try {
|
|
2669
|
-
const out = await build2(
|
|
2670
|
-
entryPoints: [
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
platform: "neutral",
|
|
2674
|
-
write: false,
|
|
2675
|
-
logLevel: "silent",
|
|
2676
|
-
sourcemap: "inline",
|
|
2677
|
-
// both specifiers accepted: the guide's canonical "@reframe/core" and
|
|
2678
|
-
// the published package name
|
|
2679
|
-
alias: { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY }
|
|
2680
|
-
});
|
|
2681
|
-
code = out.outputFiles[0].text;
|
|
2703
|
+
const out = await build2(
|
|
2704
|
+
"path" in input ? { ...common, entryPoints: [input.path] } : { ...common, stdin: { contents: input.code, resolveDir: input.resolveDir, loader: "ts", sourcefile: "scene.ts" } }
|
|
2705
|
+
);
|
|
2706
|
+
return out.outputFiles[0].text;
|
|
2682
2707
|
} catch (err) {
|
|
2683
|
-
throw new
|
|
2684
|
-
${err instanceof Error ? err.message : String(err)}`);
|
|
2708
|
+
throw new SceneLoadError("bundle", clean(err), { cause: err });
|
|
2685
2709
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2710
|
+
}
|
|
2711
|
+
async function importDefault(code, label) {
|
|
2712
|
+
let mod;
|
|
2713
|
+
try {
|
|
2714
|
+
mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
|
|
2715
|
+
} catch (err) {
|
|
2716
|
+
const kind = err instanceof Error && err.name === "SceneValidationError" ? "validation" : "eval";
|
|
2717
|
+
throw new SceneLoadError(kind, clean(err), { cause: err });
|
|
2718
|
+
}
|
|
2719
|
+
if (mod.default === void 0) throw new SceneLoadError("eval", `${label} must default-export a scene or composition`);
|
|
2688
2720
|
return mod.default;
|
|
2689
2721
|
}
|
|
2722
|
+
async function loadDefault(path2) {
|
|
2723
|
+
if (path2.endsWith(".json")) {
|
|
2724
|
+
try {
|
|
2725
|
+
return JSON.parse(await readFile6(path2, "utf8"));
|
|
2726
|
+
} catch (err) {
|
|
2727
|
+
throw new SceneLoadError("eval", `failed to read ${path2}: ${clean(err)}`, { cause: err });
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return importDefault(await bundle({ path: path2 }), path2);
|
|
2731
|
+
}
|
|
2690
2732
|
function isComposition(def) {
|
|
2691
2733
|
return typeof def === "object" && def !== null && Array.isArray(def.scenes);
|
|
2692
2734
|
}
|
|
2693
|
-
|
|
2694
|
-
const def = await loadDefault(path2);
|
|
2735
|
+
function asScene(def, label) {
|
|
2695
2736
|
if (isComposition(def)) {
|
|
2696
|
-
throw new
|
|
2737
|
+
throw new SceneLoadError("validation", `${label} is a composition \u2014 render it directly, not as a single scene`);
|
|
2738
|
+
}
|
|
2739
|
+
try {
|
|
2740
|
+
validateScene(def);
|
|
2741
|
+
} catch (err) {
|
|
2742
|
+
throw new SceneLoadError("validation", clean(err), { cause: err });
|
|
2697
2743
|
}
|
|
2698
|
-
validateScene(def);
|
|
2699
2744
|
return def;
|
|
2700
2745
|
}
|
|
2746
|
+
async function loadScene(path2) {
|
|
2747
|
+
return asScene(await loadDefault(path2), path2);
|
|
2748
|
+
}
|
|
2749
|
+
async function loadSceneFromCode(code, resolveDir = process.cwd()) {
|
|
2750
|
+
return asScene(await importDefault(await bundle({ code, resolveDir }), "<source>"), "<source>");
|
|
2751
|
+
}
|
|
2701
2752
|
async function loadModule(path2) {
|
|
2702
2753
|
const def = await loadDefault(path2);
|
|
2703
2754
|
if (isComposition(def)) {
|
|
2704
|
-
|
|
2755
|
+
try {
|
|
2756
|
+
validateComposition(def);
|
|
2757
|
+
} catch (err) {
|
|
2758
|
+
throw new SceneLoadError("validation", clean(err), { cause: err });
|
|
2759
|
+
}
|
|
2705
2760
|
return { kind: "composition", ir: def };
|
|
2706
2761
|
}
|
|
2707
|
-
|
|
2708
|
-
return { kind: "scene", ir: def };
|
|
2762
|
+
return { kind: "scene", ir: asScene(def, path2) };
|
|
2709
2763
|
}
|
|
2710
|
-
var HERE, CORE_ENTRY;
|
|
2764
|
+
var HERE, CORE_ENTRY, SceneLoadError, clean, ALIAS;
|
|
2711
2765
|
var init_loadScene = __esm({
|
|
2712
2766
|
"../render-cli/src/loadScene.ts"() {
|
|
2713
2767
|
"use strict";
|
|
2714
2768
|
init_src();
|
|
2715
2769
|
HERE = dirname6(fileURLToPath4(import.meta.url));
|
|
2716
2770
|
CORE_ENTRY = true ? resolve5(HERE, "index.js") : resolve5(HERE, "..", "..", "core", "src", "index.ts");
|
|
2771
|
+
SceneLoadError = class extends Error {
|
|
2772
|
+
kind;
|
|
2773
|
+
constructor(kind, message, options) {
|
|
2774
|
+
super(message, options);
|
|
2775
|
+
this.name = "SceneLoadError";
|
|
2776
|
+
this.kind = kind;
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
clean = (err) => (err instanceof Error ? err.message : String(err)).replace(
|
|
2780
|
+
/data:text\/javascript;base64,[A-Za-z0-9+/=]+/g,
|
|
2781
|
+
"<scene bundle>"
|
|
2782
|
+
);
|
|
2783
|
+
ALIAS = { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY };
|
|
2717
2784
|
}
|
|
2718
2785
|
});
|
|
2719
2786
|
|
|
@@ -2730,6 +2797,7 @@ var ROOT2 = PACKAGED ? resolve6(HERE2, "..") : resolve6(HERE2, "..", "..", "..")
|
|
|
2730
2797
|
var USER_CWD = process.env.INIT_CWD ?? process.cwd();
|
|
2731
2798
|
var RENDER_CLI = PACKAGED ? join9(ROOT2, "dist", "cli.js") : join9(ROOT2, "packages", "render-cli", "src", "cli.ts");
|
|
2732
2799
|
var LABELS = PACKAGED ? join9(ROOT2, "dist", "labels.js") : join9(ROOT2, "packages", "render-cli", "src", "labels.ts");
|
|
2800
|
+
var COMPILE = PACKAGED ? join9(ROOT2, "dist", "compile.js") : join9(ROOT2, "packages", "render-cli", "src", "compile.ts");
|
|
2733
2801
|
var DIFF = PACKAGED ? join9(ROOT2, "dist", "diff.js") : join9(ROOT2, "packages", "render-cli", "src", "diff.ts");
|
|
2734
2802
|
var PLAYER = PACKAGED ? join9(ROOT2, "dist", "player.js") : join9(ROOT2, "packages", "render-cli", "src", "player.ts");
|
|
2735
2803
|
var ANALYZE = PACKAGED ? join9(ROOT2, "dist", "analyze.js") : join9(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
|
|
@@ -2748,6 +2816,8 @@ usage:
|
|
|
2748
2816
|
${CMD} preview open the scrub/edit UI (lists scenes in your directory)
|
|
2749
2817
|
${CMD} new <scene-name> scaffold <scene-name>.ts in your directory
|
|
2750
2818
|
${CMD} labels <scene.ts|.json> print the event clock (label \u2192 exact seconds; for sound design / timing)
|
|
2819
|
+
${CMD} compile <scene.ts|.json> [-o out.json] [--stdin] [--code "<src>"] [--json]
|
|
2820
|
+
bundle + validate a scene to SceneIR JSON, no render (fast; no ffmpeg/chromium)
|
|
2751
2821
|
${CMD} motion <mp4|framesDir> motion-profile a rendered clip
|
|
2752
2822
|
${CMD} trace <ref.mp4> [--apply scene.ts] extract a video's motion structure \u2192 MotionSketch / timeline
|
|
2753
2823
|
${CMD} diff <ref-image> [<scene.ts>] [--t S] [--mode side|blend|diff|grid] compare/measure a render against a reference image
|
|
@@ -2896,6 +2966,22 @@ ${USAGE}`);
|
|
|
2896
2966
|
await (PACKAGED ? run2(process.execPath, [LABELS, inputPath]) : run2("npx", ["tsx", LABELS, inputPath]))
|
|
2897
2967
|
);
|
|
2898
2968
|
}
|
|
2969
|
+
case "compile": {
|
|
2970
|
+
const hasInlineSource = rest.includes("--stdin") || rest.includes("--code");
|
|
2971
|
+
const fileArg = rest.find((a, i) => !a.startsWith("-") && !["-o", "--code", "--timeout"].includes(rest[i - 1] ?? ""));
|
|
2972
|
+
if (!fileArg && !hasInlineSource) fail(`compile needs a scene file, --stdin, or --code "<src>"
|
|
2973
|
+
|
|
2974
|
+
${USAGE}`);
|
|
2975
|
+
const passed = rest.map((a, i) => {
|
|
2976
|
+
if (a === fileArg) return userPath(a);
|
|
2977
|
+
if (rest[i - 1] === "-o") return userPath(a);
|
|
2978
|
+
return a;
|
|
2979
|
+
});
|
|
2980
|
+
if (fileArg && !existsSync6(userPath(fileArg))) fail(`no such file: ${userPath(fileArg)}`);
|
|
2981
|
+
process.exit(
|
|
2982
|
+
await (PACKAGED ? run2(process.execPath, [COMPILE, ...passed]) : run2("npx", ["tsx", COMPILE, ...passed]))
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2899
2985
|
case "player": {
|
|
2900
2986
|
const input = rest[0];
|
|
2901
2987
|
if (!input || input.startsWith("-")) fail(`player needs a scene file
|
package/dist/browserEntry.js
CHANGED
|
@@ -350,7 +350,7 @@
|
|
|
350
350
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
351
351
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
352
352
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
353
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
353
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
354
354
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
355
355
|
group: COMMON_PROPS
|
|
356
356
|
};
|
package/dist/cli.js
CHANGED
|
@@ -355,7 +355,7 @@ var PROPS_BY_TYPE = {
|
|
|
355
355
|
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
|
|
356
356
|
text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "prefix", "suffix", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
357
357
|
image: [...COMMON_PROPS, "src", "width", "height", "fit"],
|
|
358
|
-
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
|
|
358
|
+
video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume", "fadeIn", "pan"],
|
|
359
359
|
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
360
360
|
group: COMMON_PROPS
|
|
361
361
|
};
|
|
@@ -592,6 +592,15 @@ function validateScene(ir) {
|
|
|
592
592
|
if (cue.gain !== void 0 && cue.gain < 0) {
|
|
593
593
|
problems.push(`audio.cues[${i}]: gain must be >= 0`);
|
|
594
594
|
}
|
|
595
|
+
if (cue.fadeIn !== void 0 && cue.fadeIn < 0) {
|
|
596
|
+
problems.push(`audio.cues[${i}]: fadeIn must be >= 0`);
|
|
597
|
+
}
|
|
598
|
+
if (cue.fadeOut !== void 0 && cue.fadeOut < 0) {
|
|
599
|
+
problems.push(`audio.cues[${i}]: fadeOut must be >= 0`);
|
|
600
|
+
}
|
|
601
|
+
if (cue.pan !== void 0 && (cue.pan < -1 || cue.pan > 1)) {
|
|
602
|
+
problems.push(`audio.cues[${i}]: pan must be in [-1, 1] (-1 left \u2026 +1 right)`);
|
|
603
|
+
}
|
|
595
604
|
}
|
|
596
605
|
const duck = ir.audio?.bgm?.duck;
|
|
597
606
|
if (typeof duck === "object" && duck !== null && duck.depth !== void 0 && (duck.depth < 0 || duck.depth > 1)) {
|
|
@@ -919,7 +928,7 @@ function collectClipAudio(ir, duration, warnings) {
|
|
|
919
928
|
warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
|
|
920
929
|
continue;
|
|
921
930
|
}
|
|
922
|
-
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain });
|
|
931
|
+
out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain, fadeIn: node.props.fadeIn ?? 0, pan: node.props.pan ?? 0 });
|
|
923
932
|
}
|
|
924
933
|
if (node.type === "group") walk(node.children);
|
|
925
934
|
}
|
|
@@ -961,6 +970,9 @@ function resolveAudioPlan(compiled) {
|
|
|
961
970
|
t,
|
|
962
971
|
gain: cue.gain ?? 1,
|
|
963
972
|
duration: cueDuration,
|
|
973
|
+
fadeIn: cue.fadeIn ?? 0,
|
|
974
|
+
fadeOut: cue.fadeOut ?? 0,
|
|
975
|
+
pan: cue.pan ?? 0,
|
|
964
976
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
965
977
|
});
|
|
966
978
|
}
|
|
@@ -1038,6 +1050,9 @@ function resolveCompositionAudioPlan(comp) {
|
|
|
1038
1050
|
t,
|
|
1039
1051
|
gain: cue.gain ?? 1,
|
|
1040
1052
|
duration: cueDuration,
|
|
1053
|
+
fadeIn: cue.fadeIn ?? 0,
|
|
1054
|
+
fadeOut: cue.fadeOut ?? 0,
|
|
1055
|
+
pan: cue.pan ?? 0,
|
|
1041
1056
|
source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
|
|
1042
1057
|
});
|
|
1043
1058
|
}
|
|
@@ -1457,6 +1472,10 @@ function atempoChain(rate) {
|
|
|
1457
1472
|
out.push(`atempo=${r.toFixed(4)}`);
|
|
1458
1473
|
return out;
|
|
1459
1474
|
}
|
|
1475
|
+
function panFilter(pan) {
|
|
1476
|
+
const clamp = (v) => Math.max(0, Math.min(1, v)).toFixed(4);
|
|
1477
|
+
return `pan=stereo|c0=${clamp(1 - pan)}*c0|c1=${clamp(1 + pan)}*c1`;
|
|
1478
|
+
}
|
|
1460
1479
|
function buildFilterGraph(plan, inputs) {
|
|
1461
1480
|
const lines = [];
|
|
1462
1481
|
const mixIn = ["[anchor]"];
|
|
@@ -1485,7 +1504,14 @@ function buildFilterGraph(plan, inputs) {
|
|
|
1485
1504
|
}
|
|
1486
1505
|
plan.cues.forEach((cue, i) => {
|
|
1487
1506
|
const delayMs = Math.round(cue.t * 1e3);
|
|
1488
|
-
|
|
1507
|
+
const chain = [FORMAT, `volume=${cue.gain}`];
|
|
1508
|
+
if (cue.fadeIn > 0) chain.push(`afade=t=in:st=0:d=${cue.fadeIn}`);
|
|
1509
|
+
if (cue.fadeOut > 0) {
|
|
1510
|
+
chain.push(`afade=t=out:st=${Math.max(0, cue.duration - cue.fadeOut).toFixed(3)}:d=${cue.fadeOut}`);
|
|
1511
|
+
}
|
|
1512
|
+
if (cue.pan !== 0) chain.push(panFilter(cue.pan));
|
|
1513
|
+
chain.push(`adelay=${delayMs}:all=1`);
|
|
1514
|
+
lines.push(`[${inputIndex}:a]${chain.join(",")}[c${i}]`);
|
|
1489
1515
|
mixIn.push(`[c${i}]`);
|
|
1490
1516
|
inputIndex++;
|
|
1491
1517
|
});
|
|
@@ -1493,6 +1519,8 @@ function buildFilterGraph(plan, inputs) {
|
|
|
1493
1519
|
const chain = [];
|
|
1494
1520
|
if (audio.clipStart > 0) chain.push(`atrim=start=${audio.clipStart.toFixed(3)}`, "asetpts=PTS-STARTPTS");
|
|
1495
1521
|
chain.push(...atempoChain(audio.rate), FORMAT, `volume=${audio.gain}`);
|
|
1522
|
+
if (audio.fadeIn > 0) chain.push(`afade=t=in:st=0:d=${audio.fadeIn}`);
|
|
1523
|
+
if (audio.pan !== 0) chain.push(panFilter(audio.pan));
|
|
1496
1524
|
const delayMs = Math.round(audio.start * 1e3);
|
|
1497
1525
|
if (delayMs > 0) chain.push(`adelay=${delayMs}:all=1`);
|
|
1498
1526
|
lines.push(`[${inputIndex}:a]${chain.join(",")}[k${i}]`);
|
|
@@ -1878,13 +1906,13 @@ async function captureIr(ir, opts) {
|
|
|
1878
1906
|
const assets = await buildImageAssets(ir, sceneDir);
|
|
1879
1907
|
const { fps, duration } = resolveTiming(ir, opts);
|
|
1880
1908
|
const videoAssets = await buildVideoFrameAssets(ir, sceneDir, fps, duration);
|
|
1881
|
-
const
|
|
1909
|
+
const bundle2 = await browserBundle();
|
|
1882
1910
|
return withPage(ir.size, async (page) => {
|
|
1883
1911
|
await page.setContent(
|
|
1884
1912
|
`<!DOCTYPE html><html><body style="margin:0;background:#000"></body></html>`
|
|
1885
1913
|
);
|
|
1886
1914
|
await injectFonts(page);
|
|
1887
|
-
await page.addScriptTag({ content:
|
|
1915
|
+
await page.addScriptTag({ content: bundle2 });
|
|
1888
1916
|
await page.evaluate(
|
|
1889
1917
|
([sceneIr, imageAssets, vAssets]) => window.__reframe.init(sceneIr, imageAssets, vAssets),
|
|
1890
1918
|
[ir, assets, videoAssets]
|
|
@@ -2049,42 +2077,84 @@ import { dirname as dirname6, resolve as resolve5 } from "node:path";
|
|
|
2049
2077
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2050
2078
|
var HERE = dirname6(fileURLToPath4(import.meta.url));
|
|
2051
2079
|
var CORE_ENTRY = true ? resolve5(HERE, "index.js") : resolve5(HERE, "..", "..", "core", "src", "index.ts");
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2080
|
+
var SceneLoadError = class extends Error {
|
|
2081
|
+
kind;
|
|
2082
|
+
constructor(kind, message, options) {
|
|
2083
|
+
super(message, options);
|
|
2084
|
+
this.name = "SceneLoadError";
|
|
2085
|
+
this.kind = kind;
|
|
2086
|
+
}
|
|
2087
|
+
};
|
|
2088
|
+
var clean = (err) => (err instanceof Error ? err.message : String(err)).replace(
|
|
2089
|
+
/data:text\/javascript;base64,[A-Za-z0-9+/=]+/g,
|
|
2090
|
+
"<scene bundle>"
|
|
2091
|
+
);
|
|
2092
|
+
var ALIAS = { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY };
|
|
2093
|
+
async function bundle(input) {
|
|
2094
|
+
const common = {
|
|
2095
|
+
bundle: true,
|
|
2096
|
+
format: "esm",
|
|
2097
|
+
platform: "neutral",
|
|
2098
|
+
write: false,
|
|
2099
|
+
logLevel: "silent",
|
|
2100
|
+
sourcemap: "inline",
|
|
2101
|
+
alias: ALIAS
|
|
2102
|
+
};
|
|
2055
2103
|
try {
|
|
2056
|
-
const out = await build2(
|
|
2057
|
-
entryPoints: [
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
});
|
|
2068
|
-
code = out.outputFiles[0].text;
|
|
2104
|
+
const out = await build2(
|
|
2105
|
+
"path" in input ? { ...common, entryPoints: [input.path] } : { ...common, stdin: { contents: input.code, resolveDir: input.resolveDir, loader: "ts", sourcefile: "scene.ts" } }
|
|
2106
|
+
);
|
|
2107
|
+
return out.outputFiles[0].text;
|
|
2108
|
+
} catch (err) {
|
|
2109
|
+
throw new SceneLoadError("bundle", clean(err), { cause: err });
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
async function importDefault(code, label) {
|
|
2113
|
+
let mod;
|
|
2114
|
+
try {
|
|
2115
|
+
mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
|
|
2069
2116
|
} catch (err) {
|
|
2070
|
-
|
|
2071
|
-
|
|
2117
|
+
const kind = err instanceof Error && err.name === "SceneValidationError" ? "validation" : "eval";
|
|
2118
|
+
throw new SceneLoadError(kind, clean(err), { cause: err });
|
|
2072
2119
|
}
|
|
2073
|
-
|
|
2074
|
-
if (mod.default === void 0) throw new Error(`${path2} must default-export a scene or composition`);
|
|
2120
|
+
if (mod.default === void 0) throw new SceneLoadError("eval", `${label} must default-export a scene or composition`);
|
|
2075
2121
|
return mod.default;
|
|
2076
2122
|
}
|
|
2123
|
+
async function loadDefault(path2) {
|
|
2124
|
+
if (path2.endsWith(".json")) {
|
|
2125
|
+
try {
|
|
2126
|
+
return JSON.parse(await readFile4(path2, "utf8"));
|
|
2127
|
+
} catch (err) {
|
|
2128
|
+
throw new SceneLoadError("eval", `failed to read ${path2}: ${clean(err)}`, { cause: err });
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return importDefault(await bundle({ path: path2 }), path2);
|
|
2132
|
+
}
|
|
2077
2133
|
function isComposition(def) {
|
|
2078
2134
|
return typeof def === "object" && def !== null && Array.isArray(def.scenes);
|
|
2079
2135
|
}
|
|
2136
|
+
function asScene(def, label) {
|
|
2137
|
+
if (isComposition(def)) {
|
|
2138
|
+
throw new SceneLoadError("validation", `${label} is a composition \u2014 render it directly, not as a single scene`);
|
|
2139
|
+
}
|
|
2140
|
+
try {
|
|
2141
|
+
validateScene(def);
|
|
2142
|
+
} catch (err) {
|
|
2143
|
+
throw new SceneLoadError("validation", clean(err), { cause: err });
|
|
2144
|
+
}
|
|
2145
|
+
return def;
|
|
2146
|
+
}
|
|
2080
2147
|
async function loadModule(path2) {
|
|
2081
2148
|
const def = await loadDefault(path2);
|
|
2082
2149
|
if (isComposition(def)) {
|
|
2083
|
-
|
|
2150
|
+
try {
|
|
2151
|
+
validateComposition(def);
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
throw new SceneLoadError("validation", clean(err), { cause: err });
|
|
2154
|
+
}
|
|
2084
2155
|
return { kind: "composition", ir: def };
|
|
2085
2156
|
}
|
|
2086
|
-
|
|
2087
|
-
return { kind: "scene", ir: def };
|
|
2157
|
+
return { kind: "scene", ir: asScene(def, path2) };
|
|
2088
2158
|
}
|
|
2089
2159
|
|
|
2090
2160
|
// ../render-cli/src/cli.ts
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CompositionIR, SceneIR } from "./index.js";
|
|
2
|
+
export declare class SceneLoadError extends Error { readonly kind: "bundle" | "eval" | "validation"; }
|
|
3
|
+
export declare function loadScene(path: string): Promise<SceneIR>;
|
|
4
|
+
export declare function loadSceneFromCode(code: string, resolveDir?: string): Promise<SceneIR>;
|
|
5
|
+
export declare function isComposition(def: unknown): def is CompositionIR;
|
|
6
|
+
export declare function loadModule(path: string): Promise<{ kind: "scene"; ir: SceneIR } | { kind: "composition"; ir: CompositionIR }>;
|