import * as fs from "node:fs"; import * as path from "node:path"; import { generateImage } from "../src/api/gemini"; import { loadConfigFromObject } from "../src/api/config"; import type { GeminiConfig } from "../src/api/config"; // Visual descriptions per companion (from SPEC.md section 9.2) const companionDefs = [ { type: "fee-lila", name: "Fee Lila", visualDescription: "a small fairy with lavender-purple wings, a flowing lilac dress, big warm brown eyes, a tiny flower crown made of forget-me-nots, light skin, and short wavy auburn hair", }, { type: "einhorn-stella", name: "Einhorn Stella", visualDescription: "a small unicorn with a white coat, a shimmering golden horn, a flowing mane in soft rainbow pastels (pink, lavender, light blue), big dark eyes with long lashes, and small silver hooves", }, { type: "fuchs-finn", name: "Fuchs Finn", visualDescription: "a young red fox with warm orange-brown fur, a white chest and belly, a bushy tail with a white tip, bright curious amber eyes, and slightly oversized pointed ears", }, { type: "eule-elsa", name: "Eule Elsa", visualDescription: "a small round owl with soft brown-and-cream feathers, a heart-shaped face, large round golden eyes, tiny ear tufts, and small talons perched on a mossy branch", }, ]; function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } async function saveBlobToFile(blob: Blob, filePath: string): Promise { const buffer = Buffer.from(await blob.arrayBuffer()); const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, buffer); } async function main(): Promise { // Load config from public/config.json const configPath = path.resolve("public/config.json"); if (!fs.existsSync(configPath)) { console.error("ERROR: public/config.json not found"); process.exit(1); } const configData = JSON.parse( fs.readFileSync(configPath, "utf-8"), ) as GeminiConfig; if (!configData.geminiApiKey || !configData.imageModel) { console.error("ERROR: config.json missing geminiApiKey or imageModel"); process.exit(1); } loadConfigFromObject(configData); let failures = 0; // Generate character sheets and avatars for each companion for (const companion of companionDefs) { const charSheetPath = path.resolve( `src/assets/companions/${companion.type}/character-sheet.png`, ); const avatarPath = path.resolve( `src/assets/companions/${companion.type}/avatar.png`, ); // Skip if both files already exist and are non-empty if ( fs.existsSync(charSheetPath) && fs.statSync(charSheetPath).size > 1024 && fs.existsSync(avatarPath) && fs.statSync(avatarPath).size > 1024 ) { console.log( `Skipping ${companion.name} — character-sheet.png and avatar.png already exist`, ); continue; } // Generate character sheet console.log(`Generating character sheet for ${companion.name}...`); const charSheetPrompt = `Character sheet for a children's book. The character is ${companion.name}, ${companion.visualDescription}. Show the character in 3 views: front view, side view, and a close-up of the face. All views must show the EXACT SAME character with identical colors, proportions, and features. Art style: Watercolor children's book illustration, warm pastel colors, soft edges, magical forest theme. White background. Label each view clearly: "FRONT" "SIDE" "FACE" The character must be: cute, friendly, approachable, with large expressive eyes. Suitable for a 7-year-old audience.`; try { const charSheetBlob = await generateImage( charSheetPrompt, [], configData, ); if (charSheetBlob) { await saveBlobToFile(charSheetBlob, charSheetPath); console.log( ` Saved character sheet: ${charSheetPath} (${fs.statSync(charSheetPath).size} bytes)`, ); } else { console.error( ` ERROR: Failed to generate character sheet for ${companion.name}`, ); failures++; continue; // Skip avatar if character sheet failed (needed as reference) } } catch (err) { console.error( ` ERROR generating character sheet for ${companion.name}:`, err, ); failures++; continue; } await sleep(2000); // Rate limit delay // Generate avatar using character sheet as reference console.log(`Generating avatar for ${companion.name}...`); const avatarPrompt = `Create a single portrait of this exact character from the reference sheet. Show only the face and upper body, looking directly at the viewer with a warm, friendly expression. Same art style as the reference. Square format. No text. No background (or simple soft gradient).`; try { // Read the character sheet back as a reference image const charSheetBuffer = fs.readFileSync(charSheetPath); const charSheetRef = new Blob([charSheetBuffer], { type: "image/png", }); const avatarBlob = await generateImage( avatarPrompt, [charSheetRef], configData, ); if (avatarBlob) { await saveBlobToFile(avatarBlob, avatarPath); console.log( ` Saved avatar: ${avatarPath} (${fs.statSync(avatarPath).size} bytes)`, ); } else { console.error( ` ERROR: Failed to generate avatar for ${companion.name}`, ); failures++; } } catch (err) { console.error( ` ERROR generating avatar for ${companion.name}:`, err, ); failures++; } await sleep(2000); // Rate limit delay } // Generate style reference (initial forest scene) const styleRefPath = path.resolve( "src/assets/style-reference/initial-forest-scene.png", ); if ( fs.existsSync(styleRefPath) && fs.statSync(styleRefPath).size > 1024 ) { console.log( "Skipping style reference — initial-forest-scene.png already exists", ); } else { console.log("Generating style reference (initial forest scene)..."); const styleRefPrompt = `Watercolor children's book illustration of a magical forest clearing. Warm pastel colors, soft edges, dreamy atmosphere. A gentle meadow surrounded by friendly trees, dappled sunlight, small flowers, and a hint of magic sparkles. No text, no letters, no characters. This sets the visual style for all future forest images.`; try { const styleRefBlob = await generateImage( styleRefPrompt, [], configData, ); if (styleRefBlob) { await saveBlobToFile(styleRefBlob, styleRefPath); console.log( ` Saved style reference: ${styleRefPath} (${fs.statSync(styleRefPath).size} bytes)`, ); } else { console.error(" ERROR: Failed to generate style reference"); failures++; } } catch (err) { console.error(" ERROR generating style reference:", err); failures++; } } // Summary console.log("\n--- Generation Summary ---"); const pngFiles = [ "src/assets/companions/fee-lila/character-sheet.png", "src/assets/companions/fee-lila/avatar.png", "src/assets/companions/einhorn-stella/character-sheet.png", "src/assets/companions/einhorn-stella/avatar.png", "src/assets/companions/fuchs-finn/character-sheet.png", "src/assets/companions/fuchs-finn/avatar.png", "src/assets/companions/eule-elsa/character-sheet.png", "src/assets/companions/eule-elsa/avatar.png", "src/assets/style-reference/initial-forest-scene.png", ]; let existing = 0; for (const f of pngFiles) { const resolved = path.resolve(f); if (fs.existsSync(resolved) && fs.statSync(resolved).size > 1024) { existing++; console.log(` OK: ${f}`); } else { console.log(` MISSING: ${f}`); } } console.log(`\n${existing}/9 assets generated successfully.`); if (failures > 0) { console.log(`${failures} generation(s) failed.`); } process.exit(failures > 0 ? 1 : 0); } main();