reframe-video 0.6.6 → 0.6.8

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,7 +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"],
625
+ video: [...COMMON_PROPS, "src", "width", "height", "fit", "start", "rate", "clipStart", "volume"],
626
626
  path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
627
627
  group: COMMON_PROPS
628
628
  };
@@ -1210,11 +1210,34 @@ var init_motionOps = __esm({
1210
1210
  });
1211
1211
 
1212
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
+ }
1213
1233
  function resolveAudioPlan(compiled) {
1214
1234
  const audio = compiled.ir.audio;
1215
- if (!audio || !audio.bgm && (audio.cues ?? []).length === 0) return null;
1216
1235
  const warnings = [];
1217
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
+ }
1218
1241
  const cues = [];
1219
1242
  for (const [index, cue] of (audio.cues ?? []).entries()) {
1220
1243
  let anchor;
@@ -1250,6 +1273,7 @@ function resolveAudioPlan(compiled) {
1250
1273
  bgm: resolveBgm(audio.bgm),
1251
1274
  cues,
1252
1275
  duckWindows: mergeDuckWindows(cues, duration),
1276
+ clipAudio,
1253
1277
  warnings
1254
1278
  };
1255
1279
  }
@@ -1831,11 +1855,89 @@ var init_sfx = __esm({
1831
1855
  }
1832
1856
  });
1833
1857
 
1834
- // ../render-cli/src/audio/mux.ts
1858
+ // ../render-cli/src/audio/clip.ts
1835
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";
1836
1923
  import { mkdtemp, rm, writeFile as writeFile2 } from "node:fs/promises";
1837
1924
  import { tmpdir as tmpdir2 } from "node:os";
1838
- 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
+ }
1839
1941
  function buildFilterGraph(plan, inputs) {
1840
1942
  const lines = [];
1841
1943
  const mixIn = ["[anchor]"];
@@ -1868,15 +1970,25 @@ function buildFilterGraph(plan, inputs) {
1868
1970
  mixIn.push(`[c${i}]`);
1869
1971
  inputIndex++;
1870
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
+ });
1871
1983
  lines.push(
1872
1984
  `${mixIn.join("")}amix=inputs=${mixIn.length}:duration=first:normalize=0,alimiter=limit=0.891,aresample=async=1:first_pts=0[aout]`
1873
1985
  );
1874
1986
  return lines.join(";\n");
1875
1987
  }
