Files
EnragedAntelope-ComfyUI_Eas…/RegionalPrompting.py
T
Claude 963e73a894 CRITICAL: Add per-region strength controls for Flux
Based on user testing, per-region strength is REQUIRED:
- Both regions at 2.0: car appears, giraffe doesn't
- Car 2.0, giraffe 4.0: BOTH appear correctly

Changes:
- Added region1_strength, region2_strength, region3_strength, region4_strength inputs
- Set progressive defaults: 2.5, 3.5, 4.0, 4.0 (later regions need more strength)
- Updated function to use per-region strengths instead of canvas values
- Fixed CFG guidance: Base Flux uses CFG 1.0 ONLY (no negative prompt)
- Updated CONTEXT.md with critical findings

Why per-region strength:
- Background conditioning competes with regional conditioning
- Later regions need progressively higher strength to appear
- Global strength doesn't work - each region needs independent control
2025-11-22 13:59:59 +00:00

450 lines
19 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.
# Enhanced All-in-One Regional Prompting Nodes
# Created: 2025-11-22
# Inline prompt boxes - no external CLIP Text Encode nodes needed!
import torch
from nodes import MAX_RESOLUTION
import folder_paths
class RegionalPrompterSimple:
"""
ALL-IN-ONE regional prompting with inline text inputs!
Just connect your CLIP model, type prompts directly, and draw boxes.
No need for external CLIP Text Encode nodes!
Compatible with: SD1.5, SD2.x, SDXL (area-based conditioning)
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"clip": ("CLIP", {"tooltip": "CLIP model from your checkpoint"}),
"background_prompt": ("STRING", {
"default": "photo of a city street, high quality",
"multiline": True,
"tooltip": "Background/scene description (applies to whole image)"
}),
"region1_prompt": ("STRING", {
"default": "red sports car",
"multiline": True,
"tooltip": "Region 1 - First region/box (see canvas below)"
}),
},
"optional": {
"region2_prompt": ("STRING", {
"default": "street vendor",
"multiline": True,
"tooltip": "Region 2 - Second region/box (see canvas below)"
}),
"region3_prompt": ("STRING", {
"default": "",
"multiline": True,
"tooltip": "Region 3 - Third region/box (see canvas below)"
}),
"region4_prompt": ("STRING", {
"default": "",
"multiline": True,
"tooltip": "Region 4 - Fourth region/box (see canvas below)"
}),
},
"hidden": {"extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("CONDITIONING",)
RETURN_NAMES = ("conditioning",)
FUNCTION = "encode_regions"
CATEGORY = "RegionalPrompter"
DESCRIPTION = """🎨 ALL-IN-ONE Regional Prompting (EASY MODE!)
Just type your prompts and draw boxes - that's it!
✅ No external CLIP Text Encode nodes needed
✅ Simple workflow: CLIP → This Node → Sampler
✅ Up to 4 regions + background
✅ Visual box drawing interface
How to use:
1. Connect CLIP from your checkpoint
2. Type background prompt (whole image)
3. Type region prompts (for each box)
4. Draw boxes on canvas
5. Connect to sampler!
Compatible: SD1.5, SD2.x, SDXL"""
def encode_regions(self, clip, background_prompt, region1_prompt, extra_pnginfo, unique_id,
region2_prompt="", region3_prompt="", region4_prompt=""):
"""Encode all prompts and apply regional conditioning."""
# Default template boxes (if workflow not saved yet)
# Region 1 (red sports car): left side, 200x250px starting at (50, 150)
# Region 2 (street vendor): right side, 180x250px starting at (280, 150)
values = [
[50, 150, 200, 250, 1.0], # Region 1
[280, 150, 180, 250, 1.0] # Region 2
]
resolutionX = 512
resolutionY = 512
# Get region data from UI (overrides defaults if workflow saved)
try:
if extra_pnginfo and "workflow" in extra_pnginfo and "nodes" in extra_pnginfo["workflow"]:
for node in extra_pnginfo["workflow"]["nodes"]:
if node["id"] == int(unique_id):
saved_values = node["properties"].get("values", [])
if saved_values: # Only override if we actually have saved values
values = saved_values
resolutionX = node["properties"].get("width", 512)
resolutionY = node["properties"].get("height", 512)
break
except Exception as e:
print(f"ℹ️ Using default template boxes and canvas size 512x512")
# Collect all non-empty prompts
prompts = [background_prompt, region1_prompt, region2_prompt, region3_prompt, region4_prompt]
# Encode each prompt using CLIP (standard SD/SDXL encoding)
encoded_conditionings = []
for prompt in prompts:
if prompt and prompt.strip(): # Only encode non-empty prompts
# Standard CLIP encoding for SD/SDXL
tokens = clip.tokenize(prompt)
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
encoded_conditionings.append([[cond, {"pooled_output": pooled}]])
else:
encoded_conditionings.append(None)
# Apply regional conditioning using area-based method
c = []
# Background (fullscreen)
if encoded_conditionings[0]:
for t in encoded_conditionings[0]:
c.append(t)
# Process each region
for i in range(1, min(len(encoded_conditionings), len(values) + 1)):
if encoded_conditionings[i] is None:
continue
if i - 1 >= len(values):
continue
try:
x, y = int(values[i-1][0]), int(values[i-1][1])
w, h = int(values[i-1][2]), int(values[i-1][3])
strength = float(values[i-1][4]) if len(values[i-1]) > 4 else 2.0
except (IndexError, ValueError, TypeError):
continue
# Skip if fullscreen (already added as background)
if x == 0 and y == 0 and w == resolutionX and h == resolutionY:
continue
# Clip to bounds
if x + w > resolutionX:
w = max(0, resolutionX - x)
if y + h > resolutionY:
h = max(0, resolutionY - y)
if w == 0 or h == 0:
continue
# Apply area to conditioning
for t in encoded_conditionings[i]:
n = [t[0], t[1].copy()]
n[1]['area'] = (h // 8, w // 8, y // 8, x // 8)
n[1]['strength'] = max(0.0, min(10.0, strength))
n[1]['min_sigma'] = 0.0
n[1]['max_sigma'] = 99.0
c.append(n)
return (c,)
class RegionalPrompterFlux:
"""
ALL-IN-ONE regional prompting for FLUX/CHROMA with inline text inputs!
Just connect your CLIP model, type prompts directly, and draw boxes.
Automatically converts to masks optimized for Flux!
Compatible with: Flux, Chroma, SD3, SD3.5 (mask-based conditioning)
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"clip": ("CLIP", {"tooltip": "CLIP model from your checkpoint"}),
"width": ("INT", {
"default": 1024,
"min": 64,
"max": MAX_RESOLUTION,
"step": 64,
"tooltip": "Output width - must match your latent/sampler"
}),
"height": ("INT", {
"default": 1024,
"min": 64,
"max": MAX_RESOLUTION,
"step": 64,
"tooltip": "Output height - must match your latent/sampler"
}),
"soften_masks": ("BOOLEAN", {
"default": True,
"tooltip": "Enable feathering at region edges - recommended ON"
}),
"background_prompt": ("STRING", {
"default": "photo of a city street, high quality",
"multiline": True,
"tooltip": "Scene description (applies to entire image as base)"
}),
"region1_prompt": ("STRING", {
"default": "red sports car",
"multiline": True,
"tooltip": "Region 1 - First region/box (see canvas below)"
}),
},
"optional": {
"region1_strength": ("FLOAT", {
"default": 2.5,
"min": 0.0,
"max": 10.0,
"step": 0.1,
"tooltip": "Region 1 conditioning strength (2.5-4.0 recommended for Flux)"
}),
"region2_prompt": ("STRING", {
"default": "street vendor",
"multiline": True,
"tooltip": "Region 2 - Second region/box (see canvas below)"
}),
"region2_strength": ("FLOAT", {
"default": 3.5,
"min": 0.0,
"max": 10.0,
"step": 0.1,
"tooltip": "Region 2 conditioning strength (increase if region doesn't show)"
}),
"region3_prompt": ("STRING", {
"default": "",
"multiline": True,
"tooltip": "Region 3 - Third region/box (see canvas below)"
}),
"region3_strength": ("FLOAT", {
"default": 4.0,
"min": 0.0,
"max": 10.0,
"step": 0.1,
"tooltip": "Region 3 conditioning strength (increase if region doesn't show)"
}),
"region4_prompt": ("STRING", {
"default": "",
"multiline": True,
"tooltip": "Region 4 - Fourth region/box (see canvas below)"
}),
"region4_strength": ("FLOAT", {
"default": 4.0,
"min": 0.0,
"max": 10.0,
"step": 0.1,
"tooltip": "Region 4 conditioning strength (increase if region doesn't show)"
}),
},
"hidden": {"extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("CONDITIONING",)
RETURN_NAMES = ("conditioning",)
FUNCTION = "encode_regions_flux"
CATEGORY = "RegionalPrompter"
DESCRIPTION = """Mask-based regional prompting for Flux, Chroma, SD3, SD3.5, Qwen-Image.
Quick Start:
1. Connect CLIP from checkpoint
2. Set width/height to match your latent exactly
3. Type prompts (background + regions)
4. Adjust per-region strength (default: 2.5, 3.5, 4.0, 4.0)
5. Draw/adjust boxes on canvas
6. Use CFG 1.0 for Base Flux (no negative prompt)
Tips: Increase per-region strength if regions don't show. 3-4 regions max for Flux."""
def encode_regions_flux(self, clip, width, height, soften_masks, background_prompt, region1_prompt,
extra_pnginfo, unique_id,
region1_strength=2.5, region2_prompt="", region2_strength=3.5,
region3_prompt="", region3_strength=4.0, region4_prompt="", region4_strength=4.0):
"""Encode all prompts and apply mask-based regional conditioning for Flux/Chroma/SD3."""
# Default template boxes (if workflow not saved yet)
# Region 1 (red sports car): left side, 400x500px starting at (100, 300)
# Region 2 (street vendor): right side, 350x500px starting at (560, 300)
# NOTE: Strength values in 'values' array are now ignored - using per-region strength inputs instead
values = [
[100, 300, 400, 500, 0.0], # Region 1 - strength ignored (using region1_strength input)
[560, 300, 350, 500, 0.0] # Region 2 - strength ignored (using region2_strength input)
]
# Get region data from UI (overrides defaults if workflow saved)
try:
if extra_pnginfo and "workflow" in extra_pnginfo and "nodes" in extra_pnginfo["workflow"]:
for node in extra_pnginfo["workflow"]["nodes"]:
if node["id"] == int(unique_id):
saved_values = node["properties"].get("values", [])
if saved_values: # Only override if we actually have saved values
values = saved_values
# Override with property dimensions if available
prop_width = node["properties"].get("width", width)
prop_height = node["properties"].get("height", height)
break
except Exception as e:
print(f"ℹ️ Using default template boxes and dimensions {width}x{height}")
prop_width = width
prop_height = height
# Collect all non-empty prompts
prompts = [background_prompt, region1_prompt, region2_prompt, region3_prompt, region4_prompt]
# Count non-empty regions (excluding background)
num_regions = sum(1 for p in prompts[1:] if p and p.strip())
print(f"\n🎨 Regional Prompter - Processing:")
print(f" Dimensions: {width}x{height} (latent: {width//8}x{height//8})")
print(f" Regions: {num_regions} active (background + {num_regions} regions)")
print(f" Soften masks: {soften_masks}")
print(f" Default boxes: {values}")
if num_regions > 4:
print(f"⚠️ Warning: {num_regions} regions detected. Flux works best with 3-4 regions maximum.")
# Encode each prompt using CLIP
# ComfyUI's CLIP object handles multi-encoder complexity internally
# Works for all models: Flux (2 encoders), SD3 (3 encoders), Chroma/Qwen (1 encoder), etc.
encoded_conditionings = []
for prompt in prompts:
if prompt and prompt.strip():
tokens = clip.tokenize(prompt)
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
encoded_conditionings.append([[cond, {"pooled_output": pooled}]])
else:
encoded_conditionings.append(None)
# Apply mask-based conditioning
combined_conditioning = []
# Determine mask strength based on soften setting
# Research shows 0.8 (softer masks) work better than full 1.0 strength for Flux
# Users can try it for Chroma/SD3 (default ON), disable if they prefer sharper edges
mask_strength = 0.8 if soften_masks else 1.0
# Background (fullscreen - no mask)
if encoded_conditionings[0]:
for t in encoded_conditionings[0]:
combined_conditioning.append(t)
# Per-region strength values from inputs (not canvas)
region_strengths = [region1_strength, region2_strength, region3_strength, region4_strength]
# Process each region with masks
latent_width = width // 8
latent_height = height // 8
for i in range(1, min(len(encoded_conditionings), len(values) + 1)):
if encoded_conditionings[i] is None:
continue
if i - 1 >= len(values):
continue
try:
x, y = int(values[i-1][0]), int(values[i-1][1])
w, h = int(values[i-1][2]), int(values[i-1][3])
# Use per-region strength from inputs instead of canvas values
strength = region_strengths[i-1] if i-1 < len(region_strengths) else 2.5
except (IndexError, ValueError, TypeError):
continue
# Get canvas dimensions
canvas_width = prop_width if 'prop_width' in locals() else width
canvas_height = prop_height if 'prop_height' in locals() else height
# Skip fullscreen (already added as background)
if x == 0 and y == 0 and w == canvas_width and h == canvas_height:
continue
# Scale to output dimensions if needed
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)
# Clip to bounds
x = max(0, min(x, width))
y = max(0, min(y, height))
w = max(0, min(w, width - x))
h = max(0, min(h, height - y))
if w == 0 or h == 0:
continue
# Convert to latent space
x_latent = x // 8
y_latent = y // 8
w_latent = max(1, w // 8)
h_latent = max(1, h // 8)
# Create mask with Flux-optimized strength
mask = torch.zeros((1, latent_height, latent_width), dtype=torch.float32)
x_end = min(x_latent + w_latent, latent_width)
y_end = min(y_latent + h_latent, latent_height)
# Fill mask with specified strength
mask[0, y_latent:y_end, x_latent:x_end] = mask_strength
print(f" Region {i}: '{prompts[i][:40]}...'")
print(f" Box: x={x}, y={y}, w={w}, h={h}")
print(f" Latent: x={x_latent}, y={y_latent}, w={w_latent}, h={h_latent}")
print(f" Mask strength: {mask_strength}, Region strength: {strength}")
# Apply feathering if softening enabled (gentle edge blending to avoid harsh boundaries)
if soften_masks:
# Apply gentle gaussian-like feather at edges (40-60px = 5-8 latent pixels)
feather_size = min(6, w_latent // 4, h_latent // 4)
if feather_size > 0:
for edge_idx in range(feather_size):
fade = (edge_idx + 1) / feather_size * mask_strength
# Top edge
if y_latent + edge_idx < y_end:
mask[0, y_latent + edge_idx, x_latent:x_end] *= (fade / mask_strength)
# Bottom edge
if y_end - 1 - edge_idx >= y_latent:
mask[0, y_end - 1 - edge_idx, x_latent:x_end] *= (fade / mask_strength)
# Left edge
if x_latent + edge_idx < x_end:
mask[0, y_latent:y_end, x_latent + edge_idx] *= (fade / mask_strength)
# Right edge
if x_end - 1 - edge_idx >= x_latent:
mask[0, y_latent:y_end, x_end - 1 - edge_idx] *= (fade / mask_strength)
# Apply mask to conditioning
for t in encoded_conditionings[i]:
n = [t[0], t[1].copy()]
n[1]['mask'] = mask
n[1]['mask_strength'] = max(0.0, min(10.0, strength)) # FIXED: was 'strength', should be 'mask_strength'
n[1]['set_area_to_bounds'] = False
combined_conditioning.append(n)
print(f" ✅ Generated {len(combined_conditioning)} conditioning blocks")
print(f" 💡 Tip: For Base Flux, use CFG 1.0 with no negative prompt\n")
return (combined_conditioning,)
# Note: These enhanced nodes need the same JavaScript UI as the original nodes
# They will use the canvas interface from MultiAreaConditioning/MultiAreaConditioningMask