From 2bc4b5968f7fbf0b6e65f2465b064c6af48f965a Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Sun, 9 Mar 2025 03:30:20 -0400 Subject: [PATCH 01/25] ComfyUI version v0.3.25 --- comfyui_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/comfyui_version.py b/comfyui_version.py index a68a65323..9cf4c13fa 100644 --- a/comfyui_version.py +++ b/comfyui_version.py @@ -1,3 +1,3 @@ # This file is automatically generated by the build process when version is # updated in pyproject.toml. -__version__ = "0.3.24" +__version__ = "0.3.25" diff --git a/pyproject.toml b/pyproject.toml index 4c11c71bb..3b53d1492 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.3.24" +version = "0.3.25" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" From 528d1b35638ad4a5d08b8584f7bacb19afe785cc Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Sun, 9 Mar 2025 03:26:31 -0500 Subject: [PATCH 02/25] When cached_hook_patches contain weights for hooks, only use hook_backup for unused keys (#7067) --- comfy/model_patcher.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/comfy/model_patcher.py b/comfy/model_patcher.py index 8a1f8fb63..e291158ce 100644 --- a/comfy/model_patcher.py +++ b/comfy/model_patcher.py @@ -1089,7 +1089,6 @@ class ModelPatcher: def patch_hooks(self, hooks: comfy.hooks.HookGroup): with self.use_ejected(): - self.unpatch_hooks() if hooks is not None: model_sd_keys = list(self.model_state_dict().keys()) memory_counter = None @@ -1100,12 +1099,16 @@ class ModelPatcher: # if have cached weights for hooks, use it cached_weights = self.cached_hook_patches.get(hooks, None) if cached_weights is not None: + model_sd_keys_set = set(model_sd_keys) for key in cached_weights: if key not in model_sd_keys: logging.warning(f"Cached hook could not patch. Key does not exist in model: {key}") continue self.patch_cached_hook_weights(cached_weights=cached_weights, key=key, memory_counter=memory_counter) + model_sd_keys_set.remove(key) + self.unpatch_hooks(model_sd_keys_set) else: + self.unpatch_hooks() relevant_patches = self.get_combined_hook_patches(hooks=hooks) original_weights = None if len(relevant_patches) > 0: @@ -1116,6 +1119,8 @@ class ModelPatcher: continue self.patch_hook_weight_to_device(hooks=hooks, combined_patches=relevant_patches, key=key, original_weights=original_weights, memory_counter=memory_counter) + else: + self.unpatch_hooks() self.current_hooks = hooks def patch_cached_hook_weights(self, cached_weights: dict, key: str, memory_counter: MemoryCounter): @@ -1172,17 +1177,23 @@ class ModelPatcher: del out_weight del weight - def unpatch_hooks(self) -> None: + def unpatch_hooks(self, whitelist_keys_set: set[str]=None) -> None: with self.use_ejected(): if len(self.hook_backup) == 0: self.current_hooks = None return keys = list(self.hook_backup.keys()) - for k in keys: - comfy.utils.copy_to_param(self.model, k, self.hook_backup[k][0].to(device=self.hook_backup[k][1])) + if whitelist_keys_set: + for k in keys: + if k in whitelist_keys_set: + comfy.utils.copy_to_param(self.model, k, self.hook_backup[k][0].to(device=self.hook_backup[k][1])) + self.hook_backup.pop(k) + else: + for k in keys: + comfy.utils.copy_to_param(self.model, k, self.hook_backup[k][0].to(device=self.hook_backup[k][1])) - self.hook_backup.clear() - self.current_hooks = None + self.hook_backup.clear() + self.current_hooks = None def clean_hooks(self): self.unpatch_hooks() From 9aac21f894a122ddb8d825c57ad61c0db5e630db Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Sun, 9 Mar 2025 04:59:15 -0400 Subject: [PATCH 03/25] Fix issues with new hunyuan img2vid model and bumb version to v0.3.26 --- comfy/ldm/flux/layers.py | 14 +++++++------- comfy/ldm/hunyuan_video/model.py | 12 +++++++----- comfyui_version.py | 2 +- pyproject.toml | 2 +- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/comfy/ldm/flux/layers.py b/comfy/ldm/flux/layers.py index 1b3e9f313..76af967e6 100644 --- a/comfy/ldm/flux/layers.py +++ b/comfy/ldm/flux/layers.py @@ -159,20 +159,20 @@ class DoubleStreamBlock(nn.Module): ) self.flipped_img_txt = flipped_img_txt - def forward(self, img: Tensor, txt: Tensor, vec: Tensor, pe: Tensor, attn_mask=None, modulation_dims=None): + def forward(self, img: Tensor, txt: Tensor, vec: Tensor, pe: Tensor, attn_mask=None, modulation_dims_img=None, modulation_dims_txt=None): img_mod1, img_mod2 = self.img_mod(vec) txt_mod1, txt_mod2 = self.txt_mod(vec) # prepare image for attention img_modulated = self.img_norm1(img) - img_modulated = apply_mod(img_modulated, (1 + img_mod1.scale), img_mod1.shift, modulation_dims) + img_modulated = apply_mod(img_modulated, (1 + img_mod1.scale), img_mod1.shift, modulation_dims_img) img_qkv = self.img_attn.qkv(img_modulated) img_q, img_k, img_v = img_qkv.view(img_qkv.shape[0], img_qkv.shape[1], 3, self.num_heads, -1).permute(2, 0, 3, 1, 4) img_q, img_k = self.img_attn.norm(img_q, img_k, img_v) # prepare txt for attention txt_modulated = self.txt_norm1(txt) - txt_modulated = apply_mod(txt_modulated, (1 + txt_mod1.scale), txt_mod1.shift, modulation_dims) + txt_modulated = apply_mod(txt_modulated, (1 + txt_mod1.scale), txt_mod1.shift, modulation_dims_txt) txt_qkv = self.txt_attn.qkv(txt_modulated) txt_q, txt_k, txt_v = txt_qkv.view(txt_qkv.shape[0], txt_qkv.shape[1], 3, self.num_heads, -1).permute(2, 0, 3, 1, 4) txt_q, txt_k = self.txt_attn.norm(txt_q, txt_k, txt_v) @@ -195,12 +195,12 @@ class DoubleStreamBlock(nn.Module): txt_attn, img_attn = attn[:, : txt.shape[1]], attn[:, txt.shape[1]:] # calculate the img bloks - img = img + apply_mod(self.img_attn.proj(img_attn), img_mod1.gate, None, modulation_dims) - img = img + apply_mod(self.img_mlp(apply_mod(self.img_norm2(img), (1 + img_mod2.scale), img_mod2.shift, modulation_dims)), img_mod2.gate, None, modulation_dims) + img = img + apply_mod(self.img_attn.proj(img_attn), img_mod1.gate, None, modulation_dims_img) + img = img + apply_mod(self.img_mlp(apply_mod(self.img_norm2(img), (1 + img_mod2.scale), img_mod2.shift, modulation_dims_img)), img_mod2.gate, None, modulation_dims_img) # calculate the txt bloks - txt += apply_mod(self.txt_attn.proj(txt_attn), txt_mod1.gate, None, modulation_dims) - txt += apply_mod(self.txt_mlp(apply_mod(self.txt_norm2(txt), (1 + txt_mod2.scale), txt_mod2.shift, modulation_dims)), txt_mod2.gate, None, modulation_dims) + txt += apply_mod(self.txt_attn.proj(txt_attn), txt_mod1.gate, None, modulation_dims_txt) + txt += apply_mod(self.txt_mlp(apply_mod(self.txt_norm2(txt), (1 + txt_mod2.scale), txt_mod2.shift, modulation_dims_txt)), txt_mod2.gate, None, modulation_dims_txt) if txt.dtype == torch.float16: txt = torch.nan_to_num(txt, nan=0.0, posinf=65504, neginf=-65504) diff --git a/comfy/ldm/hunyuan_video/model.py b/comfy/ldm/hunyuan_video/model.py index 001e302b5..72af3d5bb 100644 --- a/comfy/ldm/hunyuan_video/model.py +++ b/comfy/ldm/hunyuan_video/model.py @@ -244,9 +244,11 @@ class HunyuanVideo(nn.Module): vec = torch.cat([(vec_ + token_replace_vec).unsqueeze(1), (vec_ + vec).unsqueeze(1)], dim=1) frame_tokens = (initial_shape[-1] // self.patch_size[-1]) * (initial_shape[-2] // self.patch_size[-2]) modulation_dims = [(0, frame_tokens, 0), (frame_tokens, None, 1)] + modulation_dims_txt = [(0, None, 1)] else: vec = vec + self.vector_in(y[:, :self.params.vec_in_dim]) modulation_dims = None + modulation_dims_txt = None if self.params.guidance_embed: if guidance is not None: @@ -273,14 +275,14 @@ class HunyuanVideo(nn.Module): if ("double_block", i) in blocks_replace: def block_wrap(args): out = {} - out["img"], out["txt"] = block(img=args["img"], txt=args["txt"], vec=args["vec"], pe=args["pe"], attn_mask=args["attention_mask"]) + out["img"], out["txt"] = block(img=args["img"], txt=args["txt"], vec=args["vec"], pe=args["pe"], attn_mask=args["attention_mask"], modulation_dims_img=args["modulation_dims_img"], modulation_dims_txt=args["modulation_dims_txt"]) return out - out = blocks_replace[("double_block", i)]({"img": img, "txt": txt, "vec": vec, "pe": pe, "attention_mask": attn_mask}, {"original_block": block_wrap}) + out = blocks_replace[("double_block", i)]({"img": img, "txt": txt, "vec": vec, "pe": pe, "attention_mask": attn_mask, 'modulation_dims_img': modulation_dims, 'modulation_dims_txt': modulation_dims_txt}, {"original_block": block_wrap}) txt = out["txt"] img = out["img"] else: - img, txt = block(img=img, txt=txt, vec=vec, pe=pe, attn_mask=attn_mask, modulation_dims=modulation_dims) + img, txt = block(img=img, txt=txt, vec=vec, pe=pe, attn_mask=attn_mask, modulation_dims_img=modulation_dims, modulation_dims_txt=modulation_dims_txt) if control is not None: # Controlnet control_i = control.get("input") @@ -295,10 +297,10 @@ class HunyuanVideo(nn.Module): if ("single_block", i) in blocks_replace: def block_wrap(args): out = {} - out["img"] = block(args["img"], vec=args["vec"], pe=args["pe"], attn_mask=args["attention_mask"]) + out["img"] = block(args["img"], vec=args["vec"], pe=args["pe"], attn_mask=args["attention_mask"], modulation_dims=args["modulation_dims"]) return out - out = blocks_replace[("single_block", i)]({"img": img, "vec": vec, "pe": pe, "attention_mask": attn_mask}, {"original_block": block_wrap}) + out = blocks_replace[("single_block", i)]({"img": img, "vec": vec, "pe": pe, "attention_mask": attn_mask, 'modulation_dims': modulation_dims}, {"original_block": block_wrap}) img = out["img"] else: img = block(img, vec=vec, pe=pe, attn_mask=attn_mask, modulation_dims=modulation_dims) diff --git a/comfyui_version.py b/comfyui_version.py index 9cf4c13fa..b5e6fbead 100644 --- a/comfyui_version.py +++ b/comfyui_version.py @@ -1,3 +1,3 @@ # This file is automatically generated by the build process when version is # updated in pyproject.toml. -__version__ = "0.3.25" +__version__ = "0.3.26" diff --git a/pyproject.toml b/pyproject.toml index 3b53d1492..f13fed8dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.3.25" +version = "0.3.26" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" From a73410aafa573940ebaba9a9a908476a538a8981 Mon Sep 17 00:00:00 2001 From: bymyself Date: Sun, 9 Mar 2025 03:46:08 -0700 Subject: [PATCH 04/25] remove overrides --- nodes.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/nodes.py b/nodes.py index bbf49915c..e43c29295 100644 --- a/nodes.py +++ b/nodes.py @@ -1785,14 +1785,7 @@ class LoadImageOutput(LoadImage): DESCRIPTION = "Load an image from the output folder. When the refresh button is clicked, the node will update the image list and automatically select the first image, allowing for easy iteration." EXPERIMENTAL = True - FUNCTION = "load_image_output" - - def load_image_output(self, image): - return self.load_image(f"{image} [output]") - - @classmethod - def VALIDATE_INPUTS(s, image): - return True + FUNCTION = "load_image" class ImageScale: From e1da98a14a21f5d4af31935832437b55e81d2399 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Sun, 9 Mar 2025 14:07:09 -0400 Subject: [PATCH 05/25] remove unused params (#6931) --- comfy_extras/nodes_load_3d.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/comfy_extras/nodes_load_3d.py b/comfy_extras/nodes_load_3d.py index 53a66b95a..8b43cf218 100644 --- a/comfy_extras/nodes_load_3d.py +++ b/comfy_extras/nodes_load_3d.py @@ -19,8 +19,6 @@ class Load3D(): "image": ("LOAD_3D", {}), "width": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), "height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), - "material": (["original", "normal", "wireframe", "depth"],), - "up_direction": (["original", "-x", "+x", "-y", "+y", "-z", "+z"],), }} RETURN_TYPES = ("IMAGE", "MASK", "STRING") @@ -55,8 +53,6 @@ class Load3DAnimation(): "image": ("LOAD_3D_ANIMATION", {}), "width": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), "height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), - "material": (["original", "normal", "wireframe", "depth"],), - "up_direction": (["original", "-x", "+x", "-y", "+y", "-z", "+z"],), }} RETURN_TYPES = ("IMAGE", "MASK", "STRING") @@ -82,8 +78,6 @@ class Preview3D(): def INPUT_TYPES(s): return {"required": { "model_file": ("STRING", {"default": "", "multiline": False}), - "material": (["original", "normal", "wireframe", "depth"],), - "up_direction": (["original", "-x", "+x", "-y", "+y", "-z", "+z"],), }} OUTPUT_NODE = True @@ -102,8 +96,6 @@ class Preview3DAnimation(): def INPUT_TYPES(s): return {"required": { "model_file": ("STRING", {"default": "", "multiline": False}), - "material": (["original", "normal", "wireframe", "depth"],), - "up_direction": (["original", "-x", "+x", "-y", "+y", "-z", "+z"],), }} OUTPUT_NODE = True From 6f8e766509c0c44ae2e04a79ab05a06e4467b51b Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Mon, 10 Mar 2025 03:33:17 -0400 Subject: [PATCH 06/25] Prevent custom nodes from accidentally overwriting global modules. --- nodes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nodes.py b/nodes.py index bbf49915c..43697a24d 100644 --- a/nodes.py +++ b/nodes.py @@ -2129,10 +2129,12 @@ def get_module_name(module_path: str) -> str: def load_custom_node(module_path: str, ignore=set(), module_parent="custom_nodes") -> bool: - module_name = os.path.basename(module_path) if os.path.isfile(module_path): sp = os.path.splitext(module_path) module_name = sp[0] + elif os.path.isdir(module_path): + module_name = module_path + try: logging.debug("Trying to load custom node {}".format(module_path)) if os.path.isfile(module_path): From 67c7184b7432105d2db52cc19fc82ccd4aa06fb3 Mon Sep 17 00:00:00 2001 From: Andrew Kvochko Date: Mon, 10 Mar 2025 10:11:48 +0200 Subject: [PATCH 07/25] ltxv: relax frame_idx divisibility for single frames. (#7146) This commit relaxes divisibility constraint for single-frame conditionings. For single frames, the index can be arbitrary, while multi-frame conditionings (>= 9 frames) must still be aligned to 8 frames. Co-authored-by: Andrew Kvochko --- comfy_extras/nodes_lt.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/comfy_extras/nodes_lt.py b/comfy_extras/nodes_lt.py index b608b9407..fdc6c7c13 100644 --- a/comfy_extras/nodes_lt.py +++ b/comfy_extras/nodes_lt.py @@ -99,12 +99,13 @@ class LTXVAddGuide: "negative": ("CONDITIONING", ), "vae": ("VAE",), "latent": ("LATENT",), - "image": ("IMAGE", {"tooltip": "Image or video to condition the latent video on. Must be 8*n + 1 frames." \ + "image": ("IMAGE", {"tooltip": "Image or video to condition the latent video on. Must be 8*n + 1 frames." "If the video is not 8*n + 1 frames, it will be cropped to the nearest 8*n + 1 frames."}), "frame_idx": ("INT", {"default": 0, "min": -9999, "max": 9999, - "tooltip": "Frame index to start the conditioning at. Must be divisible by 8. " \ - "If a frame is not divisible by 8, it will be rounded down to the nearest multiple of 8. " \ - "Negative values are counted from the end of the video."}), + "tooltip": "Frame index to start the conditioning at. For single-frame images or " + "videos with 1-8 frames, any frame_idx value is acceptable. For videos with 9+ " + "frames, frame_idx must be divisible by 8, otherwise it will be rounded down to " + "the nearest multiple of 8. Negative values are counted from the end of the video."}), "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), } } @@ -127,12 +128,13 @@ class LTXVAddGuide: t = vae.encode(encode_pixels) return encode_pixels, t - def get_latent_index(self, cond, latent_length, frame_idx, scale_factors): + def get_latent_index(self, cond, latent_length, guide_length, frame_idx, scale_factors): time_scale_factor, _, _ = scale_factors _, num_keyframes = get_keyframe_idxs(cond) latent_count = latent_length - num_keyframes - frame_idx = frame_idx if frame_idx >= 0 else max((latent_count - 1) * 8 + 1 + frame_idx, 0) - frame_idx = frame_idx // time_scale_factor * time_scale_factor # frame index must be divisible by 8 + frame_idx = frame_idx if frame_idx >= 0 else max((latent_count - 1) * time_scale_factor + 1 + frame_idx, 0) + if guide_length > 1: + frame_idx = frame_idx // time_scale_factor * time_scale_factor # frame index must be divisible by 8 latent_idx = (frame_idx + time_scale_factor - 1) // time_scale_factor @@ -191,7 +193,7 @@ class LTXVAddGuide: _, _, latent_length, latent_height, latent_width = latent_image.shape image, t = self.encode(vae, latent_width, latent_height, image, scale_factors) - frame_idx, latent_idx = self.get_latent_index(positive, latent_length, frame_idx, scale_factors) + frame_idx, latent_idx = self.get_latent_index(positive, latent_length, len(image), frame_idx, scale_factors) assert latent_idx + t.shape[2] <= latent_length, "Conditioning frames exceed the length of the latent sequence." num_prefix_frames = min(self._num_prefix_frames, t.shape[2]) From 35e2dcf5d710f258f40f107f70f24a4cd58ba223 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Mon, 10 Mar 2025 06:14:43 -0400 Subject: [PATCH 08/25] Hack to fix broken manager. --- nodes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nodes.py b/nodes.py index 43697a24d..4608a0d36 100644 --- a/nodes.py +++ b/nodes.py @@ -2134,6 +2134,8 @@ def load_custom_node(module_path: str, ignore=set(), module_parent="custom_nodes module_name = sp[0] elif os.path.isdir(module_path): module_name = module_path + if module_path.endswith("comfyui-manager"): #TODO: remove this eventually + module_name = get_module_name(module_path) try: logging.debug("Trying to load custom node {}".format(module_path)) From b779349b55e79aff81a98b752f5cb486c71812db Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Mon, 10 Mar 2025 06:30:17 -0400 Subject: [PATCH 09/25] Temporarily revert fix to give time for people to update their nodes. --- nodes.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nodes.py b/nodes.py index 4608a0d36..bbf49915c 100644 --- a/nodes.py +++ b/nodes.py @@ -2129,14 +2129,10 @@ def get_module_name(module_path: str) -> str: def load_custom_node(module_path: str, ignore=set(), module_parent="custom_nodes") -> bool: + module_name = os.path.basename(module_path) if os.path.isfile(module_path): sp = os.path.splitext(module_path) module_name = sp[0] - elif os.path.isdir(module_path): - module_name = module_path - if module_path.endswith("comfyui-manager"): #TODO: remove this eventually - module_name = get_module_name(module_path) - try: logging.debug("Trying to load custom node {}".format(module_path)) if os.path.isfile(module_path): From 1f138dd382bd4fe40c46a1fd1954dfbf0ddae924 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Mon, 10 Mar 2025 15:07:44 -0400 Subject: [PATCH 10/25] Only check frontend package if using default frontend --- app/frontend_management.py | 51 +++++++++++++++++++++++++++----------- main.py | 15 ----------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/app/frontend_management.py b/app/frontend_management.py index 308f71da6..f5a0358e6 100644 --- a/app/frontend_management.py +++ b/app/frontend_management.py @@ -17,27 +17,38 @@ from typing_extensions import NotRequired from comfy.cli_args import DEFAULT_VERSION_STRING +# The path to the requirements.txt file +req_path = Path(__file__).parents[1] / "requirements.txt" def frontend_install_warning_message(): - req_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'requirements.txt')) + """The warning message to display when the frontend version is not up to date.""" + extra = "" if sys.flags.no_user_site: extra = "-s " return f"Please install the updated requirements.txt file by running:\n{sys.executable} {extra}-m pip install -r {req_path}\n\nThis error is happening because the ComfyUI frontend is no longer shipped as part of the main repo but as a pip package instead.\n\nIf you are on the portable package you can run: update\\update_comfyui.bat to solve this problem" -try: - import comfyui_frontend_package -except ImportError: - # TODO: Remove the check after roll out of 0.3.16 - logging.error(f"\n\n********** ERROR ***********\n\ncomfyui-frontend-package is not installed. {frontend_install_warning_message()}\n********** ERROR **********\n") - exit(-1) +def check_frontend_version(): + """Check if the frontend version is up to date.""" + + def parse_version(version: str) -> tuple[int, int, int]: + return tuple(map(int, version.split("."))) + + try: + import comfyui_frontend_package + + frontend_version = parse_version(comfyui_frontend_package.__version__) + required_frontend = parse_version((0,)) + with open(req_path, 'r', encoding='utf-8') as f: + required_frontend = parse_version(f.readline().split('=')[-1]) + if frontend_version < required_frontend: + logging.warning("________________________________________________________________________\nWARNING WARNING WARNING WARNING WARNING\n\nInstalled frontend version {} is lower than the recommended version {}.\n\n{}\n________________________________________________________________________".format('.'.join(map(str, frontend_version)), '.'.join(map(str, required_frontend)), frontend_install_warning_message())) + else: + logging.info("ComfyUI frontend version: {}".format(comfyui_frontend_package.__version__)) + except Exception as e: + logging.error(f"Failed to check frontend version: {e}") -try: - frontend_version = tuple(map(int, comfyui_frontend_package.__version__.split("."))) -except: - frontend_version = (0,) - pass REQUEST_TIMEOUT = 10 # seconds @@ -133,9 +144,17 @@ def download_release_asset_zip(release: Release, destination_path: str) -> None: class FrontendManager: - DEFAULT_FRONTEND_PATH = str(importlib.resources.files(comfyui_frontend_package) / "static") CUSTOM_FRONTENDS_ROOT = str(Path(__file__).parents[1] / "web_custom_versions") + @classmethod + def default_frontend_path(cls) -> str: + try: + import comfyui_frontend_package + return str(importlib.resources.files(comfyui_frontend_package) / "static") + except ImportError: + logging.error(f"\n\n********** ERROR ***********\n\ncomfyui-frontend-package is not installed. {frontend_install_warning_message()}\n********** ERROR **********\n") + sys.exit(-1) + @classmethod def parse_version_string(cls, value: str) -> tuple[str, str, str]: """ @@ -172,7 +191,8 @@ class FrontendManager: main error source might be request timeout or invalid URL. """ if version_string == DEFAULT_VERSION_STRING: - return cls.DEFAULT_FRONTEND_PATH + check_frontend_version() + return cls.default_frontend_path() repo_owner, repo_name, version = cls.parse_version_string(version_string) @@ -225,4 +245,5 @@ class FrontendManager: except Exception as e: logging.error("Failed to initialize frontend: %s", e) logging.info("Falling back to the default frontend.") - return cls.DEFAULT_FRONTEND_PATH + check_frontend_version() + return cls.default_frontend_path() diff --git a/main.py b/main.py index 6fa1cfb0f..dbc15b8ba 100644 --- a/main.py +++ b/main.py @@ -293,28 +293,13 @@ def start_comfyui(asyncio_loop=None): return asyncio_loop, prompt_server, start_all -def warn_frontend_version(frontend_version): - try: - required_frontend = (0,) - req_path = os.path.join(os.path.dirname(__file__), 'requirements.txt') - with open(req_path, 'r') as f: - required_frontend = tuple(map(int, f.readline().split('=')[-1].split('.'))) - if frontend_version < required_frontend: - logging.warning("________________________________________________________________________\nWARNING WARNING WARNING WARNING WARNING\n\nInstalled frontend version {} is lower than the recommended version {}.\n\n{}\n________________________________________________________________________".format('.'.join(map(str, frontend_version)), '.'.join(map(str, required_frontend)), app.frontend_management.frontend_install_warning_message())) - except: - pass - - if __name__ == "__main__": # Running directly, just start ComfyUI. logging.info("ComfyUI version: {}".format(comfyui_version.__version__)) - frontend_version = app.frontend_management.frontend_version - logging.info("ComfyUI frontend version: {}".format('.'.join(map(str, frontend_version)))) event_loop, _, start_all_func = start_comfyui() try: x = start_all_func() - warn_frontend_version(frontend_version) event_loop.run_until_complete(x) except KeyboardInterrupt: logging.info("\nStopped server") From 6f6349b6a76fde39ab65d4952aa0aee7d2eade15 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Mon, 10 Mar 2025 15:10:40 -0400 Subject: [PATCH 11/25] nit --- main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/main.py b/main.py index dbc15b8ba..c6f5c3c1e 100644 --- a/main.py +++ b/main.py @@ -139,7 +139,6 @@ from server import BinaryEventTypes import nodes import comfy.model_management import comfyui_version -import app.frontend_management def cuda_malloc_warning(): From 79460497941d090bc197898dc6e9c5e4feaf0c1d Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Mon, 10 Mar 2025 15:14:40 -0400 Subject: [PATCH 12/25] nit --- app/frontend_management.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/frontend_management.py b/app/frontend_management.py index f5a0358e6..95df5dee4 100644 --- a/app/frontend_management.py +++ b/app/frontend_management.py @@ -39,9 +39,8 @@ def check_frontend_version(): import comfyui_frontend_package frontend_version = parse_version(comfyui_frontend_package.__version__) - required_frontend = parse_version((0,)) - with open(req_path, 'r', encoding='utf-8') as f: - required_frontend = parse_version(f.readline().split('=')[-1]) + with open(req_path, "r", encoding="utf-8") as f: + required_frontend = parse_version(f.readline().split("=")[-1]) if frontend_version < required_frontend: logging.warning("________________________________________________________________________\nWARNING WARNING WARNING WARNING WARNING\n\nInstalled frontend version {} is lower than the recommended version {}.\n\n{}\n________________________________________________________________________".format('.'.join(map(str, frontend_version)), '.'.join(map(str, required_frontend)), frontend_install_warning_message())) else: From db9f2a34fc87d49abea4e5aa29a8573f5073e0ce Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Mon, 10 Mar 2025 15:19:52 -0400 Subject: [PATCH 13/25] Fix unit test --- tests-unit/app_test/frontend_manager_test.py | 57 +++++++++++++++++--- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/tests-unit/app_test/frontend_manager_test.py b/tests-unit/app_test/frontend_manager_test.py index a8df52484..7a91ad410 100644 --- a/tests-unit/app_test/frontend_manager_test.py +++ b/tests-unit/app_test/frontend_manager_test.py @@ -70,7 +70,7 @@ def test_get_release_invalid_version(mock_provider): def test_init_frontend_default(): version_string = DEFAULT_VERSION_STRING frontend_path = FrontendManager.init_frontend(version_string) - assert frontend_path == FrontendManager.DEFAULT_FRONTEND_PATH + assert frontend_path == FrontendManager.default_frontend_path() def test_init_frontend_invalid_version(): @@ -84,24 +84,29 @@ def test_init_frontend_invalid_provider(): with pytest.raises(HTTPError): FrontendManager.init_frontend_unsafe(version_string) + @pytest.fixture def mock_os_functions(): - with patch('app.frontend_management.os.makedirs') as mock_makedirs, \ - patch('app.frontend_management.os.listdir') as mock_listdir, \ - patch('app.frontend_management.os.rmdir') as mock_rmdir: + with ( + patch("app.frontend_management.os.makedirs") as mock_makedirs, + patch("app.frontend_management.os.listdir") as mock_listdir, + patch("app.frontend_management.os.rmdir") as mock_rmdir, + ): mock_listdir.return_value = [] # Simulate empty directory yield mock_makedirs, mock_listdir, mock_rmdir + @pytest.fixture def mock_download(): - with patch('app.frontend_management.download_release_asset_zip') as mock: + with patch("app.frontend_management.download_release_asset_zip") as mock: mock.side_effect = Exception("Download failed") # Simulate download failure yield mock + def test_finally_block(mock_os_functions, mock_download, mock_provider): # Arrange mock_makedirs, mock_listdir, mock_rmdir = mock_os_functions - version_string = 'test-owner/test-repo@1.0.0' + version_string = "test-owner/test-repo@1.0.0" # Act & Assert with pytest.raises(Exception): @@ -128,3 +133,43 @@ def test_parse_version_string_invalid(): version_string = "invalid" with pytest.raises(argparse.ArgumentTypeError): FrontendManager.parse_version_string(version_string) + + +def test_init_frontend_default_with_mocks(): + # Arrange + version_string = DEFAULT_VERSION_STRING + + # Act + with ( + patch("app.frontend_management.check_frontend_version") as mock_check, + patch.object( + FrontendManager, "default_frontend_path", return_value="/mocked/path" + ), + ): + frontend_path = FrontendManager.init_frontend(version_string) + + # Assert + assert frontend_path == "/mocked/path" + mock_check.assert_called_once() + + +def test_init_frontend_fallback_on_error(): + # Arrange + version_string = "test-owner/test-repo@1.0.0" + + # Act + with ( + patch.object( + FrontendManager, "init_frontend_unsafe", side_effect=Exception("Test error") + ), + patch("app.frontend_management.check_frontend_version") as mock_check, + patch.object( + FrontendManager, "default_frontend_path", return_value="/default/path" + ), + ): + frontend_path = FrontendManager.init_frontend(version_string) + + # Assert + assert frontend_path == "/default/path" + mock_check.assert_called_once() + From 65ea778a5e5f69ec83e59a1f08678272fb2725d3 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Mon, 10 Mar 2025 15:19:59 -0400 Subject: [PATCH 14/25] nit --- tests-unit/app_test/frontend_manager_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests-unit/app_test/frontend_manager_test.py b/tests-unit/app_test/frontend_manager_test.py index 7a91ad410..ce67df6c6 100644 --- a/tests-unit/app_test/frontend_manager_test.py +++ b/tests-unit/app_test/frontend_manager_test.py @@ -172,4 +172,3 @@ def test_init_frontend_fallback_on_error(): # Assert assert frontend_path == "/default/path" mock_check.assert_called_once() - From ca8efab79fa19bc9745b4f7346d38a49ba1b1b7c Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Mon, 10 Mar 2025 17:23:13 -0400 Subject: [PATCH 15/25] Support control loras on Wan. --- comfy/model_base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/comfy/model_base.py b/comfy/model_base.py index bf4ebefa1..976702b60 100644 --- a/comfy/model_base.py +++ b/comfy/model_base.py @@ -973,11 +973,11 @@ class WAN21(BaseModel): self.image_to_video = image_to_video def concat_cond(self, **kwargs): - if not self.image_to_video: + noise = kwargs.get("noise", None) + if self.diffusion_model.patch_embedding.weight.shape[1] == noise.shape[1]: return None image = kwargs.get("concat_latent_image", None) - noise = kwargs.get("noise", None) device = kwargs["device"] if image is None: @@ -987,6 +987,9 @@ class WAN21(BaseModel): image = self.process_latent_in(image) image = utils.resize_to_batch_size(image, noise.shape[0]) + if not self.image_to_video: + return image + mask = kwargs.get("concat_mask", kwargs.get("denoise_mask", None)) if mask is None: mask = torch.zeros_like(noise)[:, :4] From cfbe4b49ca63eae79fe4f3206d03a41b43ef275e Mon Sep 17 00:00:00 2001 From: huchenlei Date: Mon, 10 Mar 2025 20:43:59 -0400 Subject: [PATCH 16/25] Access package version --- app/frontend_management.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/frontend_management.py b/app/frontend_management.py index 95df5dee4..4b7dfbb98 100644 --- a/app/frontend_management.py +++ b/app/frontend_management.py @@ -11,6 +11,7 @@ from dataclasses import dataclass from functools import cached_property from pathlib import Path from typing import TypedDict, Optional +from importlib.metadata import version import requests from typing_extensions import NotRequired @@ -36,15 +37,14 @@ def check_frontend_version(): return tuple(map(int, version.split("."))) try: - import comfyui_frontend_package - - frontend_version = parse_version(comfyui_frontend_package.__version__) + frontend_version_str = version("comfyui-frontend-package") + frontend_version = parse_version(frontend_version_str) with open(req_path, "r", encoding="utf-8") as f: required_frontend = parse_version(f.readline().split("=")[-1]) if frontend_version < required_frontend: logging.warning("________________________________________________________________________\nWARNING WARNING WARNING WARNING WARNING\n\nInstalled frontend version {} is lower than the recommended version {}.\n\n{}\n________________________________________________________________________".format('.'.join(map(str, frontend_version)), '.'.join(map(str, required_frontend)), frontend_install_warning_message())) else: - logging.info("ComfyUI frontend version: {}".format(comfyui_frontend_package.__version__)) + logging.info("ComfyUI frontend version: {}".format(frontend_version_str)) except Exception as e: logging.error(f"Failed to check frontend version: {e}") From 2330754b0ed3e4864c8ba8165e57ea18aafa30b8 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Tue, 11 Mar 2025 15:07:00 -0400 Subject: [PATCH 17/25] Fix error saving some latents. --- nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes.py b/nodes.py index e43c29295..63791e208 100644 --- a/nodes.py +++ b/nodes.py @@ -489,7 +489,7 @@ class SaveLatent: file = os.path.join(full_output_folder, file) output = {} - output["latent_tensor"] = samples["samples"] + output["latent_tensor"] = samples["samples"].contiguous() output["latent_format_version_0"] = torch.tensor([]) comfy.utils.save_torch_file(output, file, metadata=metadata) From 01015bff166988c926e5ed1d03842fddc9a0f925 Mon Sep 17 00:00:00 2001 From: chaObserv <154517000+chaObserv@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:42:37 +0800 Subject: [PATCH 18/25] Add er_sde sampler (#7187) --- comfy/k_diffusion/sampling.py | 56 +++++++++++++++++++++++++++++++++++ comfy/samplers.py | 2 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/comfy/k_diffusion/sampling.py b/comfy/k_diffusion/sampling.py index 456679989..78678abd7 100644 --- a/comfy/k_diffusion/sampling.py +++ b/comfy/k_diffusion/sampling.py @@ -1366,3 +1366,59 @@ def sample_gradient_estimation(model, x, sigmas, extra_args=None, callback=None, x = x + d_bar * dt old_d = d return x + +@torch.no_grad() +def sample_er_sde(model, x, sigmas, extra_args=None, callback=None, disable=None, s_noise=1., noise_sampler=None, noise_scaler=None, max_stage=3): + """ + Extended Reverse-Time SDE solver (VE ER-SDE-Solver-3). Arxiv: https://arxiv.org/abs/2309.06169. + Code reference: https://github.com/QinpengCui/ER-SDE-Solver/blob/main/er_sde_solver.py. + """ + extra_args = {} if extra_args is None else extra_args + seed = extra_args.get("seed", None) + noise_sampler = default_noise_sampler(x, seed=seed) if noise_sampler is None else noise_sampler + s_in = x.new_ones([x.shape[0]]) + + def default_noise_scaler(sigma): + return sigma * ((sigma ** 0.3).exp() + 10.0) + noise_scaler = default_noise_scaler if noise_scaler is None else noise_scaler + num_integration_points = 200.0 + point_indice = torch.arange(0, num_integration_points, dtype=torch.float32, device=x.device) + + old_denoised = None + old_denoised_d = None + + for i in trange(len(sigmas) - 1, disable=disable): + denoised = model(x, sigmas[i] * s_in, **extra_args) + if callback is not None: + callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised}) + stage_used = min(max_stage, i + 1) + if sigmas[i + 1] == 0: + x = denoised + elif stage_used == 1: + r = noise_scaler(sigmas[i + 1]) / noise_scaler(sigmas[i]) + x = r * x + (1 - r) * denoised + else: + r = noise_scaler(sigmas[i + 1]) / noise_scaler(sigmas[i]) + x = r * x + (1 - r) * denoised + + dt = sigmas[i + 1] - sigmas[i] + sigma_step_size = -dt / num_integration_points + sigma_pos = sigmas[i + 1] + point_indice * sigma_step_size + scaled_pos = noise_scaler(sigma_pos) + + # Stage 2 + s = torch.sum(1 / scaled_pos) * sigma_step_size + denoised_d = (denoised - old_denoised) / (sigmas[i] - sigmas[i - 1]) + x = x + (dt + s * noise_scaler(sigmas[i + 1])) * denoised_d + + if stage_used >= 3: + # Stage 3 + s_u = torch.sum((sigma_pos - sigmas[i]) / scaled_pos) * sigma_step_size + denoised_u = (denoised_d - old_denoised_d) / ((sigmas[i] - sigmas[i - 2]) / 2) + x = x + ((dt ** 2) / 2 + s_u * noise_scaler(sigmas[i + 1])) * denoised_u + old_denoised_d = denoised_d + + if s_noise != 0 and sigmas[i + 1] > 0: + x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * (sigmas[i + 1] ** 2 - sigmas[i] ** 2 * r ** 2).sqrt() + old_denoised = denoised + return x diff --git a/comfy/samplers.py b/comfy/samplers.py index 7578ac1ef..10728bd1f 100644 --- a/comfy/samplers.py +++ b/comfy/samplers.py @@ -710,7 +710,7 @@ KSAMPLER_NAMES = ["euler", "euler_cfg_pp", "euler_ancestral", "euler_ancestral_c "lms", "dpm_fast", "dpm_adaptive", "dpmpp_2s_ancestral", "dpmpp_2s_ancestral_cfg_pp", "dpmpp_sde", "dpmpp_sde_gpu", "dpmpp_2m", "dpmpp_2m_cfg_pp", "dpmpp_2m_sde", "dpmpp_2m_sde_gpu", "dpmpp_3m_sde", "dpmpp_3m_sde_gpu", "ddpm", "lcm", "ipndm", "ipndm_v", "deis", "res_multistep", "res_multistep_cfg_pp", "res_multistep_ancestral", "res_multistep_ancestral_cfg_pp", - "gradient_estimation"] + "gradient_estimation", "er_sde"] class KSAMPLER(Sampler): def __init__(self, sampler_function, extra_options={}, inpaint_options={}): From d2a0fb6bb0da1bf481a3b2417bca2cebac4a4e03 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Wed, 12 Mar 2025 06:39:14 -0400 Subject: [PATCH 19/25] Add unwrap widget value support (#7197) * Add unwrap widget value support * nit --- execution.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/execution.py b/execution.py index 2c979205b..fcb4f6f40 100644 --- a/execution.py +++ b/execution.py @@ -634,6 +634,13 @@ def validate_inputs(prompt, item, validated): continue else: try: + # Unwraps values wrapped in __value__ key. This is used to pass + # list widget value to execution, as by default list value is + # reserved to represent the connection between nodes. + if isinstance(val, dict) and "__value__" in val: + val = val["__value__"] + inputs[x] = val + if type_input == "INT": val = int(val) inputs[x] = val From f4411250f311f1ba93b8ba57b4252e7c23f7d925 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 12 Mar 2025 07:13:40 -0400 Subject: [PATCH 20/25] Repeat frontend version warning at the end. This way someone running ComfyUI with the command line is more likely to actually see it. --- app/frontend_management.py | 3 ++- app/logger.py | 14 ++++++++++++++ main.py | 2 ++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/frontend_management.py b/app/frontend_management.py index 4b7dfbb98..b4ba994d1 100644 --- a/app/frontend_management.py +++ b/app/frontend_management.py @@ -17,6 +17,7 @@ import requests from typing_extensions import NotRequired from comfy.cli_args import DEFAULT_VERSION_STRING +import app.logger # The path to the requirements.txt file req_path = Path(__file__).parents[1] / "requirements.txt" @@ -42,7 +43,7 @@ def check_frontend_version(): with open(req_path, "r", encoding="utf-8") as f: required_frontend = parse_version(f.readline().split("=")[-1]) if frontend_version < required_frontend: - logging.warning("________________________________________________________________________\nWARNING WARNING WARNING WARNING WARNING\n\nInstalled frontend version {} is lower than the recommended version {}.\n\n{}\n________________________________________________________________________".format('.'.join(map(str, frontend_version)), '.'.join(map(str, required_frontend)), frontend_install_warning_message())) + app.logger.log_startup_warning("________________________________________________________________________\nWARNING WARNING WARNING WARNING WARNING\n\nInstalled frontend version {} is lower than the recommended version {}.\n\n{}\n________________________________________________________________________".format('.'.join(map(str, frontend_version)), '.'.join(map(str, required_frontend)), frontend_install_warning_message())) else: logging.info("ComfyUI frontend version: {}".format(frontend_version_str)) except Exception as e: diff --git a/app/logger.py b/app/logger.py index 9e9f84ccf..3d26d98fe 100644 --- a/app/logger.py +++ b/app/logger.py @@ -82,3 +82,17 @@ def setup_logger(log_level: str = 'INFO', capacity: int = 300, use_stdout: bool logger.addHandler(stdout_handler) logger.addHandler(stream_handler) + + +STARTUP_WARNINGS = [] + + +def log_startup_warning(msg): + logging.warning(msg) + STARTUP_WARNINGS.append(msg) + + +def print_startup_warnings(): + for s in STARTUP_WARNINGS: + logging.warning(s) + STARTUP_WARNINGS.clear() diff --git a/main.py b/main.py index c6f5c3c1e..1b100fa8a 100644 --- a/main.py +++ b/main.py @@ -139,6 +139,7 @@ from server import BinaryEventTypes import nodes import comfy.model_management import comfyui_version +import app.logger def cuda_malloc_warning(): @@ -299,6 +300,7 @@ if __name__ == "__main__": event_loop, _, start_all_func = start_comfyui() try: x = start_all_func() + app.logger.print_startup_warnings() event_loop.run_until_complete(x) except KeyboardInterrupt: logging.info("\nStopped server") From 3fc688aebd9f54f8351da3a4282bd12c74e4a02e Mon Sep 17 00:00:00 2001 From: chaObserv <154517000+chaObserv@users.noreply.github.com> Date: Thu, 13 Mar 2025 05:28:59 +0800 Subject: [PATCH 21/25] Ensure the extra_args in dpmpp sde series (#7204) --- comfy/k_diffusion/sampling.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/comfy/k_diffusion/sampling.py b/comfy/k_diffusion/sampling.py index 78678abd7..a28a30ac2 100644 --- a/comfy/k_diffusion/sampling.py +++ b/comfy/k_diffusion/sampling.py @@ -688,10 +688,10 @@ def sample_dpmpp_sde(model, x, sigmas, extra_args=None, callback=None, disable=N if len(sigmas) <= 1: return x + extra_args = {} if extra_args is None else extra_args sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max() seed = extra_args.get("seed", None) noise_sampler = BrownianTreeNoiseSampler(x, sigma_min, sigma_max, seed=seed, cpu=True) if noise_sampler is None else noise_sampler - extra_args = {} if extra_args is None else extra_args s_in = x.new_ones([x.shape[0]]) sigma_fn = lambda t: t.neg().exp() t_fn = lambda sigma: sigma.log().neg() @@ -762,10 +762,10 @@ def sample_dpmpp_2m_sde(model, x, sigmas, extra_args=None, callback=None, disabl if solver_type not in {'heun', 'midpoint'}: raise ValueError('solver_type must be \'heun\' or \'midpoint\'') + extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max() noise_sampler = BrownianTreeNoiseSampler(x, sigma_min, sigma_max, seed=seed, cpu=True) if noise_sampler is None else noise_sampler - extra_args = {} if extra_args is None else extra_args s_in = x.new_ones([x.shape[0]]) old_denoised = None @@ -808,10 +808,10 @@ def sample_dpmpp_3m_sde(model, x, sigmas, extra_args=None, callback=None, disabl if len(sigmas) <= 1: return x + extra_args = {} if extra_args is None else extra_args seed = extra_args.get("seed", None) sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max() noise_sampler = BrownianTreeNoiseSampler(x, sigma_min, sigma_max, seed=seed, cpu=True) if noise_sampler is None else noise_sampler - extra_args = {} if extra_args is None else extra_args s_in = x.new_ones([x.shape[0]]) denoised_1, denoised_2 = None, None @@ -858,7 +858,7 @@ def sample_dpmpp_3m_sde(model, x, sigmas, extra_args=None, callback=None, disabl def sample_dpmpp_3m_sde_gpu(model, x, sigmas, extra_args=None, callback=None, disable=None, eta=1., s_noise=1., noise_sampler=None): if len(sigmas) <= 1: return x - + extra_args = {} if extra_args is None else extra_args sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max() noise_sampler = BrownianTreeNoiseSampler(x, sigma_min, sigma_max, seed=extra_args.get("seed", None), cpu=False) if noise_sampler is None else noise_sampler return sample_dpmpp_3m_sde(model, x, sigmas, extra_args=extra_args, callback=callback, disable=disable, eta=eta, s_noise=s_noise, noise_sampler=noise_sampler) @@ -867,7 +867,7 @@ def sample_dpmpp_3m_sde_gpu(model, x, sigmas, extra_args=None, callback=None, di def sample_dpmpp_2m_sde_gpu(model, x, sigmas, extra_args=None, callback=None, disable=None, eta=1., s_noise=1., noise_sampler=None, solver_type='midpoint'): if len(sigmas) <= 1: return x - + extra_args = {} if extra_args is None else extra_args sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max() noise_sampler = BrownianTreeNoiseSampler(x, sigma_min, sigma_max, seed=extra_args.get("seed", None), cpu=False) if noise_sampler is None else noise_sampler return sample_dpmpp_2m_sde(model, x, sigmas, extra_args=extra_args, callback=callback, disable=disable, eta=eta, s_noise=s_noise, noise_sampler=noise_sampler, solver_type=solver_type) @@ -876,7 +876,7 @@ def sample_dpmpp_2m_sde_gpu(model, x, sigmas, extra_args=None, callback=None, di def sample_dpmpp_sde_gpu(model, x, sigmas, extra_args=None, callback=None, disable=None, eta=1., s_noise=1., noise_sampler=None, r=1 / 2): if len(sigmas) <= 1: return x - + extra_args = {} if extra_args is None else extra_args sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max() noise_sampler = BrownianTreeNoiseSampler(x, sigma_min, sigma_max, seed=extra_args.get("seed", None), cpu=False) if noise_sampler is None else noise_sampler return sample_dpmpp_sde(model, x, sigmas, extra_args=extra_args, callback=callback, disable=disable, eta=eta, s_noise=s_noise, noise_sampler=noise_sampler, r=r) From 9b6cd9b874b7f4ff2e9770f80e84b712fa8f1661 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Wed, 12 Mar 2025 17:29:39 -0400 Subject: [PATCH 22/25] [NodeDef] Add documentation on multi_select input option (#7212) --- comfy/comfy_types/node_typing.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/comfy/comfy_types/node_typing.py b/comfy/comfy_types/node_typing.py index 4967de716..1b71208d4 100644 --- a/comfy/comfy_types/node_typing.py +++ b/comfy/comfy_types/node_typing.py @@ -2,6 +2,7 @@ from __future__ import annotations from typing import Literal, TypedDict +from typing_extensions import NotRequired from abc import ABC, abstractmethod from enum import Enum @@ -26,6 +27,7 @@ class IO(StrEnum): BOOLEAN = "BOOLEAN" INT = "INT" FLOAT = "FLOAT" + COMBO = "COMBO" CONDITIONING = "CONDITIONING" SAMPLER = "SAMPLER" SIGMAS = "SIGMAS" @@ -66,6 +68,7 @@ class IO(StrEnum): b = frozenset(value.split(",")) return not (b.issubset(a) or a.issubset(b)) + class RemoteInputOptions(TypedDict): route: str """The route to the remote source.""" @@ -80,6 +83,14 @@ class RemoteInputOptions(TypedDict): refresh: int """The TTL of the remote input's value in milliseconds. Specifies the interval at which the remote input's value is refreshed.""" + +class MultiSelectOptions(TypedDict): + placeholder: NotRequired[str] + """The placeholder text to display in the multi-select widget when no items are selected.""" + chip: NotRequired[bool] + """Specifies whether to use chips instead of comma separated values for the multi-select widget.""" + + class InputTypeOptions(TypedDict): """Provides type hinting for the return type of the INPUT_TYPES node function. @@ -133,9 +144,22 @@ class InputTypeOptions(TypedDict): """Specifies which folder to get preview images from if the input has the ``image_upload`` flag. """ remote: RemoteInputOptions - """Specifies the configuration for a remote input.""" + """Specifies the configuration for a remote input. + Available after ComfyUI frontend v1.9.7 + https://github.com/Comfy-Org/ComfyUI_frontend/pull/2422""" control_after_generate: bool """Specifies whether a control widget should be added to the input, adding options to automatically change the value after each prompt is queued. Currently only used for INT and COMBO types.""" + options: NotRequired[list[str | int | float]] + """COMBO type only. Specifies the selectable options for the combo widget. + Prefer: + ["COMBO", {"options": ["Option 1", "Option 2", "Option 3"]}] + Over: + [["Option 1", "Option 2", "Option 3"]] + """ + multi_select: NotRequired[MultiSelectOptions] + """COMBO type only. Specifies the configuration for a multi-select widget. + Available after ComfyUI frontend v1.13.4 + https://github.com/Comfy-Org/ComfyUI_frontend/pull/2987""" class HiddenInputTypeDict(TypedDict): From 52e566d2bcf3321cce84d3f08a3a39d55bf556cf Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Wed, 12 Mar 2025 17:30:00 -0400 Subject: [PATCH 23/25] Add codeowner for comfy/comfy_types (#7213) --- CODEOWNERS | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index eeec358de..72a59effe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -19,5 +19,6 @@ /app/ @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata /utils/ @yoland68 @robinjhuang @huchenlei @webfiltered @pythongosssss @ltdrdata -# Extra nodes -/comfy_extras/ @yoland68 @robinjhuang @huchenlei @pythongosssss @ltdrdata @Kosinkadink +# Node developers +/comfy_extras/ @yoland68 @robinjhuang @huchenlei @pythongosssss @ltdrdata @Kosinkadink @webfiltered +/comfy/comfy_types/ @yoland68 @robinjhuang @huchenlei @pythongosssss @ltdrdata @Kosinkadink @webfiltered From 299436cfed82cb6490779fcecba8a72eee5ce39f Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Thu, 13 Mar 2025 10:05:15 -0400 Subject: [PATCH 24/25] Print mac version. --- comfy/model_management.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/comfy/model_management.py b/comfy/model_management.py index 3a4c93e30..1bb6156d3 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -186,12 +186,21 @@ def get_total_memory(dev=None, torch_total_too=False): else: return mem_total +def mac_version(): + try: + return tuple(int(n) for n in platform.mac_ver()[0].split(".")) + except: + return None + total_vram = get_total_memory(get_torch_device()) / (1024 * 1024) total_ram = psutil.virtual_memory().total / (1024 * 1024) logging.info("Total VRAM {:0.0f} MB, total RAM {:0.0f} MB".format(total_vram, total_ram)) try: logging.info("pytorch version: {}".format(torch_version)) + mac_ver = mac_version() + if mac_ver is not None: + print("Mac Version", mac_ver) except: pass @@ -969,12 +978,6 @@ def pytorch_attention_flash_attention(): return True #if you have pytorch attention enabled on AMD it probably supports at least mem efficient attention return False -def mac_version(): - try: - return tuple(int(n) for n in platform.mac_ver()[0].split(".")) - except: - return None - def force_upcast_attention_dtype(): upcast = args.force_upcast_attention From 35504e2f931c59190d0dd1b4ab2288f1c7f0e9f8 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Thu, 13 Mar 2025 15:03:18 -0400 Subject: [PATCH 25/25] Fix. --- comfy/model_management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy/model_management.py b/comfy/model_management.py index 1bb6156d3..b6f4e2d19 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -200,7 +200,7 @@ try: logging.info("pytorch version: {}".format(torch_version)) mac_ver = mac_version() if mac_ver is not None: - print("Mac Version", mac_ver) + logging.info("Mac Version {}".format(mac_ver)) except: pass