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