motionrender 0.2.2 → 0.2.4

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 : [];
@@ -4243,6 +4430,13 @@ var init_template = __esm({
4243
4430
  };
4244
4431
  templatesFor = (templates, size) => templates.filter((template) => sameShape(template, size));
4245
4432
  systemText = `You fill in ready-made video templates. Reply with JSON only: {"template": "<id>", "theme": "<theme id, only for templates that take a theme>", "values": {"<variable>": <value>, ...}}.
4433
+ Choose the template that best matches the story structure and topic of the brief (stories and stats -> story-tall; features and cards -> showcase-tall; tips and steps -> social-reel).
4434
+ For templates that take a theme, ALWAYS choose the theme that best matches the mood, industry, and visual tone of the brief:
4435
+ - "midnight": modern tech, SaaS, B2B, cloud, finance
4436
+ - "neon": gaming, nightlife, social apps, crypto, cutting-edge AI
4437
+ - "espresso": artisan craft, food & beverage, coffee, lifestyle, podcasts, warmth
4438
+ - "paper": clean editorial, education, publishing, thought leadership, light minimalist
4439
+ - "signal": high-impact, fitness, sports, breaking news, urgency
4246
4440
  Picture variables (image, image2\u2026) take the id of one supplied picture, or "" for none; never invent ids. Examples only show the length and kind of text wanted: never reuse their wording (including labels like INTRODUCING); use the brief's own names, numbers and details, and prefer a concrete line over a generic one such as "Ready to start?". Rules: write the words viewers will read, never a description of the video; do not invent dates, prices or claims that are not in the brief; use only the listed variable names; write real copy for the brief, about as long as each example (never much longer); "color" values are hex like "#f4d58d"; list values are arrays of objects with the listed fields.
4247
4441
  If no template suits the brief, reply {"template": "none"}.`;
4248
4442
  }
