reframe-video 0.6.18 → 0.6.20

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.
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "reframe",
3
+ "owner": {
4
+ "name": "Kiyeon Jeon",
5
+ "url": "https://github.com/kiyeonjeon21"
6
+ },
7
+ "plugins": [
8
+ {
9
+ "name": "reframe",
10
+ "source": "./",
11
+ "description": "Motion-graphics videos as addressable data — generate, tweak, regenerate without losing human edits."
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "reframe",
3
+ "version": "0.1.0",
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
+ "author": {
6
+ "name": "Kiyeon Jeon",
7
+ "url": "https://github.com/kiyeonjeon21"
8
+ }
9
+ }
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
@@ -2480,13 +2480,13 @@ async function captureIr(ir, opts) {
2480
2480
  const assets = await buildImageAssets(ir, sceneDir);
2481
2481
  const { fps, duration } = resolveTiming(ir, opts);
2482
2482
  const videoAssets = await buildVideoFrameAssets(ir, sceneDir, fps, duration);
2483
- const bundle = await browserBundle();
2483
+ const bundle2 = await browserBundle();
2484
2484
  return withPage(ir.size, async (page) => {
2485
2485
  await page.setContent(
2486
2486
  `<!DOCTYPE html><html><body style="margin:0;background:#000"></body></html>`
2487
2487
  );
2488
2488
  await injectFonts(page);
2489
- await page.addScriptTag({ content: bundle });
2489
+ await page.addScriptTag({ content: bundle2 });
2490
2490
  await page.evaluate(
2491
2491
  ([sceneIr, imageAssets, vAssets]) => window.__reframe.init(sceneIr, imageAssets, vAssets),
2492
2492
  [ir, assets, videoAssets]
@@ -2679,66 +2679,108 @@ var init_batch = __esm({
2679
2679
  // ../render-cli/src/loadScene.ts
2680
2680
  var loadScene_exports = {};
2681
2681
  __export(loadScene_exports, {
2682
+ SceneLoadError: () => SceneLoadError,
2682
2683
  isComposition: () => isComposition,
2683
2684
  loadModule: () => loadModule,
2684
- loadScene: () => loadScene
2685
+ loadScene: () => loadScene,
2686
+ loadSceneFromCode: () => loadSceneFromCode
2685
2687
  });
2686
2688
  import { build as build2 } from "esbuild";
2687
2689
  import { readFile as readFile6 } from "node:fs/promises";
2688
2690
  import { dirname as dirname6, resolve as resolve5 } from "node:path";
2689
2691
  import { fileURLToPath as fileURLToPath4 } from "node:url";
2690
- async function loadDefault(path2) {
2691
- if (path2.endsWith(".json")) return JSON.parse(await readFile6(path2, "utf8"));
2692
- let code;
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
+ };
2693
2702
  try {
2694
- const out = await build2({
2695
- entryPoints: [path2],
2696
- bundle: true,
2697
- format: "esm",
2698
- platform: "neutral",
2699
- write: false,
2700
- logLevel: "silent",
2701
- sourcemap: "inline",
2702
- // both specifiers accepted: the guide's canonical "@reframe/core" and
2703
- // the published package name
2704
- alias: { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY }
2705
- });
2706
- 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;
2707
2707
  } catch (err) {
2708
- throw new Error(`failed to bundle ${path2}:
2709
- ${err instanceof Error ? err.message : String(err)}`);
2708
+ throw new SceneLoadError("bundle", clean(err), { cause: err });
2710
2709
  }
2711
- const mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
2712
- if (mod.default === void 0) throw new Error(`${path2} must default-export a scene or composition`);
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`);
2713
2720
  return mod.default;
2714
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
+ }
2715
2732
  function isComposition(def) {
2716
2733
  return typeof def === "object" && def !== null && Array.isArray(def.scenes);
2717
2734
  }
2718
- async function loadScene(path2) {
2719
- const def = await loadDefault(path2);
2735
+ function asScene(def, label) {
2720
2736
  if (isComposition(def)) {
2721
- throw new Error(`${path2} is a composition \u2014 render it directly, not as a single scene`);
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 });
2722
2743
  }
2723
- validateScene(def);
2724
2744
  return def;
2725
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
+ }
2726
2752
  async function loadModule(path2) {
2727
2753
  const def = await loadDefault(path2);
2728
2754
  if (isComposition(def)) {
2729
- validateComposition(def);
2755
+ try {
2756
+ validateComposition(def);
2757
+ } catch (err) {
2758
+ throw new SceneLoadError("validation", clean(err), { cause: err });
2759
+ }
2730
2760
  return { kind: "composition", ir: def };
2731
2761
  }
2732
- validateScene(def);
2733
- return { kind: "scene", ir: def };
2762
+ return { kind: "scene", ir: asScene(def, path2) };
2734
2763
  }
2735
- var HERE, CORE_ENTRY;
2764
+ var HERE, CORE_ENTRY, SceneLoadError, clean, ALIAS;
2736
2765
  var init_loadScene = __esm({
2737
2766
  "../render-cli/src/loadScene.ts"() {
2738
2767
  "use strict";
2739
2768
  init_src();
2740
2769
  HERE = dirname6(fileURLToPath4(import.meta.url));
2741
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 };
2742
2784
  }
2743
2785
  });
2744
2786
 
@@ -2755,7 +2797,9 @@ var ROOT2 = PACKAGED ? resolve6(HERE2, "..") : resolve6(HERE2, "..", "..", "..")
2755
2797
  var USER_CWD = process.env.INIT_CWD ?? process.cwd();
2756
2798
  var RENDER_CLI = PACKAGED ? join9(ROOT2, "dist", "cli.js") : join9(ROOT2, "packages", "render-cli", "src", "cli.ts");
2757
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");
2758
2801
  var DIFF = PACKAGED ? join9(ROOT2, "dist", "diff.js") : join9(ROOT2, "packages", "render-cli", "src", "diff.ts");
2802
+ var FRAME = PACKAGED ? join9(ROOT2, "dist", "frame.js") : join9(ROOT2, "packages", "render-cli", "src", "frame.ts");
2759
2803
  var PLAYER = PACKAGED ? join9(ROOT2, "dist", "player.js") : join9(ROOT2, "packages", "render-cli", "src", "player.ts");
2760
2804
  var ANALYZE = PACKAGED ? join9(ROOT2, "dist", "analyze.js") : join9(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
2761
2805
  var TRACE = PACKAGED ? join9(ROOT2, "dist", "trace-cli.js") : join9(ROOT2, "benchmark", "harness", "motion", "trace-cli.ts");
@@ -2773,6 +2817,10 @@ usage:
2773
2817
  ${CMD} preview open the scrub/edit UI (lists scenes in your directory)
2774
2818
  ${CMD} new <scene-name> scaffold <scene-name>.ts in your directory
2775
2819
  ${CMD} labels <scene.ts|.json> print the event clock (label \u2192 exact seconds; for sound design / timing)
2820
+ ${CMD} compile <scene.ts|.json> [-o out.json] [--stdin] [--code "<src>"] [--json]
2821
+ bundle + validate a scene to SceneIR JSON, no render (fast; no ffmpeg/chromium)
2822
+ ${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)
2823
+ ${CMD} skill [--path] print the authoring skill (SKILL.md) for an agent; --path prints the plugin dir to load
2776
2824
  ${CMD} motion <mp4|framesDir> motion-profile a rendered clip
2777
2825
  ${CMD} trace <ref.mp4> [--apply scene.ts] extract a video's motion structure \u2192 MotionSketch / timeline
2778
2826
  ${CMD} diff <ref-image> [<scene.ts>] [--t S] [--mode side|blend|diff|grid] compare/measure a render against a reference image
@@ -2921,6 +2969,42 @@ ${USAGE}`);
2921
2969
  await (PACKAGED ? run2(process.execPath, [LABELS, inputPath]) : run2("npx", ["tsx", LABELS, inputPath]))
2922
2970
  );
2923
2971
  }
2972
+ case "compile": {
2973
+ const hasInlineSource = rest.includes("--stdin") || rest.includes("--code");
2974
+ const fileArg = rest.find((a, i) => !a.startsWith("-") && !["-o", "--code", "--timeout"].includes(rest[i - 1] ?? ""));
2975
+ if (!fileArg && !hasInlineSource) fail(`compile needs a scene file, --stdin, or --code "<src>"
2976
+
2977
+ ${USAGE}`);
2978
+ const passed = rest.map((a, i) => {
2979
+ if (a === fileArg) return userPath(a);
2980
+ if (rest[i - 1] === "-o") return userPath(a);
2981
+ return a;
2982
+ });
2983
+ if (fileArg && !existsSync6(userPath(fileArg))) fail(`no such file: ${userPath(fileArg)}`);
2984
+ process.exit(
2985
+ await (PACKAGED ? run2(process.execPath, [COMPILE, ...passed]) : run2("npx", ["tsx", COMPILE, ...passed]))
2986
+ );
2987
+ }
2988
+ case "frame": {
2989
+ const input = rest[0];
2990
+ if (!input || input.startsWith("-")) fail(`frame needs a scene file
2991
+
2992
+ ${USAGE}`);
2993
+ const inputPath = userPath(input);
2994
+ if (!existsSync6(inputPath)) fail(`no such file: ${inputPath}`);
2995
+ const args = rest.slice(1);
2996
+ const outBase = PACKAGED ? join9(USER_CWD, "out") : join9(ROOT2, "out");
2997
+ const stem = `${basename(input).replace(/\.[^.]+$/, "")}.png`;
2998
+ let outArgs = args;
2999
+ if (args.indexOf("-o") === -1) {
3000
+ await mkdir4(outBase, { recursive: true });
3001
+ outArgs = [...args, "-o", join9(outBase, stem)];
3002
+ }
3003
+ outArgs = outArgs.map((a, i) => outArgs[i - 1] === "-o" ? userPath(a) : a);
3004
+ process.exit(
3005
+ await (PACKAGED ? run2(process.execPath, [FRAME, inputPath, ...outArgs]) : run2("npx", ["tsx", FRAME, inputPath, ...outArgs]))
3006
+ );
3007
+ }
2924
3008
  case "player": {
2925
3009
  const input = rest[0];
2926
3010
  if (!input || input.startsWith("-")) fail(`player needs a scene file
@@ -3109,6 +3193,16 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
3109
3193
  process.stdout.write(await readFile7(file, "utf8"));
3110
3194
  return;
3111
3195
  }
3196
+ case "skill": {
3197
+ if (rest.includes("--path")) {
3198
+ process.stdout.write(`${ROOT2}
3199
+ `);
3200
+ return;
3201
+ }
3202
+ const { readFile: readFile7 } = await import("node:fs/promises");
3203
+ process.stdout.write(await readFile7(join9(ROOT2, "skills", "reframe", "SKILL.md"), "utf8"));
3204
+ return;
3205
+ }
3112
3206
  case "demo":
3113
3207
  if (PACKAGED) {
3114
3208
  fail(
package/dist/cli.js CHANGED
@@ -1906,13 +1906,13 @@ async function captureIr(ir, opts) {
1906
1906
  const assets = await buildImageAssets(ir, sceneDir);
1907
1907
  const { fps, duration } = resolveTiming(ir, opts);
1908
1908
  const videoAssets = await buildVideoFrameAssets(ir, sceneDir, fps, duration);
1909
- const bundle = await browserBundle();
1909
+ const bundle2 = await browserBundle();
1910
1910
  return withPage(ir.size, async (page) => {
1911
1911
  await page.setContent(
1912
1912
  `<!DOCTYPE html><html><body style="margin:0;background:#000"></body></html>`
1913
1913
  );
1914
1914
  await injectFonts(page);
1915
- await page.addScriptTag({ content: bundle });
1915
+ await page.addScriptTag({ content: bundle2 });
1916
1916
  await page.evaluate(
1917
1917
  ([sceneIr, imageAssets, vAssets]) => window.__reframe.init(sceneIr, imageAssets, vAssets),
1918
1918
  [ir, assets, videoAssets]
@@ -2077,42 +2077,84 @@ import { dirname as dirname6, resolve as resolve5 } from "node:path";
2077
2077
  import { fileURLToPath as fileURLToPath4 } from "node:url";
2078
2078
  var HERE = dirname6(fileURLToPath4(import.meta.url));
2079
2079
  var CORE_ENTRY = true ? resolve5(HERE, "index.js") : resolve5(HERE, "..", "..", "core", "src", "index.ts");
2080
- async function loadDefault(path2) {
2081
- if (path2.endsWith(".json")) return JSON.parse(await readFile4(path2, "utf8"));
2082
- let code;
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
+ };
2083
2103
  try {
2084
- const out = await build2({
2085
- entryPoints: [path2],
2086
- bundle: true,
2087
- format: "esm",
2088
- platform: "neutral",
2089
- write: false,
2090
- logLevel: "silent",
2091
- sourcemap: "inline",
2092
- // both specifiers accepted: the guide's canonical "@reframe/core" and
2093
- // the published package name
2094
- alias: { "@reframe/core": CORE_ENTRY, "reframe-video": CORE_ENTRY }
2095
- });
2096
- 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;
2097
2108
  } catch (err) {
2098
- throw new Error(`failed to bundle ${path2}:
2099
- ${err instanceof Error ? err.message : String(err)}`);
2109
+ throw new SceneLoadError("bundle", clean(err), { cause: err });
2100
2110
  }
2101
- const mod = await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`);
2102
- if (mod.default === void 0) throw new Error(`${path2} must default-export a scene or composition`);
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")}`);
2116
+ } catch (err) {
2117
+ const kind = err instanceof Error && err.name === "SceneValidationError" ? "validation" : "eval";
2118
+ throw new SceneLoadError(kind, clean(err), { cause: err });
2119
+ }
2120
+ if (mod.default === void 0) throw new SceneLoadError("eval", `${label} must default-export a scene or composition`);
2103
2121
  return mod.default;
2104
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
+ }
2105
2133
  function isComposition(def) {
2106
2134
  return typeof def === "object" && def !== null && Array.isArray(def.scenes);
2107
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
+ }
2108
2147
  async function loadModule(path2) {
2109
2148
  const def = await loadDefault(path2);
2110
2149
  if (isComposition(def)) {
2111
- validateComposition(def);
2150
+ try {
2151
+ validateComposition(def);
2152
+ } catch (err) {
2153
+ throw new SceneLoadError("validation", clean(err), { cause: err });
2154
+ }
2112
2155
  return { kind: "composition", ir: def };
2113
2156
  }
2114
- validateScene(def);
2115
- return { kind: "scene", ir: def };
2157
+ return { kind: "scene", ir: asScene(def, path2) };
2116
2158
  }
2117
2159
 
2118
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 }>;