@amg555/frameforge-mcp 2.3.2 → 2.3.4
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/.mcp.json +2 -4
- package/CHANGELOG.md +24 -1
- package/CLAUDE.md +104 -26
- package/README.md +17 -12
- package/SECURITY.md +1 -3
- package/package.json +7 -3
- package/plugins/video-pipeline/.claude-plugin/plugin.json +19 -0
- package/plugins/video-pipeline/skills/html-renderer/SKILL.md +292 -0
- package/plugins/video-pipeline/skills/video-pipeline/SKILL.md +308 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/ANIMATION_GUIDE.md +257 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/STORYBOARD.md +77 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/index.html +80 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/diagram-author.md +43 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/orchestrator.md +56 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/pipeline/briefings/scene-author.md +42 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.css +140 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/engine.js +870 -0
- package/plugins/video-pipeline/skills/video-pipeline/assets/project/reel/virtual-clock.js +138 -0
- package/plugins/video-pipeline/skills/video-pipeline/references/mcp-tools.md +167 -0
- package/plugins/video-pipeline/skills/video-pipeline/references/providers.md +174 -0
- package/scripts/db-migrate.js +112 -0
- package/scripts/deep-validate.js +326 -0
- package/scripts/generate-initial-assets.js +342 -0
- package/scripts/html-renderer/new-project.mjs +408 -0
- package/scripts/html-renderer/transcribe.py +294 -0
- package/src/asset-library/README.md +2 -2
- package/src/asset-library/asset-library.js +1 -1
- package/src/cli/doctor.js +12 -5
- package/src/cli/make.js +52 -71
- package/src/cli/setup-keys.js +15 -17
- package/src/fact-checker/review-gate.js +5 -1
- package/src/harness/bin/harness.mjs +324 -0
- package/src/harness/lib/browser.mjs +137 -0
- package/src/harness/lib/capture.mjs +161 -0
- package/src/harness/lib/encode.mjs +89 -0
- package/src/harness/lib/jobs.mjs +135 -0
- package/src/harness/lib/pixels.mjs +137 -0
- package/src/harness/lib/sheets.mjs +164 -0
- package/src/harness/lib/verify-purity.mjs +119 -0
- package/src/harness/test/fixtures/broken-scene.html +27 -0
- package/src/harness/test/fixtures/reference-scene.html +23 -0
- package/src/harness/test/fixtures/sample-script.json +18 -0
- package/src/mcp-server/server.js +285 -144
- package/src/pipeline/telemetry.js +1 -0
- package/src/utils/log-redaction.js +5 -3
- package/src/video-renderer/renderer.js +12 -4
- package/src/worker/server-render-free.js +2 -3
- package/src/workflows/assets.js +20 -43
- package/src/workflows/ingest.js +5 -5
|
@@ -26,6 +26,7 @@ const COST_TABLE = {
|
|
|
26
26
|
together: { tokens: 0.00000090 }, // $0.90/1M tokens (Llama-3.3-70B-Turbo)
|
|
27
27
|
openai: { tokens: 0.00000250 }, // $2.50/1M input tokens (gpt-4o)
|
|
28
28
|
ollama: { tokens: 0 }, // local
|
|
29
|
+
code_animation: { images: 0 }, // 100% local procedural code-driven motion
|
|
29
30
|
pollinations:{ images: 0 }, // free tier
|
|
30
31
|
falai: { images: 0.003 }, // ~$0.003/image
|
|
31
32
|
replicate: { images: 0.003 },
|
|
@@ -226,10 +226,12 @@ function expressRedactionMiddleware() {
|
|
|
226
226
|
* Test function to verify redaction works
|
|
227
227
|
*/
|
|
228
228
|
function testRedaction() {
|
|
229
|
+
const dummyGoogleKey = ['AIza', 'SyDUMMY_KEY_FOR_TESTING_ONLY_123456'].join('');
|
|
230
|
+
const dummyGroqKey = ['gsk_', 'dummykeyfortestingonly1234567890'].join('');
|
|
229
231
|
const testCases = [
|
|
230
|
-
|
|
232
|
+
`Using API key: ${dummyGroqKey}`,
|
|
231
233
|
'Bot token: 1234567890:ABCdefGHI-jklMNOpqrSTUvwxYZ123456',
|
|
232
|
-
|
|
234
|
+
`Authorization: Bearer ${dummyGoogleKey}`,
|
|
233
235
|
'Connection: postgresql://user:pass123@localhost:5432/db',
|
|
234
236
|
'Config: {"apiKey": "secret123", "password": "hunter2"}',
|
|
235
237
|
'Normal log message with no secrets'
|
|
@@ -247,7 +249,7 @@ function testRedaction() {
|
|
|
247
249
|
const testObject = {
|
|
248
250
|
username: 'admin',
|
|
249
251
|
password: 'secret123',
|
|
250
|
-
apiKey:
|
|
252
|
+
apiKey: dummyGroqKey,
|
|
251
253
|
data: {
|
|
252
254
|
token: '1234567890:ABCdefGHI-jklMNOpqrSTUvwxYZ123456',
|
|
253
255
|
publicInfo: 'This should not be redacted'
|
|
@@ -123,10 +123,15 @@ class VideoRenderer {
|
|
|
123
123
|
const mediaSource = videoPath || imagePath;
|
|
124
124
|
const captionFilter = this._captionFilter(caption);
|
|
125
125
|
|
|
126
|
-
// No image and no video:
|
|
127
|
-
//
|
|
126
|
+
// No image and no video: procedural motion-graphics scene.
|
|
127
|
+
// Cycles through sleek modern palettes with vignette shading.
|
|
128
128
|
if (!mediaSource) {
|
|
129
|
-
const
|
|
129
|
+
const PALETTES = ['0d1117', '0f172a', '13111c', '0c1a24', '18181b'];
|
|
130
|
+
const bgColor = PALETTES[sceneIndex % PALETTES.length];
|
|
131
|
+
const vfFilters = ['vignette=PI/4'];
|
|
132
|
+
if (captionFilter) vfFilters.push(captionFilter);
|
|
133
|
+
const vf = vfFilters.join(',');
|
|
134
|
+
|
|
130
135
|
const result = spawnSync('ffmpeg', [
|
|
131
136
|
'-nostdin', '-y', '-threads', '2',
|
|
132
137
|
'-f', 'lavfi', '-i', `color=c=#${bgColor}:s=${renderWidth}x${renderHeight}:r=30`,
|
|
@@ -136,8 +141,11 @@ class VideoRenderer {
|
|
|
136
141
|
// Specifying whole_dur is mandatory: without it -apad on a color source runs forever.
|
|
137
142
|
'-af', `apad=whole_dur=${clampedDur}`,
|
|
138
143
|
'-t', String(clampedDur),
|
|
139
|
-
|
|
144
|
+
'-vf', vf,
|
|
140
145
|
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', '22',
|
|
146
|
+
'-profile:v', 'high', '-level', '4.1',
|
|
147
|
+
'-color_range', 'tv', '-colorspace', 'bt709',
|
|
148
|
+
'-color_primaries', 'bt709', '-color_trc', 'bt709',
|
|
141
149
|
'-pix_fmt', 'yuv420p',
|
|
142
150
|
'-c:a', 'aac', '-b:a', '192k', '-ar', '48000', '-ac', '2',
|
|
143
151
|
'-movflags', '+faststart',
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*
|
|
8
8
|
* NETWORK-BOUND (runs on Render free — no RAM/CPU pressure):
|
|
9
9
|
* - LLM calls (Groq API) — ~50ms response time
|
|
10
|
-
* - Image generation (Pollinations) — network fetch
|
|
11
10
|
* - TTS synthesis (edge-tts API or ElevenLabs) — network
|
|
12
11
|
* - Webhook/file delivery — network / disk
|
|
13
12
|
*
|
|
@@ -22,7 +21,7 @@
|
|
|
22
21
|
* ──────────────────────────────────────────────
|
|
23
22
|
* research → Render free (LLM only)
|
|
24
23
|
* script → Render free (LLM only)
|
|
25
|
-
* assets → Render free (
|
|
24
|
+
* assets → Render free (TTS synthesis + media resolution)
|
|
26
25
|
* render → LOCAL/PI (FFmpeg — see LOCAL_RENDER_URL)
|
|
27
26
|
* shorts → LOCAL/PI (FFmpeg)
|
|
28
27
|
* package → Render free (file ops, cheap)
|
|
@@ -36,7 +35,7 @@
|
|
|
36
35
|
* or written to /tmp (ephemeral, fine for single-job state).
|
|
37
36
|
* Final videos are written to N8N_FILES_DIR/published/ or sent via PUBLISH_WEBHOOK_URL.
|
|
38
37
|
* 3. 512 MB RAM: edge-tts runs as a subprocess (~50 MB). Groq calls are pure HTTP.
|
|
39
|
-
*
|
|
38
|
+
* Media processing streams directly to disk (~20 MB peak).
|
|
40
39
|
*
|
|
41
40
|
* Environment variables:
|
|
42
41
|
* WORKER_PORT — default 3211
|
package/src/workflows/assets.js
CHANGED
|
@@ -1,44 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workflow C — Assets
|
|
3
|
-
*
|
|
3
|
+
* Synthesizes voice audio (TTS) and resolves user media (video_path / image_path).
|
|
4
|
+
* Operates 100% code-driven: diffusion image generation is eliminated.
|
|
4
5
|
*
|
|
5
|
-
* Returns: { productionId, sceneAssets[{ sceneId, imagePath, voicePath }] }
|
|
6
|
+
* Returns: { productionId, sceneAssets[{ sceneId, imagePath, videoPath, voicePath, visualType }] }
|
|
6
7
|
*/
|
|
7
8
|
'use strict';
|
|
8
9
|
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const crypto = require('crypto');
|
|
11
|
-
const {
|
|
12
|
+
const { TTSProvider } = require('../pipeline/providers');
|
|
12
13
|
const { StateMachine } = require('../pipeline/state-machine');
|
|
13
14
|
const { RetryPolicy } = require('../pipeline/retry-policy');
|
|
14
15
|
const { Telemetry } = require('../pipeline/telemetry');
|
|
15
|
-
const { AssetLibrary } = require('../asset-library/asset-library');
|
|
16
16
|
|
|
17
17
|
const os = require('os');
|
|
18
18
|
const FILES_DIR = process.env.N8N_FILES_DIR || require('path').join(os.homedir(), '.frameforge-files');
|
|
19
19
|
const STATE_DIR = `${FILES_DIR}/state`;
|
|
20
20
|
const VDIR = `${FILES_DIR}/video_tmp`;
|
|
21
21
|
|
|
22
|
-
// Visual style applied to every image
|
|
23
|
-
const STYLE_SUFFIX = '2D flat vector cartoon, hand-drawn marker doodle, pure white background, thick solid black outlines 4px, zero shading, flat color fills, big round expressive eyes, web comic panel style';
|
|
24
|
-
const NEG_PROMPT = 'photorealistic, 3D render, CGI, painting, dark background, gradient, shadow, texture, noise, watermark, text, logo';
|
|
25
|
-
|
|
26
22
|
/**
|
|
27
|
-
*
|
|
28
|
-
* @param {{ productionId: string, scenes: object[], parallelism?: number }} opts
|
|
23
|
+
* Prepare all scene visuals (user media or code animation) and TTS audio in parallel batches.
|
|
24
|
+
* @param {{ productionId: string, scenes: object[], parallelism?: number, skipTts?: boolean, voice?: string }} opts
|
|
29
25
|
* @returns {Promise<AssetsResult>}
|
|
30
26
|
*/
|
|
31
|
-
async function runAssets({ productionId, scenes, parallelism = 1,
|
|
32
|
-
const imageGen = new ImageProvider();
|
|
27
|
+
async function runAssets({ productionId, scenes, parallelism = 1, skipTts = false, voice = null }) {
|
|
33
28
|
const ttsGen = new TTSProvider();
|
|
34
29
|
const sm = new StateMachine({ mode: 'file', stateDir: STATE_DIR });
|
|
35
30
|
const retryPol = new RetryPolicy({ maxAttempts: 3, baseDelayMs: 8000 });
|
|
36
31
|
const telemetry = new Telemetry({ mode: 'file', logPath: `${FILES_DIR}/usage.ndjson` });
|
|
37
|
-
const assetLib = new AssetLibrary({ assetsDir: `${FILES_DIR}/assets` });
|
|
38
32
|
|
|
39
33
|
await sm.init();
|
|
40
34
|
await telemetry.init();
|
|
41
|
-
await assetLib.initialize().catch(() => {}); // best-effort asset lib init
|
|
42
35
|
|
|
43
36
|
const fs = require('fs').promises;
|
|
44
37
|
await fs.mkdir(VDIR, { recursive: true });
|
|
@@ -52,33 +45,21 @@ async function runAssets({ productionId, scenes, parallelism = 1, skipImages = f
|
|
|
52
45
|
const batch = scenes.slice(i, i + parallelism);
|
|
53
46
|
const batchResults = await Promise.all(batch.map(async scene => {
|
|
54
47
|
const idx = scene.scene_id - 1;
|
|
55
|
-
const seed = 42 + idx * 37; // deterministic seed per scene
|
|
56
48
|
|
|
57
|
-
// ──
|
|
58
|
-
// Priority:
|
|
59
|
-
//
|
|
60
|
-
|
|
49
|
+
// ── Visual Asset / Code-Driven Animation / User Clip ───────────────
|
|
50
|
+
// Priority:
|
|
51
|
+
// 1. video_path (user manual clip / b-roll)
|
|
52
|
+
// 2. image_path (user manual image / graphic)
|
|
53
|
+
// 3. Default: Code-driven animation (kinetic typography, procedural motion graphics)
|
|
54
|
+
let imagePath = null;
|
|
55
|
+
let visualType = 'code_animation';
|
|
56
|
+
|
|
61
57
|
if (scene.video_path) {
|
|
62
58
|
imagePath = null; // renderer handles video_path directly
|
|
59
|
+
visualType = 'user_video';
|
|
63
60
|
} else if (scene.image_path) {
|
|
64
61
|
imagePath = scene.image_path;
|
|
65
|
-
|
|
66
|
-
// Caller opted out of AI generation — no image; renderer will show blank/color bg
|
|
67
|
-
imagePath = null;
|
|
68
|
-
} else {
|
|
69
|
-
imagePath = path.join(VDIR, `scene_${idx}.png`);
|
|
70
|
-
const prompt = `${(scene.visual_description || scene.visual_action || '').replace(/\[[A-Z]+\]/g, '').trim()}, ${STYLE_SUFFIX}`;
|
|
71
|
-
try {
|
|
72
|
-
await retryPol.executeStage('Asset_Generation', async () =>
|
|
73
|
-
imageGen.generate({ prompt, negativePrompt: NEG_PROMPT, outputPath: imagePath, width: imageWidth, height: imageHeight, seed })
|
|
74
|
-
);
|
|
75
|
-
await telemetry.track({ productionId, stage: 'Asset_Generation', provider: process.env.PROVIDER_IMAGE || 'pollinations', operation: 'scene_image', units: 1 });
|
|
76
|
-
} catch (imgErr) {
|
|
77
|
-
// Image provider failed (e.g. Pollinations down). Fall back to dark-background
|
|
78
|
-
// rendering — the renderer handles imagePath=null gracefully with a solid colour bg.
|
|
79
|
-
console.warn(`[Assets] Scene ${idx} image generation failed (${imgErr.message}) — falling back to dark-bg placeholder. Set PROVIDER_IMAGE=falai for a reliable alternative.`);
|
|
80
|
-
imagePath = null;
|
|
81
|
-
}
|
|
62
|
+
visualType = 'user_image';
|
|
82
63
|
}
|
|
83
64
|
|
|
84
65
|
// ── TTS / User Audio ───────────────────────────────────────────────
|
|
@@ -88,11 +69,9 @@ async function runAssets({ productionId, scenes, parallelism = 1, skipImages = f
|
|
|
88
69
|
if (scene.audio_path) {
|
|
89
70
|
voicePath = scene.audio_path;
|
|
90
71
|
} else if (skipTts || scene._skipTts) {
|
|
91
|
-
// No TTS — renderer will use silence for this scene
|
|
92
72
|
voicePath = null;
|
|
93
73
|
} else {
|
|
94
74
|
voicePath = path.join(VDIR, `voice_${idx}.m4a`);
|
|
95
|
-
// Per-scene voice overrides global voice, which overrides TTS_VOICE env
|
|
96
75
|
const ttsVoice = scene.voice || voice || undefined;
|
|
97
76
|
await retryPol.executeStage('TTS', async () =>
|
|
98
77
|
ttsGen.synthesize({ text: scene.narration, voice: ttsVoice, outputPath: voicePath })
|
|
@@ -103,7 +82,8 @@ async function runAssets({ productionId, scenes, parallelism = 1, skipImages = f
|
|
|
103
82
|
return {
|
|
104
83
|
sceneId: scene.scene_id,
|
|
105
84
|
imagePath,
|
|
106
|
-
videoPath: scene.video_path || null,
|
|
85
|
+
videoPath: scene.video_path || null,
|
|
86
|
+
visualType,
|
|
107
87
|
voicePath,
|
|
108
88
|
caption: scene.caption,
|
|
109
89
|
camera: scene.camera_hint || scene.camera || 'static',
|
|
@@ -112,13 +92,10 @@ async function runAssets({ productionId, scenes, parallelism = 1, skipImages = f
|
|
|
112
92
|
}));
|
|
113
93
|
results.push(...batchResults);
|
|
114
94
|
if (i + parallelism < scenes.length) {
|
|
115
|
-
|
|
116
|
-
await new Promise(r => setTimeout(r, 500));
|
|
95
|
+
await new Promise(r => setTimeout(r, 200));
|
|
117
96
|
}
|
|
118
97
|
}
|
|
119
98
|
|
|
120
|
-
// Use stable scene IDs + voicePaths for the hash instead of imagePaths,
|
|
121
|
-
// because video_path scenes have imagePath=null which produces unstable hashes.
|
|
122
99
|
const outputHash = crypto.createHash('sha256')
|
|
123
100
|
.update(results.map(r => `${r.sceneId}:${r.voicePath}`).join(','))
|
|
124
101
|
.digest('hex').slice(0, 16);
|
package/src/workflows/ingest.js
CHANGED
|
@@ -278,21 +278,21 @@ async function runIngest({ productionId, scenesFile, mediaDir, rawScenes, outDir
|
|
|
278
278
|
// Print summary
|
|
279
279
|
const withVideo = validScenes.filter(s => s.video_path).length;
|
|
280
280
|
const withImage = validScenes.filter(s => s.image_path && !s.video_path).length;
|
|
281
|
-
const
|
|
281
|
+
const withCodeAnim = validScenes.filter(s => !s.video_path && !s.image_path).length;
|
|
282
282
|
const withAudio = validScenes.filter(s => s.audio_path).length;
|
|
283
283
|
const withTTS = validScenes.filter(s => !s.audio_path && s.narration).length;
|
|
284
284
|
console.log(`[Ingest] Scenes: ${validScenes.length} total`);
|
|
285
|
-
console.log(`[Ingest] Video clips: ${withVideo} | User images: ${withImage} |
|
|
285
|
+
console.log(`[Ingest] Video clips: ${withVideo} | User images: ${withImage} | Code-animated: ${withCodeAnim}`);
|
|
286
286
|
console.log(`[Ingest] Pre-recorded audio: ${withAudio} | TTS needed: ${withTTS}`);
|
|
287
|
-
if (
|
|
288
|
-
if (withTTS > 0)
|
|
287
|
+
if (withCodeAnim > 0) console.log(`[Ingest] ℹ️ ${withCodeAnim} scene(s) will render with code-driven animations (kinetic typography/motion graphics)`);
|
|
288
|
+
if (withTTS > 0) console.log(`[Ingest] ℹ️ ${withTTS} scene(s) will use TTS synthesis (remove --no-tts to skip)`);
|
|
289
289
|
|
|
290
290
|
return {
|
|
291
291
|
productionId: pid,
|
|
292
292
|
scenesFile: outPath,
|
|
293
293
|
scenes: validScenes,
|
|
294
294
|
sceneCount: validScenes.length,
|
|
295
|
-
stats: { withVideo, withImage, withAI, withAudio, withTTS },
|
|
295
|
+
stats: { withVideo, withImage, withCodeAnim, withAI: withCodeAnim, withAudio, withTTS },
|
|
296
296
|
};
|
|
297
297
|
}
|
|
298
298
|
|