Compare commits
1 commit
main
...
feature/kl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9f35de5ef |
9 changed files with 1702 additions and 273 deletions
|
|
@ -19,6 +19,13 @@ BACKEND_PORT=5015
|
|||
# Get your API key from: https://aistudio.google.com/app/apikey
|
||||
GEMINI_API_KEY=your-api-key-here
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Kling AI API Credentials (https://platform.klingai.com)
|
||||
# ----------------------------------------------------------------------------
|
||||
# Get credentials from: https://platform.klingai.com
|
||||
KLING_ACCESS_KEY=your-kling-access-key-here
|
||||
KLING_SECRET_KEY=your-kling-secret-key-here
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Frontend URL for CORS (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -17,6 +17,13 @@ BACKEND_PORT=5015
|
|||
# Get your API key from: https://aistudio.google.com/app/apikey
|
||||
# GEMINI_API_KEY=AIzaSyCMKLSJJYEx4c6-TtBACnjdULrLzsr_fts
|
||||
GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Kling AI API Credentials (https://platform.klingai.com)
|
||||
# ----------------------------------------------------------------------------
|
||||
KLING_ACCESS_KEY=
|
||||
KLING_SECRET_KEY=
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Frontend URL for CORS (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@
|
|||
GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
|
||||
# AIzaSyCMKLSJJYEx4c6-TtBACnjdULrLzsr_fts
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Kling AI API Credentials (https://platform.klingai.com)
|
||||
# ----------------------------------------------------------------------------
|
||||
KLING_ACCESS_KEY=
|
||||
KLING_SECRET_KEY=
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Frontend URL for CORS (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -57,10 +57,17 @@ try {
|
|||
class NanoBananaProAPI {
|
||||
private $apiKey;
|
||||
private $baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
|
||||
private $model = 'gemini-3-pro-image-preview';
|
||||
private $model;
|
||||
|
||||
public function __construct($apiKey) {
|
||||
// Available models
|
||||
private static $models = [
|
||||
'pro' => 'gemini-3-pro-image-preview',
|
||||
'flash' => 'gemini-3.1-flash-image-preview'
|
||||
];
|
||||
|
||||
public function __construct($apiKey, $modelType = 'pro') {
|
||||
$this->apiKey = $apiKey;
|
||||
$this->model = self::$models[$modelType] ?? self::$models['pro'];
|
||||
}
|
||||
|
||||
public function generateImage($prompt, $aspectRatio = '16:9', $imageSize = '2K', $inputImage = null, $referenceImages = []) {
|
||||
|
|
@ -296,6 +303,12 @@ try {
|
|||
error_log("Received " . count($referenceImages) . " reference images from frontend");
|
||||
}
|
||||
|
||||
// Read and validate model type
|
||||
$modelType = $_POST['modelType'] ?? 'pro';
|
||||
if (!in_array($modelType, ['pro', 'flash'])) {
|
||||
$modelType = 'pro';
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
|
@ -307,7 +320,7 @@ try {
|
|||
}
|
||||
|
||||
// Initialize API
|
||||
$api = new NanoBananaProAPI(GEMINI_API_KEY);
|
||||
$api = new NanoBananaProAPI(GEMINI_API_KEY, $modelType);
|
||||
|
||||
// Determine input image for editing:
|
||||
// 1. Frontend sends uploadedImage when editing from library or a displayed image
|
||||
|
|
@ -347,7 +360,7 @@ try {
|
|||
'aspectRatio' => $aspectRatio,
|
||||
'imageSize' => $imageSize,
|
||||
'actionType' => $inputImage ? 'edit' : 'generate',
|
||||
'model' => 'Gemini 3 Pro Image'
|
||||
'model' => $modelType === 'flash' ? 'Nano Banana 2 (Flash)' : 'Nano Banana Pro'
|
||||
];
|
||||
|
||||
logImageGeneration($prompt, $imageData['base64'], $imageData['mime_type'], $webhookSettings, $userEmail, $inputImage ? 'edit' : 'generate');
|
||||
|
|
|
|||
628
backend/kling_api.php
Normal file
628
backend/kling_api.php
Normal file
|
|
@ -0,0 +1,628 @@
|
|||
<?php
|
||||
/**
|
||||
* Kling AI Video Generation API for Lux Studio
|
||||
* Handles Kling video generation: Text-to-Video, Image-to-Video, Video Extension, Lip Sync
|
||||
*/
|
||||
|
||||
// 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(600);
|
||||
ini_set('memory_limit', '512M');
|
||||
ini_set('post_max_size', '100M');
|
||||
ini_set('upload_max_filesize', '100M');
|
||||
|
||||
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 KlingAPI {
|
||||
private $accessKey;
|
||||
private $secretKey;
|
||||
private $baseUrl = 'https://api-singapore.klingai.com';
|
||||
|
||||
public function __construct($accessKey, $secretKey) {
|
||||
$this->accessKey = $accessKey;
|
||||
$this->secretKey = $secretKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HS256 JWT token for Kling API authentication
|
||||
* Pure PHP implementation - no external libraries required
|
||||
*/
|
||||
private function generateJWT() {
|
||||
$header = json_encode(['alg' => 'HS256', 'typ' => 'JWT']);
|
||||
$now = time();
|
||||
$payload = json_encode([
|
||||
'iss' => $this->accessKey,
|
||||
'exp' => $now + 1800, // 30 minute expiry
|
||||
'nbf' => $now - 5 // Valid from 5 seconds ago (clock skew tolerance)
|
||||
]);
|
||||
|
||||
$b64Header = $this->base64UrlEncode($header);
|
||||
$b64Payload = $this->base64UrlEncode($payload);
|
||||
|
||||
$signature = hash_hmac('sha256', "$b64Header.$b64Payload", $this->secretKey, true);
|
||||
$b64Signature = $this->base64UrlEncode($signature);
|
||||
|
||||
return "$b64Header.$b64Payload.$b64Signature";
|
||||
}
|
||||
|
||||
/**
|
||||
* URL-safe Base64 encoding
|
||||
*/
|
||||
private function base64UrlEncode($data) {
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an authenticated request to the Kling API
|
||||
*/
|
||||
private function makeRequest($method, $endpoint, $payload = null) {
|
||||
$url = $this->baseUrl . $endpoint;
|
||||
$jwt = $this->generateJWT();
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $jwt
|
||||
];
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_TIMEOUT => 120,
|
||||
CURLOPT_HTTPHEADER => $headers
|
||||
]);
|
||||
|
||||
if ($method === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
if ($payload !== null) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
}
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError) {
|
||||
throw new Exception("Kling API connection error: $curlError");
|
||||
}
|
||||
|
||||
$decoded = json_decode($response, true);
|
||||
|
||||
if ($httpCode === 401 || (isset($decoded['code']) && $decoded['code'] >= 1000 && $decoded['code'] <= 1004)) {
|
||||
throw new Exception('Kling API authentication failed. Check your KLING_ACCESS_KEY and KLING_SECRET_KEY in .env');
|
||||
}
|
||||
|
||||
if ($httpCode === 429 || (isset($decoded['code']) && $decoded['code'] >= 1302 && $decoded['code'] <= 1304)) {
|
||||
throw new Exception('Kling API rate limit or concurrent task limit exceeded. Please wait and try again.');
|
||||
}
|
||||
|
||||
if (isset($decoded['code']) && $decoded['code'] >= 1200 && $decoded['code'] <= 1201) {
|
||||
$detail = $decoded['message'] ?? 'Unknown parameter error';
|
||||
throw new Exception("Kling API invalid parameters: $detail");
|
||||
}
|
||||
|
||||
if ($httpCode >= 500) {
|
||||
throw new Exception('Kling server error. Please try again.');
|
||||
}
|
||||
|
||||
if ($httpCode !== 200 && $httpCode !== 201) {
|
||||
$msg = $decoded['message'] ?? "HTTP $httpCode";
|
||||
throw new Exception("Kling API error: $msg");
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text-to-Video generation
|
||||
*/
|
||||
public function textToVideo($prompt, $opts = []) {
|
||||
$payload = [
|
||||
'prompt' => $prompt,
|
||||
'model_name' => $opts['model_name'] ?? 'kling-v2-6',
|
||||
'mode' => $opts['mode'] ?? 'std',
|
||||
'duration' => $opts['duration'] ?? '5',
|
||||
'aspect_ratio' => $opts['aspect_ratio'] ?? '16:9',
|
||||
'cfg_scale' => floatval($opts['cfg_scale'] ?? 0.5)
|
||||
];
|
||||
|
||||
if (!empty($opts['negative_prompt'])) {
|
||||
$payload['negative_prompt'] = $opts['negative_prompt'];
|
||||
}
|
||||
|
||||
if (!empty($opts['sound']) && $opts['sound'] === 'on') {
|
||||
$payload['sound'] = 'on';
|
||||
}
|
||||
|
||||
// Camera control - only include if a preset is selected
|
||||
if (!empty($opts['camera_control']) && $opts['camera_control'] !== 'none') {
|
||||
$payload['camera_control'] = [
|
||||
'type' => 'predefined',
|
||||
'config' => $opts['camera_control']
|
||||
];
|
||||
}
|
||||
|
||||
error_log("Kling T2V request: model={$payload['model_name']}, mode={$payload['mode']}, duration={$payload['duration']}");
|
||||
|
||||
$response = $this->makeRequest('POST', '/v1/videos/text2video', $payload);
|
||||
|
||||
return $this->extractTaskInfo($response, 'text2video');
|
||||
}
|
||||
|
||||
/**
|
||||
* Image-to-Video generation
|
||||
*/
|
||||
public function imageToVideo($images, $prompt, $opts = []) {
|
||||
$payload = [
|
||||
'model_name' => $opts['model_name'] ?? 'kling-v2-6',
|
||||
'mode' => $opts['mode'] ?? 'std',
|
||||
'duration' => $opts['duration'] ?? '5',
|
||||
'aspect_ratio' => $opts['aspect_ratio'] ?? '16:9',
|
||||
'cfg_scale' => floatval($opts['cfg_scale'] ?? 0.5)
|
||||
];
|
||||
|
||||
if (!empty($prompt)) {
|
||||
$payload['prompt'] = $prompt;
|
||||
}
|
||||
|
||||
if (!empty($opts['negative_prompt'])) {
|
||||
$payload['negative_prompt'] = $opts['negative_prompt'];
|
||||
}
|
||||
|
||||
if (!empty($opts['sound']) && $opts['sound'] === 'on') {
|
||||
$payload['sound'] = 'on';
|
||||
}
|
||||
|
||||
// Camera control
|
||||
if (!empty($opts['camera_control']) && $opts['camera_control'] !== 'none') {
|
||||
$payload['camera_control'] = [
|
||||
'type' => 'predefined',
|
||||
'config' => $opts['camera_control']
|
||||
];
|
||||
}
|
||||
|
||||
// Start frame image (required)
|
||||
if (isset($images[0])) {
|
||||
$imageData = $this->cleanBase64($images[0]['data']);
|
||||
$payload['image'] = $imageData;
|
||||
}
|
||||
|
||||
// Optional end frame image
|
||||
if (isset($images[1])) {
|
||||
$tailData = $this->cleanBase64($images[1]['data']);
|
||||
$payload['image_tail'] = $tailData;
|
||||
}
|
||||
|
||||
error_log("Kling I2V request: model={$payload['model_name']}, images=" . count($images));
|
||||
|
||||
$response = $this->makeRequest('POST', '/v1/videos/image2video', $payload);
|
||||
|
||||
return $this->extractTaskInfo($response, 'image2video');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend an existing Kling video
|
||||
*/
|
||||
public function extendVideo($videoId, $opts = []) {
|
||||
$payload = [
|
||||
'video_id' => $videoId
|
||||
];
|
||||
|
||||
if (!empty($opts['prompt'])) {
|
||||
$payload['prompt'] = $opts['prompt'];
|
||||
}
|
||||
|
||||
if (!empty($opts['negative_prompt'])) {
|
||||
$payload['negative_prompt'] = $opts['negative_prompt'];
|
||||
}
|
||||
|
||||
if (isset($opts['cfg_scale'])) {
|
||||
$payload['cfg_scale'] = floatval($opts['cfg_scale']);
|
||||
}
|
||||
|
||||
error_log("Kling Extend request: videoId=$videoId");
|
||||
|
||||
$response = $this->makeRequest('POST', '/v1/videos/video-extend', $payload);
|
||||
|
||||
return $this->extractTaskInfo($response, 'video-extend');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lip Sync generation (image + audio → talking head video)
|
||||
*/
|
||||
public function lipSync($imageData, $audioData, $opts = []) {
|
||||
$payload = [
|
||||
'image' => $this->cleanBase64($imageData),
|
||||
'sound_file' => $this->cleanBase64($audioData)
|
||||
];
|
||||
|
||||
if (!empty($opts['prompt'])) {
|
||||
$payload['prompt'] = $opts['prompt'];
|
||||
}
|
||||
|
||||
if (!empty($opts['mode'])) {
|
||||
$payload['mode'] = $opts['mode'];
|
||||
}
|
||||
|
||||
error_log("Kling Lip Sync request: mode=" . ($opts['mode'] ?? 'std'));
|
||||
|
||||
$response = $this->makeRequest('POST', '/v1/videos/avatar', $payload);
|
||||
|
||||
return $this->extractTaskInfo($response, 'avatar');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the status of a Kling task
|
||||
*/
|
||||
public function checkStatus($taskId, $taskType) {
|
||||
// Map task types to their status endpoints
|
||||
$endpointMap = [
|
||||
'text2video' => '/v1/videos/text2video/',
|
||||
'image2video' => '/v1/videos/image2video/',
|
||||
'video-extend' => '/v1/videos/video-extend/',
|
||||
'avatar' => '/v1/videos/avatar/'
|
||||
];
|
||||
|
||||
$endpoint = ($endpointMap[$taskType] ?? '/v1/videos/text2video/') . $taskId;
|
||||
|
||||
$response = $this->makeRequest('GET', $endpoint);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a video from Kling CDN and save locally
|
||||
*/
|
||||
public function downloadAndSaveVideo($url) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_TIMEOUT => 120
|
||||
]);
|
||||
|
||||
$videoData = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError || $httpCode !== 200 || !$videoData) {
|
||||
throw new Exception("Failed to download video from Kling CDN: HTTP $httpCode");
|
||||
}
|
||||
|
||||
$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 $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract task ID and type from Kling API response
|
||||
*/
|
||||
private function extractTaskInfo($response, $taskType) {
|
||||
// Kling API returns: { "code": 0, "message": "...", "request_id": "...", "data": { "task_id": "...", "task_status": "submitted" } }
|
||||
if (isset($response['data']['task_id'])) {
|
||||
return [
|
||||
'taskId' => $response['data']['task_id'],
|
||||
'taskType' => $taskType,
|
||||
'status' => $response['data']['task_status'] ?? 'submitted'
|
||||
];
|
||||
}
|
||||
|
||||
// Also check for task_id at top level
|
||||
if (isset($response['task_id'])) {
|
||||
return [
|
||||
'taskId' => $response['task_id'],
|
||||
'taskType' => $taskType,
|
||||
'status' => $response['task_status'] ?? 'submitted'
|
||||
];
|
||||
}
|
||||
|
||||
throw new Exception('Kling API returned unexpected response: ' . json_encode($response));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean base64 data - remove data URI prefix and whitespace
|
||||
*/
|
||||
private function cleanBase64($data) {
|
||||
$data = preg_replace('/^data:[^;]+;base64,/', '', $data);
|
||||
$data = preg_replace('/\s+/', '', $data);
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Request Handler
|
||||
// =============================================================================
|
||||
|
||||
try {
|
||||
$action = $_POST['action'] ?? $_GET['action'] ?? null;
|
||||
|
||||
if (!$action) {
|
||||
throw new Exception('No action specified');
|
||||
}
|
||||
|
||||
// Validate Kling credentials
|
||||
if (!defined('KLING_ACCESS_KEY') || empty(KLING_ACCESS_KEY) || !defined('KLING_SECRET_KEY') || empty(KLING_SECRET_KEY)) {
|
||||
throw new Exception('Kling API credentials not configured. Please set KLING_ACCESS_KEY and KLING_SECRET_KEY in your .env file.');
|
||||
}
|
||||
|
||||
$api = new KlingAPI(KLING_ACCESS_KEY, KLING_SECRET_KEY);
|
||||
|
||||
// =========================================================================
|
||||
// Action: generate (Text-to-Video or Image-to-Video)
|
||||
// =========================================================================
|
||||
if ($action === 'generate') {
|
||||
$prompt = $_POST['prompt'] ?? '';
|
||||
$modelName = $_POST['modelName'] ?? 'kling-v2-6';
|
||||
$duration = $_POST['duration'] ?? '5';
|
||||
$aspectRatio = $_POST['aspectRatio'] ?? '16:9';
|
||||
$mode = $_POST['mode'] ?? 'std';
|
||||
$cfgScale = $_POST['cfgScale'] ?? '0.5';
|
||||
$sound = $_POST['sound'] ?? 'off';
|
||||
$negativePrompt = $_POST['negativePrompt'] ?? '';
|
||||
$cameraControl = $_POST['cameraControl'] ?? 'none';
|
||||
|
||||
// Validate model
|
||||
$validModels = ['kling-v2-6', 'kling-v2-5-turbo', 'kling-v2-1-master', 'kling-v1-6'];
|
||||
if (!in_array($modelName, $validModels)) {
|
||||
$modelName = 'kling-v2-6';
|
||||
}
|
||||
|
||||
// Validate duration
|
||||
if (!in_array($duration, ['5', '10'])) {
|
||||
$duration = '5';
|
||||
}
|
||||
|
||||
// Validate aspect ratio
|
||||
if (!in_array($aspectRatio, ['16:9', '9:16', '1:1'])) {
|
||||
$aspectRatio = '16:9';
|
||||
}
|
||||
|
||||
// Validate mode
|
||||
if (!in_array($mode, ['std', 'pro'])) {
|
||||
$mode = 'std';
|
||||
}
|
||||
|
||||
$opts = [
|
||||
'model_name' => $modelName,
|
||||
'mode' => $mode,
|
||||
'duration' => $duration,
|
||||
'aspect_ratio' => $aspectRatio,
|
||||
'cfg_scale' => $cfgScale,
|
||||
'sound' => $sound,
|
||||
'negative_prompt' => trim($negativePrompt),
|
||||
'camera_control' => $cameraControl
|
||||
];
|
||||
|
||||
// Collect reference images
|
||||
$referenceImages = [];
|
||||
$refCount = intval($_POST['referenceImageCount'] ?? 0);
|
||||
for ($i = 0; $i < min($refCount, 2); $i++) {
|
||||
if (isset($_POST["referenceImage_$i"])) {
|
||||
$referenceImages[] = [
|
||||
'data' => $_POST["referenceImage_$i"],
|
||||
'mime_type' => $_POST["referenceImageType_$i"] ?? 'image/jpeg'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Branch: T2V or I2V based on reference images
|
||||
if (!empty($referenceImages)) {
|
||||
if (empty($prompt)) {
|
||||
$prompt = ''; // Prompt is optional for I2V
|
||||
}
|
||||
$result = $api->imageToVideo($referenceImages, $prompt, $opts);
|
||||
} else {
|
||||
if (empty($prompt)) {
|
||||
throw new Exception('Prompt is required for text-to-video generation');
|
||||
}
|
||||
$result = $api->textToVideo($prompt, $opts);
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'pending',
|
||||
'taskId' => $result['taskId'],
|
||||
'taskType' => $result['taskType'],
|
||||
'message' => 'Kling video generation started. Polling for status.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Action: extend (Video Extension)
|
||||
// =========================================================================
|
||||
if ($action === 'extend') {
|
||||
$videoId = $_POST['videoId'] ?? null;
|
||||
$prompt = $_POST['prompt'] ?? '';
|
||||
$cfgScale = $_POST['cfgScale'] ?? '0.5';
|
||||
$negativePrompt = $_POST['negativePrompt'] ?? '';
|
||||
|
||||
if (!$videoId) {
|
||||
throw new Exception('Video ID is required for video extension');
|
||||
}
|
||||
|
||||
$opts = [
|
||||
'prompt' => trim($prompt),
|
||||
'negative_prompt' => trim($negativePrompt),
|
||||
'cfg_scale' => $cfgScale
|
||||
];
|
||||
|
||||
$result = $api->extendVideo($videoId, $opts);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'pending',
|
||||
'taskId' => $result['taskId'],
|
||||
'taskType' => 'video-extend',
|
||||
'message' => 'Video extension started. Polling for status.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Action: lipsync (Lip Sync Generation)
|
||||
// =========================================================================
|
||||
if ($action === 'lipsync') {
|
||||
$imageData = $_POST['image'] ?? null;
|
||||
$audioData = $_POST['audioFile'] ?? null;
|
||||
$prompt = $_POST['prompt'] ?? '';
|
||||
$mode = $_POST['mode'] ?? 'std';
|
||||
|
||||
if (!$imageData) {
|
||||
throw new Exception('Face image is required for lip sync');
|
||||
}
|
||||
|
||||
if (!$audioData) {
|
||||
throw new Exception('Audio file is required for lip sync');
|
||||
}
|
||||
|
||||
if (!in_array($mode, ['std', 'pro'])) {
|
||||
$mode = 'std';
|
||||
}
|
||||
|
||||
$opts = [
|
||||
'prompt' => trim($prompt),
|
||||
'mode' => $mode
|
||||
];
|
||||
|
||||
$result = $api->lipSync($imageData, $audioData, $opts);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'pending',
|
||||
'taskId' => $result['taskId'],
|
||||
'taskType' => 'avatar',
|
||||
'message' => 'Lip sync generation started. Polling for status.'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Action: check_status (Poll task status for any workflow)
|
||||
// =========================================================================
|
||||
if ($action === 'check_status') {
|
||||
$taskId = $_POST['taskId'] ?? null;
|
||||
$taskType = $_POST['taskType'] ?? 'text2video';
|
||||
|
||||
if (!$taskId) {
|
||||
throw new Exception('Task ID is required');
|
||||
}
|
||||
|
||||
$response = $api->checkStatus($taskId, $taskType);
|
||||
|
||||
// Extract status from response
|
||||
$taskStatus = $response['data']['task_status'] ?? 'processing';
|
||||
$taskStatusMsg = $response['data']['task_status_msg'] ?? '';
|
||||
|
||||
if ($taskStatus === 'succeed') {
|
||||
// Task completed - extract video URL
|
||||
$videos = $response['data']['task_result']['videos'] ?? [];
|
||||
|
||||
if (empty($videos)) {
|
||||
throw new Exception('Kling generation completed but no video was returned');
|
||||
}
|
||||
|
||||
$videoUrl = $videos[0]['url'] ?? null;
|
||||
$videoDuration = $videos[0]['duration'] ?? null;
|
||||
|
||||
if (!$videoUrl) {
|
||||
throw new Exception('Kling generation completed but video URL is missing');
|
||||
}
|
||||
|
||||
// Download from Kling CDN and save locally (CDN URLs are temporary)
|
||||
$filename = $api->downloadAndSaveVideo($videoUrl);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'complete',
|
||||
'video' => [
|
||||
'url' => '/generated_videos/' . $filename,
|
||||
'filename' => $filename,
|
||||
'mime_type' => 'video/mp4',
|
||||
'duration' => $videoDuration
|
||||
],
|
||||
'taskId' => $taskId
|
||||
]);
|
||||
} elseif ($taskStatus === 'failed') {
|
||||
throw new Exception('Kling video generation failed: ' . ($taskStatusMsg ?: 'Unknown error'));
|
||||
} else {
|
||||
// Still processing (submitted, processing)
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'pending',
|
||||
'taskStatus' => $taskStatus,
|
||||
'message' => 'Kling video generation in progress...'
|
||||
]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Action: download_video (Proxy download from Kling CDN)
|
||||
// =========================================================================
|
||||
if ($action === 'download_video') {
|
||||
$videoUrl = $_POST['videoUrl'] ?? null;
|
||||
|
||||
if (!$videoUrl) {
|
||||
throw new Exception('Video URL is required');
|
||||
}
|
||||
|
||||
$filename = $api->downloadAndSaveVideo($videoUrl);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'video' => [
|
||||
'url' => '/generated_videos/' . $filename,
|
||||
'filename' => $filename,
|
||||
'mime_type' => 'video/mp4'
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
throw new Exception('Invalid action: ' . $action);
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
|
||||
error_log("Exception in kling_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;
|
||||
}
|
||||
|
|
@ -10,6 +10,12 @@ ini_set('display_errors', 0);
|
|||
ini_set('log_errors', 1);
|
||||
set_time_limit(300); // Veo 3.1 operations poll for up to 5 minutes
|
||||
|
||||
// Increase execution time and memory/POST limits for video generation with large images
|
||||
set_time_limit(600);
|
||||
ini_set('memory_limit', '512M');
|
||||
ini_set('post_max_size', '100M');
|
||||
ini_set('upload_max_filesize', '100M');
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
||||
|
|
@ -60,6 +66,14 @@ class VeoVideoAPI {
|
|||
'sampleCount' => 1
|
||||
];
|
||||
|
||||
// Person generation: T2V supports 'allow_all', but I2V only supports 'allow_adult'
|
||||
// Note: 'allow_adult' means children will still be blocked in I2V mode (Google restriction)
|
||||
if (!empty($referenceImages)) {
|
||||
$parameters['personGeneration'] = 'allow_adult';
|
||||
} else {
|
||||
$parameters['personGeneration'] = 'allow_all';
|
||||
}
|
||||
|
||||
// Frame mode: first image is starting frame, optional second image is last frame for interpolation
|
||||
if (!empty($referenceImages)) {
|
||||
if (isset($referenceImages[0])) {
|
||||
|
|
|
|||
|
|
@ -725,12 +725,19 @@ const CinePromptStudio = ({ activeProjectId, editData, onEditLoaded }) => {
|
|||
const [imageError, setImageError] = useState('');
|
||||
const [referenceImages, setReferenceImages] = useState([]);
|
||||
const [imageResolution, setImageResolution] = useState('2K');
|
||||
const [imageModelType, setImageModelType] = useState('pro');
|
||||
const [imagePreviewExpanded, setImagePreviewExpanded] = useState(false);
|
||||
|
||||
// Project images state (for "Add from Library" feature)
|
||||
const [projectImages, setProjectImages] = useState([]);
|
||||
const [showProjectPicker, setShowProjectPicker] = useState(false);
|
||||
|
||||
// Image Model Options
|
||||
const imageModelOptions = [
|
||||
{ value: 'pro', label: 'Pro', description: 'Higher quality' },
|
||||
{ value: 'flash', label: 'Flash', description: 'Faster, lower cost' }
|
||||
];
|
||||
|
||||
// Effect: Auto-select camera and lens based on application
|
||||
useEffect(() => {
|
||||
const selectedApp = allApplicationData.find(app => app.value === application);
|
||||
|
|
@ -1158,6 +1165,7 @@ Return ONLY the final prompt text. No markdown, no preamble, no "Here is your pr
|
|||
formData.append('prompt', finalPrompt);
|
||||
formData.append('aspectRatio', aspectRatio);
|
||||
formData.append('imageSize', imageResolution);
|
||||
formData.append('modelType', imageModelType);
|
||||
|
||||
// If we have a generatedImage (from library edit or previous generation),
|
||||
// send it to the backend so it uses the correct image for editing
|
||||
|
|
@ -1197,7 +1205,7 @@ Return ONLY the final prompt text. No markdown, no preamble, no "Here is your pr
|
|||
await addItemToProject(activeProjectId, {
|
||||
type: 'image',
|
||||
prompt: generatedPrompt,
|
||||
settings: { camera, lens, application, aspectRatio, imageResolution },
|
||||
settings: { camera, lens, application, aspectRatio, imageResolution, imageModelType },
|
||||
thumbnail: null,
|
||||
data: imageData.data,
|
||||
mimeType: imageData.mime_type
|
||||
|
|
@ -1840,6 +1848,27 @@ Return ONLY the final prompt text. No markdown, no preamble, no "Here is your pr
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{/* Model Selector */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-slate-400 uppercase tracking-wider">Model</label>
|
||||
<div className="flex gap-2">
|
||||
{imageModelOptions.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setImageModelType(opt.value)}
|
||||
className={`flex-1 px-3 py-2 rounded-lg font-medium transition-all text-sm ${
|
||||
imageModelType === opt.value
|
||||
? 'bg-cinema-gold text-slate-950'
|
||||
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
<div>{opt.label}</div>
|
||||
<div className="text-xs opacity-70">{opt.description}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resolution Selector */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold text-slate-400 uppercase tracking-wider">Output Resolution</label>
|
||||
|
|
|
|||
|
|
@ -1164,9 +1164,11 @@ const ProjectsTab = ({ onProjectSelect, activeProjectId, onRerunVideo, onEditInI
|
|||
<div className={`absolute top-2 left-2 px-2 py-1 rounded text-xs font-medium ${
|
||||
item.type === 'image'
|
||||
? 'bg-blue-900/80 text-blue-300'
|
||||
: item.settings?.engine === 'kling'
|
||||
? 'bg-indigo-900/80 text-indigo-300'
|
||||
: 'bg-purple-900/80 text-purple-300'
|
||||
}`}>
|
||||
{item.type === 'image' ? 'IMG' : 'VID'}
|
||||
{item.type === 'image' ? 'IMG' : item.settings?.engine === 'kling' ? 'KLING' : 'VID'}
|
||||
</div>
|
||||
|
||||
{/* Hover Overlay (hide when selecting) */}
|
||||
|
|
@ -1492,6 +1494,24 @@ const ProjectsTab = ({ onProjectSelect, activeProjectId, onRerunVideo, onEditInI
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
{previewItem.type === 'video' && (
|
||||
<div className="mt-2 flex gap-1.5">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
||||
previewItem.settings?.engine === 'kling'
|
||||
? 'bg-indigo-900/50 text-indigo-300 border border-indigo-700'
|
||||
: 'bg-emerald-900/50 text-emerald-300 border border-emerald-700'
|
||||
}`}>
|
||||
{previewItem.settings?.engine === 'kling'
|
||||
? `Kling ${previewItem.settings?.klingModel?.replace('kling-', '').toUpperCase() || 'AI'}`
|
||||
: `Veo ${previewItem.settings?.modelType === 'fast' ? 'Fast' : 'Std'}`}
|
||||
</span>
|
||||
{previewItem.settings?.engine === 'kling' && previewItem.settings?.klingTaskId && (
|
||||
<span className="text-xs px-2 py-0.5 bg-slate-800 rounded-full text-slate-500 border border-slate-700 font-mono">
|
||||
ID: {previewItem.settings.klingTaskId.substring(0, 8)}...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<span className="text-xs text-slate-500">{formatDate(previewItem.createdAt)}</span>
|
||||
<div className="flex gap-2">
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue