175 lines
6.4 KiB
JavaScript
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
|
|
};
|
|
}
|