Files
facok-ComfyUI-DiversityBoost/sampling.py
T
facok 4d6794e7c5 Release V3: polynomial frequency modulation + legacy node
Major rewrite of DiversityBoost core:

- Replace Butterworth LPF with polynomial frequency modulation
  (smooth continuous curve, token-grid normalized via DiT patch_size)
- Add hf_factor, lf_factor, transition parameters for fine control
- Add schedule parameter (flat/linear/cosine) for timestep decay
- Protect near-DC frequencies (r_norm < 1.0) from dc_preserve
  to prevent brightness/color shift
- Fix DC handling: step 1+ always preserves full DC
- Support 5D video latents

Rename Core node to V3 (node_id=DiversityBoostCoreV3).
Preserve old node as Legacy (node_id=DiversityBoostCore) for
backward compatibility.

Remove experimental DCW and Smart Patch nodes (superseded by V3).

Aggressive defaults tuned for strong diversity:
  strength=2.0, clamp=0.5, hf=1.0, lf=0.3, trans=2.0, sched=linear
2026-04-25 04:31:06 +08:00

140 lines
5.2 KiB
Python

"""Shared sampling utilities for DiversityBoost hooks."""
import math
import comfy.utils
import torch
def find_step_index(sigma, sigmas):
"""Find the step index for a given sigma value in the sigma schedule.
Uses torch.isclose for robust matching across dtype differences (e.g. bfloat16
sigma vs float32 sample_sigmas), with argmin fallback for edge cases.
"""
sigma_val = sigma.flatten()[0].float()
sigmas_f = sigmas.float()
matched = torch.isclose(sigmas_f, sigma_val, rtol=1e-3, atol=1e-5).nonzero()
if len(matched) > 0:
return matched[0].item()
return (sigmas_f - sigma_val).abs().argmin().item()
def denoised_to_raw(denoised, model):
"""Convert denoised tensor from process_in space to raw VAE space."""
return model.latent_format.process_out(denoised)
def raw_to_denoised(raw, model):
"""Convert raw VAE space tensor back to process_in space."""
return model.latent_format.process_in(raw)
def unpack_video_if_needed(denoised, args):
"""Unpack LTXAV-style packed latents if detected.
Returns (tensor_to_process, pack_info) where pack_info is None for
non-packed formats or a dict for repacking.
"""
if denoised.ndim == 3 and denoised.shape[1] == 1:
cond = args.get("cond")
latent_shapes = _extract_latent_shapes(cond)
if latent_shapes is not None and len(latent_shapes) > 1:
tensors = comfy.utils.unpack_latents(denoised, latent_shapes)
return tensors[0], {"other_tensors": tensors[1:]}
return denoised, None
def repack_video_if_needed(modified, pack_info):
"""Repack video tensor back into LTXAV packed format if it was unpacked."""
if pack_info is None:
return modified
all_tensors = [modified] + pack_info["other_tensors"]
packed, _ = comfy.utils.pack_latents(all_tensors)
return packed
def _extract_latent_shapes(cond):
"""Try to extract latent_shapes from conditioning data."""
if cond is None:
return None
for c in cond:
if isinstance(c, dict):
model_conds = c.get('model_conds', {})
if 'latent_shapes' in model_conds:
ls = model_conds['latent_shapes']
if hasattr(ls, 'cond'):
return ls.cond
return ls
return None
# ---------------------------------------------------------------------------
# Shared saturation helper
# ---------------------------------------------------------------------------
def apply_saturation(field, mode, max_amp, hard_clamp_val, min_scale=0.10):
"""Map normalized field to multiplicative scale.
mode="tanh": smooth symmetric saturation, scale ∈ [1-max_amp, 1+max_amp]
mode="hard" : clamp(1+field, min_scale, 1+hard_clamp_val)
"""
if mode == "tanh":
return 1.0 + torch.tanh(field) * max_amp
return (1.0 + field).clamp(min=min_scale, max=1.0 + hard_clamp_val)
# ---------------------------------------------------------------------------
# Pure PyTorch Haar DWT/IDWT
# ---------------------------------------------------------------------------
def dwt2_haar(x):
"""2D Haar DWT. x: [B, C, H, W] where H, W are even.
Returns ll, lh, hl, hh each [B, C, H//2, W//2].
"""
x_l = (x[:, :, :, 0::2] + x[:, :, :, 1::2]) / math.sqrt(2.0)
x_h = (x[:, :, :, 0::2] - x[:, :, :, 1::2]) / math.sqrt(2.0)
ll = (x_l[:, :, 0::2, :] + x_l[:, :, 1::2, :]) / math.sqrt(2.0)
lh = (x_l[:, :, 0::2, :] - x_l[:, :, 1::2, :]) / math.sqrt(2.0)
hl = (x_h[:, :, 0::2, :] + x_h[:, :, 1::2, :]) / math.sqrt(2.0)
hh = (x_h[:, :, 0::2, :] - x_h[:, :, 1::2, :]) / math.sqrt(2.0)
return ll, lh, hl, hh
def idwt2_haar(ll, lh, hl, hh):
"""2D Haar IDWT. Inverse of dwt2_haar."""
B, C, H2, W2 = ll.shape
# Vertical inverse: interleave rows
x_l = torch.zeros(B, C, H2 * 2, W2, device=ll.device, dtype=ll.dtype)
x_h = torch.zeros(B, C, H2 * 2, W2, device=hl.device, dtype=hl.dtype)
x_l[:, :, 0::2, :] = (ll + lh) / math.sqrt(2.0)
x_l[:, :, 1::2, :] = (ll - lh) / math.sqrt(2.0)
x_h[:, :, 0::2, :] = (hl + hh) / math.sqrt(2.0)
x_h[:, :, 1::2, :] = (hl - hh) / math.sqrt(2.0)
# Horizontal inverse: interleave cols
x = torch.zeros(B, C, H2 * 2, W2 * 2, device=x_l.device, dtype=x_l.dtype)
x[:, :, :, 0::2] = (x_l + x_h) / math.sqrt(2.0)
x[:, :, :, 1::2] = (x_l - x_h) / math.sqrt(2.0)
return x
# ---------------------------------------------------------------------------
# Gaussian blur (separable, no Gibbs ringing)
# ---------------------------------------------------------------------------
def gaussian_blur_2d(x, sigma):
"""Separable Gaussian blur over last two spatial dims of [B, C, H, W]."""
if sigma <= 0.0:
return x
radius = max(1, int(math.ceil(sigma * 3)))
coords = torch.arange(2 * radius + 1, dtype=torch.float32) - radius
g = torch.exp(-coords.pow(2) / (2.0 * sigma * sigma))
g = g / g.sum()
g = g.to(device=x.device, dtype=x.dtype)
C = x.shape[1]
kx = g.view(1, 1, 1, -1).expand(C, 1, 1, -1)
ky = g.view(1, 1, -1, 1).expand(C, 1, -1, 1)
x = torch.nn.functional.conv2d(x, kx, padding=(0, radius), groups=C)
x = torch.nn.functional.conv2d(x, ky, padding=(radius, 0), groups=C)
return x