Update app/api/image/route.ts
Browse files- app/api/image/route.ts +75 -152
app/api/image/route.ts
CHANGED
|
@@ -1,57 +1,38 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import {
|
| 3 |
import { HistoryItem, HistoryPart } from "@/lib/types";
|
| 4 |
|
| 5 |
-
//
|
| 6 |
-
//
|
| 7 |
-
//
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
// Define the model ID for Gemini 2.0 Flash experimental
|
| 17 |
-
const MODEL_ID = "gemini-2.0-flash-exp";
|
| 18 |
-
|
| 19 |
const MODEL_IDS = [
|
| 20 |
-
"gemini-2.0-flash-exp",
|
| 21 |
-
"gemini-2.0-flash-
|
| 22 |
-
"gemini-
|
| 23 |
-
"gemini-
|
| 24 |
-
"gemini-2.
|
| 25 |
];
|
| 26 |
|
| 27 |
-
//
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
parts: Array<{
|
| 31 |
-
text?: string;
|
| 32 |
-
inlineData?: { data: string; mimeType: string };
|
| 33 |
-
}>;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
interface ImageResult {
|
| 37 |
-
image: string | null;
|
| 38 |
-
description: string | null;
|
| 39 |
-
model: string;
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
interface AttemptResult {
|
| 43 |
-
model: string;
|
| 44 |
-
error: string;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
function getModelIds(selectedModel?: string) {
|
| 48 |
-
const
|
| 49 |
-
|
| 50 |
-
|
|
|
|
| 51 |
|
| 52 |
return [
|
| 53 |
-
|
| 54 |
-
...MODEL_IDS.filter((
|
| 55 |
];
|
| 56 |
}
|
| 57 |
|
|
@@ -59,135 +40,79 @@ function imageToInlineData(image: string) {
|
|
| 59 |
if (!image.startsWith("data:")) {
|
| 60 |
throw new Error("Invalid image data URL format");
|
| 61 |
}
|
| 62 |
-
|
| 63 |
-
const
|
| 64 |
-
|
| 65 |
-
throw new Error("Invalid image data URL format");
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
return {
|
| 69 |
-
data: imageParts[1],
|
| 70 |
-
mimeType: image.includes("image/png") ? "image/png" : "image/jpeg",
|
| 71 |
-
};
|
| 72 |
}
|
| 73 |
|
| 74 |
function formatHistory(history?: HistoryItem[]) {
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
}
|
| 88 |
-
return { text: "" };
|
| 89 |
-
})
|
| 90 |
-
.filter((part) => Object.keys(part).length > 0), // Remove empty parts
|
| 91 |
-
};
|
| 92 |
})
|
| 93 |
-
.filter(
|
| 94 |
-
|
|
|
|
| 95 |
}
|
| 96 |
|
|
|
|
|
|
|
|
|
|
| 97 |
async function generateWithModel(
|
| 98 |
model: string,
|
| 99 |
prompt: string,
|
| 100 |
inputImage: string | null,
|
| 101 |
history?: HistoryItem[]
|
| 102 |
-
)
|
| 103 |
-
const
|
| 104 |
-
const messageParts = [];
|
| 105 |
|
| 106 |
-
|
| 107 |
-
messageParts.push({ text: prompt });
|
| 108 |
|
| 109 |
-
// Add the image if provided
|
| 110 |
if (inputImage) {
|
| 111 |
-
|
| 112 |
-
console.log("Processing image edit request");
|
| 113 |
-
|
| 114 |
-
const inlineData = imageToInlineData(inputImage);
|
| 115 |
-
console.log(
|
| 116 |
-
"Base64 image length:",
|
| 117 |
-
inlineData.data.length,
|
| 118 |
-
"MIME type:",
|
| 119 |
-
inlineData.mimeType
|
| 120 |
-
);
|
| 121 |
-
|
| 122 |
-
// Add the image to message parts
|
| 123 |
-
messageParts.push({ inlineData });
|
| 124 |
}
|
| 125 |
|
| 126 |
-
|
| 127 |
|
| 128 |
-
|
| 129 |
-
console.log("Sending message with", messageParts.length, "parts", "to", model);
|
| 130 |
-
const response = await genAI.models.generateContent({
|
| 131 |
model,
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
temperature: 1,
|
| 135 |
-
topP: 0.95,
|
| 136 |
-
topK: 40,
|
| 137 |
-
responseModalities: [Modality.TEXT, Modality.IMAGE],
|
| 138 |
-
},
|
| 139 |
});
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
let mimeType = "image/png";
|
| 144 |
-
|
| 145 |
-
// Process the response
|
| 146 |
-
if (response.candidates && response.candidates.length > 0) {
|
| 147 |
-
const parts = response.candidates[0].content?.parts || [];
|
| 148 |
-
console.log("Number of parts in response:", parts.length);
|
| 149 |
-
|
| 150 |
-
for (const part of parts) {
|
| 151 |
-
if (part.inlineData) {
|
| 152 |
-
// Get the image data
|
| 153 |
-
imageData = part.inlineData.data || null;
|
| 154 |
-
mimeType = part.inlineData.mimeType || "image/png";
|
| 155 |
-
console.log(
|
| 156 |
-
"Image data received, length:",
|
| 157 |
-
imageData?.length || 0,
|
| 158 |
-
"MIME type:",
|
| 159 |
-
mimeType
|
| 160 |
-
);
|
| 161 |
-
} else if (part.text) {
|
| 162 |
-
// Store the text
|
| 163 |
-
textResponse = part.text;
|
| 164 |
-
console.log(
|
| 165 |
-
"Text response received:",
|
| 166 |
-
textResponse.substring(0, 50) + "..."
|
| 167 |
-
);
|
| 168 |
-
}
|
| 169 |
-
}
|
| 170 |
-
}
|
| 171 |
|
| 172 |
return {
|
| 173 |
-
image:
|
| 174 |
-
description
|
| 175 |
model,
|
| 176 |
};
|
| 177 |
}
|
| 178 |
|
|
|
|
|
|
|
|
|
|
| 179 |
export async function POST(req: NextRequest) {
|
| 180 |
try {
|
| 181 |
-
if (!GEMINI_API_KEY) {
|
| 182 |
return NextResponse.json(
|
| 183 |
{ error: "GEMINI_API_KEY is not configured" },
|
| 184 |
{ status: 500 }
|
| 185 |
);
|
| 186 |
}
|
| 187 |
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
const { prompt, image: inputImage, history, model: selectedModel } = requestData;
|
| 191 |
|
| 192 |
if (!prompt) {
|
| 193 |
return NextResponse.json(
|
|
@@ -196,7 +121,7 @@ export async function POST(req: NextRequest) {
|
|
| 196 |
);
|
| 197 |
}
|
| 198 |
|
| 199 |
-
const attemptedModels:
|
| 200 |
const modelIds = getModelIds(selectedModel);
|
| 201 |
|
| 202 |
for (const modelId of modelIds) {
|
|
@@ -209,12 +134,11 @@ export async function POST(req: NextRequest) {
|
|
| 209 |
);
|
| 210 |
|
| 211 |
if (!result.image) {
|
| 212 |
-
throw new Error("No image returned from
|
| 213 |
}
|
| 214 |
|
| 215 |
const fallback = modelId !== modelIds[0];
|
| 216 |
|
| 217 |
-
// Return just the base64 image and description as JSON
|
| 218 |
return NextResponse.json({
|
| 219 |
image: result.image,
|
| 220 |
description: result.description,
|
|
@@ -225,31 +149,30 @@ export async function POST(req: NextRequest) {
|
|
| 225 |
notice: fallback
|
| 226 |
? {
|
| 227 |
type: "info",
|
| 228 |
-
message: `${modelIds[0]}
|
| 229 |
}
|
| 230 |
: null,
|
| 231 |
});
|
| 232 |
-
} catch (
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
|
|
|
| 236 |
}
|
| 237 |
}
|
| 238 |
|
| 239 |
return NextResponse.json(
|
| 240 |
{
|
| 241 |
error: "Failed to generate image",
|
| 242 |
-
details: attemptedModels.map((attempt) => `${attempt.model}: ${attempt.error}`).join(" | "),
|
| 243 |
attemptedModels,
|
| 244 |
},
|
| 245 |
{ status: 500 }
|
| 246 |
);
|
| 247 |
-
} catch (
|
| 248 |
-
console.error("Error generating image:", error);
|
| 249 |
return NextResponse.json(
|
| 250 |
{
|
| 251 |
error: "Failed to generate image",
|
| 252 |
-
details:
|
| 253 |
},
|
| 254 |
{ status: 500 }
|
| 255 |
);
|
|
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
+
import { createClient } from "ai";
|
| 3 |
import { HistoryItem, HistoryPart } from "@/lib/types";
|
| 4 |
|
| 5 |
+
// -------------------------------
|
| 6 |
+
// Vercel AI SDK Client
|
| 7 |
+
// -------------------------------
|
| 8 |
+
const client = createClient({
|
| 9 |
+
apiKey: process.env.GEMINI_API_KEY || "",
|
| 10 |
+
baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
|
| 11 |
+
});
|
| 12 |
+
|
| 13 |
+
// -------------------------------
|
| 14 |
+
// Supported Models (Vercel Registry)
|
| 15 |
+
// -------------------------------
|
|
|
|
|
|
|
|
|
|
| 16 |
const MODEL_IDS = [
|
| 17 |
+
"gemini-2.0-flash-exp", // native image generation
|
| 18 |
+
"gemini-2.0-flash-image", // stable image model
|
| 19 |
+
"gemini-2.0-pro-image", // higher quality
|
| 20 |
+
"gemini-2.0-flash", // text + image
|
| 21 |
+
"gemini-2.0-pro", // text + image
|
| 22 |
];
|
| 23 |
|
| 24 |
+
// -------------------------------
|
| 25 |
+
// Helpers
|
| 26 |
+
// -------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
function getModelIds(selectedModel?: string) {
|
| 28 |
+
const preferred =
|
| 29 |
+
selectedModel && MODEL_IDS.includes(selectedModel)
|
| 30 |
+
? selectedModel
|
| 31 |
+
: MODEL_IDS[0];
|
| 32 |
|
| 33 |
return [
|
| 34 |
+
preferred,
|
| 35 |
+
...MODEL_IDS.filter((m) => m !== preferred),
|
| 36 |
];
|
| 37 |
}
|
| 38 |
|
|
|
|
| 40 |
if (!image.startsWith("data:")) {
|
| 41 |
throw new Error("Invalid image data URL format");
|
| 42 |
}
|
| 43 |
+
const [meta, base64] = image.split(",");
|
| 44 |
+
const mimeType = meta.includes("image/png") ? "image/png" : "image/jpeg";
|
| 45 |
+
return { data: base64, mimeType };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
function formatHistory(history?: HistoryItem[]) {
|
| 49 |
+
if (!history || history.length === 0) return [];
|
| 50 |
+
|
| 51 |
+
return history
|
| 52 |
+
.map((item) => ({
|
| 53 |
+
role: item.role,
|
| 54 |
+
parts: item.parts
|
| 55 |
+
.map((part: HistoryPart) => {
|
| 56 |
+
if (part.text) return { text: part.text };
|
| 57 |
+
if (part.image && item.role === "user") {
|
| 58 |
+
return { inlineData: imageToInlineData(part.image) };
|
| 59 |
+
}
|
| 60 |
+
return null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
})
|
| 62 |
+
.filter(Boolean),
|
| 63 |
+
}))
|
| 64 |
+
.filter((item) => item.parts.length > 0);
|
| 65 |
}
|
| 66 |
|
| 67 |
+
// -------------------------------
|
| 68 |
+
// Core Image Generation
|
| 69 |
+
// -------------------------------
|
| 70 |
async function generateWithModel(
|
| 71 |
model: string,
|
| 72 |
prompt: string,
|
| 73 |
inputImage: string | null,
|
| 74 |
history?: HistoryItem[]
|
| 75 |
+
) {
|
| 76 |
+
const messages = formatHistory(history);
|
|
|
|
| 77 |
|
| 78 |
+
const parts: any[] = [{ text: prompt }];
|
|
|
|
| 79 |
|
|
|
|
| 80 |
if (inputImage) {
|
| 81 |
+
parts.push({ inlineData: imageToInlineData(inputImage) });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
}
|
| 83 |
|
| 84 |
+
messages.push({ role: "user", parts });
|
| 85 |
|
| 86 |
+
const response = await client.images.generate({
|
|
|
|
|
|
|
| 87 |
model,
|
| 88 |
+
prompt,
|
| 89 |
+
images: inputImage ? [inputImage] : undefined,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
});
|
| 91 |
|
| 92 |
+
const imageBase64 = response.data?.[0]?.b64_json || null;
|
| 93 |
+
const description = response.data?.[0]?.revised_prompt || null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
return {
|
| 96 |
+
image: imageBase64 ? `data:image/png;base64,${imageBase64}` : null,
|
| 97 |
+
description,
|
| 98 |
model,
|
| 99 |
};
|
| 100 |
}
|
| 101 |
|
| 102 |
+
// -------------------------------
|
| 103 |
+
// POST Handler
|
| 104 |
+
// -------------------------------
|
| 105 |
export async function POST(req: NextRequest) {
|
| 106 |
try {
|
| 107 |
+
if (!process.env.GEMINI_API_KEY) {
|
| 108 |
return NextResponse.json(
|
| 109 |
{ error: "GEMINI_API_KEY is not configured" },
|
| 110 |
{ status: 500 }
|
| 111 |
);
|
| 112 |
}
|
| 113 |
|
| 114 |
+
const { prompt, image: inputImage, history, model: selectedModel } =
|
| 115 |
+
await req.json();
|
|
|
|
| 116 |
|
| 117 |
if (!prompt) {
|
| 118 |
return NextResponse.json(
|
|
|
|
| 121 |
);
|
| 122 |
}
|
| 123 |
|
| 124 |
+
const attemptedModels: any[] = [];
|
| 125 |
const modelIds = getModelIds(selectedModel);
|
| 126 |
|
| 127 |
for (const modelId of modelIds) {
|
|
|
|
| 134 |
);
|
| 135 |
|
| 136 |
if (!result.image) {
|
| 137 |
+
throw new Error("No image returned from model");
|
| 138 |
}
|
| 139 |
|
| 140 |
const fallback = modelId !== modelIds[0];
|
| 141 |
|
|
|
|
| 142 |
return NextResponse.json({
|
| 143 |
image: result.image,
|
| 144 |
description: result.description,
|
|
|
|
| 149 |
notice: fallback
|
| 150 |
? {
|
| 151 |
type: "info",
|
| 152 |
+
message: `${modelIds[0]} unavailable. Generated with ${modelId}.`,
|
| 153 |
}
|
| 154 |
: null,
|
| 155 |
});
|
| 156 |
+
} catch (err: any) {
|
| 157 |
+
attemptedModels.push({
|
| 158 |
+
model: modelId,
|
| 159 |
+
error: err?.message || String(err),
|
| 160 |
+
});
|
| 161 |
}
|
| 162 |
}
|
| 163 |
|
| 164 |
return NextResponse.json(
|
| 165 |
{
|
| 166 |
error: "Failed to generate image",
|
|
|
|
| 167 |
attemptedModels,
|
| 168 |
},
|
| 169 |
{ status: 500 }
|
| 170 |
);
|
| 171 |
+
} catch (err: any) {
|
|
|
|
| 172 |
return NextResponse.json(
|
| 173 |
{
|
| 174 |
error: "Failed to generate image",
|
| 175 |
+
details: err?.message || String(err),
|
| 176 |
},
|
| 177 |
{ status: 500 }
|
| 178 |
);
|