@@ -4273,7 +4467,7 @@ var init_captions2 = __esm({
4273
4467
 
4274
4468
  // packages/ai/src/narrate.ts
4275
4469
  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";
4470
+ import { basename as basename2, dirname as dirname6, join as join13, resolve as resolve6 } from "node:path";
4277
4471
  function cleanScript(text) {
4278
4472
  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
4473
  }
@@ -4338,7 +4532,7 @@ async function narrate(provider, options3) {
4338
4532
  await mkdir8(options3.outDir, { recursive: true });
4339
4533
  options3.onProgress?.("Synthesising speech\u2026");
4340
4534
  const audio = await provider.speak(text, { voice: options3.voice, signal: options3.signal });
4341
- const audioFile = join12(options3.outDir, `narration.${audio.extension}`);
4535
+ const audioFile = join13(options3.outDir, `narration.${audio.extension}`);
4342
4536
  await writeFile8(audioFile, audio.data);
4343
4537
  options3.onProgress?.("Transcribing for word timings\u2026");
4344
4538
  let transcript = { text: "", words: [], cues: [] };
@@ -4358,7 +4552,7 @@ async function narrate(provider, options3) {
4358
4552
  let captionsFile;
4359
4553
  const hasCaptions = options3.captions !== false && (transcript.words.length > 0 || transcript.cues.length > 0);
4360
4554
  if (hasCaptions) {
4361
- captionsFile = join12(options3.outDir, "narration.captions.json");
4555
+ captionsFile = join13(options3.outDir, "narration.captions.json");
4362
4556
  await writeFile8(captionsFile, JSON.stringify(transcript.words.length ? { words: transcript.words } : { cues: transcript.cues }, null, 2) + "\n");
4363
4557
  }
4364
4558
  let composition;
@@ -4389,7 +4583,7 @@ async function captionAudio(provider, options3) {
4389
4583
  }
4390
4584
  const outDir = options3.outDir ?? dirname6(audioFile);
4391
4585
  await mkdir8(outDir, { recursive: true });
4392
- const captionsFile = join12(outDir, `${basename2(audioFile).replace(/\.[^.]+$/, "")}.captions.json`);
4586
+ const captionsFile = join13(outDir, `${basename2(audioFile).replace(/\.[^.]+$/, "")}.captions.json`);
4393
4587
  await writeFile8(captionsFile, JSON.stringify(transcript.words.length ? { words: transcript.words } : { cues: transcript.cues }, null, 2) + "\n");
4394
4588
  let composition;
4395
4589
  if (options3.composition) {
@@ -4418,7 +4612,7 @@ var init_narrate = __esm({
4418
4612
 
4419
4613
  // packages/ai/src/voiceover.ts
4420
4614
  import { mkdir as mkdir9, writeFile as writeFile9 } from "node:fs/promises";
4421
- import { dirname as dirname7, join as join13 } from "node:path";
4615
+ import { dirname as dirname7, join as join14 } from "node:path";
4422
4616
  function mergeNarration(source, resolved) {
4423
4617
  const live = resolved.scenes.filter((scene) => scene.id !== "narration" && scene.id !== "captions"), extra = resolved.scenes.filter((scene) => scene.id === "narration" || scene.id === "captions");
4424
4618
  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 +4711,7 @@ async function narrateScenes(provider, composition, options3) {
4517
4711
  next.durationFrames = Math.max(...live.map((scene) => Number(scene.startFrame) + Number(scene.durationFrames)));
4518
4712
  const wav = concatWav(timeline);
4519
4713
  await mkdir9(options3.outDir, { recursive: true });
4520
- const audioFile = join13(options3.outDir, "narration.wav");
4714
+ const audioFile = join14(options3.outDir, "narration.wav");
4521
4715
  await writeFile9(audioFile, wav);
4522
4716
  let transcript = { text: "", words: [], cues: [] };
4523
4717
  if (provider.transcribe) {
@@ -4526,7 +4720,7 @@ async function narrateScenes(provider, composition, options3) {
4526
4720
  } else options3.onProgress?.("This provider cannot transcribe, so captions are skipped.");
4527
4721
  let captionsFile;
4528
4722
  if (transcript.words.length || transcript.cues.length) {
4529
- captionsFile = join13(options3.outDir, "narration.captions.json");
4723
+ captionsFile = join14(options3.outDir, "narration.captions.json");
4530
4724
  await writeFile9(captionsFile, JSON.stringify(transcript.words.length ? { words: transcript.words } : { cues: transcript.cues }, null, 2) + "\n");
4531
4725
  }
4532
4726
  const baseDir = options3.compositionPath ? dirname7(options3.compositionPath) : options3.outDir;
@@ -4553,7 +4747,7 @@ var init_voiceover = __esm({
4553
4747
 
4554
4748
  // packages/ai/src/edit.ts
4555
4749
  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";
4750
+ import { basename as basename3, join as join15, resolve as resolve7 } from "node:path";
4557
4751
  function planCuts(transcript, options3 = {}) {
4558
4752
  const maxSilence = options3.maxSilence ?? 0.5, padding = options3.padding ?? 0.3;
4559
4753
  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 +4826,8 @@ async function editVideo(provider, options3) {
4632
4826
  options3.onProgress?.("Reading the clip\u2026");
4633
4827
  if (await hasAudioStream(clip2) === false) throw new Error(`${basename3(clip2)} has no audio track; there is nothing to transcribe`);
4634
4828
  const sourceSeconds = Number(await probe(clip2)) || 0;
4635
- await mkdir10(join14(outDir, "assets"), { recursive: true });
4636
- const audioFile = join14(outDir, "assets", "source-audio.m4a");
4829
+ await mkdir10(join15(outDir, "assets"), { recursive: true });
4830
+ const audioFile = join15(outDir, "assets", "source-audio.m4a");
4637
4831
  options3.onProgress?.("Extracting the audio\u2026");
4638
4832
  await extractAudio(clip2, audioFile);
4639
4833
  options3.onProgress?.("Transcribing\u2026");
@@ -4643,7 +4837,7 @@ async function editVideo(provider, options3) {
4643
4837
  const plan = planCuts(transcript, { maxSilence: options3.maxSilence, keepFillers: options3.keepFillers, padding: options3.padding });
4644
4838
  let captionsFile;
4645
4839
  if (options3.captions !== false) {
4646
- captionsFile = join14(outDir, "assets", "edit-captions.json");
4840
+ captionsFile = join15(outDir, "assets", "edit-captions.json");
4647
4841
  await writeFile10(captionsFile, JSON.stringify({ words: plan.words }, null, 2) + "\n");
4648
4842
  }
4649
4843
  const composition = editComposition({
@@ -4682,7 +4876,7 @@ var init_edit = __esm({
4682
4876
 
4683
4877
  // packages/ai/src/shorts.ts
4684
4878
  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";
4879
+ import { basename as basename4, join as join16, resolve as resolve8 } from "node:path";
4686
4880
  function sentencesOf(words, maxSilence) {
4687
4881
  const sentences = [];
4688
4882
  let current = [];
@@ -4741,7 +4935,7 @@ async function shortsFrom(provider, options3) {
4741
4935
  if (await hasAudioStream(clip2) === false) throw new Error(`${basename4(clip2)} has no audio track; there is nothing to transcribe`);
4742
4936
  const sourceSeconds = Number(await probe(clip2)) || 0;
4743
4937
  await mkdir11(outDir, { recursive: true });
4744
- const audioFile = join15(outDir, "source-audio.m4a");
4938
+ const audioFile = join16(outDir, "source-audio.m4a");
4745
4939
  options3.onProgress?.("Extracting the audio\u2026");
4746
4940
  await extractAudio(clip2, audioFile);
4747
4941
  options3.onProgress?.("Transcribing\u2026");
@@ -4754,12 +4948,12 @@ async function shortsFrom(provider, options3) {
4754
4948
  for (const [i, candidate] of candidates.entries()) {
4755
4949
  const name2 = `short-${String(i + 1).padStart(2, "0")}`;
4756
4950
  options3.onProgress?.(`Writing ${name2}\u2026`);
4757
- const dir = join15(outDir, name2);
4951
+ const dir = join16(outDir, name2);
4758
4952
  const plan = shortsCandidatePlan(candidate, options3.cut !== false, options3.maxSilence ?? 0.5, options3.keepFillers ?? false);
4759
4953
  let captionsFile;
4760
4954
  if (options3.captions !== false) {
4761
- captionsFile = join15(dir, "assets", "captions.json");
4762
- await mkdir11(join15(dir, "assets"), { recursive: true });
4955
+ captionsFile = join16(dir, "assets", "captions.json");
4956
+ await mkdir11(join16(dir, "assets"), { recursive: true });
4763
4957
  await writeFile11(captionsFile, JSON.stringify({ words: plan.words }, null, 2) + "\n");
4764
4958
  }
4765
4959
  const composition = editComposition({
@@ -5161,7 +5355,7 @@ var init_brand_scraper = __esm({
5161
5355
 
5162
5356
  // packages/ai/src/agent.ts
5163
5357
  import { mkdir as mkdir13, writeFile as writeFile13 } from "node:fs/promises";
5164
- import { join as join16, resolve as resolve11 } from "node:path";
5358
+ import { join as join17, resolve as resolve11 } from "node:path";
5165
5359
  function detectReelFormat(brief) {
5166
5360
  return /\b(reel|short|9:16|viral|tiktok|instagram|shorts|reels|vertical)\b/i.test(brief);
5167
5361
  }
@@ -5244,7 +5438,7 @@ async function runVideoAgent(provider, options3, buildHelper) {
5244
5438
  const fps = options3.fps ?? 30;
5245
5439
  const durationSeconds = options3.durationSeconds ?? 6;
5246
5440
  const durationFrames = Math.max(30, Math.round(durationSeconds * fps));
5247
- const dir = resolve11(options3.dir ?? join16(process.cwd(), `agent-video-${Date.now()}`));
5441
+ const dir = resolve11(options3.dir ?? join17(process.cwd(), `agent-video-${Date.now()}`));
5248
5442
  options3.onProgress?.(`\u{1F3AC} Video Director Agent initializing in ${dir}\u2026`);
5249
5443
  await mkdir13(dir, { recursive: true });
5250
5444
  const messages2 = [
@@ -5261,9 +5455,9 @@ Make it visually stunning, cinematic, and punchy. Write the complete video.tsx f
5261
5455
  let attempts = 0;
5262
5456
  let currentTask = "compose";
5263
5457
  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");
5458
+ const videoPath = join17(dir, "video.tsx");
5459
+ const compositionPath = join17(dir, "composition.json");
5460
+ const bundlePath = join17(dir, "video.bundle.js");
5267
5461
  while (attempts < maxAttempts) {
5268
5462
  attempts++;
5269
5463
  options3.onProgress?.(`\u26A1 Generating React motion code with ${provider.id} (${currentTask}, attempt ${attempts}/${maxAttempts})\u2026`);
@@ -5783,17 +5977,31 @@ var init_plan = __esm({
5783
5977
  init_registry();
5784
5978
  paths = ["template", "json", "react"];
5785
5979
  allowedPaths = (options3) => (options3.paths ?? paths).filter((path) => path !== "react" || options3.allowReact);
5786
- systemText2 = (options3) => `You are the production planner for Open Video Engine. Decide how the brief below should be produced, before anything is generated.
5980
+ systemText2 = (options3) => `You are the creative production planner for Open Video Engine. Decide how the brief below should be produced, before anything is generated.
5787
5981
  Reply with ONE JSON object and nothing else:
5788
5982
  {
5789
5983
  "path": "template" | "json" | "react",
5790
- "template": "<template id>", // only with "template": the one template that suits the brief
5984
+ "template": "<template id>", // only with "template": the one template that best suits the brief's structure and topic
5791
5985
  "theme": "<theme id>", // optional, only with a template that takes a theme
5792
5986
  "idea": "<what the video says, in one line>",
5793
5987
  "beats": [{ "name": "Hook", "onScreen": "what is on screen, concretely", "seconds": 3, "motion": "how it moves in" }],
5794
5988
  "reasons": ["why this path suits this brief"]
5795
5989
  }
5796
- Templates are the primary path: choose "template" whenever one listed template suits the brief's kind and aspect ratio, even when its length differs \u2014 a template's length is its own, and a length difference is not a reason to leave the template route. Choose "json" only when no template fits the kind and aspect: a bespoke composition with scene layers, camera moves and kinetic type. Choose "react" only when allowed: it writes React code for bespoke high-end motion graphics. Every beat needs a concrete picture, not a mood.
5990
+ Templates provide proven layouts, but pick the template that specifically fits the brief's narrative style:
5991
+ - Testimonials, customer stories, quotes, metrics, or personal founder stories: choose "story-tall" or "quote-card".
5992
+ - Multi-feature tours, app UI cards, feature lists, and product showcases: choose "showcase-tall", "product-wide" or "product-card".
5993
+ - Quick tips, 3-step processes, how-to guides, and numbered reels: choose "social-reel".
5994
+ - Company keynotes, presentations, and product launch films: choose "keynote-wide", "launch-wide" or "case-study".
5995
+ - Choose "json" when the brief requires a bespoke kinetic video with custom motion graphics, camera moves, and scene architecture.
5996
+ - Choose "react" only when allowed: it writes React code for bespoke high-end motion graphics.
5997
+
5998
+ Themes: For templates that take a theme, ALWAYS choose the theme matching the tone and brand of the brief:
5999
+ - "midnight": modern tech, SaaS, B2B, cloud, finance (deep slate navy, sky blue).
6000
+ - "neon": gaming, nightlife, social apps, crypto, cutting-edge AI (dark violet, hot magenta, electric lime).
6001
+ - "espresso": artisan craft, food & beverage, coffee, lifestyle, podcasts, warmth (dark espresso, amber gold).
6002
+ - "paper": clean editorial, education, publishing, thought leadership (clean light aesthetic, deep ink).
6003
+ - "signal": high-impact, fitness, sports, breaking news, urgency (bold monochrome with alert red).
6004
+ Every beat needs a concrete picture, not a mood.
5797
6005
  Reply with "path" set to one of: ${allowedPaths(options3).join(" | ")}.
5798
6006
  Available capabilities:
5799
6007
  ${describeCapabilities(options3.registry, options3)}`;
@@ -5840,7 +6048,7 @@ __export(templates_exports, {
5840
6048
  templatesDir: () => templatesDir
5841
6049
  });
5842
6050
  import { readdir as readdir5, readFile as readFile12 } from "node:fs/promises";
5843
- import { join as join17 } from "node:path";
6051
+ import { join as join18 } from "node:path";
5844
6052
  import { fileURLToPath as fileURLToPath4 } from "node:url";
5845
6053
  function assertTemplateId(id2) {
5846
6054
  if (!idPattern2.test(id2)) throw new Error(`Template ids use lowercase letters, digits and dashes, not "${id2}"`);
@@ -5882,8 +6090,8 @@ function infoFrom(id2, meta, composition) {
5882
6090
  }
5883
6091
  async function readTemplate(id2, root = templatesDir) {
5884
6092
  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"))]);
6093
+ const dir = join18(root, safe);
6094
+ const [meta, composition] = await Promise.all([readJson2(join18(dir, "template.json")), readJson2(join18(dir, "composition.json"))]);
5887
6095
  return { dir, info: infoFrom(safe, meta, composition), composition };
5888
6096
  }
5889
6097
  async function listTemplates(root = templatesDir) {
@@ -5928,7 +6136,7 @@ async function cliRegistry() {
5928
6136
  async function composeSmart(provider, options3, mode = {}) {
5929
6137
  const needsFull = mode.full || !!options3.fields?.length || (options3.assets ?? []).some((asset) => asset.type !== "image");
5930
6138
  let plan;
5931
- if (!mode.noPlan) {
6139
+ if (!mode.noPlan && !mode.full && !mode.template) {
5932
6140
  const registry = mode.registry ?? await cliRegistry();
5933
6141
  try {
5934
6142
  plan = await planBuild(provider, { ...options3, registry, maxAttempts: Math.min(options3.maxAttempts ?? 2, 2) });
@@ -5936,13 +6144,16 @@ async function composeSmart(provider, options3, mode = {}) {
5936
6144
  options3.onProgress?.(`Planning skipped (${error.message.split("\n")[0]}); using the built-in routing.`);
5937
6145
  }
5938
6146
  }
5939
- const templatePath = plan ? plan.path === "template" : !needsFull;
6147
+ const templatePath = (plan ? plan.path === "template" : !needsFull) || Boolean(mode.template);
5940
6148
  if (templatePath && !needsFull) {
5941
6149
  try {
5942
6150
  const choices = await templateChoices();
5943
- const picked = plan?.template ? choices.filter((choice) => choice.id === plan.template) : choices;
5944
- const themes = plan?.theme ? (await listThemes()).filter((theme) => theme.id === plan.theme) : await listThemes();
5945
- const filled2 = await composeFromTemplate(provider, picked.length ? picked : choices, { ...options3, themes });
6151
+ const targetTemplateId = mode.template ?? plan?.template;
6152
+ const picked = targetTemplateId ? choices.filter((choice) => choice.id === targetTemplateId) : choices;
6153
+ const targetThemeId = mode.theme ?? plan?.theme;
6154
+ const allThemes = await listThemes();
6155
+ const themes = targetThemeId ? allThemes.filter((theme) => theme.id === targetThemeId) : allThemes;
6156
+ const filled2 = await composeFromTemplate(provider, picked.length ? picked : choices, { ...options3, themes: themes.length ? themes : allThemes });
5946
6157
  if (filled2) {
5947
6158
  const wanted = options3.durationSeconds;
5948
6159
  const current = filled2.composition.durationFrames / filled2.composition.fps;
@@ -6348,7 +6559,7 @@ var init_bundle = __esm({
6348
6559
  import { createServer } from "node:http";
6349
6560
  import { watch } from "node:fs";
6350
6561
  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";
6562
+ 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
6563
  import { fileURLToPath as fileURLToPath7 } from "node:url";
6353
6564
  async function startDevServer(file2, options3 = {}) {
6354
6565
  const path = resolve13(file2), baseDir = dirname11(path), concurrency = options3.concurrency ?? defaultConcurrency();
@@ -6492,7 +6703,7 @@ data: ${JSON.stringify({ version, error: error ?? null })}
6492
6703
  if (req.method === "GET" && url.pathname.startsWith("/fonts/")) {
6493
6704
  const face = builtinFontFaces.find((f) => `/fonts/${f.file}` === url.pathname);
6494
6705
  if (!face) return send2(res, 404, "text/plain", "Not found");
6495
- return send2(res, 200, "font/woff2", await readFile14(join19(builtinFontDir(), face.file)));
6706
+ return send2(res, 200, "font/woff2", await readFile14(join20(builtinFontDir(), face.file)));
6496
6707
  }
6497
6708
  if (req.method === "GET" && url.pathname.startsWith("/media/")) {
6498
6709
  const target = resolve13(baseDir, decodeURIComponent(url.pathname.slice("/media/".length)));
@@ -6716,7 +6927,7 @@ var init_http = __esm({
6716
6927
 
6717
6928
  // packages/storage/src/index.ts
6718
6929
  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";
6930
+ import { dirname as dirname12, extname as extname6, isAbsolute as isAbsolute5, join as join22, relative as relativePath, resolve as resolve14, sep } from "node:path";
6720
6931
  function within(root, target) {
6721
6932
  const rel = relativePath(resolve14(root), resolve14(target));
6722
6933
  return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute5(rel);
@@ -6740,7 +6951,7 @@ var init_src10 = __esm({
6740
6951
  tenantRoot(who) {
6741
6952
  if (who.tenantId === LOCAL_PRINCIPAL.tenantId) return this.root;
6742
6953
  if (!tenantPattern.test(who.tenantId)) throw new Error("That account name is not valid");
6743
- return join21(this.root, "_tenants", who.tenantId);
6954
+ return join22(this.root, "_tenants", who.tenantId);
6744
6955
  }
6745
6956
  /** A path inside the principal's space: relative values resolve from `base` (default: the space itself), absolute ones must already be inside. */
6746
6957
  resolve(who, value, label = "path", base) {
@@ -6757,7 +6968,7 @@ var init_src10 = __esm({
6757
6968
  return this.resolve(who, id2, "project");
6758
6969
  }
6759
6970
  file(who, id2) {
6760
- return join21(this.projectDir(who, id2), "composition.json");
6971
+ return join22(this.projectDir(who, id2), "composition.json");
6761
6972
  }
6762
6973
  async ids(who) {
6763
6974
  const entries2 = await readdir7(this.tenantRoot(who), { withFileTypes: true }).catch(() => []);
@@ -6772,7 +6983,7 @@ var init_src10 = __esm({
6772
6983
  const taken = new Set((await readdir7(space).catch(() => [])).map(String)), base = slugify(name2);
6773
6984
  let id2 = base, n = 1;
6774
6985
  while (taken.has(id2)) id2 = `${base}-${++n}`;
6775
- await mkdir15(join21(space, id2, "assets"), { recursive: true });
6986
+ await mkdir15(join22(space, id2, "assets"), { recursive: true });
6776
6987
  return id2;
6777
6988
  }
6778
6989
  async readRaw(who, id2) {
@@ -6794,37 +7005,37 @@ var init_src10 = __esm({
6794
7005
  return copy;
6795
7006
  }
6796
7007
  async trash(who, id2) {
6797
- const dir = this.projectDir(who, id2), trash = join21(this.tenantRoot(who), ".trash");
7008
+ const dir = this.projectDir(who, id2), trash = join22(this.tenantRoot(who), ".trash");
6798
7009
  await mkdir15(trash, { recursive: true });
6799
- await rename3(dir, join21(trash, `${id2}-${Date.now()}`));
7010
+ await rename3(dir, join22(trash, `${id2}-${Date.now()}`));
6800
7011
  }
6801
7012
  async assets(who, id2) {
6802
- const folder = join21(this.projectDir(who, id2), "assets"), out = [];
7013
+ const folder = join22(this.projectDir(who, id2), "assets"), out = [];
6803
7014
  for (const name2 of await readdir7(folder).catch(() => [])) {
6804
7015
  const kind = assetKinds[extname6(name2).toLowerCase()];
6805
7016
  if (!kind) continue;
6806
- out.push({ name: name2, path: `assets/${name2}`, kind, bytes: (await stat6(join21(folder, name2))).size });
7017
+ out.push({ name: name2, path: `assets/${name2}`, kind, bytes: (await stat6(join22(folder, name2))).size });
6807
7018
  }
6808
7019
  return out;
6809
7020
  }
6810
7021
  async putAsset(who, id2, name2, bytes) {
6811
7022
  const clean = name2.split(/[\\/]/).pop().replace(/[^A-Za-z0-9._-]+/g, "-");
6812
7023
  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");
7024
+ const folder = join22(this.projectDir(who, id2), "assets");
6814
7025
  await mkdir15(folder, { recursive: true });
6815
7026
  const taken = new Set(await readdir7(folder).catch(() => []));
6816
7027
  let final = clean;
6817
7028
  for (let n = 2; taken.has(final); n++) final = clean.replace(/(\.[^.]*)?$/, `-${n}$1`);
6818
- await writeFile16(join21(folder, final), bytes);
7029
+ await writeFile16(join22(folder, final), bytes);
6819
7030
  return this.assets(who, id2);
6820
7031
  }
6821
7032
  async thumb(who, id2) {
6822
- const cache = join21(this.projectDir(who, id2), ".studio", "thumb.png");
7033
+ const cache = join22(this.projectDir(who, id2), ".studio", "thumb.png");
6823
7034
  const [source, cached2] = await Promise.all([stat6(this.file(who, id2)), stat6(cache).catch(() => void 0)]);
6824
7035
  return cached2 && cached2.mtimeMs >= source.mtimeMs ? readFile16(cache) : void 0;
6825
7036
  }
6826
7037
  async putThumb(who, id2, png) {
6827
- const cache = join21(this.projectDir(who, id2), ".studio", "thumb.png");
7038
+ const cache = join22(this.projectDir(who, id2), ".studio", "thumb.png");
6828
7039
  await mkdir15(dirname12(cache), { recursive: true });
6829
7040
  await writeFile16(cache, png);
6830
7041
  }
@@ -6832,7 +7043,7 @@ var init_src10 = __esm({
6832
7043
  const walk = async (dir) => {
6833
7044
  let total = 0;
6834
7045
  for (const entry of await readdir7(dir, { withFileTypes: true }).catch(() => [])) {
6835
- const path = join21(dir, entry.name);
7046
+ const path = join22(dir, entry.name);
6836
7047
  total += entry.isDirectory() ? entry.name === "_tenants" ? 0 : await walk(path) : (await stat6(path).catch(() => ({ size: 0 }))).size;
6837
7048
  }
6838
7049
  return total;
@@ -7001,7 +7212,9 @@ function createHandlers(env) {
7001
7212
  maxAttempts: integerIn(input.maxAttempts, 3, "maxAttempts", 1, 10),
7002
7213
  onProgress: (message2) => progress(message2)
7003
7214
  };
7004
- const result = await capabilities.composeSmart(ai, options3, { full });
7215
+ const theme = typeof input.theme === "string" && input.theme.trim() ? input.theme.trim() : void 0;
7216
+ const template = typeof input.template === "string" && input.template.trim() ? input.template.trim() : void 0;
7217
+ const result = await capabilities.composeSmart(ai, options3, { full, theme, template });
7005
7218
  let composition = result.composition;
7006
7219
  let narration;
7007
7220
  if (options3.narration) {
@@ -7504,7 +7717,7 @@ var init_src11 = __esm({
7504
7717
  });
7505
7718
 
7506
7719
  // services/api/src/routes/jobs.ts
7507
- import { join as join22 } from "node:path";
7720
+ import { join as join23 } from "node:path";
7508
7721
  import { readFile as readFile20 } from "node:fs/promises";
7509
7722
  function jobsRoutes() {
7510
7723
  const start = async (c, kind) => {
@@ -7551,7 +7764,7 @@ ${user}` });
7551
7764
  { method: "GET", path: "/api/still", handle: async (c) => {
7552
7765
  if (c.deps.mode !== "local") return c.json(404, { error: "Not found" });
7553
7766
  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(/^/, ""));
7767
+ const baseDir = (await store.workdir(c.who, id2)).dir, raw = JSON.parse((await readFile20(join23(baseDir, "composition.json"), "utf8")).replace(/^/, ""));
7555
7768
  const composition = await attachCaptions(loadComposition(raw), baseDir), requested = c.url.searchParams.get("frame");
7556
7769
  const frame = requested === null ? Math.floor(composition.durationFrames / 2) : integerIn(requested, 0, "frame", 0, Math.max(0, composition.durationFrames - 1));
7557
7770
  c.send(200, "image/png", await renderStill(composition, { frame, baseDir }));
@@ -9379,7 +9592,7 @@ __export(ai_studio_exports, {
9379
9592
  within: () => within
9380
9593
  });
9381
9594
  import { mkdir as mkdir18, readFile as readFile21 } from "node:fs/promises";
9382
- import { join as join23, resolve as resolve19 } from "node:path";
9595
+ import { join as join24, resolve as resolve19 } from "node:path";
9383
9596
  import { fileURLToPath as fileURLToPath8 } from "node:url";
9384
9597
  function jobEnvFor(store, requireProvider, providerInfo, local) {
9385
9598
  return (who) => ({
@@ -9469,7 +9682,7 @@ var init_ai_studio = __esm({
9469
9682
  },
9470
9683
  async font(path) {
9471
9684
  const face = builtinFontFaces.find((f) => `/fonts/${f.file}` === path);
9472
- return face ? readFile21(join23(builtinFontDir(), face.file)) : void 0;
9685
+ return face ? readFile21(join24(builtinFontDir(), face.file)) : void 0;
9473
9686
  }
9474
9687
  };
9475
9688
  studioApp = { dir: fileURLToPath8(new URL("../studio", import.meta.url)), fallbackPage: aiStudioPage };
@@ -9676,7 +9889,7 @@ __export(build_exports, {
9676
9889
  buildReact: () => buildReact,
9677
9890
  runBuild: () => runBuild
9678
9891
  });
9679
- import { existsSync as existsSync5 } from "node:fs";
9892
+ import { existsSync as existsSync6 } from "node:fs";
9680
9893
  import { mkdir as mkdir20, readFile as readFile22, writeFile as writeFile21 } from "node:fs/promises";
9681
9894
  import { basename as basename7, dirname as dirname17, resolve as resolve22 } from "node:path";
9682
9895
  import { fileURLToPath as fileURLToPath9, pathToFileURL } from "node:url";
@@ -9687,7 +9900,7 @@ async function buildReact(source, output) {
9687
9900
  } catch {
9688
9901
  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
9902
  }
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));
9903
+ 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
9904
  const common = {
9692
9905
  bundle: true,
9693
9906
  jsx: "automatic",
@@ -10882,7 +11095,7 @@ import { resolve as resolve26, dirname as dirname20 } from "node:path";
10882
11095
  init_src();
10883
11096
  init_templates();
10884
11097
  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";
11098
+ import { basename as basename5, join as join19, relative as relative2, resolve as resolve12 } from "node:path";
10886
11099
  import { fileURLToPath as fileURLToPath5 } from "node:url";
10887
11100
  var starterTemplate = fileURLToPath5(new URL("../templates/starter", import.meta.url));
10888
11101
  var skillDir = fileURLToPath5(new URL("../skills/ove", import.meta.url));
@@ -10896,24 +11109,24 @@ var isEmptyDir = async (dir) => {
10896
11109
  }
10897
11110
  };
10898
11111
  async function listFiles(dir) {
10899
- return (await readdir6(dir, { recursive: true, withFileTypes: true })).filter((e) => e.isFile()).map((e) => join18(e.parentPath, e.name));
11112
+ return (await readdir6(dir, { recursive: true, withFileTypes: true })).filter((e) => e.isFile()).map((e) => join19(e.parentPath, e.name));
10900
11113
  }
10901
11114
  async function initProject(dir, options3 = {}) {
10902
11115
  const target = resolve12(dir);
10903
11116
  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;
11117
+ const source = options3.template ? join19(templatesDir, assertTemplateId(options3.template)) : starterTemplate;
10905
11118
  await mkdir14(target, { recursive: true });
10906
11119
  await cp(source, target, { recursive: true, force: true });
10907
- await rm5(join18(target, "template.json"), { force: true });
10908
- const compositionPath = join18(target, "composition.json");
11120
+ await rm5(join19(target, "template.json"), { force: true });
11121
+ const compositionPath = join19(target, "composition.json");
10909
11122
  const name2 = basename5(target);
10910
11123
  const composition = JSON.parse((await readFile13(compositionPath, "utf8")).replace(/^\uFEFF/, ""));
10911
11124
  composition.id = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "video";
10912
11125
  composition.name = name2;
10913
11126
  loadComposition(composition);
10914
11127
  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 });
11128
+ await writeFile14(join19(target, "composition.schema.json"), JSON.stringify(compositionJsonSchema, null, 2) + "\n");
11129
+ if (options3.skill !== false) await cp(skillsRoot, join19(target, ".claude", "skills"), { recursive: true, force: true });
10917
11130
  return (await listFiles(target)).map((file2) => relative2(target, file2)).sort();
10918
11131
  }
10919
11132
 
@@ -10993,7 +11206,7 @@ init_src8();
10993
11206
  import { createServer as createServer2 } from "node:http";
10994
11207
  import { mkdtemp as mkdtemp3, readFile as readFile15, rm as rm6 } from "node:fs/promises";
10995
11208
  import { tmpdir as tmpdir4 } from "node:os";
10996
- import { join as join20 } from "node:path";
11209
+ import { join as join21 } from "node:path";
10997
11210
  import { randomBytes } from "node:crypto";
10998
11211
  async function startWorker(options3 = {}) {
10999
11212
  const token = options3.token || randomBytes(18).toString("base64url"), host = options3.host ?? "127.0.0.1", log = options3.log ?? (() => {
@@ -11028,8 +11241,8 @@ async function startWorker(options3 = {}) {
11028
11241
  if (job.preset && !x264Presets.includes(job.preset)) return send2(res, 400, "text/plain", "Unknown preset");
11029
11242
  jobs++;
11030
11243
  const run4 = queue.then(async () => {
11031
- const dir = await mkdtemp3(join20(tmpdir4(), "ove-worker-"));
11032
- const output = join20(dir, `chunk.${format}`);
11244
+ const dir = await mkdtemp3(join21(tmpdir4(), "ove-worker-"));
11245
+ const output = join21(dir, `chunk.${format}`);
11033
11246
  const started = Date.now();
11034
11247
  try {
11035
11248
  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 +11276,7 @@ async function startWorker(options3 = {}) {
11063
11276
  }
11064
11277
 
11065
11278
  // packages/cli/src/index.ts
11066
- import { spawnSync } from "node:child_process";
11067
- import { createRequire as createRequire3 } from "node:module";
11279
+ import { spawnSync as spawnSync2 } from "node:child_process";
11068
11280
  try {
11069
11281
  process.loadEnvFile?.();
11070
11282
  } catch {
@@ -11264,7 +11476,7 @@ async function doctor() {
11264
11476
  } catch (error) {
11265
11477
  line(false, "FFmpeg", error.message);
11266
11478
  }
11267
- const probe2 = spawnSync(ffprobePath(), ["-version"], { encoding: "utf8" });
11479
+ const probe2 = spawnSync2(ffprobePath(), ["-version"], { encoding: "utf8" });
11268
11480
  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
11481
  try {
11270
11482
  line(true, "Browser", await describeBrowser());
@@ -11309,10 +11521,8 @@ Press Ctrl+C to stop.`);
11309
11521
  process.once("SIGTERM", stop);
11310
11522
  } else if (command === "browser") {
11311
11523
  if (file !== "install") throw new Error("Usage: ove browser install");
11312
- const cli = createRequire3(import.meta.url).resolve("playwright/cli");
11313
11524
  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.");
11525
+ await ensureChromium();
11316
11526
  } else if (command === "schema") {
11317
11527
  const json = JSON.stringify(compositionJsonSchema, null, 2) + "\n";
11318
11528
  const output = option4("--output");