Add automatic Gemini 3.1 Flash-Lite vector mockup fallback when Free Tier key reports limit: 0 for Nano Banana 2.1
This commit is contained in:
1 parent
b7c5f79d26
commit
744d29c003
1 file changed
+116
-34
+116
-34
@@ -5,8 +5,8 @@ import { GoogleGenAI } from "@google/genai";
|
|||||||
export const NANO_BANANA_MODELS = [
|
export const NANO_BANANA_MODELS = [
|
||||||
{
|
{
|
||||||
id: "gemini-nano-banana-2.1",
|
id: "gemini-nano-banana-2.1",
|
||||||
label: "Nano Banana 2.1 (Default High-Efficiency)",
|
label: "Nano Banana 2.1 (Default High-Efficiency + Free-Tier Auto Fallback)",
|
||||||
description: "gemini-nano-banana-2.1 — Fast, high-efficiency native image generation and conversational editing"
|
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",
|
id: "gemini-3-pro-image",
|
||||||
@@ -88,9 +88,80 @@ export function buildNanoBananaPrompt({
|
|||||||
return finalPrompt;
|
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`)
|
* 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`).
|
* 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({
|
export async function generateWithNanoBanana({
|
||||||
apiKey,
|
apiKey,
|
||||||
@@ -104,7 +175,7 @@ export async function generateWithNanoBanana({
|
|||||||
const resolvedKey = apiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
const resolvedKey = apiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
|
||||||
if (!resolvedKey) {
|
if (!resolvedKey) {
|
||||||
const err = new Error(
|
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."
|
"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";
|
err.code = "MISSING_API_KEY";
|
||||||
throw err;
|
throw err;
|
||||||
@@ -117,12 +188,10 @@ export async function generateWithNanoBanana({
|
|||||||
const ai = new GoogleGenAI({ apiKey: resolvedKey });
|
const ai = new GoogleGenAI({ apiKey: resolvedKey });
|
||||||
|
|
||||||
let inputPayload = prompt;
|
let inputPayload = prompt;
|
||||||
|
|
||||||
// Check if we are performing conversational image-to-image editing
|
|
||||||
let base64Data = referenceImageBase64;
|
let base64Data = referenceImageBase64;
|
||||||
let mimeType = referenceMimeType || "image/png";
|
let mimeType = referenceMimeType || "image/png";
|
||||||
|
|
||||||
if (!base64Data && referenceImagePath && fs.existsSync(referenceImagePath)) {
|
if (!base64Data && referenceImagePath && fs.existsSync(referenceImagePath) && !referenceImagePath.endsWith(".svg")) {
|
||||||
const bytes = fs.readFileSync(referenceImagePath);
|
const bytes = fs.readFileSync(referenceImagePath);
|
||||||
base64Data = bytes.toString("base64");
|
base64Data = bytes.toString("base64");
|
||||||
if (referenceImagePath.endsWith(".jpg") || referenceImagePath.endsWith(".jpeg")) {
|
if (referenceImagePath.endsWith(".jpg") || referenceImagePath.endsWith(".jpeg")) {
|
||||||
@@ -131,7 +200,6 @@ export async function generateWithNanoBanana({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (base64Data) {
|
if (base64Data) {
|
||||||
// Strip data URL prefix if present
|
|
||||||
const cleanedBase64 = base64Data.replace(/^data:image\/[a-zA-Z0-9+.-]+;base64,/, "");
|
const cleanedBase64 = base64Data.replace(/^data:image\/[a-zA-Z0-9+.-]+;base64,/, "");
|
||||||
inputPayload = [
|
inputPayload = [
|
||||||
{ type: "text", text: prompt },
|
{ type: "text", text: prompt },
|
||||||
@@ -143,32 +211,46 @@ export async function generateWithNanoBanana({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const interaction = await ai.interactions.create({
|
try {
|
||||||
model: validModel,
|
const interaction = await ai.interactions.create({
|
||||||
input: inputPayload
|
model: validModel,
|
||||||
});
|
input: inputPayload
|
||||||
|
});
|
||||||
|
|
||||||
const generatedImage = interaction.output_image;
|
const generatedImage = interaction.output_image;
|
||||||
if (!generatedImage || !generatedImage.data) {
|
if (!generatedImage || !generatedImage.data) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
interaction.output_text ||
|
interaction.output_text ||
|
||||||
"Nano Banana completed the interaction without returning an image block. Try refining your visual prompt."
|
"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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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