@amg555/frameforge-mcp 2.3.6 → 2.3.7

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.
@@ -131,6 +131,13 @@ class VideoRenderer {
131
131
  * renderWidth?: number, // default 1920
132
132
  * renderHeight?: number, // default 1080
133
133
  * fps?: number, // default 24
134
+ * startTime?: number|string,
135
+ * trimStart?: number|string,
136
+ * endTime?: number|string,
137
+ * trimEnd?: number|string,
138
+ * speed?: number, // default 1.0
139
+ * clipVolume?: number, // default 0 (voiceover primary)
140
+ * colorGrade?: string, // cinematic, warm, cool, etc.
134
141
  * }} opts
135
142
  * @returns {string} outputPath
136
143
  */
@@ -157,6 +164,13 @@ class VideoRenderer {
157
164
  renderWidth = 1920,
158
165
  renderHeight = 1080,
159
166
  fps = 24,
167
+ startTime = 0,
168
+ trimStart,
169
+ endTime,
170
+ trimEnd,
171
+ speed = 1.0,
172
+ clipVolume = 0,
173
+ colorGrade = null,
160
174
  } = {}) {
161
175
  const outputPath = path.join(this.vdir, `scene_${sceneIndex}_out.mp4`);
162
176
 
@@ -179,17 +193,48 @@ class VideoRenderer {
179
193
 
180
194
  // Priority 1: User-supplied video clip
181
195
  if (isVideoInput) {
196
+ const { buildColorFilter, parseTimestamp, buildSpeedFilters } = require('../video-editor/editor');
197
+ const startSec = parseTimestamp(trimStart != null ? trimStart : startTime);
198
+ const sp = buildSpeedFilters(speed);
199
+
182
200
  const captionFilter = this._captionFilter(caption);
183
201
  const scaleFilter = `scale=${renderWidth}:${renderHeight}:force_original_aspect_ratio=increase:flags=lanczos,crop=${renderWidth}:${renderHeight}`;
184
- const vf = [scaleFilter, captionFilter].filter(Boolean).join(',');
202
+ const colorFilter = buildColorFilter(colorGrade);
203
+
204
+ const vfList = [scaleFilter];
205
+ if (sp.speed !== 1.0) vfList.push(sp.videoFilter);
206
+ if (colorFilter) vfList.push(colorFilter);
207
+ if (captionFilter) vfList.push(captionFilter);
208
+ vfList.push('format=yuv420p');
209
+ const vf = vfList.join(',');
210
+
211
+ const ffmpegArgs = ['-nostdin', '-y', '-threads', '2'];
212
+
213
+ if (startSec > 0) {
214
+ ffmpegArgs.push('-ss', startSec.toFixed(3));
215
+ }
216
+
217
+ ffmpegArgs.push('-stream_loop', '-1', '-i', videoPath, '-i', voicePath);
218
+
219
+ if (clipVolume > 0) {
220
+ const caParts = [`[0:a]volume=${clipVolume.toFixed(2)}`];
221
+ if (sp.audioFilter) caParts.push(sp.audioFilter);
222
+ caParts.push('[ca]');
223
+ const filterComplex = `[0:v]${vf}[vout];` +
224
+ `${caParts.join(',')};` +
225
+ `[1:a]apad=whole_dur=${clampedDur}[va];` +
226
+ `[ca][va]amix=inputs=2:duration=first:dropout_transition=2[aout]`;
227
+ ffmpegArgs.push('-filter_complex', filterComplex, '-map', '[vout]', '-map', '[aout]');
228
+ } else {
229
+ ffmpegArgs.push(
230
+ '-vf', vf,
231
+ '-af', `apad=whole_dur=${clampedDur}`,
232
+ '-map', '0:v:0', '-map', '1:a:0'
233
+ );
234
+ }
185
235
 
186
- const result = spawnSync('ffmpeg', [
187
- '-nostdin', '-y', '-threads', '2',
188
- '-stream_loop', '-1', '-i', videoPath, '-i', voicePath,
189
- '-vf', vf || `scale=${renderWidth}:${renderHeight},format=yuv420p`,
190
- '-af', `apad=whole_dur=${clampedDur}`,
236
+ ffmpegArgs.push(
191
237
  '-t', String(clampedDur),
192
- '-map', '0:v:0', '-map', '1:a:0',
193
238
  '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '22',
194
239
  '-profile:v', 'high', '-level', '4.1',
195
240
  '-color_range', 'tv', '-colorspace', 'bt709',
@@ -198,8 +243,10 @@ class VideoRenderer {
198
243
  '-c:a', 'aac', '-b:a', '192k', '-ar', '48000', '-ac', '2',
199
244
  '-movflags', '+faststart',
200
245
  '-pix_fmt', 'yuv420p',
201
- outputPath,
202
- ], { timeout: 5 * 60 * 1000, encoding: 'utf8' });
246
+ outputPath
247
+ );
248
+
249
+ const result = spawnSync('ffmpeg', ffmpegArgs, { timeout: 5 * 60 * 1000, encoding: 'utf8' });
203
250
 
204
251
  if (result.status !== 0) {
205
252
  const err = `Scene ${sceneIndex} render failed: ${(result.stderr || '').slice(-300)}`;
@@ -577,4 +624,9 @@ class VideoRenderer {
577
624
  }
578
625
  }
579
626
 
580
- module.exports = { VideoRenderer };
627
+ module.exports = {
628
+ VideoRenderer,
629
+ resolveFont,
630
+ FONTS,
631
+ INDIC_FONTS,
632
+ };
@@ -177,6 +177,11 @@ async function dispatchJob(job) {
177
177
  case 'shorts': return runShorts(payload);
178
178
  case 'package': return runPackaging(payload);
179
179
  case 'publish': return runPublishing(payload);
180
+ case 'edit_video': {
181
+ const { VideoEditor } = require('../video-editor/editor');
182
+ const editor = new VideoEditor({ filesDir: FILES_DIR });
183
+ return editor.editAndAssemble(payload);
184
+ }
180
185
  case 'full_pipeline': return runFullPipeline(payload, job.id);
181
186
  default: throw new Error(`Unknown job type: ${type}`);
182
187
  }
@@ -96,6 +96,12 @@ async function executeJob(job) {
96
96
  case 'shorts': result = await runShorts(job.payload); break;
97
97
  case 'package': result = await runPackaging(job.payload); break;
98
98
  case 'publish': result = await runPublishing(job.payload); break;
99
+ case 'edit_video': {
100
+ const { VideoEditor } = require('../video-editor/editor');
101
+ const editor = new VideoEditor({ filesDir: FILES_DIR });
102
+ result = await editor.editAndAssemble(job.payload);
103
+ break;
104
+ }
99
105
  default: throw new Error(`Unknown job type: ${job.type}`);
100
106
  }
101
107
  job.status = 'SUCCEEDED';
@@ -109,6 +109,11 @@ async function runAssets({
109
109
  language: scene.language || language || '',
110
110
  html: scene.html || null,
111
111
  code: scene.code || scene.animation_code || null,
112
+ startTime: scene.start_time != null ? scene.start_time : scene.trim_start,
113
+ endTime: scene.end_time != null ? scene.end_time : scene.trim_end,
114
+ speed: scene.speed != null ? scene.speed : 1.0,
115
+ clipVolume: scene.clip_volume != null ? scene.clip_volume : (scene.clipVolume != null ? scene.clipVolume : 0),
116
+ colorGrade: scene.color_grade || scene.colorGrade || null,
112
117
  };
113
118
  }));
114
119
  results.push(...batchResults);
@@ -72,6 +72,11 @@ async function runRender({
72
72
  focalPoint: asset.focalPoint || null,
73
73
  html: asset.html || null,
74
74
  code: asset.code || null,
75
+ startTime: asset.startTime,
76
+ endTime: asset.endTime,
77
+ speed: asset.speed,
78
+ clipVolume: asset.clipVolume,
79
+ colorGrade: asset.colorGrade,
75
80
  renderWidth,
76
81
  renderHeight,
77
82
  });