@amg555/frameforge-mcp 2.3.5 → 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.
@@ -29,7 +29,7 @@ const DEFAULT_FILES_DIR = process.env.N8N_FILES_DIR || path.join(os.homedir(), '
29
29
 
30
30
  const { buildCameraFilter, selectCameraHint } = require('../animation/animation-system');
31
31
  const { AudioMixer } = require('../audio-mixer/mixer');
32
- const { generateSceneHtml } = require('./scene-animator');
32
+ const { generateSceneHtml, wrapCodeInHarnessHtml, detectAesthetic, AESTHETICS } = require('./scene-animator');
33
33
  const { findChromePath } = require('../harness/lib/browser.mjs');
34
34
 
35
35
  // ── Cross-platform Indic / Malayalam font resolver ────────────────────────────
@@ -117,14 +117,27 @@ class VideoRenderer {
117
117
  * narration?: string,
118
118
  * visualDescription?: string,
119
119
  * visualType?: string,
120
+ * topic?: string,
121
+ * style?: string,
122
+ * theme?: string,
123
+ * title?: string,
120
124
  * language?: string,
121
125
  * musicMood?: string,
122
126
  * camera?: string, // camera_hint from scene schema v1.0.0
123
127
  * focalPoint?: { x: number, y: number },
128
+ * html?: string, // direct agent/user pure-t HTML scene
129
+ * code?: string, // direct agent/user canvas seek(t) drawing code
124
130
  * manifest?: ArtifactManifest // optional — enables idempotency
125
131
  * renderWidth?: number, // default 1920
126
132
  * renderHeight?: number, // default 1080
127
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.
128
141
  * }} opts
129
142
  * @returns {string} outputPath
130
143
  */
