Files
Hyun-Puer-ComfyUI-Contextua…/nodes.py
T
2026-04-01 15:52:50 +00:00

152 lines
5.5 KiB
Python

from typing_extensions import override
import torch
import comfy.model_patcher
from comfy_api.latest import ComfyExtension, io
from .repulsion import compute_repulsion_gradient
class ContextualRepulsion(io.ComfyNode):
"""Applies contextual repulsion to text embeddings across a batch to increase
diversity of generated images, adapted from the Contextual Space Repulsion paper."""
@classmethod
def define_schema(cls):
return io.Schema(
node_id="ContextualRepulsion",
display_name="Contextual Repulsion (Diversity)",
category="model_patches",
description=(
"Increases batch diversity by applying repulsion to text conditioning "
"embeddings. Based on 'On-the-fly Repulsion in the Contextual Space' "
"(SIGGRAPH 2026). Requires batch_size >= 2."
),
inputs=[
io.Model.Input("model"),
io.Float.Input(
"repulsion_scale",
default=0.1,
min=0.0,
max=5.0,
step=0.01,
round=0.001,
tooltip="Repulsion strength. 0.1 = shift embeddings by ~10% of their magnitude per gradient step. Start with 0.05-0.2, increase for more diversity.",
),
io.Float.Input(
"timestep_cutoff",
default=0.35,
min=0.0,
max=1.0,
step=0.05,
round=0.01,
tooltip="Fraction of early denoising steps to apply repulsion. 0.35 = first 35% of steps.",
),
io.Int.Input(
"gradient_steps",
default=3,
min=1,
max=10,
tooltip="Number of iterative gradient steps per model call. More steps = stronger effect.",
),
],
outputs=[
io.Model.Output(),
],
)
@classmethod
def execute(cls, model, repulsion_scale, timestep_cutoff, gradient_steps) -> io.NodeOutput:
if repulsion_scale == 0.0:
return io.NodeOutput(model)
m = model.clone()
# Closure state for sigma progress tracking
sigma_state = {"max": None, "min": None}
def unet_wrapper(apply_model, params):
input_x = params["input"]
timestep = params["timestep"]
c = params["c"]
cond_or_uncond = params["cond_or_uncond"]
crossattn = c.get("c_crossattn")
if crossattn is None or len(cond_or_uncond) < 1:
return apply_model(input_x, timestep, **c)
# --- Sigma progress estimation ---
sigma = timestep[0].item()
if sigma_state["max"] is None or sigma > sigma_state["max"]:
sigma_state["max"] = sigma
if sigma_state["min"] is None or sigma < sigma_state["min"]:
sigma_state["min"] = sigma
sigma_range = sigma_state["max"] - sigma_state["min"]
if sigma_range > 0:
progress = (sigma_state["max"] - sigma) / sigma_range
else:
progress = 0.0 # First step — always apply
# Skip repulsion after cutoff
if progress > timestep_cutoff:
return apply_model(input_x, timestep, **c)
# --- Identify conditional embeddings ---
# cond_or_uncond: list like [0, 1] where 0=cond, 1=uncond
# crossattn is batched: each chunk has batch_size samples
batch_chunks = len(cond_or_uncond)
total_samples = crossattn.shape[0]
chunk_size = total_samples // batch_chunks
# Need at least 2 conditional samples for repulsion
if chunk_size < 2:
return apply_model(input_x, timestep, **c)
# Find which chunk indices are conditional (cond_or_uncond == 0)
cond_indices = [i for i, v in enumerate(cond_or_uncond) if v == 0]
if not cond_indices:
return apply_model(input_x, timestep, **c)
# Extract conditional embeddings
# Each chunk i occupies crossattn[i*chunk_size : (i+1)*chunk_size]
cond_slices = []
for idx in cond_indices:
start = idx * chunk_size
end = start + chunk_size
cond_slices.append((start, end))
cond_emb = torch.cat([crossattn[s:e] for s, e in cond_slices], dim=0)
# --- Apply iterative repulsion ---
for _ in range(gradient_steps):
grad = compute_repulsion_gradient(cond_emb)
cond_emb = cond_emb - repulsion_scale * grad
# --- Reassemble crossattn ---
modified = crossattn.clone()
offset = 0
for start, end in cond_slices:
size = end - start
modified[start:end] = cond_emb[offset:offset + size]
offset += size
c["c_crossattn"] = modified
return apply_model(input_x, timestep, **c)
m.set_model_unet_function_wrapper(unet_wrapper)
return io.NodeOutput(m)
class ContextualRepulsionExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
ContextualRepulsion,
]
async def comfy_entrypoint() -> ContextualRepulsionExtension:
return ContextualRepulsionExtension()