[Partner Nodes] chore(Kling): remove retired legacy models and Virtual Try-On API (#15249)

Signed-off-by: bigcat88 <bigcat88@icloud.com>
This commit is contained in:
Alexander Piskun
2026-08-04 19:24:21 +03:00
committed by Jedrzej Kosinski
parent 0764232429
commit 13e16c8d62

View File

@@ -5,17 +5,13 @@ For source of truth on the allowed permutations of request fields, please refere
"""
import logging
import math
import re
import torch
from typing_extensions import override
from comfy_api.latest import IO, ComfyExtension, Input, InputImpl
from comfy_api.latest import IO, ComfyExtension, Input
from comfy_api_nodes.apis import (
KlingCameraControl,
KlingCameraConfig,
KlingCameraControlType,
KlingVideoGenDuration,
KlingVideoGenMode,
KlingVideoGenAspectRatio,
@@ -30,23 +26,12 @@ from comfy_api_nodes.apis import (
KlingLipSyncInputObject,
KlingLipSyncRequest,
KlingLipSyncResponse,
KlingVirtualTryOnModelName,
KlingVirtualTryOnRequest,
KlingVirtualTryOnResponse,
KlingVideoResult,
KlingImageResult,
KlingImageGenerationsRequest,
KlingImageGenerationsResponse,
KlingImageGenImageReferenceType,
KlingImageGenAspectRatio,
KlingVideoEffectsRequest,
KlingVideoEffectsResponse,
KlingDualCharacterEffectsScene,
KlingSingleImageEffectsScene,
KlingDualCharacterEffectInput,
KlingSingleImageEffectInput,
KlingCharacterEffectModelName,
KlingSingleImageEffectModelName,
)
from comfy_api_nodes.apis.kling import (
ImageToVideoWithAudioRequest,
@@ -119,9 +104,6 @@ PATH_TEXT_TO_VIDEO = f"/proxy/kling/{KLING_API_VERSION}/videos/text2video"
PATH_IMAGE_TO_VIDEO = f"/proxy/kling/{KLING_API_VERSION}/videos/image2video"
PATH_VIDEO_EXTEND = f"/proxy/kling/{KLING_API_VERSION}/videos/video-extend"
PATH_LIP_SYNC = f"/proxy/kling/{KLING_API_VERSION}/videos/lip-sync"
PATH_VIDEO_EFFECTS = f"/proxy/kling/{KLING_API_VERSION}/videos/effects"
PATH_CHARACTER_IMAGE = f"/proxy/kling/{KLING_API_VERSION}/images/generations"
PATH_VIRTUAL_TRY_ON = f"/proxy/kling/{KLING_API_VERSION}/images/kolors-virtual-try-on"
PATH_IMAGE_GENERATIONS = f"/proxy/kling/{KLING_API_VERSION}/images/generations"
MAX_PROMPT_LENGTH_T2V = 2500
@@ -133,21 +115,11 @@ MAX_PROMPT_LENGTH_LIP_SYNC = 120
AVERAGE_DURATION_T2V = 319
AVERAGE_DURATION_I2V = 164
AVERAGE_DURATION_LIP_SYNC = 455
AVERAGE_DURATION_VIRTUAL_TRY_ON = 19
AVERAGE_DURATION_IMAGE_GEN = 32
AVERAGE_DURATION_VIDEO_EFFECTS = 320
AVERAGE_DURATION_VIDEO_EXTEND = 320
MODE_TEXT2VIDEO = {
"standard mode / 5s duration / kling-v1-6": ("std", "5", "kling-v1-6"),
"standard mode / 10s duration / kling-v1-6": ("std", "10", "kling-v1-6"),
"pro mode / 5s duration / kling-v2-master": ("pro", "5", "kling-v2-master"),
"pro mode / 10s duration / kling-v2-master": ("pro", "10", "kling-v2-master"),
"standard mode / 5s duration / kling-v2-master": ("std", "5", "kling-v2-master"),
"standard mode / 10s duration / kling-v2-master": ("std", "10", "kling-v2-master"),
"pro mode / 5s duration / kling-v2-1-master": ("pro", "5", "kling-v2-1-master"),
"pro mode / 10s duration / kling-v2-1-master": ("pro", "10", "kling-v2-1-master"),
"pro mode / 5s duration / kling-v2-5-turbo": ("pro", "5", "kling-v2-5-turbo"),
"pro mode / 10s duration / kling-v2-5-turbo": ("pro", "10", "kling-v2-5-turbo"),
}
@@ -160,12 +132,6 @@ See: [Kling API Docs Capability Map](https://app.klingai.com/global/dev/document
MODE_START_END_FRAME = {
"pro mode / 5s duration / kling-v1-5": ("pro", "5", "kling-v1-5"),
"pro mode / 10s duration / kling-v1-5": ("pro", "10", "kling-v1-5"),
"pro mode / 5s duration / kling-v1-6": ("pro", "5", "kling-v1-6"),
"pro mode / 10s duration / kling-v1-6": ("pro", "10", "kling-v1-6"),
"pro mode / 5s duration / kling-v2-1": ("pro", "5", "kling-v2-1"),
"pro mode / 10s duration / kling-v2-1": ("pro", "10", "kling-v2-1"),
"pro mode / 5s duration / kling-v2-5-turbo": ("pro", "5", "kling-v2-5-turbo"),
"pro mode / 10s duration / kling-v2-5-turbo": ("pro", "10", "kling-v2-5-turbo"),
}
@@ -287,11 +253,6 @@ async def finish_omni_video_task(cls: type[IO.ComfyNode], response: TaskStatusRe
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
def is_valid_camera_control_configs(configs: list[float]) -> bool:
"""Verifies that at least one camera control configuration is non-zero."""
return any(not math.isclose(value, 0.0) for value in configs)
def is_valid_task_creation_response(response: KlingText2VideoResponse) -> bool:
"""Verifies that the initial response contains a task ID."""
return bool(response.data.task_id)
@@ -307,7 +268,7 @@ def is_valid_video_response(response: KlingText2VideoResponse) -> bool:
)
def is_valid_image_response(response: KlingVirtualTryOnResponse) -> bool:
def is_valid_image_response(response: KlingImageGenerationsResponse) -> bool:
"""Verifies that the response contains a task result with at least one image."""
return (
response.data is not None
@@ -430,7 +391,6 @@ async def execute_text2video(
model_mode: str,
duration: str,
aspect_ratio: str,
camera_control: KlingCameraControl | None = None,
) -> IO.NodeOutput:
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_T2V)
task_creation_response = await sync_op(
@@ -445,7 +405,6 @@ async def execute_text2video(
model_name=model_name,
cfg_scale=cfg_scale,
aspect_ratio=KlingVideoGenAspectRatio(aspect_ratio),
camera_control=camera_control,
),
)
@@ -475,19 +434,11 @@ async def execute_image2video(
model_mode: str,
aspect_ratio: str,
duration: str,
camera_control: KlingCameraControl | None = None,
end_frame: torch.Tensor | None = None,
) -> IO.NodeOutput:
validate_prompts(prompt, negative_prompt, MAX_PROMPT_LENGTH_I2V)
validate_input_image(start_frame)
if camera_control is not None:
# Camera control type for image 2 video is always `simple`
camera_control.type = KlingCameraControlType.simple
if model_mode == "std" and model_name == KlingVideoGenModelName.kling_v2_5_turbo.value:
model_mode = "pro" # October 5: currently "std" mode is not supported for this model
task_creation_response = await sync_op(
cls,
ApiEndpoint(path=PATH_IMAGE_TO_VIDEO, method="POST"),
@@ -505,7 +456,6 @@ async def execute_image2video(
cfg_scale=cfg_scale,
mode=KlingVideoGenMode(model_mode),
duration=KlingVideoGenDuration(duration),
camera_control=camera_control,
),
)
@@ -525,59 +475,6 @@ async def execute_image2video(
return IO.NodeOutput(await download_url_to_video_output(str(video.url)), str(video.id), str(video.duration))
async def execute_video_effect(
cls: type[IO.ComfyNode],
dual_character: bool,
effect_scene: KlingDualCharacterEffectsScene | KlingSingleImageEffectsScene,
model_name: str,
duration: KlingVideoGenDuration,
image_1: torch.Tensor,
image_2: torch.Tensor | None = None,
model_mode: KlingVideoGenMode | None = None,
) -> tuple[InputImpl.VideoFromFile, str, str]:
if dual_character:
request_input_field = KlingDualCharacterEffectInput(
model_name=model_name,
mode=model_mode,
images=[
tensor_to_base64_string(image_1),
tensor_to_base64_string(image_2),
],
duration=duration,
)
else:
request_input_field = KlingSingleImageEffectInput(
model_name=model_name,
image=tensor_to_base64_string(image_1),
duration=duration,
)
task_creation_response = await sync_op(
cls,
endpoint=ApiEndpoint(path=PATH_VIDEO_EFFECTS, method="POST"),
response_model=KlingVideoEffectsResponse,
data=KlingVideoEffectsRequest(
effect_scene=effect_scene,
input=request_input_field,
),
)
validate_task_creation_response(task_creation_response)
task_id = task_creation_response.data.task_id
final_response = await poll_op(
cls,
ApiEndpoint(path=f"{PATH_VIDEO_EFFECTS}/{task_id}"),
response_model=KlingVideoEffectsResponse,
estimated_duration=AVERAGE_DURATION_VIDEO_EFFECTS,
status_extractor=lambda r: (r.data.task_status.value if r.data and r.data.task_status else None),
)
validate_video_result_response(final_response)
video = get_video_from_response(final_response)
return await download_url_to_video_output(str(video.url)), str(video.id), str(video.duration)
async def execute_lipsync(
cls: type[IO.ComfyNode],
video: Input.Video,
@@ -640,125 +537,6 @@ async def execute_lipsync(
return IO.NodeOutput(await download_url_to_video_output(str(video.url)), str(video.id), str(video.duration))
class KlingCameraControls(IO.ComfyNode):
"""Kling Camera Controls Node"""
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="KlingCameraControls",
display_name="Kling Camera Controls",
category="partner/video/Kling",
description="Allows specifying configuration options for Kling Camera Controls and motion control effects.",
inputs=[
IO.Combo.Input("camera_control_type", options=KlingCameraControlType),
IO.Float.Input(
"horizontal_movement",
default=0.0,
min=-10.0,
max=10.0,
step=0.25,
display_mode=IO.NumberDisplay.slider,
tooltip="Controls camera's movement along horizontal axis (x-axis). Negative indicates left, positive indicates right",
),
IO.Float.Input(
"vertical_movement",
default=0.0,
min=-10.0,
max=10.0,
step=0.25,
display_mode=IO.NumberDisplay.slider,
tooltip="Controls camera's movement along vertical axis (y-axis). Negative indicates downward, positive indicates upward.",
),
IO.Float.Input(
"pan",
default=0.5,
min=-10.0,
max=10.0,
step=0.25,
display_mode=IO.NumberDisplay.slider,
tooltip="Controls camera's rotation in vertical plane (x-axis). Negative indicates downward rotation, positive indicates upward rotation.",
),
IO.Float.Input(
"tilt",
default=0.0,
min=-10.0,
max=10.0,
step=0.25,
display_mode=IO.NumberDisplay.slider,
tooltip="Controls camera's rotation in horizontal plane (y-axis). Negative indicates left rotation, positive indicates right rotation.",
),
IO.Float.Input(
"roll",
default=0.0,
min=-10.0,
max=10.0,
step=0.25,
display_mode=IO.NumberDisplay.slider,
tooltip="Controls camera's rolling amount (z-axis). Negative indicates counterclockwise, positive indicates clockwise.",
),
IO.Float.Input(
"zoom",
default=0.0,
min=-10.0,
max=10.0,
step=0.25,
display_mode=IO.NumberDisplay.slider,
tooltip="Controls change in camera's focal length. Negative indicates narrower field of view, positive indicates wider field of view.",
),
],
outputs=[IO.Custom("CAMERA_CONTROL").Output(display_name="camera_control")],
)
@classmethod
def validate_inputs(
cls,
horizontal_movement: float,
vertical_movement: float,
pan: float,
tilt: float,
roll: float,
zoom: float,
) -> bool | str:
if not is_valid_camera_control_configs(
[
horizontal_movement,
vertical_movement,
pan,
tilt,
roll,
zoom,
]
):
return "Invalid camera control configs: at least one of the values must be non-zero"
return True
@classmethod
def execute(
cls,
camera_control_type: str,
horizontal_movement: float,
vertical_movement: float,
pan: float,
tilt: float,
roll: float,
zoom: float,
) -> IO.NodeOutput:
return IO.NodeOutput(
KlingCameraControl(
type=KlingCameraControlType(camera_control_type),
config=KlingCameraConfig(
horizontal=horizontal_movement,
vertical=vertical_movement,
pan=pan,
roll=roll,
tilt=tilt,
zoom=zoom,
),
)
)
class KlingTextToVideoNode(IO.ComfyNode):
"""Kling Text to Video Node"""
@@ -782,7 +560,7 @@ class KlingTextToVideoNode(IO.ComfyNode):
IO.Combo.Input(
"mode",
options=modes,
default=modes[8],
default=modes[0],
tooltip="The configuration to use for the video generation following the format: mode / duration / model_name.",
),
],
@@ -802,25 +580,7 @@ class KlingTextToVideoNode(IO.ComfyNode):
expr="""
(
$m := widgets.mode;
$contains($m,"v2-5-turbo")
? ($contains($m,"10") ? {"type":"usd","usd":0.7} : {"type":"usd","usd":0.35})
: $contains($m,"v2-1-master")
? ($contains($m,"10s") ? {"type":"usd","usd":2.8} : {"type":"usd","usd":1.4})
: $contains($m,"v2-master")
? ($contains($m,"10s") ? {"type":"usd","usd":2.8} : {"type":"usd","usd":1.4})
: $contains($m,"v1-6")
? (
$contains($m,"pro")
? ($contains($m,"10s") ? {"type":"usd","usd":0.98} : {"type":"usd","usd":0.49})
: ($contains($m,"10s") ? {"type":"usd","usd":0.56} : {"type":"usd","usd":0.28})
)
: $contains($m,"v1")
? (
$contains($m,"pro")
? ($contains($m,"10s") ? {"type":"usd","usd":0.98} : {"type":"usd","usd":0.49})
: ($contains($m,"10s") ? {"type":"usd","usd":0.28} : {"type":"usd","usd":0.14})
)
: {"type":"usd","usd":0.14}
$contains($m,"10") ? {"type":"usd","usd":0.7} : {"type":"usd","usd":0.35}
)
""",
),
@@ -1716,71 +1476,6 @@ class OmniProImageNode(IO.ComfyNode):
return IO.NodeOutput(torch.cat(tensors, dim=0))
class KlingCameraControlT2VNode(IO.ComfyNode):
"""
Kling Text to Video Camera Control Node. This node is a text to video node, but it supports controlling the camera.
Duration, mode, and model_name request fields are hard-coded because camera control is only supported in pro mode with the kling-v1-5 model at 5s duration as of 2025-05-02.
"""
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="KlingCameraControlT2VNode",
display_name="Kling Text to Video (Camera Control)",
category="partner/video/Kling",
description="Transform text into cinematic videos with professional camera movements that simulate real-world cinematography. Control virtual camera actions including zoom, rotation, pan, tilt, and first-person view, while maintaining focus on your original text.",
inputs=[
IO.String.Input("prompt", multiline=True, tooltip="Positive text prompt"),
IO.String.Input("negative_prompt", multiline=True, tooltip="Negative text prompt"),
IO.Float.Input("cfg_scale", default=0.75, min=0.0, max=1.0),
IO.Combo.Input(
"aspect_ratio",
options=KlingVideoGenAspectRatio,
default="16:9",
),
IO.Custom("CAMERA_CONTROL").Input(
"camera_control",
tooltip="Can be created using the Kling Camera Controls node. Controls the camera movement and motion during the video generation.",
),
],
outputs=[
IO.Video.Output(),
IO.String.Output(display_name="video_id"),
IO.String.Output(display_name="duration"),
],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
price_badge=IO.PriceBadge(
expr="""{"type":"usd","usd":0.14}""",
),
)
@classmethod
async def execute(
cls,
prompt: str,
negative_prompt: str,
cfg_scale: float,
aspect_ratio: str,
camera_control: KlingCameraControl | None = None,
) -> IO.NodeOutput:
return await execute_text2video(
cls,
model_name=KlingVideoGenModelName.kling_v1,
cfg_scale=cfg_scale,
model_mode=KlingVideoGenMode.std,
aspect_ratio=KlingVideoGenAspectRatio(aspect_ratio),
duration=KlingVideoGenDuration.field_5,
prompt=prompt,
negative_prompt=negative_prompt,
camera_control=camera_control,
)
class KlingImage2VideoNode(IO.ComfyNode):
"""Kling Image to Video Node"""
@@ -1796,11 +1491,10 @@ class KlingImage2VideoNode(IO.ComfyNode):
IO.String.Input("negative_prompt", multiline=True, tooltip="Negative text prompt"),
IO.Combo.Input(
"model_name",
options=KlingVideoGenModelName,
default="kling-v2-master",
options=["kling-v2-5-turbo"],
),
IO.Float.Input("cfg_scale", default=0.8, min=0.0, max=1.0),
IO.Combo.Input("mode", options=KlingVideoGenMode, default=KlingVideoGenMode.std),
IO.Combo.Input("mode", options=["pro"]),
IO.Combo.Input(
"aspect_ratio",
options=KlingVideoGenAspectRatio,
@@ -1820,29 +1514,10 @@ class KlingImage2VideoNode(IO.ComfyNode):
],
is_api_node=True,
price_badge=IO.PriceBadge(
depends_on=IO.PriceBadgeDepends(widgets=["mode", "model_name", "duration"]),
depends_on=IO.PriceBadgeDepends(widgets=["duration"]),
expr="""
(
$mode := widgets.mode;
$model := widgets.model_name;
$dur := widgets.duration;
$contains($model,"v2-5-turbo")
? ($contains($dur,"10") ? {"type":"usd","usd":0.7} : {"type":"usd","usd":0.35})
: ($contains($model,"v2-1-master") or $contains($model,"v2-master"))
? ($contains($dur,"10") ? {"type":"usd","usd":2.8} : {"type":"usd","usd":1.4})
: ($contains($model,"v2-1") or $contains($model,"v1-6") or $contains($model,"v1-5"))
? (
$contains($mode,"pro")
? ($contains($dur,"10") ? {"type":"usd","usd":0.98} : {"type":"usd","usd":0.49})
: ($contains($dur,"10") ? {"type":"usd","usd":0.56} : {"type":"usd","usd":0.28})
)
: $contains($model,"v1")
? (
$contains($mode,"pro")
? ($contains($dur,"10") ? {"type":"usd","usd":0.98} : {"type":"usd","usd":0.49})
: ($contains($dur,"10") ? {"type":"usd","usd":0.28} : {"type":"usd","usd":0.14})
)
: {"type":"usd","usd":0.14}
$contains(widgets.duration,"10") ? {"type":"usd","usd":0.7} : {"type":"usd","usd":0.35}
)
""",
),
@@ -1859,8 +1534,6 @@ class KlingImage2VideoNode(IO.ComfyNode):
mode: str,
aspect_ratio: str,
duration: str,
camera_control: KlingCameraControl | None = None,
end_frame: torch.Tensor | None = None,
) -> IO.NodeOutput:
return await execute_image2video(
cls,
@@ -1872,79 +1545,6 @@ class KlingImage2VideoNode(IO.ComfyNode):
aspect_ratio=aspect_ratio,
model_mode=mode,
duration=duration,
camera_control=camera_control,
end_frame=end_frame,
)
class KlingCameraControlI2VNode(IO.ComfyNode):
"""
Kling Image to Video Camera Control Node. This node is a image to video node, but it supports controlling the camera.
Duration, mode, and model_name request fields are hard-coded because camera control is only supported in pro mode with the kling-v1-5 model at 5s duration as of 2025-05-02.
"""
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="KlingCameraControlI2VNode",
display_name="Kling Image to Video (Camera Control)",
category="partner/video/Kling",
description="Transform still images into cinematic videos with professional camera movements that simulate real-world cinematography. Control virtual camera actions including zoom, rotation, pan, tilt, and first-person view, while maintaining focus on your original image.",
inputs=[
IO.Image.Input(
"start_frame",
tooltip="Reference Image - URL or Base64 encoded string, cannot exceed 10MB, resolution not less than 300*300px, aspect ratio between 1:2.5 ~ 2.5:1. Base64 should not include data:image prefix.",
),
IO.String.Input("prompt", multiline=True, tooltip="Positive text prompt"),
IO.String.Input("negative_prompt", multiline=True, tooltip="Negative text prompt"),
IO.Float.Input("cfg_scale", default=0.75, min=0.0, max=1.0),
IO.Combo.Input(
"aspect_ratio",
options=KlingVideoGenAspectRatio,
default=KlingVideoGenAspectRatio.field_16_9,
),
IO.Custom("CAMERA_CONTROL").Input(
"camera_control",
tooltip="Can be created using the Kling Camera Controls node. Controls the camera movement and motion during the video generation.",
),
],
outputs=[
IO.Video.Output(),
IO.String.Output(display_name="video_id"),
IO.String.Output(display_name="duration"),
],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
price_badge=IO.PriceBadge(
expr="""{"type":"usd","usd":0.49}""",
),
)
@classmethod
async def execute(
cls,
start_frame: torch.Tensor,
prompt: str,
negative_prompt: str,
cfg_scale: float,
aspect_ratio: str,
camera_control: KlingCameraControl,
) -> IO.NodeOutput:
return await execute_image2video(
cls,
model_name=KlingVideoGenModelName.kling_v1_5,
start_frame=start_frame,
cfg_scale=cfg_scale,
model_mode=KlingVideoGenMode.pro,
aspect_ratio=KlingVideoGenAspectRatio(aspect_ratio),
duration=KlingVideoGenDuration.field_5,
prompt=prompt,
negative_prompt=negative_prompt,
camera_control=camera_control,
)
@@ -1977,7 +1577,7 @@ class KlingStartEndFrameNode(IO.ComfyNode):
IO.Combo.Input(
"mode",
options=modes,
default=modes[6],
default=modes[0],
tooltip="The configuration to use for the video generation following the format: mode / duration / model_name.",
),
],
@@ -1997,25 +1597,7 @@ class KlingStartEndFrameNode(IO.ComfyNode):
expr="""
(
$m := widgets.mode;
$contains($m,"v2-5-turbo")
? ($contains($m,"10") ? {"type":"usd","usd":0.7} : {"type":"usd","usd":0.35})
: $contains($m,"v2-1")
? ($contains($m,"10s") ? {"type":"usd","usd":0.98} : {"type":"usd","usd":0.49})
: $contains($m,"v2-master")
? ($contains($m,"10s") ? {"type":"usd","usd":2.8} : {"type":"usd","usd":1.4})
: $contains($m,"v1-6")
? (
$contains($m,"pro")
? ($contains($m,"10s") ? {"type":"usd","usd":0.98} : {"type":"usd","usd":0.49})
: ($contains($m,"10s") ? {"type":"usd","usd":0.56} : {"type":"usd","usd":0.28})
)
: $contains($m,"v1")
? (
$contains($m,"pro")
? ($contains($m,"10s") ? {"type":"usd","usd":0.98} : {"type":"usd","usd":0.49})
: ($contains($m,"10s") ? {"type":"usd","usd":0.28} : {"type":"usd","usd":0.14})
)
: {"type":"usd","usd":0.14}
$contains($m,"10") ? {"type":"usd","usd":0.7} : {"type":"usd","usd":0.35}
)
""",
),
@@ -2126,169 +1708,6 @@ class KlingVideoExtendNode(IO.ComfyNode):
return IO.NodeOutput(await download_url_to_video_output(str(video.url)), str(video.id), str(video.duration))
class KlingDualCharacterVideoEffectNode(IO.ComfyNode):
"""Kling Dual Character Video Effect Node"""
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="KlingDualCharacterVideoEffectNode",
display_name="Kling Dual Character Video Effects",
category="partner/video/Kling",
description="Achieve different special effects when generating a video based on the effect_scene. First image will be positioned on left side, second on right side of the composite.",
inputs=[
IO.Image.Input("image_left", tooltip="Left side image"),
IO.Image.Input("image_right", tooltip="Right side image"),
IO.Combo.Input(
"effect_scene",
options=[i.value for i in KlingDualCharacterEffectsScene],
),
IO.Combo.Input(
"model_name",
options=[i.value for i in KlingCharacterEffectModelName],
default="kling-v1",
),
IO.Combo.Input(
"mode",
options=[i.value for i in KlingVideoGenMode],
default="std",
),
IO.Combo.Input(
"duration",
options=[i.value for i in KlingVideoGenDuration],
),
],
outputs=[
IO.Video.Output(),
IO.String.Output(display_name="duration"),
],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
price_badge=IO.PriceBadge(
depends_on=IO.PriceBadgeDepends(widgets=["mode", "model_name", "duration"]),
expr="""
(
$mode := widgets.mode;
$model := widgets.model_name;
$dur := widgets.duration;
($contains($model,"v1-6") or $contains($model,"v1-5"))
? (
$contains($mode,"pro")
? ($contains($dur,"10") ? {"type":"usd","usd":0.98} : {"type":"usd","usd":0.49})
: ($contains($dur,"10") ? {"type":"usd","usd":0.56} : {"type":"usd","usd":0.28})
)
: $contains($model,"v1")
? (
$contains($mode,"pro")
? ($contains($dur,"10") ? {"type":"usd","usd":0.98} : {"type":"usd","usd":0.49})
: ($contains($dur,"10") ? {"type":"usd","usd":0.28} : {"type":"usd","usd":0.14})
)
: {"type":"usd","usd":0.14}
)
""",
),
)
@classmethod
async def execute(
cls,
image_left: torch.Tensor,
image_right: torch.Tensor,
effect_scene: KlingDualCharacterEffectsScene,
model_name: KlingCharacterEffectModelName,
mode: KlingVideoGenMode,
duration: KlingVideoGenDuration,
) -> IO.NodeOutput:
video, _, duration = await execute_video_effect(
cls,
dual_character=True,
effect_scene=effect_scene,
model_name=model_name,
model_mode=mode,
duration=duration,
image_1=image_left,
image_2=image_right,
)
return IO.NodeOutput(video, duration)
class KlingSingleImageVideoEffectNode(IO.ComfyNode):
"""Kling Single Image Video Effect Node"""
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="KlingSingleImageVideoEffectNode",
display_name="Kling Video Effects",
category="partner/video/Kling",
description="Achieve different special effects when generating a video based on the effect_scene.",
inputs=[
IO.Image.Input(
"image",
tooltip=" Reference Image. URL or Base64 encoded string (without data:image prefix). File size cannot exceed 10MB, resolution not less than 300*300px, aspect ratio between 1:2.5 ~ 2.5:1",
),
IO.Combo.Input(
"effect_scene",
options=[i.value for i in KlingSingleImageEffectsScene],
),
IO.Combo.Input(
"model_name",
options=[i.value for i in KlingSingleImageEffectModelName],
),
IO.Combo.Input(
"duration",
options=[i.value for i in KlingVideoGenDuration],
),
],
outputs=[
IO.Video.Output(),
IO.String.Output(display_name="video_id"),
IO.String.Output(display_name="duration"),
],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
price_badge=IO.PriceBadge(
depends_on=IO.PriceBadgeDepends(widgets=["effect_scene"]),
expr="""
(
($contains(widgets.effect_scene,"dizzydizzy") or $contains(widgets.effect_scene,"bloombloom"))
? {"type":"usd","usd":0.49}
: {"type":"usd","usd":0.28}
)
""",
),
)
@classmethod
async def execute(
cls,
image: torch.Tensor,
effect_scene: KlingSingleImageEffectsScene,
model_name: KlingSingleImageEffectModelName,
duration: KlingVideoGenDuration,
) -> IO.NodeOutput:
return IO.NodeOutput(
*(
await execute_video_effect(
cls,
dual_character=False,
effect_scene=effect_scene,
model_name=model_name,
duration=duration,
image_1=image,
)
)
)
class KlingLipSyncAudioToVideoNode(IO.ComfyNode):
"""Kling Lip Sync Audio to Video Node. Syncs mouth movements in a video file to the audio content of an audio file."""
@@ -2409,73 +1828,6 @@ class KlingLipSyncTextToVideoNode(IO.ComfyNode):
)
class KlingVirtualTryOnNode(IO.ComfyNode):
"""Kling Virtual Try On Node."""
@classmethod
def define_schema(cls) -> IO.Schema:
return IO.Schema(
node_id="KlingVirtualTryOnNode",
display_name="Kling Virtual Try On",
category="partner/image/Kling",
description="Kling Virtual Try On Node. Input a human image and a cloth image to try on the cloth on the human. You can merge multiple clothing item pictures into one image with a white background.",
inputs=[
IO.Image.Input("human_image"),
IO.Image.Input("cloth_image"),
IO.Combo.Input(
"model_name",
options=[i.value for i in KlingVirtualTryOnModelName],
default="kolors-virtual-try-on-v1",
),
],
outputs=[
IO.Image.Output(),
],
hidden=[
IO.Hidden.auth_token_comfy_org,
IO.Hidden.api_key_comfy_org,
IO.Hidden.unique_id,
],
is_api_node=True,
price_badge=IO.PriceBadge(
expr="""{"type":"usd","usd":0.7}""",
),
)
@classmethod
async def execute(
cls,
human_image: torch.Tensor,
cloth_image: torch.Tensor,
model_name: KlingVirtualTryOnModelName,
) -> IO.NodeOutput:
task_creation_response = await sync_op(
cls,
ApiEndpoint(path=PATH_VIRTUAL_TRY_ON, method="POST"),
response_model=KlingVirtualTryOnResponse,
data=KlingVirtualTryOnRequest(
human_image=tensor_to_base64_string(human_image),
cloth_image=tensor_to_base64_string(cloth_image),
model_name=model_name,
),
)
validate_task_creation_response(task_creation_response)
task_id = task_creation_response.data.task_id
final_response = await poll_op(
cls,
ApiEndpoint(path=f"{PATH_VIRTUAL_TRY_ON}/{task_id}"),
response_model=KlingVirtualTryOnResponse,
estimated_duration=AVERAGE_DURATION_VIRTUAL_TRY_ON,
status_extractor=lambda r: (r.data.task_status.value if r.data and r.data.task_status else None),
)
validate_image_result_response(final_response)
images = get_images_from_response(final_response)
return IO.NodeOutput(await image_result_to_node_output(images))
class KlingImageGenerationNode(IO.ComfyNode):
"""Kling Image Generation Node. Generate an image from a text prompt with an optional reference image."""
@@ -2514,7 +1866,7 @@ class KlingImageGenerationNode(IO.ComfyNode):
tooltip="Subject reference similarity",
advanced=True,
),
IO.Combo.Input("model_name", options=["kling-v3", "kling-v2", "kling-v1-5"]),
IO.Combo.Input("model_name", options=["kling-v3", "kling-v2"]),
IO.Combo.Input(
"aspect_ratio",
options=[i.value for i in KlingImageGenAspectRatio],
@@ -2550,14 +1902,10 @@ class KlingImageGenerationNode(IO.ComfyNode):
],
is_api_node=True,
price_badge=IO.PriceBadge(
depends_on=IO.PriceBadgeDepends(widgets=["model_name", "n"], inputs=["image"]),
depends_on=IO.PriceBadgeDepends(widgets=["model_name", "n"]),
expr="""
(
$m := widgets.model_name;
$base :=
$contains($m,"kling-v1-5")
? (inputs.image.connected ? 0.028 : 0.014)
: $contains($m,"kling-v3") ? 0.028 : 0.014;
$base := $contains(widgets.model_name,"kling-v3") ? 0.028 : 0.014;
{"type":"usd","usd": $base * widgets.n}
)
""",
@@ -3394,19 +2742,13 @@ class KlingExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
return [
KlingCameraControls,
KlingTextToVideoNode,
KlingImage2VideoNode,
KlingCameraControlI2VNode,
KlingCameraControlT2VNode,
KlingStartEndFrameNode,
KlingVideoExtendNode,
KlingLipSyncAudioToVideoNode,
KlingLipSyncTextToVideoNode,
KlingVirtualTryOnNode,
KlingImageGenerationNode,
KlingSingleImageVideoEffectNode,
KlingDualCharacterVideoEffectNode,
OmniProTextToVideoNode,
OmniProFirstLastFrameNode,
OmniProImageToVideoNode,