@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.
- package/CHANGELOG.md +23 -0
- package/README.md +7 -5
- package/package.json +3 -3
- package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +45 -2
- package/scripts/deep-validate.js +5 -3
- package/src/mcp-server/server.js +110 -8
- package/src/video-editor/editor.js +852 -0
- package/src/video-renderer/renderer.js +62 -10
- package/src/worker/server-render-free.js +5 -0
- package/src/worker/server.js +6 -0
- package/src/workflows/assets.js +5 -0
- package/src/workflows/render.js +5 -0
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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
|
}
|
package/src/worker/server.js
CHANGED
|
@@ -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';
|
package/src/workflows/assets.js
CHANGED
|
@@ -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);
|
package/src/workflows/render.js
CHANGED
|
@@ -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
|
});
|