reframe-video 0.6.5 → 0.6.7

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/dist/bin.js CHANGED
@@ -622,6 +622,7 @@ var init_validate = __esm({
622
622
  line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress", ...FX_PROPS],
623
623
  text: [...COMMON_PROPS, "content", "contentDecimals", "contentThousands", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
624
624
  image: [...COMMON_PROPS, "src", "width", "height", "fit"],
625
+ video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
625
626
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
626
627
  group: COMMON_PROPS
627
628
  };
@@ -1209,11 +1210,34 @@ var init_motionOps = __esm({
1209
1210
  });
1210
1211
 
1211
1212
  // ../core/src/audio.ts
1213
+ function collectClipAudio(ir, duration, warnings) {
1214
+ const out = [];
1215
+ const walk = (nodes) => {
1216
+ for (const node of nodes) {
1217
+ if (node.type === "video") {
1218
+ const gain = node.props.volume ?? 1;
1219
+ const start = node.props.start ?? 0;
1220
+ if (gain <= 0) continue;
1221
+ if (start >= duration) {
1222
+ warnings.push(`video "${node.id}": start ${start.toFixed(2)}s past the scene end \u2014 audio dropped`);
1223
+ continue;
1224
+ }
1225
+ out.push({ nodeId: node.id, src: node.props.src, start, rate: node.props.rate ?? 1, clipStart: node.props.clipStart ?? 0, gain });
1226
+ }
1227
+ if (node.type === "group") walk(node.children);
1228
+ }
1229
+ };
1230
+ walk(ir.nodes);
1231
+ return out;
1232
+ }
1212
1233
  function resolveAudioPlan(compiled) {
1213
1234
  const audio = compiled.ir.audio;
1214
- if (!audio || !audio.bgm && (audio.cues ?? []).length === 0) return null;
1215
1235
  const warnings = [];
1216
1236
  const duration = compiled.duration;
1237
+ const clipAudio = collectClipAudio(compiled.ir, duration, warnings);
1238
+ if (!audio || !audio.bgm && (audio.cues ?? []).length === 0) {
1239
+ return clipAudio.length === 0 ? null : { duration, bgm: null, cues: [], duckWindows: [], clipAudio, warnings };
1240
+ }
1217
1241
  const cues = [];
1218
1242
  for (const [index, cue] of (audio.cues ?? []).entries()) {
1219
1243
  let anchor;
@@ -1249,6 +1273,7 @@ function resolveAudioPlan(compiled) {
1249
1273
  bgm: resolveBgm(audio.bgm),
1250
1274
  cues,
1251
1275
  duckWindows: mergeDuckWindows(cues, duration),
1276
+ clipAudio,
1252
1277
  warnings
1253
1278
  };
1254
1279
  }
@@ -1363,13 +1388,13 @@ var init_evaluate = __esm({
1363
1388
  });
1364
1389
 
1365
1390
  // ../core/src/assets.ts
1366
- function collectImageSrcs(ir) {
1391
+ function collectSrcs(ir, type) {
1367
1392
  const srcs = /* @__PURE__ */ new Set();
1368
- const imageIds = /* @__PURE__ */ new Set();
1393
+ const ids = /* @__PURE__ */ new Set();
1369
1394
  const walkNodes = (nodes) => {
1370
1395
  for (const node of nodes) {
1371
- if (node.type === "image") {
1372
- imageIds.add(node.id);
1396
+ if (node.type === type) {
1397
+ ids.add(node.id);
1373
1398
  srcs.add(node.props.src);
1374
1399
  }
1375
1400
  if (node.type === "group") walkNodes(node.children);
@@ -1378,14 +1403,14 @@ function collectImageSrcs(ir) {
1378
1403
  walkNodes(ir.nodes);
1379
1404
  for (const overrides of Object.values(ir.states ?? {})) {
1380
1405
  for (const [nodeId, props] of Object.entries(overrides)) {
1381
- if (imageIds.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
1406
+ if (ids.has(nodeId) && typeof props.src === "string") srcs.add(props.src);
1382
1407
  }
1383
1408
  }
1384
1409
  const walkTimeline = (step) => {
1385
1410
  if (!step) return;
1386
1411
  if (step.kind === "seq" || step.kind === "par" || step.kind === "stagger") {
1387
1412
  for (const child of step.children) walkTimeline(child);
1388
- } else if (step.kind === "tween" && imageIds.has(step.target)) {
1413
+ } else if (step.kind === "tween" && ids.has(step.target)) {
1389
1414
  const src = step.props.src;
1390
1415
  if (typeof src === "string") srcs.add(src);
1391
1416
  }
@@ -1393,6 +1418,12 @@ function collectImageSrcs(ir) {
1393
1418
  walkTimeline(ir.timeline);
1394
1419
  return [...srcs];
1395
1420
  }
1421
+ function collectImageSrcs(ir) {
1422
+ return collectSrcs(ir, "image");
1423
+ }
1424
+ function collectVideoSrcs(ir) {
1425
+ return collectSrcs(ir, "video");
1426
+ }
1396
1427
  var init_assets = __esm({
1397
1428
  "../core/src/assets.ts"() {
1398
1429
  "use strict";
@@ -1824,11 +1855,89 @@ var init_sfx = __esm({
1824
1855
  }
1825
1856
  });
1826
1857
 
1827
- // ../render-cli/src/audio/mux.ts
1858
+ // ../render-cli/src/audio/clip.ts
1828
1859
  import { spawn } from "node:child_process";
1860
+ import { existsSync as existsSync3 } from "node:fs";
1861
+ import { isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "node:path";
1862
+ function run(cmd, args) {
1863
+ return new Promise((res, reject) => {
1864
+ const proc = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1865
+ let stdout = "", stderr = "";
1866
+ proc.stdout.on("data", (d) => stdout += d.toString());
1867
+ proc.stderr.on("data", (d) => stderr += d.toString());
1868
+ proc.on("close", (code) => res({ code: code ?? 1, stdout, stderr }));
1869
+ proc.on("error", reject);
1870
+ });
1871
+ }
1872
+ async function hasAudioStream(file) {
1873
+ const { stdout } = await run("ffprobe", [
1874
+ "-v",
1875
+ "error",
1876
+ "-select_streams",
1877
+ "a",
1878
+ "-show_entries",
1879
+ "stream=index",
1880
+ "-of",
1881
+ "csv=p=0",
1882
+ file
1883
+ ]);
1884
+ return stdout.trim().length > 0;
1885
+ }
1886
+ function resolveSrc(src, sceneDir) {
1887
+ const candidates = [isAbsolute2(src) ? src : null, resolve2(sceneDir, src)].filter(
1888
+ (c) => c !== null
1889
+ );
1890
+ const found = candidates.find((c) => existsSync3(c));
1891
+ if (!found) throw new Error(`video "${src}" not found (tried: ${candidates.join(", ")})`);
1892
+ return found;
1893
+ }
1894
+ async function resolveClipAudio(entry, sceneDir, workDir) {
1895
+ const src = resolveSrc(entry.src, sceneDir);
1896
+ if (!await hasAudioStream(src)) return null;
1897
+ const out = join2(workDir, `clip-${entry.nodeId}.wav`);
1898
+ const { code, stderr } = await run("ffmpeg", [
1899
+ "-y",
1900
+ "-i",
1901
+ src,
1902
+ "-vn",
1903
+ "-ac",
1904
+ "2",
1905
+ "-ar",
1906
+ "44100",
1907
+ "-c:a",
1908
+ "pcm_s16le",
1909
+ out
1910
+ ]);
1911
+ if (code !== 0) throw new Error(`clip audio extract failed for "${entry.src}":
1912
+ ${stderr.slice(-1500)}`);
1913
+ return out;
1914
+ }
1915
+ var init_clip = __esm({
1916
+ "../render-cli/src/audio/clip.ts"() {
1917
+ "use strict";
1918
+ }
1919
+ });
1920
+
1921
+ // ../render-cli/src/audio/mux.ts
1922
+ import { spawn as spawn2 } from "node:child_process";
1829
1923
  import { mkdtemp, rm, writeFile as writeFile2 } from "node:fs/promises";
1830
1924
  import { tmpdir as tmpdir2 } from "node:os";
1831
- import { join as join2 } from "node:path";
1925
+ import { join as join3 } from "node:path";
1926
+ function atempoChain(rate) {
1927
+ if (!(rate > 0) || rate === 1) return [];
1928
+ const out = [];
1929
+ let r = rate;
1930
+ while (r > 2) {
1931
+ out.push("atempo=2.0");
1932
+ r /= 2;
1933
+ }
1934
+ while (r < 0.5) {
1935
+ out.push("atempo=0.5");
1936
+ r /= 0.5;
1937
+ }
1938
+ out.push(`atempo=${r.toFixed(4)}`);
1939
+ return out;
1940
+ }
1832
1941
  function buildFilterGraph(plan, inputs) {
1833
1942
  const lines = [];
1834
1943
  const mixIn = ["[anchor]"];
@@ -1861,15 +1970,25 @@ function buildFilterGraph(plan, inputs) {
1861
1970
  mixIn.push(`[c${i}]`);
1862
1971
  inputIndex++;
1863
1972
  });
1973
+ (inputs.clipFiles ?? []).forEach(({ audio }, i) => {
1974
+ const chain = [];
1975
+ if (audio.clipStart > 0) chain.push(`atrim=start=${audio.clipStart.toFixed(3)}`, "asetpts=PTS-STARTPTS");
1976
+ chain.push(...atempoChain(audio.rate), FORMAT, `volume=${audio.gain}`);
1977
+ const delayMs = Math.round(audio.start * 1e3);
1978
+ if (delayMs > 0) chain.push(`adelay=${delayMs}:all=1`);
1979
+ lines.push(`[${inputIndex}:a]${chain.join(",")}[k${i}]`);
1980
+ mixIn.push(`[k${i}]`);
1981
+ inputIndex++;
1982
+ });
1864
1983
  lines.push(
1865
1984
  `${mixIn.join("")}amix=inputs=${mixIn.length}:duration=first:normalize=0,alimiter=limit=0.891,aresample=async=1:first_pts=0[aout]`
1866
1985
  );
1867
1986
  return lines.join(";\n");
1868
1987
  }
1869
1988
  async function muxAudio(videoIn, plan, inputs, outFile) {
1870
- const work = await mkdtemp(join2(tmpdir2(), "reframe-mux-"));
1989
+ const work = await mkdtemp(join3(tmpdir2(), "reframe-mux-"));
1871
1990
  try {
1872
- const graphFile = join2(work, "graph.txt");
1991
+ const graphFile = join3(work, "graph.txt");
1873
1992
  await writeFile2(graphFile, buildFilterGraph(plan, inputs));
1874
1993
  const args = [
1875
1994
  "-y",
@@ -1877,6 +1996,7 @@ async function muxAudio(videoIn, plan, inputs, outFile) {
1877
1996
  videoIn,
1878
1997
  ...plan.bgm && inputs.bgmFile ? ["-i", inputs.bgmFile] : [],
1879
1998
  ...inputs.cueFiles.flatMap((f) => ["-i", f]),
1999
+ ...(inputs.clipFiles ?? []).flatMap((c) => ["-i", c.file]),
1880
2000
  "-filter_complex_script",
1881
2001
  graphFile,
1882
2002
  "-map",
@@ -1895,7 +2015,7 @@ async function muxAudio(videoIn, plan, inputs, outFile) {
1895
2015
  outFile
1896
2016
  ];
1897
2017
  await new Promise((resolvePromise, reject) => {
1898
- const proc = spawn("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
2018
+ const proc = spawn2("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
1899
2019
  let stderr = "";
1900
2020
  proc.stderr.on("data", (d) => stderr += d.toString());
1901
2021
  proc.on("close", (code) => {
@@ -1918,17 +2038,35 @@ var init_mux = __esm({
1918
2038
  });
1919
2039
 
1920
2040
  // ../render-cli/src/audio/index.ts
1921
- import { dirname as dirname2 } from "node:path";
2041
+ import { copyFile, mkdtemp as mkdtemp2, rm as rm2 } from "node:fs/promises";
2042
+ import { tmpdir as tmpdir3 } from "node:os";
2043
+ import { dirname as dirname2, join as join4 } from "node:path";
1922
2044
  async function buildAudioTrack(plan, scenePath, videoIn, outFile) {
1923
2045
  const sceneDir = dirname2(scenePath);
1924
2046
  const cueFiles = await Promise.all(plan.cues.map((cue) => resolveCueFile(cue, sceneDir)));
1925
2047
  const bgmFile = plan.bgm ? await resolveBgmFile(plan.bgm.source, plan.duration, sceneDir) : null;
1926
- await muxAudio(videoIn, plan, { cueFiles, bgmFile }, outFile);
2048
+ const work = await mkdtemp2(join4(tmpdir3(), "reframe-clipaudio-"));
2049
+ try {
2050
+ const clipFiles = [];
2051
+ for (const entry of plan.clipAudio) {
2052
+ const file = await resolveClipAudio(entry, sceneDir, work);
2053
+ if (file) clipFiles.push({ audio: entry, file });
2054
+ else console.error(`audio: video "${entry.nodeId}" has no audio track \u2014 skipped`);
2055
+ }
2056
+ if (!plan.bgm && plan.cues.length === 0 && clipFiles.length === 0) {
2057
+ await copyFile(videoIn, outFile);
2058
+ return;
2059
+ }
2060
+ await muxAudio(videoIn, plan, { cueFiles, bgmFile, clipFiles }, outFile);
2061
+ } finally {
2062
+ await rm2(work, { recursive: true, force: true });
2063
+ }
1927
2064
  }
1928
2065
  var init_audio2 = __esm({
1929
2066
  "../render-cli/src/audio/index.ts"() {
1930
2067
  "use strict";
1931
2068
  init_sfx();
2069
+ init_clip();
1932
2070
  init_mux();
1933
2071
  init_mux();
1934
2072
  init_synth();
@@ -1937,7 +2075,7 @@ var init_audio2 = __esm({
1937
2075
  });
1938
2076
 
1939
2077
  // ../render-cli/src/encode.ts
1940
- import { spawn as spawn2 } from "node:child_process";
2078
+ import { spawn as spawn3 } from "node:child_process";
1941
2079
  async function encodeMp4(framesDir, fps, outFile) {
1942
2080
  const args = [
1943
2081
  "-y",
@@ -1957,12 +2095,12 @@ async function encodeMp4(framesDir, fps, outFile) {
1957
2095
  "+faststart",
1958
2096
  outFile
1959
2097
  ];
1960
- await new Promise((resolve5, reject) => {
1961
- const proc = spawn2("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
2098
+ await new Promise((resolve7, reject) => {
2099
+ const proc = spawn3("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
1962
2100
  let stderr = "";
1963
2101
  proc.stderr.on("data", (d) => stderr += d.toString());
1964
2102
  proc.on("close", (code) => {
1965
- if (code === 0) resolve5();
2103
+ if (code === 0) resolve7();
1966
2104
  else reject(new Error(`ffmpeg exited with ${code}:
1967
2105
  ${stderr.slice(-2e3)}`));
1968
2106
  });
@@ -1977,13 +2115,13 @@ var init_encode = __esm({
1977
2115
 
1978
2116
  // ../render-cli/src/fonts.ts
1979
2117
  import { readFile as readFile2 } from "node:fs/promises";
1980
- import { dirname as dirname3, join as join3 } from "node:path";
2118
+ import { dirname as dirname3, join as join5 } from "node:path";
1981
2119
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1982
2120
  async function fontFaceCss() {
1983
2121
  if (cssCache) return cssCache;
1984
2122
  const rules = await Promise.all(
1985
2123
  WEIGHTS.map(async (weight) => {
1986
- const data = await readFile2(join3(FONTS_DIR, `inter-${weight}.woff2`));
2124
+ const data = await readFile2(join5(FONTS_DIR, `inter-${weight}.woff2`));
1987
2125
  return `@font-face {
1988
2126
  font-family: "Inter";
1989
2127
  font-style: normal;
@@ -1999,7 +2137,7 @@ var FONTS_DIR, WEIGHTS, cssCache;
1999
2137
  var init_fonts = __esm({
2000
2138
  "../render-cli/src/fonts.ts"() {
2001
2139
  "use strict";
2002
- FONTS_DIR = true ? join3(dirname3(fileURLToPath2(import.meta.url)), "..", "assets", "fonts") : join3(dirname3(fileURLToPath2(import.meta.url)), "..", "..", "..", "assets", "fonts");
2140
+ FONTS_DIR = true ? join5(dirname3(fileURLToPath2(import.meta.url)), "..", "assets", "fonts") : join5(dirname3(fileURLToPath2(import.meta.url)), "..", "..", "..", "assets", "fonts");
2003
2141
  WEIGHTS = [400, 700, 800];
2004
2142
  cssCache = null;
2005
2143
  }
@@ -2007,8 +2145,8 @@ var init_fonts = __esm({
2007
2145
 
2008
2146
  // ../render-cli/src/images.ts
2009
2147
  import { readFile as readFile3 } from "node:fs/promises";
2010
- import { existsSync as existsSync3 } from "node:fs";
2011
- import { extname, isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
2148
+ import { existsSync as existsSync4 } from "node:fs";
2149
+ import { extname, isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
2012
2150
  async function buildImageAssets(ir, sceneDir) {
2013
2151
  const assets = {};
2014
2152
  for (const src of collectImageSrcs(ir)) {
@@ -2018,10 +2156,10 @@ async function buildImageAssets(ir, sceneDir) {
2018
2156
  `image "${src}": unsupported format "${extname(src)}" \u2014 supported: ${Object.keys(MIME).join(" ")}`
2019
2157
  );
2020
2158
  }
2021
- const candidates = [isAbsolute2(src) ? src : null, resolve2(sceneDir, src)].filter(
2159
+ const candidates = [isAbsolute3(src) ? src : null, resolve3(sceneDir, src)].filter(
2022
2160
  (c) => c !== null
2023
2161
  );
2024
- const found = candidates.find((c) => existsSync3(c));
2162
+ const found = candidates.find((c) => existsSync4(c));
2025
2163
  if (!found) {
2026
2164
  throw new Error(`image "${src}" not found (tried: ${candidates.join(", ")})`);
2027
2165
  }
@@ -2044,6 +2182,103 @@ var init_images = __esm({
2044
2182
  }
2045
2183
  });
2046
2184
 
2185
+ // ../render-cli/src/videos.ts
2186
+ import { spawn as spawn4 } from "node:child_process";
2187
+ import { mkdtemp as mkdtemp3, readFile as readFile4, readdir, rm as rm3 } from "node:fs/promises";
2188
+ import { existsSync as existsSync5 } from "node:fs";
2189
+ import { tmpdir as tmpdir4 } from "node:os";
2190
+ import { extname as extname2, isAbsolute as isAbsolute4, join as join6, resolve as resolve4 } from "node:path";
2191
+ function runFfmpeg(args) {
2192
+ return new Promise((resolve7, reject) => {
2193
+ const proc = spawn4("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
2194
+ let stderr = "";
2195
+ proc.stderr.on("data", (d) => stderr += d.toString());
2196
+ proc.on(
2197
+ "close",
2198
+ (code) => code === 0 ? resolve7() : reject(new Error(`ffmpeg exited ${code}:
2199
+ ${stderr.slice(-2e3)}`))
2200
+ );
2201
+ proc.on("error", reject);
2202
+ });
2203
+ }
2204
+ function neededSeconds(node, duration) {
2205
+ const start = node.props.start ?? 0;
2206
+ const rate = node.props.rate ?? 1;
2207
+ const clipStart = node.props.clipStart ?? 0;
2208
+ return clipStart + Math.max(0, duration - start) * Math.max(0, rate) + 1 / 30;
2209
+ }
2210
+ function videoNodes(ir) {
2211
+ const out = [];
2212
+ const walk = (nodes) => {
2213
+ for (const n of nodes) {
2214
+ if (n.type === "video") out.push(n);
2215
+ if (n.type === "group") walk(n.children);
2216
+ }
2217
+ };
2218
+ walk(ir.nodes);
2219
+ return out;
2220
+ }
2221
+ async function buildVideoFrameAssets(ir, sceneDir, fps, duration) {
2222
+ const srcs = collectVideoSrcs(ir);
2223
+ if (srcs.length === 0) return {};
2224
+ const nodes = videoNodes(ir);
2225
+ const reachBySrc = /* @__PURE__ */ new Map();
2226
+ for (const n of nodes) {
2227
+ const reach = neededSeconds(n, duration);
2228
+ reachBySrc.set(n.props.src, Math.max(reachBySrc.get(n.props.src) ?? 0, reach));
2229
+ }
2230
+ const assets = {};
2231
+ for (const src of srcs) {
2232
+ if (!VIDEO_EXT.has(extname2(src).toLowerCase())) {
2233
+ throw new Error(
2234
+ `video "${src}": unsupported format "${extname2(src)}" \u2014 supported: ${[...VIDEO_EXT].join(" ")}`
2235
+ );
2236
+ }
2237
+ const candidates = [isAbsolute4(src) ? src : null, resolve4(sceneDir, src)].filter(
2238
+ (c) => c !== null
2239
+ );
2240
+ const found = candidates.find((c) => existsSync5(c));
2241
+ if (!found) throw new Error(`video "${src}" not found (tried: ${candidates.join(", ")})`);
2242
+ const dir = await mkdtemp3(join6(tmpdir4(), "reframe-vframes-"));
2243
+ try {
2244
+ const seconds = Math.max(1 / fps, reachBySrc.get(src) ?? duration);
2245
+ await runFfmpeg([
2246
+ "-y",
2247
+ "-i",
2248
+ found,
2249
+ "-t",
2250
+ seconds.toFixed(3),
2251
+ "-vf",
2252
+ `fps=${fps},scale='min(iw,1280)':-2`,
2253
+ "-q:v",
2254
+ "4",
2255
+ join6(dir, "%05d.jpg")
2256
+ ]);
2257
+ const files = (await readdir(dir)).filter((f) => f.endsWith(".jpg")).sort();
2258
+ assets[src] = await Promise.all(
2259
+ files.map(async (f) => `data:image/jpeg;base64,${(await readFile4(join6(dir, f))).toString("base64")}`)
2260
+ );
2261
+ if (assets[src].length === 0) throw new Error(`video "${src}": ffmpeg extracted no frames`);
2262
+ } finally {
2263
+ await rm3(dir, { recursive: true, force: true });
2264
+ }
2265
+ }
2266
+ return assets;
2267
+ }
2268
+ function resolveTiming(ir, opts) {
2269
+ const fps = opts.fps ?? ir.fps ?? 30;
2270
+ const duration = opts.duration ?? compileScene(ir).duration;
2271
+ return { fps, duration };
2272
+ }
2273
+ var VIDEO_EXT;
2274
+ var init_videos = __esm({
2275
+ "../render-cli/src/videos.ts"() {
2276
+ "use strict";
2277
+ init_src();
2278
+ VIDEO_EXT = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".m4v", ".mkv"]);
2279
+ }
2280
+ });
2281
+
2047
2282
  // ../render-cli/src/vclock.ts
2048
2283
  var VCLOCK_SOURCE;
2049
2284
  var init_vclock = __esm({
@@ -2124,7 +2359,7 @@ var init_reframeGlobal = __esm({
2124
2359
 
2125
2360
  // ../render-cli/src/frameLoop.ts
2126
2361
  import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
2127
- import { join as join4, dirname as dirname4 } from "node:path";
2362
+ import { join as join7, dirname as dirname4 } from "node:path";
2128
2363
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
2129
2364
  import { build } from "esbuild";
2130
2365
  import { chromium } from "playwright";
@@ -2149,14 +2384,14 @@ async function withPage(size, fn) {
2149
2384
  async function browserBundle() {
2150
2385
  if (bundleCache) return bundleCache;
2151
2386
  if (true) {
2152
- const { readFile: readFile6 } = await import("node:fs/promises");
2153
- bundleCache = await readFile6(
2154
- join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
2387
+ const { readFile: readFile7 } = await import("node:fs/promises");
2388
+ bundleCache = await readFile7(
2389
+ join7(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
2155
2390
  "utf8"
2156
2391
  );
2157
2392
  return bundleCache;
2158
2393
  }
2159
- const entry = join4(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.ts");
2394
+ const entry = join7(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.ts");
2160
2395
  const result = await build({
2161
2396
  entryPoints: [entry],
2162
2397
  bundle: true,
@@ -2169,7 +2404,10 @@ async function browserBundle() {
2169
2404
  }
2170
2405
  async function captureIr(ir, opts) {
2171
2406
  await mkdir2(opts.framesDir, { recursive: true });
2172
- const assets = await buildImageAssets(ir, opts.sceneDir ?? process.cwd());
2407
+ const sceneDir = opts.sceneDir ?? process.cwd();
2408
+ const assets = await buildImageAssets(ir, sceneDir);
2409
+ const { fps, duration } = resolveTiming(ir, opts);
2410
+ const videoAssets = await buildVideoFrameAssets(ir, sceneDir, fps, duration);
2173
2411
  const bundle = await browserBundle();
2174
2412
  return withPage(ir.size, async (page) => {
2175
2413
  await page.setContent(
@@ -2177,12 +2415,10 @@ async function captureIr(ir, opts) {
2177
2415
  );
2178
2416
  await injectFonts(page);
2179
2417
  await page.addScriptTag({ content: bundle });
2180
- const info = await page.evaluate(
2181
- ([sceneIr, imageAssets]) => window.__reframe.init(sceneIr, imageAssets),
2182
- [ir, assets]
2418
+ await page.evaluate(
2419
+ ([sceneIr, imageAssets, vAssets]) => window.__reframe.init(sceneIr, imageAssets, vAssets),
2420
+ [ir, assets, videoAssets]
2183
2421
  );
2184
- const fps = opts.fps ?? info.fps;
2185
- const duration = opts.duration ?? info.duration;
2186
2422
  const frameCount = Math.max(1, Math.round(duration * fps));
2187
2423
  for (let f = 0; f < frameCount; f++) {
2188
2424
  const dataUrl = await page.evaluate((t) => window.__reframe.renderFrame(t), f / fps);
@@ -2197,9 +2433,10 @@ var init_frameLoop = __esm({
2197
2433
  "use strict";
2198
2434
  init_fonts();
2199
2435
  init_images();
2436
+ init_videos();
2200
2437
  init_vclock();
2201
2438
  init_reframeGlobal();
2202
- framePath = (dir, i) => join4(dir, `${String(i).padStart(5, "0")}.png`);
2439
+ framePath = (dir, i) => join7(dir, `${String(i).padStart(5, "0")}.png`);
2203
2440
  bundleCache = null;
2204
2441
  }
2205
2442
  });
@@ -2212,9 +2449,9 @@ __export(batch_exports, {
2212
2449
  parseCsv: () => parseCsv,
2213
2450
  runBatch: () => runBatch
2214
2451
  });
2215
- import { mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile4, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
2216
- import { tmpdir as tmpdir3 } from "node:os";
2217
- import { join as join5, dirname as dirname5 } from "node:path";
2452
+ import { mkdir as mkdir3, mkdtemp as mkdtemp4, readFile as readFile5, rm as rm4, writeFile as writeFile4 } from "node:fs/promises";
2453
+ import { tmpdir as tmpdir5 } from "node:os";
2454
+ import { join as join8, dirname as dirname5 } from "node:path";
2218
2455
  function overlayFromFlat(row, name) {
2219
2456
  const doc = { reframeOverlay: 1, name };
2220
2457
  for (const [key2, raw] of Object.entries(row)) {
@@ -2288,7 +2525,7 @@ function parseCsv(text2) {
2288
2525
  });
2289
2526
  }
2290
2527
  async function loadRows(path2) {
2291
- const text2 = await readFile4(path2, "utf8");
2528
+ const text2 = await readFile5(path2, "utf8");
2292
2529
  if (path2.endsWith(".csv")) return parseCsv(text2);
2293
2530
  const parsed = JSON.parse(text2);
2294
2531
  if (!Array.isArray(parsed)) throw new Error(`${path2}: expected a JSON array of row objects`);
@@ -2308,8 +2545,8 @@ async function runBatch(scene2, rows, opts) {
2308
2545
  try {
2309
2546
  const rowOverlay = overlayFromFlat(row, name);
2310
2547
  const { ir, report } = composeScene(scene2, ...opts.baseOverlays, rowOverlay);
2311
- const framesDir = await mkdtemp2(join5(tmpdir3(), `reframe-batch-${index}-`));
2312
- const output = join5(opts.outDir, `${name}.mp4`);
2548
+ const framesDir = await mkdtemp4(join8(tmpdir5(), `reframe-batch-${index}-`));
2549
+ const output = join8(opts.outDir, `${name}.mp4`);
2313
2550
  const plan = opts.noAudio ? null : resolveAudioPlan(compileScene(ir));
2314
2551
  try {
2315
2552
  const captured = await captureIr(ir, {
@@ -2321,12 +2558,12 @@ async function runBatch(scene2, rows, opts) {
2321
2558
  const videoTmp = `${output}.video.mp4`;
2322
2559
  await encodeMp4(captured.framesDir, captured.fps, videoTmp);
2323
2560
  await buildAudioTrack(plan, opts.scenePath ?? output, videoTmp, output);
2324
- await rm2(videoTmp, { force: true });
2561
+ await rm4(videoTmp, { force: true });
2325
2562
  } else {
2326
2563
  await encodeMp4(captured.framesDir, captured.fps, output);
2327
2564
  }
2328
2565
  } finally {
2329
- await rm2(framesDir, { recursive: true, force: true });
2566
+ await rm4(framesDir, { recursive: true, force: true });
2330
2567
  }
2331
2568
  result = {
2332
2569
  name,
@@ -2350,7 +2587,7 @@ async function runBatch(scene2, rows, opts) {
2350
2587
  };
2351
2588
  await Promise.all(Array.from({ length: Math.max(1, opts.concurrency) }, worker));
2352
2589
  await writeFile4(
2353
- join5(opts.outDir, "batch-report.json"),
2590
+ join8(opts.outDir, "batch-report.json"),
2354
2591
  JSON.stringify({ rows: results }, null, 2)
2355
2592
  );
2356
2593
  return results;
@@ -2375,11 +2612,11 @@ __export(loadScene_exports, {
2375
2612
  loadScene: () => loadScene
2376
2613
  });
2377
2614
  import { build as build2 } from "esbuild";
2378
- import { readFile as readFile5 } from "node:fs/promises";
2379
- import { dirname as dirname6, resolve as resolve3 } from "node:path";
2615
+ import { readFile as readFile6 } from "node:fs/promises";
2616
+ import { dirname as dirname6, resolve as resolve5 } from "node:path";
2380
2617
  import { fileURLToPath as fileURLToPath4 } from "node:url";
2381
2618
  async function loadDefault(path2) {
2382
- if (path2.endsWith(".json")) return JSON.parse(await readFile5(path2, "utf8"));
2619
+ if (path2.endsWith(".json")) return JSON.parse(await readFile6(path2, "utf8"));
2383
2620
  let code;
2384
2621
  try {
2385
2622
  const out = await build2({
@@ -2429,26 +2666,26 @@ var init_loadScene = __esm({
2429
2666
  "use strict";
2430
2667
  init_src();
2431
2668
  HERE = dirname6(fileURLToPath4(import.meta.url));
2432
- CORE_ENTRY = true ? resolve3(HERE, "index.js") : resolve3(HERE, "..", "..", "core", "src", "index.ts");
2669
+ CORE_ENTRY = true ? resolve5(HERE, "index.js") : resolve5(HERE, "..", "..", "core", "src", "index.ts");
2433
2670
  }
2434
2671
  });
2435
2672
 
2436
2673
  // ../render-cli/src/reframe.ts
2437
- import { spawn as spawn3, spawnSync } from "node:child_process";
2438
- import { existsSync as existsSync4 } from "node:fs";
2674
+ import { spawn as spawn5, spawnSync } from "node:child_process";
2675
+ import { existsSync as existsSync6 } from "node:fs";
2439
2676
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
2440
- import { basename, isAbsolute as isAbsolute3, join as join6, resolve as resolve4 } from "node:path";
2677
+ import { basename, isAbsolute as isAbsolute5, join as join9, resolve as resolve6 } from "node:path";
2441
2678
  import { dirname as dirname7 } from "node:path";
2442
2679
  import { fileURLToPath as fileURLToPath5 } from "node:url";
2443
2680
  var PACKAGED = true;
2444
2681
  var HERE2 = dirname7(fileURLToPath5(import.meta.url));
2445
- var ROOT2 = PACKAGED ? resolve4(HERE2, "..") : resolve4(HERE2, "..", "..", "..");
2682
+ var ROOT2 = PACKAGED ? resolve6(HERE2, "..") : resolve6(HERE2, "..", "..", "..");
2446
2683
  var USER_CWD = process.env.INIT_CWD ?? process.cwd();
2447
- var RENDER_CLI = PACKAGED ? join6(ROOT2, "dist", "cli.js") : join6(ROOT2, "packages", "render-cli", "src", "cli.ts");
2448
- var LABELS = PACKAGED ? join6(ROOT2, "dist", "labels.js") : join6(ROOT2, "packages", "render-cli", "src", "labels.ts");
2449
- var PLAYER = PACKAGED ? join6(ROOT2, "dist", "player.js") : join6(ROOT2, "packages", "render-cli", "src", "player.ts");
2450
- var ANALYZE = PACKAGED ? join6(ROOT2, "dist", "analyze.js") : join6(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
2451
- var TRACE = PACKAGED ? join6(ROOT2, "dist", "trace-cli.js") : join6(ROOT2, "benchmark", "harness", "motion", "trace-cli.ts");
2684
+ var RENDER_CLI = PACKAGED ? join9(ROOT2, "dist", "cli.js") : join9(ROOT2, "packages", "render-cli", "src", "cli.ts");
2685
+ var LABELS = PACKAGED ? join9(ROOT2, "dist", "labels.js") : join9(ROOT2, "packages", "render-cli", "src", "labels.ts");
2686
+ var PLAYER = PACKAGED ? join9(ROOT2, "dist", "player.js") : join9(ROOT2, "packages", "render-cli", "src", "player.ts");
2687
+ var ANALYZE = PACKAGED ? join9(ROOT2, "dist", "analyze.js") : join9(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
2688
+ var TRACE = PACKAGED ? join9(ROOT2, "dist", "trace-cli.js") : join9(ROOT2, "benchmark", "harness", "motion", "trace-cli.ts");
2452
2689
  var CMD = PACKAGED ? "reframe" : "pnpm reframe";
2453
2690
  var USAGE = `reframe \u2014 declarative motion graphics
2454
2691
 
@@ -2468,7 +2705,7 @@ usage:
2468
2705
  ${CMD} guide [--regen] print the scene-authoring guide (for you or your AI)
2469
2706
  ${CMD} demo run the edit-survival demo (3 mp4s into out/)
2470
2707
  `;
2471
- var userPath = (p) => isAbsolute3(p) ? p : resolve4(USER_CWD, p);
2708
+ var userPath = (p) => isAbsolute5(p) ? p : resolve6(USER_CWD, p);
2472
2709
  function fail(message) {
2473
2710
  console.error(`error: ${message}`);
2474
2711
  process.exit(2);
@@ -2478,9 +2715,9 @@ function preflightFfmpeg() {
2478
2715
  fail("ffmpeg not found on PATH \u2014 install it first (macOS: brew install ffmpeg, debian: apt install ffmpeg)");
2479
2716
  }
2480
2717
  }
2481
- function run(cmd, args, opts = {}) {
2718
+ function run2(cmd, args, opts = {}) {
2482
2719
  return new Promise((res) => {
2483
- const proc = spawn3(cmd, args, {
2720
+ const proc = spawn5(cmd, args, {
2484
2721
  cwd: opts.cwd ?? (PACKAGED ? USER_CWD : ROOT2),
2485
2722
  stdio: ["inherit", "inherit", "pipe"],
2486
2723
  ...opts.env && { env: { ...process.env, ...opts.env } }
@@ -2570,7 +2807,7 @@ async function main() {
2570
2807
 
2571
2808
  ${USAGE}`);
2572
2809
  const inputPath = userPath(input);
2573
- if (!existsSync4(inputPath)) fail(`no such file: ${inputPath}`);
2810
+ if (!existsSync6(inputPath)) fail(`no such file: ${inputPath}`);
2574
2811
  const mode = /\.(ts|json)$/.test(input) ? "ir" : /\.html$/.test(input) ? "html" : null;
2575
2812
  if (!mode) {
2576
2813
  fail(`cannot infer render mode from "${input}" \u2014 expected .ts/.json (reframe scene) or .html (GSAP page)`);
@@ -2580,17 +2817,17 @@ ${USAGE}`);
2580
2817
  fail("html render requires --duration <seconds> (the page does not declare its own length)");
2581
2818
  }
2582
2819
  preflightFfmpeg();
2583
- const outBase = PACKAGED ? join6(USER_CWD, "out") : join6(ROOT2, "out");
2820
+ const outBase = PACKAGED ? join9(USER_CWD, "out") : join9(ROOT2, "out");
2584
2821
  let outArgs = args;
2585
2822
  if (!args.includes("-o")) {
2586
2823
  await mkdir4(outBase, { recursive: true });
2587
- outArgs = [...args, "-o", join6(outBase, `${basename(input).replace(/\.[^.]+$/, "")}.mp4`)];
2824
+ outArgs = [...args, "-o", join9(outBase, `${basename(input).replace(/\.[^.]+$/, "")}.mp4`)];
2588
2825
  }
2589
2826
  outArgs = outArgs.map(
2590
2827
  (a, i) => outArgs[i - 1] === "--overlay" || outArgs[i - 1] === "-o" ? userPath(a) : a
2591
2828
  );
2592
2829
  process.exit(
2593
- await (PACKAGED ? run(process.execPath, [RENDER_CLI, mode, inputPath, ...outArgs]) : run("npx", ["tsx", RENDER_CLI, mode, inputPath, ...outArgs]))
2830
+ await (PACKAGED ? run2(process.execPath, [RENDER_CLI, mode, inputPath, ...outArgs]) : run2("npx", ["tsx", RENDER_CLI, mode, inputPath, ...outArgs]))
2594
2831
  );
2595
2832
  }
2596
2833
  case "labels": {
@@ -2599,9 +2836,9 @@ ${USAGE}`);
2599
2836
 
2600
2837
  ${USAGE}`);
2601
2838
  const inputPath = userPath(input);
2602
- if (!existsSync4(inputPath)) fail(`no such file: ${inputPath}`);
2839
+ if (!existsSync6(inputPath)) fail(`no such file: ${inputPath}`);
2603
2840
  process.exit(
2604
- await (PACKAGED ? run(process.execPath, [LABELS, inputPath]) : run("npx", ["tsx", LABELS, inputPath]))
2841
+ await (PACKAGED ? run2(process.execPath, [LABELS, inputPath]) : run2("npx", ["tsx", LABELS, inputPath]))
2605
2842
  );
2606
2843
  }
2607
2844
  case "player": {
@@ -2610,13 +2847,13 @@ ${USAGE}`);
2610
2847
 
2611
2848
  ${USAGE}`);
2612
2849
  const inputPath = userPath(input);
2613
- if (!existsSync4(inputPath)) fail(`no such file: ${inputPath}`);
2850
+ if (!existsSync6(inputPath)) fail(`no such file: ${inputPath}`);
2614
2851
  const oIdx = rest.indexOf("-o");
2615
- const outBase = PACKAGED ? join6(USER_CWD, "out") : join6(ROOT2, "out");
2616
- const outPath = oIdx >= 0 && rest[oIdx + 1] ? userPath(rest[oIdx + 1]) : join6(outBase, `${basename(input).replace(/\.[^.]+$/, "")}.html`);
2852
+ const outBase = PACKAGED ? join9(USER_CWD, "out") : join9(ROOT2, "out");
2853
+ const outPath = oIdx >= 0 && rest[oIdx + 1] ? userPath(rest[oIdx + 1]) : join9(outBase, `${basename(input).replace(/\.[^.]+$/, "")}.html`);
2617
2854
  await mkdir4(dirname7(outPath), { recursive: true });
2618
2855
  process.exit(
2619
- await (PACKAGED ? run(process.execPath, [PLAYER, inputPath, outPath]) : run("npx", ["tsx", PLAYER, inputPath, outPath]))
2856
+ await (PACKAGED ? run2(process.execPath, [PLAYER, inputPath, outPath]) : run2("npx", ["tsx", PLAYER, inputPath, outPath]))
2620
2857
  );
2621
2858
  }
2622
2859
  case "logo": {
@@ -2633,7 +2870,7 @@ ${USAGE}`);
2633
2870
  fail(`usage: ${CMD} logo <logo.svg | brand-slug> ["Display Name"] [--motion <preset>] [--energy 0..1] [--speed n] [--intensity 0..1] [--from left|right|top|bottom] [--seed n] [-o out.mp4]`);
2634
2871
  }
2635
2872
  preflightFfmpeg();
2636
- const { tmpdir: tmpdir4 } = await import("node:os");
2873
+ const { tmpdir: tmpdir6 } = await import("node:os");
2637
2874
  const { resolveLogo: resolveLogo2, buildLogoSting: buildLogoSting2 } = await Promise.resolve().then(() => (init_logoSting(), logoSting_exports));
2638
2875
  const num = (k) => flags[k] !== void 0 ? Number(flags[k]) : void 0;
2639
2876
  console.log(`loading logo: ${arg} \u2026`);
@@ -2646,14 +2883,14 @@ ${USAGE}`);
2646
2883
  seed: num("seed")
2647
2884
  });
2648
2885
  const sceneIR = buildLogoSting2(data);
2649
- const tmp = join6(tmpdir4(), `reframe-logo-${slug}-${process.pid}.json`);
2886
+ const tmp = join9(tmpdir6(), `reframe-logo-${slug}-${process.pid}.json`);
2650
2887
  await writeFile5(tmp, JSON.stringify(sceneIR));
2651
- const outBase = PACKAGED ? join6(USER_CWD, "out") : join6(ROOT2, "out");
2652
- const out = flags.o ? userPath(flags.o) : join6(outBase, `logo-${slug}.mp4`);
2888
+ const outBase = PACKAGED ? join9(USER_CWD, "out") : join9(ROOT2, "out");
2889
+ const out = flags.o ? userPath(flags.o) : join9(outBase, `logo-${slug}.mp4`);
2653
2890
  await mkdir4(dirname7(out), { recursive: true });
2654
2891
  console.log(`rendering ${data.name} (${data.paths.length} path${data.paths.length > 1 ? "s" : ""}, motion: ${data.motion ?? "reveal-orbit"}) \u2192 ${out}`);
2655
2892
  process.exit(
2656
- await (PACKAGED ? run(process.execPath, [RENDER_CLI, "ir", tmp, "-o", out, "--no-audio"]) : run("npx", ["tsx", RENDER_CLI, "ir", tmp, "-o", out, "--no-audio"]))
2893
+ await (PACKAGED ? run2(process.execPath, [RENDER_CLI, "ir", tmp, "-o", out, "--no-audio"]) : run2("npx", ["tsx", RENDER_CLI, "ir", tmp, "-o", out, "--no-audio"]))
2657
2894
  );
2658
2895
  }
2659
2896
  case "batch": {
@@ -2661,9 +2898,9 @@ ${USAGE}`);
2661
2898
  if (!sceneArg || !dataArg) fail(`usage: ${CMD} batch <scene.ts> <data.json|csv> [...]`);
2662
2899
  const scenePath = userPath(sceneArg);
2663
2900
  const dataPath = userPath(dataArg);
2664
- for (const p of [scenePath, dataPath]) if (!existsSync4(p)) fail(`no such file: ${p}`);
2901
+ for (const p of [scenePath, dataPath]) if (!existsSync6(p)) fail(`no such file: ${p}`);
2665
2902
  preflightFfmpeg();
2666
- let outDir = PACKAGED ? join6(USER_CWD, "out", "batch") : join6(ROOT2, "out", "batch");
2903
+ let outDir = PACKAGED ? join9(USER_CWD, "out", "batch") : join9(ROOT2, "out", "batch");
2667
2904
  let concurrency = 3;
2668
2905
  let fps;
2669
2906
  const baseOverlayPaths = [];
@@ -2676,10 +2913,10 @@ ${USAGE}`);
2676
2913
  }
2677
2914
  const { loadRows: loadRows2, runBatch: runBatch2 } = await Promise.resolve().then(() => (init_batch(), batch_exports));
2678
2915
  const { loadScene: loadScene2 } = await Promise.resolve().then(() => (init_loadScene(), loadScene_exports));
2679
- const { readFile: readFile6 } = await import("node:fs/promises");
2916
+ const { readFile: readFile7 } = await import("node:fs/promises");
2680
2917
  const scene2 = await loadScene2(scenePath);
2681
2918
  const baseOverlays = await Promise.all(
2682
- baseOverlayPaths.map(async (p) => JSON.parse(await readFile6(p, "utf8")))
2919
+ baseOverlayPaths.map(async (p) => JSON.parse(await readFile7(p, "utf8")))
2683
2920
  );
2684
2921
  const rows = await loadRows2(dataPath);
2685
2922
  if (rows.length === 0) fail(`${dataPath}: no data rows`);
@@ -2702,7 +2939,7 @@ ${USAGE}`);
2702
2939
  const orphaned = results.filter((r) => !r.error && r.orphans.length > 0).length;
2703
2940
  console.log(
2704
2941
  `
2705
- ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed \u2014 report: ${join6(outDir, "batch-report.json")}`
2942
+ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed \u2014 report: ${join9(outDir, "batch-report.json")}`
2706
2943
  );
2707
2944
  process.exit(failed > 0 ? 1 : 0);
2708
2945
  }
@@ -2713,15 +2950,15 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2713
2950
  if (PACKAGED) {
2714
2951
  const { createRequire } = await import("node:module");
2715
2952
  const vitePkg = createRequire(import.meta.url).resolve("vite/package.json");
2716
- const viteBin = join6(dirname7(vitePkg), "bin", "vite.js");
2953
+ const viteBin = join9(dirname7(vitePkg), "bin", "vite.js");
2717
2954
  process.exit(
2718
- await run(process.execPath, [viteBin, join6(ROOT2, "preview")], {
2955
+ await run2(process.execPath, [viteBin, join9(ROOT2, "preview")], {
2719
2956
  env: { REFRAME_SCENE_DIR: USER_CWD }
2720
2957
  })
2721
2958
  );
2722
2959
  }
2723
2960
  process.exit(
2724
- await run("pnpm", ["--filter", "@reframe/preview", "dev"], {
2961
+ await run2("pnpm", ["--filter", "@reframe/preview", "dev"], {
2725
2962
  env: { REFRAME_SCENE_DIR: USER_CWD }
2726
2963
  })
2727
2964
  );
@@ -2733,10 +2970,10 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2733
2970
  fail(`scene name must be kebab-case (a-z, 0-9, -): got "${name}"`);
2734
2971
  }
2735
2972
  const inRepo = USER_CWD === ROOT2 || USER_CWD.startsWith(ROOT2 + "/");
2736
- const targetDir = inRepo ? join6(ROOT2, "examples", "scenes") : USER_CWD;
2737
- const target = join6(targetDir, `${name}.ts`);
2973
+ const targetDir = inRepo ? join9(ROOT2, "examples", "scenes") : USER_CWD;
2974
+ const target = join9(targetDir, `${name}.ts`);
2738
2975
  const shown = inRepo ? `examples/scenes/${name}.ts` : `${name}.ts`;
2739
- if (existsSync4(target)) fail(`${shown} already exists`);
2976
+ if (existsSync6(target)) fail(`${shown} already exists`);
2740
2977
  const id = name.split("-")[0] ?? name;
2741
2978
  await writeFile5(target, SCENE_TEMPLATE(name, id));
2742
2979
  console.log(`created ${shown}
@@ -2750,7 +2987,7 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2750
2987
  if (!input) fail(`usage: ${CMD} motion <mp4|framesDir> [...args]`);
2751
2988
  preflightFfmpeg();
2752
2989
  process.exit(
2753
- await (PACKAGED ? run(process.execPath, [ANALYZE, userPath(input), ...rest.slice(1)]) : run("npx", ["tsx", ANALYZE, userPath(input), ...rest.slice(1)]))
2990
+ await (PACKAGED ? run2(process.execPath, [ANALYZE, userPath(input), ...rest.slice(1)]) : run2("npx", ["tsx", ANALYZE, userPath(input), ...rest.slice(1)]))
2754
2991
  );
2755
2992
  }
2756
2993
  case "trace": {
@@ -2761,13 +2998,13 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2761
2998
  (a, i) => rest.slice(1)[i - 1] === "--apply" || rest.slice(1)[i - 1] === "-o" ? userPath(a) : a
2762
2999
  );
2763
3000
  process.exit(
2764
- await (PACKAGED ? run(process.execPath, [TRACE, userPath(input), ...args]) : run("npx", ["tsx", TRACE, userPath(input), ...args]))
3001
+ await (PACKAGED ? run2(process.execPath, [TRACE, userPath(input), ...args]) : run2("npx", ["tsx", TRACE, userPath(input), ...args]))
2765
3002
  );
2766
3003
  }
2767
3004
  case "guide": {
2768
- const file = rest.includes("--regen") ? PACKAGED ? join6(ROOT2, "guides", "regen-contract.md") : join6(ROOT2, "docs", "regen-contract.md") : PACKAGED ? join6(ROOT2, "guides", "edsl-guide.md") : join6(ROOT2, "benchmark", "guides", "edsl-guide.md");
2769
- const { readFile: readFile6 } = await import("node:fs/promises");
2770
- process.stdout.write(await readFile6(file, "utf8"));
3005
+ const file = rest.includes("--regen") ? PACKAGED ? join9(ROOT2, "guides", "regen-contract.md") : join9(ROOT2, "docs", "regen-contract.md") : PACKAGED ? join9(ROOT2, "guides", "edsl-guide.md") : join9(ROOT2, "benchmark", "guides", "edsl-guide.md");
3006
+ const { readFile: readFile7 } = await import("node:fs/promises");
3007
+ process.stdout.write(await readFile7(file, "utf8"));
2771
3008
  return;
2772
3009
  }
2773
3010
  case "demo":
@@ -2778,7 +3015,7 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2778
3015
  }
2779
3016
  preflightFfmpeg();
2780
3017
  process.exit(
2781
- await run("npx", ["tsx", join6(ROOT2, "examples", "scripts", "demo-edit-survival.ts")])
3018
+ await run2("npx", ["tsx", join9(ROOT2, "examples", "scripts", "demo-edit-survival.ts")])
2782
3019
  );
2783
3020
  default:
2784
3021
  console.log(USAGE);