Skip to content

Commit b2eb9b6

Browse files
committed
add app
1 parent ed97d35 commit b2eb9b6

35 files changed

+9000
-2
lines changed

.env.example

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
GEMINI_API_KEY=your_gemini_api_key

Dockerfile

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# syntax=docker.io/docker/dockerfile:1
2+
3+
FROM node:22-alpine AS base
4+
5+
# Install dependencies only when needed
6+
FROM base AS deps
7+
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
8+
RUN apk add --no-cache libc6-compat
9+
WORKDIR /app
10+
11+
# Install dependencies based on the preferred package manager
12+
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
13+
RUN \
14+
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
15+
elif [ -f package-lock.json ]; then npm ci; \
16+
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
17+
else echo "Lockfile not found." && exit 1; \
18+
fi
19+
20+
21+
# Rebuild the source code only when needed
22+
FROM base AS builder
23+
WORKDIR /app
24+
COPY --from=deps /app/node_modules ./node_modules
25+
COPY . .
26+
27+
# Next.js collects completely anonymous telemetry data about general usage.
28+
# Learn more here: https://nextjs.org/telemetry
29+
# Uncomment the following line in case you want to disable telemetry during the build.
30+
# ENV NEXT_TELEMETRY_DISABLED=1
31+
32+
RUN \
33+
if [ -f yarn.lock ]; then yarn run build; \
34+
elif [ -f package-lock.json ]; then npm run build; \
35+
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
36+
else echo "Lockfile not found." && exit 1; \
37+
fi
38+
39+
# Production image, copy all the files and run next
40+
FROM base AS runner
41+
WORKDIR /app
42+
43+
ENV NODE_ENV=production
44+
# Uncomment the following line in case you want to disable telemetry during runtime.
45+
# ENV NEXT_TELEMETRY_DISABLED=1
46+
47+
RUN addgroup --system --gid 1001 nodejs
48+
RUN adduser --system --uid 1001 nextjs
49+
50+
COPY --from=builder /app/public ./public
51+
52+
# Automatically leverage output traces to reduce image size
53+
# https://nextjs.org/docs/advanced-features/output-file-tracing
54+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
55+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
56+
57+
USER nextjs
58+
59+
EXPOSE 3000
60+
61+
ENV PORT=3000
62+
63+
# server.js is created by next build from the standalone output
64+
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
65+
ENV HOSTNAME="0.0.0.0"
66+
CMD ["node", "server.js"]

README.md