@@ -137,14 +150,27 @@ class VideoRenderer {
137
150
  narration = '',
138
151
  visualDescription = '',
139
152
  visualType = 'code_animation',
153
+ topic = '',
154
+ style = '',
155
+ theme = '',
156
+ title = '',
140
157
  language = '',
141
158
  musicMood = 'neutral',
142
159
  camera,
143
160
  focalPoint,
161
+ html = null,
162
+ code = null,
144
163
  manifest,
145
164
  renderWidth = 1920,
146
165
  renderHeight = 1080,
147
166
  fps = 24,
167
+ startTime = 0,
168
+ trimStart,
169
+ endTime,
170
+ trimEnd,
171
+ speed = 1.0,
172
+ clipVolume = 0,
173
+ colorGrade = null,
148
174
  } = {}) {
149
175
  const outputPath = path.join(this.vdir, `scene_${sceneIndex}_out.mp4`);
150
176
 
@@ -167,17 +193,48 @@ class VideoRenderer {
167
193
 
168
194
  // Priority 1: User-supplied video clip
169
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
+
170
200
  const captionFilter = this._captionFilter(caption);
171
201
  const scaleFilter = `scale=${renderWidth}:${renderHeight}:force_original_aspect_ratio=increase:flags=lanczos,crop=${renderWidth}:${renderHeight}`;
172
- 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
+ }
173
235
 
174
- const result = spawnSync('ffmpeg', [
175
- '-nostdin', '-y', '-threads', '2',
176
- '-stream_loop', '-1', '-i', videoPath, '-i', voicePath,
177
- '-vf', vf || `scale=${renderWidth}:${renderHeight},format=yuv420p`,
178
- '-af', `apad=whole_dur=${clampedDur}`,
236
+ ffmpegArgs.push(
179
237
  '-t', String(clampedDur),
180
- '-map', '0:v:0', '-map', '1:a:0',
181
238
  '-c:v', 'libx264', '-preset', 'veryfast', '-crf', '22',
182
239
  '-profile:v', 'high', '-level', '4.1',
183
240
  '-color_range', 'tv', '-colorspace', 'bt709',
@@ -186,8 +243,10 @@ class VideoRenderer {
186
243
  '-c:a', 'aac', '-b:a', '192k', '-ar', '48000', '-ac', '2',
187
244
  '-movflags', '+faststart',
188
245
  '-pix_fmt', 'yuv420p',
189
- outputPath,
190
- ], { timeout: 5 * 60 * 1000, encoding: 'utf8' });
246
+ outputPath
247
+ );
248
+
249
+ const result = spawnSync('ffmpeg', ffmpegArgs, { timeout: 5 * 60 * 1000, encoding: 'utf8' });
191
250
 
192
251
  if (result.status !== 0) {
193
252
  const err = `Scene ${sceneIndex} render failed: ${(result.stderr || '').slice(-300)}`;
@@ -212,6 +271,13 @@ class VideoRenderer {
212
271
  caption,
213
272
  narration,
214
273
  visualDescription,
274
+ topic,
275
+ style,
276
+ theme,
277
+ title,
278
+ language,
279
+ html,
280
+ code,
215
281
  clampedDur,
216
282
  renderWidth,
217
283
  renderHeight,
@@ -237,16 +303,13 @@ class VideoRenderer {
237
303
  const cameraFilter = buildCameraFilter(hint, { duration: clampedDur, w: renderWidth, h: renderHeight, ...fx });
238
304
  vf = [cameraFilter, captionFilter].filter(Boolean).join(',');
239
305
  } else {
240
- const PALETTES = [
241
- { bg: '0d1117', c1: '38bdf8' },
242
- { bg: '0f172a', c1: '818cf8' },
243
- { bg: '13111c', c1: 'c084fc' },
244
- { bg: '0c1a24', c1: '34d399' },
245
- { bg: '18181b', c1: 'fbbf24' },
246
- ];
247
- const pal = PALETTES[sceneIndex % PALETTES.length];
306
+ const aestheticKey = detectAesthetic({ topic, style, theme, title, narration, visualDescription, caption, language, musicMood });
307
+ const aesthetic = AESTHETICS[aestheticKey] || AESTHETICS.cyber_tech;
308
+ const pal = aesthetic.palettes[sceneIndex % aesthetic.palettes.length];
309
+ const c0 = (pal.bg0 || '#0d1117').replace('#', '');
310
+ const c1 = (pal.accent || '#38bdf8').replace('#', '');
248
311
  inputArgs = [
249
- '-f', 'lavfi', '-i', `gradients=s=${renderWidth}x${renderHeight}:r=30:c0=0x${pal.bg}:c1=0x${pal.c1}:speed=0.5:type=radial`,
312
+ '-f', 'lavfi', '-i', `gradients=s=${renderWidth}x${renderHeight}:r=30:c0=0x${c0}:c1=0x${c1}:speed=0.5:type=radial`,
250
313
  '-i', voicePath,
251
314
  ];
252
315
  const vfFilters = ['vignette=PI/4'];
@@ -289,6 +352,13 @@ class VideoRenderer {
289
352
  caption,
290
353
  narration,
291
354
  visualDescription,
355
+ topic = '',
356
+ style = '',
357
+ theme = '',
358
+ title = '',
359
+ language = '',
360
+ html = null,
361
+ code = null,
292
362
  clampedDur,
293
363
  renderWidth,
294
364
  renderHeight,
@@ -297,17 +367,40 @@ class VideoRenderer {
297
367
  manifest,
298
368
  musicMood,
299
369
  }) {
300
- const htmlContent = generateSceneHtml({
301
- caption,
302
- narration,
303
- visualDescription,
304
- imagePath,
305
- duration: clampedDur,
306
- width: renderWidth,
307
- height: renderHeight,
308
- sceneIndex,
309
- musicMood,
310
- });
370
+ let htmlContent;
371
+ if (html && typeof html === 'string' && html.trim().length > 0) {
372
+ htmlContent = html;
373
+ if (!htmlContent.includes('window.DURATION')) {
374
+ htmlContent = htmlContent.replace('<script>', `<script>\nwindow.DURATION = ${clampedDur};\n`);
375
+ }
376
+ } else if (code && typeof code === 'string' && code.trim().length > 0) {
377
+ htmlContent = wrapCodeInHarnessHtml({
378
+ code,
379
+ duration: clampedDur,
380
+ width: renderWidth,
381
+ height: renderHeight,
382
+ language,
383
+ style,
384
+ topic,
385
+ });
386
+ } else {
387
+ htmlContent = generateSceneHtml({
388
+ caption,
389
+ narration,
390
+ visualDescription,
391
+ imagePath,
392
+ topic,
393
+ style,
394
+ theme,
395
+ title,
396
+ language,
397
+ duration: clampedDur,
398
+ width: renderWidth,
399
+ height: renderHeight,
400
+ sceneIndex,
401
+ musicMood,
402
+ });
403
+ }
311
404
 
312
405
  const animHtmlPath = path.join(this.vdir, `scene_${sceneIndex}_anim.html`);
313
406
  const framesDir = path.join(this.vdir, `scene_${sceneIndex}_frames`);
@@ -531,4 +624,9 @@ class VideoRenderer {
531
624
  }
532
625
  }
533
626
 
534
- module.exports = { VideoRenderer };
627
+ module.exports = {
628
+ VideoRenderer,
629
+ resolveFont,
630
+ FONTS,
631
+ INDIC_FONTS,
632
+ };