motionrender 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +298 -114
- package/dist/index.js +224 -42
- 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/renderer/src/index.d.ts +3 -0
- package/package.json +1 -1
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
|
|
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",
|
|
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 = () =>
|
|
1412
|
-
ffprobePath = () =>
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
1807
|
+
await writeFile2(join5(scratch, complete), "");
|
|
1665
1808
|
await rename(scratch, dir).catch(async (error) => {
|
|
1666
|
-
if (!await stat3(
|
|
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) =>
|
|
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
|
|
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 (
|
|
1783
|
-
return
|
|
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(
|
|
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
|
|
1938
|
+
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
1796
1939
|
import { createRequire as createRequire2 } from "node:module";
|
|
1797
|
-
import { join as
|
|
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
|
|
1948
|
+
return existsSync3(join7(bundled, "canvas-runtime.js")) ? bundled : fileURLToPath2(new URL("../runtime/", import.meta.url));
|
|
1806
1949
|
};
|
|
1807
|
-
read = (name2) => readFileSync(
|
|
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 =
|
|
1812
|
-
return
|
|
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
|
|
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 (!
|
|
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 (
|
|
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
|
-
|
|
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})` :
|
|
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
|
|
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
|
|
2187
|
+
import { join as join8 } from "node:path";
|
|
2002
2188
|
function sfxFile(name2) {
|
|
2003
|
-
const file2 =
|
|
2004
|
-
if (
|
|
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(
|
|
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
|
|
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(
|
|
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 =
|
|
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,
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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) =>
|
|
4071
|
-
indexFile = (compositionFile) =>
|
|
4257
|
+
historyDir = (compositionFile) => join12(dirname4(resolve4(compositionFile)), "history", basename(compositionFile).replace(/\.json$/i, ""));
|
|
4258
|
+
indexFile = (compositionFile) => join12(historyDir(compositionFile), "index.json");
|
|
4072
4259
|
pad = (n) => String(n).padStart(4, "0");
|
|
4073
4260
|
readText = (file2) => readFile7(file2, "utf8").then((text) => text, () => void 0);
|
|
4074
4261
|
scenesOf = (composition) => Array.isArray(composition?.scenes) ? composition.scenes : [];
|
|
@@ -4273,7 +4460,7 @@ var init_captions2 = __esm({
|
|
|
4273
4460
|
|
|
4274
4461
|
// packages/ai/src/narrate.ts
|
|
4275
4462
|
import { mkdir as mkdir8, readFile as readFile8, writeFile as writeFile8 } from "node:fs/promises";
|
|
4276
|
-
import { basename as basename2, dirname as dirname6, join as
|
|
4463
|
+
import { basename as basename2, dirname as dirname6, join as join13, resolve as resolve6 } from "node:path";
|
|
4277
4464
|
function cleanScript(text) {
|
|
4278
4465
|
return text.replace(/^\s*(narration|narrator|voiceover|vo)\s*:\s*/gim, "").replace(/^#{1,6}\s+/gm, "").replace(/\*\*|__|`/g, "").replace(/^\s*[-*]\s+/gm, "").replace(/[“”]/g, '"').replace(/[‘’]/g, "'").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
4279
4466
|
}
|
|
@@ -4338,7 +4525,7 @@ async function narrate(provider, options3) {
|
|
|
4338
4525
|
await mkdir8(options3.outDir, { recursive: true });
|
|
4339
4526
|
options3.onProgress?.("Synthesising speech\u2026");
|
|
4340
4527
|
const audio = await provider.speak(text, { voice: options3.voice, signal: options3.signal });
|
|
4341
|
-
const audioFile =
|
|
4528
|
+
const audioFile = join13(options3.outDir, `narration.${audio.extension}`);
|
|
4342
4529
|
await writeFile8(audioFile, audio.data);
|
|
4343
4530
|
options3.onProgress?.("Transcribing for word timings\u2026");
|
|
4344
4531
|
let transcript = { text: "", words: [], cues: [] };
|
|
@@ -4358,7 +4545,7 @@ async function narrate(provider, options3) {
|
|
|
4358
4545
|
let captionsFile;
|
|
4359
4546
|
const hasCaptions = options3.captions !== false && (transcript.words.length > 0 || transcript.cues.length > 0);
|
|
4360
4547
|
if (hasCaptions) {
|
|
4361
|
-
captionsFile =
|
|
4548
|
+
captionsFile = join13(options3.outDir, "narration.captions.json");
|
|
4362
4549
|
await writeFile8(captionsFile, JSON.stringify(transcript.words.length ? { words: transcript.words } : { cues: transcript.cues }, null, 2) + "\n");
|
|
4363
4550
|
}
|
|
4364
4551
|
let composition;
|
|
@@ -4389,7 +4576,7 @@ async function captionAudio(provider, options3) {
|
|
|
4389
4576
|
}
|
|
4390
4577
|
const outDir = options3.outDir ?? dirname6(audioFile);
|
|
4391
4578
|
await mkdir8(outDir, { recursive: true });
|
|
4392
|
-
const captionsFile =
|
|
4579
|
+
const captionsFile = join13(outDir, `${basename2(audioFile).replace(/\.[^.]+$/, "")}.captions.json`);
|
|
4393
4580
|
await writeFile8(captionsFile, JSON.stringify(transcript.words.length ? { words: transcript.words } : { cues: transcript.cues }, null, 2) + "\n");
|
|
4394
4581
|
let composition;
|
|
4395
4582
|
if (options3.composition) {
|
|
@@ -4418,7 +4605,7 @@ var init_narrate = __esm({
|
|
|
4418
4605
|
|
|
4419
4606
|
// packages/ai/src/voiceover.ts
|
|
4420
4607
|
import { mkdir as mkdir9, writeFile as writeFile9 } from "node:fs/promises";
|
|
4421
|
-
import { dirname as dirname7, join as
|
|
4608
|
+
import { dirname as dirname7, join as join14 } from "node:path";
|
|
4422
4609
|
function mergeNarration(source, resolved) {
|
|
4423
4610
|
const live = resolved.scenes.filter((scene) => scene.id !== "narration" && scene.id !== "captions"), extra = resolved.scenes.filter((scene) => scene.id === "narration" || scene.id === "captions");
|
|
4424
4611
|
if (!Array.isArray(source.scenes) || source.scenes.length !== live.length || source.scenes.some((scene, i) => scene?.id !== live[i].id)) return void 0;
|
|
@@ -4517,7 +4704,7 @@ async function narrateScenes(provider, composition, options3) {
|
|
|
4517
4704
|
next.durationFrames = Math.max(...live.map((scene) => Number(scene.startFrame) + Number(scene.durationFrames)));
|
|
4518
4705
|
const wav = concatWav(timeline);
|
|
4519
4706
|
await mkdir9(options3.outDir, { recursive: true });
|
|
4520
|
-
const audioFile =
|
|
4707
|
+
const audioFile = join14(options3.outDir, "narration.wav");
|
|
4521
4708
|
await writeFile9(audioFile, wav);
|
|
4522
4709
|
let transcript = { text: "", words: [], cues: [] };
|
|
4523
4710
|
if (provider.transcribe) {
|
|
@@ -4526,7 +4713,7 @@ async function narrateScenes(provider, composition, options3) {
|
|
|
4526
4713
|
} else options3.onProgress?.("This provider cannot transcribe, so captions are skipped.");
|
|
4527
4714
|
let captionsFile;
|
|
4528
4715
|
if (transcript.words.length || transcript.cues.length) {
|
|
4529
|
-
captionsFile =
|
|
4716
|
+
captionsFile = join14(options3.outDir, "narration.captions.json");
|
|
4530
4717
|
await writeFile9(captionsFile, JSON.stringify(transcript.words.length ? { words: transcript.words } : { cues: transcript.cues }, null, 2) + "\n");
|
|
4531
4718
|
}
|
|
4532
4719
|
const baseDir = options3.compositionPath ? dirname7(options3.compositionPath) : options3.outDir;
|
|
@@ -4553,7 +4740,7 @@ var init_voiceover = __esm({
|
|
|
4553
4740
|
|
|
4554
4741
|
// packages/ai/src/edit.ts
|
|
4555
4742
|
import { mkdir as mkdir10, readFile as readFile9, writeFile as writeFile10 } from "node:fs/promises";
|
|
4556
|
-
import { basename as basename3, join as
|
|
4743
|
+
import { basename as basename3, join as join15, resolve as resolve7 } from "node:path";
|
|
4557
4744
|
function planCuts(transcript, options3 = {}) {
|
|
4558
4745
|
const maxSilence = options3.maxSilence ?? 0.5, padding = options3.padding ?? 0.3;
|
|
4559
4746
|
const words = transcript.words.filter((w) => Number.isFinite(w.start) && Number.isFinite(w.end) && w.end >= w.start).sort((a, b) => a.start - b.start);
|
|
@@ -4632,8 +4819,8 @@ async function editVideo(provider, options3) {
|
|
|
4632
4819
|
options3.onProgress?.("Reading the clip\u2026");
|
|
4633
4820
|
if (await hasAudioStream(clip2) === false) throw new Error(`${basename3(clip2)} has no audio track; there is nothing to transcribe`);
|
|
4634
4821
|
const sourceSeconds = Number(await probe(clip2)) || 0;
|
|
4635
|
-
await mkdir10(
|
|
4636
|
-
const audioFile =
|
|
4822
|
+
await mkdir10(join15(outDir, "assets"), { recursive: true });
|
|
4823
|
+
const audioFile = join15(outDir, "assets", "source-audio.m4a");
|
|
4637
4824
|
options3.onProgress?.("Extracting the audio\u2026");
|
|
4638
4825
|
await extractAudio(clip2, audioFile);
|
|
4639
4826
|
options3.onProgress?.("Transcribing\u2026");
|
|
@@ -4643,7 +4830,7 @@ async function editVideo(provider, options3) {
|
|
|
4643
4830
|
const plan = planCuts(transcript, { maxSilence: options3.maxSilence, keepFillers: options3.keepFillers, padding: options3.padding });
|
|
4644
4831
|
let captionsFile;
|
|
4645
4832
|
if (options3.captions !== false) {
|
|
4646
|
-
captionsFile =
|
|
4833
|
+
captionsFile = join15(outDir, "assets", "edit-captions.json");
|
|
4647
4834
|
await writeFile10(captionsFile, JSON.stringify({ words: plan.words }, null, 2) + "\n");
|
|
4648
4835
|
}
|
|
4649
4836
|
const composition = editComposition({
|
|
@@ -4682,7 +4869,7 @@ var init_edit = __esm({
|
|
|
4682
4869
|
|
|
4683
4870
|
// packages/ai/src/shorts.ts
|
|
4684
4871
|
import { mkdir as mkdir11, readFile as readFile10, writeFile as writeFile11 } from "node:fs/promises";
|
|
4685
|
-
import { basename as basename4, join as
|
|
4872
|
+
import { basename as basename4, join as join16, resolve as resolve8 } from "node:path";
|
|
4686
4873
|
function sentencesOf(words, maxSilence) {
|
|
4687
4874
|
const sentences = [];
|
|
4688
4875
|
let current = [];
|
|
@@ -4741,7 +4928,7 @@ async function shortsFrom(provider, options3) {
|
|
|
4741
4928
|
if (await hasAudioStream(clip2) === false) throw new Error(`${basename4(clip2)} has no audio track; there is nothing to transcribe`);
|
|
4742
4929
|
const sourceSeconds = Number(await probe(clip2)) || 0;
|
|
4743
4930
|
await mkdir11(outDir, { recursive: true });
|
|
4744
|
-
const audioFile =
|
|
4931
|
+
const audioFile = join16(outDir, "source-audio.m4a");
|
|
4745
4932
|
options3.onProgress?.("Extracting the audio\u2026");
|
|
4746
4933
|
await extractAudio(clip2, audioFile);
|
|
4747
4934
|
options3.onProgress?.("Transcribing\u2026");
|
|
@@ -4754,12 +4941,12 @@ async function shortsFrom(provider, options3) {
|
|
|
4754
4941
|
for (const [i, candidate] of candidates.entries()) {
|
|
4755
4942
|
const name2 = `short-${String(i + 1).padStart(2, "0")}`;
|
|
4756
4943
|
options3.onProgress?.(`Writing ${name2}\u2026`);
|
|
4757
|
-
const dir =
|
|
4944
|
+
const dir = join16(outDir, name2);
|
|
4758
4945
|
const plan = shortsCandidatePlan(candidate, options3.cut !== false, options3.maxSilence ?? 0.5, options3.keepFillers ?? false);
|
|
4759
4946
|
let captionsFile;
|
|
4760
4947
|
if (options3.captions !== false) {
|
|
4761
|
-
captionsFile =
|
|
4762
|
-
await mkdir11(
|
|
4948
|
+
captionsFile = join16(dir, "assets", "captions.json");
|
|
4949
|
+
await mkdir11(join16(dir, "assets"), { recursive: true });
|
|
4763
4950
|
await writeFile11(captionsFile, JSON.stringify({ words: plan.words }, null, 2) + "\n");
|
|
4764
4951
|
}
|
|
4765
4952
|
const composition = editComposition({
|
|
@@ -5161,7 +5348,7 @@ var init_brand_scraper = __esm({
|
|
|
5161
5348
|
|
|
5162
5349
|
// packages/ai/src/agent.ts
|
|
5163
5350
|
import { mkdir as mkdir13, writeFile as writeFile13 } from "node:fs/promises";
|
|
5164
|
-
import { join as
|
|
5351
|
+
import { join as join17, resolve as resolve11 } from "node:path";
|
|
5165
5352
|
function detectReelFormat(brief) {
|
|
5166
5353
|
return /\b(reel|short|9:16|viral|tiktok|instagram|shorts|reels|vertical)\b/i.test(brief);
|
|
5167
5354
|
}
|
|
@@ -5244,7 +5431,7 @@ async function runVideoAgent(provider, options3, buildHelper) {
|
|
|
5244
5431
|
const fps = options3.fps ?? 30;
|
|
5245
5432
|
const durationSeconds = options3.durationSeconds ?? 6;
|
|
5246
5433
|
const durationFrames = Math.max(30, Math.round(durationSeconds * fps));
|
|
5247
|
-
const dir = resolve11(options3.dir ??
|
|
5434
|
+
const dir = resolve11(options3.dir ?? join17(process.cwd(), `agent-video-${Date.now()}`));
|
|
5248
5435
|
options3.onProgress?.(`\u{1F3AC} Video Director Agent initializing in ${dir}\u2026`);
|
|
5249
5436
|
await mkdir13(dir, { recursive: true });
|
|
5250
5437
|
const messages2 = [
|
|
@@ -5261,9 +5448,9 @@ Make it visually stunning, cinematic, and punchy. Write the complete video.tsx f
|
|
|
5261
5448
|
let attempts = 0;
|
|
5262
5449
|
let currentTask = "compose";
|
|
5263
5450
|
const maxAttempts = 3;
|
|
5264
|
-
const videoPath =
|
|
5265
|
-
const compositionPath =
|
|
5266
|
-
const bundlePath =
|
|
5451
|
+
const videoPath = join17(dir, "video.tsx");
|
|
5452
|
+
const compositionPath = join17(dir, "composition.json");
|
|
5453
|
+
const bundlePath = join17(dir, "video.bundle.js");
|
|
5267
5454
|
while (attempts < maxAttempts) {
|
|
5268
5455
|
attempts++;
|
|
5269
5456
|
options3.onProgress?.(`\u26A1 Generating React motion code with ${provider.id} (${currentTask}, attempt ${attempts}/${maxAttempts})\u2026`);
|
|
@@ -5840,7 +6027,7 @@ __export(templates_exports, {
|
|
|
5840
6027
|
templatesDir: () => templatesDir
|
|
5841
6028
|
});
|
|
5842
6029
|
import { readdir as readdir5, readFile as readFile12 } from "node:fs/promises";
|
|
5843
|
-
import { join as
|
|
6030
|
+
import { join as join18 } from "node:path";
|
|
5844
6031
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5845
6032
|
function assertTemplateId(id2) {
|
|
5846
6033
|
if (!idPattern2.test(id2)) throw new Error(`Template ids use lowercase letters, digits and dashes, not "${id2}"`);
|
|
@@ -5882,8 +6069,8 @@ function infoFrom(id2, meta, composition) {
|
|
|
5882
6069
|
}
|
|
5883
6070
|
async function readTemplate(id2, root = templatesDir) {
|
|
5884
6071
|
const safe = assertTemplateId(id2);
|
|
5885
|
-
const dir =
|
|
5886
|
-
const [meta, composition] = await Promise.all([readJson2(
|
|
6072
|
+
const dir = join18(root, safe);
|
|
6073
|
+
const [meta, composition] = await Promise.all([readJson2(join18(dir, "template.json")), readJson2(join18(dir, "composition.json"))]);
|
|
5887
6074
|
return { dir, info: infoFrom(safe, meta, composition), composition };
|
|
5888
6075
|
}
|
|
5889
6076
|
async function listTemplates(root = templatesDir) {
|
|
@@ -6348,7 +6535,7 @@ var init_bundle = __esm({
|
|
|
6348
6535
|
import { createServer } from "node:http";
|
|
6349
6536
|
import { watch } from "node:fs";
|
|
6350
6537
|
import { readFile as readFile14, writeFile as writeFile15 } from "node:fs/promises";
|
|
6351
|
-
import { basename as basename6, dirname as dirname11, extname as extname4, isAbsolute as isAbsolute4, join as
|
|
6538
|
+
import { basename as basename6, dirname as dirname11, extname as extname4, isAbsolute as isAbsolute4, join as join20, relative as relative3, resolve as resolve13 } from "node:path";
|
|
6352
6539
|
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
6353
6540
|
async function startDevServer(file2, options3 = {}) {
|
|
6354
6541
|
const path = resolve13(file2), baseDir = dirname11(path), concurrency = options3.concurrency ?? defaultConcurrency();
|
|
@@ -6492,7 +6679,7 @@ data: ${JSON.stringify({ version, error: error ?? null })}
|
|
|
6492
6679
|
if (req.method === "GET" && url.pathname.startsWith("/fonts/")) {
|
|
6493
6680
|
const face = builtinFontFaces.find((f) => `/fonts/${f.file}` === url.pathname);
|
|
6494
6681
|
if (!face) return send2(res, 404, "text/plain", "Not found");
|
|
6495
|
-
return send2(res, 200, "font/woff2", await readFile14(
|
|
6682
|
+
return send2(res, 200, "font/woff2", await readFile14(join20(builtinFontDir(), face.file)));
|
|
6496
6683
|
}
|
|
6497
6684
|
if (req.method === "GET" && url.pathname.startsWith("/media/")) {
|
|
6498
6685
|
const target = resolve13(baseDir, decodeURIComponent(url.pathname.slice("/media/".length)));
|
|
@@ -6716,7 +6903,7 @@ var init_http = __esm({
|
|
|
6716
6903
|
|
|
6717
6904
|
// packages/storage/src/index.ts
|
|
6718
6905
|
import { cp as cp2, mkdir as mkdir15, readdir as readdir7, readFile as readFile16, rename as rename3, stat as stat6, writeFile as writeFile16 } from "node:fs/promises";
|
|
6719
|
-
import { dirname as dirname12, extname as extname6, isAbsolute as isAbsolute5, join as
|
|
6906
|
+
import { dirname as dirname12, extname as extname6, isAbsolute as isAbsolute5, join as join22, relative as relativePath, resolve as resolve14, sep } from "node:path";
|
|
6720
6907
|
function within(root, target) {
|
|
6721
6908
|
const rel = relativePath(resolve14(root), resolve14(target));
|
|
6722
6909
|
return rel !== "" && rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute5(rel);
|
|
@@ -6740,7 +6927,7 @@ var init_src10 = __esm({
|
|
|
6740
6927
|
tenantRoot(who) {
|
|
6741
6928
|
if (who.tenantId === LOCAL_PRINCIPAL.tenantId) return this.root;
|
|
6742
6929
|
if (!tenantPattern.test(who.tenantId)) throw new Error("That account name is not valid");
|
|
6743
|
-
return
|
|
6930
|
+
return join22(this.root, "_tenants", who.tenantId);
|
|
6744
6931
|
}
|
|
6745
6932
|
/** A path inside the principal's space: relative values resolve from `base` (default: the space itself), absolute ones must already be inside. */
|
|
6746
6933
|
resolve(who, value, label = "path", base) {
|
|
@@ -6757,7 +6944,7 @@ var init_src10 = __esm({
|
|
|
6757
6944
|
return this.resolve(who, id2, "project");
|
|
6758
6945
|
}
|
|
6759
6946
|
file(who, id2) {
|
|
6760
|
-
return
|
|
6947
|
+
return join22(this.projectDir(who, id2), "composition.json");
|
|
6761
6948
|
}
|
|
6762
6949
|
async ids(who) {
|
|
6763
6950
|
const entries2 = await readdir7(this.tenantRoot(who), { withFileTypes: true }).catch(() => []);
|
|
@@ -6772,7 +6959,7 @@ var init_src10 = __esm({
|
|
|
6772
6959
|
const taken = new Set((await readdir7(space).catch(() => [])).map(String)), base = slugify(name2);
|
|
6773
6960
|
let id2 = base, n = 1;
|
|
6774
6961
|
while (taken.has(id2)) id2 = `${base}-${++n}`;
|
|
6775
|
-
await mkdir15(
|
|
6962
|
+
await mkdir15(join22(space, id2, "assets"), { recursive: true });
|
|
6776
6963
|
return id2;
|
|
6777
6964
|
}
|
|
6778
6965
|
async readRaw(who, id2) {
|
|
@@ -6794,37 +6981,37 @@ var init_src10 = __esm({
|
|
|
6794
6981
|
return copy;
|
|
6795
6982
|
}
|
|
6796
6983
|
async trash(who, id2) {
|
|
6797
|
-
const dir = this.projectDir(who, id2), trash =
|
|
6984
|
+
const dir = this.projectDir(who, id2), trash = join22(this.tenantRoot(who), ".trash");
|
|
6798
6985
|
await mkdir15(trash, { recursive: true });
|
|
6799
|
-
await rename3(dir,
|
|
6986
|
+
await rename3(dir, join22(trash, `${id2}-${Date.now()}`));
|
|
6800
6987
|
}
|
|
6801
6988
|
async assets(who, id2) {
|
|
6802
|
-
const folder =
|
|
6989
|
+
const folder = join22(this.projectDir(who, id2), "assets"), out = [];
|
|
6803
6990
|
for (const name2 of await readdir7(folder).catch(() => [])) {
|
|
6804
6991
|
const kind = assetKinds[extname6(name2).toLowerCase()];
|
|
6805
6992
|
if (!kind) continue;
|
|
6806
|
-
out.push({ name: name2, path: `assets/${name2}`, kind, bytes: (await stat6(
|
|
6993
|
+
out.push({ name: name2, path: `assets/${name2}`, kind, bytes: (await stat6(join22(folder, name2))).size });
|
|
6807
6994
|
}
|
|
6808
6995
|
return out;
|
|
6809
6996
|
}
|
|
6810
6997
|
async putAsset(who, id2, name2, bytes) {
|
|
6811
6998
|
const clean = name2.split(/[\\/]/).pop().replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
6812
6999
|
if (!assetKinds[extname6(clean).toLowerCase()]) throw new Error(`Pictures, video and audio only (${Object.keys(assetKinds).join(" ")})`);
|
|
6813
|
-
const folder =
|
|
7000
|
+
const folder = join22(this.projectDir(who, id2), "assets");
|
|
6814
7001
|
await mkdir15(folder, { recursive: true });
|
|
6815
7002
|
const taken = new Set(await readdir7(folder).catch(() => []));
|
|
6816
7003
|
let final = clean;
|
|
6817
7004
|
for (let n = 2; taken.has(final); n++) final = clean.replace(/(\.[^.]*)?$/, `-${n}$1`);
|
|
6818
|
-
await writeFile16(
|
|
7005
|
+
await writeFile16(join22(folder, final), bytes);
|
|
6819
7006
|
return this.assets(who, id2);
|
|
6820
7007
|
}
|
|
6821
7008
|
async thumb(who, id2) {
|
|
6822
|
-
const cache =
|
|
7009
|
+
const cache = join22(this.projectDir(who, id2), ".studio", "thumb.png");
|
|
6823
7010
|
const [source, cached2] = await Promise.all([stat6(this.file(who, id2)), stat6(cache).catch(() => void 0)]);
|
|
6824
7011
|
return cached2 && cached2.mtimeMs >= source.mtimeMs ? readFile16(cache) : void 0;
|
|
6825
7012
|
}
|
|
6826
7013
|
async putThumb(who, id2, png) {
|
|
6827
|
-
const cache =
|
|
7014
|
+
const cache = join22(this.projectDir(who, id2), ".studio", "thumb.png");
|
|
6828
7015
|
await mkdir15(dirname12(cache), { recursive: true });
|
|
6829
7016
|
await writeFile16(cache, png);
|
|
6830
7017
|
}
|
|
@@ -6832,7 +7019,7 @@ var init_src10 = __esm({
|
|
|
6832
7019
|
const walk = async (dir) => {
|
|
6833
7020
|
let total = 0;
|
|
6834
7021
|
for (const entry of await readdir7(dir, { withFileTypes: true }).catch(() => [])) {
|
|
6835
|
-
const path =
|
|
7022
|
+
const path = join22(dir, entry.name);
|
|
6836
7023
|
total += entry.isDirectory() ? entry.name === "_tenants" ? 0 : await walk(path) : (await stat6(path).catch(() => ({ size: 0 }))).size;
|
|
6837
7024
|
}
|
|
6838
7025
|
return total;
|
|
@@ -7504,7 +7691,7 @@ var init_src11 = __esm({
|
|
|
7504
7691
|
});
|
|
7505
7692
|
|
|
7506
7693
|
// services/api/src/routes/jobs.ts
|
|
7507
|
-
import { join as
|
|
7694
|
+
import { join as join23 } from "node:path";
|
|
7508
7695
|
import { readFile as readFile20 } from "node:fs/promises";
|
|
7509
7696
|
function jobsRoutes() {
|
|
7510
7697
|
const start = async (c, kind) => {
|
|
@@ -7551,7 +7738,7 @@ ${user}` });
|
|
|
7551
7738
|
{ method: "GET", path: "/api/still", handle: async (c) => {
|
|
7552
7739
|
if (c.deps.mode !== "local") return c.json(404, { error: "Not found" });
|
|
7553
7740
|
const { store } = c.deps, id2 = (c.url.searchParams.get("file") ?? "").split("/")[0] ?? "";
|
|
7554
|
-
const baseDir = (await store.workdir(c.who, id2)).dir, raw = JSON.parse((await readFile20(
|
|
7741
|
+
const baseDir = (await store.workdir(c.who, id2)).dir, raw = JSON.parse((await readFile20(join23(baseDir, "composition.json"), "utf8")).replace(/^/, ""));
|
|
7555
7742
|
const composition = await attachCaptions(loadComposition(raw), baseDir), requested = c.url.searchParams.get("frame");
|
|
7556
7743
|
const frame = requested === null ? Math.floor(composition.durationFrames / 2) : integerIn(requested, 0, "frame", 0, Math.max(0, composition.durationFrames - 1));
|
|
7557
7744
|
c.send(200, "image/png", await renderStill(composition, { frame, baseDir }));
|
|
@@ -9379,7 +9566,7 @@ __export(ai_studio_exports, {
|
|
|
9379
9566
|
within: () => within
|
|
9380
9567
|
});
|
|
9381
9568
|
import { mkdir as mkdir18, readFile as readFile21 } from "node:fs/promises";
|
|
9382
|
-
import { join as
|
|
9569
|
+
import { join as join24, resolve as resolve19 } from "node:path";
|
|
9383
9570
|
import { fileURLToPath as fileURLToPath8 } from "node:url";
|
|
9384
9571
|
function jobEnvFor(store, requireProvider, providerInfo, local) {
|
|
9385
9572
|
return (who) => ({
|
|
@@ -9469,7 +9656,7 @@ var init_ai_studio = __esm({
|
|
|
9469
9656
|
},
|
|
9470
9657
|
async font(path) {
|
|
9471
9658
|
const face = builtinFontFaces.find((f) => `/fonts/${f.file}` === path);
|
|
9472
|
-
return face ? readFile21(
|
|
9659
|
+
return face ? readFile21(join24(builtinFontDir(), face.file)) : void 0;
|
|
9473
9660
|
}
|
|
9474
9661
|
};
|
|
9475
9662
|
studioApp = { dir: fileURLToPath8(new URL("../studio", import.meta.url)), fallbackPage: aiStudioPage };
|
|
@@ -9676,7 +9863,7 @@ __export(build_exports, {
|
|
|
9676
9863
|
buildReact: () => buildReact,
|
|
9677
9864
|
runBuild: () => runBuild
|
|
9678
9865
|
});
|
|
9679
|
-
import { existsSync as
|
|
9866
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
9680
9867
|
import { mkdir as mkdir20, readFile as readFile22, writeFile as writeFile21 } from "node:fs/promises";
|
|
9681
9868
|
import { basename as basename7, dirname as dirname17, resolve as resolve22 } from "node:path";
|
|
9682
9869
|
import { fileURLToPath as fileURLToPath9, pathToFileURL } from "node:url";
|
|
@@ -9687,7 +9874,7 @@ async function buildReact(source, output) {
|
|
|
9687
9874
|
} catch {
|
|
9688
9875
|
throw new Error("Building a React video needs esbuild, react and react-dom in your project: npm install --save-dev esbuild react react-dom");
|
|
9689
9876
|
}
|
|
9690
|
-
const reactEntry =
|
|
9877
|
+
const reactEntry = existsSync6(fileURLToPath9(new URL("./react.js", import.meta.url))) ? fileURLToPath9(new URL("./react.js", import.meta.url)) : fileURLToPath9(new URL("../../react/src/index.ts", import.meta.url));
|
|
9691
9878
|
const common = {
|
|
9692
9879
|
bundle: true,
|
|
9693
9880
|
jsx: "automatic",
|
|
@@ -10882,7 +11069,7 @@ import { resolve as resolve26, dirname as dirname20 } from "node:path";
|
|
|
10882
11069
|
init_src();
|
|
10883
11070
|
init_templates();
|
|
10884
11071
|
import { cp, mkdir as mkdir14, readdir as readdir6, readFile as readFile13, rm as rm5, writeFile as writeFile14 } from "node:fs/promises";
|
|
10885
|
-
import { basename as basename5, join as
|
|
11072
|
+
import { basename as basename5, join as join19, relative as relative2, resolve as resolve12 } from "node:path";
|
|
10886
11073
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
10887
11074
|
var starterTemplate = fileURLToPath5(new URL("../templates/starter", import.meta.url));
|
|
10888
11075
|
var skillDir = fileURLToPath5(new URL("../skills/ove", import.meta.url));
|
|
@@ -10896,24 +11083,24 @@ var isEmptyDir = async (dir) => {
|
|
|
10896
11083
|
}
|
|
10897
11084
|
};
|
|
10898
11085
|
async function listFiles(dir) {
|
|
10899
|
-
return (await readdir6(dir, { recursive: true, withFileTypes: true })).filter((e) => e.isFile()).map((e) =>
|
|
11086
|
+
return (await readdir6(dir, { recursive: true, withFileTypes: true })).filter((e) => e.isFile()).map((e) => join19(e.parentPath, e.name));
|
|
10900
11087
|
}
|
|
10901
11088
|
async function initProject(dir, options3 = {}) {
|
|
10902
11089
|
const target = resolve12(dir);
|
|
10903
11090
|
if (!options3.force && !await isEmptyDir(target)) throw new Error(`${target} is not empty; pass --force to write into it anyway`);
|
|
10904
|
-
const source = options3.template ?
|
|
11091
|
+
const source = options3.template ? join19(templatesDir, assertTemplateId(options3.template)) : starterTemplate;
|
|
10905
11092
|
await mkdir14(target, { recursive: true });
|
|
10906
11093
|
await cp(source, target, { recursive: true, force: true });
|
|
10907
|
-
await rm5(
|
|
10908
|
-
const compositionPath =
|
|
11094
|
+
await rm5(join19(target, "template.json"), { force: true });
|
|
11095
|
+
const compositionPath = join19(target, "composition.json");
|
|
10909
11096
|
const name2 = basename5(target);
|
|
10910
11097
|
const composition = JSON.parse((await readFile13(compositionPath, "utf8")).replace(/^\uFEFF/, ""));
|
|
10911
11098
|
composition.id = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "video";
|
|
10912
11099
|
composition.name = name2;
|
|
10913
11100
|
loadComposition(composition);
|
|
10914
11101
|
await writeFile14(compositionPath, JSON.stringify(composition, null, 2) + "\n");
|
|
10915
|
-
await writeFile14(
|
|
10916
|
-
if (options3.skill !== false) await cp(skillsRoot,
|
|
11102
|
+
await writeFile14(join19(target, "composition.schema.json"), JSON.stringify(compositionJsonSchema, null, 2) + "\n");
|
|
11103
|
+
if (options3.skill !== false) await cp(skillsRoot, join19(target, ".claude", "skills"), { recursive: true, force: true });
|
|
10917
11104
|
return (await listFiles(target)).map((file2) => relative2(target, file2)).sort();
|
|
10918
11105
|
}
|
|
10919
11106
|
|
|
@@ -10993,7 +11180,7 @@ init_src8();
|
|
|
10993
11180
|
import { createServer as createServer2 } from "node:http";
|
|
10994
11181
|
import { mkdtemp as mkdtemp3, readFile as readFile15, rm as rm6 } from "node:fs/promises";
|
|
10995
11182
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10996
|
-
import { join as
|
|
11183
|
+
import { join as join21 } from "node:path";
|
|
10997
11184
|
import { randomBytes } from "node:crypto";
|
|
10998
11185
|
async function startWorker(options3 = {}) {
|
|
10999
11186
|
const token = options3.token || randomBytes(18).toString("base64url"), host = options3.host ?? "127.0.0.1", log = options3.log ?? (() => {
|
|
@@ -11028,8 +11215,8 @@ async function startWorker(options3 = {}) {
|
|
|
11028
11215
|
if (job.preset && !x264Presets.includes(job.preset)) return send2(res, 400, "text/plain", "Unknown preset");
|
|
11029
11216
|
jobs++;
|
|
11030
11217
|
const run4 = queue.then(async () => {
|
|
11031
|
-
const dir = await mkdtemp3(
|
|
11032
|
-
const output =
|
|
11218
|
+
const dir = await mkdtemp3(join21(tmpdir4(), "ove-worker-"));
|
|
11219
|
+
const output = join21(dir, `chunk.${format}`);
|
|
11033
11220
|
const started = Date.now();
|
|
11034
11221
|
try {
|
|
11035
11222
|
await renderMedia(composition, { output, format, codec: job.codec, transparent: job.transparent, preset: job.preset, crf: job.crf, bitrate: job.bitrate, scale: job.scale, frameRange: range, concurrency, audio: false });
|
|
@@ -11063,8 +11250,7 @@ async function startWorker(options3 = {}) {
|
|
|
11063
11250
|
}
|
|
11064
11251
|
|
|
11065
11252
|
// packages/cli/src/index.ts
|
|
11066
|
-
import { spawnSync } from "node:child_process";
|
|
11067
|
-
import { createRequire as createRequire3 } from "node:module";
|
|
11253
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
11068
11254
|
try {
|
|
11069
11255
|
process.loadEnvFile?.();
|
|
11070
11256
|
} catch {
|
|
@@ -11264,7 +11450,7 @@ async function doctor() {
|
|
|
11264
11450
|
} catch (error) {
|
|
11265
11451
|
line(false, "FFmpeg", error.message);
|
|
11266
11452
|
}
|
|
11267
|
-
const probe2 =
|
|
11453
|
+
const probe2 = spawnSync2(ffprobePath(), ["-version"], { encoding: "utf8" });
|
|
11268
11454
|
line(!probe2.error && probe2.status === 0, "ffprobe", probe2.error ? "not found (install FFmpeg, or set OVE_FFPROBE_PATH)" : (probe2.stdout.split(/\r?\n/)[0] ?? "").trim());
|
|
11269
11455
|
try {
|
|
11270
11456
|
line(true, "Browser", await describeBrowser());
|
|
@@ -11309,10 +11495,8 @@ Press Ctrl+C to stop.`);
|
|
|
11309
11495
|
process.once("SIGTERM", stop);
|
|
11310
11496
|
} else if (command === "browser") {
|
|
11311
11497
|
if (file !== "install") throw new Error("Usage: ove browser install");
|
|
11312
|
-
const cli = createRequire3(import.meta.url).resolve("playwright/cli");
|
|
11313
11498
|
console.log("Downloading Playwright Chromium\u2026");
|
|
11314
|
-
|
|
11315
|
-
if (result.status !== 0) throw new Error("Chromium download failed; see the output above.");
|
|
11499
|
+
await ensureChromium();
|
|
11316
11500
|
} else if (command === "schema") {
|
|
11317
11501
|
const json = JSON.stringify(compositionJsonSchema, null, 2) + "\n";
|
|
11318
11502
|
const output = option4("--output");
|