motionrender 0.2.1 → 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.
Files changed (69) hide show
  1. package/dist/cli.js +2872 -582
  2. package/dist/index.js +270 -43
  3. package/dist/player.js +13 -13
  4. package/dist/react.js +704 -9
  5. package/dist/types/packages/ai/src/agent.d.ts +57 -0
  6. package/dist/types/packages/ai/src/brand-scraper.d.ts +35 -0
  7. package/dist/types/packages/ai/src/captions.d.ts +11 -0
  8. package/dist/types/packages/ai/src/compose.d.ts +2 -0
  9. package/dist/types/packages/ai/src/edit.d.ts +69 -0
  10. package/dist/types/packages/ai/src/index.d.ts +8 -1
  11. package/dist/types/packages/ai/src/plan.d.ts +42 -0
  12. package/dist/types/packages/ai/src/prompt.d.ts +18 -0
  13. package/dist/types/packages/ai/src/providers.d.ts +35 -2
  14. package/dist/types/packages/ai/src/registry.d.ts +81 -0
  15. package/dist/types/packages/ai/src/retime.d.ts +7 -0
  16. package/dist/types/packages/ai/src/shorts.d.ts +75 -0
  17. package/dist/types/packages/cli/src/agent-cli.d.ts +15 -0
  18. package/dist/types/packages/cli/src/ai-studio-page.d.ts +1 -1
  19. package/dist/types/packages/cli/src/ai.d.ts +1 -1
  20. package/dist/types/packages/cli/src/build.d.ts +2 -0
  21. package/dist/types/packages/cli/src/templates.d.ts +13 -4
  22. package/dist/types/packages/contracts/src/index.d.ts +12 -12
  23. package/dist/types/packages/core/src/index.d.ts +2 -2
  24. package/dist/types/packages/core/src/review.d.ts +24 -0
  25. package/dist/types/packages/ffmpeg/src/auto-install.d.ts +12 -0
  26. package/dist/types/packages/ffmpeg/src/index.d.ts +2 -0
  27. package/dist/types/packages/jobs/src/queue.d.ts +2 -2
  28. package/dist/types/packages/jobs/src/redis.d.ts +2 -2
  29. package/dist/types/packages/react/src/components.d.ts +166 -0
  30. package/dist/types/packages/react/src/index.d.ts +54 -2
  31. package/dist/types/packages/react/src/springs.d.ts +25 -0
  32. package/dist/types/packages/renderer/src/index.d.ts +3 -0
  33. package/dist/types/packages/schema/src/captions.d.ts +2 -0
  34. package/dist/types/packages/schema/src/index.d.ts +66 -66
  35. package/dist/types/scripts/eval-briefs.d.ts +67 -0
  36. package/package.json +1 -1
  37. package/skills/ove/SKILL.md +40 -2
  38. package/skills/ove-agent/SKILL.md +144 -0
  39. package/skills/ove-code-video/SKILL.md +17 -9
  40. package/skills/ove-motion-graphics/SKILL.md +113 -0
  41. package/skills/ove-react-video/SKILL.md +212 -0
  42. package/skills/ove-remotion/SKILL.md +122 -0
  43. package/studio/assets/index-DmZj9wTZ.css +1 -0
  44. package/studio/assets/index-n3DJI_GV.js +10 -0
  45. package/studio/index.html +2 -2
  46. package/templates/case-study/composition.json +25 -10
  47. package/templates/event-invite/composition.json +12 -2
  48. package/templates/explainer-square/composition.json +1 -1
  49. package/templates/keynote-wide/composition.json +2 -2
  50. package/templates/kinetic-reel/composition.json +73 -0
  51. package/templates/kinetic-reel/composition.schema.json +9295 -0
  52. package/templates/kinetic-reel/template.json +17 -0
  53. package/templates/kinetic-reel/video.bundle.js +72 -0
  54. package/templates/kinetic-reel/video.tsx +283 -0
  55. package/templates/product-card/composition.json +7 -2
  56. package/templates/product-wide/composition.json +1 -1
  57. package/templates/quote-card/composition.json +6 -1
  58. package/templates/saas-launch/composition.json +70 -0
  59. package/templates/saas-launch/composition.schema.json +9295 -0
  60. package/templates/saas-launch/template.json +16 -0
  61. package/templates/saas-launch/video.bundle.js +72 -0
  62. package/templates/saas-launch/video.tsx +217 -0
  63. package/templates/sale-promo/composition.json +16 -1
  64. package/templates/showcase-tall/composition.json +4 -4
  65. package/templates/slideshow/composition.json +1 -1
  66. package/templates/social-reel/composition.json +19 -4
  67. package/templates/story-tall/composition.json +1 -1
  68. package/studio/assets/index-CzOvWv9w.css +0 -1
  69. package/studio/assets/index-J_fvKgng.js +0 -11
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // packages/core/src/index.ts
2
2
  import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
3
- import { dirname as dirname3, extname as extname3, join as join9, resolve as resolve3 } from "node:path";
3
+ import { dirname as dirname3, extname as extname3, join as join10, resolve as resolve3 } from "node:path";
4
4
 
5
5
  // packages/schema/src/index.ts
6
6
  import { z as z3 } from "zod";
