Resolved conflict in AppContent.jsx: combined MSAL fix (unconditional hooks) with new features (Local Developer display name, conditional logout button). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
385 lines
14 KiB
PHP
385 lines
14 KiB
PHP
<?php
|
|
// Suppress HTML error output to prevent breaking JSON responses
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 0);
|
|
ini_set('log_errors', 1);
|
|
set_time_limit(120); // Imagen 3 API calls can take up to 60s
|
|
|
|
// Increase execution time for Gemini API calls (image generation can take 60+ seconds)
|
|
set_time_limit(120);
|
|
|
|
// Increase memory and POST limits for 4K images (base64 can be 20MB+)
|
|
ini_set('memory_limit', '512M');
|
|
ini_set('post_max_size', '100M');
|
|
ini_set('upload_max_filesize', '100M');
|
|
|
|
header('Content-Type: application/json');
|
|
|
|
// Load configuration, session manager, and webhook logger
|
|
require_once 'config.php';
|
|
require_once 'session_manager.php';
|
|
require_once 'webhook_logger.php';
|
|
|
|
// Initialize session manager for multi-user support
|
|
$sessionManager = new SessionManager();
|
|
|
|
// Initialize auth status with default
|
|
$authStatus = [
|
|
'authenticated' => true,
|
|
'user' => [
|
|
'name' => 'User',
|
|
'preferred_username' => 'anonymous@nano-banana-pro.com'
|
|
]
|
|
];
|
|
|
|
// Check authentication (with graceful fallback)
|
|
try {
|
|
if (file_exists(__DIR__ . '/AuthMiddleware.php')) {
|
|
require_once 'AuthMiddleware.php';
|
|
$auth = new AuthMiddleware();
|
|
$authStatus = $auth->isAuthenticated();
|
|
|
|
if (!$authStatus['authenticated']) {
|
|
http_response_code(401);
|
|
echo json_encode([
|
|
'success' => false,
|
|
'error' => 'Authentication required',
|
|
'requiresAuth' => true
|
|
]);
|
|
exit;
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// Log error but continue without auth (for testing)
|
|
error_log("Auth check failed in api.php: " . $e->getMessage());
|
|
}
|
|
|
|
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, $referenceImages = []) {
|
|
$parts = [];
|
|
|
|
// IMPORTANT: Input image (the one being edited) must come FIRST
|
|
// Gemini treats the first image as the primary subject to modify
|
|
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 FIRST to request (mime_type: image/jpeg)");
|
|
}
|
|
|
|
// Add reference images after input image (up to 14 allowed by Gemini 3 Pro)
|
|
if (!empty($referenceImages)) {
|
|
error_log("Adding " . count($referenceImages) . " reference image(s) to request");
|
|
|
|
foreach ($referenceImages as $index => $refImg) {
|
|
$data = preg_replace('/\s+/', '', $refImg['data']);
|
|
|
|
// Basic validation
|
|
if (!preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $data)) {
|
|
error_log("Base64 validation failed for reference image $index");
|
|
continue;
|
|
}
|
|
|
|
$parts[] = [
|
|
'inline_data' => [
|
|
'mime_type' => $refImg['mime_type'] ?? 'image/jpeg',
|
|
'data' => $data
|
|
]
|
|
];
|
|
|
|
error_log("Added reference image $index (mime_type: " . ($refImg['mime_type'] ?? 'image/jpeg') . ")");
|
|
}
|
|
}
|
|
|
|
if (!$inputImage && empty($referenceImages)) {
|
|
error_log("Generation mode: No input image or reference images");
|
|
}
|
|
|
|
// 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 => false, // Disabled for MAMP development
|
|
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 && $retryCount < 3) {
|
|
// Retry on internal/server errors (up to 3 times with exponential backoff)
|
|
$waitTime = pow(2, $retryCount + 1); // 2s, 4s, 8s
|
|
error_log("Retrying request due to server error (attempt " . ($retryCount + 1) . " of 3, waiting {$waitTime}s)");
|
|
sleep($waitTime);
|
|
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.");
|
|
}
|
|
|
|
// Provide user-friendly error for common issues
|
|
if ($httpCode === 500) {
|
|
throw new Exception("Google's image generation service is temporarily unavailable. Please try again in a few moments. (HTTP 500)");
|
|
}
|
|
|
|
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;
|
|
|
|
// Collect reference images (up to 14)
|
|
$referenceImages = [];
|
|
$refCount = intval($_POST['referenceImageCount'] ?? 0);
|
|
for ($i = 0; $i < min($refCount, 14); $i++) {
|
|
if (isset($_POST["referenceImage_$i"])) {
|
|
$referenceImages[] = [
|
|
'data' => $_POST["referenceImage_$i"],
|
|
'mime_type' => $_POST["referenceImageType_$i"] ?? 'image/jpeg'
|
|
];
|
|
}
|
|
}
|
|
|
|
if (!empty($referenceImages)) {
|
|
error_log("Received " . count($referenceImages) . " reference images from frontend");
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
|
|
// Regular generation/editing flow
|
|
if (!$prompt) {
|
|
throw new Exception('Prompt is required');
|
|
}
|
|
|
|
// Initialize API
|
|
$api = new NanoBananaProAPI(GEMINI_API_KEY);
|
|
|
|
// Determine input image for editing:
|
|
// 1. Frontend sends uploadedImage when editing from library or a displayed image
|
|
// 2. Fall back to session's current image for iterative edits
|
|
$inputImage = null;
|
|
if ($uploadedImage) {
|
|
// Frontend explicitly sent an image - use it (this is the source of truth)
|
|
$inputImage = $uploadedImage;
|
|
error_log("Using uploaded image from frontend for editing");
|
|
} else {
|
|
// No uploaded image - check session for iterative editing
|
|
$currentImage = $sessionManager->getCurrentImage();
|
|
$inputImage = $currentImage ? $currentImage['data'] : null;
|
|
if ($inputImage) {
|
|
error_log("Using session image for editing");
|
|
} else {
|
|
error_log("No input image - generating new image");
|
|
}
|
|
}
|
|
|
|
// Generate or edit image (with optional reference images)
|
|
$response = $api->generateImage($prompt, $aspectRatio, $imageSize, $inputImage, $referenceImages);
|
|
$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');
|
|
|
|
// Log to webhook
|
|
try {
|
|
$userEmail = $authStatus['user']['preferred_username'] ?? $authStatus['user']['email'] ?? 'anonymous@nano-banana-pro.com';
|
|
$webhookSettings = [
|
|
'prompt' => $prompt,
|
|
'aspectRatio' => $aspectRatio,
|
|
'imageSize' => $imageSize,
|
|
'actionType' => $inputImage ? 'edit' : 'generate',
|
|
'model' => 'Gemini 3 Pro Image'
|
|
];
|
|
|
|
logImageGeneration($prompt, $imageData['base64'], $imageData['mime_type'], $webhookSettings, $userEmail, $inputImage ? 'edit' : 'generate');
|
|
} catch (Exception $e) {
|
|
// Don't fail if webhook fails
|
|
error_log("Webhook logging failed: " . $e->getMessage());
|
|
}
|
|
|
|
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;
|
|
}
|