@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
306 lines • 12.8 kB
JavaScript
/**
* Kling Video Handler (PiAPI)
*
* Image-to-video generation via PiAPI's Kling endpoint. Async job model:
* POST /image-to-video → poll /task/{id} until completed.
*
* NOTE: PiAPI Kling requires a publicly accessible image URL, not inline
* base64 data. Callers must supply `options.imageUrl` (a URL string) when
* using KlingVideoHandler. The `image` Buffer parameter is still accepted
* for interface compatibility (e.g., metadata / downstream use) but is not
* sent to the API. A clear error is thrown if no URL is provided.
*
* @module adapters/video/klingVideoHandler
* @see https://piapi.ai/docs/kling-api
*/
import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
import { VIDEO_ERROR_CODES } from "../../constants/videoErrors.js";
import { logger } from "../../utils/logger.js";
import { sanitizeForLog } from "../../utils/logSanitize.js";
import { safeDownload } from "../../utils/safeFetch.js";
import { VideoError } from "../../utils/videoProcessor.js";
import { MAX_VIDEO_BYTES } from "../../utils/sizeGuard.js";
const DEFAULT_BASE_URL = "https://api.piapi.ai/api/kling/v1";
const REQUEST_TIMEOUT_MS = 30_000;
const POLL_INTERVAL_MS = 5_000;
const TOTAL_TIMEOUT_MS = 5 * 60_000;
/**
* Kling Video Handler.
*
* Auth: `Authorization: Bearer ${KLING_API_KEY}` (PiAPI / Kling key).
* Models: kling-1.6-i2v (default), kling-1.5-i2v, kling-1.0.
*/
export class KlingVideoHandler {
maxDurationSeconds = 10;
supportedAspectRatios = ["16:9", "9:16", "1:1"];
supportedResolutions = [
"720p",
"1080p",
];
apiKey;
baseUrl;
constructor(apiKey) {
const resolved = (apiKey ?? process.env.KLING_API_KEY ?? "").trim();
this.apiKey = resolved.length > 0 ? resolved : null;
this.baseUrl = (process.env.KLING_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
}
isConfigured() {
return this.apiKey !== null;
}
async generate(image, prompt, options) {
if (!this.apiKey) {
throw new VideoError({
code: VIDEO_ERROR_CODES.PROVIDER_NOT_CONFIGURED,
message: "KLING_API_KEY not configured",
category: ErrorCategory.CONFIGURATION,
severity: ErrorSeverity.HIGH,
retriable: false,
});
}
if (!options.imageUrl) {
throw new VideoError({
code: VIDEO_ERROR_CODES.INVALID_INPUT,
message: "KlingVideoHandler requires a publicly accessible image URL. " +
"Pass options.imageUrl with a URL string pointing to the input image. " +
"The PiAPI Kling API does not accept inline base64 image data.",
category: ErrorCategory.CONFIGURATION,
severity: ErrorSeverity.HIGH,
retriable: false,
});
}
const startTime = Date.now();
const abortSignal = options.abortSignal;
// 1. Submit job.
const taskId = await this.submitJob(image, prompt, options);
// 2. Poll until complete.
const videoUrl = await this.pollUntilComplete(taskId, abortSignal);
// 3. Download video.
const buffer = await this.downloadVideo(videoUrl);
const processingTime = Date.now() - startTime;
logger.info(`[KlingVideoHandler] Generated ${buffer.length} bytes in ${processingTime}ms — task ${taskId}`);
return {
data: buffer,
mediaType: "video/mp4",
metadata: {
duration: options.length ?? 4,
dimensions: this.calculateDimensions(options),
model: options.model ?? "kling",
provider: "kling",
aspectRatio: options.aspectRatio ?? "16:9",
audioEnabled: false,
processingTime,
},
};
}
async submitJob(_image, prompt, options) {
// PiAPI Kling requires a publicly accessible URL for the input image.
// options.imageUrl is validated (non-null) before submitJob is called.
const body = {
model: options.model ?? "kling",
task_type: "video_generation",
input: {
image_url: options.imageUrl,
prompt,
duration: options.length ?? 4,
aspect_ratio: options.aspectRatio ?? "16:9",
cfg_scale: 0.5,
},
};
const response = await this.fetchWithTimeout(`${this.baseUrl}/image-to-video`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const raw = await response.text();
const retriable = response.status === 408 ||
response.status === 429 ||
response.status >= 500;
throw new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: `Kling submit failed: ${response.status} — ${sanitizeForLog(raw, 500)}`,
category: retriable ? ErrorCategory.NETWORK : ErrorCategory.EXECUTION,
severity: ErrorSeverity.HIGH,
retriable,
context: { status: response.status },
});
}
const json = (await response.json());
const taskId = json.task_id ?? json.data?.task_id;
if (!taskId) {
throw new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: "Kling submit response missing task_id",
category: ErrorCategory.EXECUTION,
severity: ErrorSeverity.HIGH,
retriable: false,
context: { response: json },
});
}
return taskId;
}
async pollUntilComplete(taskId, abortSignal) {
const startTime = Date.now();
while (Date.now() - startTime < TOTAL_TIMEOUT_MS) {
if (abortSignal?.aborted) {
throw new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: `Kling poll for task ${taskId} aborted by caller`,
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.MEDIUM,
retriable: false,
context: { taskId },
});
}
const response = await this.fetchWithTimeout(`${this.baseUrl}/task/${taskId}`, {
method: "GET",
headers: { Authorization: `Bearer ${this.apiKey}` },
}, abortSignal);
if (!response.ok) {
const raw = await response.text();
throw new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: `Kling poll failed: ${response.status} — ${sanitizeForLog(raw, 500)}`,
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.MEDIUM,
retriable: response.status >= 500,
context: { status: response.status, taskId },
});
}
const data = (await response.json());
if (data.status === "completed" || data.status === "succeeded") {
const videoUrl = data.video_url ?? data.output?.video_url;
if (!videoUrl) {
throw new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: `Kling task ${taskId} completed but no video URL returned`,
category: ErrorCategory.EXECUTION,
severity: ErrorSeverity.HIGH,
retriable: false,
context: { taskId, data },
});
}
return videoUrl;
}
if (data.status === "failed") {
throw new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: `Kling task ${taskId} failed: ${data.error ?? "unknown"}`,
category: ErrorCategory.EXECUTION,
severity: ErrorSeverity.HIGH,
retriable: false,
context: { taskId, data },
});
}
// Abortable sleep.
await new Promise((resolve, reject) => {
const onAbort = () => {
clearTimeout(timer);
reject(new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: `Kling poll for task ${taskId} aborted by caller`,
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.MEDIUM,
retriable: false,
context: { taskId },
}));
};
const timer = setTimeout(() => {
abortSignal?.removeEventListener("abort", onAbort);
resolve();
}, POLL_INTERVAL_MS);
abortSignal?.addEventListener("abort", onAbort, { once: true });
});
}
throw new VideoError({
code: VIDEO_ERROR_CODES.POLL_TIMEOUT,
message: `Kling task ${taskId} did not complete within ${TOTAL_TIMEOUT_MS / 1000}s`,
category: ErrorCategory.TIMEOUT,
severity: ErrorSeverity.MEDIUM,
retriable: true,
context: { taskId },
});
}
async downloadVideo(url) {
try {
return await safeDownload(url, {
maxBytes: MAX_VIDEO_BYTES,
label: "Kling video",
timeoutMs: REQUEST_TIMEOUT_MS,
});
}
catch (err) {
throw new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: `Kling video download rejected: ${err instanceof Error ? err.message : String(err)}`,
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.HIGH,
retriable: false,
context: { url },
originalError: err instanceof Error ? err : undefined,
});
}
}
async fetchWithTimeout(url, init, callerAbortSignal) {
const controller = new AbortController();
let timedOut = false;
const timeoutId = setTimeout(() => {
timedOut = true;
controller.abort();
}, REQUEST_TIMEOUT_MS);
const onCallerAbort = () => controller.abort();
callerAbortSignal?.addEventListener("abort", onCallerAbort, { once: true });
try {
return await fetch(url, { ...init, signal: controller.signal });
}
catch (err) {
if (err instanceof Error && err.name === "AbortError") {
if (timedOut) {
throw new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: `Kling request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`,
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.HIGH,
retriable: true,
originalError: err,
});
}
throw new VideoError({
code: VIDEO_ERROR_CODES.GENERATION_FAILED,
message: "Kling request aborted by caller",
category: ErrorCategory.NETWORK,
severity: ErrorSeverity.MEDIUM,
retriable: false,
originalError: err,
});
}
throw err;
}
finally {
callerAbortSignal?.removeEventListener("abort", onCallerAbort);
clearTimeout(timeoutId);
}
}
calculateDimensions(options) {
const aspectRatio = options.aspectRatio ?? "16:9";
const resolution = options.resolution ?? "720p";
if (resolution === "1080p") {
if (aspectRatio === "1:1") {
return { width: 1080, height: 1080 };
}
return aspectRatio === "9:16"
? { width: 1080, height: 1920 }
: { width: 1920, height: 1080 };
}
if (aspectRatio === "1:1") {
return { width: 720, height: 720 };
}
return aspectRatio === "9:16"
? { width: 720, height: 1280 }
: { width: 1280, height: 720 };
}
}
//# sourceMappingURL=klingVideoHandler.js.map