import * as fs from "node:fs"; import * as path from "node:path"; import { GoogleGenAI } from "@google/genai"; export const NANO_BANANA_MODELS = [ { id: "gemini-nano-banana-2.1", label: "Nano Banana 2.1 (Default High-Efficiency + Free-Tier Auto Fallback)", description: "gemini-nano-banana-2.1 — Uses native Nano Banana 2.1 on Tier 1 keys, with automatic Gemini 3.1 Flash-Lite Vector Mockup fallback on Free Tier keys" }, { id: "gemini-3-pro-image", label: "Nano Banana Pro (Studio Quality)", description: "gemini-3-pro-image — High-detail studio mockup generation and multi-turn editing" }, { id: "gemini-3.1-flash-lite-image", label: "Nano Banana 2 Lite (Ultra-Fast)", description: "gemini-3.1-flash-lite-image — Rapid concept sketching and quick iterations" } ]; export const SURFACE_PRESETS = { "tshirt-front": { label: "👕 T-Shirt (Full Front Print)", buildPrompt: ({ prompt, garmentColorName, garmentHex }) => `Photorealistic studio flat-lay apparel mockup of a heavyweight ${garmentColorName || "vintage cream"} (${garmentHex || "#F5E8D3"}) crewneck t-shirt. Centered full-front screenprint graphic design: ${prompt}. Crisp fabric texture, authentic screenprint ink detail, clean studio lighting.` }, "tshirt-left-chest": { label: "👕 T-Shirt (5-inch Left-Chest Emblem)", buildPrompt: ({ prompt, garmentColorName, garmentHex }) => `Photorealistic studio closeup & flat-lay apparel mockup of a heavyweight ${garmentColorName || "vintage cream"} (${garmentHex || "#F5E8D3"}) crewneck t-shirt featuring a compact 5-inch left-chest pocket-zone screenprinted emblem: ${prompt}. Clean vector-style spot-color ink on fabric, studio lighting.` }, "isolated-graphic": { label: "🎨 Isolated Screenprint Artwork (Vector Style)", buildPrompt: ({ prompt, garmentColorName }) => `Isolated flat vector screenprint t-shirt graphic emblem on a clean solid ${garmentColorName || "cream"} background, no shirt folds, bold spot-color illustration and outlined vintage typography: ${prompt}` }, "sticker": { label: "🏷️ Die-Cut Vinyl Sticker / Patch", buildPrompt: ({ prompt }) => `Photorealistic die-cut vinyl sticker with a crisp white contour border resting on a dark slate studio surface. Bold vector illustration and typography: ${prompt}` }, "poster": { label: "🖼️ Framed Studio Art Poster", buildPrompt: ({ prompt }) => `Gallery-framed vertical screenprint poster mockup in a minimalist studio interior. Bold editorial illustration and typography: ${prompt}` }, "app-ui": { label: "💻 Product / App Interface Concept", buildPrompt: ({ prompt }) => `High-craft modern product design and UI concept presentation mockup on a sleek dark desktop studio display: ${prompt}` } }; export function buildNanoBananaPrompt({ rawPrompt, ideaTitle, elevatorPitch, template = "tshirt-front", garmentColorName = "Vintage Cream", garmentHex = "#F5E8D3", whackCard = null, editInstruction = null }) { if (editInstruction && editInstruction.trim()) { let editText = editInstruction.trim(); if (whackCard) { editText += ` Incorporate creative twist from Card #${whackCard.number} (${whackCard.title}): ${whackCard.promptQuestion}`; } return editText; } const baseConcept = (rawPrompt && rawPrompt.trim()) ? rawPrompt.trim() : `${ideaTitle}${elevatorPitch ? ` — ${elevatorPitch}` : ""}`; const surface = SURFACE_PRESETS[template] || SURFACE_PRESETS["tshirt-front"]; let finalPrompt = surface.buildPrompt({ prompt: baseConcept, garmentColorName, garmentHex }); if (whackCard) { finalPrompt += ` Creative direction inspired by "${whackCard.title}": ${whackCard.promptQuestion}`; } return finalPrompt; } function extractSvgBlock(text) { if (!text) return null; const match = String(text).match(//i); if (!match) return null; let svg = match[0]; if (!svg.includes("xmlns=")) { svg = svg.replace("...` code block with no markdown explanation." ] .filter(Boolean) .join("\n\n"); const interaction = await ai.interactions.create({ model: "gemini-3.1-flash-lite", input: svgPrompt }); const svgContent = extractSvgBlock(interaction.output_text); if (!svgContent) { throw new Error("Free-tier fallback completed without valid SVG output."); } const filename = `nb-svg-${Date.now()}-${Math.random().toString(36).slice(2, 7)}.svg`; const fullPath = path.join(outputDir, filename); fs.mkdirSync(outputDir, { recursive: true }); fs.writeFileSync(fullPath, svgContent, "utf-8"); return { filename, imageUrl: `/mockups/${filename}`, mimeType: "image/svg+xml", modelUsed: "gemini-3.1-flash-lite (Free-Tier Vector Mockup)", interactionId: interaction.id || null, textNotes: "Generated via Gemini 3.1 Flash-Lite Vector Studio (Free Tier key detected; enable Tier 1 billing in Google AI Studio for raster Nano Banana 2.1)." }; } /** * Generates or edits an image using Google Gemini Nano Banana (`gemini-nano-banana-2.1`) * via the official `@google/genai` Interactions API (`ai.interactions.create`), * with automatic fallback to `gemini-3.1-flash-lite` vector mockup generation when * a Free Tier key hits `limit: 0`. */ export async function generateWithNanoBanana({ apiKey, model = "gemini-nano-banana-2.1", prompt, referenceImagePath = null, referenceImageBase64 = null, referenceMimeType = "image/png", outputDir }) { const resolvedKey = apiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY; if (!resolvedKey) { const err = new Error( "No Gemini API key configured. Click '🍌 Nano Banana' in the top bar (or set GEMINI_API_KEY) to generate live mockups, or use the Interactive T-Shirt Canvas Compositor." ); err.code = "MISSING_API_KEY"; throw err; } const validModel = NANO_BANANA_MODELS.some((m) => m.id === model) ? model : "gemini-nano-banana-2.1"; const ai = new GoogleGenAI({ apiKey: resolvedKey }); let inputPayload = prompt; let base64Data = referenceImageBase64; let mimeType = referenceMimeType || "image/png"; if (!base64Data && referenceImagePath && fs.existsSync(referenceImagePath) && !referenceImagePath.endsWith(".svg")) { const bytes = fs.readFileSync(referenceImagePath); base64Data = bytes.toString("base64"); if (referenceImagePath.endsWith(".jpg") || referenceImagePath.endsWith(".jpeg")) { mimeType = "image/jpeg"; } } if (base64Data) { const cleanedBase64 = base64Data.replace(/^data:image\/[a-zA-Z0-9+.-]+;base64,/, ""); inputPayload = [ { type: "text", text: prompt }, { type: "image", mime_type: mimeType, data: cleanedBase64 } ]; } try { const interaction = await ai.interactions.create({ model: validModel, input: inputPayload }); const generatedImage = interaction.output_image; if (!generatedImage || !generatedImage.data) { throw new Error( interaction.output_text || "Nano Banana completed the interaction without returning an image block." ); } const ext = (generatedImage.mime_type || "").includes("jpeg") ? "jpg" : "png"; const filename = `nb-${Date.now()}-${Math.random().toString(36).slice(2, 7)}.${ext}`; const fullPath = path.join(outputDir, filename); fs.mkdirSync(outputDir, { recursive: true }); fs.writeFileSync(fullPath, Buffer.from(generatedImage.data, "base64")); return { filename, imageUrl: `/mockups/${filename}`, mimeType: generatedImage.mime_type || `image/${ext}`, modelUsed: validModel, interactionId: interaction.id || null, textNotes: interaction.output_text || null }; } catch (err) { const msg = String(err?.message || ""); // If Free Tier quota is 0 for native raster image models, seamlessly fall back to Gemini 3.1 Flash-Lite SVG Vector Mockup! if (msg.includes("429") || msg.includes("Free Tier") || msg.includes("limit: 0") || msg.includes("quota")) { return await generateFreeTierSvgMockup({ ai, prompt, referenceImagePath, outputDir }); } throw err; } }