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>
763 lines
31 KiB
PHP
763 lines
31 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);
|
|
set_time_limit(300); // Veo 3.1 operations poll for up to 5 minutes
|
|
|
|
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', $negativePrompt = '') {
|
|
// Build the instance object
|
|
$instance = [
|
|
'prompt' => $prompt
|
|
];
|
|
|
|
// Build parameters first
|
|
$parameters = [
|
|
'aspectRatio' => $aspectRatio,
|
|
'sampleCount' => 1
|
|
];
|
|
|
|
// 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)) {
|
|
// Use bytesBase64Encoded format (original working format)
|
|
$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 AND duration is 8 seconds)
|
|
// CRITICAL: lastFrame goes in instance[], NOT parameters{} per Vertex AI docs
|
|
// Veo 3.1: lastFrame works with both standard and fast models, but requires 8s duration
|
|
if (count($referenceImages) >= 2 && isset($referenceImages[1]) && intval($duration) === 8) {
|
|
$lastImg = $referenceImages[1];
|
|
$lastData = preg_replace('/\s+/', '', $lastImg['data']);
|
|
|
|
if (preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $lastData)) {
|
|
// Use bytesBase64Encoded format, placed in instance (same level as image)
|
|
$instance['lastFrame'] = [
|
|
'bytesBase64Encoded' => $lastData,
|
|
'mimeType' => $lastImg['mime_type'] ?? 'image/jpeg'
|
|
];
|
|
error_log("Added lastFrame to instance for video interpolation (8s duration)");
|
|
}
|
|
} elseif (count($referenceImages) >= 2 && intval($duration) !== 8) {
|
|
error_log("WARNING: Second reference image provided but duration is not 8s - ignoring lastFrame (API requires 8s for interpolation)");
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Resolution: 720p (default), 1080p (8s only), 4k (8s only)
|
|
// Per API docs: higher resolutions only available for 8-second videos
|
|
$effectiveResolution = $resolution;
|
|
|
|
// Normalize 4K to lowercase for API
|
|
if ($resolution === '4K') {
|
|
$effectiveResolution = '4k';
|
|
}
|
|
|
|
// Check duration requirement for high-res
|
|
if (($effectiveResolution === '1080p' || $effectiveResolution === '4k') && intval($duration) !== 8) {
|
|
error_log("WARNING: $effectiveResolution resolution requires 8s duration - falling back to 720p");
|
|
$effectiveResolution = '720p';
|
|
}
|
|
$parameters['resolution'] = $effectiveResolution;
|
|
error_log("Video resolution set to: $effectiveResolution (requested: $resolution)");
|
|
|
|
// Add negative prompt if provided
|
|
if (!empty($negativePrompt)) {
|
|
$parameters['negativePrompt'] = $negativePrompt;
|
|
error_log("Negative prompt: " . substr($negativePrompt, 0, 200));
|
|
}
|
|
|
|
// 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']['bytesBase64Encoded'])) {
|
|
$logPayload['instances'][0]['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['image']['bytesBase64Encoded']) . '_bytes]';
|
|
}
|
|
if (isset($logPayload['instances'][0]['lastFrame']['bytesBase64Encoded'])) {
|
|
$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) {
|
|
if (isset($refImg['image']['bytesBase64Encoded'])) {
|
|
$refImg['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['referenceImages'][$i]['image']['bytesBase64Encoded']) . '_bytes]';
|
|
}
|
|
}
|
|
}
|
|
error_log("Video generation request - Model: " . $this->model);
|
|
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') {
|
|
// Check if error is about lastFrame feature
|
|
if (stripos($errorMessage, 'lastFrame') !== false) {
|
|
throw new Exception("The lastFrame feature may not be available for your API key yet. Google is still rolling out Veo 3.1 features. Try using single image-to-video instead, or check Google AI Studio for feature availability. Error: " . $errorMessage);
|
|
}
|
|
// Include the actual error message from Google API for debugging
|
|
throw new Exception("Invalid request format: " . $errorMessage);
|
|
}
|
|
|
|
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 response
|
|
// Two possible formats:
|
|
// 1. Gemini API: response.generateVideoResponse.generatedSamples[0].video.uri
|
|
// 2. Vertex AI: response.videos[0].gcsUri or response.videos[0].bytesBase64Encoded
|
|
$videoResponse = $response['response'] ?? $response;
|
|
error_log("videoResponse keys: " . json_encode(array_keys($videoResponse)));
|
|
|
|
// Check for Vertex AI format first (videos array directly in response)
|
|
if (isset($videoResponse['videos'])) {
|
|
$videos = $videoResponse['videos'];
|
|
error_log("Found videos array (Vertex AI format) with " . count($videos) . " items");
|
|
|
|
// Check for raiMediaFilteredCount
|
|
if (isset($videoResponse['raiMediaFilteredCount']) && intval($videoResponse['raiMediaFilteredCount']) > 0) {
|
|
$filteredCount = intval($videoResponse['raiMediaFilteredCount']);
|
|
$reasons = isset($videoResponse['raiMediaFilteredReasons'])
|
|
? implode(', ', $videoResponse['raiMediaFilteredReasons'])
|
|
: 'content policy violation';
|
|
error_log("Videos filtered: $filteredCount, reasons: $reasons");
|
|
if (empty($videos)) {
|
|
throw new Exception("Video generation blocked by content safety filters ($reasons). All $filteredCount video(s) were filtered. Try a different prompt.");
|
|
}
|
|
}
|
|
|
|
if (!empty($videos)) {
|
|
$video = $videos[0];
|
|
error_log("First video keys: " . json_encode(array_keys($video)));
|
|
|
|
// Check for gcsUri (Cloud Storage)
|
|
if (isset($video['gcsUri'])) {
|
|
error_log("Found video gcsUri: " . $video['gcsUri']);
|
|
return [
|
|
'url' => $video['gcsUri'],
|
|
'mime_type' => $video['mimeType'] ?? 'video/mp4',
|
|
'type' => 'uri'
|
|
];
|
|
}
|
|
|
|
// Check for uri (standard)
|
|
if (isset($video['uri'])) {
|
|
error_log("Found video uri: " . $video['uri']);
|
|
return [
|
|
'url' => $video['uri'],
|
|
'mime_type' => $video['mimeType'] ?? 'video/mp4',
|
|
'type' => 'uri'
|
|
];
|
|
}
|
|
|
|
// Check for bytesBase64Encoded
|
|
if (isset($video['bytesBase64Encoded'])) {
|
|
error_log("Found bytesBase64Encoded in video");
|
|
return [
|
|
'base64' => $video['bytesBase64Encoded'],
|
|
'mime_type' => $video['mimeType'] ?? 'video/mp4',
|
|
'type' => 'inline'
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
$generateVideoResponse = $videoResponse['generateVideoResponse'] ?? null;
|
|
error_log("generateVideoResponse: " . ($generateVideoResponse ? json_encode(array_keys($generateVideoResponse)) : 'null'));
|
|
|
|
if ($generateVideoResponse) {
|
|
// Log full structure for debugging
|
|
error_log("Full generateVideoResponse structure: " . json_encode($generateVideoResponse));
|
|
|
|
// Check for content filter - raiMediaFilteredCount > 0 means all videos were filtered
|
|
if (isset($generateVideoResponse['raiMediaFilteredCount'])) {
|
|
$filteredCount = intval($generateVideoResponse['raiMediaFilteredCount']);
|
|
error_log("raiMediaFilteredCount: $filteredCount");
|
|
if ($filteredCount > 0 && !isset($generateVideoResponse['generatedSamples'])) {
|
|
throw new Exception("Video generation blocked by content safety filters. All $filteredCount video(s) were filtered. Try a different prompt.");
|
|
}
|
|
}
|
|
|
|
// Check for empty generateVideoResponse (only has @type, no actual video data)
|
|
// This happens when generation silently fails
|
|
$actualKeys = array_filter(array_keys($generateVideoResponse), function($k) {
|
|
return $k !== '@type';
|
|
});
|
|
if (empty($actualKeys)) {
|
|
error_log("generateVideoResponse is empty (only contains @type)");
|
|
throw new Exception("Video generation failed silently. The API returned an empty response. This may be due to content filtering or a temporary issue. Try a different prompt or try again.");
|
|
}
|
|
|
|
// Check for generatedSamples (standard format)
|
|
if (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'
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Alternative format: videos array directly in generateVideoResponse
|
|
if (isset($generateVideoResponse['videos'])) {
|
|
$videos = $generateVideoResponse['videos'];
|
|
error_log("Found videos array with " . count($videos) . " items");
|
|
|
|
if (!empty($videos) && isset($videos[0]['uri'])) {
|
|
error_log("Found video URI in videos array: " . $videos[0]['uri']);
|
|
return [
|
|
'url' => $videos[0]['uri'],
|
|
'mime_type' => 'video/mp4',
|
|
'type' => 'uri'
|
|
];
|
|
}
|
|
|
|
if (!empty($videos) && isset($videos[0]['video']['uri'])) {
|
|
error_log("Found video URI in videos[0].video: " . $videos[0]['video']['uri']);
|
|
return [
|
|
'url' => $videos[0]['video']['uri'],
|
|
'mime_type' => 'video/mp4',
|
|
'type' => 'uri'
|
|
];
|
|
}
|
|
|
|
// Check for base64 encoded video in videos array
|
|
if (!empty($videos) && isset($videos[0]['bytesBase64Encoded'])) {
|
|
error_log("Found base64 video in videos array");
|
|
return [
|
|
'base64' => $videos[0]['bytesBase64Encoded'],
|
|
'mime_type' => $videos[0]['mimeType'] ?? 'video/mp4',
|
|
'type' => 'inline'
|
|
];
|
|
}
|
|
|
|
// Check for video object with base64 in videos array
|
|
if (!empty($videos) && isset($videos[0]['video']['bytesBase64Encoded'])) {
|
|
error_log("Found base64 video in videos[0].video");
|
|
return [
|
|
'base64' => $videos[0]['video']['bytesBase64Encoded'],
|
|
'mime_type' => $videos[0]['video']['mimeType'] ?? 'video/mp4',
|
|
'type' => 'inline'
|
|
];
|
|
}
|
|
}
|
|
|
|
// Check for raiMediaFilteredReasons (content blocked)
|
|
if (isset($generateVideoResponse['raiMediaFilteredReasons'])) {
|
|
$reasons = implode(', ', $generateVideoResponse['raiMediaFilteredReasons']);
|
|
throw new Exception("Video generation blocked by content filter: $reasons");
|
|
}
|
|
|
|
// If generateVideoResponse exists but no videos found, log all its keys
|
|
error_log("generateVideoResponse found but no video data. All keys: " . json_encode(array_keys($generateVideoResponse)));
|
|
}
|
|
}
|
|
|
|
// 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';
|
|
$negativePrompt = $_POST['negativePrompt'] ?? '';
|
|
|
|
// 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, $negativePrompt);
|
|
$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' => '/generated_videos/' . $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' => '/generated_videos/' . $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 URL in /generated_videos/ format for frontend to handle
|
|
echo json_encode([
|
|
'success' => true,
|
|
'video' => [
|
|
'url' => '/generated_videos/' . $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;
|
|
}
|