Skip to content

Commit d698231

Browse files
authored
feat: Gemini infographic generation service (Task 4)
New infographic generation service using Imagen 4 Fast via @google/genai SDK.\n\nExports: generateInfographic, generateInfographicBatch, generateInfographicsForTopic, buildInfographicPrompt.\nModel + instructions from Sanity config. Deterministic seeds for brand consistency.\nPer-item fault isolation. ~$0.10 per topic (5 images).
1 parent 06ecabb commit d698231

File tree

1 file changed

+283
-0
lines changed

1 file changed

+283
-0
lines changed
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
/**
2+
* Gemini Infographic Generation Service
3+
*
4+
* Generates brand-consistent infographics using Google's Imagen 4 Fast model
5+
* via the @google/genai SDK. Designed for the CodingCat.dev automated video
6+
* pipeline — produces visual assets from research data for use in videos and
7+
* blog posts.
8+
*
9+
* Pricing: Imagen 4 Fast — $0.02/image
10+
* Supports seed-based reproducibility for brand consistency.
11+
*
12+
* @module lib/services/gemini-infographics
13+
*/
14+
15+
import { GoogleGenAI } from "@google/genai";
16+
import { getConfigValue } from "@/lib/config";
17+
18+
// ---------------------------------------------------------------------------
19+
// Types
20+
// ---------------------------------------------------------------------------
21+
22+
/** A single generated infographic result. */
23+
export interface InfographicResult {
24+
/** Base64-encoded PNG image bytes. */
25+
imageBase64: string;
26+
/** MIME type — always "image/png" for Imagen. */
27+
mimeType: string;
28+
/** The prompt used to generate this image. */
29+
prompt: string;
30+
/** Seed used (if provided), for reproducibility. */
31+
seed?: number;
32+
}
33+
34+
/** Options for a single infographic generation request. */
35+
export interface InfographicRequest {
36+
/** Text prompt describing the infographic to generate. */
37+
prompt: string;
38+
/** Aspect ratio. Defaults to "16:9" for video pipeline use. */
39+
aspectRatio?: "1:1" | "3:4" | "4:3" | "9:16" | "16:9";
40+
/** Optional seed for reproducibility (not compatible with watermarks). */
41+
seed?: number;
42+
/** Negative prompt — what to avoid in the image. */
43+
negativePrompt?: string;
44+
}
45+
46+
/** Options for batch infographic generation. */
47+
export interface InfographicBatchOptions {
48+
/** Override the Imagen model (defaults to pipeline_config.infographicModel or "imagen-4-fast"). */
49+
model?: string;
50+
/** Number of images per prompt (1–4). Defaults to 1. */
51+
numberOfImages?: number;
52+
}
53+
54+
/** Result of a batch generation run. */
55+
export interface InfographicBatchResult {
56+
/** Successfully generated infographics. */
57+
results: InfographicResult[];
58+
/** Prompts that failed, with error messages. */
59+
errors: Array<{ prompt: string; error: string }>;
60+
}
61+
62+
// ---------------------------------------------------------------------------
63+
// Internal: lazy client
64+
// ---------------------------------------------------------------------------
65+
66+
let _ai: GoogleGenAI | null = null;
67+
68+
function getAI(): GoogleGenAI {
69+
if (!_ai) {
70+
const apiKey = process.env.GEMINI_API_KEY ?? "";
71+
_ai = new GoogleGenAI({ apiKey });
72+
}
73+
return _ai;
74+
}
75+
76+
// ---------------------------------------------------------------------------
77+
// Core: single image generation
78+
// ---------------------------------------------------------------------------
79+
80+
/**
81+
* Generate a single infographic image using Imagen 4 Fast.
82+
*
83+
* @param request - Prompt and generation options.
84+
* @param model - Imagen model ID (e.g. "imagen-4-fast").
85+
* @returns InfographicResult with base64 image bytes.
86+
* @throws If the API call fails or no image is returned.
87+
*/
88+
export async function generateInfographic(
89+
request: InfographicRequest,
90+
model: string = "imagen-4-fast",
91+
): Promise<InfographicResult> {
92+
const ai = getAI();
93+
94+
const response = await ai.models.generateImages({
95+
model,
96+
prompt: request.prompt,
97+
config: {
98+
numberOfImages: 1,
99+
aspectRatio: request.aspectRatio ?? "16:9",
100+
...(request.seed !== undefined && { seed: request.seed }),
101+
...(request.negativePrompt && { negativePrompt: request.negativePrompt }),
102+
},
103+
});
104+
105+
const generated = response.generatedImages?.[0];
106+
if (!generated?.image?.imageBytes) {
107+
const reason = generated?.raiFilteredReason ?? "unknown";
108+
throw new Error(
109+
`Imagen returned no image for prompt "${request.prompt.slice(0, 80)}…" — RAI reason: ${reason}`,
110+
);
111+
}
112+
113+
const imageBytes = generated.image.imageBytes;
114+
// imageBytes may be a Uint8Array or base64 string depending on SDK version
115+
const imageBase64 =
116+
typeof imageBytes === "string"
117+
? imageBytes
118+
: Buffer.from(imageBytes).toString("base64");
119+
120+
return {
121+
imageBase64,
122+
mimeType: "image/png",
123+
prompt: request.prompt,
124+
...(request.seed !== undefined && { seed: request.seed }),
125+
};
126+
}
127+
128+
// ---------------------------------------------------------------------------
129+
// Batch: generate multiple infographics
130+
// ---------------------------------------------------------------------------
131+
132+
/**
133+
* Generate a batch of infographics from an array of prompts.
134+
*
135+
* Processes requests sequentially to avoid rate-limit issues.
136+
* Failed individual images are collected in `errors` rather than throwing.
137+
*
138+
* @param requests - Array of infographic requests (prompts + options).
139+
* @param options - Batch-level options (model override, numberOfImages).
140+
* @returns InfographicBatchResult with successes and failures.
141+
*
142+
* @example
143+
* ```ts
144+
* const { results, errors } = await generateInfographicBatch([
145+
* { prompt: "A clean infographic showing React hooks lifecycle, dark theme, CodingCat.dev branding" },
146+
* { prompt: "Comparison chart: REST vs GraphQL vs tRPC, developer-friendly, purple accent" },
147+
* ]);
148+
* console.log(`Generated ${results.length} images, ${errors.length} failed`);
149+
* ```
150+
*/
151+
export async function generateInfographicBatch(
152+
requests: InfographicRequest[],
153+
options: InfographicBatchOptions = {},
154+
): Promise<InfographicBatchResult> {
155+
const model =
156+
options.model ??
157+
(await getConfigValue("pipeline_config", "infographicModel", "imagen-4-fast"));
158+
159+
const results: InfographicResult[] = [];
160+
const errors: Array<{ prompt: string; error: string }> = [];
161+
162+
for (const request of requests) {
163+
try {
164+
const result = await generateInfographic(request, model);
165+
results.push(result);
166+
} catch (err) {
167+
const message = err instanceof Error ? err.message : String(err);
168+
errors.push({ prompt: request.prompt, error: message });
169+
}
170+
}
171+
172+
return { results, errors };
173+
}
174+
175+
// ---------------------------------------------------------------------------
176+
// Helpers
177+
// ---------------------------------------------------------------------------
178+
179+
/**
180+
* Build a brand-consistent infographic prompt for CodingCat.dev.
181+
*
182+
* Wraps a topic description with standard brand guidelines so all generated
183+
* infographics share a consistent visual identity.
184+
*
185+
* @param topic - The subject matter (e.g. "React Server Components").
186+
* @param style - Visual style hint (default: "dark tech").
187+
* @returns A fully-formed Imagen prompt string.
188+
*/
189+
export function buildInfographicPrompt(
190+
topic: string,
191+
style: string = "dark tech",
192+
): string {
193+
return (
194+
`Create a professional, visually striking infographic about: ${topic}. ` +
195+
`Style: ${style}, purple and teal accent colors, clean sans-serif typography, ` +
196+
`CodingCat.dev brand aesthetic. ` +
197+
`Layout: structured sections with icons, data visualizations, and clear hierarchy. ` +
198+
`No watermarks. High information density. Developer audience.`
199+
);
200+
}
201+
202+
// ---------------------------------------------------------------------------
203+
// Default instructions (blueprint style from spec)
204+
// ---------------------------------------------------------------------------
205+
206+
/** Default infographic instructions if Sanity contentConfig is not set up */
207+
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.',
213+
];
214+
215+
// ---------------------------------------------------------------------------
216+
// High-level API: generate all infographics for a topic
217+
// ---------------------------------------------------------------------------
218+
219+
/**
220+
* Generate a deterministic seed from a topic + instruction index.
221+
* Ensures the same topic always produces the same seed per instruction,
222+
* enabling brand-consistent regeneration.
223+
*/
224+
function generateSeed(topic: string, index: number): number {
225+
let hash = 0;
226+
const str = `${topic}-infographic-${index}`;
227+
for (let i = 0; i < str.length; i++) {
228+
const char = str.charCodeAt(i);
229+
hash = ((hash << 5) - hash) + char;
230+
hash = hash & hash;
231+
}
232+
return Math.abs(hash) % 2147483647;
233+
}
234+
235+
/**
236+
* Generate all infographics for a research topic using instructions
237+
* from Sanity contentConfig.
238+
*
239+
* This is the main entry point for the check-research cron route.
240+
* Reads infographic instructions from contentConfig.infographicInstructions,
241+
* appends topic context to each, and generates images with deterministic
242+
* seeds for brand consistency.
243+
*
244+
* @param topic - The research topic (e.g. "React Server Components")
245+
* @param briefing - Optional research briefing text for additional context
246+
* @returns InfographicBatchResult with generated images and any errors
247+
*/
248+
export async function generateInfographicsForTopic(
249+
topic: string,
250+
briefing?: string,
251+
): Promise<InfographicBatchResult> {
252+
const instructions = await getConfigValue(
253+
"content_config", "infographicInstructions", DEFAULT_INSTRUCTIONS
254+
);
255+
256+
const model = await getConfigValue(
257+
"pipeline_config", "infographicModel", "imagen-4-fast"
258+
);
259+
260+
const contextSuffix = briefing
261+
? `\n\nTopic: ${topic}\nContext: ${briefing.slice(0, 500)}`
262+
: `\n\nTopic: ${topic}`;
263+
264+
const requests: InfographicRequest[] = instructions.map(
265+
(instruction, index) => ({
266+
prompt: `${instruction}${contextSuffix}`,
267+
seed: generateSeed(topic, index),
268+
aspectRatio: "16:9" as const,
269+
})
270+
);
271+
272+
console.log(
273+
`[infographics] Generating ${requests.length} infographics for "${topic}" with ${model}`
274+
);
275+
276+
const result = await generateInfographicBatch(requests, { model });
277+
278+
console.log(
279+
`[infographics] Complete: ${result.results.length} generated, ${result.errors.length} failed`
280+
);
281+
282+
return result;
283+
}

0 commit comments

Comments
 (0)