Compare commits

...
9 Commits
Author SHA1 Message Date
Dr.Lt.Data 48a814315f modified: detailer - allow 5D latent model (WAN)
- assuming it's a single-frame latent, not a video latent

https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/1066
2025-07-27 15:53:40 +09:00
Dr.Lt.Data 3d90c579e8 bump version 2025-07-25 12:35:06 +09:00
Dustin f03dd5e79e Fix: PreviewBridge mask editor KeyError with clipspace files + restore fresh image behavior (#1009)
* fix: Handle clipspace files in PreviewBridge mask editor and restore fresh image behavior

- Fix KeyError when using mask editor multiple times
- Add register_clipspace_image() method to detect and register clipspace files
- Resolve timing issue between frontend JS conversion and backend processing
- Restore original "fresh start" behavior for new image generations
- Clear mask cache when images/latents change to ensure clean slate for new content
- Maintain backwards compatibility with existing preview bridge functionality
- Add dual registration for clipspace paths and preview IDs

Fixes issue where second+ mask saves would fail with:
KeyError: 'clipspace/clipspace-mask-XXXXX.png [input]'

Also fixed regression where new images would retain previous masks instead of starting fresh.

* Fix restore_mask 'always' and 'if_same_size' modes in PreviewBridge

- Fixed 'always' mode to properly preserve masks when changing input images/latents
- Fixed 'if_same_size' mode to preserve masks when switching between same-sized images
- Modified cache clearing logic to preserve cache for both 'always' and 'if_same_size' modes
- Updated restoration logic to handle size comparison correctly for 'if_same_size'

This addresses the issue where masks were being cleared even when restore_mask was set to 'always' or when switching between same-sized images with 'if_same_size'.

Fixes the issue discussed in PR #1009
2025-07-25 12:34:23 +09:00
Dr.Lt.Data e1e95c14d3 improved: Muted inputs are now allowed for Make...List and Make...Batch.
fixed: A bug where the number of input slots did not decrease below 3 when disconnected in `Make...List` and `Make...Batch`.

https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/1052
2025-07-19 20:04:46 +09:00
Dr.Lt.Data 17e0a05769 fixed: The {n$$...}` wildcard pattern was not working properly.
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/1055
2025-07-19 19:09:49 +09:00
meneraing b3a815b43d Update impact_pack.py (#1047)
Change the scale factor step to 0.05 for better parity with "Upscale Latent By" node from ComfyUI
2025-07-16 17:51:11 +09:00
Miguel C 8ab2e168f7 fix: update mask combination to use utils module (#1048) 2025-07-16 16:43:57 +09:00
Dr.Lt.Data b980035588 feat: extra schedulers - support OSS Chroma 2025-07-15 12:28:01 +09:00
Dr.Lt.Data df330c1b06 fixed: an invalid namespace issue introduced by refactoring.
fixed: ruff check

https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/1042
2025-07-09 23:51:49 +09:00
13 changed files with 159 additions and 51 deletions
+6 -1
View File
@@ -594,7 +594,12 @@ app.registerExtension({
let select_slot = this.inputs.find(x => x.name == "select");
if (!connected && (this.inputs.length > 3)) {
let widget_count = 0;
if(nodeData.name == 'ImpactSwitch' || nodeData.name == 'LatentSwitch' || nodeData.name == 'SEGSSwitch') {
widget_count += 1;
}
if (!connected && (this.inputs.length > widget_count+1)) {
if(
!stackTrace.includes('LGraphNode.prototype.connect') && // for touch device
!stackTrace.includes('LGraphNode.connect') && // for mouse device
+111 -15
View File
@@ -52,10 +52,10 @@ class PreviewBridge:
if pb_id not in core.preview_bridge_image_id_map:
is_fail = True
image_path, ui_item = core.preview_bridge_image_id_map[pb_id]
if not os.path.isfile(image_path):
is_fail = True
if not is_fail:
image_path, ui_item = core.preview_bridge_image_id_map[pb_id]
if not os.path.isfile(image_path):
is_fail = True
if not is_fail:
i = Image.open(image_path)
@@ -80,23 +80,93 @@ class PreviewBridge:
return image, mask.unsqueeze(0), ui_item
@staticmethod
def register_clipspace_image(clipspace_path, node_id):
"""Register a clipspace image file in the preview bridge system.
This handles the case where ComfyUI's mask editor creates clipspace files
that need to be integrated with the preview bridge system.
"""
# Remove [input] suffix if present
clean_path = clipspace_path.replace(" [input]", "").replace("[input]", "")
# Try to find the actual clipspace file
input_dir = folder_paths.get_input_directory()
potential_paths = [
clean_path,
os.path.join(input_dir, clean_path),
os.path.join(input_dir, "clipspace", os.path.basename(clean_path)),
os.path.abspath(clean_path),
]
actual_file = None
for path in potential_paths:
if os.path.isfile(path):
actual_file = path
break
if not actual_file:
return False
# Create ui_item for the clipspace file
ui_item = {
'filename': os.path.basename(actual_file),
'subfolder': 'clipspace',
'type': 'input'
}
# Register it using the preview bridge system
core.set_previewbridge_image(node_id, actual_file, ui_item)
# Also register under the original clipspace path for compatibility
core.preview_bridge_image_id_map[clipspace_path] = (actual_file, ui_item)
return True
def doit(self, images, image, unique_id, block=False, restore_mask="never", prompt=None, extra_pnginfo=None):
need_refresh = False
images_changed = False
# Check if images have changed (this determines if we start fresh)
if unique_id not in core.preview_bridge_cache:
need_refresh = True
images_changed = True
elif core.preview_bridge_cache[unique_id][0] is not images:
need_refresh = True
images_changed = True
# If images changed, clear the mask cache to ensure fresh start behavior
# This restores the original behavior where new images start with empty masks
# unless restore_mask is set to "always" or "if_same_size"
if images_changed and restore_mask not in ["always", "if_same_size"] and unique_id in core.preview_bridge_last_mask_cache:
del core.preview_bridge_last_mask_cache[unique_id]
# Handle clipspace files that aren't registered in the preview bridge system
# This only applies when images haven't changed (same image, new mask scenario)
if not need_refresh and image not in core.preview_bridge_image_id_map:
# Check if this is a clipspace file that needs to be registered
is_clipspace = image and ("clipspace" in image.lower() or "[input]" in image)
if is_clipspace:
if not PreviewBridge.register_clipspace_image(image, unique_id):
need_refresh = True
else:
need_refresh = True
if not need_refresh:
pixels, mask, path_item = PreviewBridge.load_image(image)
image = [path_item]
else:
if restore_mask != "never":
# For new images (images_changed=True), we want to start fresh regardless of restore_mask
# For same image with refresh needed, respect the restore_mask setting
# Exception: when restore_mask is "always", restore even with new images
# Exception: when restore_mask is "if_same_size", allow restoration to check size compatibility
if restore_mask != "never" and (not images_changed or restore_mask in ["always", "if_same_size"]):
mask = core.preview_bridge_last_mask_cache.get(unique_id)
if mask is None or (restore_mask != "always" and mask.shape[1:] != images.shape[1:3]):
if mask is None:
mask = None
elif restore_mask == "if_same_size" and mask.shape[1:] != images.shape[1:3]:
# For if_same_size, clear mask if dimensions don't match
mask = None
# For "always", keep the mask regardless of size
else:
mask = None
@@ -252,10 +322,10 @@ class PreviewBridgeLatent:
if pb_id not in core.preview_bridge_image_id_map:
is_fail = True
image_path, ui_item = core.preview_bridge_image_id_map[pb_id]
if not os.path.isfile(image_path):
is_fail = True
if not is_fail:
image_path, ui_item = core.preview_bridge_image_id_map[pb_id]
if not os.path.isfile(image_path):
is_fail = True
if not is_fail:
i = Image.open(image_path)
@@ -295,15 +365,33 @@ class PreviewBridgeLatent:
raise Exception("The version of latent is not compatible with preview_method.<BR>SD3, SD1/SD2, SDXL, SC-Prior, SC-B and FLUX.1 are not compatible with each other.")
need_refresh = False
latent_changed = False
# Check if latent has changed
if unique_id not in core.preview_bridge_cache:
need_refresh = True
latent_changed = True
elif (core.preview_bridge_cache[unique_id][0] is not latent
or (vae_opt is None and core.preview_bridge_cache[unique_id][2] is not None)
or (vae_opt is None and core.preview_bridge_cache[unique_id][1] != preview_method)
or (vae_opt is not None and core.preview_bridge_cache[unique_id][2] is not vae_opt)):
need_refresh = True
latent_changed = True
# If latent changed, clear the mask cache to ensure fresh start behavior
# unless restore_mask is set to "always" or "if_same_size"
if latent_changed and restore_mask not in ["always", "if_same_size"] and unique_id in core.preview_bridge_last_mask_cache:
del core.preview_bridge_last_mask_cache[unique_id]
# Handle clipspace files that aren't registered in the preview bridge system
# This only applies when latent hasn't changed (same latent, new mask scenario)
if not need_refresh and image not in core.preview_bridge_image_id_map:
is_clipspace = image and ("clipspace" in image.lower() or "[input]" in image)
if is_clipspace:
if not PreviewBridge.register_clipspace_image(image, unique_id):
need_refresh = True
else:
need_refresh = True
if not need_refresh:
pixels, mask, path_item = PreviewBridge.load_image(image)
@@ -347,10 +435,18 @@ class PreviewBridgeLatent:
is_empty_mask = False
else:
if restore_mask != "never":
# For new latents (latent_changed=True), start fresh regardless of restore_mask
# For same latent with refresh needed, respect the restore_mask setting
# Exception: when restore_mask is "always", restore even with new latents
# Exception: when restore_mask is "if_same_size", allow restoration to check size compatibility
if restore_mask != "never" and (not latent_changed or restore_mask in ["always", "if_same_size"]):
mask = core.preview_bridge_last_mask_cache.get(unique_id)
if mask is None or (restore_mask != "always" and mask.shape[1:] != decoded_image.shape[1:3]):
if mask is None:
mask = None
elif restore_mask == "if_same_size" and mask.shape[1:] != decoded_image.shape[1:3]:
# For if_same_size, clear mask if dimensions don't match
mask = None
# For "always", keep the mask regardless of size
else:
mask = None
@@ -391,4 +487,4 @@ class PreviewBridgeLatent:
return {
"ui": {"images": res_image},
"result": result,
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import logging
version_code = [8, 19]
version_code = [8, 21, 2]
version = f"V{version_code[0]}.{version_code[1]}" + (f'.{version_code[2]}' if len(version_code) > 2 else '')
my_path = os.path.dirname(__file__)
+9 -4
View File
@@ -57,14 +57,14 @@ preview_bridge_last_mask_cache = {}
current_prompt = None
SCHEDULERS = comfy.samplers.KSampler.SCHEDULERS + ['AYS SDXL', 'AYS SD1', 'AYS SVD', 'GITS[coeff=1.2]', 'LTXV[default]', 'OSS FLUX', 'OSS Wan']
SCHEDULERS = comfy.samplers.KSampler.SCHEDULERS + ['AYS SDXL', 'AYS SD1', 'AYS SVD', 'GITS[coeff=1.2]', 'LTXV[default]', 'OSS FLUX', 'OSS Wan', 'OSS Chroma']
def is_execution_model_version_supported():
try:
import comfy_execution # noqa: F401
return True
except:
except Exception:
return False
@@ -407,6 +407,11 @@ def enhance_detail(image, model, clip, vae, guide_size, guide_size_for_bbox, max
refined_image = detailer_hook.post_decode(refined_image)
# downscale
# workaround: support WAN as an i2i model
if len(refined_image.shape) == 5:
refined_image = refined_image.squeeze(0)
refined_image = utils.tensor_resize(refined_image, w, h)
# prevent mixing of device
@@ -2346,7 +2351,7 @@ class SafeToGPU:
if model_management.get_free_memory(device) > self.size * 1.3:
try:
obj.to(device)
except:
except Exception:
logging.warning(f"[Impact Pack] The model is not moved to the '{device}' due to insufficient memory. [1]")
else:
logging.warning(f"[Impact Pack] The model is not moved to the '{device}' due to insufficient memory. [2]")
@@ -2395,7 +2400,7 @@ try:
previewer = Latent2RGBPreviewer(latent_format.latent_rgb_factors)
return previewer
except:
except Exception:
logging.error("#########################################################################")
logging.error("[ERROR] ComfyUI-Impact-Pack: Please update ComfyUI to the latest version.")
logging.error("#########################################################################")
+4 -4
View File
@@ -377,7 +377,7 @@ class DetailerForEach:
if cnet_pils is not None:
cnet_pil_list.extend(cnet_pils)
if not (enhanced_image is None):
if enhanced_image is not None:
# don't latent composite-> converting to latent caused poor quality
# use image paste
image = image.cpu()
@@ -388,7 +388,7 @@ class DetailerForEach:
if detailer_hook is not None:
image = detailer_hook.post_paste(image)
if not (enhanced_image is None):
if enhanced_image is not None:
# Convert enhanced_pil_alpha to RGBA mode
enhanced_image_alpha = utils.tensor_convert_rgba(enhanced_image)
new_seg_image = enhanced_image.numpy() # alpha should not be applied to seg_image
@@ -672,7 +672,7 @@ class LatentPixelScale:
return {"required": {
"samples": ("LATENT", ),
"scale_method": (s.upscale_methods,),
"scale_factor": ("FLOAT", {"default": 1.5, "min": 0.1, "max": 10000, "step": 0.1}),
"scale_factor": ("FLOAT", {"default": 1.5, "min": 0.1, "max": 10000, "step": 0.05}),
"vae": ("VAE", ),
"use_tiled_vae": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
},
@@ -2468,7 +2468,7 @@ class ImpactSchedulerAdapter:
def INPUT_TYPES(s):
return {"required": {
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"defaultInput": True, }),
"extra_scheduler": (['None', 'AYS SDXL', 'AYS SD1', 'AYS SVD', 'GITS[coeff=1.2]', 'LTXV[default]', 'OSS FLUX', 'OSS Wan'],),
"extra_scheduler": (['None', 'AYS SDXL', 'AYS SD1', 'AYS SVD', 'GITS[coeff=1.2]', 'LTXV[default]', 'OSS FLUX', 'OSS Wan', 'OSS Chroma'],),
}}
CATEGORY = "ImpactPack/Util"
+1 -1
View File
@@ -14,7 +14,7 @@ import comfy.model_management as mm
try:
from comfy_extras.nodes_custom_sampler import Noise_EmptyNoise, Noise_RandomNoise
import node_helpers
except:
except Exception:
logging.warning("\n#############################################\n[Impact Pack] ComfyUI is an outdated version.\n#############################################\n")
raise Exception("[Impact Pack] ComfyUI is an outdated version.")
+4 -3
View File
@@ -12,6 +12,7 @@ import torchvision
import impact.core as core
import impact.impact_pack as impact_pack
from impact.utils import to_tensor
import impact.utils as utils
from segment_anything import SamPredictor, sam_model_registry
import numpy as np
import nodes
@@ -145,7 +146,7 @@ async def sam_detect(request):
plabs.append(0)
detected_masks = core.sam_predict(sam_predictor, points, plabs, None, threshold)
mask = core.combine_masks2(detected_masks)
mask = utils.combine_masks2(detected_masks)
if mask is None:
return web.Response(status=400)
@@ -373,7 +374,7 @@ def onprompt_for_switch(json_data):
if 'BOOLEAN' == input_node['inputs']['typ']:
try:
onprompt_cond_branch_info[k] = input_node['inputs']['value'].lower() == "true"
except:
except Exception:
pass
else:
onprompt_cond_branch_info[k] = cond_input
@@ -507,7 +508,7 @@ def onprompt_populate_wildcards(json_data):
else:
logging.info(f"[Impact Pack] Only `ImpactInt`, `Seed (rgthree)` and `Primitive` Node are allowed as the seed for '{v['class_type']}'. It will be ignored. ")
continue
except:
except Exception:
continue
else:
input_seed = int(inputs['seed'])
+2 -2
View File
@@ -654,7 +654,7 @@ class ImpactControlBridge:
# so extra_pnginfo is useless in here
try:
workflow = core.current_prompt['extra_data']['extra_pnginfo']['workflow']
except:
except Exception:
logging.info("[Impact Pack] core.current_prompt['extra_data']['extra_pnginfo']['workflow']")
return 0
@@ -713,7 +713,7 @@ class ImpactControlBridge:
PromptServer.instance.send_sync("impact-bridge-continue", {"node_id": unique_id, 'actives': list(should_be_active_nodes)})
nodes.interrupt_processing()
elif behavior == "Mute" or behavior == True:
elif behavior == "Mute" or behavior == True: # noqa: E712
# mute
should_be_mute_nodes = active_nodes + bypass_nodes
if len(should_be_mute_nodes) > 0:
+3 -3
View File
@@ -1130,7 +1130,7 @@ class DilateMaskInSEGS:
def doit(self, segs, dilation):
new_segs = []
for seg in segs[1]:
mask = core.dilate_mask(seg.cropped_mask, dilation)
mask = utils.dilate_mask(seg.cropped_mask, dilation)
seg = SEG(seg.cropped_image, mask, seg.confidence, seg.crop_region, seg.bbox, seg.label, seg.control_net_wrapper)
new_segs.append(seg)
@@ -1178,7 +1178,7 @@ class Dilate_SEG_ELT:
CATEGORY = "ImpactPack/Util"
def doit(self, seg, dilation):
mask = core.dilate_mask(seg.cropped_mask, dilation)
mask = utils.dilate_mask(seg.cropped_mask, dilation)
seg = SEG(seg.cropped_image, mask, seg.confidence, seg.crop_region, seg.bbox, seg.label, seg.control_net_wrapper)
return (seg,)
@@ -1963,7 +1963,7 @@ class SEGSUpscaler:
positive, negative, denoise,
noise_mask=cropped_mask, control_net_wrapper=seg.control_net_wrapper,
inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather, scheduler_func_opt=scheduler_func_opt)
if not (enhanced_image is None):
if enhanced_image is not None:
new_image = new_image.cpu()
enhanced_image = enhanced_image.cpu()
left = seg.crop_region[0]
+12 -14
View File
@@ -298,7 +298,7 @@ class ImpactDummyInput:
class MasksToMaskList:
@classmethod
def INPUT_TYPES(s):
return {"required": {
return {"optional": {
"masks": ("MASK", ),
}
}
@@ -473,7 +473,7 @@ class NthItemOfAnyList:
class MakeImageList:
@classmethod
def INPUT_TYPES(s):
return {"required": {"image1": ("IMAGE",), }}
return {"optional": {"image1": ("IMAGE",), }}
RETURN_TYPES = ("IMAGE",)
OUTPUT_IS_LIST = (True,)
@@ -493,7 +493,7 @@ class MakeImageList:
class MakeImageBatch:
@classmethod
def INPUT_TYPES(s):
return {"required": {"image1": ("IMAGE",), }}
return {"optional": {"image1": ("IMAGE",), }}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "doit"
@@ -501,14 +501,13 @@ class MakeImageBatch:
CATEGORY = "ImpactPack/Util"
def doit(self, **kwargs):
image1 = kwargs['image1']
del kwargs['image1']
images = [value for value in kwargs.values()]
if len(images) == 0:
return (image1,)
if len(images) == 1:
return (images[0],)
else:
for image2 in images:
image1 = images[0]
for image2 in images[1:]:
if image1.shape[1:] != image2.shape[1:]:
image2 = comfy.utils.common_upscale(image2.movedim(-1, 1), image1.shape[2], image1.shape[1], "lanczos", "center").movedim(1, -1)
image1 = torch.cat((image1, image2), dim=0)
@@ -518,7 +517,7 @@ class MakeImageBatch:
class MakeMaskBatch:
@classmethod
def INPUT_TYPES(s):
return {"required": {"mask1": ("MASK",), }}
return {"optional": {"mask1": ("MASK",), }}
RETURN_TYPES = ("MASK",)
FUNCTION = "doit"
@@ -526,14 +525,13 @@ class MakeMaskBatch:
CATEGORY = "ImpactPack/Util"
def doit(self, **kwargs):
mask1 = kwargs['mask1']
del kwargs['mask1']
masks = [make_3d_mask(value) for value in kwargs.values()]
if len(masks) == 0:
return (mask1,)
if len(masks) == 1:
return (masks[0],)
else:
for mask2 in masks:
mask1 = masks[0]
for mask2 in masks[1:]:
if mask1.shape[1:] != mask2.shape[1:]:
mask2 = comfy.utils.common_upscale(mask2.movedim(-1, 1), mask1.shape[2], mask1.shape[1], "lanczos", "center").movedim(1, -1)
mask1 = torch.cat((mask1, mask2), dim=0)
+2 -2
View File
@@ -139,7 +139,7 @@ def process(text, seed=None):
if b is not None:
b = b.strip()
else:
b = "-1"
b = a
if r is not None:
if b is not None and is_numeric_string(a) and is_numeric_string(b):
@@ -572,7 +572,7 @@ def split_string_with_sep(input_string):
else:
try:
seed = int(matches[i][5:-1])
except:
except Exception:
seed = None
result_list.append(seed)
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-impact-pack"
description = "This node pack offers various detector nodes and detailer nodes that allow you to configure a workflow that automatically enhances facial details. And provide iterative upscaler."
version = "8.19"
version = "8.21.2"
license = { file = "LICENSE.txt" }
dependencies = ["segment-anything", "scikit-image", "piexif", "transformers", "opencv-python-headless", "GitPython", "scipy>=1.11.4"]
+3
View File
@@ -0,0 +1,3 @@
[lint]
ignore = ["E402","E701"]
exclude = ["install.py", "*.ipynb"]