cinema-studio-pro/api.php.backup

370 lines
14 KiB
Text

<?php
// Suppress HTML error output to prevent breaking JSON responses
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
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 => 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;
// 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' => 'Google Imagen 3'
];
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;
}