+81-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,81 @@
1-
# gemini-image-editing-nextjs-quickstart
2-
Get started with native image generation and editing using Gemini 2.0 and Next.js
1+
# Image Creation & Editing with Next.js and Gemini 2.0 Flash
2+
3+
This project demonstrates how to create and edit images using Google's Gemini 2.0 Flash AI model in a Next.js web application. It allows users to generate images from text prompts or edit existing images through natural language instructions, maintaining conversation context for iterative refinements.
4+
5+
**How It Works:**
6+
7+
1. **Create Images**: Generate images from text prompts using Gemini 2.0 Flash
8+
2. **Edit Images**: Upload an image and provide instructions to modify it
9+
3. **Conversation History**: Maintain context through a conversation with the AI for iterative refinements
10+
4. **Download Results**: Save your generated or edited images
11+
12+
## Features
13+
14+
- 🎨 Text-to-image generation with Gemini 2.0 Flash
15+
- 🖌️ Image editing through natural language instructions
16+
- 💬 Conversation history for context-aware image refinements
17+
- 📱 Responsive UI built with Next.js and shadcn/ui
18+
- 🔄 Seamless workflow between creation and editing modes
19+
- ⚡ Uses Gemini 2.0 Flash Javascript SDK
20+
21+
## Known Issues
22+
23+
- **Hydration Mismatch**: If you encounter hydration mismatch errors related to attributes like `data-llm4sre-ubi-main-called`, this is likely caused by browser extensions that modify the DOM. We've added `suppressHydrationWarning` to the body tag to prevent these errors from affecting the application.
24+
25+
## Getting Started
26+
27+
### Local Development
28+
29+
First, set up your environment variables:
30+
31+
```bash
32+
cp .env.example .env
33+
```
34+
35+
Add your Google AI Studio API key to the `.env` file:
36+
37+
```
38+
GEMINI_API_KEY=your_google_api_key
39+
```
40+
41+
Then, install dependencies and run the development server:
42+
43+
```bash
44+
npm install
45+
npm run dev
46+
```
47+
48+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the application.
49+
50+
### Docker Deployment
51+
52+
1. Build the Docker image:
53+
54+
```bash
55+
docker build -t nextjs-gemini-image-editing .
56+
```
57+
58+
2. Run the container with your Google API key:
59+
60+
```bash
61+
docker run -p 3000:3000 -e GEMINI_API_KEY=your_google_api_key nextjs-gemini-image-editing
62+
```
63+
64+
Or using an environment file:
65+
66+
```bash
67+
# Run container with env file
68+
docker run -p 3000:3000 --env-file .env nextjs-gemini-image-editing
69+
```
70+
71+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the application.
72+
73+
## Technologies Used
74+
75+
- [Next.js](https://nextjs.org/) - React framework for the web application
76+
- [Google Gemini 2.0 Flash](https://deepmind.google/technologies/gemini/) - AI model for image generation and editing
77+
- [shadcn/ui](https://ui.shadcn.com/) - Re-usable components built using Radix UI and Tailwind CSS
78+
79+
## License
80+
81+
This project is licensed under the Apache License 2.0 - see the [LICENSE](./LICENSE) file for details.

app/api/image/route.ts

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { GoogleGenerativeAI } from "@google/generative-ai";
3+
import { HistoryItem, HistoryPart } from "@/lib/types";
4+
5+
// Initialize the Google Gen AI client with your API key
6+
const GEMINI_API_KEY = process.env.GEMINI_API_KEY || "";
7+
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
8+
9+
// Define the model ID for Gemini 2.0 Flash experimental
10+
const MODEL_ID = "gemini-2.0-flash-exp";
11+
12+
// Define interface for the formatted history item
13+
interface FormattedHistoryItem {
14+
role: "user" | "model";
15+
parts: Array<{
16+
text?: string;
17+
inlineData?: { data: string; mimeType: string };
18+
}>;
19+
}
20+
21+
export async function POST(req: NextRequest) {
22+
try {
23+
// Parse JSON request instead of FormData
24+
const requestData = await req.json();
25+
const { prompt, image: inputImage, history } = requestData;
26+
27+
if (!prompt) {
28+
return NextResponse.json(
29+
{ error: "Prompt is required" },
30+
{ status: 400 }
31+
);
32+
}
33+
34+
// Get the model with the correct configuration
35+
const model = genAI.getGenerativeModel({
36+
model: MODEL_ID,
37+
generationConfig: {
38+
temperature: 1,
39+
topP: 0.95,
40+
topK: 40,
41+
// @ts-expect-error - Gemini API JS is missing this type
42+
responseModalities: ["Text", "Image"],
43+
},
44+
});
45+
46+
let result;
47+
48+
try {
49+
// Convert history to the format expected by Gemini API
50+
const formattedHistory =
51+
history && history.length > 0
52+
? history
53+
.map((item: HistoryItem) => {
54+
return {
55+
role: item.role,
56+
parts: item.parts
57+
.map((part: HistoryPart) => {
58+
if (part.text) {
59+
return { text: part.text };
60+
}
61+
if (part.image && item.role === "user") {
62+
const imgParts = part.image.split(",");
63+
if (imgParts.length > 1) {
64+
return {
65+
inlineData: {
66+
data: imgParts[1],
67+
mimeType: part.image.includes("image/png")
68+
? "image/png"
69+
: "image/jpeg",
70+
},
71+
};
72+
}
73+
}
74+
return { text: "" };
75+
})
76+
.filter((part) => Object.keys(part).length > 0), // Remove empty parts
77+
};
78+
})
79+
.filter((item: FormattedHistoryItem) => item.parts.length > 0) // Remove items with no parts
80+
: [];
81+
82+
// Create a chat session with the formatted history
83+
const chat = model.startChat({
84+
history: formattedHistory,
85+
});
86+
87+
// Prepare the current message parts
88+
const messageParts = [];
89+
90+
// Add the text prompt
91+
messageParts.push({ text: prompt });
92+
93+
// Add the image if provided
94+
if (inputImage) {
95+
// For image editing
96+
console.log("Processing image edit request");
97+
98+
// Check if the image is a valid data URL
99+
if (!inputImage.startsWith("data:")) {
100+
throw new Error("Invalid image data URL format");
101+
}
102+
103+
const imageParts = inputImage.split(",");
104+
if (imageParts.length < 2) {
105+
throw new Error("Invalid image data URL format");
106+
}
107+
108+
const base64Image = imageParts[1];
109+
const mimeType = inputImage.includes("image/png")
110+
? "image/png"
111+
: "image/jpeg";
112+
console.log(
113+
"Base64 image length:",
114+
base64Image.length,
115+
"MIME type:",
116+
mimeType
117+
);
118+
119+
// Add the image to message parts
120+
messageParts.push({
121+
inlineData: {
122+
data: base64Image,
123+
mimeType: mimeType,
124+
},
125+
});
126+
}
127+
128+
// Send the message to the chat
129+
console.log("Sending message with", messageParts.length, "parts");
130+
result = await chat.sendMessage(messageParts);
131+
} catch (error) {
132+
console.error("Error in chat.sendMessage:", error);
133+
throw error;
134+
}
135+
136+
const response = result.response;
137+
138+
let textResponse = null;
139+
let imageData = null;
140+
let mimeType = "image/png";
141+
142+
// Process the response
143+
if (response.candidates && response.candidates.length > 0) {
144+
const parts = response.candidates[0].content.parts;
145+
console.log("Number of parts in response:", parts.length);
146+
147+
for (const part of parts) {
148+
if ("inlineData" in part && part.inlineData) {
149+
// Get the image data
150+
imageData = part.inlineData.data;
151+
mimeType = part.inlineData.mimeType || "image/png";
152+
console.log(
153+
"Image data received, length:",
154+
imageData.length,
155+
"MIME type:",
156+
mimeType
157+
);
158+
} else if ("text" in part && part.text) {
159+
// Store the text
160+
textResponse = part.text;
161+
console.log(
162+
"Text response received:",
163+
textResponse.substring(0, 50) + "..."
164+
);
165+
}
166+
}
167+
}
168+
169+
// Return just the base64 image and description as JSON
170+
return NextResponse.json({
171+
image: imageData ? `data:${mimeType};base64,${imageData}` : null,
172+
description: textResponse,
173+
});
174+
} catch (error) {
175+
console.error("Error generating image:", error);
176+
return NextResponse.json(
177+
{
178+
error: "Failed to generate image",
179+
details: error instanceof Error ? error.message : String(error),
180+
},
181+
{ status: 500 }
182+
);
183+
}
184+
}

app/favicon.ico

25.3 KB
Binary file not shown.

0 commit comments

Comments
 (0)