@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.
@@ -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 || 'static',
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,
@@ -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: typeof raw.duration === 'number' ? raw.duration : null,
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
- const parsed = JSON.parse(content);
221
- rawList = Array.isArray(parsed) ? parsed : (parsed.scenes || parsed.data || []);
222
- baseDir = path.dirname(absPath);
223
- console.log(`[Ingest] Loaded ${rawList.length} scene(s) from: ${absPath}`);
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 };
@@ -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 || 'static',
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,