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:
commit
4deed84ba0
13 changed files with 2700 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
39
.htaccess
Normal 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
685
AI_IMPLEMENTATION_GUIDE.md
Normal 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
37
Banana.save
Normal file
File diff suppressed because one or more lines are too long
242
QUICK_REFERENCE.md
Normal file
242
QUICK_REFERENCE.md
Normal 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
244
README.md
Normal 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
313
api.php
Normal 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
36
clear_session.php
Normal 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
14
config.example.php
Normal 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
101
debug.php
Normal 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
155
debug_request.php
Normal 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
38
get_logs.php
Normal 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
772
index.php
Normal 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>
|
||||
Loading…
Add table
Reference in a new issue