158 lines
5.8 KiB
PHP
158 lines
5.8 KiB
PHP
<?php
|
|
// backend/api.php
|
|
|
|
require_once __DIR__ . '/config.php'; // Load environment variables and set CORS headers
|
|
|
|
/**
|
|
* Handles error responses for the API.
|
|
* @param string $message The error message.
|
|
* @param int $statusCode The HTTP status code (default 500).
|
|
*/
|
|
function sendErrorResponse($message, $statusCode = 500) {
|
|
http_response_code($statusCode);
|
|
echo json_encode(['status' => 'error', 'message' => $message]);
|
|
exit();
|
|
}
|
|
|
|
// Ensure the request method is POST
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
sendErrorResponse('Method Not Allowed', 405);
|
|
}
|
|
|
|
// Check for Authorization header
|
|
$headers = getallheaders();
|
|
$authHeader = $headers['Authorization'] ?? '';
|
|
|
|
if (!preg_match('/Bearer\s+(.+)/', $authHeader, $matches)) {
|
|
sendErrorResponse('Authentication required', 401);
|
|
}
|
|
|
|
$accessToken = $matches[1];
|
|
|
|
// Validate the token (basic validation - check if it looks like a JWT)
|
|
if (empty($accessToken) || substr_count($accessToken, '.') !== 2) {
|
|
sendErrorResponse('Invalid authentication token', 401);
|
|
}
|
|
|
|
// Get the raw POST data
|
|
$input = file_get_contents('php://input');
|
|
$data = json_decode($input, true);
|
|
|
|
// --- Input Validation ---
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
sendErrorResponse('Invalid JSON payload.', 400);
|
|
}
|
|
|
|
$imageBase64 = $data['image_base64'] ?? null;
|
|
$prompt = $data['prompt'] ?? null;
|
|
$apiOptions = $data['api_options'] ?? [];
|
|
|
|
if (empty($imageBase64) || !is_string($imageBase64) || !str_starts_with($imageBase64, 'data:image/')) {
|
|
sendErrorResponse('Invalid or missing image_base64. Must be a base64 encoded image data URL.', 400);
|
|
}
|
|
if (empty($prompt) || !is_string($prompt) || trim($prompt) === '') {
|
|
sendErrorResponse('Prompt cannot be empty.', 400);
|
|
}
|
|
if (!is_array($apiOptions)) {
|
|
sendErrorResponse('Invalid api_options. Must be an object/array.', 400);
|
|
}
|
|
|
|
error_log("Received generation request: " . json_encode([
|
|
'prompt' => $prompt,
|
|
'image_size' => round(strlen($imageBase64) / 1024) . ' KB',
|
|
'api_options' => $apiOptions
|
|
]));
|
|
|
|
// --- ACTUAL RUNWAY API INTEGRATION ---
|
|
try {
|
|
// --- CONFIGURE THIS SECTION WITH YOUR ACTUAL RUNWAY GEN4 API DETAILS ---
|
|
// Refer to RunwayML's official Gen4 API documentation for the correct model ID and parameters.
|
|
|
|
// Endpoint for Image to Video as per documentation:
|
|
$runwayApiEndpoint = 'https://api.dev.runwayml.com/v1/image_to_video';
|
|
|
|
// Validate image size (base64 data URI should be under 5MB)
|
|
$imageSizeBytes = strlen($imageBase64);
|
|
if ($imageSizeBytes > 5 * 1024 * 1024) {
|
|
throw new Exception('Image too large. Base64 data URI must be under 5MB.');
|
|
}
|
|
|
|
// Validate the data URI format
|
|
if (!preg_match('/^data:image\/(jpeg|jpg|png|webp);base64,/', $imageBase64)) {
|
|
throw new Exception('Invalid image format. Must be JPEG, PNG, or WebP.');
|
|
}
|
|
|
|
$runwayPayload = [
|
|
'promptImage' => $imageBase64, // Send full data URL format
|
|
'promptText' => $prompt,
|
|
'model' => 'gen4_turbo',
|
|
'ratio' => $apiOptions['ratio'] ?? '1280:720' // Required parameter
|
|
];
|
|
|
|
// Add optional parameters if provided
|
|
if (isset($apiOptions['duration']) && $apiOptions['duration'] !== null) {
|
|
$runwayPayload['duration'] = (int)$apiOptions['duration'];
|
|
}
|
|
if (isset($apiOptions['seed']) && $apiOptions['seed'] !== null) {
|
|
$runwayPayload['seed'] = (int)$apiOptions['seed'];
|
|
}
|
|
|
|
// Log payload without the full image data to avoid huge logs
|
|
$logPayload = $runwayPayload;
|
|
$logPayload['promptImage'] = substr($imageBase64, 0, 50) . '... (' . strlen($imageBase64) . ' chars)';
|
|
error_log("Sending to Runway API: " . json_encode($logPayload));
|
|
|
|
$ch = curl_init($runwayApiEndpoint);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return response as string
|
|
curl_setopt($ch, CURLOPT_POST, true); // Set as POST request
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($runwayPayload)); // Encode payload to JSON
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . RUNWAY_API_KEY, // Use your RUNWAY_API_KEY from .env
|
|
'X-Runway-Version: 2024-11-06' // Required API version header
|
|
]);
|
|
// Optional: for debugging cURL
|
|
// curl_setopt($ch, CURLOPT_VERBOSE, true);
|
|
// $verbose = fopen('php://temp', 'rw+');
|
|
// curl_setopt($ch, CURLOPT_STDERR, $verbose);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$curlError = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($response === false) {
|
|
throw new Exception("cURL error connecting to Runway API: " . $curlError);
|
|
}
|
|
|
|
$runwayResult = json_decode($response, true);
|
|
|
|
if ($httpCode >= 400) {
|
|
// Attempt to extract more specific error message from Runway API response
|
|
$errorMessage = 'Unknown Runway API error';
|
|
if (isset($runwayResult['error']['message'])) {
|
|
$errorMessage = $runwayResult['error']['message'];
|
|
} elseif (isset($runwayResult['message'])) {
|
|
$errorMessage = $runwayResult['message'];
|
|
}
|
|
throw new Exception("Runway API returned error ($httpCode): " . $errorMessage . ". Full response: " . json_encode($runwayResult));
|
|
}
|
|
|
|
// --- Handle Runway's async response ---
|
|
$jobId = $runwayResult['id'] ?? null;
|
|
|
|
if (empty($jobId)) {
|
|
throw new Exception('Runway API did not return a job ID. Full response: ' . json_encode($runwayResult));
|
|
}
|
|
|
|
// For async generation, return job ID for polling
|
|
echo json_encode([
|
|
'status' => 'processing',
|
|
'job_id' => $jobId,
|
|
'message' => 'Video generation started. Please wait...'
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
error_log("Backend processing error: " . $e->getMessage());
|
|
sendErrorResponse("Failed to generate video: " . $e->getMessage(), 500);
|
|
}
|