motionrender 0.2.2 → 0.2.3

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/cli.js CHANGED
@@ -1272,10 +1272,146 @@ var init_src4 = __esm({
1272
1272
  }
1273
1273
  });
1274
1274
 
1275
+ // packages/ffmpeg/src/auto-install.ts
1276
+ import { homedir } from "node:os";
1277
+ import { join as join2 } from "node:path";
1278
+ import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, renameSync } from "node:fs";
1279
+ import { spawnSync } from "node:child_process";
1280
+ import { pipeline } from "node:stream/promises";
1281
+ import { Readable } from "node:stream";
1282
+ import { createGunzip } from "node:zlib";
1283
+ function hasSystemFfmpeg() {
1284
+ if (checkedSystemFfmpeg) return systemFfmpegAvailable;
1285
+ try {
1286
+ const res = spawnSync("ffmpeg", ["-version"], { stdio: "ignore" });
1287
+ systemFfmpegAvailable = res.status === 0;
1288
+ } catch {
1289
+ systemFfmpegAvailable = false;
1290
+ }
1291
+ checkedSystemFfmpeg = true;
1292
+ return systemFfmpegAvailable;
1293
+ }
1294
+ function hasSystemFfprobe() {
1295
+ if (checkedSystemFfprobe) return systemFfprobeAvailable;
1296
+ try {
1297
+ const res = spawnSync("ffprobe", ["-version"], { stdio: "ignore" });
1298
+ systemFfprobeAvailable = res.status === 0;
1299
+ } catch {
1300
+ systemFfprobeAvailable = false;
1301
+ }
1302
+ checkedSystemFfprobe = true;
1303
+ return systemFfprobeAvailable;
1304
+ }
1305
+ function getPlatformArch() {
1306
+ const p2 = process.platform;
1307
+ const a = process.arch;
1308
+ if (p2 === "win32") return { platform: "win32", arch: "x64" };
1309
+ if (p2 === "darwin") return { platform: "darwin", arch: a === "arm64" ? "arm64" : "x64" };
1310
+ if (p2 === "linux") return { platform: "linux", arch: a === "arm64" ? "arm64" : a === "arm" ? "arm" : "x64" };
1311
+ return { platform: p2, arch: a };
1312
+ }
1313
+ async function downloadAndExtract(name2, destPath, onProgress) {
1314
+ const { platform, arch } = getPlatformArch();
1315
+ const url = `${BASE_URL}/${name2}-${platform}-${arch}.gz`;
1316
+ if (onProgress) onProgress(`Downloading ${name2} (${platform}-${arch})...`);
1317
+ const res = await fetch(url, { redirect: "follow" });
1318
+ if (!res.ok || !res.body) {
1319
+ throw new Error(`Failed to download ${name2} from ${url} (HTTP ${res.status})`);
1320
+ }
1321
+ const tmpPath = `${destPath}.tmp-${Date.now()}`;
1322
+ try {
1323
+ const fileStream = createWriteStream(tmpPath);
1324
+ const nodeReadable = Readable.fromWeb(res.body);
1325
+ await pipeline(nodeReadable, createGunzip(), fileStream);
1326
+ if (!isWin) {
1327
+ chmodSync(tmpPath, 493);
1328
+ }
1329
+ if (existsSync(destPath)) {
1330
+ try {
1331
+ unlinkSync(destPath);
1332
+ } catch {
1333
+ }
1334
+ }
1335
+ renameSync(tmpPath, destPath);
1336
+ } catch (err) {
1337
+ try {
1338
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
1339
+ } catch {
1340
+ }
1341
+ throw err;
1342
+ }
1343
+ }
1344
+ async function ensureFfmpeg(options3) {
1345
+ if (ensureFfmpegPromise) return ensureFfmpegPromise;
1346
+ ensureFfmpegPromise = (async () => {
1347
+ const ffmpegFromEnv = process.env.OVE_FFMPEG_PATH;
1348
+ const ffprobeFromEnv = process.env.OVE_FFPROBE_PATH;
1349
+ const hasLocalFfmpeg = existsSync(localFfmpegPath);
1350
+ const hasLocalFfprobe = existsSync(localFfprobePath);
1351
+ const hasSysFfmpeg = !ffmpegFromEnv && !hasLocalFfmpeg ? hasSystemFfmpeg() : false;
1352
+ const hasSysFfprobe = !ffprobeFromEnv && !hasLocalFfprobe ? hasSystemFfprobe() : false;
1353
+ const ffmpegReady = Boolean(ffmpegFromEnv || hasLocalFfmpeg || hasSysFfmpeg);
1354
+ const ffprobeReady = Boolean(ffprobeFromEnv || hasLocalFfprobe || hasSysFfprobe);
1355
+ if (ffmpegReady && ffprobeReady) {
1356
+ return { ffmpeg: getFfmpegPath(), ffprobe: getFfprobePath() };
1357
+ }
1358
+ if (!existsSync(binDir)) {
1359
+ mkdirSync(binDir, { recursive: true });
1360
+ }
1361
+ const log = options3?.onProgress ?? ((msg) => console.log(msg));
1362
+ if (!ffmpegReady) {
1363
+ log("FFmpeg not found. Automatically downloading static build (one-time setup)...");
1364
+ await downloadAndExtract("ffmpeg", localFfmpegPath, log);
1365
+ log("\u2713 FFmpeg installed successfully.");
1366
+ }
1367
+ if (!ffprobeReady) {
1368
+ log("ffprobe not found. Automatically downloading static build (one-time setup)...");
1369
+ await downloadAndExtract("ffprobe", localFfprobePath, log);
1370
+ log("\u2713 ffprobe installed successfully.");
1371
+ }
1372
+ return { ffmpeg: getFfmpegPath(), ffprobe: getFfprobePath() };
1373
+ })();
1374
+ try {
1375
+ return await ensureFfmpegPromise;
1376
+ } catch (err) {
1377
+ ensureFfmpegPromise = null;
1378
+ throw err;
1379
+ }
1380
+ }
1381
+ var isWin, binDir, localFfmpegPath, localFfprobePath, checkedSystemFfmpeg, systemFfmpegAvailable, checkedSystemFfprobe, systemFfprobeAvailable, getFfmpegPath, getFfprobePath, RELEASE_TAG, BASE_URL, ensureFfmpegPromise;
1382
+ var init_auto_install = __esm({
1383
+ "packages/ffmpeg/src/auto-install.ts"() {
1384
+ "use strict";
1385
+ isWin = process.platform === "win32";
1386
+ binDir = join2(homedir(), ".motionrender", "bin");
1387
+ localFfmpegPath = join2(binDir, isWin ? "ffmpeg.exe" : "ffmpeg");
1388
+ localFfprobePath = join2(binDir, isWin ? "ffprobe.exe" : "ffprobe");
1389
+ checkedSystemFfmpeg = false;
1390
+ systemFfmpegAvailable = false;
1391
+ checkedSystemFfprobe = false;
1392
+ systemFfprobeAvailable = false;
1393
+ getFfmpegPath = () => {
1394
+ if (process.env.OVE_FFMPEG_PATH) return process.env.OVE_FFMPEG_PATH;
1395
+ if (existsSync(localFfmpegPath)) return localFfmpegPath;
1396
+ if (hasSystemFfmpeg()) return "ffmpeg";
1397
+ return localFfmpegPath;
1398
+ };
1399
+ getFfprobePath = () => {
1400
+ if (process.env.OVE_FFPROBE_PATH) return process.env.OVE_FFPROBE_PATH;
1401
+ if (existsSync(localFfprobePath)) return localFfprobePath;
1402
+ if (hasSystemFfprobe()) return "ffprobe";
1403
+ return localFfprobePath;
1404
+ };
1405
+ RELEASE_TAG = "b6.1.1";
1406
+ BASE_URL = `https://github.com/eugeneware/ffmpeg-static/releases/download/${RELEASE_TAG}`;
1407
+ ensureFfmpegPromise = null;
1408
+ }
1409
+ });
1410
+
1275
1411
  // packages/ffmpeg/src/index.ts
1276
1412
  import { spawn } from "node:child_process";
1277
1413
  import { once } from "node:events";
