@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.
- package/CHANGELOG.md +47 -0
- package/README.md +7 -5
- package/package.json +3 -3
- package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +70 -14
- package/scripts/deep-validate.js +5 -3
- package/src/cli/make.js +6 -0
- package/src/mcp-server/server.js +166 -14
- package/src/video-editor/editor.js +852 -0
- package/src/video-renderer/renderer.js +129 -31
- package/src/video-renderer/scene-animator.js +1033 -251
- package/src/worker/server-render-free.js +5 -0
- package/src/worker/server.js +6 -0
- package/src/workflows/assets.js +25 -3
- package/src/workflows/render.js +26 -3
- package/src/workflows/script.js +2 -0
|
@@ -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 }
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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${
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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 = {
|
|
627
|
+
module.exports = {
|
|
628
|
+
VideoRenderer,
|
|
629
|
+
resolveFont,
|
|
630
|
+
FONTS,
|
|
631
|
+
INDIC_FONTS,
|
|
632
|
+
};
|