From 5feab93829fe9ae10a2f7440fa9c792a87b635bf Mon Sep 17 00:00:00 2001 From: Dreaming Computers Date: Sat, 10 Jan 2026 16:38:28 +0700 Subject: [PATCH] adding kling 2.5 i2v --- README.md | 4 + kie_api/kling25_i2v.py | 153 ++++++++++++++++++++++++++++++++ nodes.py | 86 ++++++++++++++++++ web/docs/KIE_Kling25_I2V_Pro.md | 25 ++++++ 4 files changed, 268 insertions(+) create mode 100644 kie_api/kling25_i2v.py create mode 100644 web/docs/KIE_Kling25_I2V_Pro.md diff --git a/README.md b/README.md index c66a47d..8ecef7a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,10 @@ This node pack currently includes the following nodes: - Generates video from a single input image. - Uses the Kling 2.6 image-to-video model. +- **Kling 2.5 I2V Pro** + - Generates video from a required first image and optional tail image. + - Uses the Kling 2.5 Turbo image-to-video Pro model. + - **Kling 2.6 Text-to-Video** - Generates video directly from a text prompt. - Supports aspect ratio, duration, and sound options as exposed by the API. diff --git a/kie_api/kling25_i2v.py b/kie_api/kling25_i2v.py new file mode 100644 index 0000000..80005c0 --- /dev/null +++ b/kie_api/kling25_i2v.py @@ -0,0 +1,153 @@ +"""Kling 2.5 Turbo Image-to-Video Pro helper.""" + +import json +import time +from typing import Any, Callable + +import torch + +from .auth import _load_api_key +from .credits import _log_remaining_credits +from .http import TransientKieError, requests +from .jobs import _poll_task_until_complete +from .log import _log +from .results import _extract_result_urls +from .upload import _image_tensor_to_png_bytes, _truncate_url, _upload_image +from .validation import _validate_prompt +from .video import _download_video, _video_bytes_to_comfy_video + + +CREATE_TASK_URL = "https://api.kie.ai/api/v1/jobs/createTask" +MODEL_NAME = "kling/v2-5-turbo-image-to-video-pro" +PROMPT_MAX_LENGTH = 1000 +DURATION_OPTIONS = ["5", "10"] + + +def _validate_options(duration: str, cfg_scale: float) -> None: + if duration not in DURATION_OPTIONS: + raise RuntimeError("Invalid duration. Use the pinned enum options.") + if not isinstance(cfg_scale, (int, float)): + raise RuntimeError("cfg_scale must be a float value.") + if cfg_scale < 0.0 or cfg_scale > 1.0: + raise RuntimeError("cfg_scale must be between 0.0 and 1.0.") + + +def _validate_image_input(images: torch.Tensor | None, *, label: str, required: bool) -> torch.Tensor | None: + if images is None: + if required: + raise RuntimeError(f"{label} input is required.") + return None + if not isinstance(images, torch.Tensor): + raise RuntimeError(f"{label} input must be a tensor batch.") + if images.dim() != 4 or images.shape[-1] != 3: + raise RuntimeError(f"{label} input must have shape [B, H, W, 3].") + if images.shape[0] < 1: + raise RuntimeError(f"{label} input batch is empty.") + return images + + +def _create_kling_task(api_key: str, payload: dict[str, Any]) -> tuple[str, str]: + try: + response = requests.post( + CREATE_TASK_URL, + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + json=payload, + timeout=30, + ) + except requests.RequestException as exc: + raise RuntimeError(f"Failed to call createTask endpoint: {exc}") from exc + + if response.status_code == 429 or response.status_code >= 500: + raise TransientKieError( + f"createTask returned HTTP {response.status_code}: {response.text}", status_code=response.status_code + ) + + try: + payload_json: Any = response.json() + except json.JSONDecodeError as exc: + raise RuntimeError("createTask endpoint did not return valid JSON.") from exc + + if payload_json.get("code") != 200: + message = payload_json.get("message") or payload_json.get("msg") + raise RuntimeError(f"createTask endpoint returned error code {payload_json.get('code')}: {message}") + + task_id = (payload_json.get("data") or {}).get("taskId") + if not task_id: + raise RuntimeError("createTask endpoint did not return a taskId.") + + return task_id, response.text + + +def run_kling25_i2v_job( + image: torch.Tensor, + tail_image: torch.Tensor | None, + prompt: str, + negative_prompt: str = "", + duration: str = "5", + cfg_scale: float = 0.5, + timeout_seconds: int | None = None, + log: Callable[[str], None] | None = None, +) -> dict: + _validate_prompt(prompt, max_length=PROMPT_MAX_LENGTH) + _validate_options(duration, cfg_scale) + images = _validate_image_input(image, label="image", required=True) + tail_images = _validate_image_input(tail_image, label="tail_image", required=False) + + api_key = _load_api_key() + + if images.shape[0] > 1: + _log(log, f"More than 1 image provided ({images.shape[0]}); only the first will be used.") + if tail_images is not None and tail_images.shape[0] > 1: + _log(log, f"More than 1 tail image provided ({tail_images.shape[0]}); only the first will be used.") + + _log(log, "Uploading source image for Kling 2.5 I2V Pro...") + png_bytes = _image_tensor_to_png_bytes(images[0]) + image_url = _upload_image(api_key, png_bytes) + _log(log, f"Image upload success: {_truncate_url(image_url)}") + + tail_image_url = None + if tail_images is not None: + _log(log, "Uploading tail image for Kling 2.5 I2V Pro...") + tail_png_bytes = _image_tensor_to_png_bytes(tail_images[0]) + tail_image_url = _upload_image(api_key, tail_png_bytes) + _log(log, f"Tail image upload success: {_truncate_url(tail_image_url)}") + + payload_input: dict[str, Any] = { + "prompt": prompt, + "image_url": image_url, + "duration": duration, + "cfg_scale": cfg_scale, + } + if tail_image_url: + payload_input["tail_image_url"] = tail_image_url + if negative_prompt.strip(): + payload_input["negative_prompt"] = negative_prompt + + payload = {"model": MODEL_NAME, "input": payload_input} + + _log(log, "Creating Kling 2.5 I2V Pro task...") + start_time = time.time() + task_id, create_response_text = _create_kling_task(api_key, payload) + _log(log, f"createTask response (elapsed={time.time() - start_time:.1f}s): {create_response_text}") + _log(log, f"Task created with ID {task_id}. Polling for completion...") + _log(log, "Check https://kie.ai/logs for request status if needed.") + + effective_timeout = 1000 if timeout_seconds is None else timeout_seconds + record_data = _poll_task_until_complete( + api_key, + task_id, + 10.0, + effective_timeout, + log, + start_time, + ) + + result_urls = _extract_result_urls(record_data) + video_url = result_urls[0] + _log(log, f"Final video URL: {video_url}") + + video_bytes = _download_video(video_url) + video_output = _video_bytes_to_comfy_video(video_bytes) + + _log_remaining_credits(log, record_data, api_key, _log) + return video_output diff --git a/nodes.py b/nodes.py index ccc778e..59baced 100644 --- a/nodes.py +++ b/nodes.py @@ -27,6 +27,10 @@ from .kie_api.kling26_i2v import ( DURATION_OPTIONS as KLING26_DURATION_OPTIONS, run_kling26_i2v_video, ) +from .kie_api.kling25_i2v import ( + DURATION_OPTIONS as KLING25_DURATION_OPTIONS, + run_kling25_i2v_job, +) from .kie_api.kling26motion_i2v import ( CHARACTER_ORIENTATION_OPTIONS as KLING26MOTION_CHARACTER_ORIENTATION_OPTIONS, MODE_OPTIONS as KLING26MOTION_MODE_OPTIONS, @@ -250,6 +254,86 @@ Outputs: return (image_tensor,) +class KIE_Kling25_I2V_Pro: + HELP = """ +KIE Kling 2.5 I2V Pro + +Generate a short video clip from a prompt and one or two input images using Kling 2.5 Turbo I2V Pro. + +Inputs: +- prompt: Text prompt (required) +- first_frame: Source image batch (first image used) +- last_frame: Optional tail image batch (first image used) +- negative_prompt: Optional negative prompt +- duration: 5s or 10s +- cfg_scale: 0.0 to 1.0 +- poll_interval_s / timeout_s / log +- retry_on_fail / max_retries / retry_backoff_s + +Outputs: +- VIDEO: ComfyUI video output referencing a temporary .mp4 file +""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "first_frame": ("IMAGE",), + "prompt": ("STRING", {"multiline": True, "default": ""}), + }, + "optional": { + "last_frame": ("IMAGE",), + "negative_prompt": ("STRING", {"multiline": True, "default": ""}), + "duration": ("COMBO", {"options": KLING25_DURATION_OPTIONS, "default": "5"}), + "cfg_scale": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}), + "log": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("VIDEO",) + RETURN_NAMES = ("video",) + FUNCTION = "generate" + CATEGORY = "kie/api" + + def generate( + self, + first_frame: torch.Tensor, + prompt: str, + last_frame: torch.Tensor | None = None, + negative_prompt: str = "", + duration: str = "5", + cfg_scale: float = 0.5, + log: bool = True, + poll_interval_s: float = 10.0, + timeout_s: int = 1000, + retry_on_fail: bool = True, + max_retries: int = 2, + retry_backoff_s: float = 3.0, + ): + attempts = max_retries + 1 if retry_on_fail else 1 + attempts = max(attempts, 1) + backoff = retry_backoff_s if retry_backoff_s >= 0 else 0.0 + + for attempt in range(1, attempts + 1): + try: + video_output = run_kling25_i2v_job( + image=first_frame, + tail_image=last_frame, + prompt=prompt, + negative_prompt=negative_prompt, + duration=duration, + cfg_scale=cfg_scale, + timeout_seconds=timeout_s, + log=log, + ) + return (video_output,) + except TransientKieError: + if not retry_on_fail or attempt >= attempts: + raise + _log(log, f"Retrying (attempt {attempt + 1}/{attempts}) after {backoff}s") + time.sleep(backoff) + + class KIE_Kling26_I2V: HELP = """ KIE Kling 2.6 (Video) @@ -641,6 +725,7 @@ NODE_CLASS_MAPPINGS = { "KIE_Seedream45_Edit": KIE_Seedream45_Edit, "KIE_SeedanceV1Pro_Fast_I2V": KIE_SeedanceV1Pro_Fast_I2V, "KIE_Seedance15Pro_I2V": KIE_Seedance15Pro_I2V, + "KIE_Kling25_I2V_Pro": KIE_Kling25_I2V_Pro, "KIE_Kling26_I2V": KIE_Kling26_I2V, "KIE_Kling26_T2V": KIE_Kling26_T2V, "KIE_Kling26Motion_I2V": KIE_Kling26Motion_I2V, @@ -654,6 +739,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "KIE_Seedream45_Edit": "KIE Seedream 4.5 Edit", "KIE_SeedanceV1Pro_Fast_I2V": "KIE Seedance V1 Pro Fast (I2V)", "KIE_Seedance15Pro_I2V": "KIE Seedance 1.5 Pro (I2V/T2V)", + "KIE_Kling25_I2V_Pro": "KIE Kling 2.5 I2V Pro", "KIE_Kling26_I2V": "KIE Kling 2.6 (I2V)", "KIE_Kling26_T2V": "KIE Kling 2.6 (T2V)", "KIE_Kling26Motion_I2V": "KIE Kling 2.6 Motion-Control (I2V)", diff --git a/web/docs/KIE_Kling25_I2V_Pro.md b/web/docs/KIE_Kling25_I2V_Pro.md new file mode 100644 index 0000000..67b4563 --- /dev/null +++ b/web/docs/KIE_Kling25_I2V_Pro.md @@ -0,0 +1,25 @@ +# KIE Kling 2.5 I2V Pro + +## Overview +Generate a short video clip from a prompt and a required first image, with an optional tail image, using the Kling 2.5 Turbo I2V Pro model. + +## Inputs +- first_frame (IMAGE, required): Source image batch; the first image is used. +- prompt (STRING, multiline, required): The text prompt for the video. +- last_frame (IMAGE, optional): Optional tail frame; the first image is used if provided. +- negative_prompt (STRING, multiline, optional): Optional negative prompt. +- duration (COMBO, seconds, default: 5): Video length. Options: 5, 10. +- cfg_scale (FLOAT, default: 0.5): Guidance scale from 0.0 to 1.0. +- poll_interval_s (FLOAT, seconds, default: 10.0): How often to poll job status. +- timeout_s (INT, seconds, default: 1000): Max wait time before timing out. +- log (BOOLEAN, default: true): Enable KIE console logs. +- retry_on_fail (BOOLEAN, default: true): Retry on transient API failures. +- max_retries (INT, default: 2): Number of retry attempts. +- retry_backoff_s (FLOAT, seconds, default: 3.0): Delay between retries. + +## Outputs +- video (VIDEO): ComfyUI video output referencing a temporary .mp4 file. + +## Troubleshooting +- If a job stalls or fails, check https://kie.ai/logs. +- Timeouts: Increase timeout_s or use duration 5 seconds.