Compare commits

...
4 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
6 changed files with 136 additions and 32 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, 20, 1]
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__)
+5
View File
@@ -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
+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)
+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.20.1"
version = "8.21.2"
license = { file = "LICENSE.txt" }
dependencies = ["segment-anything", "scikit-image", "piexif", "transformers", "opencv-python-headless", "GitPython", "scipy>=1.11.4"]