Skip to content

Commit dba2766

Browse files
authored
feat(api-nodes): add KlingAvatar node (Comfy-Org#12591)
1 parent caa43d2 commit dba2766

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

comfy_api_nodes/apis/kling.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ class ImageToVideoWithAudioRequest(BaseModel):
134134
shot_type: str | None = Field(None)
135135

136136

137+
class KlingAvatarRequest(BaseModel):
138+
image: str = Field(...)
139+
sound_file: str = Field(...)
140+
prompt: str | None = Field(None)
141+
mode: str = Field(...)
142+
143+
137144
class MotionControlRequest(BaseModel):
138145
prompt: str = Field(...)
139146
image_url: str = Field(...)

comfy_api_nodes/nodes_kling.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
)
5151
from comfy_api_nodes.apis.kling import (
5252
ImageToVideoWithAudioRequest,
53+
KlingAvatarRequest,
5354
MotionControlRequest,
5455
MultiPromptEntry,
5556
OmniImageParamImage,
@@ -74,6 +75,7 @@
7475
upload_image_to_comfyapi,
7576
upload_images_to_comfyapi,
7677
upload_video_to_comfyapi,
78+
validate_audio_duration,
7779
validate_image_aspect_ratio,
7880
validate_image_dimensions,
7981
validate_string,
@@ -3139,6 +3141,103 @@ async def execute(
31393141
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
31403142

31413143

3144+
class KlingAvatarNode(IO.ComfyNode):
3145+
3146+
@classmethod
3147+
def define_schema(cls) -> IO.Schema:
3148+
return IO.Schema(
3149+
node_id="KlingAvatarNode",
3150+
display_name="Kling Avatar 2.0",
3151+
category="api node/video/Kling",
3152+
description="Generate broadcast-style digital human videos from a single photo and an audio file.",
3153+
inputs=[
3154+
IO.Image.Input(
3155+
"image",
3156+
tooltip="Avatar reference image. "
3157+
"Width and height must be at least 300px. Aspect ratio must be between 1:2.5 and 2.5:1.",
3158+
),
3159+
IO.Audio.Input(
3160+
"sound_file",
3161+
tooltip="Audio input. Must be between 2 and 300 seconds in duration.",
3162+
),
3163+
IO.Combo.Input("mode", options=["std", "pro"]),
3164+
IO.String.Input(
3165+
"prompt",
3166+
multiline=True,
3167+
default="",
3168+
optional=True,
3169+
tooltip="Optional prompt to define avatar actions, emotions, and camera movements.",
3170+
),
3171+
IO.Int.Input(
3172+
"seed",
3173+
default=0,
3174+
min=0,
3175+
max=2147483647,
3176+
display_mode=IO.NumberDisplay.number,
3177+
control_after_generate=True,
3178+
tooltip="Seed controls whether the node should re-run; "
3179+
"results are non-deterministic regardless of seed.",
3180+
),
3181+
],
3182+
outputs=[
3183+
IO.Video.Output(),
3184+
],
3185+
hidden=[
3186+
IO.Hidden.auth_token_comfy_org,
3187+
IO.Hidden.api_key_comfy_org,
3188+
IO.Hidden.unique_id,
3189+
],
3190+
is_api_node=True,
3191+
price_badge=IO.PriceBadge(
3192+
depends_on=IO.PriceBadgeDepends(widgets=["mode"]),
3193+
expr="""
3194+
(
3195+
$prices := {"std": 0.056, "pro": 0.112};
3196+
{"type":"usd","usd": $lookup($prices, widgets.mode), "format":{"suffix":"/second"}}
3197+
)
3198+
""",
3199+
),
3200+
)
3201+
3202+
@classmethod
3203+
async def execute(
3204+
cls,
3205+
image: Input.Image,
3206+
sound_file: Input.Audio,
3207+
mode: str,
3208+
seed: int,
3209+
prompt: str = "",
3210+
) -> IO.NodeOutput:
3211+
validate_image_dimensions(image, min_width=300, min_height=300)
3212+
validate_image_aspect_ratio(image, (1, 2.5), (2.5, 1))
3213+
validate_audio_duration(sound_file, min_duration=2, max_duration=300)
3214+
response = await sync_op(
3215+
cls,
3216+
ApiEndpoint(path="/proxy/kling/v1/videos/avatar/image2video", method="POST"),
3217+
response_model=TaskStatusResponse,
3218+
data=KlingAvatarRequest(
3219+
image=await upload_image_to_comfyapi(cls, image),
3220+
sound_file=await upload_audio_to_comfyapi(
3221+
cls, sound_file, container_format="mp3", codec_name="libmp3lame", mime_type="audio/mpeg"
3222+
),
3223+
prompt=prompt or None,
3224+
mode=mode,
3225+
),
3226+
)
3227+
if response.code:
3228+
raise RuntimeError(
3229+
f"Kling request failed. Code: {response.code}, Message: {response.message}, Data: {response.data}"
3230+
)
3231+
final_response = await poll_op(
3232+
cls,
3233+
ApiEndpoint(path=f"/proxy/kling/v1/videos/avatar/image2video/{response.data.task_id}"),
3234+
response_model=TaskStatusResponse,
3235+
status_extractor=lambda r: (r.data.task_status if r.data else None),
3236+
max_poll_attempts=800,
3237+
)
3238+
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
3239+
3240+
31423241
class KlingExtension(ComfyExtension):
31433242
@override
31443243
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
@@ -3167,6 +3266,7 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]:
31673266
MotionControl,
31683267
KlingVideoNode,
31693268
KlingFirstLastFrameNode,
3269+
KlingAvatarNode,
31703270
]
31713271

31723272

0 commit comments

Comments
 (0)