Upload 5 files
#3
by John6666 - opened
- GEMINI_2_MODEL_DRIFT_NOTE.md +84 -0
- app/api/image/route.ts +182 -159
- app/page.tsx +16 -20
- package-lock.json +0 -0
- package.json +38 -42
GEMINI_2_MODEL_DRIFT_NOTE.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gemini 2.0 image generation drift note for future AI maintainers
|
| 2 |
+
|
| 3 |
+
This note exists to prevent accidental regressions when editing this Space.
|
| 4 |
+
|
| 5 |
+
## Do not try to revive Gemini 2.0 Experimental with code
|
| 6 |
+
|
| 7 |
+
Some Gemini 2.0 image-generation model IDs were experimental or preview model IDs. Google can shut those models down server-side. Once a model is shut down, changing JavaScript code or reverting a package cannot make that endpoint available again.
|
| 8 |
+
|
| 9 |
+
Primary source:
|
| 10 |
+
- Gemini API deprecations: https://ai.google.dev/gemini-api/docs/deprecations
|
| 11 |
+
- Gemini API release notes: https://ai.google.dev/gemini-api/docs/changelog
|
| 12 |
+
|
| 13 |
+
Google's deprecations page says that once a model is shut down, it is completely turned off and the endpoint is no longer available.
|
| 14 |
+
|
| 15 |
+
Known affected image-generation IDs include, or have included:
|
| 16 |
+
|
| 17 |
+
- `gemini-2.0-flash-exp`
|
| 18 |
+
- `gemini-2.0-flash-exp-image-generation`
|
| 19 |
+
- `gemini-2.0-flash-preview-image-generation`
|
| 20 |
+
|
| 21 |
+
Keep them only as first-try compatibility options. Do not assume they will work.
|
| 22 |
+
|
| 23 |
+
## Use the current Google Gen AI SDK, not the old JS SDK
|
| 24 |
+
|
| 25 |
+
The old package:
|
| 26 |
+
|
| 27 |
+
```txt
|
| 28 |
+
@google/generative-ai
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
is the older Google AI JavaScript SDK. Do not restore it just because old examples use `GoogleGenerativeAI`.
|
| 32 |
+
|
| 33 |
+
The current package for JavaScript/TypeScript Gemini API examples is:
|
| 34 |
+
|
| 35 |
+
```txt
|
| 36 |
+
@google/genai
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
Primary source:
|
| 40 |
+
- Gemini API libraries: https://ai.google.dev/gemini-api/docs/libraries
|
| 41 |
+
|
| 42 |
+
## Expected strategy for this Space
|
| 43 |
+
|
| 44 |
+
The app should behave like this:
|
| 45 |
+
|
| 46 |
+
1. Let the user select a preferred model.
|
| 47 |
+
2. Default to `gemini-2.0-flash-exp` because the original request asked for Gemini 2.0 Experimental.
|
| 48 |
+
3. If that model fails or returns no image, try current Gemini image models.
|
| 49 |
+
4. Show an informational notice when fallback happens.
|
| 50 |
+
5. Do not treat fallback success as a fatal error.
|
| 51 |
+
|
| 52 |
+
Recommended fallback candidates, subject to current Google documentation:
|
| 53 |
+
|
| 54 |
+
```txt
|
| 55 |
+
gemini-2.0-flash-exp
|
| 56 |
+
gemini-2.0-flash-preview-image-generation
|
| 57 |
+
gemini-3.1-flash-image-preview
|
| 58 |
+
gemini-3-pro-image-preview
|
| 59 |
+
gemini-2.5-flash-image
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
## Response shape warning
|
| 63 |
+
|
| 64 |
+
Do not mix response formats from different libraries.
|
| 65 |
+
|
| 66 |
+
With `@google/genai`, read images from response parts such as:
|
| 67 |
+
|
| 68 |
+
```ts
|
| 69 |
+
response.candidates?.[0]?.content?.parts
|
| 70 |
+
part.inlineData
|
| 71 |
+
part.text
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
Do not rewrite this route using old snippets that expect a different SDK response shape unless you also update dependencies and test the build.
|
| 75 |
+
|
| 76 |
+
## Important environment variable
|
| 77 |
+
|
| 78 |
+
The Space needs a secret:
|
| 79 |
+
|
| 80 |
+
```txt
|
| 81 |
+
GEMINI_API_KEY
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
Missing `GEMINI_API_KEY` is a runtime/API error, not a Docker build error.
|
app/api/image/route.ts
CHANGED
|
@@ -1,142 +1,193 @@
|
|
| 1 |
import { NextRequest, NextResponse } from "next/server";
|
| 2 |
-
import {
|
| 3 |
-
import {
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
const MODEL_ID = "gemini-2.0-flash-exp";
|
|
|
|
| 6 |
const MODEL_IDS = [
|
| 7 |
-
|
| 8 |
-
"gemini-2.
|
| 9 |
"gemini-3.1-flash-image-preview",
|
| 10 |
"gemini-3-pro-image-preview",
|
|
|
|
| 11 |
];
|
| 12 |
|
| 13 |
-
interface
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
-
interface
|
| 19 |
-
|
| 20 |
-
|
|
|
|
| 21 |
}
|
| 22 |
|
| 23 |
-
interface
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
history?: HistoryMessage[];
|
| 27 |
-
model?: string;
|
| 28 |
}
|
| 29 |
|
| 30 |
-
function
|
| 31 |
-
const
|
| 32 |
-
|
| 33 |
-
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
-
function
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
function toDataUrl(data: unknown, mediaType = "image/png") {
|
| 44 |
-
if (typeof data === "string") {
|
| 45 |
-
if (data.startsWith("data:")) {
|
| 46 |
-
return data;
|
| 47 |
-
}
|
| 48 |
-
if (data.startsWith("http://") || data.startsWith("https://")) {
|
| 49 |
-
return data;
|
| 50 |
-
}
|
| 51 |
-
return `data:${mediaType};base64,${data}`;
|
| 52 |
}
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
|
|
|
| 56 |
}
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
return null;
|
| 63 |
}
|
| 64 |
|
| 65 |
-
function
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
}
|
| 82 |
}
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
}
|
| 98 |
}
|
| 99 |
|
| 100 |
-
return
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
history
|
| 106 |
-
?.map((msg) => {
|
| 107 |
-
const role = msg.role === "user" ? "user" : "assistant";
|
| 108 |
-
const parts = msg.parts
|
| 109 |
-
.map((p) => {
|
| 110 |
-
if (p.text) {
|
| 111 |
-
return { type: "text", text: p.text };
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
if (p.image && role === "user") {
|
| 115 |
-
return {
|
| 116 |
-
type: "image",
|
| 117 |
-
image: getBase64Data(p.image),
|
| 118 |
-
mediaType: getMimeType(p.image),
|
| 119 |
-
};
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
return null;
|
| 123 |
-
})
|
| 124 |
-
.filter(Boolean);
|
| 125 |
-
|
| 126 |
-
if (parts.length === 0) {
|
| 127 |
-
return null;
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
return { role, content: parts };
|
| 131 |
-
})
|
| 132 |
-
.filter(Boolean) ?? []
|
| 133 |
-
);
|
| 134 |
}
|
| 135 |
|
| 136 |
export async function POST(req: NextRequest) {
|
| 137 |
try {
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
if (!prompt) {
|
| 142 |
return NextResponse.json(
|
|
@@ -145,88 +196,60 @@ export async function POST(req: NextRequest) {
|
|
| 145 |
);
|
| 146 |
}
|
| 147 |
|
| 148 |
-
const
|
| 149 |
-
|
| 150 |
-
});
|
| 151 |
-
const parts: any[] = [{ type: "text", text: prompt }];
|
| 152 |
|
| 153 |
-
|
| 154 |
-
if (!inputImage.startsWith("data:")) {
|
| 155 |
-
return NextResponse.json(
|
| 156 |
-
{ error: "Invalid image data URL format" },
|
| 157 |
-
{ status: 400 }
|
| 158 |
-
);
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
parts.push({
|
| 162 |
-
type: "image",
|
| 163 |
-
image: getBase64Data(inputImage),
|
| 164 |
-
mediaType: getMimeType(inputImage),
|
| 165 |
-
});
|
| 166 |
-
}
|
| 167 |
-
|
| 168 |
-
const formattedHistory = formatHistory(history);
|
| 169 |
-
const modelCandidates = getModelCandidates(model);
|
| 170 |
-
const attemptedModels: { model: string; error?: string }[] = [];
|
| 171 |
-
|
| 172 |
-
for (const modelId of modelCandidates) {
|
| 173 |
try {
|
| 174 |
-
const result = await
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
},
|
| 181 |
-
},
|
| 182 |
-
});
|
| 183 |
-
|
| 184 |
-
const image = extractImage(result);
|
| 185 |
|
| 186 |
-
if (!image) {
|
| 187 |
-
|
| 188 |
-
continue;
|
| 189 |
}
|
| 190 |
|
| 191 |
-
const fallback = modelId !==
|
| 192 |
|
|
|
|
| 193 |
return NextResponse.json({
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
model:
|
|
|
|
| 197 |
fallback,
|
| 198 |
attemptedModels,
|
| 199 |
notice: fallback
|
| 200 |
? {
|
| 201 |
type: "info",
|
| 202 |
-
message: `${
|
| 203 |
}
|
| 204 |
: null,
|
| 205 |
});
|
| 206 |
-
} catch (
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
});
|
| 211 |
}
|
| 212 |
}
|
| 213 |
|
| 214 |
return NextResponse.json(
|
| 215 |
{
|
| 216 |
error: "Failed to generate image",
|
| 217 |
-
details: attemptedModels.map((
|
| 218 |
attemptedModels,
|
| 219 |
},
|
| 220 |
{ status: 500 }
|
| 221 |
);
|
| 222 |
-
} catch (
|
| 223 |
-
|
| 224 |
-
err instanceof Error ? err.message : "Unknown error";
|
| 225 |
-
|
| 226 |
return NextResponse.json(
|
| 227 |
{
|
| 228 |
error: "Failed to generate image",
|
| 229 |
-
details: message,
|
| 230 |
},
|
| 231 |
{ status: 500 }
|
| 232 |
);
|
|
|
|
| 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 |
|
| 58 |
+
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 |
);
|
| 197 |
}
|
| 198 |
|
| 199 |
+
const attemptedModels: AttemptResult[] = [];
|
| 200 |
+
const modelIds = getModelIds(selectedModel);
|
|
|
|
|
|
|
| 201 |
|
| 202 |
+
for (const modelId of modelIds) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
try {
|
| 204 |
+
const result = await generateWithModel(
|
| 205 |
+
modelId,
|
| 206 |
+
prompt,
|
| 207 |
+
inputImage || null,
|
| 208 |
+
history
|
| 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,
|
| 221 |
+
model: result.model,
|
| 222 |
+
preferredModel: modelIds[0],
|
| 223 |
fallback,
|
| 224 |
attemptedModels,
|
| 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 |
);
|
app/page.tsx
CHANGED
|
@@ -3,17 +3,10 @@ import { useState } from "react";
|
|
| 3 |
import { ImageUpload } from "@/components/ImageUpload";
|
| 4 |
import { ImagePromptInput } from "@/components/ImagePromptInput";
|
| 5 |
import { ImageResultDisplay } from "@/components/ImageResultDisplay";
|
| 6 |
-
import { ImageIcon, Wand2, ExternalLink } from "lucide-react";
|
| 7 |
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
| 8 |
import { HistoryItem } from "@/lib/types";
|
| 9 |
|
| 10 |
-
const MODEL_OPTIONS = [
|
| 11 |
-
"gemini-2.0-flash-exp",
|
| 12 |
-
"gemini-2.5-flash-image",
|
| 13 |
-
"gemini-3.1-flash-image-preview",
|
| 14 |
-
"gemini-3-pro-image-preview",
|
| 15 |
-
];
|
| 16 |
-
|
| 17 |
export default function Home() {
|
| 18 |
const [image, setImage] = useState<string | null>(null);
|
| 19 |
const [generatedImage, setGeneratedImage] = useState<string | null>(null);
|
|
@@ -21,8 +14,8 @@ export default function Home() {
|
|
| 21 |
const [loading, setLoading] = useState(false);
|
| 22 |
const [error, setError] = useState<string | null>(null);
|
| 23 |
const [notice, setNotice] = useState<string | null>(null);
|
|
|
|
| 24 |
const [history, setHistory] = useState<HistoryItem[]>([]);
|
| 25 |
-
const [selectedModel, setSelectedModel] = useState(MODEL_OPTIONS[0]);
|
| 26 |
|
| 27 |
const handleImageSelect = (imageData: string) => {
|
| 28 |
setImage(imageData || null);
|
|
@@ -42,7 +35,7 @@ export default function Home() {
|
|
| 42 |
prompt,
|
| 43 |
image: imageToEdit,
|
| 44 |
history: history.length > 0 ? history : undefined,
|
| 45 |
-
model
|
| 46 |
};
|
| 47 |
|
| 48 |
const response = await fetch("/api/image", {
|
|
@@ -142,8 +135,9 @@ export default function Home() {
|
|
| 142 |
)}
|
| 143 |
|
| 144 |
{notice && (
|
| 145 |
-
<div className="p-4 mb-4 text-sm text-blue-700 bg-blue-100 rounded-lg">
|
| 146 |
-
|
|
|
|
| 147 |
</div>
|
| 148 |
)}
|
| 149 |
|
|
@@ -153,16 +147,18 @@ export default function Home() {
|
|
| 153 |
</label>
|
| 154 |
<select
|
| 155 |
id="model-select"
|
| 156 |
-
value={
|
| 157 |
-
onChange={(event) =>
|
|
|
|
| 158 |
disabled={loading}
|
| 159 |
-
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
|
| 160 |
>
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
| 166 |
</select>
|
| 167 |
</div>
|
| 168 |
|
|
|
|
| 3 |
import { ImageUpload } from "@/components/ImageUpload";
|
| 4 |
import { ImagePromptInput } from "@/components/ImagePromptInput";
|
| 5 |
import { ImageResultDisplay } from "@/components/ImageResultDisplay";
|
| 6 |
+
import { ImageIcon, Wand2, ExternalLink, Info } from "lucide-react";
|
| 7 |
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
| 8 |
import { HistoryItem } from "@/lib/types";
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
export default function Home() {
|
| 11 |
const [image, setImage] = useState<string | null>(null);
|
| 12 |
const [generatedImage, setGeneratedImage] = useState<string | null>(null);
|
|
|
|
| 14 |
const [loading, setLoading] = useState(false);
|
| 15 |
const [error, setError] = useState<string | null>(null);
|
| 16 |
const [notice, setNotice] = useState<string | null>(null);
|
| 17 |
+
const [model, setModel] = useState("gemini-2.0-flash-exp");
|
| 18 |
const [history, setHistory] = useState<HistoryItem[]>([]);
|
|
|
|
| 19 |
|
| 20 |
const handleImageSelect = (imageData: string) => {
|
| 21 |
setImage(imageData || null);
|
|
|
|
| 35 |
prompt,
|
| 36 |
image: imageToEdit,
|
| 37 |
history: history.length > 0 ? history : undefined,
|
| 38 |
+
model,
|
| 39 |
};
|
| 40 |
|
| 41 |
const response = await fetch("/api/image", {
|
|
|
|
| 135 |
)}
|
| 136 |
|
| 137 |
{notice && (
|
| 138 |
+
<div className="p-4 mb-4 text-sm text-blue-700 bg-blue-100 rounded-lg flex items-start gap-2">
|
| 139 |
+
<Info className="h-4 w-4 mt-0.5 shrink-0" />
|
| 140 |
+
<span>{notice}</span>
|
| 141 |
</div>
|
| 142 |
)}
|
| 143 |
|
|
|
|
| 147 |
</label>
|
| 148 |
<select
|
| 149 |
id="model-select"
|
| 150 |
+
value={model}
|
| 151 |
+
onChange={(event) => setModel(event.target.value)}
|
| 152 |
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
| 153 |
disabled={loading}
|
|
|
|
| 154 |
>
|
| 155 |
+
<option value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
|
| 156 |
+
<option value="gemini-2.0-flash-preview-image-generation">
|
| 157 |
+
gemini-2.0-flash-preview-image-generation
|
| 158 |
+
</option>
|
| 159 |
+
<option value="gemini-3.1-flash-image-preview">gemini-3.1-flash-image-preview</option>
|
| 160 |
+
<option value="gemini-3-pro-image-preview">gemini-3-pro-image-preview</option>
|
| 161 |
+
<option value="gemini-2.5-flash-image">gemini-2.5-flash-image</option>
|
| 162 |
</select>
|
| 163 |
</div>
|
| 164 |
|
package-lock.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
CHANGED
|
@@ -1,44 +1,40 @@
|
|
| 1 |
{
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
"
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
"prettier-eslint": "^16.3.0",
|
| 41 |
-
"tailwindcss": "^3.4.1",
|
| 42 |
-
"typescript": "^5"
|
| 43 |
-
}
|
| 44 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "nextjs-gemini-2-0-pdf-structured-data",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev --turbopack",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "next lint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@google/genai": "^2.0.0",
|
| 13 |
+
"@radix-ui/react-dialog": "^1.1.6",
|
| 14 |
+
"@radix-ui/react-popover": "^1.1.6",
|
| 15 |
+
"@radix-ui/react-slot": "^1.1.2",
|
| 16 |
+
"@wojtekmaj/react-hooks": "^1.22.0",
|
| 17 |
+
"class-variance-authority": "^0.7.1",
|
| 18 |
+
"clsx": "^2.1.1",
|
| 19 |
+
"lucide-react": "^0.475.0",
|
| 20 |
+
"next": "15.1.0",
|
| 21 |
+
"next-themes": "^0.4.4",
|
| 22 |
+
"react": "^19.0.0",
|
| 23 |
+
"react-dom": "^19.0.0",
|
| 24 |
+
"react-dropzone": "^14.3.8",
|
| 25 |
+
"tailwind-merge": "^3.0.1",
|
| 26 |
+
"tailwindcss-animate": "^1.0.7"
|
| 27 |
+
},
|
| 28 |
+
"devDependencies": {
|
| 29 |
+
"@eslint/eslintrc": "^3",
|
| 30 |
+
"@types/node": "^20",
|
| 31 |
+
"@types/react": "^19",
|
| 32 |
+
"@types/react-dom": "^19",
|
| 33 |
+
"eslint": "^9",
|
| 34 |
+
"eslint-config-next": "15.1.0",
|
| 35 |
+
"postcss": "^8",
|
| 36 |
+
"prettier-eslint": "^16.3.0",
|
| 37 |
+
"tailwindcss": "^3.4.1",
|
| 38 |
+
"typescript": "^5"
|
| 39 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|