@amg555/frameforge-mcp 2.4.5 → 2.4.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 +25 -0
- package/package.json +2 -2
- package/scripts/deep-validate.js +2 -2
- package/src/cli/make.js +26 -11
- package/src/mcp-server/server.js +6 -2
- package/src/video-renderer/renderer.js +5 -5
- package/src/video-renderer/scene-animator.js +1081 -384
- package/src/workflows/assets.js +2 -1
- package/src/workflows/ingest.js +81 -6
- package/src/workflows/render.js +2 -1
package/src/workflows/assets.js
CHANGED
|
@@ -113,7 +113,7 @@ async function runAssets({
|
|
|
113
113
|
visualType,
|
|
114
114
|
voicePath,
|
|
115
115
|
caption: scene.caption,
|
|
116
|
-
camera: scene.camera_hint || scene.camera || '
|
|
116
|
+
camera: scene.camera_hint || scene.camera || 'auto',
|
|
117
117
|
focalPoint: scene.focal_point || null,
|
|
118
118
|
narration: scene.narration || '',
|
|
119
119
|
prompt: scene.prompt || '',
|
|
@@ -126,6 +126,7 @@ async function runAssets({
|
|
|
126
126
|
language: scene.language || language || '',
|
|
127
127
|
html: scene.html || null,
|
|
128
128
|
code: sceneCode,
|
|
129
|
+
duration: scene.duration != null ? Number(scene.duration) : null,
|
|
129
130
|
startTime: scene.start_time != null ? scene.start_time : scene.trim_start,
|
|
130
131
|
endTime: scene.end_time != null ? scene.end_time : scene.trim_end,
|
|
131
132
|
speed: scene.speed != null ? scene.speed : 1.0,
|
package/src/workflows/ingest.js
CHANGED
|
@@ -131,6 +131,72 @@ function discoverScenesFromDir(mediaDir) {
|
|
|
131
131
|
}));
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Parse a markdown-formatted video script (e.g. from Claude/LLM output)
|
|
136
|
+
* into a structured scenes array with timestamps, visual prompts, and narration.
|
|
137
|
+
*/
|
|
138
|
+
function parseMarkdownScript(content) {
|
|
139
|
+
if (!content || typeof content !== 'string') return { title: '', scenes: [] };
|
|
140
|
+
|
|
141
|
+
function parseTime(str) {
|
|
142
|
+
const parts = str.split(':').map(Number);
|
|
143
|
+
if (parts.length === 2) return parts[0] * 60 + parts[1];
|
|
144
|
+
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
145
|
+
return parseFloat(str) || 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let title = '';
|
|
149
|
+
const titleMatch = content.match(/^#\s+([^\n]+)/m) || content.match(/\*\*([^*]+)\*\*\s*\n\s*\*\*\d+-Minute/i);
|
|
150
|
+
if (titleMatch) title = titleMatch[1].replace(/^[#\s]+/, '').trim();
|
|
151
|
+
|
|
152
|
+
// Split into scene blocks by ## SCENE XX or ### SCENE XX
|
|
153
|
+
const sceneBlocks = content.split(/(?=^#{2,3}\s+SCENE\s+\d+)/mi);
|
|
154
|
+
const scenes = [];
|
|
155
|
+
|
|
156
|
+
for (const block of sceneBlocks) {
|
|
157
|
+
const matchHeader = block.match(/^#{2,3}\s+SCENE\s+(\d+)(?:\s*[-—–:]\s*([^\n]+))?/i);
|
|
158
|
+
if (!matchHeader) continue;
|
|
159
|
+
const sceneId = parseInt(matchHeader[1], 10);
|
|
160
|
+
const sceneTitle = (matchHeader[2] || '').trim();
|
|
161
|
+
|
|
162
|
+
// Time: mm:ss–mm:ss or mm:ss-mm:ss
|
|
163
|
+
const timeMatch = block.match(/\*\*Time:\s*(\d+:\d+(?:\.\d+)?)\s*[–—\-]\s*(\d+:\d+(?:\.\d+)?)\*\*/i);
|
|
164
|
+
let duration = null;
|
|
165
|
+
let startTime = null;
|
|
166
|
+
let endTime = null;
|
|
167
|
+
if (timeMatch) {
|
|
168
|
+
startTime = parseTime(timeMatch[1]);
|
|
169
|
+
endTime = parseTime(timeMatch[2]);
|
|
170
|
+
duration = Math.max(1, endTime - startTime);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Animation prompt / Visual description
|
|
174
|
+
const promptMatch = block.match(/\*\*(?:ANIMATION PROMPT(?:\s*\([^)]*\))?|VISUAL DESCRIPTION|PROMPT):\*\*\s*([\s\S]*?)(?=\n\s*\*\*[A-Z]|\n\s*#{2,3}|$)/i);
|
|
175
|
+
const visualDescription = promptMatch ? promptMatch[1].trim() : '';
|
|
176
|
+
|
|
177
|
+
// Voice over / Narration
|
|
178
|
+
const voMatch = block.match(/\*\*(?:[A-Za-z\s]+)?(?:VOICE\s*OVER|VOICEOVER|NARRATION|SPEECH):\*\*\s*([\s\S]*?)(?=\n\s*#{2,3}|\n\s*\*\*SCENE|$)/i);
|
|
179
|
+
let narration = voMatch ? voMatch[1].replace(/---[\s\S]*$/, '').trim() : '';
|
|
180
|
+
narration = narration.replace(/\\!/g, '!').trim();
|
|
181
|
+
|
|
182
|
+
scenes.push({
|
|
183
|
+
scene_id: sceneId,
|
|
184
|
+
title: sceneTitle,
|
|
185
|
+
duration,
|
|
186
|
+
start_time: startTime,
|
|
187
|
+
end_time: endTime,
|
|
188
|
+
visual_description: visualDescription,
|
|
189
|
+
prompt: visualDescription,
|
|
190
|
+
narration,
|
|
191
|
+
caption: sceneTitle ? sceneTitle.toUpperCase().slice(0, 40) : '',
|
|
192
|
+
camera_hint: 'auto',
|
|
193
|
+
music_mood: 'neutral',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { title, scenes };
|
|
198
|
+
}
|
|
199
|
+
|
|
134
200
|
// ── Manifest normalizer ───────────────────────────────────────────────────────
|
|
135
201
|
|
|
136
202
|
/**
|
|
@@ -184,7 +250,9 @@ function normaliseIngestScene(raw, index, baseDir = process.cwd()) {
|
|
|
184
250
|
sound_effects: Array.isArray(raw.sound_effects) ? raw.sound_effects : [],
|
|
185
251
|
metadata: raw.metadata || { importance: 'medium', hook_potential: 0.5, standalone: false },
|
|
186
252
|
// Duration hint — used by renderer if provided (otherwise audio-driven)
|
|
187
|
-
duration:
|
|
253
|
+
duration: typeof raw.duration === 'number' ? raw.duration : (raw.duration ? Number(raw.duration) : null),
|
|
254
|
+
start_time: typeof raw.start_time === 'number' ? raw.start_time : (raw.startTime != null ? Number(raw.startTime) : null),
|
|
255
|
+
end_time: typeof raw.end_time === 'number' ? raw.end_time : (raw.endTime != null ? Number(raw.endTime) : null),
|
|
188
256
|
};
|
|
189
257
|
}
|
|
190
258
|
|
|
@@ -217,10 +285,17 @@ async function runIngest({ productionId, scenesFile, mediaDir, rawScenes, outDir
|
|
|
217
285
|
const absPath = path.resolve(process.cwd(), scenesFile);
|
|
218
286
|
if (!fs.existsSync(absPath)) throw new Error(`Scenes file not found: ${absPath}`);
|
|
219
287
|
const content = fs.readFileSync(absPath, 'utf8');
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
288
|
+
if (/\.(md|markdown)$/i.test(absPath) || (!content.trim().startsWith('{') && !content.trim().startsWith('['))) {
|
|
289
|
+
const parsedMd = parseMarkdownScript(content);
|
|
290
|
+
rawList = parsedMd.scenes;
|
|
291
|
+
baseDir = path.dirname(absPath);
|
|
292
|
+
console.log(`[Ingest] Parsed ${rawList.length} scene(s) from Markdown script: ${absPath}`);
|
|
293
|
+
} else {
|
|
294
|
+
const parsed = JSON.parse(content);
|
|
295
|
+
rawList = Array.isArray(parsed) ? parsed : (parsed.scenes || parsed.data || []);
|
|
296
|
+
baseDir = path.dirname(absPath);
|
|
297
|
+
console.log(`[Ingest] Loaded ${rawList.length} scene(s) from: ${absPath}`);
|
|
298
|
+
}
|
|
224
299
|
} else if (mediaDir) {
|
|
225
300
|
rawList = discoverScenesFromDir(mediaDir);
|
|
226
301
|
baseDir = path.resolve(mediaDir);
|
|
@@ -299,4 +374,4 @@ async function runIngest({ productionId, scenesFile, mediaDir, rawScenes, outDir
|
|
|
299
374
|
};
|
|
300
375
|
}
|
|
301
376
|
|
|
302
|
-
module.exports = { runIngest, normaliseIngestScene, discoverScenesFromDir };
|
|
377
|
+
module.exports = { runIngest, normaliseIngestScene, discoverScenesFromDir, parseMarkdownScript };
|
package/src/workflows/render.js
CHANGED
|
@@ -69,10 +69,11 @@ async function runRender({
|
|
|
69
69
|
title: asset.title || title || '',
|
|
70
70
|
musicMood: asset.musicMood || 'neutral',
|
|
71
71
|
language: asset.language || language || '',
|
|
72
|
-
camera: asset.camera || '
|
|
72
|
+
camera: asset.camera || 'auto',
|
|
73
73
|
focalPoint: asset.focalPoint || null,
|
|
74
74
|
html: asset.html || null,
|
|
75
75
|
code: asset.code || null,
|
|
76
|
+
duration: asset.duration != null ? asset.duration : null,
|
|
76
77
|
startTime: asset.startTime,
|
|
77
78
|
endTime: asset.endTime,
|
|
78
79
|
speed: asset.speed,
|