'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 => true, 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 => true, 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' => '/api/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' => '/api/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 => true, 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' => '/api/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; }