@vibeframe/mcp-server 0.33.0 → 0.35.0
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 +266 -31
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -62,6 +62,7 @@ __export(remotion_exports, {
|
|
|
62
62
|
ensureRemotionInstalled: () => ensureRemotionInstalled,
|
|
63
63
|
generateAnimatedCaptionComponent: () => generateAnimatedCaptionComponent,
|
|
64
64
|
generateCaptionComponent: () => generateCaptionComponent,
|
|
65
|
+
generateTextOverlayComponent: () => generateTextOverlayComponent,
|
|
65
66
|
renderAndComposite: () => renderAndComposite,
|
|
66
67
|
renderMotion: () => renderMotion,
|
|
67
68
|
renderWithEmbeddedImage: () => renderWithEmbeddedImage,
|
|
@@ -75,15 +76,19 @@ import { writeFile as writeFile4, mkdir as mkdir2, rm, copyFile } from "node:fs/
|
|
|
75
76
|
import { existsSync } from "node:fs";
|
|
76
77
|
import { join } from "node:path";
|
|
77
78
|
import { tmpdir } from "node:os";
|
|
79
|
+
import { createHash } from "node:crypto";
|
|
78
80
|
async function ensureRemotionInstalled() {
|
|
79
81
|
try {
|
|
80
|
-
await execSafe("npx", ["remotion", "--help"], { timeout:
|
|
82
|
+
await execSafe("npx", ["--yes", "remotion", "--help"], { timeout: 6e4 });
|
|
81
83
|
return null;
|
|
82
|
-
} catch {
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
86
|
+
console.error(`[Remotion] ensureRemotionInstalled failed: ${detail.slice(0, 300)}`);
|
|
83
87
|
return [
|
|
84
|
-
"Remotion CLI not found
|
|
85
|
-
|
|
86
|
-
"
|
|
88
|
+
"Remotion CLI not found or failed to initialize.",
|
|
89
|
+
` Debug: ${detail.slice(0, 200)}`,
|
|
90
|
+
" Fix: npm install -g @remotion/cli",
|
|
91
|
+
" Or ensure npx is available and can download @remotion/cli on demand."
|
|
87
92
|
].join("\n");
|
|
88
93
|
}
|
|
89
94
|
}
|
|
@@ -91,14 +96,14 @@ async function scaffoldRemotionProject(componentCode, componentName, opts) {
|
|
|
91
96
|
const dir = join(tmpdir(), `vibe_motion_${Date.now()}`);
|
|
92
97
|
await mkdir2(dir, { recursive: true });
|
|
93
98
|
const deps = {
|
|
94
|
-
remotion:
|
|
95
|
-
"@remotion/cli":
|
|
99
|
+
remotion: REMOTION_VERSION,
|
|
100
|
+
"@remotion/cli": REMOTION_VERSION,
|
|
96
101
|
react: "^18.0.0",
|
|
97
102
|
"react-dom": "^18.0.0",
|
|
98
103
|
"@types/react": "^18.0.0"
|
|
99
104
|
};
|
|
100
105
|
if (opts.useMediaPackage) {
|
|
101
|
-
deps["@remotion/media"] =
|
|
106
|
+
deps["@remotion/media"] = REMOTION_VERSION;
|
|
102
107
|
}
|
|
103
108
|
const packageJson = {
|
|
104
109
|
name: "vibe-motion-render",
|
|
@@ -106,7 +111,8 @@ async function scaffoldRemotionProject(componentCode, componentName, opts) {
|
|
|
106
111
|
private: true,
|
|
107
112
|
dependencies: deps
|
|
108
113
|
};
|
|
109
|
-
|
|
114
|
+
const packageJsonStr = JSON.stringify(packageJson, null, 2);
|
|
115
|
+
await writeFile4(join(dir, "package.json"), packageJsonStr);
|
|
110
116
|
const tsconfig = {
|
|
111
117
|
compilerOptions: {
|
|
112
118
|
target: "ES2020",
|
|
@@ -139,16 +145,48 @@ const Root = () => {
|
|
|
139
145
|
registerRoot(Root);
|
|
140
146
|
`;
|
|
141
147
|
await writeFile4(join(dir, "Root.tsx"), rootCode);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
const depsHash = createHash("md5").update(packageJsonStr).digest("hex").slice(0, 12);
|
|
149
|
+
const cacheMarker = join(REMOTION_CACHE_DIR, `.deps-${depsHash}`);
|
|
150
|
+
const cachedModules = join(REMOTION_CACHE_DIR, "node_modules");
|
|
151
|
+
if (existsSync(cacheMarker) && existsSync(cachedModules)) {
|
|
152
|
+
const { symlink } = await import("node:fs/promises");
|
|
153
|
+
try {
|
|
154
|
+
await symlink(cachedModules, join(dir, "node_modules"), "dir");
|
|
155
|
+
} catch {
|
|
156
|
+
await this_npmInstall(dir);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
await this_npmInstall(dir);
|
|
160
|
+
try {
|
|
161
|
+
await mkdir2(REMOTION_CACHE_DIR, { recursive: true });
|
|
162
|
+
await writeFile4(join(REMOTION_CACHE_DIR, "package.json"), packageJsonStr);
|
|
163
|
+
if (existsSync(join(dir, "node_modules"))) {
|
|
164
|
+
const { rename: rename3, symlink } = await import("node:fs/promises");
|
|
165
|
+
await rename3(join(dir, "node_modules"), cachedModules).catch(() => {
|
|
166
|
+
});
|
|
167
|
+
await symlink(cachedModules, join(dir, "node_modules"), "dir").catch(() => {
|
|
168
|
+
});
|
|
169
|
+
await writeFile4(cacheMarker, depsHash);
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return dir;
|
|
175
|
+
}
|
|
176
|
+
async function this_npmInstall(dir) {
|
|
177
|
+
const { execFile: execFile2 } = await import("node:child_process");
|
|
178
|
+
const { promisify: promisify2 } = await import("node:util");
|
|
179
|
+
const execFileAsync2 = promisify2(execFile2);
|
|
180
|
+
try {
|
|
146
181
|
await execFileAsync2("npm", ["install", "--prefer-offline", "--no-audit", "--no-fund"], {
|
|
147
182
|
cwd: dir,
|
|
148
183
|
timeout: 18e4
|
|
149
184
|
});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
const msg = error instanceof Error ? error.stderr || error.message : String(error);
|
|
187
|
+
console.error(`[Remotion] npm install failed: ${msg.slice(0, 300)}`);
|
|
188
|
+
throw error;
|
|
150
189
|
}
|
|
151
|
-
return dir;
|
|
152
190
|
}
|
|
153
191
|
async function renderMotion(options) {
|
|
154
192
|
const transparent = options.transparent !== false;
|
|
@@ -220,7 +258,12 @@ async function renderMotion(options) {
|
|
|
220
258
|
], { cwd: dir, timeout: 3e5 });
|
|
221
259
|
return { success: true, outputPath: mp4Out };
|
|
222
260
|
} catch (error) {
|
|
261
|
+
const errObj = error;
|
|
262
|
+
const stderr = errObj.stderr?.slice(0, 500) || "";
|
|
223
263
|
const msg = error instanceof Error ? error.message : String(error);
|
|
264
|
+
console.error(`[Remotion] Render failed:
|
|
265
|
+
${msg.slice(0, 300)}${stderr ? `
|
|
266
|
+
stderr: ${stderr}` : ""}`);
|
|
224
267
|
return { success: false, error: `Remotion render failed: ${msg}` };
|
|
225
268
|
} finally {
|
|
226
269
|
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
@@ -403,6 +446,98 @@ async function renderWithEmbeddedVideo(options) {
|
|
|
403
446
|
});
|
|
404
447
|
}
|
|
405
448
|
}
|
|
449
|
+
function generateTextOverlayComponent(options) {
|
|
450
|
+
const { texts, style, fontSize, fontColor, startTime, endTime, fadeDuration, width, height, videoFileName } = options;
|
|
451
|
+
const name = "TextOverlay";
|
|
452
|
+
const textsJSON = JSON.stringify(texts);
|
|
453
|
+
const styleMap = {
|
|
454
|
+
"lower-third": {
|
|
455
|
+
justify: "flex-end",
|
|
456
|
+
align: "flex-start",
|
|
457
|
+
padding: `paddingBottom: ${Math.round(height * 0.12)}, paddingLeft: ${Math.round(width * 0.05)},`,
|
|
458
|
+
extraCss: `backgroundColor: "rgba(0,0,0,0.5)", padding: "8px 20px", borderRadius: 4,`
|
|
459
|
+
},
|
|
460
|
+
"center-bold": {
|
|
461
|
+
justify: "center",
|
|
462
|
+
align: "center",
|
|
463
|
+
padding: "",
|
|
464
|
+
extraCss: `fontWeight: "bold" as const, textShadow: "3px 3px 6px rgba(0,0,0,0.9)",`
|
|
465
|
+
},
|
|
466
|
+
"subtitle": {
|
|
467
|
+
justify: "flex-end",
|
|
468
|
+
align: "center",
|
|
469
|
+
padding: `paddingBottom: ${Math.round(height * 0.08)},`,
|
|
470
|
+
extraCss: `backgroundColor: "rgba(0,0,0,0.6)", padding: "6px 16px", borderRadius: 4,`
|
|
471
|
+
},
|
|
472
|
+
"minimal": {
|
|
473
|
+
justify: "flex-start",
|
|
474
|
+
align: "flex-start",
|
|
475
|
+
padding: `paddingTop: ${Math.round(height * 0.05)}, paddingLeft: ${Math.round(width * 0.05)},`,
|
|
476
|
+
extraCss: `opacity: 0.85,`
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
const s = styleMap[style];
|
|
480
|
+
const scaledFontSize = style === "center-bold" ? Math.round(fontSize * 1.5) : fontSize;
|
|
481
|
+
const code = `import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, staticFile } from "remotion";
|
|
482
|
+
import { Video } from "@remotion/media";
|
|
483
|
+
|
|
484
|
+
const texts: string[] = ${textsJSON};
|
|
485
|
+
const START_TIME = ${startTime};
|
|
486
|
+
const END_TIME = ${endTime};
|
|
487
|
+
const FADE_DURATION = ${fadeDuration};
|
|
488
|
+
|
|
489
|
+
export const ${name} = () => {
|
|
490
|
+
const frame = useCurrentFrame();
|
|
491
|
+
const { fps } = useVideoConfig();
|
|
492
|
+
const currentTime = frame / fps;
|
|
493
|
+
|
|
494
|
+
const visible = currentTime >= START_TIME && currentTime <= END_TIME;
|
|
495
|
+
|
|
496
|
+
const opacity = visible
|
|
497
|
+
? interpolate(
|
|
498
|
+
currentTime,
|
|
499
|
+
[START_TIME, START_TIME + FADE_DURATION, END_TIME - FADE_DURATION, END_TIME],
|
|
500
|
+
[0, 1, 1, 0],
|
|
501
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
502
|
+
)
|
|
503
|
+
: 0;
|
|
504
|
+
|
|
505
|
+
return (
|
|
506
|
+
<AbsoluteFill>
|
|
507
|
+
<Video src={staticFile("${videoFileName}")} style={{ width: "100%", height: "100%" }} muted />
|
|
508
|
+
{visible && (
|
|
509
|
+
<AbsoluteFill
|
|
510
|
+
style={{
|
|
511
|
+
display: "flex",
|
|
512
|
+
flexDirection: "column",
|
|
513
|
+
justifyContent: "${s.justify}",
|
|
514
|
+
alignItems: "${s.align}",
|
|
515
|
+
${s.padding}
|
|
516
|
+
opacity,
|
|
517
|
+
}}
|
|
518
|
+
>
|
|
519
|
+
<div
|
|
520
|
+
style={{
|
|
521
|
+
fontSize: ${scaledFontSize},
|
|
522
|
+
fontFamily: "Arial, Helvetica, sans-serif",
|
|
523
|
+
color: "${fontColor}",
|
|
524
|
+
lineHeight: 1.4,
|
|
525
|
+
maxWidth: "${Math.round(width * 0.9)}px",
|
|
526
|
+
${s.extraCss}
|
|
527
|
+
}}
|
|
528
|
+
>
|
|
529
|
+
{texts.map((text, i) => (
|
|
530
|
+
<div key={i}>{text}</div>
|
|
531
|
+
))}
|
|
532
|
+
</div>
|
|
533
|
+
</AbsoluteFill>
|
|
534
|
+
)}
|
|
535
|
+
</AbsoluteFill>
|
|
536
|
+
);
|
|
537
|
+
};
|
|
538
|
+
`;
|
|
539
|
+
return { code, name };
|
|
540
|
+
}
|
|
406
541
|
function generateCaptionComponent(options) {
|
|
407
542
|
const { segments, style, fontSize, fontColor, position, width, videoFileName } = options;
|
|
408
543
|
const name = videoFileName ? "VideoCaptioned" : "CaptionOverlay";
|
|
@@ -762,10 +897,13 @@ async function renderAndComposite(motionOpts, baseVideo, finalOutput) {
|
|
|
762
897
|
});
|
|
763
898
|
return compositeResult;
|
|
764
899
|
}
|
|
900
|
+
var REMOTION_VERSION, REMOTION_CACHE_DIR;
|
|
765
901
|
var init_remotion = __esm({
|
|
766
902
|
"../cli/src/utils/remotion.ts"() {
|
|
767
903
|
"use strict";
|
|
768
904
|
init_exec_safe();
|
|
905
|
+
REMOTION_VERSION = "4.0.447";
|
|
906
|
+
REMOTION_CACHE_DIR = join(tmpdir(), "vibe_remotion_cache");
|
|
769
907
|
}
|
|
770
908
|
});
|
|
771
909
|
|
|
@@ -1556,6 +1694,16 @@ async function resolveProjectPath(inputPath) {
|
|
|
1556
1694
|
}
|
|
1557
1695
|
return filePath;
|
|
1558
1696
|
}
|
|
1697
|
+
async function getMediaDuration(filePath, mediaType, defaultImageDuration = 5) {
|
|
1698
|
+
if (mediaType === "image") {
|
|
1699
|
+
return defaultImageDuration;
|
|
1700
|
+
}
|
|
1701
|
+
try {
|
|
1702
|
+
return await ffprobeDuration(filePath);
|
|
1703
|
+
} catch {
|
|
1704
|
+
return defaultImageDuration;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1559
1707
|
async function checkHasAudio(filePath) {
|
|
1560
1708
|
try {
|
|
1561
1709
|
const { stdout } = await execSafe("ffprobe", [
|
|
@@ -1600,11 +1748,19 @@ async function runExport(projectPath, outputPath, options = {}) {
|
|
|
1600
1748
|
const clips = project.getClips().sort((a, b) => a.startTime - b.startTime);
|
|
1601
1749
|
const sources = project.getSources();
|
|
1602
1750
|
const sourceAudioMap = /* @__PURE__ */ new Map();
|
|
1751
|
+
const sourceActualDurationMap = /* @__PURE__ */ new Map();
|
|
1603
1752
|
for (const clip of clips) {
|
|
1604
1753
|
const source = sources.find((s) => s.id === clip.sourceId);
|
|
1605
1754
|
if (source) {
|
|
1606
1755
|
try {
|
|
1607
1756
|
await access(source.url);
|
|
1757
|
+
if (!sourceActualDurationMap.has(source.id)) {
|
|
1758
|
+
try {
|
|
1759
|
+
const dur = await getMediaDuration(source.url, source.type);
|
|
1760
|
+
if (dur > 0) sourceActualDurationMap.set(source.id, dur);
|
|
1761
|
+
} catch {
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1608
1764
|
if (source.type === "video" && !sourceAudioMap.has(source.id)) {
|
|
1609
1765
|
sourceAudioMap.set(source.id, await checkHasAudio(source.url));
|
|
1610
1766
|
}
|
|
@@ -1616,7 +1772,7 @@ async function runExport(projectPath, outputPath, options = {}) {
|
|
|
1616
1772
|
}
|
|
1617
1773
|
}
|
|
1618
1774
|
}
|
|
1619
|
-
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, finalOutputPath, { overwrite, format, gapFill }, sourceAudioMap);
|
|
1775
|
+
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, finalOutputPath, { overwrite, format, gapFill }, sourceAudioMap, sourceActualDurationMap);
|
|
1620
1776
|
await runFFmpegProcess(ffmpegPath, ffmpegArgs, () => {
|
|
1621
1777
|
});
|
|
1622
1778
|
return {
|
|
@@ -1687,11 +1843,19 @@ No API keys needed. Requires FFmpeg.`).action(async (projectPath, options) => {
|
|
|
1687
1843
|
const sources = project.getSources();
|
|
1688
1844
|
spinner.text = "Verifying source files...";
|
|
1689
1845
|
const sourceAudioMap = /* @__PURE__ */ new Map();
|
|
1846
|
+
const sourceActualDurationMap = /* @__PURE__ */ new Map();
|
|
1690
1847
|
for (const clip of clips) {
|
|
1691
1848
|
const source = sources.find((s) => s.id === clip.sourceId);
|
|
1692
1849
|
if (source) {
|
|
1693
1850
|
try {
|
|
1694
1851
|
await access(source.url);
|
|
1852
|
+
if (!sourceActualDurationMap.has(source.id)) {
|
|
1853
|
+
try {
|
|
1854
|
+
const dur = await getMediaDuration(source.url, source.type);
|
|
1855
|
+
if (dur > 0) sourceActualDurationMap.set(source.id, dur);
|
|
1856
|
+
} catch {
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1695
1859
|
if (source.type === "video" && !sourceAudioMap.has(source.id)) {
|
|
1696
1860
|
sourceAudioMap.set(source.id, await checkHasAudio(source.url));
|
|
1697
1861
|
}
|
|
@@ -1703,7 +1867,7 @@ No API keys needed. Requires FFmpeg.`).action(async (projectPath, options) => {
|
|
|
1703
1867
|
}
|
|
1704
1868
|
spinner.text = "Building export command...";
|
|
1705
1869
|
const gapFillStrategy = options.gapFill === "black" ? "black" : "extend";
|
|
1706
|
-
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, outputPath, { ...options, gapFill: gapFillStrategy }, sourceAudioMap);
|
|
1870
|
+
const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, outputPath, { ...options, gapFill: gapFillStrategy }, sourceAudioMap, sourceActualDurationMap);
|
|
1707
1871
|
if (process.env.DEBUG) {
|
|
1708
1872
|
console.log("\nFFmpeg command:");
|
|
1709
1873
|
console.log("ffmpeg", ffmpegArgs.join(" "));
|
|
@@ -1827,7 +1991,7 @@ function createGapFillPlans(gaps, clips, sources) {
|
|
|
1827
1991
|
return { gap, fills };
|
|
1828
1992
|
});
|
|
1829
1993
|
}
|
|
1830
|
-
function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, sourceAudioMap = /* @__PURE__ */ new Map()) {
|
|
1994
|
+
function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, sourceAudioMap = /* @__PURE__ */ new Map(), sourceActualDurationMap = /* @__PURE__ */ new Map()) {
|
|
1831
1995
|
const args = [];
|
|
1832
1996
|
if (options.overwrite) {
|
|
1833
1997
|
args.push("-y");
|
|
@@ -1912,6 +2076,12 @@ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, so
|
|
|
1912
2076
|
const trimStart = clip.sourceStartOffset;
|
|
1913
2077
|
const trimEnd = clip.sourceStartOffset + clip.duration;
|
|
1914
2078
|
videoFilter = `[${srcIdx}:v]trim=start=${trimStart}:end=${trimEnd},setpts=PTS-STARTPTS`;
|
|
2079
|
+
const sourceDuration = sourceActualDurationMap.get(source.id) || source.duration || 0;
|
|
2080
|
+
const availableDuration = sourceDuration - clip.sourceStartOffset;
|
|
2081
|
+
if (availableDuration > 0 && availableDuration < clip.duration - 0.1) {
|
|
2082
|
+
const padDuration = clip.duration - availableDuration;
|
|
2083
|
+
videoFilter += `,tpad=stop_mode=clone:stop_duration=${padDuration.toFixed(3)}`;
|
|
2084
|
+
}
|
|
1915
2085
|
}
|
|
1916
2086
|
videoFilter += `,scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
|
|
1917
2087
|
for (const effect of clip.effects || []) {
|
|
@@ -2704,11 +2874,14 @@ IMPORTANT GUIDELINES:
|
|
|
2704
2874
|
- ALWAYS include the character description when the person appears
|
|
2705
2875
|
|
|
2706
2876
|
3. NARRATION LENGTH (CRITICAL for audio-video sync):
|
|
2707
|
-
-
|
|
2708
|
-
-
|
|
2709
|
-
-
|
|
2877
|
+
- Average TTS speaking rate is ~2.5 words/second
|
|
2878
|
+
- For duration=5: narration MUST be 10-12 words (4-5 seconds of speech)
|
|
2879
|
+
- For duration=10: narration MUST be 20-24 words (8-10 seconds of speech)
|
|
2880
|
+
- NEVER exceed the word limit \u2014 count every word before finalizing
|
|
2881
|
+
- ALWAYS leave 0.5-1s buffer (narration should be slightly shorter than video)
|
|
2710
2882
|
- If the script has a long paragraph, break it into 2-3 shorter scenes rather than one long narration
|
|
2711
2883
|
- This prevents freeze frames where video stops but narration continues
|
|
2884
|
+
- VALIDATION: After writing each narration, count the words. If over limit, shorten it.
|
|
2712
2885
|
|
|
2713
2886
|
4. NARRATION-VISUAL ALIGNMENT: The narration must directly describe what's visible:
|
|
2714
2887
|
- When narration mentions something specific, the visual must show it
|
|
@@ -6618,7 +6791,7 @@ var openaiImageProvider = new OpenAIImageProvider();
|
|
|
6618
6791
|
|
|
6619
6792
|
// ../ai-providers/dist/runway/RunwayProvider.js
|
|
6620
6793
|
var DEFAULT_MODEL2 = "gen4.5";
|
|
6621
|
-
var RunwayProvider = class {
|
|
6794
|
+
var RunwayProvider = class _RunwayProvider {
|
|
6622
6795
|
constructor() {
|
|
6623
6796
|
this.id = "runway";
|
|
6624
6797
|
this.name = "Runway";
|
|
@@ -6683,12 +6856,12 @@ var RunwayProvider = class {
|
|
|
6683
6856
|
if (options?.seed !== void 0) {
|
|
6684
6857
|
body.seed = options.seed;
|
|
6685
6858
|
}
|
|
6686
|
-
const response = await
|
|
6859
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/${endpoint}`, {
|
|
6687
6860
|
method: "POST",
|
|
6688
6861
|
headers: {
|
|
6689
6862
|
Authorization: `Bearer ${this.apiKey}`,
|
|
6690
6863
|
"Content-Type": "application/json",
|
|
6691
|
-
"X-Runway-Version":
|
|
6864
|
+
"X-Runway-Version": _RunwayProvider.API_VERSION
|
|
6692
6865
|
},
|
|
6693
6866
|
body: JSON.stringify(body)
|
|
6694
6867
|
});
|
|
@@ -6701,6 +6874,9 @@ var RunwayProvider = class {
|
|
|
6701
6874
|
} catch {
|
|
6702
6875
|
errorMessage = errorText;
|
|
6703
6876
|
}
|
|
6877
|
+
console.error(`[Runway] POST /${endpoint} -> ${response.status} ${response.statusText}
|
|
6878
|
+
Model: ${model}, Ratio: ${apiRatio}, Duration: ${body.duration}
|
|
6879
|
+
Response: ${errorText.slice(0, 500)}`);
|
|
6704
6880
|
return {
|
|
6705
6881
|
id: "",
|
|
6706
6882
|
status: "failed",
|
|
@@ -6749,7 +6925,7 @@ var RunwayProvider = class {
|
|
|
6749
6925
|
const response = await fetch(`${this.baseUrl}/tasks/${id}`, {
|
|
6750
6926
|
headers: {
|
|
6751
6927
|
Authorization: `Bearer ${this.apiKey}`,
|
|
6752
|
-
"X-Runway-Version":
|
|
6928
|
+
"X-Runway-Version": _RunwayProvider.API_VERSION
|
|
6753
6929
|
}
|
|
6754
6930
|
});
|
|
6755
6931
|
if (!response.ok) {
|
|
@@ -6801,7 +6977,7 @@ var RunwayProvider = class {
|
|
|
6801
6977
|
method: "POST",
|
|
6802
6978
|
headers: {
|
|
6803
6979
|
Authorization: `Bearer ${this.apiKey}`,
|
|
6804
|
-
"X-Runway-Version":
|
|
6980
|
+
"X-Runway-Version": _RunwayProvider.API_VERSION
|
|
6805
6981
|
}
|
|
6806
6982
|
});
|
|
6807
6983
|
return response.ok;
|
|
@@ -6821,7 +6997,7 @@ var RunwayProvider = class {
|
|
|
6821
6997
|
method: "DELETE",
|
|
6822
6998
|
headers: {
|
|
6823
6999
|
Authorization: `Bearer ${this.apiKey}`,
|
|
6824
|
-
"X-Runway-Version":
|
|
7000
|
+
"X-Runway-Version": _RunwayProvider.API_VERSION
|
|
6825
7001
|
}
|
|
6826
7002
|
});
|
|
6827
7003
|
return response.ok;
|
|
@@ -6850,6 +7026,24 @@ var RunwayProvider = class {
|
|
|
6850
7026
|
error: "Generation timed out"
|
|
6851
7027
|
};
|
|
6852
7028
|
}
|
|
7029
|
+
/**
|
|
7030
|
+
* Fetch with retry for transient errors (429, 503)
|
|
7031
|
+
*/
|
|
7032
|
+
async fetchWithRetry(url, init) {
|
|
7033
|
+
let lastResponse;
|
|
7034
|
+
for (let attempt = 0; attempt < _RunwayProvider.MAX_RETRIES; attempt++) {
|
|
7035
|
+
const response = await fetch(url, init);
|
|
7036
|
+
if (response.status !== 429 && response.status !== 503) {
|
|
7037
|
+
return response;
|
|
7038
|
+
}
|
|
7039
|
+
lastResponse = response;
|
|
7040
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
7041
|
+
const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 2 ** (attempt + 1) * 1e3;
|
|
7042
|
+
console.error(`[Runway] ${response.status} \u2014 retrying in ${delayMs / 1e3}s (attempt ${attempt + 1}/${_RunwayProvider.MAX_RETRIES})`);
|
|
7043
|
+
await this.sleep(delayMs);
|
|
7044
|
+
}
|
|
7045
|
+
return lastResponse;
|
|
7046
|
+
}
|
|
6853
7047
|
/**
|
|
6854
7048
|
* Clamp duration to valid range for the given model
|
|
6855
7049
|
*/
|
|
@@ -6876,6 +7070,8 @@ var RunwayProvider = class {
|
|
|
6876
7070
|
return new Promise((resolve13) => setTimeout(resolve13, ms));
|
|
6877
7071
|
}
|
|
6878
7072
|
};
|
|
7073
|
+
RunwayProvider.API_VERSION = "2024-11-06";
|
|
7074
|
+
RunwayProvider.MAX_RETRIES = 3;
|
|
6879
7075
|
var runwayProvider = new RunwayProvider();
|
|
6880
7076
|
|
|
6881
7077
|
// ../ai-providers/dist/kling/KlingProvider.js
|
|
@@ -9383,9 +9579,17 @@ async function applyTextOverlays(options) {
|
|
|
9383
9579
|
if (!commandExists("ffmpeg")) {
|
|
9384
9580
|
return { success: false, error: "FFmpeg not found. Please install FFmpeg." };
|
|
9385
9581
|
}
|
|
9582
|
+
let hasDrawtext = true;
|
|
9386
9583
|
try {
|
|
9387
9584
|
const { stdout } = await execSafe("ffmpeg", ["-filters"]);
|
|
9388
|
-
|
|
9585
|
+
hasDrawtext = stdout.includes("drawtext");
|
|
9586
|
+
} catch {
|
|
9587
|
+
}
|
|
9588
|
+
if (!hasDrawtext) {
|
|
9589
|
+
console.log("FFmpeg missing drawtext filter (libfreetype) \u2014 using Remotion fallback...");
|
|
9590
|
+
const { generateTextOverlayComponent: generateTextOverlayComponent2, renderWithEmbeddedVideo: renderWithEmbeddedVideo2, ensureRemotionInstalled: ensureRemotionInstalled2 } = await Promise.resolve().then(() => (init_remotion(), remotion_exports));
|
|
9591
|
+
const remotionErr = await ensureRemotionInstalled2();
|
|
9592
|
+
if (remotionErr) {
|
|
9389
9593
|
const platform = process.platform;
|
|
9390
9594
|
let hint = "";
|
|
9391
9595
|
if (platform === "darwin") {
|
|
@@ -9395,10 +9599,41 @@ async function applyTextOverlays(options) {
|
|
|
9395
9599
|
}
|
|
9396
9600
|
return {
|
|
9397
9601
|
success: false,
|
|
9398
|
-
error: `FFmpeg 'drawtext' filter not available
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9602
|
+
error: `FFmpeg 'drawtext' filter not available and Remotion fallback unavailable.
|
|
9603
|
+
${remotionErr}${hint}`
|
|
9604
|
+
};
|
|
9605
|
+
}
|
|
9606
|
+
const { width: width2, height: height2 } = await getVideoResolution(absVideoPath);
|
|
9607
|
+
const videoDuration2 = await getVideoDuration(absVideoPath);
|
|
9608
|
+
const baseFontSize2 = customFontSize || Math.round(height2 / 20);
|
|
9609
|
+
const endTime2 = options.endTime ?? videoDuration2;
|
|
9610
|
+
const fps = 30;
|
|
9611
|
+
const durationInFrames = Math.ceil(videoDuration2 * fps);
|
|
9612
|
+
const videoFileName = "source_video.mp4";
|
|
9613
|
+
const { code, name } = generateTextOverlayComponent2({
|
|
9614
|
+
texts,
|
|
9615
|
+
style,
|
|
9616
|
+
fontSize: baseFontSize2,
|
|
9617
|
+
fontColor,
|
|
9618
|
+
startTime,
|
|
9619
|
+
endTime: endTime2,
|
|
9620
|
+
fadeDuration,
|
|
9621
|
+
width: width2,
|
|
9622
|
+
height: height2,
|
|
9623
|
+
videoFileName
|
|
9624
|
+
});
|
|
9625
|
+
const renderResult = await renderWithEmbeddedVideo2({
|
|
9626
|
+
componentCode: code,
|
|
9627
|
+
componentName: name,
|
|
9628
|
+
width: width2,
|
|
9629
|
+
height: height2,
|
|
9630
|
+
fps,
|
|
9631
|
+
durationInFrames,
|
|
9632
|
+
videoPath: absVideoPath,
|
|
9633
|
+
videoFileName,
|
|
9634
|
+
outputPath: absOutputPath
|
|
9635
|
+
});
|
|
9636
|
+
return renderResult.success ? { success: true, outputPath: renderResult.outputPath || absOutputPath } : { success: false, error: renderResult.error || "Remotion render failed" };
|
|
9402
9637
|
}
|
|
9403
9638
|
const { width, height } = await getVideoResolution(absVideoPath);
|
|
9404
9639
|
const baseFontSize = customFontSize || Math.round(height / 20);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibeframe/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
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/
|
|
61
|
-
"@vibeframe/
|
|
60
|
+
"@vibeframe/core": "0.35.0",
|
|
61
|
+
"@vibeframe/cli": "0.35.0"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|
|
64
64
|
"node": ">=20"
|