Files
Loghan LewisandClaude Opus 4.7 aee4391206 v0.2 — fixes from code review by blepping
- Fix LatentFrameInterleave axis: operate on T (dim=2), not C (dim=1).
  ComfyUI video latents are (B, C, T, H, W). Calling the old version on
  a video latent shuffled channels on individual frames rather than
  reordering frames in time.

- Replace closure counter in AttentionDropoutPatcher with a context-
  derived deterministic seed (block + sigma + seed). ComfyUI runs the
  patch node once at workflow start; the previous counter persisted
  across sampling runs and drifted unpredictably.

- Remove GlitchSampler node + euler_ancestral_step. Its extra_noise
  knob was equivalent to s_noise on the built-in SamplerEulerAncestral
  (which additionally exposes eta). The workflow and renderer now use
  the built-in directly.

- Rename extra_noise -> s_noise across schema, library, and example
  workflows. Max raised 4 -> 10 to match the built-in range.

- Re-render gallery with one photorealistic prompt + an unpatched
  KSampler reference. The previous abstract per-preset prompts hid the
  parameter effects.

- Delete stale UI-canvas example workflows; render_samples.py is the
  canonical entry point for batch / programmatic use.

- CHANGELOG.md added with full detail and credit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 16:35:40 +01:00

157 lines
5.4 KiB
Python

"""
ComfyUI node wrappers around the pure-math operations in perturbations.py.
Three nodes:
- GlitchSigmas — perturbed sigma schedule (MODEL → SIGMAS)
- AttentionDropoutPatcher — value-dropout patches on attention (MODEL → MODEL)
- LatentFrameInterleave — temporal reordering for video latents (LATENT → LATENT)
The earlier `GlitchSampler` node was removed (v0.2): its `extra_noise`
parameter was equivalent to `s_noise` on ComfyUI's built-in
`SamplerEulerAncestral`. Use that instead — it also exposes `eta`, which the
custom version didn't.
"""
from __future__ import annotations
from .perturbations import (
apply_sigma_jitter,
call_index_from_context,
interleave_frames,
value_dropout_patch,
)
# ComfyUI imports are deferred to call sites so this module can still be
# imported (e.g. by editors / language servers) outside a ComfyUI runtime.
SCHEDULER_NAMES = ["karras", "exponential", "normal", "simple", "ddim_uniform"]
class GlitchSigmas:
"""Perturbed sigma schedule. Outputs a SIGMAS tensor."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"scheduler": (SCHEDULER_NAMES, {"default": "karras"}),
"steps": ("INT", {"default": 20, "min": 1, "max": 1000}),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"jitter": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF}),
},
}
RETURN_TYPES = ("SIGMAS",)
CATEGORY = "glitch-lab"
FUNCTION = "build"
def build(self, model, scheduler, steps, denoise, jitter, seed):
import comfy.samplers
total = steps if denoise >= 1.0 else int(steps / max(denoise, 1e-6))
sigmas = comfy.samplers.calculate_sigmas(
model.get_model_object("model_sampling"), scheduler, total
)
if denoise < 1.0:
sigmas = sigmas[-(steps + 1):]
sigmas = apply_sigma_jitter(sigmas, jitter, seed)
return (sigmas,)
class AttentionDropoutPatcher:
"""Clones the model and installs value-dropout patches on attention layers.
The patches derive their per-call deterministic seed from the (block, sigma)
context ComfyUI passes in `extra_options`, not from a closure counter. The
counter approach was broken — see `call_index_from_context` for the
correction (credit: blepping, code review).
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"self_attn_dropout": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0.95, "step": 0.01}),
"cross_attn_dropout": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0.95, "step": 0.01}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF}),
},
}
RETURN_TYPES = ("MODEL",)
CATEGORY = "glitch-lab"
FUNCTION = "patch"
def patch(self, model, self_attn_dropout, cross_attn_dropout, seed):
m = model.clone()
def _sigma_from(extra_options):
t = extra_options.get("transformer_options", {}) or {}
s = t.get("sigmas")
if s is None:
return None
try:
return float(s[0]) if hasattr(s, "__len__") else float(s)
except (TypeError, ValueError):
return None
def self_patch(q, k, v, extra_options):
block = extra_options.get("block", ("?", 0))
sigma = _sigma_from(extra_options)
idx = call_index_from_context(seed, ("self", block), sigma)
return value_dropout_patch(q, k, v, self_attn_dropout, seed, idx)
def cross_patch(q, k, v, extra_options):
block = extra_options.get("block", ("?", 0))
sigma = _sigma_from(extra_options)
idx = call_index_from_context(seed + 1, ("cross", block), sigma)
return value_dropout_patch(q, k, v, cross_attn_dropout, seed + 1, idx)
if self_attn_dropout > 0.0:
m.set_model_attn1_patch(self_patch)
if cross_attn_dropout > 0.0:
m.set_model_attn2_patch(cross_patch)
return (m,)
class LatentFrameInterleave:
"""Temporal reordering for 5-D (B, C, T, H, W) latents. 4-D latents pass through."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"latent": ("LATENT",),
"intensity": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF}),
},
}
RETURN_TYPES = ("LATENT",)
CATEGORY = "glitch-lab"
FUNCTION = "run"
def run(self, latent, intensity, seed):
samples = latent["samples"]
out = interleave_frames(samples, intensity, seed)
new_latent = {k: v for k, v in latent.items()}
new_latent["samples"] = out
return (new_latent,)
NODE_CLASS_MAPPINGS = {
"GlitchSigmas": GlitchSigmas,
"AttentionDropoutPatcher": AttentionDropoutPatcher,
"LatentFrameInterleave": LatentFrameInterleave,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"GlitchSigmas": "Glitch Sigmas",
"AttentionDropoutPatcher": "Attention Dropout Patcher",
"LatentFrameInterleave": "Latent Frame Interleave",
}