cinema-studio-pro/backend/video_api.php
Manish Tanwar e1067b551e update
2026-01-27 18:36:53 +05:30

591 lines
22 KiB
PHP

<?php
/**
* Video Generation API for Cinema Studio Pro
* Handles Veo 3.1 video generation via Gemini API
*/
// 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');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Handle preflight requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// Load configuration
require_once 'config.php';
class VeoVideoAPI {
private $apiKey;
private $baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
private $model;
// Available models
private static $models = [
'standard' => 'veo-3.1-generate-preview',
'fast' => 'veo-3.1-fast-generate-preview'
];
// Storage for pending operations
private $operationsFile;
public function __construct($apiKey, $modelType = 'standard') {
$this->apiKey = $apiKey;
$this->model = self::$models[$modelType] ?? self::$models['standard'];
$this->operationsFile = __DIR__ . '/video_operations.json';
}
/**
* Generate a video using Veo 3.1
* Returns an operation ID for async polling
*/
public function generateVideo($prompt, $duration = 4, $aspectRatio = '16:9', $resolution = '720p', $generateAudio = true, $referenceImages = [], $referenceMode = 'frame') {
// Build the instance object
$instance = [
'prompt' => $prompt
];
// Frame mode: first image is starting frame, optional second image is last frame for interpolation
if (!empty($referenceImages)) {
if (isset($referenceImages[0])) {
$refImg = $referenceImages[0];
$data = preg_replace('/\s+/', '', $refImg['data']);
if (preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $data)) {
$instance['image'] = [
'bytesBase64Encoded' => $data,
'mimeType' => $refImg['mime_type'] ?? 'image/jpeg'
];
error_log("Added first frame for I2V generation");
}
}
// Add last frame for interpolation (when 2 images provided)
if (count($referenceImages) >= 2 && isset($referenceImages[1])) {
$lastImg = $referenceImages[1];
$lastData = preg_replace('/\s+/', '', $lastImg['data']);
if (preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $lastData)) {
$instance['lastFrame'] = [
'bytesBase64Encoded' => $lastData,
'mimeType' => $lastImg['mime_type'] ?? 'image/jpeg'
];
error_log("Added last frame for video interpolation");
}
}
}
// Build parameters
$parameters = [
'aspectRatio' => $aspectRatio,
'sampleCount' => 1
];
// Duration: Veo 3.1 supports 4, 6, or 8 seconds
if (in_array(intval($duration), [4, 6, 8])) {
$parameters['durationSeconds'] = intval($duration);
} else {
$parameters['durationSeconds'] = 4;
}
// Note: generateAudio is handled automatically by Veo 3.1
// The model generates audio natively based on the scene
// No need to explicitly pass this parameter
$payload = [
'instances' => [$instance],
'parameters' => $parameters
];
// Log payload structure (without full base64 data for readability)
$logPayload = $payload;
if (isset($logPayload['instances'][0]['image'])) {
$logPayload['instances'][0]['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['image']['bytesBase64Encoded']) . '_bytes]';
}
if (isset($logPayload['instances'][0]['lastFrame'])) {
$logPayload['instances'][0]['lastFrame']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['lastFrame']['bytesBase64Encoded']) . '_bytes]';
}
if (isset($logPayload['instances'][0]['referenceImages'])) {
foreach ($logPayload['instances'][0]['referenceImages'] as $i => &$refImg) {
$refImg['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['referenceImages'][$i]['image']['bytesBase64Encoded']) . '_bytes]';
}
}
error_log("Video generation payload structure: " . json_encode($logPayload));
return $this->makeRequest($payload);
}
/**
* Make API request to Veo predictLongRunning endpoint
*/
private function makeRequest($payload, $retryCount = 0) {
// Veo uses predictLongRunning for async video generation
$url = "{$this->baseUrl}/{$this->model}:predictLongRunning";
$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 => 600 // 10 minute timeout for video 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);
error_log("Video API Response Code: $httpCode");
error_log("Video API Response: " . substr($response, 0, 1000));
if ($httpCode !== 200) {
$errorData = json_decode($response, true);
$errorMessage = $errorData['error']['message'] ?? "HTTP $httpCode";
$errorStatus = $errorData['error']['status'] ?? 'UNKNOWN';
error_log("Video API Error - HTTP $httpCode (Status: $errorStatus)");
error_log("Error message: " . $errorMessage);
// Handle specific error types
if ($httpCode === 500 && stripos($errorMessage, 'internal') !== false && $retryCount < 2) {
error_log("Retrying video request due to internal error (attempt " . ($retryCount + 1) . ")");
sleep(5); // Wait 5 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. Video generation is expensive (~\$0.75/second).");
}
if ($errorStatus === 'INVALID_ARGUMENT') {
throw new Exception("Invalid request format. Check your prompt and settings.");
}
if (stripos($errorMessage, 'model') !== false && stripos($errorMessage, 'not found') !== false) {
throw new Exception("Veo 3.1 model not available. You may need to enable it in your Google AI Studio account.");
}
throw new Exception("API error: $errorMessage (HTTP $httpCode, Status: $errorStatus)");
}
return json_decode($response, true);
}
/**
* Extract video data from API response
*/
public function extractVideoData($response) {
error_log("Extracting video data from response: " . json_encode(array_keys($response)));
// Check for completed operation with video response (from polling)
// IMPORTANT: Check this FIRST before the operation check, because completed
// operations also have a 'name' field but we want to extract the video data
if (isset($response['done']) && $response['done'] === true) {
// Check for error in operation
if (isset($response['error'])) {
$errorMsg = $response['error']['message'] ?? 'Unknown error';
throw new Exception("Video generation failed: $errorMsg");
}
// Extract video from generateVideoResponse format
// Structure: response.generateVideoResponse.generatedSamples[0].video.uri
$videoResponse = $response['response'] ?? $response;
error_log("videoResponse keys: " . json_encode(array_keys($videoResponse)));
$generateVideoResponse = $videoResponse['generateVideoResponse'] ?? null;
error_log("generateVideoResponse: " . ($generateVideoResponse ? 'found' : 'null'));
if ($generateVideoResponse && isset($generateVideoResponse['generatedSamples'])) {
$samples = $generateVideoResponse['generatedSamples'];
error_log("samples count: " . count($samples));
if (!empty($samples) && isset($samples[0]['video'])) {
$video = $samples[0]['video'];
error_log("video keys: " . json_encode(array_keys($video)));
// Check for video URI
if (isset($video['uri'])) {
error_log("Found video URI: " . $video['uri']);
return [
'url' => $video['uri'],
'mime_type' => 'video/mp4',
'type' => 'uri'
];
}
// Check for inline base64 data
if (isset($video['bytesBase64Encoded'])) {
return [
'base64' => $video['bytesBase64Encoded'],
'mime_type' => $video['mimeType'] ?? 'video/mp4',
'type' => 'inline'
];
}
}
}
}
// Check for long-running operation (initial response from predictLongRunning)
// This is for when the operation is NOT yet complete
if (isset($response['name']) && strpos($response['name'], 'operations/') !== false) {
// Only return operation type if not done
if (!isset($response['done']) || $response['done'] !== true) {
return [
'operationId' => $response['name'],
'type' => 'operation',
'done' => $response['done'] ?? false
];
}
}
// Legacy format: 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 === 'SAFETY') {
throw new Exception('Video generation blocked by safety filters. Please try a different prompt.');
}
if ($finishReason !== 'STOP' && !empty($finishMessage)) {
throw new Exception('Video generation failed: ' . $finishMessage);
}
}
// Legacy format: Look for video data in candidates
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'] ?? 'video/mp4',
'type' => 'inline'
];
}
if (isset($part['videoMetadata']['videoUri'])) {
return [
'url' => $part['videoMetadata']['videoUri'],
'mime_type' => 'video/mp4',
'type' => 'uri'
];
}
}
}
$errorDetails = "Response structure: " . json_encode(array_keys($response));
if (isset($response['response'])) {
$errorDetails .= " | Response keys: " . json_encode(array_keys($response['response']));
}
throw new Exception('No video data found in API response. ' . $errorDetails);
}
/**
* Check status of a long-running operation
*/
public function checkOperationStatus($operationId) {
$url = "https://generativelanguage.googleapis.com/v1beta/{$operationId}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'x-goog-api-key: ' . $this->apiKey
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
@curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("Failed to check operation status: HTTP $httpCode");
}
return json_decode($response, true);
}
/**
* Save video to disk
*/
public function saveVideo($base64Data, $mimeType = 'video/mp4') {
$videoDir = __DIR__ . '/generated_videos';
if (!is_dir($videoDir)) {
mkdir($videoDir, 0755, true);
}
$extension = $mimeType === 'video/webm' ? 'webm' : 'mp4';
$filename = 'video_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $extension;
$filepath = $videoDir . '/' . $filename;
$decoded = base64_decode($base64Data);
if ($decoded === false) {
throw new Exception('Failed to decode video data');
}
file_put_contents($filepath, $decoded);
return $filename;
}
}
// 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');
}
// 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');
}
// Get model type (standard or fast)
$modelType = $_POST['modelType'] ?? 'standard';
if (!in_array($modelType, ['standard', 'fast'])) {
$modelType = 'standard';
}
$api = new VeoVideoAPI(GEMINI_API_KEY, $modelType);
// Handle generate action
if ($action === 'generate') {
$prompt = $_POST['prompt'] ?? null;
$duration = intval($_POST['duration'] ?? 4);
$aspectRatio = $_POST['aspectRatio'] ?? '16:9';
$resolution = $_POST['resolution'] ?? '720p';
$generateAudio = ($_POST['generateAudio'] ?? 'true') === 'true';
$referenceMode = $_POST['referenceMode'] ?? 'frame';
// Validate reference mode
if (!in_array($referenceMode, ['frame', 'subject'])) {
$referenceMode = 'frame';
}
// Collect reference images (up to 3)
$referenceImages = [];
$refCount = intval($_POST['referenceImageCount'] ?? 0);
for ($i = 0; $i < min($refCount, 3); $i++) {
if (isset($_POST["referenceImage_$i"])) {
$referenceImages[] = [
'data' => $_POST["referenceImage_$i"],
'mime_type' => $_POST["referenceImageType_$i"] ?? 'image/jpeg'
];
}
}
if (!$prompt) {
throw new Exception('Prompt is required');
}
// Validate duration
if (!in_array($duration, [4, 6, 8])) {
$duration = 4;
}
// Validate aspect ratio
if (!in_array($aspectRatio, ['16:9', '9:16'])) {
$aspectRatio = '16:9';
}
error_log("Starting video generation: duration=$duration, aspect=$aspectRatio, audio=$generateAudio, refMode=$referenceMode, refCount=" . count($referenceImages));
// Generate video
$response = $api->generateVideo($prompt, $duration, $aspectRatio, $resolution, $generateAudio, $referenceImages, $referenceMode);
$videoData = $api->extractVideoData($response);
// Handle different response types
if ($videoData['type'] === 'operation') {
// Long-running operation - return operation ID for polling
echo json_encode([
'success' => true,
'status' => 'pending',
'operationId' => $videoData['operationId'],
'message' => 'Video generation started. Poll for status.'
]);
} else if ($videoData['type'] === 'inline') {
// Direct response with video data
$filename = $api->saveVideo($videoData['base64'], $videoData['mime_type']);
echo json_encode([
'success' => true,
'status' => 'complete',
'video' => [
'filename' => $filename,
'mime_type' => $videoData['mime_type'],
'url' => ($_ENV['API_BASE_PATH'] ?? '') . '/stream_video.php?file=' . urlencode($filename)
]
]);
} else if ($videoData['type'] === 'uri') {
// Video available at URI
echo json_encode([
'success' => true,
'status' => 'complete',
'video' => [
'url' => $videoData['url'],
'mime_type' => $videoData['mime_type']
]
]);
}
exit;
}
// Handle check status action
if ($action === 'check_status') {
$operationId = $_POST['operationId'] ?? null;
if (!$operationId) {
throw new Exception('Operation ID is required');
}
$status = $api->checkOperationStatus($operationId);
if (isset($status['done']) && $status['done'] === true) {
// Operation complete - extract video
// Pass the full status object so extractVideoData can find the video data
$videoData = $api->extractVideoData($status);
if ($videoData['type'] === 'inline') {
$filename = $api->saveVideo($videoData['base64'], $videoData['mime_type']);
echo json_encode([
'success' => true,
'status' => 'complete',
'video' => [
'filename' => $filename,
'mime_type' => $videoData['mime_type'],
'url' => ($_ENV['API_BASE_PATH'] ?? '') . '/stream_video.php?file=' . urlencode($filename)
]
]);
} else {
echo json_encode([
'success' => true,
'status' => 'complete',
'video' => [
'url' => $videoData['url'] ?? null,
'mime_type' => $videoData['mime_type']
]
]);
}
} else {
// Still processing
$progress = 0;
if (isset($status['metadata']['progress'])) {
$progress = floatval($status['metadata']['progress']) * 100;
}
echo json_encode([
'success' => true,
'status' => 'pending',
'progress' => $progress,
'message' => 'Video generation in progress...'
]);
}
exit;
}
// Handle download_video action - proxy the video download with authentication
if ($action === 'download_video') {
$videoUrl = $_POST['videoUrl'] ?? $_GET['videoUrl'] ?? null;
if (!$videoUrl) {
throw new Exception('Video URL is required');
}
// Validate URL is from Google APIs
if (strpos($videoUrl, 'generativelanguage.googleapis.com') === false) {
throw new Exception('Invalid video URL');
}
// Download the video with API key authentication
$ch = curl_init($videoUrl);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'x-goog-api-key: ' . GEMINI_API_KEY
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
CURLOPT_TIMEOUT => 120
]);
$videoData = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
@curl_close($ch);
if ($httpCode !== 200 || !$videoData) {
throw new Exception("Failed to download video: HTTP $httpCode");
}
// Save the video locally
$filename = 'video_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.mp4';
$videoDir = __DIR__ . '/generated_videos';
if (!is_dir($videoDir)) {
mkdir($videoDir, 0755, true);
}
$filepath = $videoDir . '/' . $filename;
file_put_contents($filepath, $videoData);
// Return local URL
echo json_encode([
'success' => true,
'video' => [
'url' => ($_ENV['API_BASE_PATH'] ?? '') . '/stream_video.php?file=' . urlencode($filename),
'filename' => $filename,
'mime_type' => 'video/mp4'
]
]);
exit;
}
throw new Exception('Invalid action');
} catch (Exception $e) {
http_response_code(500);
error_log("Exception in video_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;
}