1876
1988
  async function muxAudio(videoIn, plan, inputs, outFile) {
1877
- const work = await mkdtemp(join2(tmpdir2(), "reframe-mux-"));
1989
+ const work = await mkdtemp(join3(tmpdir2(), "reframe-mux-"));
1878
1990
  try {
1879
- const graphFile = join2(work, "graph.txt");
1991
+ const graphFile = join3(work, "graph.txt");
1880
1992
  await writeFile2(graphFile, buildFilterGraph(plan, inputs));
1881
1993
  const args = [
1882
1994
  "-y",
@@ -1884,6 +1996,7 @@ async function muxAudio(videoIn, plan, inputs, outFile) {
1884
1996
  videoIn,
1885
1997
  ...plan.bgm && inputs.bgmFile ? ["-i", inputs.bgmFile] : [],
1886
1998
  ...inputs.cueFiles.flatMap((f) => ["-i", f]),
1999
+ ...(inputs.clipFiles ?? []).flatMap((c) => ["-i", c.file]),
1887
2000
  "-filter_complex_script",
1888
2001
  graphFile,
1889
2002
  "-map",
@@ -1902,7 +2015,7 @@ async function muxAudio(videoIn, plan, inputs, outFile) {
1902
2015
  outFile
1903
2016
  ];
1904
2017
  await new Promise((resolvePromise, reject) => {
1905
- const proc = spawn("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
2018
+ const proc = spawn2("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
1906
2019
  let stderr = "";
1907
2020
  proc.stderr.on("data", (d) => stderr += d.toString());
1908
2021
  proc.on("close", (code) => {
@@ -1925,17 +2038,35 @@ var init_mux = __esm({
1925
2038
  });
1926
2039
 
1927
2040
  // ../render-cli/src/audio/index.ts
1928
- 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";
1929
2044
  async function buildAudioTrack(plan, scenePath, videoIn, outFile) {
1930
2045
  const sceneDir = dirname2(scenePath);
1931
2046
  const cueFiles = await Promise.all(plan.cues.map((cue) => resolveCueFile(cue, sceneDir)));
1932
2047
  const bgmFile = plan.bgm ? await resolveBgmFile(plan.bgm.source, plan.duration, sceneDir) : null;
1933
- 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
+ }
1934
2064
  }
1935
2065
  var init_audio2 = __esm({
1936
2066
  "../render-cli/src/audio/index.ts"() {
1937
2067
  "use strict";
1938
2068
  init_sfx();
2069
+ init_clip();
1939
2070
  init_mux();
1940
2071
  init_mux();
1941
2072
  init_synth();
@@ -1944,7 +2075,7 @@ var init_audio2 = __esm({
1944
2075
  });
1945
2076
 
1946
2077
  // ../render-cli/src/encode.ts
1947
- import { spawn as spawn2 } from "node:child_process";
2078
+ import { spawn as spawn3 } from "node:child_process";
1948
2079
  async function encodeMp4(framesDir, fps, outFile) {
1949
2080
  const args = [
1950
2081
  "-y",
@@ -1964,12 +2095,12 @@ async function encodeMp4(framesDir, fps, outFile) {
1964
2095
  "+faststart",
1965
2096
  outFile
1966
2097
  ];
1967
- await new Promise((resolve6, reject) => {
1968
- 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"] });
1969
2100
  let stderr = "";
1970
2101
  proc.stderr.on("data", (d) => stderr += d.toString());
1971
2102
  proc.on("close", (code) => {
1972
- if (code === 0) resolve6();
2103
+ if (code === 0) resolve7();
1973
2104
  else reject(new Error(`ffmpeg exited with ${code}:
1974
2105
  ${stderr.slice(-2e3)}`));
1975
2106
  });
@@ -1984,13 +2115,13 @@ var init_encode = __esm({
1984
2115
 
1985
2116
  // ../render-cli/src/fonts.ts
1986
2117
  import { readFile as readFile2 } from "node:fs/promises";
1987
- import { dirname as dirname3, join as join3 } from "node:path";
2118
+ import { dirname as dirname3, join as join5 } from "node:path";
1988
2119
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1989
2120
  async function fontFaceCss() {
1990
2121
  if (cssCache) return cssCache;
1991
2122
  const rules = await Promise.all(
1992
2123
  WEIGHTS.map(async (weight) => {
1993
- const data = await readFile2(join3(FONTS_DIR, `inter-${weight}.woff2`));
2124
+ const data = await readFile2(join5(FONTS_DIR, `inter-${weight}.woff2`));
1994
2125
  return `@font-face {
1995
2126
  font-family: "Inter";
1996
2127
  font-style: normal;
@@ -2006,7 +2137,7 @@ var FONTS_DIR, WEIGHTS, cssCache;
2006
2137
  var init_fonts = __esm({
2007
2138
  "../render-cli/src/fonts.ts"() {
2008
2139
  "use strict";
2009
- 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");
2010
2141
  WEIGHTS = [400, 700, 800];
2011
2142
  cssCache = null;
2012
2143
  }
@@ -2014,8 +2145,8 @@ var init_fonts = __esm({
2014
2145
 
2015
2146
  // ../render-cli/src/images.ts
2016
2147
  import { readFile as readFile3 } from "node:fs/promises";
2017
- import { existsSync as existsSync3 } from "node:fs";
2018
- 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";
2019
2150
  async function buildImageAssets(ir, sceneDir) {
2020
2151
  const assets = {};
2021
2152
  for (const src of collectImageSrcs(ir)) {
@@ -2025,10 +2156,10 @@ async function buildImageAssets(ir, sceneDir) {
2025
2156
  `image "${src}": unsupported format "${extname(src)}" \u2014 supported: ${Object.keys(MIME).join(" ")}`
2026
2157
  );
2027
2158
  }
2028
- const candidates = [isAbsolute2(src) ? src : null, resolve2(sceneDir, src)].filter(
2159
+ const candidates = [isAbsolute3(src) ? src : null, resolve3(sceneDir, src)].filter(
2029
2160
  (c) => c !== null
2030
2161
  );
2031
- const found = candidates.find((c) => existsSync3(c));
2162
+ const found = candidates.find((c) => existsSync4(c));
2032
2163
  if (!found) {
2033
2164
  throw new Error(`image "${src}" not found (tried: ${candidates.join(", ")})`);
2034
2165
  }
@@ -2052,19 +2183,19 @@ var init_images = __esm({
2052
2183
  });
2053
2184
 
2054
2185
  // ../render-cli/src/videos.ts
2055
- import { spawn as spawn3 } from "node:child_process";
2056
- import { mkdtemp as mkdtemp2, readFile as readFile4, readdir, rm as rm2 } from "node:fs/promises";
2057
- import { existsSync as existsSync4 } from "node:fs";
2058
- import { tmpdir as tmpdir3 } from "node:os";
2059
- import { extname as extname2, isAbsolute as isAbsolute3, join as join4, resolve as resolve3 } from "node:path";
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";
2060
2191
  function runFfmpeg(args) {
2061
- return new Promise((resolve6, reject) => {
2062
- const proc = spawn3("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
2192
+ return new Promise((resolve7, reject) => {
2193
+ const proc = spawn4("ffmpeg", args, { stdio: ["ignore", "ignore", "pipe"] });
2063
2194
  let stderr = "";
2064
2195
  proc.stderr.on("data", (d) => stderr += d.toString());
2065
2196
  proc.on(
2066
2197
  "close",
2067
- (code) => code === 0 ? resolve6() : reject(new Error(`ffmpeg exited ${code}:
2198
+ (code) => code === 0 ? resolve7() : reject(new Error(`ffmpeg exited ${code}:
2068
2199
  ${stderr.slice(-2e3)}`))
2069
2200
  );
2070
2201
  proc.on("error", reject);
@@ -2103,12 +2234,12 @@ async function buildVideoFrameAssets(ir, sceneDir, fps, duration) {
2103
2234
  `video "${src}": unsupported format "${extname2(src)}" \u2014 supported: ${[...VIDEO_EXT].join(" ")}`
2104
2235
  );
2105
2236
  }
2106
- const candidates = [isAbsolute3(src) ? src : null, resolve3(sceneDir, src)].filter(
2237
+ const candidates = [isAbsolute4(src) ? src : null, resolve4(sceneDir, src)].filter(
2107
2238
  (c) => c !== null
2108
2239
  );
2109
- const found = candidates.find((c) => existsSync4(c));
2240
+ const found = candidates.find((c) => existsSync5(c));
2110
2241
  if (!found) throw new Error(`video "${src}" not found (tried: ${candidates.join(", ")})`);
2111
- const dir = await mkdtemp2(join4(tmpdir3(), "reframe-vframes-"));
2242
+ const dir = await mkdtemp3(join6(tmpdir4(), "reframe-vframes-"));
2112
2243
  try {
2113
2244
  const seconds = Math.max(1 / fps, reachBySrc.get(src) ?? duration);
2114
2245
  await runFfmpeg([
@@ -2121,15 +2252,15 @@ async function buildVideoFrameAssets(ir, sceneDir, fps, duration) {
2121
2252
  `fps=${fps},scale='min(iw,1280)':-2`,
2122
2253
  "-q:v",
2123
2254
  "4",
2124
- join4(dir, "%05d.jpg")
2255
+ join6(dir, "%05d.jpg")
2125
2256
  ]);
2126
2257
  const files = (await readdir(dir)).filter((f) => f.endsWith(".jpg")).sort();
2127
2258
  assets[src] = await Promise.all(
2128
- files.map(async (f) => `data:image/jpeg;base64,${(await readFile4(join4(dir, f))).toString("base64")}`)
2259
+ files.map(async (f) => `data:image/jpeg;base64,${(await readFile4(join6(dir, f))).toString("base64")}`)
2129
2260
  );
2130
2261
  if (assets[src].length === 0) throw new Error(`video "${src}": ffmpeg extracted no frames`);
2131
2262
  } finally {
2132
- await rm2(dir, { recursive: true, force: true });
2263
+ await rm3(dir, { recursive: true, force: true });
2133
2264
  }
2134
2265
  }
2135
2266
  return assets;
@@ -2228,7 +2359,7 @@ var init_reframeGlobal = __esm({
2228
2359
 
2229
2360
  // ../render-cli/src/frameLoop.ts
2230
2361
  import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
2231
- import { join as join5, dirname as dirname4 } from "node:path";
2362
+ import { join as join7, dirname as dirname4 } from "node:path";
2232
2363
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
2233
2364
  import { build } from "esbuild";
2234
2365
  import { chromium } from "playwright";
@@ -2255,12 +2386,12 @@ async function browserBundle() {
2255
2386
  if (true) {
2256
2387
  const { readFile: readFile7 } = await import("node:fs/promises");
2257
2388
  bundleCache = await readFile7(
2258
- join5(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
2389
+ join7(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.js"),
2259
2390
  "utf8"
2260
2391
  );
2261
2392
  return bundleCache;
2262
2393
  }
2263
- const entry = join5(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.ts");
2394
+ const entry = join7(dirname4(fileURLToPath3(import.meta.url)), "browserEntry.ts");
2264
2395
  const result = await build({
2265
2396
  entryPoints: [entry],
2266
2397
  bundle: true,
@@ -2305,7 +2436,7 @@ var init_frameLoop = __esm({
2305
2436
  init_videos();
2306
2437
  init_vclock();
2307
2438
  init_reframeGlobal();
2308
- framePath = (dir, i) => join5(dir, `${String(i).padStart(5, "0")}.png`);
2439
+ framePath = (dir, i) => join7(dir, `${String(i).padStart(5, "0")}.png`);
2309
2440
  bundleCache = null;
2310
2441
  }
2311
2442
  });
@@ -2318,9 +2449,9 @@ __export(batch_exports, {
2318
2449
  parseCsv: () => parseCsv,
2319
2450
  runBatch: () => runBatch
2320
2451
  });
2321
- import { mkdir as mkdir3, mkdtemp as mkdtemp3, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
2322
- import { tmpdir as tmpdir4 } from "node:os";
2323
- import { join as join6, 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";
2324
2455
  function overlayFromFlat(row, name) {
2325
2456
  const doc = { reframeOverlay: 1, name };
2326
2457
  for (const [key2, raw] of Object.entries(row)) {
@@ -2414,8 +2545,8 @@ async function runBatch(scene2, rows, opts) {
2414
2545
  try {
2415
2546
  const rowOverlay = overlayFromFlat(row, name);
2416
2547
  const { ir, report } = composeScene(scene2, ...opts.baseOverlays, rowOverlay);
2417
- const framesDir = await mkdtemp3(join6(tmpdir4(), `reframe-batch-${index}-`));
2418
- const output = join6(opts.outDir, `${name}.mp4`);
2548
+ const framesDir = await mkdtemp4(join8(tmpdir5(), `reframe-batch-${index}-`));
2549
+ const output = join8(opts.outDir, `${name}.mp4`);
2419
2550
  const plan = opts.noAudio ? null : resolveAudioPlan(compileScene(ir));
2420
2551
  try {
2421
2552
  const captured = await captureIr(ir, {
@@ -2427,12 +2558,12 @@ async function runBatch(scene2, rows, opts) {
2427
2558
  const videoTmp = `${output}.video.mp4`;
2428
2559
  await encodeMp4(captured.framesDir, captured.fps, videoTmp);
2429
2560
  await buildAudioTrack(plan, opts.scenePath ?? output, videoTmp, output);
2430
- await rm3(videoTmp, { force: true });
2561
+ await rm4(videoTmp, { force: true });
2431
2562
  } else {
2432
2563
  await encodeMp4(captured.framesDir, captured.fps, output);
2433
2564
  }
2434
2565
  } finally {
2435
- await rm3(framesDir, { recursive: true, force: true });
2566
+ await rm4(framesDir, { recursive: true, force: true });
2436
2567
  }
2437
2568
  result = {
2438
2569
  name,
@@ -2456,7 +2587,7 @@ async function runBatch(scene2, rows, opts) {
2456
2587
  };
2457
2588
  await Promise.all(Array.from({ length: Math.max(1, opts.concurrency) }, worker));
2458
2589
  await writeFile4(
2459
- join6(opts.outDir, "batch-report.json"),
2590
+ join8(opts.outDir, "batch-report.json"),
2460
2591
  JSON.stringify({ rows: results }, null, 2)
2461
2592
  );
2462
2593
  return results;
@@ -2482,7 +2613,7 @@ __export(loadScene_exports, {
2482
2613
  });
2483
2614
  import { build as build2 } from "esbuild";
2484
2615
  import { readFile as readFile6 } from "node:fs/promises";
2485
- import { dirname as dirname6, resolve as resolve4 } from "node:path";
2616
+ import { dirname as dirname6, resolve as resolve5 } from "node:path";
2486
2617
  import { fileURLToPath as fileURLToPath4 } from "node:url";
2487
2618
  async function loadDefault(path2) {
2488
2619
  if (path2.endsWith(".json")) return JSON.parse(await readFile6(path2, "utf8"));
@@ -2535,26 +2666,26 @@ var init_loadScene = __esm({
2535
2666
  "use strict";
2536
2667
  init_src();
2537
2668
  HERE = dirname6(fileURLToPath4(import.meta.url));
2538
- CORE_ENTRY = true ? resolve4(HERE, "index.js") : resolve4(HERE, "..", "..", "core", "src", "index.ts");
2669
+ CORE_ENTRY = true ? resolve5(HERE, "index.js") : resolve5(HERE, "..", "..", "core", "src", "index.ts");
2539
2670
  }
2540
2671
  });
2541
2672
 
2542
2673
  // ../render-cli/src/reframe.ts
2543
- import { spawn as spawn4, spawnSync } from "node:child_process";
2544
- import { existsSync as existsSync5 } from "node:fs";
2674
+ import { spawn as spawn5, spawnSync } from "node:child_process";
2675
+ import { existsSync as existsSync6 } from "node:fs";
2545
2676
  import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
2546
- import { basename, isAbsolute as isAbsolute4, join as join7, resolve as resolve5 } from "node:path";
2677
+ import { basename, isAbsolute as isAbsolute5, join as join9, resolve as resolve6 } from "node:path";
2547
2678
  import { dirname as dirname7 } from "node:path";
2548
2679
  import { fileURLToPath as fileURLToPath5 } from "node:url";
2549
2680
  var PACKAGED = true;
2550
2681
  var HERE2 = dirname7(fileURLToPath5(import.meta.url));
2551
- var ROOT2 = PACKAGED ? resolve5(HERE2, "..") : resolve5(HERE2, "..", "..", "..");
2682
+ var ROOT2 = PACKAGED ? resolve6(HERE2, "..") : resolve6(HERE2, "..", "..", "..");
2552
2683
  var USER_CWD = process.env.INIT_CWD ?? process.cwd();
2553
- var RENDER_CLI = PACKAGED ? join7(ROOT2, "dist", "cli.js") : join7(ROOT2, "packages", "render-cli", "src", "cli.ts");
2554
- var LABELS = PACKAGED ? join7(ROOT2, "dist", "labels.js") : join7(ROOT2, "packages", "render-cli", "src", "labels.ts");
2555
- var PLAYER = PACKAGED ? join7(ROOT2, "dist", "player.js") : join7(ROOT2, "packages", "render-cli", "src", "player.ts");
2556
- var ANALYZE = PACKAGED ? join7(ROOT2, "dist", "analyze.js") : join7(ROOT2, "benchmark", "harness", "motion", "analyze.ts");
2557
- var TRACE = PACKAGED ? join7(ROOT2, "dist", "trace-cli.js") : join7(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");
2558
2689
  var CMD = PACKAGED ? "reframe" : "pnpm reframe";
2559
2690
  var USAGE = `reframe \u2014 declarative motion graphics
2560
2691
 
@@ -2574,7 +2705,7 @@ usage:
2574
2705
  ${CMD} guide [--regen] print the scene-authoring guide (for you or your AI)
2575
2706
  ${CMD} demo run the edit-survival demo (3 mp4s into out/)
2576
2707
  `;
2577
- var userPath = (p) => isAbsolute4(p) ? p : resolve5(USER_CWD, p);
2708
+ var userPath = (p) => isAbsolute5(p) ? p : resolve6(USER_CWD, p);
2578
2709
  function fail(message) {
2579
2710
  console.error(`error: ${message}`);
2580
2711
  process.exit(2);
@@ -2584,9 +2715,9 @@ function preflightFfmpeg() {
2584
2715
  fail("ffmpeg not found on PATH \u2014 install it first (macOS: brew install ffmpeg, debian: apt install ffmpeg)");
2585
2716
  }
2586
2717
  }
2587
- function run(cmd, args, opts = {}) {
2718
+ function run2(cmd, args, opts = {}) {
2588
2719
  return new Promise((res) => {
2589
- const proc = spawn4(cmd, args, {
2720
+ const proc = spawn5(cmd, args, {
2590
2721
  cwd: opts.cwd ?? (PACKAGED ? USER_CWD : ROOT2),
2591
2722
  stdio: ["inherit", "inherit", "pipe"],
2592
2723
  ...opts.env && { env: { ...process.env, ...opts.env } }
@@ -2676,7 +2807,7 @@ async function main() {
2676
2807
 
2677
2808
  ${USAGE}`);
2678
2809
  const inputPath = userPath(input);
2679
- if (!existsSync5(inputPath)) fail(`no such file: ${inputPath}`);
2810
+ if (!existsSync6(inputPath)) fail(`no such file: ${inputPath}`);
2680
2811
  const mode = /\.(ts|json)$/.test(input) ? "ir" : /\.html$/.test(input) ? "html" : null;
2681
2812
  if (!mode) {
2682
2813
  fail(`cannot infer render mode from "${input}" \u2014 expected .ts/.json (reframe scene) or .html (GSAP page)`);
@@ -2686,17 +2817,17 @@ ${USAGE}`);
2686
2817
  fail("html render requires --duration <seconds> (the page does not declare its own length)");
2687
2818
  }
2688
2819
  preflightFfmpeg();
2689
- const outBase = PACKAGED ? join7(USER_CWD, "out") : join7(ROOT2, "out");
2820
+ const outBase = PACKAGED ? join9(USER_CWD, "out") : join9(ROOT2, "out");
2690
2821
  let outArgs = args;
2691
2822
  if (!args.includes("-o")) {
2692
2823
  await mkdir4(outBase, { recursive: true });
2693
- outArgs = [...args, "-o", join7(outBase, `${basename(input).replace(/\.[^.]+$/, "")}.mp4`)];
2824
+ outArgs = [...args, "-o", join9(outBase, `${basename(input).replace(/\.[^.]+$/, "")}.mp4`)];
2694
2825
  }
2695
2826
  outArgs = outArgs.map(
2696
2827
  (a, i) => outArgs[i - 1] === "--overlay" || outArgs[i - 1] === "-o" ? userPath(a) : a
2697
2828
  );
2698
2829
  process.exit(
2699
- 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]))
2700
2831
  );
2701
2832
  }
2702
2833
  case "labels": {
@@ -2705,9 +2836,9 @@ ${USAGE}`);
2705
2836
 
2706
2837
  ${USAGE}`);
2707
2838
  const inputPath = userPath(input);
2708
- if (!existsSync5(inputPath)) fail(`no such file: ${inputPath}`);
2839
+ if (!existsSync6(inputPath)) fail(`no such file: ${inputPath}`);
2709
2840
  process.exit(
2710
- 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]))
2711
2842
  );
2712
2843
  }
2713
2844
  case "player": {
@@ -2716,13 +2847,13 @@ ${USAGE}`);
2716
2847
 
2717
2848
  ${USAGE}`);
2718
2849
  const inputPath = userPath(input);
2719
- if (!existsSync5(inputPath)) fail(`no such file: ${inputPath}`);
2850
+ if (!existsSync6(inputPath)) fail(`no such file: ${inputPath}`);
2720
2851
  const oIdx = rest.indexOf("-o");
2721
- const outBase = PACKAGED ? join7(USER_CWD, "out") : join7(ROOT2, "out");
2722
- const outPath = oIdx >= 0 && rest[oIdx + 1] ? userPath(rest[oIdx + 1]) : join7(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`);
2723
2854
  await mkdir4(dirname7(outPath), { recursive: true });
2724
2855
  process.exit(
2725
- 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]))
2726
2857
  );
2727
2858
  }
2728
2859
  case "logo": {
@@ -2739,7 +2870,7 @@ ${USAGE}`);
2739
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]`);
2740
2871
  }
2741
2872
  preflightFfmpeg();
2742
- const { tmpdir: tmpdir5 } = await import("node:os");
2873
+ const { tmpdir: tmpdir6 } = await import("node:os");
2743
2874
  const { resolveLogo: resolveLogo2, buildLogoSting: buildLogoSting2 } = await Promise.resolve().then(() => (init_logoSting(), logoSting_exports));
2744
2875
  const num = (k) => flags[k] !== void 0 ? Number(flags[k]) : void 0;
2745
2876
  console.log(`loading logo: ${arg} \u2026`);
@@ -2752,14 +2883,14 @@ ${USAGE}`);
2752
2883
  seed: num("seed")
2753
2884
  });
2754
2885
  const sceneIR = buildLogoSting2(data);
2755
- const tmp = join7(tmpdir5(), `reframe-logo-${slug}-${process.pid}.json`);
2886
+ const tmp = join9(tmpdir6(), `reframe-logo-${slug}-${process.pid}.json`);
2756
2887
  await writeFile5(tmp, JSON.stringify(sceneIR));
2757
- const outBase = PACKAGED ? join7(USER_CWD, "out") : join7(ROOT2, "out");
2758
- const out = flags.o ? userPath(flags.o) : join7(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`);
2759
2890
  await mkdir4(dirname7(out), { recursive: true });
2760
2891
  console.log(`rendering ${data.name} (${data.paths.length} path${data.paths.length > 1 ? "s" : ""}, motion: ${data.motion ?? "reveal-orbit"}) \u2192 ${out}`);
2761
2892
  process.exit(
2762
- 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"]))
2763
2894
  );
2764
2895
  }
2765
2896
  case "batch": {
@@ -2767,9 +2898,9 @@ ${USAGE}`);
2767
2898
  if (!sceneArg || !dataArg) fail(`usage: ${CMD} batch <scene.ts> <data.json|csv> [...]`);
2768
2899
  const scenePath = userPath(sceneArg);
2769
2900
  const dataPath = userPath(dataArg);
2770
- for (const p of [scenePath, dataPath]) if (!existsSync5(p)) fail(`no such file: ${p}`);
2901
+ for (const p of [scenePath, dataPath]) if (!existsSync6(p)) fail(`no such file: ${p}`);
2771
2902
  preflightFfmpeg();
2772
- let outDir = PACKAGED ? join7(USER_CWD, "out", "batch") : join7(ROOT2, "out", "batch");
2903
+ let outDir = PACKAGED ? join9(USER_CWD, "out", "batch") : join9(ROOT2, "out", "batch");
2773
2904
  let concurrency = 3;
2774
2905
  let fps;
2775
2906
  const baseOverlayPaths = [];
@@ -2808,7 +2939,7 @@ ${USAGE}`);
2808
2939
  const orphaned = results.filter((r) => !r.error && r.orphans.length > 0).length;
2809
2940
  console.log(
2810
2941
  `
2811
- ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed \u2014 report: ${join7(outDir, "batch-report.json")}`
2942
+ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed \u2014 report: ${join9(outDir, "batch-report.json")}`
2812
2943
  );
2813
2944
  process.exit(failed > 0 ? 1 : 0);
2814
2945
  }
@@ -2819,15 +2950,15 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2819
2950
  if (PACKAGED) {
2820
2951
  const { createRequire } = await import("node:module");
2821
2952
  const vitePkg = createRequire(import.meta.url).resolve("vite/package.json");
2822
- const viteBin = join7(dirname7(vitePkg), "bin", "vite.js");
2953
+ const viteBin = join9(dirname7(vitePkg), "bin", "vite.js");
2823
2954
  process.exit(
2824
- await run(process.execPath, [viteBin, join7(ROOT2, "preview")], {
2955
+ await run2(process.execPath, [viteBin, join9(ROOT2, "preview")], {
2825
2956
  env: { REFRAME_SCENE_DIR: USER_CWD }
2826
2957
  })
2827
2958
  );
2828
2959
  }
2829
2960
  process.exit(
2830
- await run("pnpm", ["--filter", "@reframe/preview", "dev"], {
2961
+ await run2("pnpm", ["--filter", "@reframe/preview", "dev"], {
2831
2962
  env: { REFRAME_SCENE_DIR: USER_CWD }
2832
2963
  })
2833
2964
  );
@@ -2839,10 +2970,10 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2839
2970
  fail(`scene name must be kebab-case (a-z, 0-9, -): got "${name}"`);
2840
2971
  }
2841
2972
  const inRepo = USER_CWD === ROOT2 || USER_CWD.startsWith(ROOT2 + "/");
2842
- const targetDir = inRepo ? join7(ROOT2, "examples", "scenes") : USER_CWD;
2843
- const target = join7(targetDir, `${name}.ts`);
2973
+ const targetDir = inRepo ? join9(ROOT2, "examples", "scenes") : USER_CWD;
2974
+ const target = join9(targetDir, `${name}.ts`);
2844
2975
  const shown = inRepo ? `examples/scenes/${name}.ts` : `${name}.ts`;
2845
- if (existsSync5(target)) fail(`${shown} already exists`);
2976
+ if (existsSync6(target)) fail(`${shown} already exists`);
2846
2977
  const id = name.split("-")[0] ?? name;
2847
2978
  await writeFile5(target, SCENE_TEMPLATE(name, id));
2848
2979
  console.log(`created ${shown}
@@ -2856,7 +2987,7 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2856
2987
  if (!input) fail(`usage: ${CMD} motion <mp4|framesDir> [...args]`);
2857
2988
  preflightFfmpeg();
2858
2989
  process.exit(
2859
- 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)]))
2860
2991
  );
2861
2992
  }
2862
2993
  case "trace": {
@@ -2867,11 +2998,11 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2867
2998
  (a, i) => rest.slice(1)[i - 1] === "--apply" || rest.slice(1)[i - 1] === "-o" ? userPath(a) : a
2868
2999
  );
2869
3000
  process.exit(
2870
- 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]))
2871
3002
  );
2872
3003
  }
2873
3004
  case "guide": {
2874
- const file = rest.includes("--regen") ? PACKAGED ? join7(ROOT2, "guides", "regen-contract.md") : join7(ROOT2, "docs", "regen-contract.md") : PACKAGED ? join7(ROOT2, "guides", "edsl-guide.md") : join7(ROOT2, "benchmark", "guides", "edsl-guide.md");
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");
2875
3006
  const { readFile: readFile7 } = await import("node:fs/promises");
2876
3007
  process.stdout.write(await readFile7(file, "utf8"));
2877
3008
  return;
@@ -2884,7 +3015,7 @@ ${results.length - failed} rendered (${orphaned} with orphans), ${failed} failed
2884
3015
  }
2885
3016
  preflightFfmpeg();
2886
3017
  process.exit(
2887
- await run("npx", ["tsx", join7(ROOT2, "examples", "scripts", "demo-edit-survival.ts")])
3018
+ await run2("npx", ["tsx", join9(ROOT2, "examples", "scripts", "demo-edit-survival.ts")])
2888
3019
  );
2889
3020
  default:
2890
3021
  console.log(USAGE);