Gertie2013 commited on
Commit
ea33d76
·
verified ·
1 Parent(s): 703c45e

Update app/api/image/route.ts

Browse files
Files changed (1) hide show
  1. 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 { GoogleGenAI, Modality } from "@google/genai";
3
  import { HistoryItem, HistoryPart } from "@/lib/types";
4
 
5
- // AI-MAINTAINER NOTE:
6
- // Do not "fix" this by reverting to @google/generative-ai.
7
- // Google marks that JS library as legacy/not actively maintained.
8
- // Gemini 2.0 image models may be shut down server-side; code cannot revive them.
9
- // Keep Gemini 2.0 as selectable first try, then fall back to current image models.
10
- // Primary source: https://ai.google.dev/gemini-api/docs/deprecations
11
-
12
- // Initialize the Google Gen AI client with your API key
13
- const GEMINI_API_KEY = process.env.GEMINI_API_KEY || "";
14
- const genAI = new GoogleGenAI({ apiKey: GEMINI_API_KEY });
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-preview-image-generation",
22
- "gemini-3.1-flash-image-preview",
23
- "gemini-3-pro-image-preview",
24
- "gemini-2.5-flash-image",
25
  ];
26
 
27
- // Define interface for the formatted history item
28
- interface FormattedHistoryItem {
29
- role: "user" | "model";
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 preferredModel = selectedModel && MODEL_IDS.includes(selectedModel)
49
- ? selectedModel
50
- : MODEL_ID;
 
51
 
52
  return [
53
- preferredModel,
54
- ...MODEL_IDS.filter((modelId) => modelId !== preferredModel),
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 imageParts = image.split(",");
64
- if (imageParts.length < 2) {
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
- return history && history.length > 0
76
- ? history
77
- .map((item: HistoryItem) => {
78
- return {
79
- role: item.role,
80
- parts: item.parts
81
- .map((part: HistoryPart) => {
82
- if (part.text) {
83
- return { text: part.text };
84
- }
85
- if (part.image && item.role === "user") {
86
- return { inlineData: imageToInlineData(part.image) };
87
- }
88
- return { text: "" };
89
- })
90
- .filter((part) => Object.keys(part).length > 0), // Remove empty parts
91
- };
92
  })
93
- .filter((item: FormattedHistoryItem) => item.parts.length > 0) // Remove items with no parts
94
- : [];
 
95
  }
96
 
 
 
 
97
  async function generateWithModel(
98
  model: string,
99
  prompt: string,
100
  inputImage: string | null,
101
  history?: HistoryItem[]
102
- ): Promise<ImageResult> {
103
- const contents = formatHistory(history);
104
- const messageParts = [];
105
 
106
- // Add the text prompt
107
- messageParts.push({ text: prompt });
108
 
109
- // Add the image if provided
110
  if (inputImage) {
111
- // For image editing
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
- contents.push({ role: "user", parts: messageParts });
127
 
128
- // Send the message to the model
129
- console.log("Sending message with", messageParts.length, "parts", "to", model);
130
- const response = await genAI.models.generateContent({
131
  model,
132
- contents,
133
- config: {
134
- temperature: 1,
135
- topP: 0.95,
136
- topK: 40,
137
- responseModalities: [Modality.TEXT, Modality.IMAGE],
138
- },
139
  });
140
 
141
- let textResponse = null;
142
- let imageData = null;
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: imageData ? `data:${mimeType};base64,${imageData}` : null,
174
- description: textResponse,
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
- // Parse JSON request instead of FormData
189
- const requestData = await req.json();
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: AttemptResult[] = [];
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 API");
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]} is unavailable or did not return an image. Generated with ${modelId} instead.`,
229
  }
230
  : null,
231
  });
232
- } catch (error) {
233
- const message = error instanceof Error ? error.message : String(error);
234
- console.error(`Error generating image with ${modelId}:`, error);
235
- attemptedModels.push({ model: modelId, error: message });
 
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 (error) {
248
- console.error("Error generating image:", error);
249
  return NextResponse.json(
250
  {
251
  error: "Failed to generate image",
252
- details: error instanceof Error ? error.message : String(error),
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
  );