@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.
Files changed (2) hide show
  1. package/dist/index.js +266 -31
  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
 
@@ -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
- - Each scene narration MUST be 12-25 words (fits within 5-10 seconds of speech)
2708
- - NEVER exceed 30 words per scene narration \u2014 long content MUST be split into multiple scenes
2709
- - 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)
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 fetch(`${this.baseUrl}/${endpoint}`, {
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": "2024-11-06"
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": "2024-11-06"
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": "2024-11-06"
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": "2024-11-06"
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
- 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) {
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. Your FFmpeg was built without libfreetype.${hint}`
9399
- };
9400
- }
9401
- } 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" };
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.33.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/cli": "0.33.0",
61
- "@vibeframe/core": "0.33.0"
60
+ "@vibeframe/core": "0.35.0",
61
+ "@vibeframe/cli": "0.35.0"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=20"