Files
EnragedAntelope-ComfyUI_Eas…/MultiAreaConditioning.py
T
Claude 7ed633055d Modernize for ComfyUI 0.3.71+ with Flux/Chroma support
Major Changes:
- Add MultiAreaConditioningMask node for Flux/Chroma/SD3 support
- Implement comprehensive error handling and input validation
- Add bounds checking for MultiLatentComposite (fixes known issue)
- Modernize JavaScript loading (js/ folder, no file copying)
- Add tooltips and descriptions to all node inputs

Fixes:
- Remove debug console.log statements
- Fix graph reference bug in background drawing (app.graph vs graph)
- Fix typo in variable name (inputLenght -> inputLength)
- Improve comment accuracy

Enhancements:
- Add detailed error messages with emoji indicators (⚠️ ℹ️ ❌)
- Implement graceful fallback behavior when metadata missing
- Add tensor shape and type validation
- Add channel compatibility checking in MultiLatentComposite
- Improve feathering with proper gradient calculation
- Add bounds clipping with informative warnings

Documentation:
- Complete README rewrite with compatibility matrix
- Add usage examples for Flux, Chroma, SD, SDXL
- Document all node inputs/outputs with descriptions
- Add troubleshooting section
- Add changelog with version 3.0 details

JavaScript:
- Move from javascript/ to js/ for modern ComfyUI compatibility
- Clean up all console.log debug statements
- Fix graph._nodes bug (should be app.graph._nodes)
- Add MultiAreaConditioningMask.js for mask-based node
- Improve code comments and formatting

Compatibility:
- SD1.5, SD2.x, SDXL (area-based, existing functionality)
- Flux, Chroma, SD3, SD3.5 (mask-based, NEW)
- Experimental Qwen-Image support noted
- WAN 2.2 marked as not supported (video architecture)

All changes maintain backward compatibility with existing workflows
while adding modern model support and significantly improving
robustness and user experience.
2025-11-22 01:10:55 +00:00

