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!
615 lines
24 KiB
Python
615 lines
24 KiB
Python
# 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,)
|