Files
sparkvault/server/nano_banana.js
T

257 lines
10 KiB
JavaScript

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(/<svg[\s\S]*?<\/svg>/i);
if (!match) return null;
let svg = match[0];
if (!svg.includes("xmlns=")) {
svg = svg.replace("<svg", '<svg xmlns="http://www.w3.org/2000/svg"');
}
return svg;
}
/**
* Free-Tier Fallback: When Google AI Studio's Free Tier blocks raster image models
* (`limit: 0 input tokens per minute on Free Tier`), uses `gemini-3.1-flash-lite`
* on the same API key to generate a custom, detailed multi-layer SVG product/apparel mockup!
*/
async function generateFreeTierSvgMockup({
ai,
prompt,
referenceImagePath,
outputDir
}) {
let previousSvgSnippet = "";
if (referenceImagePath && referenceImagePath.endsWith(".svg") && fs.existsSync(referenceImagePath)) {
previousSvgSnippet = fs.readFileSync(referenceImagePath, "utf-8").slice(0, 12000);
}
const svgPrompt = [
"You are an expert apparel & product graphic designer and SVG illustrator.",
"Generate a complete, standalone, richly detailed 800x800 SVG (`viewBox=\"0 0 800 800\"`) product mockup for the following brief:",
`BRIEF: ${prompt}`,
previousSvgSnippet
? `EXISTING SVG TO EDIT/REFINE:\n${previousSvgSnippet}\nApply the requested changes while preserving or improving the overall composition.`
: "",
"DESIGN RULES:",
"1. Include a sleek studio backdrop (`#121621` radial gradient).",
"2. If the brief mentions a t-shirt, draw a realistic, well-proportioned crewneck t-shirt silhouette with collar ribbing, sleeve seams, subtle fabric shading, and the requested garment hex color. Place the custom screenprint illustration and vintage typography either full-front (`cx=400, cy=360`) or in the 5-inch left-chest pocket zone (`cx=490, cy=285`, scaled compactly) according to the brief.",
"3. If the brief is a sticker, poster, or app UI, draw the realistic die-cut border, gallery frame, or device frame accordingly.",
"4. Draw rich, multi-layered custom vector illustration paths, badges, emblems, stars/mountains/icons, and bold screenprint typography with high contrast against the garment/surface color.",
"5. Return ONLY the raw `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 800 800\">...</svg>` 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;
}
}