Initial commit: SparkVault Idea Tracker, 64-card Creative Whack Pack & Nano Banana 2.1 Mockup Studio
This commit is contained in:
commit
49e3134758
82 files changed
+6312
No files matched your search
@@ -0,0 +1,174 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user