[Kling] Split Camera Control config to its own node (#81)

This commit is contained in:
Christian Byrne 2025-04-30 17:50:53 -07:00 committed by Jedrzej Kosinski
parent 324cfcc088
commit 7d5dde7e6c

View File

@ -1,3 +1,14 @@
"""
`camera_control` supported:
- pro | 5s duration | kling-v1-5
`camera_control` not supported:
- std | 10s duration | kling-v1-6
"""
from typing import Union, Optional from typing import Union, Optional
import math import math
import logging import logging
@ -72,11 +83,6 @@ def is_valid_video_response(response: KlingText2VideoResponse) -> bool:
) )
def is_camera_control_supported(model_name: str, duration: str, mode: str) -> bool:
"""`camera_control` is only supported in `pro` mode with `5s` duration and `kling-v1-5`"""
return model_name == "kling-v1-5" and duration == "5" and mode == "pro"
def get_camera_control_input_config( def get_camera_control_input_config(
tooltip: str, default: float = 0.0 tooltip: str, default: float = 0.0
) -> tuple[IO, InputTypeOptions]: ) -> tuple[IO, InputTypeOptions]:
@ -92,39 +98,86 @@ def get_camera_control_input_config(
return IO.FLOAT, input_config return IO.FLOAT, input_config
def _get_camera_control_inputs() -> dict[str, tuple[IO, InputTypeOptions]]: class KlingCameraControls(ComfyNodeABC):
"""Returns a dictionary of camera control inputs common to Kling video generation nodes.""" """Kling Camera Controls Node"""
return {
"camera_control_type": ( @classmethod
IO.COMBO, def INPUT_TYPES(cls):
{ return {
"options": [ "required": {
camera_control_type.value for camera_control_type in CameraType "camera_control_type": (
], IO.COMBO,
"default": "simple", {
"tooltip": "Predefined camera movements type. simple: Customizable camera movement. down_back: Camera descends and moves backward. forward_up: Camera moves forward and tilts up. right_turn_forward: Rotate right and move forward. left_turn_forward: Rotate left and move forward.", "options": [
}, camera_control_type.value
), for camera_control_type in CameraType
"camera_control_horizontal": get_camera_control_input_config( ],
"Controls camera's movement along horizontal axis (x-axis). Negative indicates left, positive indicates right" "default": "simple",
), "tooltip": "Predefined camera movements type. simple: Customizable camera movement. down_back: Camera descends and moves backward. forward_up: Camera moves forward and tilts up. right_turn_forward: Rotate right and move forward. left_turn_forward: Rotate left and move forward.",
"camera_control_vertical": get_camera_control_input_config( },
"Controls camera's movement along vertical axis (y-axis). Negative indicates downward, positive indicates upward." ),
), "horizontal_movement": get_camera_control_input_config(
"camera_control_pan": get_camera_control_input_config( "Controls camera's movement along horizontal axis (x-axis). Negative indicates left, positive indicates right"
"Controls camera's rotation in vertical plane (x-axis). Negative indicates downward rotation, positive indicates upward rotation.", ),
default=0.5, "vertical_movement": get_camera_control_input_config(
), "Controls camera's movement along vertical axis (y-axis). Negative indicates downward, positive indicates upward."
"camera_control_roll": get_camera_control_input_config( ),
"Controls camera's rotation in horizontal plane (y-axis). Negative indicates left rotation, positive indicates right rotation.", "pan": get_camera_control_input_config(
), "Controls camera's rotation in vertical plane (x-axis). Negative indicates downward rotation, positive indicates upward rotation.",
"camera_control_tilt": get_camera_control_input_config( default=0.5,
"Controls camera's rolling amount (z-axis). Negative indicates counterclockwise, positive indicates clockwise.", ),
), "tilt": get_camera_control_input_config(
"camera_control_zoom": get_camera_control_input_config( "Controls camera's rotation in horizontal plane (y-axis). Negative indicates left rotation, positive indicates right rotation.",
"Controls change in camera's focal length. Negative indicates narrower field of view, positive indicates wider field of view.", ),
), "roll": get_camera_control_input_config(
} "Controls camera's rolling amount (z-axis). Negative indicates counterclockwise, positive indicates clockwise.",
),
"zoom": get_camera_control_input_config(
"Controls change in camera's focal length. Negative indicates narrower field of view, positive indicates wider field of view.",
),
}
}
DESCRIPTION = "Kling Camera Controls Node. Not all model and mode combinations support camera control. Please refer to the Kling API documentation for more information."
RETURN_TYPES = ("CAMERA_CONTROL",)
RETURN_NAMES = ("camera_control",)
FUNCTION = "main"
def main(
self,
camera_control_type: str,
horizontal_movement: float,
vertical_movement: float,
pan: float,
tilt: float,
roll: float,
zoom: float,
):
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 (
CameraControl(
type=CameraType(camera_control_type),
config=CameraConfig(
horizontal=horizontal_movement,
vertical=vertical_movement,
pan=pan,
roll=roll,
tilt=tilt,
zoom=zoom,
),
),
)
class KlingNodeBase(ComfyNodeABC): class KlingNodeBase(ComfyNodeABC):
@ -135,12 +188,6 @@ class KlingNodeBase(ComfyNodeABC):
cls, cls,
prompt, prompt,
negative_prompt, negative_prompt,
camera_control_horizontal,
camera_control_vertical,
camera_control_pan,
camera_control_roll,
camera_control_tilt,
camera_control_zoom,
) -> Union[str, bool]: ) -> Union[str, bool]:
if not is_valid_prompt(prompt): if not is_valid_prompt(prompt):
return "Prompt is required" return "Prompt is required"
@ -148,17 +195,6 @@ class KlingNodeBase(ComfyNodeABC):
return "Prompt must be less than 2500 characters" return "Prompt must be less than 2500 characters"
if negative_prompt and len(negative_prompt) >= 2500: if negative_prompt and len(negative_prompt) >= 2500:
return "Negative prompt must be less than 2500 characters" return "Negative prompt must be less than 2500 characters"
if not is_valid_camera_control_configs(
[
camera_control_horizontal,
camera_control_vertical,
camera_control_pan,
camera_control_roll,
camera_control_tilt,
camera_control_zoom,
]
):
return "Invalid camera control configs"
return True return True
FUNCTION = "api_call" FUNCTION = "api_call"
@ -204,6 +240,13 @@ class KlingTextToVideoNode(KlingNodeBase):
"negative_prompt": model_field_to_node_input( "negative_prompt": model_field_to_node_input(
IO.STRING, KlingText2VideoRequest, "negative_prompt", multiline=True IO.STRING, KlingText2VideoRequest, "negative_prompt", multiline=True
), ),
"model_name": model_field_to_node_input(
IO.COMBO,
KlingText2VideoRequest,
"model_name",
enum_type=ModelName,
default="kling-v2-master",
),
"cfg_scale": model_field_to_node_input( "cfg_scale": model_field_to_node_input(
IO.FLOAT, KlingText2VideoRequest, "cfg_scale" IO.FLOAT, KlingText2VideoRequest, "cfg_scale"
), ),
@ -219,7 +262,9 @@ class KlingTextToVideoNode(KlingNodeBase):
"aspect_ratio", "aspect_ratio",
enum_type=AspectRatio, enum_type=AspectRatio,
), ),
**_get_camera_control_inputs(), },
"optional": {
"camera_control": ("CAMERA_CONTROL", {}),
}, },
"hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"}, "hidden": {"auth_token": "AUTH_TOKEN_COMFY_ORG"},
} }
@ -231,33 +276,14 @@ class KlingTextToVideoNode(KlingNodeBase):
self, self,
prompt: str, prompt: str,
negative_prompt: str, negative_prompt: str,
duration: int, model_name: str,
mode: str,
cfg_scale: float, cfg_scale: float,
mode: str,
duration: int,
aspect_ratio: str, aspect_ratio: str,
camera_control_type: str, camera_control: Optional[CameraControl] = None,
camera_control_horizontal: float,
camera_control_vertical: float,
camera_control_pan: float,
camera_control_roll: float,
camera_control_tilt: float,
camera_control_zoom: float,
auth_token: Optional[str] = None, auth_token: Optional[str] = None,
) -> tuple[VideoFromFile]: ) -> tuple[VideoFromFile]:
camera_control = None
if is_camera_control_supported("kling-v1-6", duration, mode):
camera_control = CameraControl(
type=CameraType(camera_control_type),
config=CameraConfig(
horizontal=camera_control_horizontal,
vertical=camera_control_vertical,
pan=camera_control_pan,
roll=camera_control_roll,
tilt=camera_control_tilt,
zoom=camera_control_zoom,
).model_dump(exclude_none=True),
)
initial_operation = SynchronousOperation( initial_operation = SynchronousOperation(
endpoint=ApiEndpoint( endpoint=ApiEndpoint(
path=PATH_TEXT_TO_VIDEO, path=PATH_TEXT_TO_VIDEO,
@ -270,6 +296,7 @@ class KlingTextToVideoNode(KlingNodeBase):
negative_prompt=negative_prompt if negative_prompt else None, negative_prompt=negative_prompt if negative_prompt else None,
duration=Duration(duration), duration=Duration(duration),
mode=Mode(mode), mode=Mode(mode),
model_name=ModelName(model_name),
cfg_scale=cfg_scale, cfg_scale=cfg_scale,
aspect_ratio=AspectRatio(aspect_ratio), aspect_ratio=AspectRatio(aspect_ratio),
camera_control=camera_control, camera_control=camera_control,
@ -284,8 +311,6 @@ class KlingTextToVideoNode(KlingNodeBase):
raise KlingApiError(error_msg) raise KlingApiError(error_msg)
task_id = initial_response.data.task_id task_id = initial_response.data.task_id
logging.debug("Kling task submitted. Task ID: %s", task_id)
final_response = self.poll_for_task_status(task_id, auth_token) final_response = self.poll_for_task_status(task_id, auth_token)
if not is_valid_video_response(final_response): if not is_valid_video_response(final_response):
error_msg = ( error_msg = (
@ -330,12 +355,6 @@ class KlingImage2VideoNode(KlingNodeBase):
def INPUT_TYPES(s): def INPUT_TYPES(s):
return { return {
"required": { "required": {
"model_name": model_field_to_node_input(
IO.COMBO, KlingImage2VideoRequest, "model_name", enum_type=ModelName
),
"start_frame": model_field_to_node_input(
IO.IMAGE, KlingImage2VideoRequest, "image"
),
"prompt": model_field_to_node_input( "prompt": model_field_to_node_input(
IO.STRING, KlingImage2VideoRequest, "prompt", multiline=True IO.STRING, KlingImage2VideoRequest, "prompt", multiline=True
), ),
@ -345,6 +364,16 @@ class KlingImage2VideoNode(KlingNodeBase):
"negative_prompt", "negative_prompt",
multiline=True, multiline=True,
), ),
"model_name": model_field_to_node_input(
IO.COMBO,
KlingImage2VideoRequest,
"model_name",
enum_type=ModelName,
default="kling-v2-master",
),
"start_frame": model_field_to_node_input(
IO.IMAGE, KlingImage2VideoRequest, "image"
),
"cfg_scale": model_field_to_node_input( "cfg_scale": model_field_to_node_input(
IO.FLOAT, KlingImage2VideoRequest, "cfg_scale" IO.FLOAT, KlingImage2VideoRequest, "cfg_scale"
), ),
@ -360,9 +389,9 @@ class KlingImage2VideoNode(KlingNodeBase):
"duration": model_field_to_node_input( "duration": model_field_to_node_input(
IO.COMBO, KlingImage2VideoRequest, "duration", enum_type=Duration IO.COMBO, KlingImage2VideoRequest, "duration", enum_type=Duration
), ),
**_get_camera_control_inputs(),
}, },
"optional": { "optional": {
"camera_control": ("CAMERA_CONTROL", {}),
"end_frame": model_field_to_node_input( "end_frame": model_field_to_node_input(
IO.IMAGE, KlingImage2VideoRequest, "image_tail" IO.IMAGE, KlingImage2VideoRequest, "image_tail"
), ),
@ -375,41 +404,18 @@ class KlingImage2VideoNode(KlingNodeBase):
def api_call( def api_call(
self, self,
model_name: str,
start_frame: torch.Tensor,
prompt: str, prompt: str,
negative_prompt: str, negative_prompt: str,
model_name: str,
start_frame: torch.Tensor,
cfg_scale: float, cfg_scale: float,
mode: str, mode: str,
aspect_ratio: str, aspect_ratio: str,
duration: str, duration: str,
camera_control_type: str, camera_control: Optional[CameraControl] = None,
camera_control_horizontal: float,
camera_control_vertical: float,
camera_control_pan: float,
camera_control_roll: float,
camera_control_tilt: float,
camera_control_zoom: float,
end_frame: Optional[torch.Tensor] = None, end_frame: Optional[torch.Tensor] = None,
auth_token: Optional[str] = None, auth_token: Optional[str] = None,
) -> tuple[VideoFromFile]: ) -> tuple[VideoFromFile]:
camera_control = None
if is_camera_control_supported(model_name, duration, mode):
config = None
if camera_control_type != "right_turn_forward":
config = CameraConfig(
horizontal=camera_control_horizontal,
vertical=camera_control_vertical,
pan=camera_control_pan,
roll=camera_control_roll,
tilt=camera_control_tilt,
zoom=camera_control_zoom,
)
camera_control = CameraControl(
type=CameraType(camera_control_type),
config=config if config else None,
)
initial_operation = SynchronousOperation( initial_operation = SynchronousOperation(
endpoint=ApiEndpoint( endpoint=ApiEndpoint(
path=PATH_IMAGE_TO_VIDEO, path=PATH_IMAGE_TO_VIDEO,
@ -442,8 +448,6 @@ class KlingImage2VideoNode(KlingNodeBase):
raise KlingApiError(error_msg) raise KlingApiError(error_msg)
task_id = initial_response.data.task_id task_id = initial_response.data.task_id
logging.debug("Kling task submitted. Task ID: %s", task_id)
final_response = KlingImage2VideoNode.poll_for_task_status(task_id, auth_token) final_response = KlingImage2VideoNode.poll_for_task_status(task_id, auth_token)
if not is_valid_video_response(final_response): if not is_valid_video_response(final_response):
error_msg = ( error_msg = (
@ -459,11 +463,13 @@ class KlingImage2VideoNode(KlingNodeBase):
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"KlingCameraControls": KlingCameraControls,
"KlingTextToVideoNode": KlingTextToVideoNode, "KlingTextToVideoNode": KlingTextToVideoNode,
"KlingImage2VideoNode": KlingImage2VideoNode, "KlingImage2VideoNode": KlingImage2VideoNode,
} }
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
"KlingCameraControls": "Kling Camera Controls",
"KlingTextToVideoNode": "Kling Text to Video", "KlingTextToVideoNode": "Kling Text to Video",
"KlingImage2VideoNode": "Kling Image to Video", "KlingImage2VideoNode": "Kling Image to Video",
} }