1278
- import { join as join2 } from "node:path";
1414
+ import { join as join3 } from "node:path";
1279
1415
  function envelopeExpression(points) {
1280
1416
  if (points.length === 1) return num2(points[0][1]);
1281
1417
  const segment = (i) => {
@@ -1350,11 +1486,13 @@ function concatSegments(segments, listFile, output, audio, options3 = {}) {
1350
1486
  const list = segments.map((s) => `file '${s.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`).join("\n");
1351
1487
  return import("node:fs/promises").then((fs) => fs.writeFile(listFile, list)).then(() => run(["-y", "-f", "concat", "-safe", "0", "-i", listFile, ...audioCodec ? audioMixArgs(audio, audioCodec) : [], ...audio && audioCodec ? [] : ["-map", "0:v"], "-c:v", "copy", ...options3.format === "mp4" || options3.format === void 0 ? ["-movflags", "+faststart"] : [], output], "ignore").done);
1352
1488
  }
1353
- function extractFrames(source, outDir, options3) {
1489
+ async function extractFrames(source, outDir, options3) {
1490
+ await ensureFfmpeg();
1354
1491
  const { fps, maxWidth, maxHeight, durationSeconds, startSeconds } = options3;
1355
- return run(["-v", "error", "-y", ...startSeconds ? ["-ss", String(startSeconds)] : [], ...durationSeconds === void 0 ? [] : ["-t", String(durationSeconds)], "-i", source, "-an", "-vf", `fps=${fps},scale=w='min(iw,${maxWidth})':h='min(ih,${maxHeight})':force_original_aspect_ratio=decrease`, "-q:v", "2", join2(outDir, "%06d.jpg")], "ignore").done;
1492
+ return run(["-v", "error", "-y", ...startSeconds ? ["-ss", String(startSeconds)] : [], ...durationSeconds === void 0 ? [] : ["-t", String(durationSeconds)], "-i", source, "-an", "-vf", `fps=${fps},scale=w='min(iw,${maxWidth})':h='min(ih,${maxHeight})':force_original_aspect_ratio=decrease`, "-q:v", "2", join3(outDir, "%06d.jpg")], "ignore").done;
1356
1493
  }
1357
- function checkFfmpeg() {
1494
+ async function checkFfmpeg() {
1495
+ await ensureFfmpeg();
1358
1496
  return new Promise((resolve27, reject) => {
1359
1497
  const child = spawn(ffmpegPath(), ["-hide_banner", "-encoders"]);
1360
1498
  let out = "";
@@ -1371,7 +1509,8 @@ function checkFfmpeg() {
1371
1509
  });
1372
1510
  });
1373
1511
  }
1374
- function hasAudioStream(path) {
1512
+ async function hasAudioStream(path) {
1513
+ await ensureFfmpeg();
1375
1514
  return new Promise((resolve27, reject) => {
1376
1515
  const child = spawn(ffprobePath(), ["-v", "error", "-select_streams", "a", "-show_entries", "stream=index", "-of", "csv=p=0", path]);
1377
1516
  let out = "";
@@ -1380,7 +1519,8 @@ function hasAudioStream(path) {
1380
1519
  child.on("close", (c) => c === 0 ? resolve27(out.trim().length > 0) : reject(new Error(`ffprobe could not read ${path}`)));
1381
1520
  });
1382
1521
  }
1383
- function probe(path) {
1522
+ async function probe(path) {
1523
+ await ensureFfmpeg();
1384
1524
  return new Promise((resolve27, reject) => {
1385
1525
  const child = spawn(ffprobePath(), ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path]);
1386
1526
  let out = "";
@@ -1389,7 +1529,8 @@ function probe(path) {
1389
1529
  child.on("close", (c) => c === 0 ? resolve27(out.trim()) : reject(new Error("ffprobe failed")));
1390
1530
  });
1391
1531
  }
1392
- function extractAudio(source, output) {
1532
+ async function extractAudio(source, output) {
1533
+ await ensureFfmpeg();
1393
1534
  return new Promise((resolve27, reject) => {
1394
1535
  const child = spawn(ffmpegPath(), ["-y", "-i", source, "-vn", "-c:a", "aac", "-b:a", "96k", output], { stdio: ["ignore", "ignore", "pipe"] });
1395
1536
  let error = "";
@@ -1406,10 +1547,12 @@ var x264Presets, defaultPreset, ffmpegPath, ffprobePath, notFound, num2, seconds
1406
1547
  var init_src5 = __esm({
1407
1548
  "packages/ffmpeg/src/index.ts"() {
1408
1549
  "use strict";
1550
+ init_auto_install();
1551
+ init_auto_install();
1409
1552
  x264Presets = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"];
1410
1553
  defaultPreset = "veryfast";
1411
- ffmpegPath = () => process.env.OVE_FFMPEG_PATH || "ffmpeg";
1412
- ffprobePath = () => process.env.OVE_FFPROBE_PATH || "ffprobe";
1554
+ ffmpegPath = () => getFfmpegPath();
1555
+ ffprobePath = () => getFfprobePath();
1413
1556
  notFound = (tool, env) => new Error(`${tool} was not found. Install FFmpeg (it includes ${tool}) and put it on PATH, or set ${env} to the executable.`);
1414
1557
  num2 = (n) => {
1415
1558
  const text = n.toFixed(5).replace(/\.?0+$/, "");
@@ -1452,13 +1595,13 @@ var init_src5 = __esm({
1452
1595
  // packages/core/src/media.ts
1453
1596
  import { spawn as spawn2 } from "node:child_process";
1454
1597
  import { mkdir, readFile as readFile2, stat as stat2, writeFile } from "node:fs/promises";
1455
- import { join as join3 } from "node:path";
1598
+ import { join as join4 } from "node:path";
1456
1599
  import { createHash } from "node:crypto";
1457
1600
  async function measureSource(source) {
1458
1601
  const version = isUrl(source) ? "" : await stat2(source).then((s) => `${s.size}:${s.mtimeMs}`, () => {
1459
1602
  throw new Error(`Media not found: ${source}`);
1460
1603
  });
1461
- const key = createHash("sha256").update(`${source}|${version}|v1`).digest("hex").slice(0, 32), file2 = join3(mediaCache(), `${key}.json`);
1604
+ const key = createHash("sha256").update(`${source}|${version}|v1`).digest("hex").slice(0, 32), file2 = join4(mediaCache(), `${key}.json`);
1462
1605
  const cached2 = await readFile2(file2, "utf8").then(JSON.parse, () => void 0);
1463
1606
  if (cached2) {
1464
1607
  void touchCacheEntry(file2);
@@ -1645,15 +1788,15 @@ ${layer.css ?? ""}`, refs = [...text.matchAll(/(?<![\w:-])src\s*=\s*(["'])([^"']
1645
1788
 
1646
1789
  // packages/renderer/src/video.ts
1647
1790
  import { mkdir as mkdir2, readdir as readdir2, rename, rm as rm2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
1648
- import { join as join4 } from "node:path";
1791
+ import { join as join5 } from "node:path";
1649
1792
  import { createHash as createHash2, randomUUID } from "node:crypto";
1650
1793
  async function decode(source, key, first = 0) {
1651
1794
  const version = isUrl(source) ? "" : await stat3(source).then((s) => `${s.size}:${s.mtimeMs}`, () => {
1652
1795
  throw new Error(`Video not found: ${source}`);
1653
1796
  });
1654
- const dir = join4(videoCacheRoot(), createHash2("sha256").update(JSON.stringify({ source, version, ...key })).digest("hex").slice(0, 32));
1797
+ const dir = join5(videoCacheRoot(), createHash2("sha256").update(JSON.stringify({ source, version, ...key })).digest("hex").slice(0, 32));
1655
1798
  const count = async () => (await readdir2(dir)).filter((f) => f.endsWith(".jpg")).length;
1656
- if (await stat3(join4(dir, complete)).then(() => true, () => false)) {
1799
+ if (await stat3(join5(dir, complete)).then(() => true, () => false)) {
1657
1800
  void touchCacheEntry(dir);
1658
1801
  return { dir, count: first + await count(), first };
1659
1802
  }
@@ -1661,9 +1804,9 @@ async function decode(source, key, first = 0) {
1661
1804
  await mkdir2(scratch, { recursive: true });
1662
1805
  try {
1663
1806
  await extractFrames(source, scratch, key);
1664
- await writeFile2(join4(scratch, complete), "");
1807
+ await writeFile2(join5(scratch, complete), "");
1665
1808
  await rename(scratch, dir).catch(async (error) => {
1666
- if (!await stat3(join4(dir, complete)).then(() => true, () => false)) throw error;
1809
+ if (!await stat3(join5(dir, complete)).then(() => true, () => false)) throw error;
1667
1810
  });
1668
1811
  } finally {
1669
1812
  await rm2(scratch, { recursive: true, force: true });
@@ -1692,7 +1835,7 @@ var init_video = __esm({
1692
1835
  init_src3();
1693
1836
  init_src4();
1694
1837
  init_src5();
1695
- videoFrameFile = (video, index) => join4(video.dir, `${String(Math.max(0, index - video.first) + 1).padStart(6, "0")}.jpg`);
1838
+ videoFrameFile = (video, index) => join5(video.dir, `${String(Math.max(0, index - video.first) + 1).padStart(6, "0")}.jpg`);
1696
1839
  videoCacheRoot = () => cacheDir("video");
1697
1840
  complete = ".complete";
1698
1841
  }
@@ -1772,29 +1915,29 @@ var init_pool = __esm({
1772
1915
  });
1773
1916
 
1774
1917
  // packages/graphics/src/fonts.ts
1775
- import { existsSync } from "node:fs";
1918
+ import { existsSync as existsSync2 } from "node:fs";
1776
1919
  import { readFile as readFile3 } from "node:fs/promises";
1777
1920
  import { createRequire } from "node:module";
1778
- import { dirname, join as join5 } from "node:path";
1921
+ import { dirname, join as join6 } from "node:path";
1779
1922
  import { fileURLToPath } from "node:url";
1780
1923
  function builtinFontDir() {
1781
1924
  const bundled = fileURLToPath(new URL("./fonts/", import.meta.url));
1782
- if (existsSync(join5(bundled, builtinFontFaces[0].file))) return bundled;
1783
- return join5(dirname(createRequire(import.meta.url).resolve("@fontsource-variable/inter/package.json")), "files");
1925
+ if (existsSync2(join6(bundled, builtinFontFaces[0].file))) return bundled;
1926
+ return join6(dirname(createRequire(import.meta.url).resolve("@fontsource-variable/inter/package.json")), "files");
1784
1927
  }
1785
1928
  var cached, builtinFonts;
1786
1929
  var init_fonts = __esm({
1787
1930
  "packages/graphics/src/fonts.ts"() {
1788
1931
  "use strict";
1789
1932
  init_src6();
1790
- builtinFonts = () => cached ??= Promise.all(builtinFontFaces.map(async (face) => ({ ...face, src: `builtin:${face.file}`, url: `data:font/woff2;base64,${(await readFile3(join5(builtinFontDir(), face.file))).toString("base64")}` })));
1933
+ builtinFonts = () => cached ??= Promise.all(builtinFontFaces.map(async (face) => ({ ...face, src: `builtin:${face.file}`, url: `data:font/woff2;base64,${(await readFile3(join6(builtinFontDir(), face.file))).toString("base64")}` })));
1791
1934
  }
1792
1935
  });
1793
1936
 
1794
1937
  // packages/graphics/src/runtime.ts
1795
- import { existsSync as existsSync2, readFileSync } from "node:fs";
1938
+ import { existsSync as existsSync3, readFileSync } from "node:fs";
1796
1939
  import { createRequire as createRequire2 } from "node:module";
1797
- import { join as join6 } from "node:path";
1940
+ import { join as join7 } from "node:path";
1798
1941
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1799
1942
  var runtimeDir, read, textLayoutSource, lottieFile, lottiePath, lottieSource, canvasRuntimeSource, layoutText, layoutRich;
1800
1943
  var init_runtime = __esm({
@@ -1802,14 +1945,14 @@ var init_runtime = __esm({
1802
1945
  "use strict";
1803
1946
  runtimeDir = () => {
1804
1947
  const bundled = fileURLToPath2(new URL("./runtime/", import.meta.url));
1805
- return existsSync2(join6(bundled, "canvas-runtime.js")) ? bundled : fileURLToPath2(new URL("../runtime/", import.meta.url));
1948
+ return existsSync3(join7(bundled, "canvas-runtime.js")) ? bundled : fileURLToPath2(new URL("../runtime/", import.meta.url));
1806
1949
  };
1807
- read = (name2) => readFileSync(join6(runtimeDir(), name2), "utf8");
1950
+ read = (name2) => readFileSync(join7(runtimeDir(), name2), "utf8");
1808
1951
  textLayoutSource = read("text-layout.js");
1809
1952
  lottieFile = "lottie_light_canvas.min.js";
1810
1953
  lottiePath = () => {
1811
- const bundled = join6(runtimeDir(), lottieFile);
1812
- return existsSync2(bundled) ? bundled : createRequire2(import.meta.url).resolve(`lottie-web/build/player/${lottieFile}`);
1954
+ const bundled = join7(runtimeDir(), lottieFile);
1955
+ return existsSync3(bundled) ? bundled : createRequire2(import.meta.url).resolve(`lottie-web/build/player/${lottieFile}`);
1813
1956
  };
1814
1957
  lottieSource = readFileSync(lottiePath(), "utf8");
1815
1958
  canvasRuntimeSource = `${textLayoutSource}
@@ -1821,28 +1964,70 @@ ${read("canvas-runtime.js")}`;
1821
1964
 
1822
1965
  // packages/renderer/src/index.ts
1823
1966
  import { chromium } from "playwright";
1824
- import { existsSync as existsSync3 } from "node:fs";
1967
+ import { existsSync as existsSync4 } from "node:fs";
1825
1968
  import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
1826
1969
  import { availableParallelism } from "node:os";
1970
+ async function ensureChromium(options3) {
1971
+ if (existsSync4(chromium.executablePath())) return;
1972
+ for (const channel of ["chrome", "msedge"]) {
1973
+ try {
1974
+ const b = await chromium.launch({ headless: true, channel, args: browserArgs });
1975
+ await b.close();
1976
+ return;
1977
+ } catch {
1978
+ }
1979
+ }
1980
+ if (ensureChromiumPromise) return ensureChromiumPromise;
1981
+ ensureChromiumPromise = (async () => {
1982
+ const log = options3?.onProgress ?? ((msg) => console.log(msg));
1983
+ log("No Chrome, Edge, or Chromium browser detected.");
1984
+ log("Downloading Playwright Chromium (one-time setup)...");
1985
+ const { createRequire: createRequire3 } = await import("node:module");
1986
+ const { dirname: dirname21, join: join25 } = await import("node:path");
1987
+ const { spawn: spawn4 } = await import("node:child_process");
1988
+ const req = createRequire3(import.meta.url);
1989
+ const cli = join25(dirname21(req.resolve("playwright/package.json")), "cli.js");
1990
+ await new Promise((resolve27, reject) => {
1991
+ const child = spawn4(process.execPath, [cli, "install", "chromium"], { stdio: "inherit" });
1992
+ child.on("close", (code) => {
1993
+ if (code === 0) resolve27();
1994
+ else reject(new Error(`Chromium installation failed (exit code ${code})`));
1995
+ });
1996
+ child.on("error", reject);
1997
+ });
1998
+ log("\u2713 Playwright Chromium installed successfully.");
1999
+ })();
2000
+ try {
2001
+ await ensureChromiumPromise;
2002
+ } catch (err) {
2003
+ ensureChromiumPromise = null;
2004
+ throw err;
2005
+ }
2006
+ }
1827
2007
  async function launchBrowser() {
1828
2008
  const explicit = process.env.OVE_CHROMIUM_EXECUTABLE;
1829
2009
  if (explicit) {
1830
- if (!existsSync3(explicit)) throw new Error(`OVE_CHROMIUM_EXECUTABLE points to ${explicit}, which does not exist.`);
2010
+ if (!existsSync4(explicit)) throw new Error(`OVE_CHROMIUM_EXECUTABLE points to ${explicit}, which does not exist.`);
1831
2011
  return chromium.launch({ headless: true, executablePath: explicit, args: browserArgs });
1832
2012
  }
1833
- if (existsSync3(chromium.executablePath())) return chromium.launch({ headless: true, args: browserArgs });
2013
+ if (existsSync4(chromium.executablePath())) return chromium.launch({ headless: true, args: browserArgs });
1834
2014
  for (const channel of ["chrome", "msedge"]) {
1835
2015
  try {
1836
2016
  return await chromium.launch({ headless: true, channel, args: browserArgs });
1837
2017
  } catch {
1838
2018
  }
1839
2019
  }
1840
- throw new Error(`No Chromium-based browser was found. ${browserInstallHint}`);
2020
+ try {
2021
+ await ensureChromium();
2022
+ return await chromium.launch({ headless: true, args: browserArgs });
2023
+ } catch (error) {
2024
+ throw new Error(`No Chromium-based browser was found and auto-install failed: ${error.message}. ${browserInstallHint}`);
2025
+ }
1841
2026
  }
1842
2027
  async function describeBrowser() {
1843
2028
  const browser = await launchBrowser();
1844
2029
  try {
1845
- return `${browser.browserType().name()} ${browser.version()}${process.env.OVE_CHROMIUM_EXECUTABLE ? ` (OVE_CHROMIUM_EXECUTABLE=${process.env.OVE_CHROMIUM_EXECUTABLE})` : existsSync3(chromium.executablePath()) ? " (Playwright Chromium)" : " (installed Chrome/Edge)"}`;
2030
+ return `${browser.browserType().name()} ${browser.version()}${process.env.OVE_CHROMIUM_EXECUTABLE ? ` (OVE_CHROMIUM_EXECUTABLE=${process.env.OVE_CHROMIUM_EXECUTABLE})` : existsSync4(chromium.executablePath()) ? " (Playwright Chromium)" : " (installed Chrome/Edge)"}`;
1846
2031
  } finally {
1847
2032
  await browser.close();
1848
2033
  }
@@ -1966,7 +2151,7 @@ async function* renderFrames(renderer, from, to, options3 = {}) {
1966
2151
  await Promise.allSettled(pending);
1967
2152
  }
1968
2153
  }
1969
- var scaledSize, runtime, sheetPage, allLayers, browserArgs, browserInstallHint, defaultConcurrency;
2154
+ var scaledSize, runtime, sheetPage, allLayers, browserArgs, browserInstallHint, ensureChromiumPromise, defaultConcurrency;
1970
2155
  var init_src7 = __esm({
1971
2156
  "packages/renderer/src/index.ts"() {
1972
2157
  "use strict";
@@ -1987,6 +2172,7 @@ for(let i=0;i<images.length;i++){const img=new Image();img.src=images[i];await i
1987
2172
  allLayers = (composition) => composition.scenes.flatMap((scene) => scene.layers);
1988
2173
  browserArgs = ["--use-angle=swiftshader", "--enable-unsafe-swiftshader", ...(process.env.OVE_CHROMIUM_ARGS ?? "").split(/\s+/).filter(Boolean)];
1989
2174
  browserInstallHint = `Install one with "ove browser install" (Playwright's Chromium, about 150 MB), install Google Chrome, or set OVE_CHROMIUM_EXECUTABLE to a Chromium-based browser.`;
2175
+ ensureChromiumPromise = null;
1990
2176
  defaultConcurrency = () => Math.max(1, Math.min(4, Math.floor(availableParallelism() / 2)));
1991
2177
  }
1992
2178
  });
@@ -1995,13 +2181,13 @@ for(let i=0;i<images.length;i++){const img=new Image();img.src=images[i];await i
1995
2181
  import { stat as stat4 } from "node:fs/promises";
1996
2182
  import { spawn as spawn3 } from "node:child_process";
1997
2183
  import { mkdtemp, rm as rm3, mkdir as mkdir3, rename as rename2 } from "node:fs/promises";
1998
- import { existsSync as existsSync4 } from "node:fs";
2184
+ import { existsSync as existsSync5 } from "node:fs";
1999
2185
  import { createHash as createHash3 } from "node:crypto";
2000
2186
  import { tmpdir as tmpdir2 } from "node:os";
2001
- import { join as join7 } from "node:path";
2187
+ import { join as join8 } from "node:path";
2002
2188
  function sfxFile(name2) {
2003
- const file2 = join7(sfxDir(), `${name2}-${createHash3("sha256").update(JSON.stringify(sfxArgs(name2, ""))).digest("hex").slice(0, 12)}.wav`);
2004
- if (existsSync4(file2)) {
2189
+ const file2 = join8(sfxDir(), `${name2}-${createHash3("sha256").update(JSON.stringify(sfxArgs(name2, ""))).digest("hex").slice(0, 12)}.wav`);
2190
+ if (existsSync5(file2)) {
2005
2191
  void touchCacheEntry(file2);
2006
2192
  return Promise.resolve(file2);
2007
2193
  }
@@ -2049,7 +2235,7 @@ async function planAudio(composition, baseDir, probe2 = hasAudioStream) {
2049
2235
  }
2050
2236
  async function measureMix(mix) {
2051
2237
  if (!mix.clips.length) return void 0;
2052
- const dir = await mkdtemp(join7(tmpdir2(), "ove-loudness-")), wav = join7(dir, "mix.wav");
2238
+ const dir = await mkdtemp(join8(tmpdir2(), "ove-loudness-")), wav = join8(dir, "mix.wav");
2053
2239
  try {
2054
2240
  const args = audioMixArgs(mix, ["-c:a", "pcm_s16le"]), video = args.indexOf("0:v");
2055
2241
  if (video > 0) args.splice(video - 1, 2);
@@ -2274,7 +2460,7 @@ var init_review = __esm({
2274
2460
  // packages/core/src/distributed.ts
2275
2461
  import { mkdtemp as mkdtemp2, rm as rm4, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
2276
2462
  import { tmpdir as tmpdir3 } from "node:os";
2277
- import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join8, resolve as resolve2 } from "node:path";
2463
+ import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join9, resolve as resolve2 } from "node:path";
2278
2464
  import { timingSafeEqual } from "node:crypto";
2279
2465
  function planChunks(first, last, chunkSize) {
2280
2466
  if (!Number.isInteger(chunkSize) || chunkSize < 1) throw new Error("Chunk size must be a positive whole number of frames");
@@ -2298,7 +2484,7 @@ async function renderDistributed(composition, options3) {
2298
2484
  if (!Number.isInteger(first) || !Number.isInteger(last) || first < 0 || last < first || last >= composition.durationFrames) throw new Error(`Frame range must be within 0\u2013${composition.durationFrames - 1}`);
2299
2485
  const chunkSettings = { format, codec: settings.codec, transparent: settings.transparent, preset: options3.preset ?? defaultPreset, crf: settings.crf, bitrate: settings.bitrate, scale: settings.scale };
2300
2486
  const jobs = planChunks(first, last, options3.chunkSize ?? Math.max(30, Math.ceil((last - first + 1) / (options3.workers.length * 2)))), total = last - first + 1, retries = options3.retries ?? 2;
2301
- const work = await mkdtemp2(join8(tmpdir3(), "ove-chunks-")), segments = new Array(jobs.length), attempts = /* @__PURE__ */ new Map();
2487
+ const work = await mkdtemp2(join9(tmpdir3(), "ove-chunks-")), segments = new Array(jobs.length), attempts = /* @__PURE__ */ new Map();
2302
2488
  const abort = new AbortController();
2303
2489
  const onOuterAbort = () => abort.abort(options3.signal?.reason);
2304
2490
  options3.signal?.addEventListener("abort", onOuterAbort);
@@ -2312,7 +2498,7 @@ async function renderDistributed(composition, options3) {
2312
2498
  const job = queue.shift();
2313
2499
  try {
2314
2500
  const bytes = await worker.renderChunk(target, job, chunkSettings, abort.signal);
2315
- const file2 = join8(work, `chunk-${String(job.index).padStart(5, "0")}.${format}`);
2501
+ const file2 = join9(work, `chunk-${String(job.index).padStart(5, "0")}.${format}`);
2316
2502
  await writeFile4(file2, bytes);
2317
2503
  segments[job.index] = file2;
2318
2504
  failures = 0;
@@ -2345,7 +2531,7 @@ async function renderDistributed(composition, options3) {
2345
2531
  if (segments.some((s) => !s)) throw new Error("Some chunks were not rendered");
2346
2532
  await mkdir4(dirname2(output), { recursive: true });
2347
2533
  const audio = await masterAudio(target, baseDir).then((mix) => mix && { ...mix, durationSeconds: total / composition.fps, offsetSeconds: first / composition.fps });
2348
- await concatSegments(segments, join8(work, "segments.txt"), output, audio, { format, audioBitrate: settings.audioBitrate });
2534
+ await concatSegments(segments, join9(work, "segments.txt"), output, audio, { format, audioBitrate: settings.audioBitrate });
2349
2535
  return { output, format, frames: total, durationSeconds: Number(await probe(output)) || total / composition.fps };
2350
2536
  } finally {
2351
2537
  options3.signal?.removeEventListener("abort", onOuterAbort);
@@ -2377,13 +2563,14 @@ var init_distributed = __esm({
2377
2563
 
2378
2564
  // packages/core/src/index.ts
2379
2565
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
2380
- import { dirname as dirname3, extname as extname3, join as join9, resolve as resolve3 } from "node:path";
2566
+ import { dirname as dirname3, extname as extname3, join as join10, resolve as resolve3 } from "node:path";
2381
2567
  function checkRange(composition, range) {
2382
2568
  const [first, last] = range ?? [0, composition.durationFrames - 1];
2383
2569
  if (!Number.isInteger(first) || !Number.isInteger(last) || first < 0 || last < first || last >= composition.durationFrames) throw new Error(`Frame range must be whole frames within 0\u2013${composition.durationFrames - 1}, first \u2264 last; got ${first}\u2013${last}`);
2384
2570
  return [first, last];
2385
2571
  }
2386
2572
  async function renderMedia(composition, options3) {
2573
+ await ensureFfmpeg();
2387
2574
  const baseDir = options3.baseDir ?? process.cwd(), output = resolve3(options3.output);
2388
2575
  const format = options3.format ?? formatForOutput(output, composition.export.format);
2389
2576
  const settings = exportSchema.parse({ ...composition.export, ...Object.fromEntries(Object.entries({ crf: options3.crf, bitrate: options3.bitrate, audioBitrate: options3.audioBitrate, scale: options3.scale, loudness: options3.loudness }).filter(([, v]) => v !== void 0)), format, codec: options3.codec ?? (format === "mp4" ? composition.export.codec : "h264"), transparent: options3.transparent ?? composition.export.transparent });
@@ -2402,7 +2589,7 @@ async function renderMedia(composition, options3) {
2402
2589
  for await (const png of renderFrames(renderer, first, last + 1)) {
2403
2590
  throwIfAborted();
2404
2591
  if (encoder) await encoder.write(png);
2405
- else await writeFile5(join9(output, `frame-${String(frame).padStart(6, "0")}.png`), png);
2592
+ else await writeFile5(join10(output, `frame-${String(frame).padStart(6, "0")}.png`), png);
2406
2593
  frame++;
2407
2594
  options3.onProgress?.({ frame: frame - 1, rendered: frame - first, total });
2408
2595
  }
@@ -2457,11 +2644,11 @@ var init_src8 = __esm({
2457
2644
 
2458
2645
  // packages/cli/src/blocks.ts
2459
2646
  import { readdir as readdir3, readFile as readFile6 } from "node:fs/promises";
2460
- import { join as join10 } from "node:path";
2647
+ import { join as join11 } from "node:path";
2461
2648
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2462
2649
  async function listJson(dir, kind) {
2463
2650
  const names = (await readdir3(dir)).filter((name2) => name2.endsWith(".json")).sort();
2464
- const items = await Promise.all(names.map((name2) => readJson(join10(dir, name2))));
2651
+ const items = await Promise.all(names.map((name2) => readJson(join11(dir, name2))));
2465
2652
  for (const item of items) if (!idPattern.test(item.id)) throw new Error(`${kind} ids use lowercase letters, digits and dashes, not "${item.id}"`);
2466
2653
  return items;
2467
2654
  }
@@ -4012,7 +4199,7 @@ var init_compose = __esm({
4012
4199
 
4013
4200
  // packages/ai/src/history.ts
4014
4201
  import { copyFile, mkdir as mkdir6, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
4015
- import { basename, dirname as dirname4, join as join11, resolve as resolve4 } from "node:path";
4202
+ import { basename, dirname as dirname4, join as join12, resolve as resolve4 } from "node:path";
4016
4203
  async function listRevisions(compositionFile) {
4017
4204
  const text = await readText(indexFile(compositionFile));
4018
4205
  if (!text) return [];
@@ -4045,11 +4232,11 @@ async function saveRevision(compositionFile, meta) {
4045
4232
  const composition = JSON.parse(text.replace(/^/, ""));
4046
4233
  const list = await listRevisions(compositionFile);
4047
4234
  const last = list.at(-1);
4048
- const previous = last ? JSON.parse(await readText(join11(historyDir(compositionFile), last.file)) ?? "null") : void 0;
4235
+ const previous = last ? JSON.parse(await readText(join12(historyDir(compositionFile), last.file)) ?? "null") : void 0;
4049
4236
  const n = (last?.n ?? 0) + 1;
4050
4237
  const revision = { n, file: `${pad(n)}.json`, at: (/* @__PURE__ */ new Date()).toISOString(), ...meta, frames: Number(composition.durationFrames) || 0, scenes: scenesOf(composition).length, changes: summarizeChange(previous, composition) };
4051
4238
  await mkdir6(historyDir(compositionFile), { recursive: true });
4052
- await writeFile6(join11(historyDir(compositionFile), revision.file), text);
4239
+ await writeFile6(join12(historyDir(compositionFile), revision.file), text);
4053
4240
  await writeFile6(indexFile(compositionFile), JSON.stringify([...list, revision], null, 2) + "\n");
4054
4241
  return revision;
4055
4242
  }
@@ -4060,15 +4247,15 @@ async function ensureBaseline(compositionFile) {
4060
4247
  async function restoreRevision(compositionFile, n) {
4061
4248
  const found = (await listRevisions(compositionFile)).find((revision) => revision.n === n);
4062
4249
  if (!found) throw new Error(`There is no revision ${n}`);
4063
- await copyFile(join11(historyDir(compositionFile), found.file), resolve4(compositionFile));
4250
+ await copyFile(join12(historyDir(compositionFile), found.file), resolve4(compositionFile));
4064
4251
  return saveRevision(compositionFile, { kind: "restore", label: `Restored revision ${n}`, restoredFrom: n });
4065
4252
  }
4066
4253
  var historyDir, indexFile, pad, readText, scenesOf, defaults;
4067
4254
  var init_history = __esm({
4068
4255
  "packages/ai/src/history.ts"() {
4069
4256
  "use strict";
4070
- historyDir = (compositionFile) => join11(dirname4(resolve4(compositionFile)), "history", basename(compositionFile).replace(/\.json$/i, ""));
4071
- indexFile = (compositionFile) => join11(historyDir(compositionFile), "index.json");
4257
+ historyDir = (compositionFile) => join12(dirname4(resolve4(compositionFile)), "history", basename(compositionFile).replace(/\.json$/i, ""));
4258
+ indexFile = (compositionFile) => join12(historyDir(compositionFile), "index.json");
4072
4259
  pad = (n) => String(n).padStart(4, "0");
4073
4260
  readText = (file2) => readFile7(file2, "utf8").then((text) => text, () => void 0);
4074
4261
  scenesOf = (composition) => Array.isArray(composition?.scenes) ? composition.scenes : [];
@@ -4273,7 +4460,7 @@ var init_captions2 = __esm({
4273
4460
 
4274
4461
  // packages/ai/src/narrate.ts
4275
4462
  import { mkdir as mkdir8, readFile as readFile8, writeFile as writeFile8 } from "node:fs/promises";
4276
- import { basename as basename2, dirname as dirname6, join as join12, resolve as resolve6 } from "node:path";
4463
+ import { basename as basename2, dirname as dirname6, join as join13, resolve as resolve6 } from "node:path";
4277
4464
  function cleanScript(text) {
4278
4465
  return text.replace(/^\s*(narration|narrator|voiceover|vo)\s*:\s*/gim, "").replace(/^#{1,6}\s+/gm, "").replace(/\*\*|__|`/g, "").replace(/^\s*[-*]\s+/gm, "").replace(/[“”]/g, '"').replace(/[‘’]/g, "'").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
4279
4466
  }
@@ -4338,7 +4525,7 @@ async function narrate(provider, options3) {
4338
4525
  await mkdir8(options3.outDir, { recursive: true });
4339
4526
  options3.onProgress?.("Synthesising speech\u2026");
4340
4527
  const audio = await provider.speak(text, { voice: options3.voice, signal: options3.signal });
4341
- const audioFile = join12(options3.outDir, `narration.${audio.extension}`);
4528
+ const audioFile = join13(options3.outDir, `narration.${audio.extension}`);
4342
4529
  await writeFile8(audioFile, audio.data);
4343
4530
  options3.onProgress?.("Transcribing for word timings\u2026");
4344
4531
  let transcript = { text: "", words: [], cues: [] };
@@ -4358,7 +4545,7 @@ async function narrate(provider, options3) {
4358
4545
  let captionsFile;
4359
4546
  const hasCaptions = options3.captions !== false && (transcript.words.length > 0 || transcript.cues.length > 0);
4360
4547
  if (hasCaptions) {
4361
- captionsFile = join12(options3.outDir, "narration.captions.json");
4548
+ captionsFile = join13(options3.outDir, "narration.captions.json");
4362
4549
  await writeFile8(captionsFile, JSON.stringify(transcript.words.length ? { words: transcript.words } : { cues: transcript.cues }, null, 2) + "\n");
4363
4550
  }
4364
4551
  let composition;
@@ -4389,7 +4576,7 @@ async function captionAudio(provider, options3) {
4389
4576
  }
4390
4577
  const outDir = options3.outDir ?? dirname6(audioFile);
4391
4578
  await mkdir8(outDir, { recursive: true });
4392
- const captionsFile = join12(outDir, `${basename2(audioFile).replace(/\.[^.]+$/, "")}.captions.json`);
4579
+ const captionsFile = join13(outDir, `${basename2(audioFile).replace(/\.[^.]+$/, "")}.captions.json`);
4393
4580
  await writeFile8(captionsFile, JSON.stringify(transcript.words.length ? { words: transcript.words } : { cues: transcript.cues }, null, 2) + "\n");
4394
4581
  let composition;
4395
4582
  if (options3.composition) {
@@ -4418,7 +4605,7 @@ var init_narrate = __esm({
4418
4605
 
4419
4606
  // packages/ai/src/voiceover.ts
4420
4607
  import { mkdir as mkdir9, writeFile as writeFile9 } from "node:fs/promises";
4421
- import { dirname as dirname7, join as join13 } from "node:path";
4608
+ import { dirname as dirname7, join as join14 } from "node:path";
4422
4609
  function mergeNarration(source, resolved) {
4423
4610
  const live = resolved.scenes.filter((scene) => scene.id !== "narration" && scene.id !== "captions"), extra = resolved.scenes.filter((scene) => scene.id === "narration" || scene.id === "captions");
4424
4611
  if (!Array.isArray(source.scenes) || source.scenes.length !== live.length || source.scenes.some((scene, i) => scene?.id !== live[i].id)) return void 0;
@@ -4517,7 +4704,7 @@ async function narrateScenes(provider, composition, options3) {
4517
4704
  next.durationFrames = Math.max(...live.map((scene) => Number(scene.startFrame) + Number(scene.durationFrames)));
4518
4705
  const wav = concatWav(timeline);
4519
4706
  await mkdir9(options3.outDir, { recursive: true });
4520
- const audioFile = join13(options3.outDir, "narration.wav");
4707
+ const audioFile = join14(options3.outDir, "narration.wav");
4521
4708
  await writeFile9(audioFile, wav);
4522
4709
  let transcript = { text: "", words: [], cues: [] };
4523
4710
  if (provider.transcribe) {
@@ -4526,7 +4713,7 @@ async function narrateScenes(provider, composition, options3) {
4526
4713
  } else options3.onProgress?.("This provider cannot transcribe, so captions are skipped.");
4527
4714
  let captionsFile;
4528
4715
  if (transcript.words.length || transcript.cues.length) {
4529
- captionsFile = join13(options3.outDir, "narration.captions.json");
4716
+ captionsFile = join14(options3.outDir, "narration.captions.json");
4530
4717
  await writeFile9(captionsFile, JSON.stringify(transcript.words.length ? { words: transcript.words } : { cues: transcript.cues }, null, 2) + "\n");
4531
4718
  }
4532
4719
  const baseDir = options3.compositionPath ? dirname7(options3.compositionPath) : options3.outDir;
@@ -4553,7 +4740,7 @@ var init_voiceover = __esm({
4553
4740
 
4554
4741
  // packages/ai/src/edit.ts
4555
4742
  import { mkdir as mkdir10, readFile as readFile9, writeFile as writeFile10 } from "node:fs/promises";
4556
- import { basename as basename3, join as join14, resolve as resolve7 } from "node:path";
4743
+ import { basename as basename3, join as join15, resolve as resolve7 } from "node:path";
4557
4744
  function planCuts(transcript, options3 = {}) {
4558
4745
  const maxSilence = options3.maxSilence ?? 0.5, padding = options3.padding ?? 0.3;
4559
4746
  const words = transcript.words.filter((w) => Number.isFinite(w.start) && Number.isFinite(w.end) && w.end >= w.start).sort((a, b) => a.start - b.start);
@@ -4632,8 +4819,8 @@ async function editVideo(provider, options3) {
4632
4819
  options3.onProgress?.("Reading the clip\u2026");
4633
4820
  if (await hasAudioStream(clip2) === false) throw new Error(`${basename3(clip2)} has no audio track; there is nothing to transcribe`);
4634
4821
  const sourceSeconds = Number(await probe(clip2)) || 0;
4635
- await mkdir10(join14(outDir, "assets"), { recursive: true });
4636
- const audioFile = join14(outDir, "assets", "source-audio.m4a");
4822
+ await mkdir10(join15(outDir, "assets"), { recursive: true });
4823
+ const audioFile = join15(outDir, "assets", "source-audio.m4a");
4637
4824
  options3.onProgress?.("Extracting the audio\u2026");
4638
4825
  await extractAudio(clip2, audioFile);
4639
4826
  options3.onProgress?.("Transcribing\u2026");
@@ -4643,7 +4830,7 @@ async function editVideo(provider, options3) {
4643
4830
  const plan = planCuts(transcript, { maxSilence: options3.maxSilence, keepFillers: options3.keepFillers, padding: options3.padding });
4644
4831
  let captionsFile;
4645
4832
  if (options3.captions !== false) {
4646
- captionsFile = join14(outDir, "assets", "edit-captions.json");
4833
+ captionsFile = join15(outDir, "assets", "edit-captions.json");
4647
4834
  await writeFile10(captionsFile, JSON.stringify({ words: plan.words }, null, 2) + "\n");
4648
4835
  }
4649
4836
  const composition = editComposition({
@@ -4682,7 +4869,7 @@ var init_edit = __esm({
4682
4869
 
4683
4870
  // packages/ai/src/shorts.ts
4684
4871
  import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile11 } from "node:fs/promises";
4685
- import { basename as basename4, join as join15, resolve as resolve8 } from "node:path";
4872
+ import { basename as basename4, join as join16, resolve as resolve8 } from "node:path";
4686
4873
  function sentencesOf(words, maxSilence) {
4687
4874
  const sentences = [];
4688
4875
  let current = [];
@@ -4741,7 +4928,7 @@ async function shortsFrom(provider, options3) {
4741
4928
  if (await hasAudioStream(clip2) === false) throw new Error(`${basename4(clip2)} has no audio track; there is nothing to transcribe`);
4742
4929
  const sourceSeconds = Number(await probe(clip2)) || 0;
4743
4930
  await mkdir11(outDir, { recursive: true });
4744
- const audioFile = join15(outDir, "source-audio.m4a");
4931
+ const audioFile = join16(outDir, "source-audio.m4a");
4745
4932
  options3.onProgress?.("Extracting the audio\u2026");
4746
4933
  await extractAudio(clip2, audioFile);
4747
4934
  options3.onProgress?.("Transcribing\u2026");
@@ -4754,12 +4941,12 @@ async function shortsFrom(provider, options3) {
4754
4941
  for (const [i, candidate] of candidates.entries()) {
4755
4942
  const name2 = `short-${String(i + 1).padStart(2, "0")}`;
4756
4943
  options3.onProgress?.(`Writing ${name2}\u2026`);
4757
- const dir = join15(outDir, name2);
4944
+ const dir = join16(outDir, name2);
4758
4945
  const plan = shortsCandidatePlan(candidate, options3.cut !== false, options3.maxSilence ?? 0.5, options3.keepFillers ?? false);
4759
4946
  let captionsFile;
4760
4947
  if (options3.captions !== false) {
4761
- captionsFile = join15(dir, "assets", "captions.json");
4762
- await mkdir11(join15(dir, "assets"), { recursive: true });
4948
+ captionsFile = join16(dir, "assets", "captions.json");
4949
+ await mkdir11(join16(dir, "assets"), { recursive: true });
4763
4950
  await writeFile11(captionsFile, JSON.stringify({ words: plan.words }, null, 2) + "\n");
4764
4951
  }
4765
4952
  const composition = editComposition({
@@ -5161,7 +5348,7 @@ var init_brand_scraper = __esm({
5161
5348
 
5162
5349
  // packages/ai/src/agent.ts
5163
5350
  import { mkdir as mkdir13, writeFile as writeFile13 } from "node:fs/promises";
5164
- import { join as join16, resolve as resolve11 } from "node:path";
5351
+ import { join as join17, resolve as resolve11 } from "node:path";
5165
5352
  function detectReelFormat(brief) {
5166
5353
  return /\b(reel|short|9:16|viral|tiktok|instagram|shorts|reels|vertical)\b/i.test(brief);
5167
5354
  }
@@ -5244,7 +5431,7 @@ async function runVideoAgent(provider, options3, buildHelper) {
5244
5431
  const fps = options3.fps ?? 30;
5245
5432
  const durationSeconds = options3.durationSeconds ?? 6;
5246
5433
  const durationFrames = Math.max(30, Math.round(durationSeconds * fps));
5247
- const dir = resolve11(options3.dir ?? join16(process.cwd(), `agent-video-${Date.now()}`));
5434
+ const dir = resolve11(options3.dir ?? join17(process.cwd(), `agent-video-${Date.now()}`));
5248
5435
  options3.onProgress?.(`\u{1F3AC} Video Director Agent initializing in ${dir}\u2026`);
5249
5436
  await mkdir13(dir, { recursive: true });
5250
5437
  const messages2 = [
@@ -5261,9 +5448,9 @@ Make it visually stunning, cinematic, and punchy. Write the complete video.tsx f
5261
5448
  let attempts = 0;
5262
5449
  let currentTask = "compose";
5263
5450
  const maxAttempts = 3;
5264
- const videoPath = join16(dir, "video.tsx");
5265
- const compositionPath = join16(dir, "composition.json");
5266
- const bundlePath = join16(dir, "video.bundle.js");
5451
+ const videoPath = join17(dir, "video.tsx");
5452
+ const compositionPath = join17(dir, "composition.json");
5453
+ const bundlePath = join17(dir, "video.bundle.js");
5267
5454
  while (attempts < maxAttempts) {
5268
5455
  attempts++;
5269
5456
  options3.onProgress?.(`\u26A1 Generating React motion code with ${provider.id} (${currentTask}, attempt ${attempts}/${maxAttempts})\u2026`);
@@ -5840,7 +6027,7 @@ __export(templates_exports, {
5840
6027
  templatesDir: () => templatesDir
5841
6028
  });
5842
6029
  import { readdir as readdir5, readFile as readFile12 } from "node:fs/promises";
5843
- import { join as join17 } from "node:path";
6030
+ import { join as join18 } from "node:path";
5844
6031
  import { fileURLToPath as fileURLToPath4 } from "node:url";
5845
6032
  function assertTemplateId(id2) {
5846
6033
  if (!idPattern2.test(id2)) throw new Error(`Template ids use lowercase letters, digits and dashes, not "${id2}"`);
@@ -5882,8 +6069,8 @@ function infoFrom(id2, meta, composition) {
5882
6069
  }
5883
6070
  async function readTemplate(id2, root = templatesDir) {
5884
6071
  const safe = assertTemplateId(id2);
5885
- const dir = join17(root, safe);
5886
- const [meta, composition] = await Promise.all([readJson2(join17(dir, "template.json")), readJson2(join17(dir, "composition.json"))]);
6072
+ const dir = join18(root, safe);
6073
+ const [meta, composition] = await Promise.all([readJson2(join18(dir, "template.json")), readJson2(join18(dir, "composition.json"))]);
5887
6074
  return { dir, info: infoFrom(safe, meta, composition), composition };
5888
6075
  }
5889
6076
  async function listTemplates(root = templatesDir) {
@@ -6348,7 +6535,7 @@ var init_bundle = __esm({
6348
6535
  import { createServer } from "node:http";
6349
6536
  import { watch } from "node:fs";
6350
6537
  import { readFile as readFile14, writeFile as writeFile15 } from "node:fs/promises";
6351
- import { basename as basename6, dirname as dirname11, extname as extname4, isAbsolute as isAbsolute4, join as join19, relative as relative3, resolve as resolve13 } from "node:path";
6538
+ import { basename as basename6, dirname as dirname11, extname as extname4, isAbsolute as isAbsolute4, join as join20, relative as relative3, resolve as resolve13 } from "node:path";
6352
6539
  import { fileURLToPath as fileURLToPath7 } from "node:url";
6353
6540
  async function startDevServer(file2, options3 = {}) {
6354
6541
  const path = resolve13(file2), baseDir = dirname11(path), concurrency = options3.concurrency ?? defaultConcurrency();
@@ -6492,7 +6679,7 @@ data: ${JSON.stringify({ version, error: error ?? null })}
6492
6679
  if (req.method === "GET" && url.pathname.startsWith("/fonts/")) {
6493
6680
  const face = builtinFontFaces.find((f) => `/fonts/${f.file}` === url.pathname);
6494
6681
  if (!face) return send2(res, 404, "text/plain", "Not found");
6495
- return send2(res, 200, "font/woff2", await readFile14(join19(builtinFontDir(), face.file)));
6682
+ return send2(res, 200, "font/woff2", await readFile14(join20(builtinFontDir(), face.file)));
6496
6683
  }
6497
6684
  if (req.method === "GET" && url.pathname.startsWith("/media/")) {
6498
6685
  const target = resolve13(baseDir, decodeURIComponent(url.pathname.slice("/media/".length)));
@@ -6716,7 +6903,7 @@ var init_http = __esm({
6716
6903
 
6717
6904
  // packages/storage/src/index.ts
6718
6905
  import { cp as cp2, mkdir as mkdir15, readdir as readdir7, readFile as readFile16, rename as rename3, stat as stat6, writeFile as writeFile16 } from "node:fs/promises";
6719
- import { dirname as dirname12, extname as extname6, isAbsolute as isAbsolute5, join as join21, relative as relativePath, resolve as resolve14, sep } from "node:path";
6906
+ import { dirname as dirname12, extname as extname6, isAbsolute as isAbsolute5, join as join22, relative as relativePath, resolve as resolve14, sep } from "node:path";
6720
6907
  function within(root, target) {
6721
6908
  const rel = relativePath(resolve14(root), resolve14(target));
6722
6909
  return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute5(rel);
@@ -6740,7 +6927,7 @@ var init_src10 = __esm({
6740
6927
  tenantRoot(who) {
6741
6928
  if (who.tenantId === LOCAL_PRINCIPAL.tenantId) return this.root;
6742
6929
  if (!tenantPattern.test(who.tenantId)) throw new Error("That account name is not valid");
6743
- return join21(this.root, "_tenants", who.tenantId);
6930
+ return join22(this.root, "_tenants", who.tenantId);
6744
6931
  }
6745
6932
  /** A path inside the principal's space: relative values resolve from `base` (default: the space itself), absolute ones must already be inside. */
6746
6933
  resolve(who, value, label = "path", base) {
@@ -6757,7 +6944,7 @@ var init_src10 = __esm({
6757
6944
  return this.resolve(who, id2, "project");
6758
6945
  }
6759
6946
  file(who, id2) {
6760
- return join21(this.projectDir(who, id2), "composition.json");
6947
+ return join22(this.projectDir(who, id2), "composition.json");
6761
6948
  }
6762
6949
  async ids(who) {
6763
6950
  const entries2 = await readdir7(this.tenantRoot(who), { withFileTypes: true }).catch(() => []);
@@ -6772,7 +6959,7 @@ var init_src10 = __esm({
6772
6959
  const taken = new Set((await readdir7(space).catch(() => [])).map(String)), base = slugify(name2);
6773
6960
  let id2 = base, n = 1;
6774
6961
  while (taken.has(id2)) id2 = `${base}-${++n}`;
6775
- await mkdir15(join21(space, id2, "assets"), { recursive: true });
6962
+ await mkdir15(join22(space, id2, "assets"), { recursive: true });
6776
6963
  return id2;
6777
6964
  }
6778
6965
  async readRaw(who, id2) {
@@ -6794,37 +6981,37 @@ var init_src10 = __esm({
6794
6981
  return copy;
6795
6982
  }
6796
6983
  async trash(who, id2) {
6797
- const dir = this.projectDir(who, id2), trash = join21(this.tenantRoot(who), ".trash");
6984
+ const dir = this.projectDir(who, id2), trash = join22(this.tenantRoot(who), ".trash");
6798
6985
  await mkdir15(trash, { recursive: true });
6799
- await rename3(dir, join21(trash, `${id2}-${Date.now()}`));
6986
+ await rename3(dir, join22(trash, `${id2}-${Date.now()}`));
6800
6987
  }
6801
6988
  async assets(who, id2) {
6802
- const folder = join21(this.projectDir(who, id2), "assets"), out = [];
6989
+ const folder = join22(this.projectDir(who, id2), "assets"), out = [];
6803
6990
  for (const name2 of await readdir7(folder).catch(() => [])) {
6804
6991
  const kind = assetKinds[extname6(name2).toLowerCase()];
6805
6992
  if (!kind) continue;
6806
- out.push({ name: name2, path: `assets/${name2}`, kind, bytes: (await stat6(join21(folder, name2))).size });
6993
+ out.push({ name: name2, path: `assets/${name2}`, kind, bytes: (await stat6(join22(folder, name2))).size });
6807
6994
  }
6808
6995
  return out;
6809
6996
  }
6810
6997
  async putAsset(who, id2, name2, bytes) {
6811
6998
  const clean = name2.split(/[\\/]/).pop().replace(/[^A-Za-z0-9._-]+/g, "-");
6812
6999
  if (!assetKinds[extname6(clean).toLowerCase()]) throw new Error(`Pictures, video and audio only (${Object.keys(assetKinds).join(" ")})`);
6813
- const folder = join21(this.projectDir(who, id2), "assets");
7000
+ const folder = join22(this.projectDir(who, id2), "assets");
6814
7001
  await mkdir15(folder, { recursive: true });
6815
7002
  const taken = new Set(await readdir7(folder).catch(() => []));
6816
7003
  let final = clean;
6817
7004
  for (let n = 2; taken.has(final); n++) final = clean.replace(/(\.[^.]*)?$/, `-${n}$1`);
6818
- await writeFile16(join21(folder, final), bytes);
7005
+ await writeFile16(join22(folder, final), bytes);
6819
7006
  return this.assets(who, id2);
6820
7007
  }
6821
7008
  async thumb(who, id2) {
6822
- const cache = join21(this.projectDir(who, id2), ".studio", "thumb.png");
7009
+ const cache = join22(this.projectDir(who, id2), ".studio", "thumb.png");
6823
7010
  const [source, cached2] = await Promise.all([stat6(this.file(who, id2)), stat6(cache).catch(() => void 0)]);
6824
7011
  return cached2 && cached2.mtimeMs >= source.mtimeMs ? readFile16(cache) : void 0;
6825
7012
  }
6826
7013
  async putThumb(who, id2, png) {
6827
- const cache = join21(this.projectDir(who, id2), ".studio", "thumb.png");
7014
+ const cache = join22(this.projectDir(who, id2), ".studio", "thumb.png");
6828
7015
  await mkdir15(dirname12(cache), { recursive: true });
6829
7016
  await writeFile16(cache, png);
6830
7017
  }
@@ -6832,7 +7019,7 @@ var init_src10 = __esm({
6832
7019
  const walk = async (dir) => {
6833
7020
  let total = 0;
6834
7021
  for (const entry of await readdir7(dir, { withFileTypes: true }).catch(() => [])) {
6835
- const path = join21(dir, entry.name);
7022
+ const path = join22(dir, entry.name);
6836
7023
  total += entry.isDirectory() ? entry.name === "_tenants" ? 0 : await walk(path) : (await stat6(path).catch(() => ({ size: 0 }))).size;
6837
7024
  }
6838
7025
  return total;
@@ -7504,7 +7691,7 @@ var init_src11 = __esm({
7504
7691
  });
7505
7692
 
7506
7693
  // services/api/src/routes/jobs.ts
7507
- import { join as join22 } from "node:path";
7694
+ import { join as join23 } from "node:path";
7508
7695
  import { readFile as readFile20 } from "node:fs/promises";
7509
7696
  function jobsRoutes() {
7510
7697
  const start = async (c, kind) => {
@@ -7551,7 +7738,7 @@ ${user}` });
7551
7738
  { method: "GET", path: "/api/still", handle: async (c) => {
7552
7739
  if (c.deps.mode !== "local") return c.json(404, { error: "Not found" });
7553
7740
  const { store } = c.deps, id2 = (c.url.searchParams.get("file") ?? "").split("/")[0] ?? "";
7554
- const baseDir = (await store.workdir(c.who, id2)).dir, raw = JSON.parse((await readFile20(join22(baseDir, "composition.json"), "utf8")).replace(/^/, ""));
7741
+ const baseDir = (await store.workdir(c.who, id2)).dir, raw = JSON.parse((await readFile20(join23(baseDir, "composition.json"), "utf8")).replace(/^/, ""));
7555
7742
  const composition = await attachCaptions(loadComposition(raw), baseDir), requested = c.url.searchParams.get("frame");
7556
7743
  const frame = requested === null ? Math.floor(composition.durationFrames / 2) : integerIn(requested, 0, "frame", 0, Math.max(0, composition.durationFrames - 1));
7557
7744
  c.send(200, "image/png", await renderStill(composition, { frame, baseDir }));
@@ -9379,7 +9566,7 @@ __export(ai_studio_exports, {
9379
9566
  within: () => within
9380
9567
  });
9381
9568
  import { mkdir as mkdir18, readFile as readFile21 } from "node:fs/promises";
9382
- import { join as join23, resolve as resolve19 } from "node:path";
9569
+ import { join as join24, resolve as resolve19 } from "node:path";
9383
9570
  import { fileURLToPath as fileURLToPath8 } from "node:url";
9384
9571
  function jobEnvFor(store, requireProvider, providerInfo, local) {
9385
9572
  return (who) => ({
@@ -9469,7 +9656,7 @@ var init_ai_studio = __esm({
9469
9656
  },
9470
9657
  async font(path) {
9471
9658
  const face = builtinFontFaces.find((f) => `/fonts/${f.file}` === path);
9472
- return face ? readFile21(join23(builtinFontDir(), face.file)) : void 0;
9659
+ return face ? readFile21(join24(builtinFontDir(), face.file)) : void 0;
9473
9660
  }
9474
9661
  };
9475
9662
  studioApp = { dir: fileURLToPath8(new URL("../studio", import.meta.url)), fallbackPage: aiStudioPage };
@@ -9676,7 +9863,7 @@ __export(build_exports, {
9676
9863
  buildReact: () => buildReact,
9677
9864
  runBuild: () => runBuild
9678
9865
  });
9679
- import { existsSync as existsSync5 } from "node:fs";
9866
+ import { existsSync as existsSync6 } from "node:fs";
9680
9867
  import { mkdir as mkdir20, readFile as readFile22, writeFile as writeFile21 } from "node:fs/promises";
9681
9868
  import { basename as basename7, dirname as dirname17, resolve as resolve22 } from "node:path";
9682
9869
  import { fileURLToPath as fileURLToPath9, pathToFileURL } from "node:url";
@@ -9687,7 +9874,7 @@ async function buildReact(source, output) {
9687
9874
  } catch {
9688
9875
  throw new Error("Building a React video needs esbuild, react and react-dom in your project: npm install --save-dev esbuild react react-dom");
9689
9876
  }
9690
- const reactEntry = existsSync5(fileURLToPath9(new URL("./react.js", import.meta.url))) ? fileURLToPath9(new URL("./react.js", import.meta.url)) : fileURLToPath9(new URL("../../react/src/index.ts", import.meta.url));
9877
+ const reactEntry = existsSync6(fileURLToPath9(new URL("./react.js", import.meta.url))) ? fileURLToPath9(new URL("./react.js", import.meta.url)) : fileURLToPath9(new URL("../../react/src/index.ts", import.meta.url));
9691
9878
  const common = {
9692
9879
  bundle: true,
9693
9880
  jsx: "automatic",
@@ -10882,7 +11069,7 @@ import { resolve as resolve26, dirname as dirname20 } from "node:path";
10882
11069
  init_src();
10883
11070
  init_templates();
10884
11071
  import { cp, mkdir as mkdir14, readdir as readdir6, readFile as readFile13, rm as rm5, writeFile as writeFile14 } from "node:fs/promises";
10885
- import { basename as basename5, join as join18, relative as relative2, resolve as resolve12 } from "node:path";
11072
+ import { basename as basename5, join as join19, relative as relative2, resolve as resolve12 } from "node:path";
10886
11073
  import { fileURLToPath as fileURLToPath5 } from "node:url";
10887
11074
  var starterTemplate = fileURLToPath5(new URL("../templates/starter", import.meta.url));
10888
11075
  var skillDir = fileURLToPath5(new URL("../skills/ove", import.meta.url));
@@ -10896,24 +11083,24 @@ var isEmptyDir = async (dir) => {
10896
11083
  }
10897
11084
  };
10898
11085
  async function listFiles(dir) {
10899
- return (await readdir6(dir, { recursive: true, withFileTypes: true })).filter((e) => e.isFile()).map((e) => join18(e.parentPath, e.name));
11086
+ return (await readdir6(dir, { recursive: true, withFileTypes: true })).filter((e) => e.isFile()).map((e) => join19(e.parentPath, e.name));
10900
11087
  }
10901
11088
  async function initProject(dir, options3 = {}) {
10902
11089
  const target = resolve12(dir);
10903
11090
  if (!options3.force && !await isEmptyDir(target)) throw new Error(`${target} is not empty; pass --force to write into it anyway`);
10904
- const source = options3.template ? join18(templatesDir, assertTemplateId(options3.template)) : starterTemplate;
11091
+ const source = options3.template ? join19(templatesDir, assertTemplateId(options3.template)) : starterTemplate;
10905
11092
  await mkdir14(target, { recursive: true });
10906
11093
  await cp(source, target, { recursive: true, force: true });
10907
- await rm5(join18(target, "template.json"), { force: true });
10908
- const compositionPath = join18(target, "composition.json");
11094
+ await rm5(join19(target, "template.json"), { force: true });
11095
+ const compositionPath = join19(target, "composition.json");
10909
11096
  const name2 = basename5(target);
10910
11097
  const composition = JSON.parse((await readFile13(compositionPath, "utf8")).replace(/^\uFEFF/, ""));
10911
11098
  composition.id = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "video";
10912
11099
  composition.name = name2;
10913
11100
  loadComposition(composition);
10914
11101
  await writeFile14(compositionPath, JSON.stringify(composition, null, 2) + "\n");
10915
- await writeFile14(join18(target, "composition.schema.json"), JSON.stringify(compositionJsonSchema, null, 2) + "\n");
10916
- if (options3.skill !== false) await cp(skillsRoot, join18(target, ".claude", "skills"), { recursive: true, force: true });
11102
+ await writeFile14(join19(target, "composition.schema.json"), JSON.stringify(compositionJsonSchema, null, 2) + "\n");
11103
+ if (options3.skill !== false) await cp(skillsRoot, join19(target, ".claude", "skills"), { recursive: true, force: true });
10917
11104
  return (await listFiles(target)).map((file2) => relative2(target, file2)).sort();
10918
11105
  }
10919
11106
 
@@ -10993,7 +11180,7 @@ init_src8();
10993
11180
  import { createServer as createServer2 } from "node:http";
10994
11181
  import { mkdtemp as mkdtemp3, readFile as readFile15, rm as rm6 } from "node:fs/promises";
10995
11182
  import { tmpdir as tmpdir4 } from "node:os";
10996
- import { join as join20 } from "node:path";
11183
+ import { join as join21 } from "node:path";
10997
11184
  import { randomBytes } from "node:crypto";
10998
11185
  async function startWorker(options3 = {}) {
10999
11186
  const token = options3.token || randomBytes(18).toString("base64url"), host = options3.host ?? "127.0.0.1", log = options3.log ?? (() => {
@@ -11028,8 +11215,8 @@ async function startWorker(options3 = {}) {
11028
11215
  if (job.preset && !x264Presets.includes(job.preset)) return send2(res, 400, "text/plain", "Unknown preset");
11029
11216
  jobs++;
11030
11217
  const run4 = queue.then(async () => {
11031
- const dir = await mkdtemp3(join20(tmpdir4(), "ove-worker-"));
11032
- const output = join20(dir, `chunk.${format}`);
11218
+ const dir = await mkdtemp3(join21(tmpdir4(), "ove-worker-"));
11219
+ const output = join21(dir, `chunk.${format}`);
11033
11220
  const started = Date.now();
11034
11221
  try {
11035
11222
  await renderMedia(composition, { output, format, codec: job.codec, transparent: job.transparent, preset: job.preset, crf: job.crf, bitrate: job.bitrate, scale: job.scale, frameRange: range, concurrency, audio: false });
@@ -11063,8 +11250,7 @@ async function startWorker(options3 = {}) {
11063
11250
  }
11064
11251
 
11065
11252
  // packages/cli/src/index.ts
11066
- import { spawnSync } from "node:child_process";
11067
- import { createRequire as createRequire3 } from "node:module";
11253
+ import { spawnSync as spawnSync2 } from "node:child_process";
11068
11254
  try {
11069
11255
  process.loadEnvFile?.();
11070
11256
  } catch {
@@ -11264,7 +11450,7 @@ async function doctor() {
11264
11450
  } catch (error) {
11265
11451
  line(false, "FFmpeg", error.message);
11266
11452
  }
11267
- const probe2 = spawnSync(ffprobePath(), ["-version"], { encoding: "utf8" });
11453
+ const probe2 = spawnSync2(ffprobePath(), ["-version"], { encoding: "utf8" });
11268
11454
  line(!probe2.error && probe2.status === 0, "ffprobe", probe2.error ? "not found (install FFmpeg, or set OVE_FFPROBE_PATH)" : (probe2.stdout.split(/\r?\n/)[0] ?? "").trim());
11269
11455
  try {
11270
11456
  line(true, "Browser", await describeBrowser());
@@ -11309,10 +11495,8 @@ Press Ctrl+C to stop.`);
11309
11495
  process.once("SIGTERM", stop);
11310
11496
  } else if (command === "browser") {
11311
11497
  if (file !== "install") throw new Error("Usage: ove browser install");
11312
- const cli = createRequire3(import.meta.url).resolve("playwright/cli");
11313
11498
  console.log("Downloading Playwright Chromium\u2026");
11314
- const result = spawnSync(process.execPath, [cli, "install", "chromium"], { stdio: "inherit" });
11315
- if (result.status !== 0) throw new Error("Chromium download failed; see the output above.");
11499
+ await ensureChromium();
11316
11500
  } else if (command === "schema") {
11317
11501
  const json = JSON.stringify(compositionJsonSchema, null, 2) + "\n";
11318
11502
  const output = option4("--output");