Files
sparkvault/server/nano_banana.js
T

175 lines
6.4 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)",
description: "gemini-nano-banana-2.1 — Fast, high-efficiency native image generation and conversational editing"
},
{
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;
}
/**
* 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`).
*/
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 API Key' in the Mockup Studio (or set GEMINI_API_KEY) to generate live images with Nano Banana 2.1, 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;
// Check if we are performing conversational image-to-image editing
let base64Data = referenceImageBase64;
let mimeType = referenceMimeType || "image/png";
if (!base64Data && referenceImagePath && fs.existsSync(referenceImagePath)) {
const bytes = fs.readFileSync(referenceImagePath);
base64Data = bytes.toString("base64");
if (referenceImagePath.endsWith(".jpg") || referenceImagePath.endsWith(".jpeg")) {
mimeType = "image/jpeg";
}
}
if (base64Data) {
// Strip data URL prefix if present
const cleanedBase64 = base64Data.replace(/^data:image\/[a-zA-Z0-9+.-]+;base64,/, "");
inputPayload = [
{ type: "text", text: prompt },
{
type: "image",
mime_type: mimeType,
data: cleanedBase64
}
];
}
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. Try refining your visual prompt."
);
}
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
};
}