@vibeframe/mcp-server 0.31.1 → 0.33.1
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/index.js +404 -174
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1464,22 +1464,108 @@ async function handleTimelineToolCall(name, args) {
|
|
|
1464
1464
|
// ../cli/src/commands/export.ts
|
|
1465
1465
|
import { Command } from "commander";
|
|
1466
1466
|
import { readFile as readFile2, access, stat } from "node:fs/promises";
|
|
1467
|
-
import { resolve as
|
|
1467
|
+
import { resolve as resolve4, basename } from "node:path";
|
|
1468
1468
|
import { spawn } from "node:child_process";
|
|
1469
|
+
import chalk2 from "chalk";
|
|
1470
|
+
import ora2 from "ora";
|
|
1471
|
+
init_exec_safe();
|
|
1472
|
+
|
|
1473
|
+
// ../cli/src/commands/output.ts
|
|
1469
1474
|
import chalk from "chalk";
|
|
1470
1475
|
import ora from "ora";
|
|
1471
|
-
|
|
1476
|
+
function usageError(msg, suggestion) {
|
|
1477
|
+
return { success: false, error: msg, code: "USAGE_ERROR", exitCode: 2 /* USAGE */, suggestion, retryable: false };
|
|
1478
|
+
}
|
|
1479
|
+
function notFoundError(path) {
|
|
1480
|
+
return { success: false, error: `File not found: ${path}`, code: "NOT_FOUND", exitCode: 3 /* NOT_FOUND */, retryable: false };
|
|
1481
|
+
}
|
|
1482
|
+
function generalError(msg, suggestion) {
|
|
1483
|
+
return { success: false, error: msg, code: "ERROR", exitCode: 1 /* GENERAL */, suggestion, retryable: false };
|
|
1484
|
+
}
|
|
1485
|
+
function exitWithError(err) {
|
|
1486
|
+
if (isJsonMode()) {
|
|
1487
|
+
console.error(JSON.stringify(err, null, 2));
|
|
1488
|
+
} else {
|
|
1489
|
+
console.error(chalk.red(`
|
|
1490
|
+
${err.error}`));
|
|
1491
|
+
if (err.suggestion) {
|
|
1492
|
+
console.error(chalk.dim(` ${err.suggestion}`));
|
|
1493
|
+
}
|
|
1494
|
+
console.error();
|
|
1495
|
+
}
|
|
1496
|
+
process.exit(err.exitCode);
|
|
1497
|
+
}
|
|
1498
|
+
function isJsonMode() {
|
|
1499
|
+
return process.env.VIBE_JSON_OUTPUT === "1";
|
|
1500
|
+
}
|
|
1501
|
+
function isQuietMode() {
|
|
1502
|
+
return process.env.VIBE_QUIET_OUTPUT === "1";
|
|
1503
|
+
}
|
|
1504
|
+
function outputResult(result) {
|
|
1505
|
+
if (isJsonMode()) {
|
|
1506
|
+
const fields = process.env.VIBE_OUTPUT_FIELDS;
|
|
1507
|
+
if (fields) {
|
|
1508
|
+
const keys = fields.split(",").map((k) => k.trim());
|
|
1509
|
+
const filtered = {};
|
|
1510
|
+
for (const key of keys) {
|
|
1511
|
+
if (key in result) filtered[key] = result[key];
|
|
1512
|
+
}
|
|
1513
|
+
if ("success" in result) filtered.success = result.success;
|
|
1514
|
+
console.log(JSON.stringify(filtered, null, 2));
|
|
1515
|
+
} else {
|
|
1516
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1517
|
+
}
|
|
1518
|
+
} else if (isQuietMode()) {
|
|
1519
|
+
const primary = result.output ?? result.path ?? result.url ?? result.id ?? result.result;
|
|
1520
|
+
if (primary !== void 0) console.log(String(primary));
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// ../cli/src/commands/validate.ts
|
|
1525
|
+
import { resolve as resolve3, relative, isAbsolute } from "node:path";
|
|
1526
|
+
function validateOutputPath(path, cwd = process.cwd()) {
|
|
1527
|
+
rejectControlChars(path);
|
|
1528
|
+
if (isAbsolute(path)) {
|
|
1529
|
+
return resolve3(path);
|
|
1530
|
+
}
|
|
1531
|
+
const resolved = resolve3(cwd, path);
|
|
1532
|
+
const rel = relative(cwd, resolved);
|
|
1533
|
+
if (rel.startsWith("..")) {
|
|
1534
|
+
throw new Error(
|
|
1535
|
+
`Output path "${path}" escapes the working directory. Use a path within "${cwd}".`
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
return resolved;
|
|
1539
|
+
}
|
|
1540
|
+
function rejectControlChars(input) {
|
|
1541
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(input)) {
|
|
1542
|
+
throw new Error("Input contains invalid control characters.");
|
|
1543
|
+
}
|
|
1544
|
+
return input;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// ../cli/src/commands/export.ts
|
|
1472
1548
|
async function resolveProjectPath(inputPath) {
|
|
1473
|
-
const filePath =
|
|
1549
|
+
const filePath = resolve4(process.cwd(), inputPath);
|
|
1474
1550
|
try {
|
|
1475
1551
|
const stats = await stat(filePath);
|
|
1476
1552
|
if (stats.isDirectory()) {
|
|
1477
|
-
return
|
|
1553
|
+
return resolve4(filePath, "project.vibe.json");
|
|
1478
1554
|
}
|
|
1479
1555
|
} catch {
|
|
1480
1556
|
}
|
|
1481
1557
|
return filePath;
|
|
1482
1558
|
}
|
|
1559
|
+
async function getMediaDuration(filePath, mediaType, defaultImageDuration = 5) {
|
|
1560
|
+
if (mediaType === "image") {
|
|
1561
|
+
return defaultImageDuration;
|
|
1562
|
+
}
|
|
1563
|
+
try {
|
|
1564
|
+
return await ffprobeDuration(filePath);
|
|
1565
|
+
} catch {
|
|
1566
|
+
return defaultImageDuration;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1483
1569
|
async function checkHasAudio(filePath) {
|
|
1484
1570
|
try {
|
|
1485
1571
|
const { stdout } = await execSafe("ffprobe", [
|
|
@@ -1519,16 +1605,24 @@ async function runExport(projectPath, outputPath, options = {}) {
|
|
|
1519
1605
|
message: "Project has no clips to export"
|
|
1520
1606
|
};
|
|
1521
1607
|
}
|
|
1522
|
-
const finalOutputPath =
|
|
1608
|
+
const finalOutputPath = resolve4(process.cwd(), outputPath);
|
|
1523
1609
|
const presetSettings = getPresetSettings(preset, summary.aspectRatio);
|
|
1524
1610
|
const clips = project.getClips().sort((a, b) => a.startTime - b.startTime);
|
|
1525
1611
|
const sources = project.getSources();
|
|
1526
1612
|
const sourceAudioMap = /* @__PURE__ */ new Map();
|
|
1613
|
+
const sourceActualDurationMap = /* @__PURE__ */ new Map();
|
|
1527
1614
|
for (const clip of clips) {
|
|
1528
1615
|
const source = sources.find((s) => s.id === clip.sourceId);
|
|
1529
1616
|
if (source) {
|
|
1530
1617
|
try {
|
|
1531
1618
|
await access(source.url);
|
|
1619
|
+
if (!sourceActualDurationMap.has(source.id)) {
|
|
1620
|
+
try {
|
|
1621
|
+
const dur = await getMediaDuration(source.url, source.type);
|
|
1622
|
+
if (dur > 0) sourceActualDurationMap.set(source.id, dur);
|
|
1623
|
+
} catch {
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1532
1626
|
if (source.type === "video" && !sourceAudioMap.has(source.id)) {
|
|
1533
1627
|
sourceAudioMap.set(source.id, await checkHasAudio(source.url));
|
|
1534
1628
|
}
|
|
@@ -1540,7 +1634,7 @@ async function runExport(projectPath, outputPath, options = {}) {
|
|
|
1540
1634
|
}
|
|
1541
1635
|
}
|
|
1542
1636
|
}
|
|
1543
|
-
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, finalOutputPath, { overwrite, format, gapFill }, sourceAudioMap);
|
|
1637
|
+
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, finalOutputPath, { overwrite, format, gapFill }, sourceAudioMap, sourceActualDurationMap);
|
|
1544
1638
|
await runFFmpegProcess(ffmpegPath, ffmpegArgs, () => {
|
|
1545
1639
|
});
|
|
1546
1640
|
return {
|
|
@@ -1560,24 +1654,37 @@ var exportCommand = new Command("export").description("Export project to video f
|
|
|
1560
1654
|
"-p, --preset <preset>",
|
|
1561
1655
|
"Quality preset (draft, standard, high, ultra)",
|
|
1562
1656
|
"standard"
|
|
1563
|
-
).option("-y, --overwrite", "Overwrite output file if exists", false).option("-g, --gap-fill <strategy>", "Gap filling strategy (black, extend)", "extend").addHelpText("after", `
|
|
1657
|
+
).option("-y, --overwrite", "Overwrite output file if exists", false).option("-g, --gap-fill <strategy>", "Gap filling strategy (black, extend)", "extend").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
|
|
1564
1658
|
Examples:
|
|
1565
1659
|
$ vibe export project.vibe.json -o output.mp4
|
|
1566
1660
|
$ vibe export project.vibe.json -o output.mp4 -p high -y
|
|
1567
1661
|
$ vibe export project.vibe.json -o output.webm -f webm
|
|
1568
1662
|
|
|
1569
1663
|
No API keys needed. Requires FFmpeg.`).action(async (projectPath, options) => {
|
|
1570
|
-
const spinner =
|
|
1664
|
+
const spinner = ora2("Checking FFmpeg...").start();
|
|
1571
1665
|
try {
|
|
1666
|
+
if (options.output) {
|
|
1667
|
+
validateOutputPath(options.output);
|
|
1668
|
+
}
|
|
1669
|
+
if (options.dryRun) {
|
|
1670
|
+
outputResult({
|
|
1671
|
+
dryRun: true,
|
|
1672
|
+
command: "export",
|
|
1673
|
+
params: {
|
|
1674
|
+
project: projectPath,
|
|
1675
|
+
output: options.output || null,
|
|
1676
|
+
format: options.format,
|
|
1677
|
+
preset: options.preset,
|
|
1678
|
+
overwrite: options.overwrite,
|
|
1679
|
+
gapFill: options.gapFill
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1572
1684
|
const ffmpegPath = await findFFmpeg();
|
|
1573
1685
|
if (!ffmpegPath) {
|
|
1574
|
-
spinner.fail(
|
|
1575
|
-
|
|
1576
|
-
console.error(chalk.yellow("Please install FFmpeg:"));
|
|
1577
|
-
console.error(chalk.dim(" macOS: brew install ffmpeg"));
|
|
1578
|
-
console.error(chalk.dim(" Ubuntu: sudo apt install ffmpeg"));
|
|
1579
|
-
console.error(chalk.dim(" Windows: winget install ffmpeg"));
|
|
1580
|
-
process.exit(1);
|
|
1686
|
+
spinner.fail("FFmpeg not found");
|
|
1687
|
+
exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS), apt install ffmpeg (Linux), or winget install ffmpeg (Windows)"));
|
|
1581
1688
|
}
|
|
1582
1689
|
spinner.text = "Loading project...";
|
|
1583
1690
|
const filePath = await resolveProjectPath(projectPath);
|
|
@@ -1586,10 +1693,10 @@ No API keys needed. Requires FFmpeg.`).action(async (projectPath, options) => {
|
|
|
1586
1693
|
const project = Project.fromJSON(data);
|
|
1587
1694
|
const summary = project.getSummary();
|
|
1588
1695
|
if (summary.clipCount === 0) {
|
|
1589
|
-
spinner.fail(
|
|
1590
|
-
|
|
1696
|
+
spinner.fail("Project has no clips to export");
|
|
1697
|
+
exitWithError(usageError("Project has no clips to export"));
|
|
1591
1698
|
}
|
|
1592
|
-
const outputPath = options.output ?
|
|
1699
|
+
const outputPath = options.output ? resolve4(process.cwd(), options.output) : resolve4(
|
|
1593
1700
|
process.cwd(),
|
|
1594
1701
|
`${basename(projectPath, ".vibe.json")}.${options.format}`
|
|
1595
1702
|
);
|
|
@@ -1598,23 +1705,31 @@ No API keys needed. Requires FFmpeg.`).action(async (projectPath, options) => {
|
|
|
1598
1705
|
const sources = project.getSources();
|
|
1599
1706
|
spinner.text = "Verifying source files...";
|
|
1600
1707
|
const sourceAudioMap = /* @__PURE__ */ new Map();
|
|
1708
|
+
const sourceActualDurationMap = /* @__PURE__ */ new Map();
|
|
1601
1709
|
for (const clip of clips) {
|
|
1602
1710
|
const source = sources.find((s) => s.id === clip.sourceId);
|
|
1603
1711
|
if (source) {
|
|
1604
1712
|
try {
|
|
1605
1713
|
await access(source.url);
|
|
1714
|
+
if (!sourceActualDurationMap.has(source.id)) {
|
|
1715
|
+
try {
|
|
1716
|
+
const dur = await getMediaDuration(source.url, source.type);
|
|
1717
|
+
if (dur > 0) sourceActualDurationMap.set(source.id, dur);
|
|
1718
|
+
} catch {
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1606
1721
|
if (source.type === "video" && !sourceAudioMap.has(source.id)) {
|
|
1607
1722
|
sourceAudioMap.set(source.id, await checkHasAudio(source.url));
|
|
1608
1723
|
}
|
|
1609
1724
|
} catch {
|
|
1610
|
-
spinner.fail(
|
|
1611
|
-
|
|
1725
|
+
spinner.fail(`Source file not found: ${source.url}`);
|
|
1726
|
+
exitWithError(notFoundError(source.url));
|
|
1612
1727
|
}
|
|
1613
1728
|
}
|
|
1614
1729
|
}
|
|
1615
1730
|
spinner.text = "Building export command...";
|
|
1616
1731
|
const gapFillStrategy = options.gapFill === "black" ? "black" : "extend";
|
|
1617
|
-
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, outputPath, { ...options, gapFill: gapFillStrategy }, sourceAudioMap);
|
|
1732
|
+
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, outputPath, { ...options, gapFill: gapFillStrategy }, sourceAudioMap, sourceActualDurationMap);
|
|
1618
1733
|
if (process.env.DEBUG) {
|
|
1619
1734
|
console.log("\nFFmpeg command:");
|
|
1620
1735
|
console.log("ffmpeg", ffmpegArgs.join(" "));
|
|
@@ -1624,23 +1739,18 @@ No API keys needed. Requires FFmpeg.`).action(async (projectPath, options) => {
|
|
|
1624
1739
|
await runFFmpegProcess(ffmpegPath, ffmpegArgs, (progress) => {
|
|
1625
1740
|
spinner.text = `Encoding... ${progress}%`;
|
|
1626
1741
|
});
|
|
1627
|
-
spinner.succeed(
|
|
1742
|
+
spinner.succeed(chalk2.green(`Exported: ${outputPath}`));
|
|
1628
1743
|
console.log();
|
|
1629
|
-
console.log(
|
|
1630
|
-
console.log(
|
|
1631
|
-
console.log(
|
|
1632
|
-
console.log(
|
|
1633
|
-
console.log(
|
|
1744
|
+
console.log(chalk2.dim(" Duration:"), `${summary.duration.toFixed(1)}s`);
|
|
1745
|
+
console.log(chalk2.dim(" Clips:"), summary.clipCount);
|
|
1746
|
+
console.log(chalk2.dim(" Format:"), options.format);
|
|
1747
|
+
console.log(chalk2.dim(" Preset:"), options.preset);
|
|
1748
|
+
console.log(chalk2.dim(" Resolution:"), presetSettings.resolution);
|
|
1634
1749
|
console.log();
|
|
1635
1750
|
} catch (error) {
|
|
1636
|
-
spinner.fail(
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
if (process.env.DEBUG) {
|
|
1640
|
-
console.error(error.stack);
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
process.exit(1);
|
|
1751
|
+
spinner.fail("Export failed");
|
|
1752
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1753
|
+
exitWithError(generalError(`Export failed: ${msg}`));
|
|
1644
1754
|
}
|
|
1645
1755
|
});
|
|
1646
1756
|
async function findFFmpeg() {
|
|
@@ -1743,7 +1853,7 @@ function createGapFillPlans(gaps, clips, sources) {
|
|
|
1743
1853
|
return { gap, fills };
|
|
1744
1854
|
});
|
|
1745
1855
|
}
|
|
1746
|
-
function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, sourceAudioMap = /* @__PURE__ */ new Map()) {
|
|
1856
|
+
function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, sourceAudioMap = /* @__PURE__ */ new Map(), sourceActualDurationMap = /* @__PURE__ */ new Map()) {
|
|
1747
1857
|
const args = [];
|
|
1748
1858
|
if (options.overwrite) {
|
|
1749
1859
|
args.push("-y");
|
|
@@ -1828,6 +1938,12 @@ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, so
|
|
|
1828
1938
|
const trimStart = clip.sourceStartOffset;
|
|
1829
1939
|
const trimEnd = clip.sourceStartOffset + clip.duration;
|
|
1830
1940
|
videoFilter = `[${srcIdx}:v]trim=start=${trimStart}:end=${trimEnd},setpts=PTS-STARTPTS`;
|
|
1941
|
+
const sourceDuration = sourceActualDurationMap.get(source.id) || source.duration || 0;
|
|
1942
|
+
const availableDuration = sourceDuration - clip.sourceStartOffset;
|
|
1943
|
+
if (availableDuration > 0 && availableDuration < clip.duration - 0.1) {
|
|
1944
|
+
const padDuration = clip.duration - availableDuration;
|
|
1945
|
+
videoFilter += `,tpad=stop_mode=clone:stop_duration=${padDuration.toFixed(3)}`;
|
|
1946
|
+
}
|
|
1831
1947
|
}
|
|
1832
1948
|
videoFilter += `,scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
|
|
1833
1949
|
for (const effect of clip.effects || []) {
|
|
@@ -1862,62 +1978,95 @@ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, so
|
|
|
1862
1978
|
videoStreamIdx++;
|
|
1863
1979
|
}
|
|
1864
1980
|
}
|
|
1865
|
-
const
|
|
1866
|
-
const audioSegments = [];
|
|
1981
|
+
const audioTrackMap = /* @__PURE__ */ new Map();
|
|
1867
1982
|
for (const clip of audioClips) {
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1983
|
+
const trackId = clip.trackId || "audio-track-1";
|
|
1984
|
+
if (!audioTrackMap.has(trackId)) {
|
|
1985
|
+
audioTrackMap.set(trackId, []);
|
|
1986
|
+
}
|
|
1987
|
+
audioTrackMap.get(trackId).push(clip);
|
|
1872
1988
|
}
|
|
1873
|
-
|
|
1874
|
-
const
|
|
1989
|
+
const videoDuration = videoClips.length > 0 ? Math.max(...videoClips.map((c) => c.startTime + c.duration)) : 0;
|
|
1990
|
+
const audioDuration = audioClips.length > 0 ? Math.max(...audioClips.map((c) => c.startTime + c.duration)) : 0;
|
|
1991
|
+
const timelineDuration = totalDuration || Math.max(videoDuration, audioDuration);
|
|
1992
|
+
const trackOutputLabels = [];
|
|
1875
1993
|
let audioStreamIdx = 0;
|
|
1876
|
-
for (const
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1994
|
+
for (const [, trackClips] of audioTrackMap) {
|
|
1995
|
+
const sorted = [...trackClips].sort((a, b) => a.startTime - b.startTime);
|
|
1996
|
+
const trackGaps = detectTimelineGaps(sorted, timelineDuration);
|
|
1997
|
+
const segments = [];
|
|
1998
|
+
for (const clip of sorted) {
|
|
1999
|
+
segments.push({ type: "clip", clip, startTime: clip.startTime });
|
|
2000
|
+
}
|
|
2001
|
+
for (const gap of trackGaps) {
|
|
2002
|
+
segments.push({ type: "gap", gap, startTime: gap.start });
|
|
2003
|
+
}
|
|
2004
|
+
segments.sort((a, b) => a.startTime - b.startTime);
|
|
2005
|
+
const segmentLabels = [];
|
|
2006
|
+
for (const segment of segments) {
|
|
2007
|
+
if (segment.type === "clip" && segment.clip) {
|
|
2008
|
+
const clip = segment.clip;
|
|
2009
|
+
const source = sources.find((s) => s.id === clip.sourceId);
|
|
2010
|
+
if (!source) continue;
|
|
2011
|
+
const srcIdx = sourceMap.get(source.id);
|
|
2012
|
+
if (srcIdx === void 0) continue;
|
|
2013
|
+
const hasAudio = source.type === "audio" || sourceAudioMap.get(source.id) === true;
|
|
2014
|
+
let audioFilter;
|
|
2015
|
+
if (hasAudio) {
|
|
2016
|
+
const audioTrimStart = clip.sourceStartOffset;
|
|
2017
|
+
const audioTrimEnd = clip.sourceStartOffset + clip.duration;
|
|
2018
|
+
const sourceDuration = source.duration || 0;
|
|
2019
|
+
const clipDuration = clip.duration;
|
|
2020
|
+
if (source.type === "audio" && sourceDuration > clipDuration && audioTrimStart === 0) {
|
|
2021
|
+
const tempo = sourceDuration / clipDuration;
|
|
2022
|
+
if (tempo <= 2) {
|
|
2023
|
+
audioFilter = `[${srcIdx}:a]atempo=${tempo.toFixed(4)},asetpts=PTS-STARTPTS`;
|
|
2024
|
+
} else {
|
|
2025
|
+
audioFilter = `[${srcIdx}:a]atrim=start=${audioTrimStart}:end=${audioTrimEnd},asetpts=PTS-STARTPTS`;
|
|
2026
|
+
}
|
|
1894
2027
|
} else {
|
|
1895
2028
|
audioFilter = `[${srcIdx}:a]atrim=start=${audioTrimStart}:end=${audioTrimEnd},asetpts=PTS-STARTPTS`;
|
|
1896
2029
|
}
|
|
1897
2030
|
} else {
|
|
1898
|
-
audioFilter = `
|
|
2031
|
+
audioFilter = `anullsrc=r=48000:cl=stereo,atrim=0:${clip.duration},asetpts=PTS-STARTPTS`;
|
|
1899
2032
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2033
|
+
const clipVolume = clip.volume;
|
|
2034
|
+
if (clipVolume !== void 0 && clipVolume !== 1) {
|
|
2035
|
+
audioFilter += `,volume=${clipVolume.toFixed(2)}`;
|
|
2036
|
+
}
|
|
2037
|
+
for (const effect of clip.effects || []) {
|
|
2038
|
+
if (effect.type === "fadeIn") {
|
|
2039
|
+
audioFilter += `,afade=t=in:st=0:d=${effect.duration}`;
|
|
2040
|
+
} else if (effect.type === "fadeOut") {
|
|
2041
|
+
const fadeStart = clip.duration - effect.duration;
|
|
2042
|
+
audioFilter += `,afade=t=out:st=${fadeStart}:d=${effect.duration}`;
|
|
2043
|
+
} else if (effect.type === "volume" && effect.params?.level !== void 0) {
|
|
2044
|
+
audioFilter += `,volume=${effect.params.level}`;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
const label = `a${audioStreamIdx}`;
|
|
2048
|
+
audioFilter += `[${label}]`;
|
|
2049
|
+
filterParts.push(audioFilter);
|
|
2050
|
+
segmentLabels.push(`[${label}]`);
|
|
2051
|
+
audioStreamIdx++;
|
|
2052
|
+
} else if (segment.type === "gap" && segment.gap) {
|
|
2053
|
+
const gapDuration = segment.gap.end - segment.gap.start;
|
|
2054
|
+
if (gapDuration > 1e-3) {
|
|
2055
|
+
const label = `a${audioStreamIdx}`;
|
|
2056
|
+
filterParts.push(`anullsrc=r=48000:cl=stereo,atrim=0:${gapDuration.toFixed(4)},asetpts=PTS-STARTPTS[${label}]`);
|
|
2057
|
+
segmentLabels.push(`[${label}]`);
|
|
2058
|
+
audioStreamIdx++;
|
|
1909
2059
|
}
|
|
1910
2060
|
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
const
|
|
1918
|
-
filterParts.push(
|
|
1919
|
-
|
|
1920
|
-
audioStreamIdx++;
|
|
2061
|
+
}
|
|
2062
|
+
if (segmentLabels.length > 1) {
|
|
2063
|
+
const trackLabel = `atrack${trackOutputLabels.length}`;
|
|
2064
|
+
filterParts.push(`${segmentLabels.join("")}concat=n=${segmentLabels.length}:v=0:a=1[${trackLabel}]`);
|
|
2065
|
+
trackOutputLabels.push(`[${trackLabel}]`);
|
|
2066
|
+
} else if (segmentLabels.length === 1) {
|
|
2067
|
+
const trackLabel = `atrack${trackOutputLabels.length}`;
|
|
2068
|
+
filterParts.push(`${segmentLabels[0]}acopy[${trackLabel}]`);
|
|
2069
|
+
trackOutputLabels.push(`[${trackLabel}]`);
|
|
1921
2070
|
}
|
|
1922
2071
|
}
|
|
1923
2072
|
if (videoStreams.length > 1) {
|
|
@@ -1927,16 +2076,16 @@ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, so
|
|
|
1927
2076
|
} else if (videoStreams.length === 1) {
|
|
1928
2077
|
filterParts.push(`${videoStreams[0]}copy[outv]`);
|
|
1929
2078
|
}
|
|
1930
|
-
if (
|
|
2079
|
+
if (trackOutputLabels.length > 1) {
|
|
1931
2080
|
filterParts.push(
|
|
1932
|
-
`${
|
|
2081
|
+
`${trackOutputLabels.join("")}amix=inputs=${trackOutputLabels.length}:duration=longest:normalize=0[outa]`
|
|
1933
2082
|
);
|
|
1934
|
-
} else if (
|
|
1935
|
-
filterParts.push(`${
|
|
2083
|
+
} else if (trackOutputLabels.length === 1) {
|
|
2084
|
+
filterParts.push(`${trackOutputLabels[0]}acopy[outa]`);
|
|
1936
2085
|
}
|
|
1937
2086
|
args.push("-filter_complex", filterParts.join(";"));
|
|
1938
2087
|
args.push("-map", "[outv]");
|
|
1939
|
-
if (
|
|
2088
|
+
if (trackOutputLabels.length > 0) {
|
|
1940
2089
|
args.push("-map", "[outa]");
|
|
1941
2090
|
}
|
|
1942
2091
|
args.push(...presetSettings.ffmpegArgs);
|
|
@@ -1944,7 +2093,7 @@ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, so
|
|
|
1944
2093
|
return args;
|
|
1945
2094
|
}
|
|
1946
2095
|
function runFFmpegProcess(ffmpegPath, args, onProgress) {
|
|
1947
|
-
return new Promise((
|
|
2096
|
+
return new Promise((resolve13, reject) => {
|
|
1948
2097
|
const ffmpeg = spawn(ffmpegPath, args, {
|
|
1949
2098
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1950
2099
|
});
|
|
@@ -1968,7 +2117,7 @@ function runFFmpegProcess(ffmpegPath, args, onProgress) {
|
|
|
1968
2117
|
});
|
|
1969
2118
|
ffmpeg.on("close", (code) => {
|
|
1970
2119
|
if (code === 0) {
|
|
1971
|
-
|
|
2120
|
+
resolve13();
|
|
1972
2121
|
} else {
|
|
1973
2122
|
const errorMatch = stderr.match(/Error.*$/m);
|
|
1974
2123
|
const errorMsg = errorMatch ? errorMatch[0] : `FFmpeg exited with code ${code}`;
|
|
@@ -2108,7 +2257,7 @@ async function handleExportToolCall(name, args) {
|
|
|
2108
2257
|
}
|
|
2109
2258
|
|
|
2110
2259
|
// ../cli/src/commands/ai-edit.ts
|
|
2111
|
-
import { resolve as
|
|
2260
|
+
import { resolve as resolve7, dirname, basename as basename2, extname, join as join2 } from "node:path";
|
|
2112
2261
|
import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir3 } from "node:fs/promises";
|
|
2113
2262
|
import { existsSync as existsSync2 } from "node:fs";
|
|
2114
2263
|
|
|
@@ -2913,7 +3062,7 @@ var GeminiProvider = class {
|
|
|
2913
3062
|
* Sleep helper
|
|
2914
3063
|
*/
|
|
2915
3064
|
sleep(ms) {
|
|
2916
|
-
return new Promise((
|
|
3065
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
2917
3066
|
}
|
|
2918
3067
|
/**
|
|
2919
3068
|
* Extend a previously generated Veo video
|
|
@@ -6756,7 +6905,7 @@ var RunwayProvider = class {
|
|
|
6756
6905
|
* Sleep helper
|
|
6757
6906
|
*/
|
|
6758
6907
|
sleep(ms) {
|
|
6759
|
-
return new Promise((
|
|
6908
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
6760
6909
|
}
|
|
6761
6910
|
};
|
|
6762
6911
|
var runwayProvider = new RunwayProvider();
|
|
@@ -7172,7 +7321,7 @@ var KlingProvider = class {
|
|
|
7172
7321
|
* Sleep helper
|
|
7173
7322
|
*/
|
|
7174
7323
|
sleep(ms) {
|
|
7175
|
-
return new Promise((
|
|
7324
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
7176
7325
|
}
|
|
7177
7326
|
};
|
|
7178
7327
|
var klingProvider = new KlingProvider();
|
|
@@ -7467,7 +7616,7 @@ var GrokProvider = class {
|
|
|
7467
7616
|
}
|
|
7468
7617
|
}
|
|
7469
7618
|
sleep(ms) {
|
|
7470
|
-
return new Promise((
|
|
7619
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
7471
7620
|
}
|
|
7472
7621
|
};
|
|
7473
7622
|
var grokProvider = new GrokProvider();
|
|
@@ -7726,7 +7875,7 @@ var ReplicateProvider = class {
|
|
|
7726
7875
|
* Sleep helper
|
|
7727
7876
|
*/
|
|
7728
7877
|
sleep(ms) {
|
|
7729
|
-
return new Promise((
|
|
7878
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
7730
7879
|
}
|
|
7731
7880
|
/**
|
|
7732
7881
|
* Generate music from text prompt using MusicGen
|
|
@@ -8110,12 +8259,12 @@ var replicateProvider = new ReplicateProvider();
|
|
|
8110
8259
|
// ../cli/src/utils/api-key.ts
|
|
8111
8260
|
import { createInterface } from "node:readline";
|
|
8112
8261
|
import { readFile as readFile4, writeFile as writeFile3, access as access3 } from "node:fs/promises";
|
|
8113
|
-
import { resolve as
|
|
8262
|
+
import { resolve as resolve6 } from "node:path";
|
|
8114
8263
|
import { config } from "dotenv";
|
|
8115
|
-
import
|
|
8264
|
+
import chalk3 from "chalk";
|
|
8116
8265
|
|
|
8117
8266
|
// ../cli/src/config/index.ts
|
|
8118
|
-
import { resolve as
|
|
8267
|
+
import { resolve as resolve5 } from "node:path";
|
|
8119
8268
|
import { homedir } from "node:os";
|
|
8120
8269
|
import { readFile as readFile3, writeFile as writeFile2, mkdir, access as access2 } from "node:fs/promises";
|
|
8121
8270
|
import { parse, stringify } from "yaml";
|
|
@@ -8151,8 +8300,8 @@ function createDefaultConfig() {
|
|
|
8151
8300
|
}
|
|
8152
8301
|
|
|
8153
8302
|
// ../cli/src/config/index.ts
|
|
8154
|
-
var CONFIG_DIR =
|
|
8155
|
-
var CONFIG_PATH =
|
|
8303
|
+
var CONFIG_DIR = resolve5(homedir(), ".vibeframe");
|
|
8304
|
+
var CONFIG_PATH = resolve5(CONFIG_DIR, "config.yaml");
|
|
8156
8305
|
async function loadConfig() {
|
|
8157
8306
|
try {
|
|
8158
8307
|
await access2(CONFIG_PATH);
|
|
@@ -8185,20 +8334,20 @@ async function getApiKeyFromConfig(providerKey) {
|
|
|
8185
8334
|
|
|
8186
8335
|
// ../cli/src/utils/api-key.ts
|
|
8187
8336
|
function loadEnv() {
|
|
8188
|
-
config({ path:
|
|
8337
|
+
config({ path: resolve6(process.cwd(), ".env"), debug: false, quiet: true });
|
|
8189
8338
|
const monorepoRoot = findMonorepoRoot();
|
|
8190
8339
|
if (monorepoRoot && monorepoRoot !== process.cwd()) {
|
|
8191
|
-
config({ path:
|
|
8340
|
+
config({ path: resolve6(monorepoRoot, ".env"), debug: false, quiet: true });
|
|
8192
8341
|
}
|
|
8193
8342
|
}
|
|
8194
8343
|
function findMonorepoRoot() {
|
|
8195
8344
|
let dir = process.cwd();
|
|
8196
8345
|
while (dir !== "/") {
|
|
8197
8346
|
try {
|
|
8198
|
-
__require.resolve(
|
|
8347
|
+
__require.resolve(resolve6(dir, "pnpm-workspace.yaml"));
|
|
8199
8348
|
return dir;
|
|
8200
8349
|
} catch {
|
|
8201
|
-
dir =
|
|
8350
|
+
dir = resolve6(dir, "..");
|
|
8202
8351
|
}
|
|
8203
8352
|
}
|
|
8204
8353
|
return null;
|
|
@@ -8208,7 +8357,7 @@ async function prompt(question, hidden = false) {
|
|
|
8208
8357
|
input: process.stdin,
|
|
8209
8358
|
output: process.stdout
|
|
8210
8359
|
});
|
|
8211
|
-
return new Promise((
|
|
8360
|
+
return new Promise((resolve13) => {
|
|
8212
8361
|
if (hidden && process.stdin.isTTY) {
|
|
8213
8362
|
process.stdout.write(question);
|
|
8214
8363
|
let input = "";
|
|
@@ -8222,7 +8371,7 @@ async function prompt(question, hidden = false) {
|
|
|
8222
8371
|
process.stdin.removeListener("data", onData);
|
|
8223
8372
|
process.stdout.write("\n");
|
|
8224
8373
|
rl.close();
|
|
8225
|
-
|
|
8374
|
+
resolve13(input);
|
|
8226
8375
|
} else if (char === "") {
|
|
8227
8376
|
process.exit(1);
|
|
8228
8377
|
} else if (char === "\x7F" || char === "\b") {
|
|
@@ -8237,7 +8386,7 @@ async function prompt(question, hidden = false) {
|
|
|
8237
8386
|
} else {
|
|
8238
8387
|
rl.question(question, (answer) => {
|
|
8239
8388
|
rl.close();
|
|
8240
|
-
|
|
8389
|
+
resolve13(answer);
|
|
8241
8390
|
});
|
|
8242
8391
|
}
|
|
8243
8392
|
});
|
|
@@ -8250,9 +8399,11 @@ async function getApiKey(envVar, providerName, optionValue) {
|
|
|
8250
8399
|
ANTHROPIC_API_KEY: "anthropic",
|
|
8251
8400
|
OPENAI_API_KEY: "openai",
|
|
8252
8401
|
GOOGLE_API_KEY: "google",
|
|
8402
|
+
XAI_API_KEY: "xai",
|
|
8253
8403
|
ELEVENLABS_API_KEY: "elevenlabs",
|
|
8254
8404
|
RUNWAY_API_SECRET: "runway",
|
|
8255
8405
|
KLING_API_KEY: "kling",
|
|
8406
|
+
OPENROUTER_API_KEY: "openrouter",
|
|
8256
8407
|
IMGBB_API_KEY: "imgbb",
|
|
8257
8408
|
REPLICATE_API_TOKEN: "replicate"
|
|
8258
8409
|
};
|
|
@@ -8272,22 +8423,22 @@ async function getApiKey(envVar, providerName, optionValue) {
|
|
|
8272
8423
|
return null;
|
|
8273
8424
|
}
|
|
8274
8425
|
console.log();
|
|
8275
|
-
console.log(
|
|
8276
|
-
console.log(
|
|
8426
|
+
console.log(chalk3.yellow(`${providerName} API key not found.`));
|
|
8427
|
+
console.log(chalk3.dim(`Set ${envVar} in .env (current directory), run 'vibe setup', or enter below.`));
|
|
8277
8428
|
console.log();
|
|
8278
|
-
const apiKey = await prompt(
|
|
8429
|
+
const apiKey = await prompt(chalk3.cyan(`Enter ${providerName} API key: `), true);
|
|
8279
8430
|
if (!apiKey || apiKey.trim() === "") {
|
|
8280
8431
|
return null;
|
|
8281
8432
|
}
|
|
8282
|
-
const save = await prompt(
|
|
8433
|
+
const save = await prompt(chalk3.cyan("Save to .env for future use? (y/N): "));
|
|
8283
8434
|
if (save.toLowerCase() === "y" || save.toLowerCase() === "yes") {
|
|
8284
8435
|
await saveApiKeyToEnv(envVar, apiKey.trim());
|
|
8285
|
-
console.log(
|
|
8436
|
+
console.log(chalk3.green("API key saved to .env"));
|
|
8286
8437
|
}
|
|
8287
8438
|
return apiKey.trim();
|
|
8288
8439
|
}
|
|
8289
8440
|
async function saveApiKeyToEnv(envVar, apiKey) {
|
|
8290
|
-
const envPath =
|
|
8441
|
+
const envPath = resolve6(process.cwd(), ".env");
|
|
8291
8442
|
let content = "";
|
|
8292
8443
|
try {
|
|
8293
8444
|
await access3(envPath);
|
|
@@ -9256,8 +9407,8 @@ async function applyTextOverlays(options) {
|
|
|
9256
9407
|
if (!texts || texts.length === 0) {
|
|
9257
9408
|
return { success: false, error: "No texts provided" };
|
|
9258
9409
|
}
|
|
9259
|
-
const absVideoPath =
|
|
9260
|
-
const absOutputPath =
|
|
9410
|
+
const absVideoPath = resolve7(process.cwd(), videoPath);
|
|
9411
|
+
const absOutputPath = resolve7(process.cwd(), outputPath);
|
|
9261
9412
|
if (!existsSync2(absVideoPath)) {
|
|
9262
9413
|
return { success: false, error: `Video not found: ${absVideoPath}` };
|
|
9263
9414
|
}
|
|
@@ -9628,10 +9779,10 @@ async function handleAiEditingToolCall(name, args) {
|
|
|
9628
9779
|
|
|
9629
9780
|
// ../cli/src/commands/ai-analyze.ts
|
|
9630
9781
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
9631
|
-
import { extname as extname2, resolve as
|
|
9782
|
+
import { extname as extname2, resolve as resolve8 } from "node:path";
|
|
9632
9783
|
import { existsSync as existsSync3 } from "node:fs";
|
|
9633
|
-
import
|
|
9634
|
-
import
|
|
9784
|
+
import chalk4 from "chalk";
|
|
9785
|
+
import ora3 from "ora";
|
|
9635
9786
|
async function executeGeminiVideo(options) {
|
|
9636
9787
|
try {
|
|
9637
9788
|
const apiKey = await getApiKey("GOOGLE_API_KEY", "Google");
|
|
@@ -9649,7 +9800,7 @@ async function executeGeminiVideo(options) {
|
|
|
9649
9800
|
if (isYouTube) {
|
|
9650
9801
|
videoData = options.source;
|
|
9651
9802
|
} else {
|
|
9652
|
-
const absPath =
|
|
9803
|
+
const absPath = resolve8(process.cwd(), options.source);
|
|
9653
9804
|
if (!existsSync3(absPath)) {
|
|
9654
9805
|
return { success: false, error: `File not found: ${absPath}` };
|
|
9655
9806
|
}
|
|
@@ -9722,7 +9873,7 @@ async function executeAnalyze(options) {
|
|
|
9722
9873
|
}
|
|
9723
9874
|
imageBuffer = Buffer.from(await response.arrayBuffer());
|
|
9724
9875
|
} else {
|
|
9725
|
-
const absPath =
|
|
9876
|
+
const absPath = resolve8(process.cwd(), source);
|
|
9726
9877
|
if (!existsSync3(absPath)) {
|
|
9727
9878
|
return { success: false, error: `File not found: ${absPath}` };
|
|
9728
9879
|
}
|
|
@@ -9757,7 +9908,7 @@ async function executeAnalyze(options) {
|
|
|
9757
9908
|
}
|
|
9758
9909
|
videoData = Buffer.from(await response.arrayBuffer());
|
|
9759
9910
|
} else {
|
|
9760
|
-
const absPath =
|
|
9911
|
+
const absPath = resolve8(process.cwd(), source);
|
|
9761
9912
|
if (!existsSync3(absPath)) {
|
|
9762
9913
|
return { success: false, error: `File not found: ${absPath}` };
|
|
9763
9914
|
}
|
|
@@ -9792,10 +9943,10 @@ async function executeAnalyze(options) {
|
|
|
9792
9943
|
|
|
9793
9944
|
// ../cli/src/commands/ai-review.ts
|
|
9794
9945
|
import { readFile as readFile7, rename } from "node:fs/promises";
|
|
9795
|
-
import { resolve as
|
|
9946
|
+
import { resolve as resolve9 } from "node:path";
|
|
9796
9947
|
import { existsSync as existsSync4 } from "node:fs";
|
|
9797
|
-
import
|
|
9798
|
-
import
|
|
9948
|
+
import chalk5 from "chalk";
|
|
9949
|
+
import ora4 from "ora";
|
|
9799
9950
|
init_exec_safe();
|
|
9800
9951
|
function parseReviewFeedback(response) {
|
|
9801
9952
|
let cleaned = response.trim();
|
|
@@ -9820,7 +9971,7 @@ function parseReviewFeedback(response) {
|
|
|
9820
9971
|
}
|
|
9821
9972
|
async function executeReview(options) {
|
|
9822
9973
|
const { videoPath, storyboardPath, autoApply = false, verify = false, model = "flash" } = options;
|
|
9823
|
-
const absVideoPath =
|
|
9974
|
+
const absVideoPath = resolve9(process.cwd(), videoPath);
|
|
9824
9975
|
if (!existsSync4(absVideoPath)) {
|
|
9825
9976
|
return { success: false, error: `Video not found: ${absVideoPath}` };
|
|
9826
9977
|
}
|
|
@@ -9830,7 +9981,7 @@ async function executeReview(options) {
|
|
|
9830
9981
|
}
|
|
9831
9982
|
let storyboardContext = "";
|
|
9832
9983
|
if (storyboardPath) {
|
|
9833
|
-
const absStoryboardPath =
|
|
9984
|
+
const absStoryboardPath = resolve9(process.cwd(), storyboardPath);
|
|
9834
9985
|
if (existsSync4(absStoryboardPath)) {
|
|
9835
9986
|
const content = await readFile7(absStoryboardPath, "utf-8");
|
|
9836
9987
|
storyboardContext = `
|
|
@@ -9886,7 +10037,7 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
|
|
|
9886
10037
|
};
|
|
9887
10038
|
if (autoApply && feedback.autoFixable.length > 0) {
|
|
9888
10039
|
let currentInput = absVideoPath;
|
|
9889
|
-
const outputBase = options.outputPath ?
|
|
10040
|
+
const outputBase = options.outputPath ? resolve9(process.cwd(), options.outputPath) : absVideoPath.replace(/(\.[^.]+)$/, "-reviewed$1");
|
|
9890
10041
|
for (const fix of feedback.autoFixable) {
|
|
9891
10042
|
if (fix.type === "color_grade" && fix.ffmpegFilter) {
|
|
9892
10043
|
try {
|
|
@@ -9935,8 +10086,8 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
|
|
|
9935
10086
|
// ../cli/src/commands/ai-image.ts
|
|
9936
10087
|
import { readFile as readFile8, writeFile as writeFile6, mkdir as mkdir4 } from "node:fs/promises";
|
|
9937
10088
|
import { existsSync as existsSync5 } from "node:fs";
|
|
9938
|
-
import
|
|
9939
|
-
import
|
|
10089
|
+
import chalk6 from "chalk";
|
|
10090
|
+
import ora5 from "ora";
|
|
9940
10091
|
init_exec_safe();
|
|
9941
10092
|
async function executeThumbnailBestFrame(options) {
|
|
9942
10093
|
const {
|
|
@@ -10158,9 +10309,9 @@ async function handleAiAnalysisToolCall(name, args) {
|
|
|
10158
10309
|
|
|
10159
10310
|
// ../cli/src/commands/ai-script-pipeline.ts
|
|
10160
10311
|
import { readFile as readFile9, writeFile as writeFile7, mkdir as mkdir5, unlink, rename as rename2 } from "node:fs/promises";
|
|
10161
|
-
import { resolve as
|
|
10312
|
+
import { resolve as resolve10, basename as basename3, extname as extname3 } from "node:path";
|
|
10162
10313
|
import { existsSync as existsSync6 } from "node:fs";
|
|
10163
|
-
import
|
|
10314
|
+
import chalk7 from "chalk";
|
|
10164
10315
|
init_exec_safe();
|
|
10165
10316
|
|
|
10166
10317
|
// ../cli/src/commands/ai-helpers.ts
|
|
@@ -10180,7 +10331,37 @@ async function downloadVideo(url, apiKey) {
|
|
|
10180
10331
|
var DEFAULT_VIDEO_RETRIES = 2;
|
|
10181
10332
|
var RETRY_DELAY_MS = 5e3;
|
|
10182
10333
|
function sleep(ms) {
|
|
10183
|
-
return new Promise((
|
|
10334
|
+
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
10335
|
+
}
|
|
10336
|
+
async function generateVideoWithRetryGrok(grok, segment, options, maxRetries, onProgress) {
|
|
10337
|
+
const prompt2 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
|
|
10338
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
10339
|
+
try {
|
|
10340
|
+
const result = await grok.generateVideo(prompt2, {
|
|
10341
|
+
prompt: prompt2,
|
|
10342
|
+
duration: options.duration,
|
|
10343
|
+
aspectRatio: options.aspectRatio,
|
|
10344
|
+
referenceImage: options.referenceImage
|
|
10345
|
+
});
|
|
10346
|
+
if (result.status !== "failed" && result.id) {
|
|
10347
|
+
return { requestId: result.id };
|
|
10348
|
+
}
|
|
10349
|
+
if (attempt < maxRetries) {
|
|
10350
|
+
onProgress?.(`\u26A0 Retry ${attempt + 1}/${maxRetries}...`);
|
|
10351
|
+
await sleep(RETRY_DELAY_MS);
|
|
10352
|
+
}
|
|
10353
|
+
} catch (err) {
|
|
10354
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
10355
|
+
if (attempt < maxRetries) {
|
|
10356
|
+
onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
10357
|
+
await sleep(RETRY_DELAY_MS);
|
|
10358
|
+
} else {
|
|
10359
|
+
console.error(chalk7.dim(`
|
|
10360
|
+
[Grok error: ${errMsg}]`));
|
|
10361
|
+
}
|
|
10362
|
+
}
|
|
10363
|
+
}
|
|
10364
|
+
return null;
|
|
10184
10365
|
}
|
|
10185
10366
|
async function generateVideoWithRetryKling(kling, segment, options, maxRetries, onProgress) {
|
|
10186
10367
|
const prompt2 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
|
|
@@ -10211,7 +10392,7 @@ async function generateVideoWithRetryKling(kling, segment, options, maxRetries,
|
|
|
10211
10392
|
onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
10212
10393
|
await sleep(RETRY_DELAY_MS);
|
|
10213
10394
|
} else {
|
|
10214
|
-
console.error(
|
|
10395
|
+
console.error(chalk7.dim(`
|
|
10215
10396
|
[Kling error: ${errMsg}]`));
|
|
10216
10397
|
}
|
|
10217
10398
|
}
|
|
@@ -10240,7 +10421,7 @@ async function generateVideoWithRetryRunway(runway, segment, referenceImage, opt
|
|
|
10240
10421
|
onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
10241
10422
|
await sleep(RETRY_DELAY_MS);
|
|
10242
10423
|
} else {
|
|
10243
|
-
console.error(
|
|
10424
|
+
console.error(chalk7.dim(`
|
|
10244
10425
|
[Runway error: ${errMsg}]`));
|
|
10245
10426
|
}
|
|
10246
10427
|
}
|
|
@@ -10271,7 +10452,7 @@ async function generateVideoWithRetryVeo(gemini, segment, options, maxRetries, o
|
|
|
10271
10452
|
onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
|
|
10272
10453
|
await sleep(RETRY_DELAY_MS);
|
|
10273
10454
|
} else {
|
|
10274
|
-
console.error(
|
|
10455
|
+
console.error(chalk7.dim(`
|
|
10275
10456
|
[Veo error: ${errMsg}]`));
|
|
10276
10457
|
}
|
|
10277
10458
|
}
|
|
@@ -10326,24 +10507,23 @@ async function executeScriptToVideo(options) {
|
|
|
10326
10507
|
}
|
|
10327
10508
|
let videoApiKey;
|
|
10328
10509
|
if (!options.imagesOnly) {
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
}
|
|
10334
|
-
}
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
}
|
|
10339
|
-
}
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
}
|
|
10510
|
+
const generatorKeyMap = {
|
|
10511
|
+
grok: { envVar: "XAI_API_KEY", name: "xAI (Grok)" },
|
|
10512
|
+
kling: { envVar: "KLING_API_KEY", name: "Kling" },
|
|
10513
|
+
runway: { envVar: "RUNWAY_API_SECRET", name: "Runway" },
|
|
10514
|
+
veo: { envVar: "GOOGLE_API_KEY", name: "Google (Veo)" }
|
|
10515
|
+
};
|
|
10516
|
+
const generator = options.generator || "grok";
|
|
10517
|
+
const generatorInfo = generatorKeyMap[generator];
|
|
10518
|
+
if (!generatorInfo) {
|
|
10519
|
+
return { success: false, outputDir, scenes: 0, error: `Invalid generator: ${options.generator}. Available: ${Object.keys(generatorKeyMap).join(", ")}` };
|
|
10520
|
+
}
|
|
10521
|
+
videoApiKey = await getApiKey(generatorInfo.envVar, generatorInfo.name) ?? void 0;
|
|
10522
|
+
if (!videoApiKey) {
|
|
10523
|
+
return { success: false, outputDir, scenes: 0, error: `${generatorInfo.name} API key required (or use imagesOnly option). Run 'vibe setup' or set ${generatorInfo.envVar} in .env` };
|
|
10344
10524
|
}
|
|
10345
10525
|
}
|
|
10346
|
-
const absOutputDir =
|
|
10526
|
+
const absOutputDir = resolve10(process.cwd(), outputDir);
|
|
10347
10527
|
if (!existsSync6(absOutputDir)) {
|
|
10348
10528
|
await mkdir5(absOutputDir, { recursive: true });
|
|
10349
10529
|
}
|
|
@@ -10365,7 +10545,7 @@ async function executeScriptToVideo(options) {
|
|
|
10365
10545
|
if (segments.length === 0) {
|
|
10366
10546
|
return { success: false, outputDir, scenes: 0, error: "Failed to generate storyboard" };
|
|
10367
10547
|
}
|
|
10368
|
-
const storyboardPath =
|
|
10548
|
+
const storyboardPath = resolve10(absOutputDir, "storyboard.json");
|
|
10369
10549
|
await writeFile7(storyboardPath, JSON.stringify(segments, null, 2), "utf-8");
|
|
10370
10550
|
const result = {
|
|
10371
10551
|
success: true,
|
|
@@ -10399,7 +10579,7 @@ async function executeScriptToVideo(options) {
|
|
|
10399
10579
|
voiceId: options.voice
|
|
10400
10580
|
});
|
|
10401
10581
|
if (ttsResult.success && ttsResult.audioBuffer) {
|
|
10402
|
-
const audioPath =
|
|
10582
|
+
const audioPath = resolve10(absOutputDir, `narration-${i + 1}.mp3`);
|
|
10403
10583
|
await writeFile7(audioPath, ttsResult.audioBuffer);
|
|
10404
10584
|
const actualDuration = await getAudioDuration(audioPath);
|
|
10405
10585
|
segment.duration = actualDuration;
|
|
@@ -10487,7 +10667,7 @@ async function executeScriptToVideo(options) {
|
|
|
10487
10667
|
}
|
|
10488
10668
|
}
|
|
10489
10669
|
}
|
|
10490
|
-
const imagePath =
|
|
10670
|
+
const imagePath = resolve10(absOutputDir, `scene-${i + 1}.png`);
|
|
10491
10671
|
if (imageBuffer) {
|
|
10492
10672
|
await writeFile7(imagePath, imageBuffer);
|
|
10493
10673
|
imagePaths.push(imagePath);
|
|
@@ -10511,7 +10691,57 @@ async function executeScriptToVideo(options) {
|
|
|
10511
10691
|
const videoPaths = [];
|
|
10512
10692
|
const maxRetries = options.retries ?? DEFAULT_VIDEO_RETRIES;
|
|
10513
10693
|
if (!options.imagesOnly && videoApiKey) {
|
|
10514
|
-
if (options.generator === "
|
|
10694
|
+
if (options.generator === "grok") {
|
|
10695
|
+
const grok = new GrokProvider();
|
|
10696
|
+
await grok.initialize({ apiKey: videoApiKey });
|
|
10697
|
+
for (let i = 0; i < segments.length; i++) {
|
|
10698
|
+
if (!imagePaths[i]) {
|
|
10699
|
+
videoPaths.push("");
|
|
10700
|
+
continue;
|
|
10701
|
+
}
|
|
10702
|
+
const segment = segments[i];
|
|
10703
|
+
const videoDuration = Math.min(15, Math.max(1, segment.duration));
|
|
10704
|
+
const imageBuffer = await readFile9(imagePaths[i]);
|
|
10705
|
+
const ext = extname3(imagePaths[i]).toLowerCase().slice(1);
|
|
10706
|
+
const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
|
|
10707
|
+
const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
|
|
10708
|
+
const taskResult = await generateVideoWithRetryGrok(
|
|
10709
|
+
grok,
|
|
10710
|
+
segment,
|
|
10711
|
+
{ duration: videoDuration, aspectRatio: options.aspectRatio || "16:9", referenceImage },
|
|
10712
|
+
maxRetries
|
|
10713
|
+
);
|
|
10714
|
+
if (taskResult) {
|
|
10715
|
+
try {
|
|
10716
|
+
const waitResult = await grok.waitForCompletion(taskResult.requestId, void 0, 3e5);
|
|
10717
|
+
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
10718
|
+
const videoPath = resolve10(absOutputDir, `scene-${i + 1}.mp4`);
|
|
10719
|
+
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
10720
|
+
await writeFile7(videoPath, buffer);
|
|
10721
|
+
const targetDuration = segment.duration;
|
|
10722
|
+
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
10723
|
+
if (actualVideoDuration < targetDuration - 0.1) {
|
|
10724
|
+
const extendedPath = resolve10(absOutputDir, `scene-${i + 1}-extended.mp4`);
|
|
10725
|
+
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
10726
|
+
await unlink(videoPath);
|
|
10727
|
+
await rename2(extendedPath, videoPath);
|
|
10728
|
+
}
|
|
10729
|
+
videoPaths.push(videoPath);
|
|
10730
|
+
result.videos.push(videoPath);
|
|
10731
|
+
} else {
|
|
10732
|
+
videoPaths.push("");
|
|
10733
|
+
result.failedScenes.push(i + 1);
|
|
10734
|
+
}
|
|
10735
|
+
} catch {
|
|
10736
|
+
videoPaths.push("");
|
|
10737
|
+
result.failedScenes.push(i + 1);
|
|
10738
|
+
}
|
|
10739
|
+
} else {
|
|
10740
|
+
videoPaths.push("");
|
|
10741
|
+
result.failedScenes.push(i + 1);
|
|
10742
|
+
}
|
|
10743
|
+
}
|
|
10744
|
+
} else if (options.generator === "kling") {
|
|
10515
10745
|
const kling = new KlingProvider();
|
|
10516
10746
|
await kling.initialize({ apiKey: videoApiKey });
|
|
10517
10747
|
if (!kling.isConfigured()) {
|
|
@@ -10534,13 +10764,13 @@ async function executeScriptToVideo(options) {
|
|
|
10534
10764
|
try {
|
|
10535
10765
|
const waitResult = await kling.waitForCompletion(taskResult.taskId, taskResult.type, void 0, 6e5);
|
|
10536
10766
|
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
10537
|
-
const videoPath =
|
|
10767
|
+
const videoPath = resolve10(absOutputDir, `scene-${i + 1}.mp4`);
|
|
10538
10768
|
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
10539
10769
|
await writeFile7(videoPath, buffer);
|
|
10540
10770
|
const targetDuration = segment.duration;
|
|
10541
10771
|
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
10542
10772
|
if (actualVideoDuration < targetDuration - 0.1) {
|
|
10543
|
-
const extendedPath =
|
|
10773
|
+
const extendedPath = resolve10(absOutputDir, `scene-${i + 1}-extended.mp4`);
|
|
10544
10774
|
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
10545
10775
|
await unlink(videoPath);
|
|
10546
10776
|
await rename2(extendedPath, videoPath);
|
|
@@ -10580,13 +10810,13 @@ async function executeScriptToVideo(options) {
|
|
|
10580
10810
|
try {
|
|
10581
10811
|
const waitResult = await veo.waitForVideoCompletion(taskResult.operationName, void 0, 3e5);
|
|
10582
10812
|
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
10583
|
-
const videoPath =
|
|
10813
|
+
const videoPath = resolve10(absOutputDir, `scene-${i + 1}.mp4`);
|
|
10584
10814
|
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
10585
10815
|
await writeFile7(videoPath, buffer);
|
|
10586
10816
|
const targetDuration = segment.duration;
|
|
10587
10817
|
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
10588
10818
|
if (actualVideoDuration < targetDuration - 0.1) {
|
|
10589
|
-
const extendedPath =
|
|
10819
|
+
const extendedPath = resolve10(absOutputDir, `scene-${i + 1}-extended.mp4`);
|
|
10590
10820
|
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
10591
10821
|
await unlink(videoPath);
|
|
10592
10822
|
await rename2(extendedPath, videoPath);
|
|
@@ -10632,13 +10862,13 @@ async function executeScriptToVideo(options) {
|
|
|
10632
10862
|
try {
|
|
10633
10863
|
const waitResult = await runway.waitForCompletion(taskResult.taskId, void 0, 3e5);
|
|
10634
10864
|
if (waitResult.status === "completed" && waitResult.videoUrl) {
|
|
10635
|
-
const videoPath =
|
|
10865
|
+
const videoPath = resolve10(absOutputDir, `scene-${i + 1}.mp4`);
|
|
10636
10866
|
const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
|
|
10637
10867
|
await writeFile7(videoPath, buffer);
|
|
10638
10868
|
const targetDuration = segment.duration;
|
|
10639
10869
|
const actualVideoDuration = await getVideoDuration(videoPath);
|
|
10640
10870
|
if (actualVideoDuration < targetDuration - 0.1) {
|
|
10641
|
-
const extendedPath =
|
|
10871
|
+
const extendedPath = resolve10(absOutputDir, `scene-${i + 1}-extended.mp4`);
|
|
10642
10872
|
await extendVideoNaturally(videoPath, targetDuration, extendedPath);
|
|
10643
10873
|
await unlink(videoPath);
|
|
10644
10874
|
await rename2(extendedPath, videoPath);
|
|
@@ -10751,13 +10981,13 @@ async function executeScriptToVideo(options) {
|
|
|
10751
10981
|
});
|
|
10752
10982
|
currentTime += actualDuration;
|
|
10753
10983
|
}
|
|
10754
|
-
const projectPath =
|
|
10984
|
+
const projectPath = resolve10(absOutputDir, "project.vibe.json");
|
|
10755
10985
|
await writeFile7(projectPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
|
|
10756
10986
|
result.projectPath = projectPath;
|
|
10757
10987
|
result.totalDuration = currentTime;
|
|
10758
10988
|
if (options.review) {
|
|
10759
10989
|
try {
|
|
10760
|
-
const storyboardFile =
|
|
10990
|
+
const storyboardFile = resolve10(absOutputDir, "storyboard.json");
|
|
10761
10991
|
const reviewTarget = videoPaths.find((p) => p && p !== "") || imagePaths.find((p) => p && p !== "");
|
|
10762
10992
|
if (reviewTarget) {
|
|
10763
10993
|
const reviewResult = await executeReview({
|
|
@@ -10788,10 +11018,10 @@ async function executeScriptToVideo(options) {
|
|
|
10788
11018
|
|
|
10789
11019
|
// ../cli/src/commands/ai-highlights.ts
|
|
10790
11020
|
import { readFile as readFile10, writeFile as writeFile8, mkdir as mkdir6 } from "node:fs/promises";
|
|
10791
|
-
import { resolve as
|
|
11021
|
+
import { resolve as resolve11, dirname as dirname2, basename as basename4, extname as extname4 } from "node:path";
|
|
10792
11022
|
import { existsSync as existsSync7 } from "node:fs";
|
|
10793
|
-
import
|
|
10794
|
-
import
|
|
11023
|
+
import chalk8 from "chalk";
|
|
11024
|
+
import ora6 from "ora";
|
|
10795
11025
|
init_exec_safe();
|
|
10796
11026
|
function filterHighlights(highlights, options) {
|
|
10797
11027
|
let filtered = highlights.filter((h) => h.confidence >= options.threshold);
|
|
@@ -10815,7 +11045,7 @@ function filterHighlights(highlights, options) {
|
|
|
10815
11045
|
}
|
|
10816
11046
|
async function executeHighlights(options) {
|
|
10817
11047
|
try {
|
|
10818
|
-
const absPath =
|
|
11048
|
+
const absPath = resolve11(process.cwd(), options.media);
|
|
10819
11049
|
if (!existsSync7(absPath)) {
|
|
10820
11050
|
return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: `File not found: ${absPath}` };
|
|
10821
11051
|
}
|
|
@@ -10963,7 +11193,7 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
|
|
|
10963
11193
|
totalHighlightDuration
|
|
10964
11194
|
};
|
|
10965
11195
|
if (options.output) {
|
|
10966
|
-
const outputPath =
|
|
11196
|
+
const outputPath = resolve11(process.cwd(), options.output);
|
|
10967
11197
|
await writeFile8(outputPath, JSON.stringify({
|
|
10968
11198
|
sourceFile: absPath,
|
|
10969
11199
|
totalDuration: sourceDuration,
|
|
@@ -10998,7 +11228,7 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
|
|
|
10998
11228
|
currentTime += highlight.duration;
|
|
10999
11229
|
}
|
|
11000
11230
|
}
|
|
11001
|
-
const projectPath =
|
|
11231
|
+
const projectPath = resolve11(process.cwd(), options.project);
|
|
11002
11232
|
await writeFile8(projectPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
|
|
11003
11233
|
extractResult.projectPath = projectPath;
|
|
11004
11234
|
}
|
|
@@ -11018,7 +11248,7 @@ async function executeAutoShorts(options) {
|
|
|
11018
11248
|
if (!commandExists("ffmpeg")) {
|
|
11019
11249
|
return { success: false, shorts: [], error: "FFmpeg not found" };
|
|
11020
11250
|
}
|
|
11021
|
-
const absPath =
|
|
11251
|
+
const absPath = resolve11(process.cwd(), options.video);
|
|
11022
11252
|
if (!existsSync7(absPath)) {
|
|
11023
11253
|
return { success: false, shorts: [], error: `File not found: ${absPath}` };
|
|
11024
11254
|
}
|
|
@@ -11146,7 +11376,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
|
|
|
11146
11376
|
}))
|
|
11147
11377
|
};
|
|
11148
11378
|
}
|
|
11149
|
-
const outputDir = options.outputDir ?
|
|
11379
|
+
const outputDir = options.outputDir ? resolve11(process.cwd(), options.outputDir) : dirname2(absPath);
|
|
11150
11380
|
if (options.outputDir && !existsSync7(outputDir)) {
|
|
11151
11381
|
await mkdir6(outputDir, { recursive: true });
|
|
11152
11382
|
}
|
|
@@ -11157,7 +11387,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
|
|
|
11157
11387
|
for (let i = 0; i < selectedHighlights.length; i++) {
|
|
11158
11388
|
const h = selectedHighlights[i];
|
|
11159
11389
|
const baseName = basename4(absPath, extname4(absPath));
|
|
11160
|
-
const outputPath =
|
|
11390
|
+
const outputPath = resolve11(outputDir, `${baseName}-short-${i + 1}.mp4`);
|
|
11161
11391
|
const { stdout: probeOut } = await execSafe("ffprobe", [
|
|
11162
11392
|
"-v",
|
|
11163
11393
|
"error",
|
|
@@ -11449,7 +11679,7 @@ async function handleToolCall(name, args) {
|
|
|
11449
11679
|
|
|
11450
11680
|
// src/resources/index.ts
|
|
11451
11681
|
import { readFile as readFile11 } from "node:fs/promises";
|
|
11452
|
-
import { resolve as
|
|
11682
|
+
import { resolve as resolve12 } from "node:path";
|
|
11453
11683
|
var resources = [
|
|
11454
11684
|
{
|
|
11455
11685
|
uri: "vibe://project/current",
|
|
@@ -11484,7 +11714,7 @@ var resources = [
|
|
|
11484
11714
|
];
|
|
11485
11715
|
var currentProjectPath = process.env.VIBE_PROJECT_PATH || null;
|
|
11486
11716
|
async function loadProject2(projectPath) {
|
|
11487
|
-
const absPath =
|
|
11717
|
+
const absPath = resolve12(process.cwd(), projectPath);
|
|
11488
11718
|
const content = await readFile11(absPath, "utf-8");
|
|
11489
11719
|
const data = JSON.parse(content);
|
|
11490
11720
|
return Project.fromJSON(data);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibeframe/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.1",
|
|
4
4
|
"description": "VibeFrame MCP Server - AI-native video editing via Model Context Protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"tsx": "^4.21.0",
|
|
58
58
|
"typescript": "^5.3.3",
|
|
59
59
|
"vitest": "^1.2.2",
|
|
60
|
-
"@vibeframe/cli": "0.
|
|
61
|
-
"@vibeframe/core": "0.
|
|
60
|
+
"@vibeframe/cli": "0.33.1",
|
|
61
|
+
"@vibeframe/core": "0.33.1"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|
|
64
64
|
"node": ">=20"
|