Skip to content

Commit 5cc3b11

Browse files
authored
feat: dual-orientation infographic generation from per-scene prompts
Task C: Infographic generation scale-up for dual orientations\n\n- New generateFromScenePrompts() generates both 16:9 and 9:16 for each prompt\n- check-research reads imagePrompts from script scenes, generates both orientations\n- Uploads to Sanity: infographicsHorizontal + infographicsVertical at doc root\n- Distributes CDN URLs back to script.scenes[].infographicUrls for Remotion\n- Rate limiting: 2s pause every 5 prompts to avoid Imagen throttling\n- DEFAULT_INSTRUCTIONS updated to Alex's brand style (black bg, #15b27b green)\n- Enrichment prompt updated with imagePrompts requirements\n- Backward compat: still writes to infographics field + researchData
1 parent d69587c commit 5cc3b11

2 files changed

Lines changed: 209 additions & 43 deletions

File tree

app/api/cron/check-research/route.ts

Lines changed: 129 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { type NextRequest } from 'next/server';
55
import { createClient, type SanityClient } from 'next-sanity';
66
import { apiVersion, dataset, projectId } from '@/sanity/lib/api';
77
import { pollResearch, parseResearchReport } from '@/lib/services/gemini-research';
8-
import { generateInfographicsForTopic } from '@/lib/services/gemini-infographics';
8+
import { generateInfographicsForTopic, generateFromScenePrompts } from '@/lib/services/gemini-infographics';
99
import { generateWithGemini, stripCodeFences } from '@/lib/gemini';
1010
import { getConfigValue } from '@/lib/config';
1111
import type { ResearchPayload } from '@/lib/services/research';
@@ -33,6 +33,7 @@ interface PipelineDoc {
3333
visualDescription: string;
3434
bRollKeywords: string[];
3535
durationEstimate: number;
36+
imagePrompts?: string[];
3637
code?: { snippet: string; language: string; highlightLines?: number[] };
3738
list?: { items: string[]; icon?: string };
3839
comparison?: {
@@ -62,6 +63,7 @@ interface EnrichedScript {
6263
visualDescription: string;
6364
bRollKeywords: string[];
6465
durationEstimate: number;
66+
imagePrompts?: string[];
6567
code?: { snippet: string; language: string; highlightLines?: number[] };
6668
list?: { items: string[]; icon?: string };
6769
comparison?: {
@@ -235,46 +237,106 @@ async function stepResearchComplete(
235237
} catch { /* ignore */ }
236238
}
237239

238-
try {
239-
// Generate all infographics using Imagen 4 Fast
240-
const batchResult = await generateInfographicsForTopic(doc.title, briefing);
241-
242-
console.log(`[check-research] Generated ${batchResult.results.length} infographics, ${batchResult.errors.length} failed`);
240+
// Collect imagePrompts from the script scenes (if available)
241+
// Collect imagePrompts from script scenes, tracking which scene each belongs to
242+
const scenePromptMap: Array<{ sceneNumber: number; promptCount: number }> = [];
243+
const sceneImagePrompts: string[] = [];
244+
if (doc.script?.scenes) {
245+
for (const scene of doc.script.scenes) {
246+
if (scene.imagePrompts && Array.isArray(scene.imagePrompts)) {
247+
scenePromptMap.push({ sceneNumber: scene.sceneNumber, promptCount: scene.imagePrompts.length });
248+
sceneImagePrompts.push(...scene.imagePrompts);
249+
}
250+
}
251+
}
243252

244-
// Upload each generated image to Sanity
245-
const infographicRefs: Array<{
246-
_type: 'image';
247-
_key: string;
248-
alt?: string;
253+
try {
254+
let horizontalRefs: Array<{
255+
_type: 'image'; _key: string; alt?: string;
249256
asset: { _type: 'reference'; _ref: string };
250257
}> = [];
251-
const infographicUrls: string[] = [];
258+
let verticalRefs: Array<{
259+
_type: 'image'; _key: string; alt?: string;
260+
asset: { _type: 'reference'; _ref: string };
261+
}> = [];
262+
let infographicUrls: string[] = [];
263+
let verticalUrls: string[] = [];
252264

253-
for (let i = 0; i < batchResult.results.length; i++) {
254-
const imgResult = batchResult.results[i];
255-
try {
256-
const buffer = Buffer.from(imgResult.imageBase64, 'base64');
257-
const filename = `infographic-${doc._id}-${i}.png`;
265+
if (sceneImagePrompts.length > 0) {
266+
// NEW PATH: Generate from per-scene prompts in both orientations
267+
console.log(`[check-research] Generating ${sceneImagePrompts.length} scene-specific infographics \u00d7 2 orientations`);
268+
const dualResult = await generateFromScenePrompts(sceneImagePrompts, doc.title);
258269

259-
const asset = await writeClient.assets.upload('image', buffer, {
260-
filename,
261-
contentType: imgResult.mimeType,
262-
});
270+
// Upload horizontal images to Sanity
271+
for (let i = 0; i < dualResult.horizontal.length; i++) {
272+
const imgResult = dualResult.horizontal[i];
273+
try {
274+
const buffer = Buffer.from(imgResult.imageBase64, 'base64');
275+
const filename = `infographic-h-${doc._id}-${i}.png`;
276+
const asset = await writeClient.assets.upload('image', buffer, {
277+
filename, contentType: imgResult.mimeType,
278+
});
279+
horizontalRefs.push({
280+
_type: 'image', _key: `h-${i}`,
281+
alt: `Infographic ${i + 1} for ${doc.title}`,
282+
asset: { _type: 'reference', _ref: asset._id },
283+
});
284+
const cdnUrl = `https://cdn.sanity.io/images/${projectId}/${dataset}/${asset._id.replace('image-', '').replace('-png', '.png').replace('-jpg', '.jpg')}`;
285+
infographicUrls.push(cdnUrl);
286+
} catch (err) {
287+
console.warn(`[check-research] Failed to upload horizontal infographic ${i}:`, err instanceof Error ? err.message : err);
288+
}
289+
}
263290

264-
console.log(`[check-research] Uploaded infographic ${i + 1}: ${asset._id}`);
291+
// Upload vertical images to Sanity
292+
for (let i = 0; i < dualResult.vertical.length; i++) {
293+
const imgResult = dualResult.vertical[i];
294+
try {
295+
const buffer = Buffer.from(imgResult.imageBase64, 'base64');
296+
const filename = `infographic-v-${doc._id}-${i}.png`;
297+
const asset = await writeClient.assets.upload('image', buffer, {
298+
filename, contentType: imgResult.mimeType,
299+
});
300+
verticalRefs.push({
301+
_type: 'image', _key: `v-${i}`,
302+
alt: `Infographic vertical ${i + 1} for ${doc.title}`,
303+
asset: { _type: 'reference', _ref: asset._id },
304+
});
305+
const cdnUrl = `https://cdn.sanity.io/images/${projectId}/${dataset}/${asset._id.replace('image-', '').replace('-png', '.png').replace('-jpg', '.jpg')}`;
306+
verticalUrls.push(cdnUrl);
307+
} catch (err) {
308+
console.warn(`[check-research] Failed to upload vertical infographic ${i}:`, err instanceof Error ? err.message : err);
309+
}
310+
}
265311

266-
infographicRefs.push({
267-
_type: 'image',
268-
_key: `infographic-${i}`,
269-
alt: `Research infographic ${i + 1} for ${doc.title}`,
270-
asset: { _type: 'reference', _ref: asset._id },
271-
});
312+
if (dualResult.errors.length > 0) {
313+
console.warn(`[check-research] ${dualResult.errors.length} infographic generation errors`);
314+
}
315+
} else {
316+
// FALLBACK: Use topic-level generation (existing behavior)
317+
console.log(`[check-research] No scene imagePrompts \u2014 falling back to topic-level generation`);
318+
const batchResult = await generateInfographicsForTopic(doc.title, briefing);
272319

273-
// Build CDN URL for backward compat
274-
const cdnUrl = `https://cdn.sanity.io/images/${projectId}/${dataset}/${asset._id.replace('image-', '').replace('-png', '.png').replace('-jpg', '.jpg')}`;
275-
infographicUrls.push(cdnUrl);
276-
} catch (err) {
277-
console.warn(`[check-research] Failed to upload infographic ${i}:`, err instanceof Error ? err.message : err);
320+
console.log(`[check-research] Generated ${batchResult.results.length} infographics, ${batchResult.errors.length} failed`);
321+
322+
for (let i = 0; i < batchResult.results.length; i++) {
323+
const imgResult = batchResult.results[i];
324+
try {
325+
const buffer = Buffer.from(imgResult.imageBase64, 'base64');
326+
const filename = `infographic-${doc._id}-${i}.png`;
327+
const asset = await writeClient.assets.upload('image', buffer, {
328+
filename, contentType: imgResult.mimeType,
329+
});
330+
horizontalRefs.push({
331+
_type: 'image', _key: `infographic-${i}`,
332+
alt: `Research infographic ${i + 1} for ${doc.title}`,
333+
asset: { _type: 'reference', _ref: asset._id },
334+
});
335+
const cdnUrl = `https://cdn.sanity.io/images/${projectId}/${dataset}/${asset._id.replace('image-', '').replace('-png', '.png').replace('-jpg', '.jpg')}`;
336+
infographicUrls.push(cdnUrl);
337+
} catch (err) {
338+
console.warn(`[check-research] Failed to upload infographic ${i}:`, err instanceof Error ? err.message : err);
339+
}
278340
}
279341
}
280342

@@ -284,21 +346,45 @@ async function stepResearchComplete(
284346
try { researchData = JSON.parse(doc.researchData); } catch { /* ignore */ }
285347
}
286348
researchData.infographicUrls = infographicUrls;
349+
if (verticalUrls.length > 0) {
350+
researchData.infographicVerticalUrls = verticalUrls;
351+
}
287352

288353
const patchData: Record<string, unknown> = {
289354
status: 'enriching',
290355
researchData: JSON.stringify(researchData),
291356
};
292-
if (infographicRefs.length > 0) {
293-
patchData.infographics = infographicRefs;
357+
if (horizontalRefs.length > 0) {
358+
patchData.infographicsHorizontal = horizontalRefs;
359+
}
360+
if (verticalRefs.length > 0) {
361+
patchData.infographicsVertical = verticalRefs;
362+
}
363+
// Keep backward compat with old infographics field
364+
if (horizontalRefs.length > 0) {
365+
patchData.infographics = horizontalRefs;
366+
}
367+
368+
// Distribute infographic URLs back to scene-level for Remotion mapInputProps()
369+
if (doc.script?.scenes && infographicUrls.length > 0) {
370+
let urlIndex = 0;
371+
const updatedScenes = doc.script.scenes.map((scene) => {
372+
const mapping = scenePromptMap.find(m => m.sceneNumber === scene.sceneNumber);
373+
if (mapping && mapping.promptCount > 0) {
374+
const sceneUrls = infographicUrls.slice(urlIndex, urlIndex + mapping.promptCount);
375+
urlIndex += mapping.promptCount;
376+
return { ...scene, infographicUrls: sceneUrls };
377+
}
378+
return scene;
379+
});
380+
patchData['script'] = { ...doc.script, scenes: updatedScenes };
294381
}
295382

296383
await sanity.patch(doc._id).set(patchData).commit();
297384

298-
console.log(`[check-research] "${doc.title}" enriching (${infographicRefs.length} infographics)`);
385+
console.log(`[check-research] "${doc.title}" \u2192 enriching (${horizontalRefs.length}H + ${verticalRefs.length}V infographics)`);
299386
return { id: doc._id, title: doc.title, step: 'research_complete', outcome: 'enriching' };
300387
} catch (err) {
301-
// Infographic generation failed — skip to enriching without infographics
302388
console.error(`[check-research] Infographic generation failed for "${doc.title}":`, err);
303389
await sanity.patch(doc._id).set({ status: 'enriching' }).commit();
304390
return { id: doc._id, title: doc.title, step: 'research_complete', outcome: 'enriching_no_infographics', error: err instanceof Error ? err.message : String(err) };
@@ -514,7 +600,8 @@ Return ONLY a JSON object:
514600
"code": { "snippet": "string", "language": "string", "highlightLines": [1, 3] },
515601
"list": { "items": ["Item 1", "Item 2"], "icon": "🚀" },
516602
"comparison": { "leftLabel": "A", "rightLabel": "B", "rows": [{ "left": "...", "right": "..." }] },
517-
"mockup": { "deviceType": "browser | phone | terminal", "screenContent": "..." }
603+
"mockup": { "deviceType": "browser | phone | terminal", "screenContent": "..." },
604+
"imagePrompts": ["Infographic 2D architecture style, black background. [specific visual for this scene]. Highlighted elements filled with #15b27b. White lines connecting components and white text annotations."]
518605
}
519606
],
520607
"cta": "string - call to action"
@@ -525,6 +612,10 @@ Return ONLY a JSON object:
525612
Requirements:
526613
- 3-5 scenes totaling 60-90 seconds
527614
- Use at least 2 different scene types
615+
- Each scene MUST include 2-5 imagePrompts following this exact template: "Infographic 2D architecture style, black background. [specific visual]. Highlighted elements filled with #15b27b. White lines connecting components and white text annotations."
616+
- imagePrompts should describe specific 2D infographic visuals that illustrate the narration content
617+
- Do NOT include any script text, titles, or word overlays in the video. The narration audio carries all words.
618+
- Think of each imagePrompt as a frame that will be shown for 3-5 seconds while the narration plays
528619
- Include REAL code snippets from the research where applicable
529620
- The qualityScore should be your honest self-assessment (0-100)
530621
- Return ONLY the JSON object, no markdown or extra text`;

lib/services/gemini-infographics.ts

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ export interface InfographicRequest {
4343
negativePrompt?: string;
4444
}
4545

46+
/** Result for dual-orientation generation */
47+
export interface DualOrientationResult {
48+
horizontal: InfographicResult[];
49+
vertical: InfographicResult[];
50+
errors: Array<{ prompt: string; error: string }>;
51+
}
52+
4653
/** Options for batch infographic generation. */
4754
export interface InfographicBatchOptions {
4855
/** Override the Imagen model (defaults to pipeline_config.infographicModel or "imagen-4-fast"). */
@@ -205,11 +212,11 @@ export function buildInfographicPrompt(
205212

206213
/** Default infographic instructions if Sanity contentConfig is not set up */
207214
const DEFAULT_INSTRUCTIONS: string[] = [
208-
'Create a technical architecture sketch using white "hand-drawn" ink lines on a deep navy blue background (#003366). Use rough-sketched server and database icons with visible "marker" strokes, handwritten labels in a casual font, and a subtle grid pattern. Style: blueprint meets whiteboard doodle.',
209-
'Create a comparison chart on a vibrant blue background (#004080) with hand-inked white headers and uneven, sketchy borders. Use white cross-hatching and doodle-style checkmarks to highlight feature differences. Include hand-drawn arrows and annotations. Style: technical chalkboard.',
210-
'Create a step-by-step workflow "blueprint" on a dark blue canvas (#003366). Use hand-drawn white arrows connecting rough-sketched boxes, simple "stick-figure" style worker avatars, and handwritten-style labels with a slight chalk texture. Add a subtle grid background. Style: engineering whiteboard.',
211-
'Create a hand-sketched timeline using a jagged white line on a royal blue background. Represent milestones with simple, iconic white doodles that look like they were quickly sketched during a brainstorming session. Use handwritten dates and labels. Style: notebook sketch on blue paper.',
212-
'Create a pros and cons summary with a "lo-fi" aesthetic. Use hand-drawn white thumbs-up/down icons and rough-sketched containers on a deep blue background (#003366). Add hand-drawn underlines and circled keywords. Style: high-contrast ink-on-blueprint with cyan accent highlights.',
215+
'Infographic 2D architecture style, black background. A high-level technical architecture overview showing system components and data flow. Highlighted elements filled with #15b27b. White lines connecting components and white text annotations.',
216+
'Infographic 2D architecture style, black background. A comparison chart showing key features and alternatives side by side. Highlighted elements filled with #15b27b. White lines connecting components and white text annotations.',
217+
'Infographic 2D architecture style, black background. A step-by-step workflow diagram showing the process from start to finish. Highlighted elements filled with #15b27b. White lines connecting components and white text annotations.',
218+
'Infographic 2D architecture style, black background. A timeline of key developments, milestones, and version releases. Highlighted elements filled with #15b27b. White lines connecting components and white text annotations.',
219+
'Infographic 2D architecture style, black background. A pros and cons visual summary with clear icons and labels. Highlighted elements filled with #15b27b. White lines connecting components and white text annotations.',
213220
];
214221

215222
// ---------------------------------------------------------------------------
@@ -281,3 +288,71 @@ export async function generateInfographicsForTopic(
281288

282289
return result;
283290
}
291+
292+
// ---------------------------------------------------------------------------
293+
// Dual-orientation generation from per-scene prompts
294+
// ---------------------------------------------------------------------------
295+
296+
/**
297+
* Generate infographics from per-scene image prompts in both orientations.
298+
* This is the new main entry point for the check-research cron.
299+
*
300+
* @param prompts - Array of image prompt strings from the enriched script
301+
* @param topic - Topic name for seed generation
302+
* @returns DualOrientationResult with horizontal (16:9) and vertical (9:16) images
303+
*/
304+
export async function generateFromScenePrompts(
305+
prompts: string[],
306+
topic: string,
307+
): Promise<DualOrientationResult> {
308+
const model = await getConfigValue(
309+
"pipeline_config", "infographicModel", "imagen-4-fast"
310+
);
311+
312+
const horizontal: InfographicResult[] = [];
313+
const vertical: InfographicResult[] = [];
314+
const errors: Array<{ prompt: string; error: string }> = [];
315+
316+
console.log(`[infographics] Generating ${prompts.length} prompts \u00d7 2 orientations (${prompts.length * 2} total) for "${topic}"`);
317+
318+
for (let i = 0; i < prompts.length; i++) {
319+
const prompt = prompts[i];
320+
const seed = generateSeed(topic, i);
321+
322+
// Generate horizontal (16:9)
323+
try {
324+
const hResult = await generateInfographic(
325+
{ prompt, aspectRatio: "16:9", seed },
326+
model,
327+
);
328+
horizontal.push(hResult);
329+
} catch (err) {
330+
const message = err instanceof Error ? err.message : String(err);
331+
errors.push({ prompt: `[16:9] ${prompt}`, error: message });
332+
}
333+
334+
// Generate vertical (9:16)
335+
try {
336+
const vResult = await generateInfographic(
337+
{ prompt, aspectRatio: "9:16", seed },
338+
model,
339+
);
340+
vertical.push(vResult);
341+
} catch (err) {
342+
const message = err instanceof Error ? err.message : String(err);
343+
errors.push({ prompt: `[9:16] ${prompt}`, error: message });
344+
}
345+
346+
// Rate limit: pause every 5 images to avoid Imagen API throttling
347+
if (i > 0 && i % 5 === 0) {
348+
console.log(`[infographics] Progress: ${i}/${prompts.length} prompts processed, pausing for rate limit...`);
349+
await new Promise(r => setTimeout(r, 2000));
350+
}
351+
}
352+
353+
console.log(
354+
`[infographics] Complete: ${horizontal.length} horizontal, ${vertical.length} vertical, ${errors.length} errors`
355+
);
356+
357+
return { horizontal, vertical, errors };
358+
}

0 commit comments

Comments
 (0)