Skip to content

Commit aeec93e

Browse files
bigcat88lrivera
authored andcommitted
feat(api-nodes): add Kling Motion Control node (Comfy-Org#11493)
1 parent 0f58013 commit aeec93e

File tree

2 files changed

+96
-0
lines changed

2 files changed

+96
-0
lines changed

comfy_api_nodes/apis/kling_api.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,12 @@ class ImageToVideoWithAudioRequest(BaseModel):
102102
prompt: str = Field(...)
103103
mode: str = Field("pro")
104104
sound: str = Field(..., description="'on' or 'off'")
105+
106+
107+
class MotionControlRequest(BaseModel):
108+
prompt: str = Field(...)
109+
image_url: str = Field(...)
110+
video_url: str = Field(...)
111+
keep_original_sound: str = Field(...)
112+
character_orientation: str = Field(...)
113+
mode: str = Field(..., description="'pro' or 'std'")

comfy_api_nodes/nodes_kling.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
)
5252
from comfy_api_nodes.apis.kling_api import (
5353
ImageToVideoWithAudioRequest,
54+
MotionControlRequest,
5455
OmniImageParamImage,
5556
OmniParamImage,
5657
OmniParamVideo,
@@ -2163,6 +2164,91 @@ async def execute(
21632164
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
21642165

21652166

2167+
class MotionControl(IO.ComfyNode):
2168+
2169+
@classmethod
2170+
def define_schema(cls) -> IO.Schema:
2171+
return IO.Schema(
2172+
node_id="KlingMotionControl",
2173+
display_name="Kling Motion Control",
2174+
category="api node/video/Kling",
2175+
inputs=[
2176+
IO.String.Input("prompt", multiline=True),
2177+
IO.Image.Input("reference_image"),
2178+
IO.Video.Input(
2179+
"reference_video",
2180+
tooltip="Motion reference video used to drive movement/expression.\n"
2181+
"Duration limits depend on character_orientation:\n"
2182+
" - image: 3–10s (max 10s)\n"
2183+
" - video: 3–30s (max 30s)",
2184+
),
2185+
IO.Boolean.Input("keep_original_sound", default=True),
2186+
IO.Combo.Input(
2187+
"character_orientation",
2188+
options=["video", "image"],
2189+
tooltip="Controls where the character's facing/orientation comes from.\n"
2190+
"video: movements, expressions, camera moves, and orientation "
2191+
"follow the motion reference video (other details via prompt).\n"
2192+
"image: movements and expressions still follow the motion reference video, "
2193+
"but the character orientation matches the reference image (camera/other details via prompt).",
2194+
),
2195+
IO.Combo.Input("mode", options=["pro", "std"]),
2196+
],
2197+
outputs=[
2198+
IO.Video.Output(),
2199+
],
2200+
hidden=[
2201+
IO.Hidden.auth_token_comfy_org,
2202+
IO.Hidden.api_key_comfy_org,
2203+
IO.Hidden.unique_id,
2204+
],
2205+
is_api_node=True,
2206+
)
2207+
2208+
@classmethod
2209+
async def execute(
2210+
cls,
2211+
prompt: str,
2212+
reference_image: Input.Image,
2213+
reference_video: Input.Video,
2214+
keep_original_sound: bool,
2215+
character_orientation: str,
2216+
mode: str,
2217+
) -> IO.NodeOutput:
2218+
validate_string(prompt, max_length=2500)
2219+
validate_image_dimensions(reference_image, min_width=340, min_height=340)
2220+
validate_image_aspect_ratio(reference_image, (1, 2.5), (2.5, 1))
2221+
if character_orientation == "image":
2222+
validate_video_duration(reference_video, min_duration=3, max_duration=10)
2223+
else:
2224+
validate_video_duration(reference_video, min_duration=3, max_duration=30)
2225+
validate_video_dimensions(reference_video, min_width=340, min_height=340, max_width=3850, max_height=3850)
2226+
response = await sync_op(
2227+
cls,
2228+
ApiEndpoint(path="/proxy/kling/v1/videos/motion-control", method="POST"),
2229+
response_model=TaskStatusResponse,
2230+
data=MotionControlRequest(
2231+
prompt=prompt,
2232+
image_url=(await upload_images_to_comfyapi(cls, reference_image))[0],
2233+
video_url=await upload_video_to_comfyapi(cls, reference_video),
2234+
keep_original_sound="yes" if keep_original_sound else "no",
2235+
character_orientation=character_orientation,
2236+
mode=mode,
2237+
),
2238+
)
2239+
if response.code:
2240+
raise RuntimeError(
2241+
f"Kling request failed. Code: {response.code}, Message: {response.message}, Data: {response.data}"
2242+
)
2243+
final_response = await poll_op(
2244+
cls,
2245+
ApiEndpoint(path=f"/proxy/kling/v1/videos/motion-control/{response.data.task_id}"),
2246+
response_model=TaskStatusResponse,
2247+
status_extractor=lambda r: (r.data.task_status if r.data else None),
2248+
)
2249+
return IO.NodeOutput(await download_url_to_video_output(final_response.data.task_result.videos[0].url))
2250+
2251+
21662252
class KlingExtension(ComfyExtension):
21672253
@override
21682254
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
@@ -2188,6 +2274,7 @@ async def get_node_list(self) -> list[type[IO.ComfyNode]]:
21882274
OmniProImageNode,
21892275
TextToVideoWithAudio,
21902276
ImageToVideoWithAudio,
2277+
MotionControl,
21912278
]
21922279

21932280

0 commit comments

Comments
 (0)