612 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Made by Davemane42#0042 for ComfyUI
# Updated: 2025-11-22
# Modernized for ComfyUI 0.3.71+ with comprehensive error handling and validation
import torch
from nodes import MAX_RESOLUTION
class MultiAreaConditioning:
"""
Visual interface for area-based regional conditioning.
Compatible with: SD1.5, SD2.x, SDXL
Draw rectangular boxes and assign different prompts to each region.
Perfect for composing images with multiple subjects in specific locations.
Example: "photo of mountain" (background) + box with "tiger" + box with "berry bush"
"""
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"conditioning0": ("CONDITIONING", {"tooltip": "Base/background conditioning (fullscreen if no box drawn)"}),
"conditioning1": ("CONDITIONING", {"tooltip": "Conditioning for first region/box"})
},
"hidden": {"extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("CONDITIONING", "INT", "INT")
RETURN_NAMES = ("conditioning", "resolutionX", "resolutionY")
FUNCTION = "apply_regional_conditioning"
CATEGORY = "Davemane42"
DESCRIPTION = """Visual area-based regional conditioning for SD1.5/SD2.x/SDXL.
Draw boxes on the canvas and connect different prompts to each region. Use right-click menu to add/remove/swap conditioning layers.
Features:
• Visual box drawing interface with color-coded regions
• Grid overlay (64px) for precise alignment
• Strength control per region (0.0-10.0)
• Fullscreen detection for background conditioning
• Automatic boundary clipping
Compatible Models: SD1.5, SD2.x, SDXL"""
def apply_regional_conditioning(self, extra_pnginfo, unique_id, **kwargs):
"""Apply area-based conditioning to rectangular regions."""
c = []
values = []
resolutionX = 512
resolutionY = 512
# Validate extra_pnginfo structure with helpful error messages
try:
if not extra_pnginfo:
raise ValueError("Workflow metadata (extra_pnginfo) is missing. Please save and reload the workflow.")
if "workflow" not in extra_pnginfo:
raise ValueError("Workflow data not found in metadata. Please save and reload the workflow.")
if "nodes" not in extra_pnginfo["workflow"]:
raise ValueError("Node data not found in workflow metadata. Please save and reload the workflow.")
# Find this node's properties in the workflow
node_found = False
for node in extra_pnginfo["workflow"]["nodes"]:
if node["id"] == int(unique_id):
node_found = True
if "properties" not in node:
raise ValueError(f"Node {unique_id} is missing properties. Please recreate the node.")
values = node["properties"].get("values", [])
resolutionX = node["properties"].get("width", 512)
resolutionY = node["properties"].get("height", 512)
break
if not node_found:
raise ValueError(f"Node with ID {unique_id} not found in workflow. Please save and reload the workflow.")
except Exception as e:
print(f"\n❌ MultiAreaConditioning Error: {str(e)}")
print(f" Using fallback values - please check your workflow setup.\n")
# Fallback: pass through first conditioning input
if "conditioning0" in kwargs:
return (kwargs["conditioning0"], 512, 512)
raise
# Validate resolution values
if not isinstance(resolutionX, (int, float)) or resolutionX <= 0:
print(f"⚠️ Warning: Invalid resolutionX ({resolutionX}), using 512")
resolutionX = 512
if not isinstance(resolutionY, (int, float)) or resolutionY <= 0:
print(f"⚠️ Warning: Invalid resolutionY ({resolutionY}), using 512")
resolutionY = 512
# Process conditioning inputs
k = 0
for arg in kwargs:
if k >= len(values):
break
# Validate conditioning input
if not isinstance(kwargs[arg], list):
print(f"⚠️ Warning: conditioning{k} is not a valid CONDITIONING type, skipping")
k += 1
continue
if len(kwargs[arg]) == 0:
print(f"⚠️ Warning: conditioning{k} is empty, skipping")
k += 1
continue
# Check if conditioning contains valid tensors
if not torch.is_tensor(kwargs[arg][0][0]):
print(f"⚠️ Warning: conditioning{k} does not contain valid tensor data, skipping")
k += 1
continue
# Parse region values with validation
try:
x, y = int(values[k][0]), int(values[k][1])
w, h = int(values[k][2]), int(values[k][3])
except (IndexError, ValueError, TypeError) as e:
print(f"⚠️ Warning: Invalid region values for conditioning{k}: {values[k]}, skipping")
k += 1
continue
# If fullscreen (background conditioning)
if (x == 0 and y == 0 and w == resolutionX and h == resolutionY):
for t in kwargs[arg]:
c.append(t)
k += 1
continue
# Clip to canvas boundaries with validation
if x + w > resolutionX:
w = max(0, resolutionX - x)
if y + h > resolutionY:
h = max(0, resolutionY - y)
# Skip zero-size regions
if w == 0 or h == 0:
print(f"⚠️ Warning: Region {k} has zero width or height after clipping, skipping")
k += 1
continue
# Apply area-based conditioning to each element
for t in kwargs[arg]:
# Validate conditioning structure
if not isinstance(t, (list, tuple)) or len(t) < 2:
print(f"⚠️ Warning: Invalid conditioning structure in conditioning{k}, skipping")
continue
# Create new conditioning with area
n = [t[0], t[1].copy()]
# Convert to latent space coordinates (divide by 8 for VAE downscaling)
n[1]['area'] = (h // 8, w // 8, y // 8, x // 8)
# Parse strength with validation
try:
strength = float(values[k][4]) if len(values[k]) > 4 else 1.0
strength = max(0.0, min(10.0, strength)) # Clamp to valid range
except (IndexError, ValueError, TypeError):
strength = 1.0
print(f"⚠️ Warning: Invalid strength for region {k}, using 1.0")
n[1]['strength'] = strength
n[1]['min_sigma'] = 0.0
n[1]['max_sigma'] = 99.0
c.append(n)
k += 1
# Validate output
if len(c) == 0:
print("⚠️ Warning: No valid conditioning regions generated, returning empty conditioning")
# Return empty conditioning with proper structure
c = []
return (c, int(resolutionX), int(resolutionY))
class MultiAreaConditioningMask:
"""
Visual interface for mask-based regional conditioning.
Compatible with: Flux, Chroma, SD3+, and any model supporting ConditioningSetMask
Same visual box-drawing interface as MultiAreaConditioning, but automatically
converts boxes to masks for models that require mask-based conditioning.
Perfect for modern models like Flux and Chroma that don't support area-based conditioning.
"""
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"conditioning0": ("CONDITIONING", {"tooltip": "Base/background conditioning"}),
"conditioning1": ("CONDITIONING", {"tooltip": "Conditioning for first region/box"}),
"width": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 64, "tooltip": "Output width in pixels"}),
"height": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 64, "tooltip": "Output height in pixels"}),
},
"hidden": {"extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("CONDITIONING",)
RETURN_NAMES = ("conditioning",)
FUNCTION = "apply_regional_conditioning_mask"
CATEGORY = "Davemane42"
DESCRIPTION = """Visual mask-based regional conditioning for Flux/Chroma/SD3+.
Same visual box-drawing interface as area-based version, but uses masks internally for compatibility with modern models.
Features:
• Draw boxes visually - automatically converted to masks
• Grid overlay (64px) for precise alignment
• Strength control per region (0.0-10.0)
• Full compatibility with Flux, Chroma, SD3, and other mask-based models
Compatible Models: Flux, Chroma, SD3, SD3.5, and any model supporting ConditioningSetMask
Note: Boxes are automatically converted to grayscale masks behind the scenes."""
def apply_regional_conditioning_mask(self, width, height, extra_pnginfo, unique_id, **kwargs):
"""Apply mask-based conditioning by converting boxes to masks."""
values = []
# Validate extra_pnginfo structure
try:
if not extra_pnginfo:
raise ValueError("Workflow metadata (extra_pnginfo) is missing. Please save and reload the workflow.")
if "workflow" not in extra_pnginfo:
raise ValueError("Workflow data not found in metadata. Please save and reload the workflow.")
if "nodes" not in extra_pnginfo["workflow"]:
raise ValueError("Node data not found in workflow metadata. Please save and reload the workflow.")
# Find this node's properties in the workflow
node_found = False
for node in extra_pnginfo["workflow"]["nodes"]:
if node["id"] == int(unique_id):
node_found = True
if "properties" not in node:
raise ValueError(f"Node {unique_id} is missing properties. Please recreate the node.")
values = node["properties"].get("values", [])
# Use properties width/height if available, otherwise use input parameters
prop_width = node["properties"].get("width", width)
prop_height = node["properties"].get("height", height)
# Validate property dimensions match input dimensions
if prop_width != width or prop_height != height:
print(f"ℹ️ Info: Canvas size ({prop_width}x{prop_height}) differs from output ({width}x{height})")
print(f" Regions will be scaled proportionally")
break
if not node_found:
raise ValueError(f"Node with ID {unique_id} not found in workflow. Please save and reload the workflow.")
except Exception as e:
print(f"\n❌ MultiAreaConditioningMask Error: {str(e)}")
print(f" Using fallback - passing through first conditioning input.\n")
# Fallback: pass through first conditioning input
if "conditioning0" in kwargs:
return (kwargs["conditioning0"],)
raise
# Validate dimensions
if width <= 0 or height <= 0:
raise ValueError(f"Invalid dimensions: {width}x{height}. Width and height must be positive.")
# Start with empty conditioning list
combined_conditioning = []
# Process each conditioning input
k = 0
for arg in sorted(kwargs.keys()): # Process in order
if k >= len(values):
break
# Validate conditioning input
if not isinstance(kwargs[arg], list):
print(f"⚠️ Warning: {arg} is not a valid CONDITIONING type, skipping")
k += 1
continue
if len(kwargs[arg]) == 0:
print(f"⚠️ Warning: {arg} is empty, skipping")
k += 1
continue
# Parse region values
try:
x, y = int(values[k][0]), int(values[k][1])
w, h = int(values[k][2]), int(values[k][3])
except (IndexError, ValueError, TypeError) as e:
print(f"⚠️ Warning: Invalid region values for {arg}: {values[k]}, skipping")
k += 1
continue
# Get canvas dimensions from properties
canvas_width = prop_width if 'prop_width' in locals() else width
canvas_height = prop_height if 'prop_height' in locals() else height
# Check if fullscreen (no mask needed)
is_fullscreen = (x == 0 and y == 0 and w == canvas_width and h == canvas_height)
if is_fullscreen:
# Fullscreen: pass through without mask
for t in kwargs[arg]:
combined_conditioning.append(t)
k += 1
continue
# Clip to canvas boundaries
x = max(0, min(x, canvas_width))
y = max(0, min(y, canvas_height))
w = max(0, min(w, canvas_width - x))
h = max(0, min(h, canvas_height - y))
# Skip zero-size regions
if w == 0 or h == 0:
print(f"⚠️ Warning: Region {k} has zero width or height, skipping")
k += 1
continue
# Create mask for this region (in latent space for efficiency)
latent_width = width // 8
latent_height = height // 8
# Scale region coordinates to match output dimensions
if canvas_width != width or canvas_height != height:
x = int(x * width / canvas_width)
y = int(y * height / canvas_height)
w = int(w * width / canvas_width)
h = int(h * height / canvas_height)
# Convert to latent space coordinates
x_latent = x // 8
y_latent = y // 8
w_latent = max(1, w // 8) # Ensure at least 1 pixel
h_latent = max(1, h // 8)
# Create binary mask
mask = torch.zeros((1, latent_height, latent_width), dtype=torch.float32)
# Ensure we don't go out of bounds
x_end = min(x_latent + w_latent, latent_width)
y_end = min(y_latent + h_latent, latent_height)
# Fill the region
mask[0, y_latent:y_end, x_latent:x_end] = 1.0
# Parse strength with validation
try:
strength = float(values[k][4]) if len(values[k]) > 4 else 1.0
strength = max(0.0, min(10.0, strength))
except (IndexError, ValueError, TypeError):
strength = 1.0
print(f"⚠️ Warning: Invalid strength for region {k}, using 1.0")
# Apply mask to conditioning
for t in kwargs[arg]:
if not isinstance(t, (list, tuple)) or len(t) < 2:
print(f"⚠️ Warning: Invalid conditioning structure in {arg}, skipping")
continue
n = [t[0], t[1].copy()]
n[1]['mask'] = mask
n[1]['strength'] = strength
n[1]['set_area_to_bounds'] = False
combined_conditioning.append(n)
k += 1
if len(combined_conditioning) == 0:
print("⚠️ Warning: No valid conditioning regions generated, returning empty conditioning")
return (combined_conditioning,)
class ConditioningUpscale():
"""
Upscale conditioning areas by a scalar factor.
Useful for hi-res fix workflows where you want to scale up your
regional conditioning along with the image resolution.
"""
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"conditioning": ("CONDITIONING", {"tooltip": "Conditioning to upscale"}),
"scalar": ("INT", {"default": 2, "min": 1, "max": 100, "step": 0.5, "tooltip": "Scaling factor (2 = 2x resolution)"}),
},
}
RETURN_TYPES = ("CONDITIONING",)
CATEGORY = "Davemane42"
FUNCTION = 'upscale'
DESCRIPTION = """Upscale conditioning areas by a scalar factor.
Multiplies all area dimensions by the specified scalar. Useful for hi-res fix workflows.
Example: If you have 512x512 regions and use scalar=2, they become 1024x1024."""
def upscale(self, conditioning, scalar):
"""Upscale conditioning areas by scalar factor."""
if not isinstance(conditioning, list):
raise ValueError("Invalid conditioning input - must be CONDITIONING type")
if scalar <= 0:
raise ValueError(f"Invalid scalar {scalar} - must be positive")
c = []
for t in conditioning:
if not isinstance(t, (list, tuple)) or len(t) < 2:
print("⚠️ Warning: Invalid conditioning structure, skipping entry")
continue
n = [t[0], t[1].copy()]
# Only upscale if area exists
if 'area' in n[1]:
try:
# Scale and round to nearest 8 pixels (latent space alignment)
n[1]['area'] = tuple(map(lambda x: ((x * scalar + 7) >> 3) << 3, n[1]['area']))
except Exception as e:
print(f"⚠️ Warning: Failed to upscale area: {e}")
c.append(n)
return (c,)
class ConditioningStretch():
"""
Stretch/resize conditioning areas to fit new dimensions.
Proportionally transforms conditioning regions when changing image resolution.
More flexible than ConditioningUpscale - specify exact target dimensions.
"""
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"conditioning": ("CONDITIONING", {"tooltip": "Conditioning to resize"}),
"resolutionX": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64, "tooltip": "Current/original width"}),
"resolutionY": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64, "tooltip": "Current/original height"}),
"newWidth": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64, "tooltip": "Target width"}),
"newHeight": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64, "tooltip": "Target height"}),
},
}
RETURN_TYPES = ("CONDITIONING",)
CATEGORY = "Davemane42"
FUNCTION = 'upscale'
DESCRIPTION = """Stretch/resize conditioning areas to fit new dimensions.
Proportionally transforms regions from one resolution to another. Perfect for when you change canvas size mid-workflow.
Example: Transform regions from 512x512 to 1024x768 - regions are scaled proportionally to fit."""
def upscale(self, conditioning, resolutionX, resolutionY, newWidth, newHeight, scalar=1):
"""Stretch conditioning areas to new resolution."""
if not isinstance(conditioning, list):
raise ValueError("Invalid conditioning input - must be CONDITIONING type")
if resolutionX <= 0 or resolutionY <= 0:
raise ValueError(f"Invalid original resolution: {resolutionX}x{resolutionY}")
if newWidth <= 0 or newHeight <= 0:
raise ValueError(f"Invalid target resolution: {newWidth}x{newHeight}")
c = []
for t in conditioning:
if not isinstance(t, (list, tuple)) or len(t) < 2:
print("⚠️ Warning: Invalid conditioning structure, skipping entry")
continue
n = [t[0], t[1].copy()]
# Only stretch if area exists
if 'area' in n[1]:
try:
newWidth_scaled = newWidth * scalar
newHeight_scaled = newHeight * scalar
# Calculate new coordinates proportionally
# area format: (height, width, y, x) in latent space
x = ((n[1]['area'][3] * 8) * newWidth_scaled / resolutionX) // 8
y = ((n[1]['area'][2] * 8) * newHeight_scaled / resolutionY) // 8
w = ((n[1]['area'][1] * 8) * newWidth_scaled / resolutionX) // 8
h = ((n[1]['area'][0] * 8) * newHeight_scaled / resolutionY) // 8
# Round to nearest 8 pixels (latent space alignment)
n[1]['area'] = tuple(map(lambda x: (((int(x) + 7) >> 3) << 3), [h, w, y, x]))
except Exception as e:
print(f"⚠️ Warning: Failed to stretch area: {e}")
c.append(n)
return (c,)
class ConditioningDebug():
"""
Debug node to print conditioning area information.
Useful for troubleshooting regional conditioning setups.
Displays all region coordinates, dimensions, and strength values.
"""
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"conditioning": ("CONDITIONING", {"tooltip": "Conditioning to inspect"}),
}
}
RETURN_TYPES = ()
FUNCTION = "debug"
OUTPUT_NODE = True
CATEGORY = "Davemane42"
DESCRIPTION = """Debug conditioning by printing region information to console.
Displays:
• Region coordinates (x, y)
• Region dimensions (width, height)
• Strength values
• Fullscreen vs regional status
Check the console/terminal for output."""
def debug(self, conditioning):
"""Print conditioning information for debugging."""
print("\n" + "="*50)
print("🔍 Conditioning Debug Output")
print("="*50)
if not isinstance(conditioning, list):
print("❌ Error: conditioning is not a valid list")
return (None,)
if len(conditioning) == 0:
print("⚠️ Warning: conditioning is empty")
return (None,)
for i, t in enumerate(conditioning):
print(f"\nRegion {i}:")
if not isinstance(t, (list, tuple)) or len(t) < 2:
print(" ❌ Invalid structure")
continue
if "area" in t[1]:
# area format: (height, width, y, x) in latent space (divided by 8)
area = t[1]['area']
print(f" 📍 Position: x={area[3]*8}, y={area[2]*8}")
print(f" 📐 Size: width={area[1]*8}, height={area[0]*8}")
if 'strength' in t[1]:
print(f" 💪 Strength: {t[1]['strength']}")
if 'min_sigma' in t[1] or 'max_sigma' in t[1]:
print(f" 🎚️ Sigma range: {t[1].get('min_sigma', 'N/A')} - {t[1].get('max_sigma', 'N/A')}")
else:
print(" 🖼️ Fullscreen (no area restriction)")
# Check for mask
if 'mask' in t[1]:
mask = t[1]['mask']
if torch.is_tensor(mask):
print(f" 🎭 Mask: shape={list(mask.shape)}, coverage={mask.sum().item()/(mask.numel()) * 100:.1f}%")
else:
print(" 🎭 Mask: present")
print("\n" + "="*50 + "\n")
return (None,)