From 7046983d9538d49d1dd286a513aa6db42b9a74fd Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Sat, 17 May 2025 03:45:36 +1000 Subject: [PATCH 1/8] Remove Desktop versioning claim from README (#8155) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index dcdaa353c..9b5f301c9 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,6 @@ ComfyUI follows a weekly release cycle every Friday, with three interconnected r 2. **[ComfyUI Desktop](https://github.com/Comfy-Org/desktop)** - Builds a new release using the latest stable core version - - Version numbers match the core release (e.g., Desktop v1.7.0 uses Core v1.7.0) 3. **[ComfyUI Frontend](https://github.com/Comfy-Org/ComfyUI_frontend)** - Weekly frontend updates are merged into the core repository From dc46db7aa48acd035dfc10730a064a9c994fb76b Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Fri, 16 May 2025 12:15:55 -0700 Subject: [PATCH 2/8] Make ImagePadForOutpaint return a 3 channel mask. (#8157) --- nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes.py b/nodes.py index 0a9db1393..95e831b8b 100644 --- a/nodes.py +++ b/nodes.py @@ -1940,7 +1940,7 @@ class ImagePadForOutpaint: mask[top:top + d2, left:left + d3] = t - return (new_image, mask) + return (new_image, mask.unsqueeze(0)) NODE_CLASS_MAPPINGS = { From aee2908d0395577a6e2e13d1307aaf271424108b Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Sat, 17 May 2025 03:27:34 -0700 Subject: [PATCH 3/8] Remove useless log. (#8166) --- comfy/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/comfy/utils.py b/comfy/utils.py index 561e1b858..1f8d71292 100644 --- a/comfy/utils.py +++ b/comfy/utils.py @@ -78,8 +78,6 @@ def load_torch_file(ckpt, safe_load=False, device=None, return_metadata=False): pl_sd = torch.load(ckpt, map_location=device, weights_only=True, **torch_args) else: pl_sd = torch.load(ckpt, map_location=device, pickle_module=comfy.checkpoint_pickle) - if "global_step" in pl_sd: - logging.debug(f"Global Step: {pl_sd['global_step']}") if "state_dict" in pl_sd: sd = pl_sd["state_dict"] else: From f5e4e976f43c9ed79e75b27f73719b2708f9ded9 Mon Sep 17 00:00:00 2001 From: Silver <65376327+silveroxides@users.noreply.github.com> Date: Sun, 18 May 2025 08:59:06 +0200 Subject: [PATCH 4/8] Add missing category for T5TokenizerOption (#8177) Change it if you need to but it should at least have a category. --- comfy_extras/nodes_cond.py | 1 + 1 file changed, 1 insertion(+) diff --git a/comfy_extras/nodes_cond.py b/comfy_extras/nodes_cond.py index 574262178..58c16f621 100644 --- a/comfy_extras/nodes_cond.py +++ b/comfy_extras/nodes_cond.py @@ -31,6 +31,7 @@ class T5TokenizerOptions: } } + CATEGORY = "_for_testing/conditioning" RETURN_TYPES = ("CLIP",) FUNCTION = "set_options" From 05eb10b43a42929033f449def7cd5a8feeb84673 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sun, 18 May 2025 01:08:47 -0700 Subject: [PATCH 5/8] Validate video inputs (#8133) * validate kling lip sync input video * add tooltips * update duration estimates * decrease epsilon * fix rebase error --- comfy_api_nodes/nodes_kling.py | 51 ++++++------ comfy_api_nodes/util/__init__.py | 0 comfy_api_nodes/util/validation_utils.py | 100 +++++++++++++++++++++++ 3 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 comfy_api_nodes/util/__init__.py create mode 100644 comfy_api_nodes/util/validation_utils.py diff --git a/comfy_api_nodes/nodes_kling.py b/comfy_api_nodes/nodes_kling.py index 456a86905..641cd6353 100644 --- a/comfy_api_nodes/nodes_kling.py +++ b/comfy_api_nodes/nodes_kling.py @@ -65,6 +65,12 @@ from comfy_api_nodes.apinode_utils import ( download_url_to_image_tensor, ) from comfy_api_nodes.mapper_utils import model_field_to_node_input +from comfy_api_nodes.util.validation_utils import ( + validate_image_dimensions, + validate_image_aspect_ratio, + validate_video_dimensions, + validate_video_duration, +) from comfy_api.input.basic_types import AudioInput from comfy_api.input.video_types import VideoInput from comfy_api.input_impl import VideoFromFile @@ -80,18 +86,16 @@ 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 MAX_PROMPT_LENGTH_I2V = 500 MAX_PROMPT_LENGTH_IMAGE_GEN = 500 MAX_NEGATIVE_PROMPT_LENGTH_IMAGE_GEN = 200 MAX_PROMPT_LENGTH_LIP_SYNC = 120 -# TODO: adjust based on tests -AVERAGE_DURATION_T2V = 319 # 319, -AVERAGE_DURATION_I2V = 164 # 164, -AVERAGE_DURATION_LIP_SYNC = 120 -AVERAGE_DURATION_VIRTUAL_TRY_ON = 19 # 19, +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 @@ -211,23 +215,8 @@ def validate_input_image(image: torch.Tensor) -> None: See: https://app.klingai.com/global/dev/document-api/apiReference/model/imageToVideo """ - if len(image.shape) == 4: - height, width = image.shape[1], image.shape[2] - elif len(image.shape) == 3: - height, width = image.shape[0], image.shape[1] - else: - raise ValueError("Invalid image tensor shape.") - - # Ensure minimum resolution is met - if height < 300: - raise ValueError("Image height must be at least 300px") - if width < 300: - raise ValueError("Image width must be at least 300px") - - # Ensure aspect ratio is within acceptable range - aspect_ratio = width / height - if aspect_ratio < 1 / 2.5 or aspect_ratio > 2.5: - raise ValueError("Image aspect ratio must be between 1:2.5 and 2.5:1") + validate_image_dimensions(image, min_width=300, min_height=300) + validate_image_aspect_ratio(image, min_aspect_ratio=1 / 2.5, max_aspect_ratio=2.5) def get_camera_control_input_config( @@ -1243,6 +1232,17 @@ class KlingLipSyncBase(KlingNodeBase): RETURN_TYPES = ("VIDEO", "STRING", "STRING") RETURN_NAMES = ("VIDEO", "video_id", "duration") + def validate_lip_sync_video(self, video: VideoInput): + """ + Validates the input video adheres to the expectations of the Kling Lip Sync API: + - Video length does not exceed 10s and is not shorter than 2s + - Length and width dimensions should both be between 720px and 1920px + + See: https://app.klingai.com/global/dev/document-api/apiReference/model/videoTolip + """ + validate_video_dimensions(video, 720, 1920) + validate_video_duration(video, 2, 10) + def validate_text(self, text: str): if not text: raise ValueError("Text is required") @@ -1282,6 +1282,7 @@ class KlingLipSyncBase(KlingNodeBase): ) -> tuple[VideoFromFile, str, str]: if text: self.validate_text(text) + self.validate_lip_sync_video(video) # Upload video to Comfy API and get download URL video_url = upload_video_to_comfyapi(video, auth_kwargs=kwargs) @@ -1352,7 +1353,7 @@ class KlingLipSyncAudioToVideoNode(KlingLipSyncBase): }, } - DESCRIPTION = "Kling Lip Sync Audio to Video Node. Syncs mouth movements in a video file to the audio content of an audio file." + DESCRIPTION = "Kling Lip Sync Audio to Video Node. Syncs mouth movements in a video file to the audio content of an audio file. When using, ensure that the audio contains clearly distinguishable vocals and that the video contains a distinct face. The audio file should not be larger than 5MB. The video file should not be larger than 100MB, should have height/width between 720px and 1920px, and should be between 2s and 10s in length." def api_call( self, @@ -1464,7 +1465,7 @@ class KlingLipSyncTextToVideoNode(KlingLipSyncBase): }, } - DESCRIPTION = "Kling Lip Sync Text to Video Node. Syncs mouth movements in a video file to a text prompt." + DESCRIPTION = "Kling Lip Sync Text to Video Node. Syncs mouth movements in a video file to a text prompt. The video file should not be larger than 100MB, should have height/width between 720px and 1920px, and should be between 2s and 10s in length." def api_call( self, diff --git a/comfy_api_nodes/util/__init__.py b/comfy_api_nodes/util/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/comfy_api_nodes/util/validation_utils.py b/comfy_api_nodes/util/validation_utils.py new file mode 100644 index 000000000..031b9fbd3 --- /dev/null +++ b/comfy_api_nodes/util/validation_utils.py @@ -0,0 +1,100 @@ +import logging +from typing import Optional + +import torch +from comfy_api.input.video_types import VideoInput + + +def get_image_dimensions(image: torch.Tensor) -> tuple[int, int]: + if len(image.shape) == 4: + return image.shape[1], image.shape[2] + elif len(image.shape) == 3: + return image.shape[0], image.shape[1] + else: + raise ValueError("Invalid image tensor shape.") + + +def validate_image_dimensions( + image: torch.Tensor, + min_width: Optional[int] = None, + max_width: Optional[int] = None, + min_height: Optional[int] = None, + max_height: Optional[int] = None, +): + height, width = get_image_dimensions(image) + + if min_width is not None and width < min_width: + raise ValueError(f"Image width must be at least {min_width}px, got {width}px") + if max_width is not None and width > max_width: + raise ValueError(f"Image width must be at most {max_width}px, got {width}px") + if min_height is not None and height < min_height: + raise ValueError( + f"Image height must be at least {min_height}px, got {height}px" + ) + if max_height is not None and height > max_height: + raise ValueError(f"Image height must be at most {max_height}px, got {height}px") + + +def validate_image_aspect_ratio( + image: torch.Tensor, + min_aspect_ratio: Optional[float] = None, + max_aspect_ratio: Optional[float] = None, +): + width, height = get_image_dimensions(image) + aspect_ratio = width / height + + if min_aspect_ratio is not None and aspect_ratio < min_aspect_ratio: + raise ValueError( + f"Image aspect ratio must be at least {min_aspect_ratio}, got {aspect_ratio}" + ) + if max_aspect_ratio is not None and aspect_ratio > max_aspect_ratio: + raise ValueError( + f"Image aspect ratio must be at most {max_aspect_ratio}, got {aspect_ratio}" + ) + + +def validate_video_dimensions( + video: VideoInput, + min_width: Optional[int] = None, + max_width: Optional[int] = None, + min_height: Optional[int] = None, + max_height: Optional[int] = None, +): + try: + width, height = video.get_dimensions() + except Exception as e: + logging.error("Error getting dimensions of video: %s", e) + return + + if min_width is not None and width < min_width: + raise ValueError(f"Video width must be at least {min_width}px, got {width}px") + if max_width is not None and width > max_width: + raise ValueError(f"Video width must be at most {max_width}px, got {width}px") + if min_height is not None and height < min_height: + raise ValueError( + f"Video height must be at least {min_height}px, got {height}px" + ) + if max_height is not None and height > max_height: + raise ValueError(f"Video height must be at most {max_height}px, got {height}px") + + +def validate_video_duration( + video: VideoInput, + min_duration: Optional[float] = None, + max_duration: Optional[float] = None, +): + try: + duration = video.get_duration() + except Exception as e: + logging.error("Error getting duration of video: %s", e) + return + + epsilon = 0.0001 + if min_duration is not None and min_duration - epsilon > duration: + raise ValueError( + f"Video duration must be at least {min_duration}s, got {duration}s" + ) + if max_duration is not None and duration > max_duration + epsilon: + raise ValueError( + f"Video duration must be at most {max_duration}s, got {duration}s" + ) From 62690eddec9b7d715b4c37246f71abf5ca1c5844 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Sun, 18 May 2025 01:09:56 -0700 Subject: [PATCH 6/8] Node to add pixel space noise to an image. (#8182) --- comfy_extras/nodes_images.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/comfy_extras/nodes_images.py b/comfy_extras/nodes_images.py index 77c305619..29a5d5b61 100644 --- a/comfy_extras/nodes_images.py +++ b/comfy_extras/nodes_images.py @@ -13,6 +13,7 @@ import os import re from io import BytesIO from inspect import cleandoc +import torch from comfy.comfy_types import FileLocator @@ -74,6 +75,24 @@ class ImageFromBatch: s = s_in[batch_index:batch_index + length].clone() return (s,) + +class ImageAddNoise: + @classmethod + def INPUT_TYPES(s): + return {"required": { "image": ("IMAGE",), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True, "tooltip": "The random seed used for creating the noise."}), + "strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + }} + RETURN_TYPES = ("IMAGE",) + FUNCTION = "repeat" + + CATEGORY = "image" + + def repeat(self, image, seed, strength): + generator = torch.manual_seed(seed) + s = torch.clip((image + strength * torch.randn(image.size(), generator=generator, device="cpu").to(image)), min=0.0, max=1.0) + return (s,) + class SaveAnimatedWEBP: def __init__(self): self.output_dir = folder_paths.get_output_directory() @@ -295,6 +314,7 @@ NODE_CLASS_MAPPINGS = { "ImageCrop": ImageCrop, "RepeatImageBatch": RepeatImageBatch, "ImageFromBatch": ImageFromBatch, + "ImageAddNoise": ImageAddNoise, "SaveAnimatedWEBP": SaveAnimatedWEBP, "SaveAnimatedPNG": SaveAnimatedPNG, "SaveSVGNode": SaveSVGNode, From 3d44a09812c4f0880c30fcd1876125b7319300b4 Mon Sep 17 00:00:00 2001 From: LaVie024 <62406970+LaVie024@users.noreply.github.com> Date: Sun, 18 May 2025 08:11:11 +0000 Subject: [PATCH 7/8] Update nodes_string.py (#8173) --- comfy_extras/nodes_string.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/comfy_extras/nodes_string.py b/comfy_extras/nodes_string.py index a852326e5..9eaa71236 100644 --- a/comfy_extras/nodes_string.py +++ b/comfy_extras/nodes_string.py @@ -8,7 +8,8 @@ class StringConcatenate(): return { "required": { "string_a": (IO.STRING, {"multiline": True}), - "string_b": (IO.STRING, {"multiline": True}) + "string_b": (IO.STRING, {"multiline": True}), + "delimiter": (IO.STRING, {"multiline": False, "default": ", "}) } } @@ -16,8 +17,8 @@ class StringConcatenate(): FUNCTION = "execute" CATEGORY = "utils/string" - def execute(self, string_a, string_b, **kwargs): - return string_a + string_b, + def execute(self, string_a, string_b, delimiter, **kwargs): + return delimiter.join((string_a, string_b)), class StringSubstring(): @classmethod From d8e5662822168101afb5e08a8ba75b6eefff6e02 Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Sun, 18 May 2025 01:12:12 -0700 Subject: [PATCH 8/8] Remove default delimiter. (#8183) --- comfy_extras/nodes_string.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy_extras/nodes_string.py b/comfy_extras/nodes_string.py index 9eaa71236..b24222cee 100644 --- a/comfy_extras/nodes_string.py +++ b/comfy_extras/nodes_string.py @@ -9,7 +9,7 @@ class StringConcatenate(): "required": { "string_a": (IO.STRING, {"multiline": True}), "string_b": (IO.STRING, {"multiline": True}), - "delimiter": (IO.STRING, {"multiline": False, "default": ", "}) + "delimiter": (IO.STRING, {"multiline": False, "default": ""}) } }