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-v3', 'kling-v3-omni', '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; }