Implemented complete session management system for multiple users:
**New Features:**
- Isolated user sessions with unique session IDs
- File-based image storage (not in PHP session)
- Automatic 24-hour image expiration
- Session directories: uploads/sessions/{session_id}/
- Images stored with metadata (creation time, expiry, MIME type)
**New Files:**
- session_manager.php - Complete session management class
- cleanup.php - Cron script to delete expired images
- uploads/.htaccess - Security: prevent direct file access
**Updated Files:**
- api.php - Uses SessionManager for file-based storage
- index.php - Loads images from disk via SessionManager
- .gitignore - Exclude user uploads from repository
**Usage:**
- Each user gets isolated session automatically
- Images auto-delete after 24 hours
- Run cleanup.php via cron: `0 * * * * php cleanup.php`
**Security:**
- Session IDs regenerated on first access
- Upload directory protected by .htaccess
- User images isolated by session
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
296 lines
11 KiB
PHP
296 lines
11 KiB
PHP
<?php
|
|
header('Content-Type: application/json');
|
|
|
|
// Load configuration and session manager
|
|
require_once 'config.php';
|
|
require_once 'session_manager.php';
|
|
|
|
// Initialize session manager for multi-user support
|
|
$sessionManager = new SessionManager();
|
|
|
|
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') {
|
|
$sessionManager->reset();
|
|
|
|
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
|
|
$currentImage = $sessionManager->getCurrentImage();
|
|
if ($uploadedImage && !$currentImage) {
|
|
error_log("Processing uploaded image (type: $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);
|
|
|
|
// Save edited image to disk
|
|
$filename = $sessionManager->saveImage($imageData['base64'], $imageData['mime_type']);
|
|
$sessionManager->setCurrentImage($filename, $imageData['mime_type']);
|
|
|
|
// Add to conversation history
|
|
$sessionManager->addToHistory('Uploaded image + ' . $prompt, 'upload_edit');
|
|
} else {
|
|
// Just uploaded, no prompt yet - save to disk
|
|
$filename = $sessionManager->saveImage($uploadedImage, $uploadedImageType);
|
|
$sessionManager->setCurrentImage($filename, $uploadedImageType);
|
|
|
|
// Add to conversation history
|
|
$sessionManager->addToHistory('Image uploaded', '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
|
|
$currentImage = $sessionManager->getCurrentImage();
|
|
$inputImage = $currentImage ? $currentImage['data'] : null;
|
|
|
|
// Generate or edit image
|
|
$response = $api->generateImage($prompt, $aspectRatio, $imageSize, $inputImage);
|
|
$imageData = $api->extractImageData($response);
|
|
|
|
// Save to disk
|
|
$filename = $sessionManager->saveImage($imageData['base64'], $imageData['mime_type']);
|
|
$sessionManager->setCurrentImage($filename, $imageData['mime_type']);
|
|
|
|
// Add to conversation history
|
|
$sessionManager->addToHistory($prompt, $inputImage ? 'edit' : 'generate');
|
|
|
|
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;
|
|
}
|