@@ -483,7 +483,35 @@ var captionLines = (cues, maxWords) => cues.flatMap((cue) => cue.words && cue.wo
483
483
 
484
484
  // packages/schema/src/index.ts
485
485
  var color = z3.string().regex(colorPattern);
486
- var fontWeight = z3.union([z3.number().int().min(1).max(1e3), z3.enum(["normal", "bold"])]);
486
+ var fontWeightMap = {
487
+ thin: 100,
488
+ hairline: 100,
489
+ extralight: 200,
490
+ ultralight: 200,
491
+ "extra-light": 200,
492
+ "ultra-light": 200,
493
+ light: 300,
494
+ regular: 400,
495
+ medium: 500,
496
+ semibold: 600,
497
+ demibold: 600,
498
+ "semi-bold": 600,
499
+ "demi-bold": 600,
500
+ extrabold: 800,
501
+ ultrabold: 800,
502
+ "extra-bold": 800,
503
+ "ultra-bold": 800,
504
+ black: 900,
505
+ heavy: 900
506
+ };
507
+ var fontWeight = z3.preprocess((val) => {
508
+ if (typeof val === "string") {
509
+ const s = val.trim().toLowerCase();
510
+ if (/^\d+$/.test(s)) return parseInt(s, 10);
511
+ if (s in fontWeightMap) return fontWeightMap[s];
512
+ }
513
+ return val;
514
+ }, z3.union([z3.number().int().min(1).max(1e3), z3.enum(["normal", "bold"])]));
487
515
  var fontStyle = z3.enum(["normal", "italic"]);
488
516
  var easingSchema = z3.union([z3.enum(["linear", "easeIn", "easeOut", "easeInOut", "easeInCubic", "easeOutCubic", "easeInOutCubic", "easeInQuart", "easeOutQuart", "easeInOutQuart", "easeInExpo", "easeOutExpo", "easeInOutExpo", "easeInBack", "easeOutBack", "spring", "hold", "land", "snap", "glide", "pop", "move", "exit", "drift"]).describe("land, snap, glide and pop for entries, move within the frame, exit for leaving, drift for slow continuous motion; or a standard curve"), z3.tuple([z3.number().min(0).max(1), z3.number(), z3.number().min(0).max(1), z3.number()]).describe("Cubic B\xE9zier control points [x1, y1, x2, y2], as in CSS")]);
489
517
  var colorProperties = ["fill", "color", "stroke"];
@@ -837,7 +865,7 @@ var describeError = (error) => error instanceof z3.ZodError ? z3.prettifyError(e
837
865
  // packages/core/src/media.ts
838
866
  import { spawn as spawn2 } from "node:child_process";
839
867
  import { mkdir, readFile as readFile2, stat as stat2, writeFile } from "node:fs/promises";
840
- import { join as join3 } from "node:path";
868
+ import { join as join4 } from "node:path";
841
869
  import { createHash } from "node:crypto";
842
870
 
843
871
  // packages/animation/src/index.ts
@@ -1194,11 +1222,143 @@ async function toDataUrl(source) {
1194
1222
  // packages/ffmpeg/src/index.ts
1195
1223
  import { spawn } from "node:child_process";
1196
1224
  import { once } from "node:events";
1225
+ import { join as join3 } from "node:path";
1226
+
1227
+ // packages/ffmpeg/src/auto-install.ts
1228
+ import { homedir } from "node:os";
1197
1229
  import { join as join2 } from "node:path";
1230
+ import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, renameSync } from "node:fs";
1231
+ import { spawnSync } from "node:child_process";
1232
+ import { pipeline } from "node:stream/promises";
1233
+ import { Readable } from "node:stream";
1234
+ import { createGunzip } from "node:zlib";
1235
+ var isWin = process.platform === "win32";
1236
+ var binDir = join2(homedir(), ".motionrender", "bin");
1237
+ var localFfmpegPath = join2(binDir, isWin ? "ffmpeg.exe" : "ffmpeg");
1238
+ var localFfprobePath = join2(binDir, isWin ? "ffprobe.exe" : "ffprobe");
1239
+ var checkedSystemFfmpeg = false;
1240
+ var systemFfmpegAvailable = false;
1241
+ var checkedSystemFfprobe = false;
1242
+ var systemFfprobeAvailable = false;
1243
+ function hasSystemFfmpeg() {
1244
+ if (checkedSystemFfmpeg) return systemFfmpegAvailable;
1245
+ try {
1246
+ const res = spawnSync("ffmpeg", ["-version"], { stdio: "ignore" });
1247
+ systemFfmpegAvailable = res.status === 0;
1248
+ } catch {
1249
+ systemFfmpegAvailable = false;
1250
+ }
1251
+ checkedSystemFfmpeg = true;
1252
+ return systemFfmpegAvailable;
1253
+ }
1254
+ function hasSystemFfprobe() {
1255
+ if (checkedSystemFfprobe) return systemFfprobeAvailable;
1256
+ try {
1257
+ const res = spawnSync("ffprobe", ["-version"], { stdio: "ignore" });
1258
+ systemFfprobeAvailable = res.status === 0;
1259
+ } catch {
1260
+ systemFfprobeAvailable = false;
1261
+ }
1262
+ checkedSystemFfprobe = true;
1263
+ return systemFfprobeAvailable;
1264
+ }
1265
+ var getFfmpegPath = () => {
1266
+ if (process.env.OVE_FFMPEG_PATH) return process.env.OVE_FFMPEG_PATH;
1267
+ if (existsSync(localFfmpegPath)) return localFfmpegPath;
1268
+ if (hasSystemFfmpeg()) return "ffmpeg";
1269
+ return localFfmpegPath;
1270
+ };
1271
+ var getFfprobePath = () => {
1272
+ if (process.env.OVE_FFPROBE_PATH) return process.env.OVE_FFPROBE_PATH;
1273
+ if (existsSync(localFfprobePath)) return localFfprobePath;
1274
+ if (hasSystemFfprobe()) return "ffprobe";
1275
+ return localFfprobePath;
1276
+ };
1277
+ var RELEASE_TAG = "b6.1.1";
1278
+ var BASE_URL = `https://github.com/eugeneware/ffmpeg-static/releases/download/${RELEASE_TAG}`;
1279
+ function getPlatformArch() {
1280
+ const p = process.platform;
1281
+ const a = process.arch;
1282
+ if (p === "win32") return { platform: "win32", arch: "x64" };
1283
+ if (p === "darwin") return { platform: "darwin", arch: a === "arm64" ? "arm64" : "x64" };
1284
+ if (p === "linux") return { platform: "linux", arch: a === "arm64" ? "arm64" : a === "arm" ? "arm" : "x64" };
1285
+ return { platform: p, arch: a };
1286
+ }
1287
+ async function downloadAndExtract(name2, destPath, onProgress) {
1288
+ const { platform, arch } = getPlatformArch();
1289
+ const url = `${BASE_URL}/${name2}-${platform}-${arch}.gz`;
1290
+ if (onProgress) onProgress(`Downloading ${name2} (${platform}-${arch})...`);
1291
+ const res = await fetch(url, { redirect: "follow" });
1292
+ if (!res.ok || !res.body) {
1293
+ throw new Error(`Failed to download ${name2} from ${url} (HTTP ${res.status})`);
1294
+ }
1295
+ const tmpPath = `${destPath}.tmp-${Date.now()}`;
1296
+ try {
1297
+ const fileStream = createWriteStream(tmpPath);
1298
+ const nodeReadable = Readable.fromWeb(res.body);
1299
+ await pipeline(nodeReadable, createGunzip(), fileStream);
1300
+ if (!isWin) {
1301
+ chmodSync(tmpPath, 493);
1302
+ }
1303
+ if (existsSync(destPath)) {
1304
+ try {
1305
+ unlinkSync(destPath);
1306
+ } catch {
1307
+ }
1308
+ }
1309
+ renameSync(tmpPath, destPath);
1310
+ } catch (err) {
1311
+ try {
1312
+ if (existsSync(tmpPath)) unlinkSync(tmpPath);
1313
+ } catch {
1314
+ }
1315
+ throw err;
1316
+ }
1317
+ }
1318
+ var ensureFfmpegPromise = null;
1319
+ async function ensureFfmpeg(options) {
1320
+ if (ensureFfmpegPromise) return ensureFfmpegPromise;
1321
+ ensureFfmpegPromise = (async () => {
1322
+ const ffmpegFromEnv = process.env.OVE_FFMPEG_PATH;
1323
+ const ffprobeFromEnv = process.env.OVE_FFPROBE_PATH;
1324
+ const hasLocalFfmpeg = existsSync(localFfmpegPath);
1325
+ const hasLocalFfprobe = existsSync(localFfprobePath);
1326
+ const hasSysFfmpeg = !ffmpegFromEnv && !hasLocalFfmpeg ? hasSystemFfmpeg() : false;
1327
+ const hasSysFfprobe = !ffprobeFromEnv && !hasLocalFfprobe ? hasSystemFfprobe() : false;
1328
+ const ffmpegReady = Boolean(ffmpegFromEnv || hasLocalFfmpeg || hasSysFfmpeg);
1329
+ const ffprobeReady = Boolean(ffprobeFromEnv || hasLocalFfprobe || hasSysFfprobe);
1330
+ if (ffmpegReady && ffprobeReady) {
1331
+ return { ffmpeg: getFfmpegPath(), ffprobe: getFfprobePath() };
1332
+ }
1333
+ if (!existsSync(binDir)) {
1334
+ mkdirSync(binDir, { recursive: true });
1335
+ }
1336
+ const log = options?.onProgress ?? ((msg) => console.log(msg));
1337
+ if (!ffmpegReady) {
1338
+ log("FFmpeg not found. Automatically downloading static build (one-time setup)...");
1339
+ await downloadAndExtract("ffmpeg", localFfmpegPath, log);
1340
+ log("\u2713 FFmpeg installed successfully.");
1341
+ }
1342
+ if (!ffprobeReady) {
1343
+ log("ffprobe not found. Automatically downloading static build (one-time setup)...");
1344
+ await downloadAndExtract("ffprobe", localFfprobePath, log);
1345
+ log("\u2713 ffprobe installed successfully.");
1346
+ }
1347
+ return { ffmpeg: getFfmpegPath(), ffprobe: getFfprobePath() };
1348
+ })();
1349
+ try {
1350
+ return await ensureFfmpegPromise;
1351
+ } catch (err) {
1352
+ ensureFfmpegPromise = null;
1353
+ throw err;
1354
+ }
1355
+ }
1356
+
1357
+ // packages/ffmpeg/src/index.ts
1198
1358
  var x264Presets = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"];
1199
1359
  var defaultPreset = "veryfast";
1200
- var ffmpegPath = () => process.env.OVE_FFMPEG_PATH || "ffmpeg";
1201
- var ffprobePath = () => process.env.OVE_FFPROBE_PATH || "ffprobe";
1360
+ var ffmpegPath = () => getFfmpegPath();
1361
+ var ffprobePath = () => getFfprobePath();
1202
1362
  var 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.`);
1203
1363
  var num2 = (n) => {
1204
1364
  const text = n.toFixed(5).replace(/\.?0+$/, "");
@@ -1308,12 +1468,14 @@ function concatSegments(segments, listFile, output, audio, options = {}) {
1308
1468
  const list = segments.map((s) => `file '${s.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`).join("\n");
1309
1469
  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", ...options.format === "mp4" || options.format === void 0 ? ["-movflags", "+faststart"] : [], output], "ignore").done);
1310
1470
  }
1311
- function extractFrames(source, outDir, options) {
1471
+ async function extractFrames(source, outDir, options) {
1472
+ await ensureFfmpeg();
1312
1473
  const { fps, maxWidth, maxHeight, durationSeconds, startSeconds } = options;
1313
- 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;
1474
+ 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;
1314
1475
  }
1315
1476
  var requiredEncoders = { mp4: ["libx264", "aac"], "mp4 (h265)": ["libx265"], webm: ["libvpx-vp9", "libopus"], mov: ["prores_ks", "pcm_s16le"], gif: ["gif"], "video layers": ["mjpeg"] };
1316
- function checkFfmpeg() {
1477
+ async function checkFfmpeg() {
1478
+ await ensureFfmpeg();
1317
1479
  return new Promise((resolve4, reject) => {
1318
1480
  const child = spawn(ffmpegPath(), ["-hide_banner", "-encoders"]);
1319
1481
  let out = "";
@@ -1330,7 +1492,8 @@ function checkFfmpeg() {
1330
1492
  });
1331
1493
  });
1332
1494
  }
1333
- function hasAudioStream(path) {
1495
+ async function hasAudioStream(path) {
1496
+ await ensureFfmpeg();
1334
1497
  return new Promise((resolve4, reject) => {
1335
1498
  const child = spawn(ffprobePath(), ["-v", "error", "-select_streams", "a", "-show_entries", "stream=index", "-of", "csv=p=0", path]);
1336
1499
  let out = "";
@@ -1339,7 +1502,8 @@ function hasAudioStream(path) {
1339
1502
  child.on("close", (c) => c === 0 ? resolve4(out.trim().length > 0) : reject(new Error(`ffprobe could not read ${path}`)));
1340
1503
  });
1341
1504
  }
1342
- function probe(path) {
1505
+ async function probe(path) {
1506
+ await ensureFfmpeg();
1343
1507
  return new Promise((resolve4, reject) => {
1344
1508
  const child = spawn(ffprobePath(), ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path]);
1345
1509
  let out = "";
@@ -1363,7 +1527,7 @@ async function measureSource(source) {
1363
1527
  const version = isUrl(source) ? "" : await stat2(source).then((s) => `${s.size}:${s.mtimeMs}`, () => {
1364
1528
  throw new Error(`Media not found: ${source}`);
1365
1529
  });
1366
- const key = createHash("sha256").update(`${source}|${version}|v1`).digest("hex").slice(0, 32), file = join3(mediaCache(), `${key}.json`);
1530
+ const key = createHash("sha256").update(`${source}|${version}|v1`).digest("hex").slice(0, 32), file = join4(mediaCache(), `${key}.json`);
1367
1531
  const cached2 = await readFile2(file, "utf8").then(JSON.parse, () => void 0);
1368
1532
  if (cached2) {
1369
1533
  void touchCacheEntry(file);
@@ -1438,7 +1602,7 @@ async function prepareComposition(raw, values = {}, baseDir = process.cwd()) {
1438
1602
 
1439
1603
  // packages/renderer/src/index.ts
1440
1604
  import { chromium } from "playwright";
1441
- import { existsSync as existsSync3 } from "node:fs";
1605
+ import { existsSync as existsSync4 } from "node:fs";
1442
1606
  import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
1443
1607
  import { availableParallelism } from "node:os";
1444
1608
 
@@ -1530,18 +1694,18 @@ var prepareOperations = (operations, fps = 30) => {
1530
1694
 
1531
1695
  // packages/renderer/src/video.ts
1532
1696
  import { mkdir as mkdir2, readdir as readdir2, rename, rm as rm2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
1533
- import { join as join4 } from "node:path";
1697
+ import { join as join5 } from "node:path";
1534
1698
  import { createHash as createHash2, randomUUID } from "node:crypto";
1535
- var videoFrameFile = (video, index) => join4(video.dir, `${String(Math.max(0, index - video.first) + 1).padStart(6, "0")}.jpg`);
1699
+ var videoFrameFile = (video, index) => join5(video.dir, `${String(Math.max(0, index - video.first) + 1).padStart(6, "0")}.jpg`);
1536
1700
  var videoCacheRoot = () => cacheDir("video");
1537
1701
  var complete = ".complete";
1538
1702
  async function decode(source, key, first = 0) {
1539
1703
  const version = isUrl(source) ? "" : await stat3(source).then((s) => `${s.size}:${s.mtimeMs}`, () => {
1540
1704
  throw new Error(`Video not found: ${source}`);
1541
1705
  });
1542
- const dir = join4(videoCacheRoot(), createHash2("sha256").update(JSON.stringify({ source, version, ...key })).digest("hex").slice(0, 32));
1706
+ const dir = join5(videoCacheRoot(), createHash2("sha256").update(JSON.stringify({ source, version, ...key })).digest("hex").slice(0, 32));
1543
1707
  const count = async () => (await readdir2(dir)).filter((f) => f.endsWith(".jpg")).length;
1544
- if (await stat3(join4(dir, complete)).then(() => true, () => false)) {
1708
+ if (await stat3(join5(dir, complete)).then(() => true, () => false)) {
1545
1709
  void touchCacheEntry(dir);
1546
1710
  return { dir, count: first + await count(), first };
1547
1711
  }
@@ -1549,9 +1713,9 @@ async function decode(source, key, first = 0) {
1549
1713
  await mkdir2(scratch, { recursive: true });
1550
1714
  try {
1551
1715
  await extractFrames(source, scratch, key);
1552
- await writeFile2(join4(scratch, complete), "");
1716
+ await writeFile2(join5(scratch, complete), "");
1553
1717
  await rename(scratch, dir).catch(async (error) => {
1554
- if (!await stat3(join4(dir, complete)).then(() => true, () => false)) throw error;
1718
+ if (!await stat3(join5(dir, complete)).then(() => true, () => false)) throw error;
1555
1719
  });
1556
1720
  } finally {
1557
1721
  await rm2(scratch, { recursive: true, force: true });
@@ -1642,34 +1806,34 @@ async function createPagePool(open, discard, size, timeoutMs = defaultFrameTimeo
1642
1806
  }
1643
1807
 
1644
1808
  // packages/graphics/src/fonts.ts
1645
- import { existsSync } from "node:fs";
1809
+ import { existsSync as existsSync2 } from "node:fs";
1646
1810
  import { readFile as readFile3 } from "node:fs/promises";
1647
1811
  import { createRequire } from "node:module";
1648
- import { dirname, join as join5 } from "node:path";
1812
+ import { dirname, join as join6 } from "node:path";
1649
1813
  import { fileURLToPath } from "node:url";
1650
1814
  function builtinFontDir() {
1651
1815
  const bundled = fileURLToPath(new URL("./fonts/", import.meta.url));
1652
- if (existsSync(join5(bundled, builtinFontFaces[0].file))) return bundled;
1653
- return join5(dirname(createRequire(import.meta.url).resolve("@fontsource-variable/inter/package.json")), "files");
1816
+ if (existsSync2(join6(bundled, builtinFontFaces[0].file))) return bundled;
1817
+ return join6(dirname(createRequire(import.meta.url).resolve("@fontsource-variable/inter/package.json")), "files");
1654
1818
  }
1655
1819
  var cached;
1656
- var 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")}` })));
1820
+ var 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")}` })));
1657
1821
 
1658
1822
  // packages/graphics/src/runtime.ts
1659
- import { existsSync as existsSync2, readFileSync } from "node:fs";
1823
+ import { existsSync as existsSync3, readFileSync } from "node:fs";
1660
1824
  import { createRequire as createRequire2 } from "node:module";
1661
- import { join as join6 } from "node:path";
1825
+ import { join as join7 } from "node:path";
1662
1826
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1663
1827
  var runtimeDir = () => {
1664
1828
  const bundled = fileURLToPath2(new URL("./runtime/", import.meta.url));
1665
- return existsSync2(join6(bundled, "canvas-runtime.js")) ? bundled : fileURLToPath2(new URL("../runtime/", import.meta.url));
1829
+ return existsSync3(join7(bundled, "canvas-runtime.js")) ? bundled : fileURLToPath2(new URL("../runtime/", import.meta.url));
1666
1830
  };
1667
- var read = (name2) => readFileSync(join6(runtimeDir(), name2), "utf8");
1831
+ var read = (name2) => readFileSync(join7(runtimeDir(), name2), "utf8");
1668
1832
  var textLayoutSource = read("text-layout.js");
1669
1833
  var lottieFile = "lottie_light_canvas.min.js";
1670
1834
  var lottiePath = () => {
1671
- const bundled = join6(runtimeDir(), lottieFile);
1672
- return existsSync2(bundled) ? bundled : createRequire2(import.meta.url).resolve(`lottie-web/build/player/${lottieFile}`);
1835
+ const bundled = join7(runtimeDir(), lottieFile);
1836
+ return existsSync3(bundled) ? bundled : createRequire2(import.meta.url).resolve(`lottie-web/build/player/${lottieFile}`);
1673
1837
  };
1674
1838
  var lottieSource = readFileSync(lottiePath(), "utf8");
1675
1839
  var canvasRuntimeSource = `${textLayoutSource}
@@ -1688,25 +1852,68 @@ for(let i=0;i<images.length;i++){const img=new Image();img.src=images[i];await i
1688
1852
  var allLayers = (composition) => composition.scenes.flatMap((scene) => scene.layers);
1689
1853
  var browserArgs = ["--use-angle=swiftshader", "--enable-unsafe-swiftshader", ...(process.env.OVE_CHROMIUM_ARGS ?? "").split(/\s+/).filter(Boolean)];
1690
1854
  var 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.`;
1855
+ var ensureChromiumPromise = null;
1856
+ async function ensureChromium(options) {
1857
+ if (existsSync4(chromium.executablePath())) return;
1858
+ for (const channel of ["chrome", "msedge"]) {
1859
+ try {
1860
+ const b = await chromium.launch({ headless: true, channel, args: browserArgs });
1861
+ await b.close();
1862
+ return;
1863
+ } catch {
1864
+ }
1865
+ }
1866
+ if (ensureChromiumPromise) return ensureChromiumPromise;
1867
+ ensureChromiumPromise = (async () => {
1868
+ const log = options?.onProgress ?? ((msg) => console.log(msg));
1869
+ log("No Chrome, Edge, or Chromium browser detected.");
1870
+ log("Downloading Playwright Chromium (one-time setup)...");
1871
+ const { createRequire: createRequire3 } = await import("node:module");
1872
+ const { dirname: dirname4, join: join11 } = await import("node:path");
1873
+ const { spawn: spawn4 } = await import("node:child_process");
1874
+ const req = createRequire3(import.meta.url);
1875
+ const cli = join11(dirname4(req.resolve("playwright/package.json")), "cli.js");
1876
+ await new Promise((resolve4, reject) => {
1877
+ const child = spawn4(process.execPath, [cli, "install", "chromium"], { stdio: "inherit" });
1878
+ child.on("close", (code) => {
1879
+ if (code === 0) resolve4();
1880
+ else reject(new Error(`Chromium installation failed (exit code ${code})`));
1881
+ });
1882
+ child.on("error", reject);
1883
+ });
1884
+ log("\u2713 Playwright Chromium installed successfully.");
1885
+ })();
1886
+ try {
1887
+ await ensureChromiumPromise;
1888
+ } catch (err) {
1889
+ ensureChromiumPromise = null;
1890
+ throw err;
1891
+ }
1892
+ }
1691
1893
  async function launchBrowser() {
1692
1894
  const explicit = process.env.OVE_CHROMIUM_EXECUTABLE;
1693
1895
  if (explicit) {
1694
- if (!existsSync3(explicit)) throw new Error(`OVE_CHROMIUM_EXECUTABLE points to ${explicit}, which does not exist.`);
1896
+ if (!existsSync4(explicit)) throw new Error(`OVE_CHROMIUM_EXECUTABLE points to ${explicit}, which does not exist.`);
1695
1897
  return chromium.launch({ headless: true, executablePath: explicit, args: browserArgs });
1696
1898
  }
1697
- if (existsSync3(chromium.executablePath())) return chromium.launch({ headless: true, args: browserArgs });
1899
+ if (existsSync4(chromium.executablePath())) return chromium.launch({ headless: true, args: browserArgs });
1698
1900
  for (const channel of ["chrome", "msedge"]) {
1699
1901
  try {
1700
1902
  return await chromium.launch({ headless: true, channel, args: browserArgs });
1701
1903
  } catch {
1702
1904
  }
1703
1905
  }
1704
- throw new Error(`No Chromium-based browser was found. ${browserInstallHint}`);
1906
+ try {
1907
+ await ensureChromium();
1908
+ return await chromium.launch({ headless: true, args: browserArgs });
1909
+ } catch (error) {
1910
+ throw new Error(`No Chromium-based browser was found and auto-install failed: ${error.message}. ${browserInstallHint}`);
1911
+ }
1705
1912
  }
1706
1913
  async function describeBrowser() {
1707
1914
  const browser = await launchBrowser();
1708
1915
  try {
1709
- 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)"}`;
1916
+ 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)"}`;
1710
1917
  } finally {
1711
1918
  await browser.close();
1712
1919
  }
@@ -1836,10 +2043,10 @@ async function* renderFrames(renderer, from, to, options = {}) {
1836
2043
  import { stat as stat4 } from "node:fs/promises";
1837
2044
  import { spawn as spawn3 } from "node:child_process";
1838
2045
  import { mkdtemp, rm as rm3, mkdir as mkdir3, rename as rename2 } from "node:fs/promises";
1839
- import { existsSync as existsSync4 } from "node:fs";
2046
+ import { existsSync as existsSync5 } from "node:fs";
1840
2047
  import { createHash as createHash3 } from "node:crypto";
1841
2048
  import { tmpdir as tmpdir2 } from "node:os";
1842
- import { join as join7 } from "node:path";
2049
+ import { join as join8 } from "node:path";
1843
2050
  var runFfmpeg = (args) => new Promise((resolve4, reject) => {
1844
2051
  const child = spawn3(ffmpegPath(), args, { stdio: ["ignore", "ignore", "pipe"] });
1845
2052
  let err = "";
@@ -1850,8 +2057,8 @@ var runFfmpeg = (args) => new Promise((resolve4, reject) => {
1850
2057
  var sfxDir = () => cacheDir("sfx");
1851
2058
  var making = /* @__PURE__ */ new Map();
1852
2059
  function sfxFile(name2) {
1853
- const file = join7(sfxDir(), `${name2}-${createHash3("sha256").update(JSON.stringify(sfxArgs(name2, ""))).digest("hex").slice(0, 12)}.wav`);
1854
- if (existsSync4(file)) {
2060
+ const file = join8(sfxDir(), `${name2}-${createHash3("sha256").update(JSON.stringify(sfxArgs(name2, ""))).digest("hex").slice(0, 12)}.wav`);
2061
+ if (existsSync5(file)) {
1855
2062
  void touchCacheEntry(file);
1856
2063
  return Promise.resolve(file);
1857
2064
  }
@@ -1905,7 +2112,7 @@ async function planAudio(composition, baseDir, probe2 = hasAudioStream) {
1905
2112
  }
1906
2113
  async function measureMix(mix) {
1907
2114
  if (!mix.clips.length) return void 0;
1908
- const dir = await mkdtemp(join7(tmpdir2(), "ove-loudness-")), wav = join7(dir, "mix.wav");
2115
+ const dir = await mkdtemp(join8(tmpdir2(), "ove-loudness-")), wav = join8(dir, "mix.wav");
1909
2116
  try {
1910
2117
  const args = audioMixArgs(mix, ["-c:a", "pcm_s16le"]), video = args.indexOf("0:v");
1911
2118
  if (video > 0) args.splice(video - 1, 2);
@@ -1932,6 +2139,16 @@ async function measureLoudness(composition, baseDir) {
1932
2139
  var area = (a) => Math.max(0, a.x1 - a.x0) * Math.max(0, a.y1 - a.y0);
1933
2140
  var overlap = (a, b) => Math.max(0, Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0)) * Math.max(0, Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0));
1934
2141
  var seconds2 = (frame, fps) => `${(frame / fps).toFixed(1)} s`;
2142
+ function safeZoneBands(width, height, override) {
2143
+ const screen = width * 16 / 9, letterbox = height >= width ? Math.max(0, (screen - height) / 2) : 0;
2144
+ const inset = { ...height >= width ? { top: Math.max(0, 0.08 * screen - letterbox) / height, bottom: Math.max(0, 0.18 * screen - letterbox) / height, left: 0.02, right: 0.11 } : {}, ...override };
2145
+ const bands = [];
2146
+ if (inset.bottom) bands.push({ name: "bottom UI band", hint: `Reels and TikTok draw their controls over the bottom ${Math.round(inset.bottom * height)} px: keep text above y ${Math.round(height - inset.bottom * height)}`, x0: 0, y0: height - inset.bottom * height, x1: width, y1: height });
2147
+ if (inset.right) bands.push({ name: "right UI band", hint: `the action buttons cover the right ${Math.round(inset.right * width)} px: keep text left of x ${Math.round(width - inset.right * width)}`, x0: width - inset.right * width, y0: 0, x1: width, y1: height });
2148
+ if (inset.top) bands.push({ name: "top UI band", hint: `search and the camera cover the top ${Math.round(inset.top * height)} px: keep text below y ${Math.round(inset.top * height)}`, x0: 0, y0: 0, x1: width, y1: inset.top * height });
2149
+ if (inset.left) bands.push({ name: "left UI band", hint: `keep text right of x ${Math.round(inset.left * width)}`, x0: 0, y0: 0, x1: inset.left * width, y1: height });
2150
+ return bands;
2151
+ }
1935
2152
  async function reviewComposition(composition, options = {}) {
1936
2153
  const { fps, width, height, durationFrames } = composition, baseDir = options.baseDir ?? process.cwd();
1937
2154
  const every = Math.max(1, Math.round(options.every ?? fps / 2)), frames = [];
@@ -1980,6 +2197,12 @@ async function reviewComposition(composition, options = {}) {
1980
2197
  const other = report.texts.find((o) => o !== t && o.id !== t.id && visible(o) && overlap(t, o) > 0.15 * Math.min(area(t), area(o)) && (t.kind === "captions" || o.kind === "captions" || t.id < o.id));
1981
2198
  return other ? t.kind === "captions" || other.kind === "captions" ? `"${(t.kind === "captions" ? other : t).text}" sits under the captions: keep content out of the captions band` : `"${t.text}" overlaps "${other.text}"` : void 0;
1982
2199
  }, "error");
2200
+ const bands = safeZoneBands(width, height, options.safeZone);
2201
+ if (bands.length) persistent("safe-zone", hold, (t) => {
2202
+ if (!visible(t)) return;
2203
+ const band = bands.find((b) => overlap(t, b) > 0.25 * area(t));
2204
+ return band ? `"${t.text}" sits in the ${band.name}: ${band.hint}` : void 0;
2205
+ }, "warning");
1983
2206
  const crowded = reports.map((r) => r.texts.filter((t) => visible(t) && t.kind === "text").reduce((n, t) => n + t.words, 0) > 40), busy = crowded.findIndex((_, i) => i + hold <= crowded.length && crowded.slice(i, i + hold).every(Boolean));
1984
2207
  if (busy >= 0) issues.push({ severity: "warning", code: "busy", message: `More than 40 words on screen at ${seconds2(frames[busy], fps)}: split it across beats or cut words`, frame: frames[busy] });
1985
2208
  const texts = reports.flatMap((r) => r.texts).filter((t) => visible(t) && t.kind === "text"), sizes = [...new Set(texts.map((t) => Math.round(t.baseSize)))].sort((a, b) => b - a), colours = [...new Set(texts.map((t) => t.color.toLowerCase()).filter((c) => c !== "gradient"))];
@@ -2078,7 +2301,7 @@ async function reviewComposition(composition, options = {}) {
2078
2301
  // packages/core/src/distributed.ts
2079
2302
  import { mkdtemp as mkdtemp2, rm as rm4, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
2080
2303
  import { tmpdir as tmpdir3 } from "node:os";
2081
- import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join8, resolve as resolve2 } from "node:path";
2304
+ import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join9, resolve as resolve2 } from "node:path";
2082
2305
  import { timingSafeEqual } from "node:crypto";
2083
2306
  function planChunks(first, last, chunkSize) {
2084
2307
  if (!Number.isInteger(chunkSize) || chunkSize < 1) throw new Error("Chunk size must be a positive whole number of frames");
@@ -2102,7 +2325,7 @@ async function renderDistributed(composition, options) {
2102
2325
  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}`);
2103
2326
  const chunkSettings = { format, codec: settings.codec, transparent: settings.transparent, preset: options.preset ?? defaultPreset, crf: settings.crf, bitrate: settings.bitrate, scale: settings.scale };
2104
2327
  const jobs = planChunks(first, last, options.chunkSize ?? Math.max(30, Math.ceil((last - first + 1) / (options.workers.length * 2)))), total = last - first + 1, retries = options.retries ?? 2;
2105
- const work = await mkdtemp2(join8(tmpdir3(), "ove-chunks-")), segments = new Array(jobs.length), attempts = /* @__PURE__ */ new Map();
2328
+ const work = await mkdtemp2(join9(tmpdir3(), "ove-chunks-")), segments = new Array(jobs.length), attempts = /* @__PURE__ */ new Map();
2106
2329
  const abort = new AbortController();
2107
2330
  const onOuterAbort = () => abort.abort(options.signal?.reason);
2108
2331
  options.signal?.addEventListener("abort", onOuterAbort);
@@ -2116,7 +2339,7 @@ async function renderDistributed(composition, options) {
2116
2339
  const job = queue.shift();
2117
2340
  try {
2118
2341
  const bytes = await worker.renderChunk(target, job, chunkSettings, abort.signal);
2119
- const file = join8(work, `chunk-${String(job.index).padStart(5, "0")}.${format}`);
2342
+ const file = join9(work, `chunk-${String(job.index).padStart(5, "0")}.${format}`);
2120
2343
  await writeFile4(file, bytes);
2121
2344
  segments[job.index] = file;
2122
2345
  failures = 0;
@@ -2149,7 +2372,7 @@ async function renderDistributed(composition, options) {
2149
2372
  if (segments.some((s) => !s)) throw new Error("Some chunks were not rendered");
2150
2373
  await mkdir4(dirname2(output), { recursive: true });
2151
2374
  const audio = await masterAudio(target, baseDir).then((mix) => mix && { ...mix, durationSeconds: total / composition.fps, offsetSeconds: first / composition.fps });
2152
- await concatSegments(segments, join8(work, "segments.txt"), output, audio, { format, audioBitrate: settings.audioBitrate });
2375
+ await concatSegments(segments, join9(work, "segments.txt"), output, audio, { format, audioBitrate: settings.audioBitrate });
2153
2376
  return { output, format, frames: total, durationSeconds: Number(await probe(output)) || total / composition.fps };
2154
2377
  } finally {
2155
2378
  options.signal?.removeEventListener("abort", onOuterAbort);
@@ -2182,6 +2405,7 @@ function checkRange(composition, range) {
2182
2405
  return [first, last];
2183
2406
  }
2184
2407
  async function renderMedia(composition, options) {
2408
+ await ensureFfmpeg();
2185
2409
  const baseDir = options.baseDir ?? process.cwd(), output = resolve3(options.output);
2186
2410
  const format = options.format ?? formatForOutput(output, composition.export.format);
2187
2411
  const settings = exportSchema.parse({ ...composition.export, ...Object.fromEntries(Object.entries({ crf: options.crf, bitrate: options.bitrate, audioBitrate: options.audioBitrate, scale: options.scale, loudness: options.loudness }).filter(([, v]) => v !== void 0)), format, codec: options.codec ?? (format === "mp4" ? composition.export.codec : "h264"), transparent: options.transparent ?? composition.export.transparent });
@@ -2200,7 +2424,7 @@ async function renderMedia(composition, options) {
2200
2424
  for await (const png of renderFrames(renderer, first, last + 1)) {
2201
2425
  throwIfAborted();
2202
2426
  if (encoder) await encoder.write(png);
2203
- else await writeFile5(join9(output, `frame-${String(frame).padStart(6, "0")}.png`), png);
2427
+ else await writeFile5(join10(output, `frame-${String(frame).padStart(6, "0")}.png`), png);
2204
2428
  frame++;
2205
2429
  options.onProgress?.({ frame: frame - 1, rendered: frame - first, total });
2206
2430
  }
@@ -2266,12 +2490,15 @@ export {
2266
2490
  dropFillerWords,
2267
2491
  duckSchema,
2268
2492
  easingSchema,
2493
+ ensureChromium,
2494
+ ensureFfmpeg,
2269
2495
  evaluateFrame,
2270
2496
  expandMattes,
2271
2497
  expandSceneGrades,
2272
2498
  exportSchema,
2273
2499
  ffmpegPath,
2274
2500
  ffprobePath,
2501
+ fillers,
2275
2502
  formatCaptions,
2276
2503
  formatForOutput,
2277
2504
  frameToSeconds,