import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { spawn } from "node:child_process"; import { GoogleGenAI } from "@google/genai"; export const NANO_BANANA_MODELS = [ { id: "codex-image-gen", label: "OpenAI Codex (gpt-5.6-sol + Built-in image_gen) — Recommended", description: "codex-image-gen — High-resolution photorealistic raster PNG product & apparel mockups via OpenAI Codex built-in image_gen" }, { id: "gemini-nano-banana-2.1", label: "Nano Banana 2.1 (Google Gemini + 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 (Gemini 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 (Gemini 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; } export function isCodexConfigured() { const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex"); return fs.existsSync(path.join(codexHome, "auth.json")); } function findNewestGeneratedImage(generatedImagesRoot, startedAtMs) { if (!fs.existsSync(generatedImagesRoot)) return null; let newestFile = null; let newestMtime = 0; for (const entry of fs.readdirSync(generatedImagesRoot, { withFileTypes: true })) { const fullEntry = path.join(generatedImagesRoot, entry.name); if (entry.isDirectory()) { for (const sub of fs.readdirSync(fullEntry, { withFileTypes: true })) { if (sub.isFile() && /\.(png|jpg|jpeg|webp)$/i.test(sub.name)) { const candidate = path.join(fullEntry, sub.name); const stat = fs.statSync(candidate); if (stat.mtimeMs >= startedAtMs - 2000 && stat.mtimeMs > newestMtime) { newestMtime = stat.mtimeMs; newestFile = candidate; } } } } else if (entry.isFile() && /\.(png|jpg|jpeg|webp)$/i.test(entry.name)) { const stat = fs.statSync(fullEntry); if (stat.mtimeMs >= startedAtMs - 2000 && stat.mtimeMs > newestMtime) { newestMtime = stat.mtimeMs; newestFile = fullEntry; } } } return newestFile; } /** * Generates or edits a high-resolution raster PNG mockup using OpenAI Codex CLI * (`codex exec -m gpt-5.6-sol`) and its built-in `image_gen` tool. */ export async function generateWithCodex({ prompt, referenceImagePath = null, outputDir }) { const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex"); const generatedImagesRoot = path.join(codexHome, "generated_images"); const startedAtMs = Date.now(); const instruction = referenceImagePath && fs.existsSync(referenceImagePath) ? `Use the built-in image_gen tool right now to edit the attached reference image according to this instruction: ${prompt}. Do not run any shell commands, just call image_gen and reply DONE.` : `Use the built-in image_gen tool right now to generate this image: ${prompt}. Do not run any shell commands, just call image_gen and reply DONE.`; const args = [ "exec", "-m", process.env.CODEX_MODEL || "gpt-5.6-sol", "--skip-git-repo-check", "--json" ]; if (referenceImagePath && fs.existsSync(referenceImagePath) && !referenceImagePath.endsWith(".svg")) { args.push("-i", referenceImagePath); } args.push(instruction); const { stdout, stderr, exitCode } = await new Promise((resolve, reject) => { const child = spawn("codex", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } }); let out = ""; let err = ""; child.stdout.on("data", (d) => { out += d.toString(); }); child.stderr.on("data", (d) => { err += d.toString(); }); child.on("error", reject); child.on("close", (code) => { resolve({ stdout: out, stderr: err, exitCode: code }); }); }); // Extract thread_id from JSONL stdout let threadId = null; let lastAgentText = null; for (const line of stdout.split("\n")) { const trimmed = line.trim(); if (!trimmed.startsWith("{")) continue; try { const evt = JSON.parse(trimmed); if (evt.type === "thread.started" && evt.thread_id) { threadId = evt.thread_id; } else if (evt.type === "item.completed" && evt.item?.type === "agent_message") { lastAgentText = evt.item.text; } } catch { // ignore non-json lines } } let sourceImagePath = null; if (threadId) { const threadDir = path.join(generatedImagesRoot, threadId); if (fs.existsSync(threadDir)) { const files = fs .readdirSync(threadDir) .filter((f) => /\.(png|jpg|jpeg|webp)$/i.test(f)) .map((f) => path.join(threadDir, f)); if (files.length > 0) { sourceImagePath = files[files.length - 1]; } } } if (!sourceImagePath) { sourceImagePath = findNewestGeneratedImage(generatedImagesRoot, startedAtMs); } if (!sourceImagePath || !fs.existsSync(sourceImagePath)) { throw new Error( `Codex completed (exit ${exitCode}) without saving an image. ${stderr || lastAgentText || ""}`.trim() ); } const ext = path.extname(sourceImagePath).replace(/^\./, "") || "png"; const filename = `codex-${Date.now()}-${Math.random().toString(36).slice(2, 7)}.${ext}`; const fullPath = path.join(outputDir, filename); fs.mkdirSync(outputDir, { recursive: true }); fs.copyFileSync(sourceImagePath, fullPath); return { filename, imageUrl: `/mockups/${filename}`, mimeType: `image/${ext === "jpg" ? "jpeg" : ext}`, modelUsed: "OpenAI Codex (gpt-5.6-sol / image_gen)", interactionId: threadId, textNotes: "Generated high-resolution raster PNG via OpenAI Codex built-in image_gen." }; } 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." }; } /** * Generates or edits an image using: * 1. OpenAI Codex (`codex-image-gen`) built-in `image_gen` tool (default when selected or when Codex auth is available), OR * 2. Google Gemini Nano Banana (`gemini-nano-banana-2.1`) with automatic fallback to `Codex` or `gemini-3.1-flash-lite` SVG when Free Tier quota is 0. */ export async function generateWithNanoBanana({ apiKey, model = "codex-image-gen", prompt, referenceImagePath = null, referenceImageBase64 = null, referenceMimeType = "image/png", outputDir }) { // 1. Primary path: OpenAI Codex built-in image_gen if (model === "codex-image-gen" || (!apiKey && isCodexConfigured())) { return await generateWithCodex({ prompt, referenceImagePath, outputDir }); } const resolvedKey = apiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY; if (!resolvedKey) { if (isCodexConfigured()) { return await generateWithCodex({ prompt, referenceImagePath, outputDir }); } const err = new Error( "No AI image provider configured. Select 'OpenAI Codex (image_gen)' or configure a Gemini API key in Settings." ); 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 (msg.includes("429") || msg.includes("Free Tier") || msg.includes("limit: 0") || msg.includes("quota")) { // If OpenAI Codex is authenticated on this host, prefer high-res raster generation via Codex! if (isCodexConfigured()) { return await generateWithCodex({ prompt, referenceImagePath, outputDir }); } return await generateFreeTierSvgMockup({ ai, prompt, referenceImagePath, outputDir }); } throw err; } }