You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
191 lines
6.0 KiB
191 lines
6.0 KiB
/**
|
|
* Gemini Provider for Generate API Route
|
|
*
|
|
* Handles image generation using Google's Gemini API models.
|
|
*/
|
|
|
|
import { NextResponse } from "next/server";
|
|
import { GoogleGenAI } from "@google/genai";
|
|
import { GenerateResponse, ModelType } from "@/types";
|
|
|
|
/**
|
|
* Map model types to Gemini model IDs
|
|
*/
|
|
export const MODEL_MAP: Record<ModelType, string> = {
|
|
"nano-banana": "gemini-2.5-flash-image",
|
|
"nano-banana-pro": "gemini-3-pro-image-preview",
|
|
"nano-banana-2": "gemini-3.1-flash-image-preview",
|
|
};
|
|
|
|
/**
|
|
* Generate image using Gemini API (legacy/default path)
|
|
*/
|
|
export async function generateWithGemini(
|
|
requestId: string,
|
|
apiKey: string,
|
|
prompt: string,
|
|
images: string[],
|
|
model: ModelType,
|
|
aspectRatio?: string,
|
|
resolution?: string,
|
|
useGoogleSearch?: boolean,
|
|
useImageSearch?: boolean
|
|
): Promise<NextResponse<GenerateResponse>> {
|
|
console.log(`[API:${requestId}] Gemini generation - Model: ${model}, Images: ${images?.length || 0}, Prompt: ${prompt?.length || 0} chars`);
|
|
|
|
// Extract base64 data and MIME types from data URLs
|
|
const imageData = (images || []).map((image, idx) => {
|
|
if (image.includes("base64,")) {
|
|
const [header, data] = image.split("base64,");
|
|
// Extract MIME type from header (e.g., "data:image/png;" -> "image/png")
|
|
const mimeMatch = header.match(/data:([^;]+)/);
|
|
const mimeType = mimeMatch ? mimeMatch[1] : "image/png";
|
|
console.log(`[API:${requestId}] Image ${idx + 1}: ${mimeType}, ${(data.length / 1024).toFixed(1)}KB`);
|
|
return { data, mimeType };
|
|
}
|
|
console.log(`[API:${requestId}] Image ${idx + 1}: raw, ${(image.length / 1024).toFixed(1)}KB`);
|
|
return { data: image, mimeType: "image/png" };
|
|
});
|
|
|
|
// Initialize Gemini client
|
|
const ai = new GoogleGenAI({ apiKey });
|
|
|
|
// Build request parts array with prompt and all images
|
|
const requestParts: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }> = [
|
|
{ text: prompt },
|
|
...imageData.map(({ data, mimeType }) => ({
|
|
inlineData: {
|
|
mimeType,
|
|
data,
|
|
},
|
|
})),
|
|
];
|
|
|
|
// Build config object based on model capabilities
|
|
const config: Record<string, unknown> = {
|
|
responseModalities: ["IMAGE", "TEXT"],
|
|
};
|
|
|
|
// Add imageConfig for both models (both support aspect ratio)
|
|
if (aspectRatio) {
|
|
config.imageConfig = {
|
|
aspectRatio,
|
|
};
|
|
}
|
|
|
|
// Add resolution for Nano Banana Pro and Nano Banana 2
|
|
if ((model === "nano-banana-pro" || model === "nano-banana-2") && resolution) {
|
|
if (!config.imageConfig) {
|
|
config.imageConfig = {};
|
|
}
|
|
(config.imageConfig as Record<string, unknown>).imageSize = resolution;
|
|
}
|
|
|
|
// Add tools array for Google Search (Nano Banana Pro and Nano Banana 2)
|
|
const tools = [];
|
|
if (model === "nano-banana-2" && (useGoogleSearch || useImageSearch)) {
|
|
// Nano Banana 2 uses searchTypes to enable web and/or image search independently
|
|
const searchTypes: Record<string, Record<string, never>> = {};
|
|
if (useGoogleSearch) searchTypes.webSearch = {};
|
|
if (useImageSearch) searchTypes.imageSearch = {};
|
|
tools.push({ googleSearch: { searchTypes } });
|
|
} else if (model === "nano-banana-pro" && useGoogleSearch) {
|
|
tools.push({ googleSearch: {} });
|
|
}
|
|
|
|
console.log(`[API:${requestId}] Config: ${JSON.stringify(config)}`);
|
|
|
|
// Make request to Gemini
|
|
const geminiStartTime = Date.now();
|
|
|
|
const response = await ai.models.generateContent({
|
|
model: MODEL_MAP[model],
|
|
contents: [
|
|
{
|
|
role: "user",
|
|
parts: requestParts,
|
|
},
|
|
],
|
|
config,
|
|
...(tools.length > 0 && { tools }),
|
|
});
|
|
|
|
const geminiDuration = Date.now() - geminiStartTime;
|
|
console.log(`[API:${requestId}] Gemini API completed in ${geminiDuration}ms`);
|
|
|
|
// Extract image from response
|
|
const candidates = response.candidates;
|
|
|
|
if (!candidates || candidates.length === 0) {
|
|
console.error(`[API:${requestId}] No candidates in Gemini response`);
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: "No response from AI model",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
const parts = candidates[0].content?.parts;
|
|
console.log(`[API:${requestId}] Response parts: ${parts?.length || 0}`);
|
|
|
|
if (!parts) {
|
|
console.error(`[API:${requestId}] No parts in Gemini candidate content`);
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: "No content in response",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
// Find image part in response
|
|
for (const part of parts) {
|
|
if (part.inlineData && part.inlineData.data) {
|
|
const mimeType = part.inlineData.mimeType || "image/png";
|
|
const imgData = part.inlineData.data;
|
|
const imageSizeKB = (imgData.length / 1024).toFixed(1);
|
|
|
|
console.log(`[API:${requestId}] Output image: ${mimeType}, ${imageSizeKB}KB`);
|
|
|
|
const dataUrl = `data:${mimeType};base64,${imgData}`;
|
|
|
|
const responsePayload = { success: true, image: dataUrl };
|
|
const responseSize = JSON.stringify(responsePayload).length;
|
|
const responseSizeMB = (responseSize / (1024 * 1024)).toFixed(2);
|
|
|
|
if (responseSize > 4.5 * 1024 * 1024) {
|
|
console.warn(`[API:${requestId}] Response size (${responseSizeMB}MB) approaching Next.js 5MB limit`);
|
|
}
|
|
|
|
console.log(`[API:${requestId}] SUCCESS - Returning ${responseSizeMB}MB payload`);
|
|
|
|
return NextResponse.json<GenerateResponse>(responsePayload);
|
|
}
|
|
}
|
|
|
|
// If no image found, check for text error
|
|
for (const part of parts) {
|
|
if (part.text) {
|
|
console.error(`[API:${requestId}] Gemini returned text instead of image: ${part.text.substring(0, 100)}`);
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: `Model returned text instead of image: ${part.text.substring(0, 200)}`,
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|
|
console.error(`[API:${requestId}] No image or text found in Gemini response`);
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: "No image in response",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|