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 +333 -123
- package/dist/index.js +224 -42
- package/dist/types/packages/cli/src/templates.d.ts +2 -0
- package/dist/types/packages/core/src/index.d.ts +2 -2
- package/dist/types/packages/ffmpeg/src/auto-install.d.ts +12 -0
- package/dist/types/packages/ffmpeg/src/index.d.ts +1 -0
- package/dist/types/packages/jobs/src/env.d.ts +3 -0
- package/dist/types/packages/renderer/src/index.d.ts +3 -0
- package/package.json +1 -1
- package/studio/assets/index-BGdqA-kT.js +10 -0
- package/studio/index.html +1 -1
- package/templates/product-card/composition.json +11 -4
- package/templates/product-card/template.json +3 -3
- package/templates/social-reel/composition.json +19 -12
- package/templates/social-reel/template.json +3 -3
- package/studio/assets/index-n3DJI_GV.js +0 -10
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
|
|
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";
|
|
@@ -865,7 +865,7 @@ var describeError = (error) => error instanceof z3.ZodError ? z3.prettifyError(e
|
|
|
865
865
|
// packages/core/src/media.ts
|
|
866
866
|
import { spawn as spawn2 } from "node:child_process";
|
|
867
867
|
import { mkdir, readFile as readFile2, stat as stat2, writeFile } from "node:fs/promises";
|
|
868
|
-
import { join as
|
|
868
|
+
import { join as join4 } from "node:path";
|
|
869
869
|
import { createHash } from "node:crypto";
|
|
870
870
|
|
|
871
871
|
// packages/animation/src/index.ts
|
|
@@ -1222,11 +1222,143 @@ async function toDataUrl(source) {
|
|
|
1222
1222
|
// packages/ffmpeg/src/index.ts
|
|
1223
1223
|
import { spawn } from "node:child_process";
|
|
1224
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";
|
|
1225
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
|
|
1226
1358
|
var x264Presets = ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"];
|
|
1227
1359
|
var defaultPreset = "veryfast";
|
|
1228
|
-
var ffmpegPath = () =>
|
|
1229
|
-
var ffprobePath = () =>
|
|
1360
|
+
var ffmpegPath = () => getFfmpegPath();
|
|
1361
|
+
var ffprobePath = () => getFfprobePath();
|
|
1230
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.`);
|
|
1231
1363
|
var num2 = (n) => {
|
|
1232
1364
|
const text = n.toFixed(5).replace(/\.?0+$/, "");
|
|
@@ -1336,12 +1468,14 @@ function concatSegments(segments, listFile, output, audio, options = {}) {
|
|
|
1336
1468
|
const list = segments.map((s) => `file '${s.replace(/\\/g, "/").replace(/'/g, "'\\''")}'`).join("\n");
|
|
1337
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);
|
|
1338
1470
|
}
|
|
1339
|
-
function extractFrames(source, outDir, options) {
|
|
1471
|
+
async function extractFrames(source, outDir, options) {
|
|
1472
|
+
await ensureFfmpeg();
|
|
1340
1473
|
const { fps, maxWidth, maxHeight, durationSeconds, startSeconds } = options;
|
|
1341
|
-
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",
|
|
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;
|
|
1342
1475
|
}
|
|
1343
1476
|
var requiredEncoders = { mp4: ["libx264", "aac"], "mp4 (h265)": ["libx265"], webm: ["libvpx-vp9", "libopus"], mov: ["prores_ks", "pcm_s16le"], gif: ["gif"], "video layers": ["mjpeg"] };
|
|
1344
|
-
function checkFfmpeg() {
|
|
1477
|
+
async function checkFfmpeg() {
|
|
1478
|
+
await ensureFfmpeg();
|
|
1345
1479
|
return new Promise((resolve4, reject) => {
|
|
1346
1480
|
const child = spawn(ffmpegPath(), ["-hide_banner", "-encoders"]);
|
|
1347
1481
|
let out = "";
|
|
@@ -1358,7 +1492,8 @@ function checkFfmpeg() {
|
|
|
1358
1492
|
});
|
|
1359
1493
|
});
|
|
1360
1494
|
}
|
|
1361
|
-
function hasAudioStream(path) {
|
|
1495
|
+
async function hasAudioStream(path) {
|
|
1496
|
+
await ensureFfmpeg();
|
|
1362
1497
|
return new Promise((resolve4, reject) => {
|
|
1363
1498
|
const child = spawn(ffprobePath(), ["-v", "error", "-select_streams", "a", "-show_entries", "stream=index", "-of", "csv=p=0", path]);
|
|
1364
1499
|
let out = "";
|
|
@@ -1367,7 +1502,8 @@ function hasAudioStream(path) {
|
|
|
1367
1502
|
child.on("close", (c) => c === 0 ? resolve4(out.trim().length > 0) : reject(new Error(`ffprobe could not read ${path}`)));
|
|
1368
1503
|
});
|
|
1369
1504
|
}
|
|
1370
|
-
function probe(path) {
|
|
1505
|
+
async function probe(path) {
|
|
1506
|
+
await ensureFfmpeg();
|
|
1371
1507
|
return new Promise((resolve4, reject) => {
|
|
1372
1508
|
const child = spawn(ffprobePath(), ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", path]);
|
|
1373
1509
|
let out = "";
|
|
@@ -1391,7 +1527,7 @@ async function measureSource(source) {
|
|
|
1391
1527
|
const version = isUrl(source) ? "" : await stat2(source).then((s) => `${s.size}:${s.mtimeMs}`, () => {
|
|
1392
1528
|
throw new Error(`Media not found: ${source}`);
|
|
1393
1529
|
});
|
|
1394
|
-
const key = createHash("sha256").update(`${source}|${version}|v1`).digest("hex").slice(0, 32), file =
|
|
1530
|
+
const key = createHash("sha256").update(`${source}|${version}|v1`).digest("hex").slice(0, 32), file = join4(mediaCache(), `${key}.json`);
|
|
1395
1531
|
const cached2 = await readFile2(file, "utf8").then(JSON.parse, () => void 0);
|
|
1396
1532
|
if (cached2) {
|
|
1397
1533
|
void touchCacheEntry(file);
|
|
@@ -1466,7 +1602,7 @@ async function prepareComposition(raw, values = {}, baseDir = process.cwd()) {
|
|
|
1466
1602
|
|
|
1467
1603
|
// packages/renderer/src/index.ts
|
|
1468
1604
|
import { chromium } from "playwright";
|
|
1469
|
-
import { existsSync as
|
|
1605
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1470
1606
|
import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
1471
1607
|
import { availableParallelism } from "node:os";
|
|
1472
1608
|
|
|
@@ -1558,18 +1694,18 @@ var prepareOperations = (operations, fps = 30) => {
|
|
|
1558
1694
|
|
|
1559
1695
|
// packages/renderer/src/video.ts
|
|
1560
1696
|
import { mkdir as mkdir2, readdir as readdir2, rename, rm as rm2, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
|
|
1561
|
-
import { join as
|
|
1697
|
+
import { join as join5 } from "node:path";
|
|
1562
1698
|
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
1563
|
-
var videoFrameFile = (video, index) =>
|
|
1699
|
+
var videoFrameFile = (video, index) => join5(video.dir, `${String(Math.max(0, index - video.first) + 1).padStart(6, "0")}.jpg`);
|
|
1564
1700
|
var videoCacheRoot = () => cacheDir("video");
|
|
1565
1701
|
var complete = ".complete";
|
|
1566
1702
|
async function decode(source, key, first = 0) {
|
|
1567
1703
|
const version = isUrl(source) ? "" : await stat3(source).then((s) => `${s.size}:${s.mtimeMs}`, () => {
|
|
1568
1704
|
throw new Error(`Video not found: ${source}`);
|
|
1569
1705
|
});
|
|
1570
|
-
const dir =
|
|
1706
|
+
const dir = join5(videoCacheRoot(), createHash2("sha256").update(JSON.stringify({ source, version, ...key })).digest("hex").slice(0, 32));
|
|
1571
1707
|
const count = async () => (await readdir2(dir)).filter((f) => f.endsWith(".jpg")).length;
|
|
1572
|
-
if (await stat3(
|
|
1708
|
+
if (await stat3(join5(dir, complete)).then(() => true, () => false)) {
|
|
1573
1709
|
void touchCacheEntry(dir);
|
|
1574
1710
|
return { dir, count: first + await count(), first };
|
|
1575
1711
|
}
|
|
@@ -1577,9 +1713,9 @@ async function decode(source, key, first = 0) {
|
|
|
1577
1713
|
await mkdir2(scratch, { recursive: true });
|
|
1578
1714
|
try {
|
|
1579
1715
|
await extractFrames(source, scratch, key);
|
|
1580
|
-
await writeFile2(
|
|
1716
|
+
await writeFile2(join5(scratch, complete), "");
|
|
1581
1717
|
await rename(scratch, dir).catch(async (error) => {
|
|
1582
|
-
if (!await stat3(
|
|
1718
|
+
if (!await stat3(join5(dir, complete)).then(() => true, () => false)) throw error;
|
|
1583
1719
|
});
|
|
1584
1720
|
} finally {
|
|
1585
1721
|
await rm2(scratch, { recursive: true, force: true });
|
|
@@ -1670,34 +1806,34 @@ async function createPagePool(open, discard, size, timeoutMs = defaultFrameTimeo
|
|
|
1670
1806
|
}
|
|
1671
1807
|
|
|
1672
1808
|
// packages/graphics/src/fonts.ts
|
|
1673
|
-
import { existsSync } from "node:fs";
|
|
1809
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
1674
1810
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
1675
1811
|
import { createRequire } from "node:module";
|
|
1676
|
-
import { dirname, join as
|
|
1812
|
+
import { dirname, join as join6 } from "node:path";
|
|
1677
1813
|
import { fileURLToPath } from "node:url";
|
|
1678
1814
|
function builtinFontDir() {
|
|
1679
1815
|
const bundled = fileURLToPath(new URL("./fonts/", import.meta.url));
|
|
1680
|
-
if (
|
|
1681
|
-
return
|
|
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");
|
|
1682
1818
|
}
|
|
1683
1819
|
var cached;
|
|
1684
|
-
var builtinFonts = () => cached ??= Promise.all(builtinFontFaces.map(async (face) => ({ ...face, src: `builtin:${face.file}`, url: `data:font/woff2;base64,${(await readFile3(
|
|
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")}` })));
|
|
1685
1821
|
|
|
1686
1822
|
// packages/graphics/src/runtime.ts
|
|
1687
|
-
import { existsSync as
|
|
1823
|
+
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
1688
1824
|
import { createRequire as createRequire2 } from "node:module";
|
|
1689
|
-
import { join as
|
|
1825
|
+
import { join as join7 } from "node:path";
|
|
1690
1826
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1691
1827
|
var runtimeDir = () => {
|
|
1692
1828
|
const bundled = fileURLToPath2(new URL("./runtime/", import.meta.url));
|
|
1693
|
-
return
|
|
1829
|
+
return existsSync3(join7(bundled, "canvas-runtime.js")) ? bundled : fileURLToPath2(new URL("../runtime/", import.meta.url));
|
|
1694
1830
|
};
|
|
1695
|
-
var read = (name2) => readFileSync(
|
|
1831
|
+
var read = (name2) => readFileSync(join7(runtimeDir(), name2), "utf8");
|
|
1696
1832
|
var textLayoutSource = read("text-layout.js");
|
|
1697
1833
|
var lottieFile = "lottie_light_canvas.min.js";
|
|
1698
1834
|
var lottiePath = () => {
|
|
1699
|
-
const bundled =
|
|
1700
|
-
return
|
|
1835
|
+
const bundled = join7(runtimeDir(), lottieFile);
|
|
1836
|
+
return existsSync3(bundled) ? bundled : createRequire2(import.meta.url).resolve(`lottie-web/build/player/${lottieFile}`);
|
|
1701
1837
|
};
|
|
1702
1838
|
var lottieSource = readFileSync(lottiePath(), "utf8");
|
|
1703
1839
|
var canvasRuntimeSource = `${textLayoutSource}
|
|
@@ -1716,25 +1852,68 @@ for(let i=0;i<images.length;i++){const img=new Image();img.src=images[i];await i
|
|
|
1716
1852
|
var allLayers = (composition) => composition.scenes.flatMap((scene) => scene.layers);
|
|
1717
1853
|
var browserArgs = ["--use-angle=swiftshader", "--enable-unsafe-swiftshader", ...(process.env.OVE_CHROMIUM_ARGS ?? "").split(/\s+/).filter(Boolean)];
|
|
1718
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
|
+
}
|
|
1719
1893
|
async function launchBrowser() {
|
|
1720
1894
|
const explicit = process.env.OVE_CHROMIUM_EXECUTABLE;
|
|
1721
1895
|
if (explicit) {
|
|
1722
|
-
if (!
|
|
1896
|
+
if (!existsSync4(explicit)) throw new Error(`OVE_CHROMIUM_EXECUTABLE points to ${explicit}, which does not exist.`);
|
|
1723
1897
|
return chromium.launch({ headless: true, executablePath: explicit, args: browserArgs });
|
|
1724
1898
|
}
|
|
1725
|
-
if (
|
|
1899
|
+
if (existsSync4(chromium.executablePath())) return chromium.launch({ headless: true, args: browserArgs });
|
|
1726
1900
|
for (const channel of ["chrome", "msedge"]) {
|
|
1727
1901
|
try {
|
|
1728
1902
|
return await chromium.launch({ headless: true, channel, args: browserArgs });
|
|
1729
1903
|
} catch {
|
|
1730
1904
|
}
|
|
1731
1905
|
}
|
|
1732
|
-
|
|
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
|
+
}
|
|
1733
1912
|
}
|
|
1734
1913
|
async function describeBrowser() {
|
|
1735
1914
|
const browser = await launchBrowser();
|
|
1736
1915
|
try {
|
|
1737
|
-
return `${browser.browserType().name()} ${browser.version()}${process.env.OVE_CHROMIUM_EXECUTABLE ? ` (OVE_CHROMIUM_EXECUTABLE=${process.env.OVE_CHROMIUM_EXECUTABLE})` :
|
|
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)"}`;
|
|
1738
1917
|
} finally {
|
|
1739
1918
|
await browser.close();
|
|
1740
1919
|
}
|
|
@@ -1864,10 +2043,10 @@ async function* renderFrames(renderer, from, to, options = {}) {
|
|
|
1864
2043
|
import { stat as stat4 } from "node:fs/promises";
|
|
1865
2044
|
import { spawn as spawn3 } from "node:child_process";
|
|
1866
2045
|
import { mkdtemp, rm as rm3, mkdir as mkdir3, rename as rename2 } from "node:fs/promises";
|
|
1867
|
-
import { existsSync as
|
|
2046
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
1868
2047
|
import { createHash as createHash3 } from "node:crypto";
|
|
1869
2048
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
1870
|
-
import { join as
|
|
2049
|
+
import { join as join8 } from "node:path";
|
|
1871
2050
|
var runFfmpeg = (args) => new Promise((resolve4, reject) => {
|
|
1872
2051
|
const child = spawn3(ffmpegPath(), args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
1873
2052
|
let err = "";
|
|
@@ -1878,8 +2057,8 @@ var runFfmpeg = (args) => new Promise((resolve4, reject) => {
|
|
|
1878
2057
|
var sfxDir = () => cacheDir("sfx");
|
|
1879
2058
|
var making = /* @__PURE__ */ new Map();
|
|
1880
2059
|
function sfxFile(name2) {
|
|
1881
|
-
const file =
|
|
1882
|
-
if (
|
|
2060
|
+
const file = join8(sfxDir(), `${name2}-${createHash3("sha256").update(JSON.stringify(sfxArgs(name2, ""))).digest("hex").slice(0, 12)}.wav`);
|
|
2061
|
+
if (existsSync5(file)) {
|
|
1883
2062
|
void touchCacheEntry(file);
|
|
1884
2063
|
return Promise.resolve(file);
|
|
1885
2064
|
}
|
|
@@ -1933,7 +2112,7 @@ async function planAudio(composition, baseDir, probe2 = hasAudioStream) {
|
|
|
1933
2112
|
}
|
|
1934
2113
|
async function measureMix(mix) {
|
|
1935
2114
|
if (!mix.clips.length) return void 0;
|
|
1936
|
-
const dir = await mkdtemp(
|
|
2115
|
+
const dir = await mkdtemp(join8(tmpdir2(), "ove-loudness-")), wav = join8(dir, "mix.wav");
|
|
1937
2116
|
try {
|
|
1938
2117
|
const args = audioMixArgs(mix, ["-c:a", "pcm_s16le"]), video = args.indexOf("0:v");
|
|
1939
2118
|
if (video > 0) args.splice(video - 1, 2);
|
|
@@ -2122,7 +2301,7 @@ async function reviewComposition(composition, options = {}) {
|
|
|
2122
2301
|
// packages/core/src/distributed.ts
|
|
2123
2302
|
import { mkdtemp as mkdtemp2, rm as rm4, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
|
|
2124
2303
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
2125
|
-
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as
|
|
2304
|
+
import { dirname as dirname2, extname as extname2, isAbsolute as isAbsolute2, join as join9, resolve as resolve2 } from "node:path";
|
|
2126
2305
|
import { timingSafeEqual } from "node:crypto";
|
|
2127
2306
|
function planChunks(first, last, chunkSize) {
|
|
2128
2307
|
if (!Number.isInteger(chunkSize) || chunkSize < 1) throw new Error("Chunk size must be a positive whole number of frames");
|
|
@@ -2146,7 +2325,7 @@ async function renderDistributed(composition, options) {
|
|
|
2146
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}`);
|
|
2147
2326
|
const chunkSettings = { format, codec: settings.codec, transparent: settings.transparent, preset: options.preset ?? defaultPreset, crf: settings.crf, bitrate: settings.bitrate, scale: settings.scale };
|
|
2148
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;
|
|
2149
|
-
const work = await mkdtemp2(
|
|
2328
|
+
const work = await mkdtemp2(join9(tmpdir3(), "ove-chunks-")), segments = new Array(jobs.length), attempts = /* @__PURE__ */ new Map();
|
|
2150
2329
|
const abort = new AbortController();
|
|
2151
2330
|
const onOuterAbort = () => abort.abort(options.signal?.reason);
|
|
2152
2331
|
options.signal?.addEventListener("abort", onOuterAbort);
|
|
@@ -2160,7 +2339,7 @@ async function renderDistributed(composition, options) {
|
|
|
2160
2339
|
const job = queue.shift();
|
|
2161
2340
|
try {
|
|
2162
2341
|
const bytes = await worker.renderChunk(target, job, chunkSettings, abort.signal);
|
|
2163
|
-
const file =
|
|
2342
|
+
const file = join9(work, `chunk-${String(job.index).padStart(5, "0")}.${format}`);
|
|
2164
2343
|
await writeFile4(file, bytes);
|
|
2165
2344
|
segments[job.index] = file;
|
|
2166
2345
|
failures = 0;
|
|
@@ -2193,7 +2372,7 @@ async function renderDistributed(composition, options) {
|
|
|
2193
2372
|
if (segments.some((s) => !s)) throw new Error("Some chunks were not rendered");
|
|
2194
2373
|
await mkdir4(dirname2(output), { recursive: true });
|
|
2195
2374
|
const audio = await masterAudio(target, baseDir).then((mix) => mix && { ...mix, durationSeconds: total / composition.fps, offsetSeconds: first / composition.fps });
|
|
2196
|
-
await concatSegments(segments,
|
|
2375
|
+
await concatSegments(segments, join9(work, "segments.txt"), output, audio, { format, audioBitrate: settings.audioBitrate });
|
|
2197
2376
|
return { output, format, frames: total, durationSeconds: Number(await probe(output)) || total / composition.fps };
|
|
2198
2377
|
} finally {
|
|
2199
2378
|
options.signal?.removeEventListener("abort", onOuterAbort);
|
|
@@ -2226,6 +2405,7 @@ function checkRange(composition, range) {
|
|
|
2226
2405
|
return [first, last];
|
|
2227
2406
|
}
|
|
2228
2407
|
async function renderMedia(composition, options) {
|
|
2408
|
+
await ensureFfmpeg();
|
|
2229
2409
|
const baseDir = options.baseDir ?? process.cwd(), output = resolve3(options.output);
|
|
2230
2410
|
const format = options.format ?? formatForOutput(output, composition.export.format);
|
|
2231
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 });
|
|
@@ -2244,7 +2424,7 @@ async function renderMedia(composition, options) {
|
|
|
2244
2424
|
for await (const png of renderFrames(renderer, first, last + 1)) {
|
|
2245
2425
|
throwIfAborted();
|
|
2246
2426
|
if (encoder) await encoder.write(png);
|
|
2247
|
-
else await writeFile5(
|
|
2427
|
+
else await writeFile5(join10(output, `frame-${String(frame).padStart(6, "0")}.png`), png);
|
|
2248
2428
|
frame++;
|
|
2249
2429
|
options.onProgress?.({ frame: frame - 1, rendered: frame - first, total });
|
|
2250
2430
|
}
|
|
@@ -2310,6 +2490,8 @@ export {
|
|
|
2310
2490
|
dropFillerWords,
|
|
2311
2491
|
duckSchema,
|
|
2312
2492
|
easingSchema,
|
|
2493
|
+
ensureChromium,
|
|
2494
|
+
ensureFfmpeg,
|
|
2313
2495
|
evaluateFrame,
|
|
2314
2496
|
expandMattes,
|
|
2315
2497
|
expandSceneGrades,
|
|
@@ -56,6 +56,8 @@ export declare function composeSmart(provider: AiProvider, options: ComposeOptio
|
|
|
56
56
|
full?: boolean;
|
|
57
57
|
noPlan?: boolean;
|
|
58
58
|
registry?: Registry;
|
|
59
|
+
theme?: string;
|
|
60
|
+
template?: string;
|
|
59
61
|
}): Promise<ComposeResult & {
|
|
60
62
|
template?: string;
|
|
61
63
|
source?: Record<string, unknown>;
|
|
@@ -8,8 +8,8 @@ export { cacheRoot, cacheUsage, cleanCache, type CacheUsage } from '../../assets
|
|
|
8
8
|
export { reviewComposition, type ReviewIssue, type ReviewResult, type ReviewOptions } from './review.js';
|
|
9
9
|
export { measureMedia, attachCaptions, prepareComposition, captionTrack } from './media.js';
|
|
10
10
|
export * from './distributed.js';
|
|
11
|
-
export { createRenderer, renderFrames, defaultConcurrency, launchBrowser, describeBrowser, browserInstallHint, type Renderer } from '../../renderer/src/index.js';
|
|
12
|
-
export { x264Presets, defaultPreset, checkFfmpeg, requiredEncoders, ffmpegPath, ffprobePath, type X264Preset } from '../../ffmpeg/src/index.js';
|
|
11
|
+
export { createRenderer, renderFrames, defaultConcurrency, launchBrowser, describeBrowser, ensureChromium, browserInstallHint, type Renderer } from '../../renderer/src/index.js';
|
|
12
|
+
export { x264Presets, defaultPreset, checkFfmpeg, requiredEncoders, ffmpegPath, ffprobePath, ensureFfmpeg, type X264Preset } from '../../ffmpeg/src/index.js';
|
|
13
13
|
export type OutputFormat = typeof outputFormats[number];
|
|
14
14
|
export type LoadedComposition = {
|
|
15
15
|
composition: Composition;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const localFfmpegPath: string;
|
|
2
|
+
export declare const localFfprobePath: string;
|
|
3
|
+
export declare function hasSystemFfmpeg(): boolean;
|
|
4
|
+
export declare function hasSystemFfprobe(): boolean;
|
|
5
|
+
export declare const getFfmpegPath: () => string;
|
|
6
|
+
export declare const getFfprobePath: () => string;
|
|
7
|
+
export declare function ensureFfmpeg(options?: {
|
|
8
|
+
onProgress?: (msg: string) => void;
|
|
9
|
+
}): Promise<{
|
|
10
|
+
ffmpeg: string;
|
|
11
|
+
ffprobe: string;
|
|
12
|
+
}>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { ensureFfmpeg, hasSystemFfmpeg, hasSystemFfprobe, localFfmpegPath, localFfprobePath } from './auto-install.js';
|
|
1
2
|
export declare const x264Presets: readonly ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"];
|
|
2
3
|
export type X264Preset = typeof x264Presets[number];
|
|
3
4
|
export type EncodeOptions = {
|
|
@@ -30,6 +30,9 @@ export interface JobPaths {
|
|
|
30
30
|
export type JobCapabilities = {
|
|
31
31
|
composeSmart(provider: AiProvider, options: ComposeOptions, mode: {
|
|
32
32
|
full?: boolean;
|
|
33
|
+
noPlan?: boolean;
|
|
34
|
+
theme?: string;
|
|
35
|
+
template?: string;
|
|
33
36
|
}): Promise<ComposeResult & {
|
|
34
37
|
template?: string;
|
|
35
38
|
source?: Record<string, unknown>;
|
|
@@ -26,6 +26,9 @@ export declare const scaledSize: (composition: Composition, scale?: number) => {
|
|
|
26
26
|
height: number;
|
|
27
27
|
};
|
|
28
28
|
export declare const 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.";
|
|
29
|
+
export declare function ensureChromium(options?: {
|
|
30
|
+
onProgress?: (msg: string) => void;
|
|
31
|
+
}): Promise<void>;
|
|
29
32
|
export declare function launchBrowser(): Promise<Browser>;
|
|
30
33
|
export declare function describeBrowser(): Promise<string>;
|
|
31
34
|
export declare const defaultConcurrency: () => number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "motionrender",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "JSON-first, frame-deterministic motion graphics video engine: render compositions to MP4, WebM, ProRes, GIF or PNG frames from Node or the command line.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|