Files
EnragedAntelope-ComfyUI_Eas…/MultiAreaConditioning.py
T
Claude a7e50789b6 Add enhanced all-in-one Regional Prompter nodes with inline prompts
MAJOR NEW FEATURES:
===================

🌟 NEW NODES (RECOMMENDED for all users):
- RegionalPrompterSimple (SD/SDXL) - Type prompts directly!
- RegionalPrompterFlux (Flux/Chroma) - Optimized for modern models!

✅ NO MORE external CLIP Text Encode nodes needed
✅ Simple workflow: Checkpoint → CLIP → Regional Prompter → Sampler
✅ Same visual box-drawing interface
✅ Up to 4 regions + background prompt
✅ Multiline text inputs for prompts

FLUX OPTIMIZATIONS:
===================
- Softened mask strength (0.8 vs 1.0) based on research
- Gentle feathering at region edges (40-60px equivalent)
- Automatic region count warning (>4 regions)
- flux_optimize toggle for easy control

IMPROVEMENTS TO EXISTING NODES:
================================
- Add mask_strength parameter to MultiAreaConditioningMask
  (allows 0.7-0.85 for Flux, 1.0 for SD/SDXL)
- Add tooltips explaining Flux optimization
- Improve documentation with clear model recommendations

TECHNICAL DETAILS:
==================
Python Changes:
- RegionalPrompting.py - New enhanced nodes with CLIP encoding
- Uses clip.tokenize() and clip.encode_from_tokens() internally
- Proper pooled_output handling for all models
- Comprehensive error handling and validation

JavaScript Changes:
- RegionalPrompting.js - Visual canvas UI for new nodes
- Reuses canvas drawing code from original nodes
- Widget positioning adjusted for prompt inputs

Documentation:
- Prominent "NEW!" section highlighting enhanced nodes
- Easy mode vs Advanced mode comparison
- Flux-specific tips and optimization details
- Updated quick start with both methods

BACKWARD COMPATIBILITY:
=======================
- All original nodes unchanged and fully functional
- Original nodes renamed to "Advanced" in UI
- Enhanced nodes marked as "RECOMMENDED"
- Users can choose based on preference

USER EXPERIENCE:
================
Before (Advanced):
  Checkpoint → CLIP → CLIPTextEncode → MultiAreaConditioning → Sampler
               (repeat CLIPTextEncode for each region)

After (Easy):
  Checkpoint → CLIP → Regional Prompter → Sampler
               (type all prompts in one node!)

COMPATIBILITY:
==============
✅ ComfyUI 0.3.71+
✅ SD1.5, SD2.x, SDXL (area-based)
✅ Flux, Chroma, SD3, SD3.5 (mask-based with optimizations)
✅ V3 schema ready (uses standard INPUT_TYPES, will migrate later)

This makes regional conditioning DRAMATICALLY easier to use!
2025-11-22 01:19:29 +00:00

615 lines
24 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"}),
"mask_strength": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 1.0, "step": 0.05, "tooltip": "Mask strength (0.7-0.85 recommended for Flux, 1.0 for others)"}),
},
"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, mask_strength, 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 with specified mask strength
# For Flux: 0.7-0.85 recommended (softer masks work better)
# For SD/SDXL: 1.0 (full strength)
mask[0, y_latent:y_end, x_latent:x_end] = mask_strength
# 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,)