@vibeframe/mcp-server 0.33.1 → 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.
Files changed (2) hide show
  1. package/dist/index.js +231 -28
  2. 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: 3e4 });
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. Install it with:",
85
- " npm install -g @remotion/cli",
86
- "Or ensure npx is available and can download @remotion/cli on demand."
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: "^4.0.0",
95
- "@remotion/cli": "^4.0.0",
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"] = "^4.0.0";
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
- await writeFile4(join(dir, "package.json"), JSON.stringify(packageJson, null, 2));
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
- if (!existsSync(join(dir, "node_modules"))) {
143
- const { execFile: execFile2 } = await import("node:child_process");
144
- const { promisify: promisify2 } = await import("node:util");
145
- const execFileAsync2 = promisify2(execFile2);
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
 
@@ -2736,11 +2874,14 @@ IMPORTANT GUIDELINES:
2736
2874
  - ALWAYS include the character description when the person appears
2737
2875
 
2738
2876
  3. NARRATION LENGTH (CRITICAL for audio-video sync):
2739
- - Each scene narration MUST be 12-25 words (fits within 5-10 seconds of speech)
2740
- - NEVER exceed 30 words per scene narration \u2014 long content MUST be split into multiple scenes
2741
- - Set duration to 5 for short narrations (12-18 words) or 10 for longer ones (19-25 words)
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)
2742
2882
  - If the script has a long paragraph, break it into 2-3 shorter scenes rather than one long narration
2743
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.
2744
2885
 
2745
2886
  4. NARRATION-VISUAL ALIGNMENT: The narration must directly describe what's visible:
2746
2887
  - When narration mentions something specific, the visual must show it
@@ -6650,7 +6791,7 @@ var openaiImageProvider = new OpenAIImageProvider();
6650
6791
 
6651
6792
  // ../ai-providers/dist/runway/RunwayProvider.js
6652
6793
  var DEFAULT_MODEL2 = "gen4.5";
6653
- var RunwayProvider = class {
6794
+ var RunwayProvider = class _RunwayProvider {
6654
6795
  constructor() {
6655
6796
  this.id = "runway";
6656
6797
  this.name = "Runway";
@@ -6715,12 +6856,12 @@ var RunwayProvider = class {
6715
6856
  if (options?.seed !== void 0) {
6716
6857
  body.seed = options.seed;
6717
6858
  }
6718
- const response = await fetch(`${this.baseUrl}/${endpoint}`, {
6859
+ const response = await this.fetchWithRetry(`${this.baseUrl}/${endpoint}`, {
6719
6860
  method: "POST",
6720
6861
  headers: {
6721
6862
  Authorization: `Bearer ${this.apiKey}`,
6722
6863
  "Content-Type": "application/json",
6723
- "X-Runway-Version": "2024-11-06"
6864
+ "X-Runway-Version": _RunwayProvider.API_VERSION
6724
6865
  },
6725
6866
  body: JSON.stringify(body)
6726
6867
  });
@@ -6733,6 +6874,9 @@ var RunwayProvider = class {
6733
6874
  } catch {
6734
6875
  errorMessage = errorText;
6735
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)}`);
6736
6880
  return {
6737
6881
  id: "",
6738
6882
  status: "failed",
@@ -6781,7 +6925,7 @@ var RunwayProvider = class {
6781
6925
  const response = await fetch(`${this.baseUrl}/tasks/${id}`, {
6782
6926
  headers: {
6783
6927
  Authorization: `Bearer ${this.apiKey}`,
6784
- "X-Runway-Version": "2024-11-06"
6928
+ "X-Runway-Version": _RunwayProvider.API_VERSION
6785
6929
  }
6786
6930
  });
6787
6931
  if (!response.ok) {
@@ -6833,7 +6977,7 @@ var RunwayProvider = class {
6833
6977
  method: "POST",
6834
6978
  headers: {
6835
6979
  Authorization: `Bearer ${this.apiKey}`,
6836
- "X-Runway-Version": "2024-11-06"
6980
+ "X-Runway-Version": _RunwayProvider.API_VERSION
6837
6981
  }
6838
6982
  });
6839
6983
  return response.ok;
@@ -6853,7 +6997,7 @@ var RunwayProvider = class {
6853
6997
  method: "DELETE",
6854
6998
  headers: {
6855
6999
  Authorization: `Bearer ${this.apiKey}`,
6856
- "X-Runway-Version": "2024-11-06"
7000
+ "X-Runway-Version": _RunwayProvider.API_VERSION
6857
7001
  }
6858
7002
  });
6859
7003
  return response.ok;
@@ -6882,6 +7026,24 @@ var RunwayProvider = class {
6882
7026
  error: "Generation timed out"
6883
7027
  };
6884
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
+ }
6885
7047
  /**
6886
7048
  * Clamp duration to valid range for the given model
6887
7049
  */
@@ -6908,6 +7070,8 @@ var RunwayProvider = class {
6908
7070
  return new Promise((resolve13) => setTimeout(resolve13, ms));
6909
7071
  }
6910
7072
  };
7073
+ RunwayProvider.API_VERSION = "2024-11-06";
7074
+ RunwayProvider.MAX_RETRIES = 3;
6911
7075
  var runwayProvider = new RunwayProvider();
6912
7076
 
6913
7077
  // ../ai-providers/dist/kling/KlingProvider.js
@@ -9415,9 +9579,17 @@ async function applyTextOverlays(options) {
9415
9579
  if (!commandExists("ffmpeg")) {
9416
9580
  return { success: false, error: "FFmpeg not found. Please install FFmpeg." };
9417
9581
  }
9582
+ let hasDrawtext = true;
9418
9583
  try {
9419
9584
  const { stdout } = await execSafe("ffmpeg", ["-filters"]);
9420
- if (!stdout.includes("drawtext")) {
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) {
9421
9593
  const platform = process.platform;
9422
9594
  let hint = "";
9423
9595
  if (platform === "darwin") {
@@ -9427,10 +9599,41 @@ async function applyTextOverlays(options) {
9427
9599
  }
9428
9600
  return {
9429
9601
  success: false,
9430
- error: `FFmpeg 'drawtext' filter not available. Your FFmpeg was built without libfreetype.${hint}`
9431
- };
9432
- }
9433
- } catch {
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" };
9434
9637
  }
9435
9638
  const { width, height } = await getVideoResolution(absVideoPath);
9436
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.33.1",
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/cli": "0.33.1",
61
- "@vibeframe/core": "0.33.1"
60
+ "@vibeframe/core": "0.35.0",
61
+ "@vibeframe/cli": "0.35.0"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=20"