import math import comfy.model_management import comfy.utils import torch import torch.nn.functional as F try: import torchvision.transforms.functional as TF except ModuleNotFoundError: # pragma: no cover - fallback for environments without torchvision TF = None def _gaussian_kernel1d(kernel_size, sigma, device, dtype): radius = kernel_size // 2 positions = torch.arange(-radius, radius + 1, device=device, dtype=dtype) sigma = max(float(sigma), 1e-6) kernel = torch.exp(-(positions ** 2) / (2.0 * sigma * sigma)) kernel = kernel / kernel.sum() return kernel def _gaussian_blur_fallback(tensor, kernel_size, sigma): if isinstance(kernel_size, int): kernel_size = (kernel_size, kernel_size) if isinstance(sigma, (int, float)): sigma = (sigma, sigma) ky, kx = kernel_size sy, sx = sigma if ky <= 1 and kx <= 1: return tensor device = tensor.device dtype = tensor.dtype kernel_y = _gaussian_kernel1d(ky, sy, device, dtype) kernel_x = _gaussian_kernel1d(kx, sx, device, dtype) kernel_2d = torch.outer(kernel_y, kernel_x) kernel_2d = kernel_2d / kernel_2d.sum() kernel = kernel_2d.view(1, 1, ky, kx) channels = tensor.shape[1] kernel = kernel.expand(channels, 1, ky, kx) padding = (ky // 2, kx // 2) return F.conv2d(tensor, kernel, padding=padding, groups=channels) if TF is None: class _FunctionalFallback: @staticmethod def gaussian_blur(tensor, kernel_size, sigma): return _gaussian_blur_fallback(tensor, kernel_size, sigma) TF = _FunctionalFallback() def _fade(t): return t * t * t * (t * (t * 6 - 15) + 10) def _lerp(a, b, t): return a + t * (b - a) def _generate_permutation(seed, device): generator = torch.Generator(device="cpu").manual_seed(seed) perm = torch.randperm(256, generator=generator) return torch.cat([perm, perm]).to(device) def _coordinate_grid(size, device, dtype): axes = [torch.linspace(0.0, 1.0, steps=s, device=device, dtype=dtype) for s in size] return torch.meshgrid(*axes, indexing="ij") def _perlin_2d(x, y, perm, dtype): gradients = torch.tensor( [[1, 1], [-1, 1], [1, -1], [-1, -1], [1, 0], [-1, 0], [0, 1], [0, -1]], dtype=dtype, device=x.device, ) x0 = torch.floor(x) y0 = torch.floor(y) xi = torch.remainder(x0.to(torch.int64), 256) yi = torch.remainder(y0.to(torch.int64), 256) xf = x - x0 yf = y - y0 xi1 = torch.remainder(xi + 1, 256) yi1 = torch.remainder(yi + 1, 256) perm_xi = perm[xi] perm_xi1 = perm[xi1] aa = perm[perm_xi + yi] ab = perm[perm_xi + yi1] ba = perm[perm_xi1 + yi] bb = perm[perm_xi1 + yi1] grad_count = gradients.shape[0] g_aa = gradients[torch.remainder(aa, grad_count)] g_ab = gradients[torch.remainder(ab, grad_count)] g_ba = gradients[torch.remainder(ba, grad_count)] g_bb = gradients[torch.remainder(bb, grad_count)] xf_1 = xf - 1.0 yf_1 = yf - 1.0 dot_aa = g_aa[..., 0] * xf + g_aa[..., 1] * yf dot_ba = g_ba[..., 0] * xf_1 + g_ba[..., 1] * yf dot_ab = g_ab[..., 0] * xf + g_ab[..., 1] * yf_1 dot_bb = g_bb[..., 0] * xf_1 + g_bb[..., 1] * yf_1 u = _fade(xf) v = _fade(yf) x1 = _lerp(dot_aa, dot_ba, u) x2 = _lerp(dot_ab, dot_bb, u) return _lerp(x1, x2, v) def _perlin_3d(x, y, z, perm, dtype): gradients = torch.tensor( [ [1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1], [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1], ], dtype=dtype, device=x.device, ) x0 = torch.floor(x) y0 = torch.floor(y) z0 = torch.floor(z) xi = torch.remainder(x0.to(torch.int64), 256) yi = torch.remainder(y0.to(torch.int64), 256) zi = torch.remainder(z0.to(torch.int64), 256) xf = x - x0 yf = y - y0 zf = z - z0 xi1 = torch.remainder(xi + 1, 256) yi1 = torch.remainder(yi + 1, 256) zi1 = torch.remainder(zi + 1, 256) perm_xi = perm[xi] perm_xi1 = perm[xi1] perm_xy = perm[perm_xi + yi] perm_xy1 = perm[perm_xi + yi1] perm_x1y = perm[perm_xi1 + yi] perm_x1y1 = perm[perm_xi1 + yi1] aaa = perm[perm_xy + zi] aab = perm[perm_xy + zi1] aba = perm[perm_xy1 + zi] abb = perm[perm_xy1 + zi1] baa = perm[perm_x1y + zi] bab = perm[perm_x1y + zi1] bba = perm[perm_x1y1 + zi] bbb = perm[perm_x1y1 + zi1] grad_count = gradients.shape[0] g_aaa = gradients[torch.remainder(aaa, grad_count)] g_aab = gradients[torch.remainder(aab, grad_count)] g_aba = gradients[torch.remainder(aba, grad_count)] g_abb = gradients[torch.remainder(abb, grad_count)] g_baa = gradients[torch.remainder(baa, grad_count)] g_bab = gradients[torch.remainder(bab, grad_count)] g_bba = gradients[torch.remainder(bba, grad_count)] g_bbb = gradients[torch.remainder(bbb, grad_count)] xf_1 = xf - 1.0 yf_1 = yf - 1.0 zf_1 = zf - 1.0 dot_aaa = g_aaa[..., 0] * xf + g_aaa[..., 1] * yf + g_aaa[..., 2] * zf dot_baa = g_baa[..., 0] * xf_1 + g_baa[..., 1] * yf + g_baa[..., 2] * zf dot_aba = g_aba[..., 0] * xf + g_aba[..., 1] * yf_1 + g_aba[..., 2] * zf dot_bba = g_bba[..., 0] * xf_1 + g_bba[..., 1] * yf_1 + g_bba[..., 2] * zf dot_aab = g_aab[..., 0] * xf + g_aab[..., 1] * yf + g_aab[..., 2] * zf_1 dot_bab = g_bab[..., 0] * xf_1 + g_bab[..., 1] * yf + g_bab[..., 2] * zf_1 dot_abb = g_abb[..., 0] * xf + g_abb[..., 1] * yf_1 + g_abb[..., 2] * zf_1 dot_bbb = g_bbb[..., 0] * xf_1 + g_bbb[..., 1] * yf_1 + g_bbb[..., 2] * zf_1 u = _fade(xf) v = _fade(yf) w = _fade(zf) x1 = _lerp(dot_aaa, dot_baa, u) x2 = _lerp(dot_aba, dot_bba, u) y1 = _lerp(x1, x2, v) x3 = _lerp(dot_aab, dot_bab, u) x4 = _lerp(dot_abb, dot_bbb, u) y2 = _lerp(x3, x4, v) return _lerp(y1, y2, w) def _fractal_perlin(size, seed, frequency, octaves, persistence, lacunarity, device): if len(size) not in (2, 3): raise ValueError("Perlin noise supports 2D or 3D shapes") noise_dtype = torch.float32 coords = _coordinate_grid(size, device, noise_dtype) total = torch.zeros(size, dtype=noise_dtype, device=device) amplitude = 1.0 max_amplitude = 0.0 freq = frequency for octave in range(octaves): perm = _generate_permutation(seed + octave, device) scaled_coords = [coord * freq for coord in coords] if len(size) == 2: octave_noise = _perlin_2d(scaled_coords[1], scaled_coords[0], perm, noise_dtype) else: octave_noise = _perlin_3d(scaled_coords[2], scaled_coords[1], scaled_coords[0], perm, noise_dtype) total = total + octave_noise * amplitude max_amplitude += amplitude amplitude *= persistence freq *= lacunarity if max_amplitude > 0: total = total / max_amplitude return total def _simplex_noise_2d(x, y, perm, dtype): F2 = 0.5 * (math.sqrt(3.0) - 1.0) G2 = (3.0 - math.sqrt(3.0)) / 6.0 s = (x + y) * F2 i = torch.floor(x + s) j = torch.floor(y + s) t = (i + j) * G2 X0 = i - t Y0 = j - t x0 = x - X0 y0 = y - Y0 cond = x0 > y0 i1 = torch.where(cond, torch.ones_like(x0), torch.zeros_like(x0)) j1 = torch.where(cond, torch.zeros_like(y0), torch.ones_like(y0)) x1 = x0 - i1 + G2 y1 = y0 - j1 + G2 x2 = x0 - 1.0 + 2.0 * G2 y2 = y0 - 1.0 + 2.0 * G2 ii = torch.remainder(i.to(torch.int64), 256) jj = torch.remainder(j.to(torch.int64), 256) i1_int = i1.to(torch.int64) j1_int = j1.to(torch.int64) gi0 = perm[ii + perm[jj]] % 12 gi1 = perm[ii + i1_int + perm[jj + j1_int]] % 12 gi2 = perm[ii + 1 + perm[jj + 1]] % 12 inv_sqrt2 = 1.0 / math.sqrt(2.0) gradients = torch.tensor( [ [1.0, 1.0], [-1.0, 1.0], [1.0, -1.0], [-1.0, -1.0], [1.0, 0.0], [-1.0, 0.0], [0.0, 1.0], [0.0, -1.0], [inv_sqrt2, inv_sqrt2], [-inv_sqrt2, inv_sqrt2], [inv_sqrt2, -inv_sqrt2], [-inv_sqrt2, -inv_sqrt2], ], dtype=dtype, device=x.device, ) def contribution(t_comp, gx, gy, x_comp, y_comp): mask = t_comp > 0 t_term = torch.zeros_like(t_comp) if mask.any(): t_square = t_comp[mask] * t_comp[mask] t_four = t_square * t_square dot = gx[mask] * x_comp[mask] + gy[mask] * y_comp[mask] t_term[mask] = t_four * dot return t_term g0 = gradients[gi0] g1 = gradients[gi1] g2 = gradients[gi2] t0 = 0.5 - x0 * x0 - y0 * y0 t1 = 0.5 - x1 * x1 - y1 * y1 t2 = 0.5 - x2 * x2 - y2 * y2 n0 = contribution(t0, g0[..., 0], g0[..., 1], x0, y0) n1 = contribution(t1, g1[..., 0], g1[..., 1], x1, y1) n2 = contribution(t2, g2[..., 0], g2[..., 1], x2, y2) return 70.0 * (n0 + n1 + n2) def _fractal_simplex(size, seed, frequency, octaves, persistence, lacunarity, device): if len(size) != 2: raise ValueError("Simplex noise is implemented for 2D shapes") dtype = torch.float32 coords = _coordinate_grid(size, device, dtype) total = torch.zeros(size, dtype=dtype, device=device) amplitude = 1.0 max_amplitude = 0.0 freq = frequency for octave in range(octaves): perm = _generate_permutation(seed + octave, device) x = coords[1] * freq y = coords[0] * freq octave_noise = _simplex_noise_2d(x, y, perm, dtype) total = total + octave_noise * amplitude max_amplitude += amplitude amplitude *= persistence freq *= lacunarity if max_amplitude > 0: total = total / max_amplitude return total def _worley_noise(size, seed, feature_points, metric, jitter, device): if len(size) != 2: raise ValueError("Worley noise expects a 2D shape") height, width = size dtype = torch.float32 cpu_gen = torch.Generator(device="cpu").manual_seed(seed) points = torch.rand((feature_points, 2), generator=cpu_gen, dtype=dtype) if jitter > 0.0: jitter_gen = torch.rand((feature_points, 2), generator=cpu_gen, dtype=dtype) - 0.5 points = torch.clamp(points + jitter_gen * jitter, 0.0, 1.0) points = points.to(device=device) ys = torch.linspace(0.0, 1.0, steps=height, device=device, dtype=dtype) xs = torch.linspace(0.0, 1.0, steps=width, device=device, dtype=dtype) grid_y = ys.view(height, 1).expand(height, width) grid_x = xs.view(1, width).expand(height, width) grid = torch.stack((grid_x, grid_y), dim=-1) point_offsets = points.view(1, 1, feature_points, 2) diff = grid.unsqueeze(2) - point_offsets if metric == "manhattan": distances = diff.abs().sum(dim=-1) elif metric == "chebyshev": distances = diff.abs().max(dim=-1).values else: distances = torch.sqrt((diff * diff).sum(dim=-1)) min_dist = distances.min(dim=-1).values max_dist = torch.max(min_dist) if max_dist > 0: min_dist = min_dist / max_dist return 1.0 - min_dist def _fractal_worley(size, seed, base_points, octaves, persistence, lacunarity, metric, jitter, device): dtype = torch.float32 total = torch.zeros(size, dtype=dtype, device=device) amplitude = 1.0 max_amplitude = 0.0 points = base_points for octave in range(octaves): octave_points = max(1, int(round(points))) noise = _worley_noise(size, seed + octave, octave_points, metric, jitter, device) total = total + noise * amplitude max_amplitude += amplitude amplitude *= persistence points *= lacunarity if max_amplitude > 0: total = total / max_amplitude return total * 2.0 - 1.0 def _gray_scott_pattern(size, seed, steps, feed, kill, diff_u, diff_v, dt, device): if len(size) != 2: raise ValueError("Gray-Scott pattern expects a 2D shape") height, width = size dtype = torch.float32 U = torch.ones((1, 1, height, width), device=device, dtype=dtype) V = torch.zeros((1, 1, height, width), device=device, dtype=dtype) cpu_gen = torch.Generator(device="cpu").manual_seed(seed) noise = torch.rand((height, width), generator=cpu_gen, dtype=dtype) noise = noise.to(device=device) U = U - 0.5 * noise.unsqueeze(0).unsqueeze(0) V = V + 0.5 * noise.unsqueeze(0).unsqueeze(0) square = max(2, min(height, width) // 10) start_y = height // 2 - square // 2 start_x = width // 2 - square // 2 V[:, :, start_y:start_y + square, start_x:start_x + square] = 1.0 U[:, :, start_y:start_y + square, start_x:start_x + square] = 0.0 lap_kernel = torch.tensor( [[0.05, 0.2, 0.05], [0.2, -1.0, 0.2], [0.05, 0.2, 0.05]], dtype=dtype, device=device, ).view(1, 1, 3, 3) for _ in range(steps): U_pad = torch.nn.functional.pad(U, (1, 1, 1, 1), mode="reflect") V_pad = torch.nn.functional.pad(V, (1, 1, 1, 1), mode="reflect") lap_u = torch.nn.functional.conv2d(U_pad, lap_kernel) lap_v = torch.nn.functional.conv2d(V_pad, lap_kernel) reaction = U * V * V U = U + (diff_u * lap_u - reaction + feed * (1.0 - U)) * dt V = V + (diff_v * lap_v + reaction - (feed + kill) * V) * dt U = torch.clamp(U, 0.0, 1.0) V = torch.clamp(V, 0.0, 1.0) pattern = V.squeeze(0).squeeze(0) return _normalize_noise_tensor(pattern) def _apply_2d_noise(latent_tensor, seed, strength, channel_mode, temporal_mode, generator): if strength == 0.0: return latent_tensor with torch.no_grad(): device = latent_tensor.device is_video = latent_tensor.dim() == 5 if is_video: batch, channels, frames, height, width = latent_tensor.shape else: batch, channels, height, width = latent_tensor.shape frames = 1 output = latent_tensor.clone() for batch_index in range(batch): sample = latent_tensor[batch_index] sample_std = torch.std(sample.float()) scale = sample_std.item() * strength if sample_std > 1e-6 else strength if channel_mode == "shared": if is_video: if temporal_mode == "locked": base_noise = generator((height, width), seed + batch_index) base_noise = _normalize_noise_tensor(base_noise) noise = base_noise.unsqueeze(0).unsqueeze(0).expand(channels, frames, height, width).clone() else: noise = torch.zeros((channels, frames, height, width), dtype=torch.float32, device=device) for frame_index in range(frames): frame_seed = seed + batch_index + frame_index * 131 frame_noise = generator((height, width), frame_seed) frame_plane = _normalize_noise_tensor(frame_noise).unsqueeze(0).expand(channels, height, width) noise[:, frame_index] = frame_plane.clone() else: base_noise = generator((height, width), seed + batch_index) base_noise = _normalize_noise_tensor(base_noise) noise = base_noise.unsqueeze(0).expand(channels, height, width).clone() else: if is_video: noise = torch.zeros((channels, frames, height, width), dtype=torch.float32, device=device) for channel_index in range(channels): channel_seed = seed + batch_index * 997 + channel_index * 1013 if temporal_mode == "locked": channel_noise = generator((height, width), channel_seed) normalized = _normalize_noise_tensor(channel_noise) noise[channel_index] = normalized.unsqueeze(0).expand(frames, height, width).clone() else: for frame_index in range(frames): frame_seed = channel_seed + frame_index * 131 frame_noise = generator((height, width), frame_seed) noise[channel_index, frame_index] = _normalize_noise_tensor(frame_noise) else: noise = torch.zeros((channels, height, width), dtype=torch.float32, device=device) for channel_index in range(channels): channel_seed = seed + batch_index * 997 + channel_index * 1013 channel_noise = generator((height, width), channel_seed) noise[channel_index] = _normalize_noise_tensor(channel_noise) scaled_noise = (noise * scale).to(sample.dtype) output[batch_index] = sample + scaled_noise return output def _apply_image_2d_noise(image_tensor, seed, strength, channel_mode, temporal_mode, generator): if strength == 0.0: return image_tensor with torch.no_grad(): device = image_tensor.device is_video = image_tensor.dim() == 5 if is_video: batch, frames, height, width, channels = image_tensor.shape else: batch, height, width, channels = image_tensor.shape frames = 1 output = image_tensor.clone() for batch_index in range(batch): sample = image_tensor[batch_index] sample_std = torch.std(sample.float()) scale = sample_std.item() * strength if sample_std > 1e-6 else strength if channel_mode == "shared": if is_video: if temporal_mode == "locked": base_noise = generator((height, width), seed + batch_index) base_noise = _normalize_noise_tensor(base_noise) noise = base_noise.unsqueeze(0).unsqueeze(-1).expand(frames, height, width, channels).clone() else: noise = torch.zeros((frames, height, width, channels), dtype=torch.float32, device=device) for frame_index in range(frames): frame_seed = seed + batch_index + frame_index * 131 frame_noise = generator((height, width), frame_seed) frame_plane = _normalize_noise_tensor(frame_noise).unsqueeze(-1).expand(height, width, channels) noise[frame_index] = frame_plane.clone() else: base_noise = generator((height, width), seed + batch_index) base_noise = _normalize_noise_tensor(base_noise) noise = base_noise.unsqueeze(-1).expand(height, width, channels).clone() else: if is_video: noise = torch.zeros((frames, height, width, channels), dtype=torch.float32, device=device) for channel_index in range(channels): channel_seed = seed + batch_index * 997 + channel_index * 1013 if temporal_mode == "locked": channel_noise = generator((height, width), channel_seed) normalized = _normalize_noise_tensor(channel_noise) noise[:, :, :, channel_index] = normalized.unsqueeze(0).unsqueeze(-1).expand(frames, height, width) else: for frame_index in range(frames): frame_seed = channel_seed + frame_index * 131 frame_noise = generator((height, width), frame_seed) noise[frame_index, :, :, channel_index] = _normalize_noise_tensor(frame_noise) else: noise = torch.zeros((height, width, channels), dtype=torch.float32, device=device) for channel_index in range(channels): channel_seed = seed + batch_index * 997 + channel_index * 1013 channel_noise = generator((height, width), channel_seed) noise[:, :, channel_index] = _normalize_noise_tensor(channel_noise) scaled_noise = (noise * scale).to(sample.dtype) output[batch_index] = sample + scaled_noise return output def _normalize_noise_tensor(noise): mean = torch.mean(noise) std = torch.std(noise) if std > 1e-6: return (noise - mean) / std return noise - mean def _normalized_meshgrid(height, width, device, dtype): ys = torch.linspace(-1.0, 1.0, steps=height, device=device, dtype=dtype) xs = torch.linspace(-1.0, 1.0, steps=width, device=device, dtype=dtype) return torch.meshgrid(ys, xs, indexing="ij") def _swirl_grid(base_y, base_x, vortices): total_dx = torch.zeros_like(base_x) total_dy = torch.zeros_like(base_y) for center_x, center_y, strength, radius, direction in vortices: radius = max(radius, 1e-3) dx = base_x - center_x dy = base_y - center_y r = torch.sqrt(dx * dx + dy * dy) falloff = torch.exp(-torch.square(r / radius)) angle = direction * strength * falloff sin_a = torch.sin(angle) cos_a = torch.cos(angle) rotated_dx = dx * cos_a - dy * sin_a rotated_dy = dx * sin_a + dy * cos_a total_dx = total_dx + (rotated_dx - dx) total_dy = total_dy + (rotated_dy - dy) x_new = base_x + total_dx y_new = base_y + total_dy grid = torch.stack((x_new, y_new), dim=-1) return torch.clamp(grid, -1.0, 1.0) def slerp(val, low, high): """ Robust, batch-aware Spherical Linear Interpolation for PyTorch tensors. """ dims = low.shape # Flatten per batch so we can SLERP across arbitrary shapes low = low.reshape(dims[0], -1) high = high.reshape(dims[0], -1) low_norm = F.normalize(low, p=2, dim=1) high_norm = F.normalize(high, p=2, dim=1) # Guard against zero-length vectors low_norm[torch.isnan(low_norm)] = 0.0 high_norm[torch.isnan(high_norm)] = 0.0 omega = torch.acos((low_norm * high_norm).sum(1)) so = torch.sin(omega) # Fallback to lerp when the angle is tiny to avoid numerical issues cond = so > 1e-3 res = torch.zeros_like(low) if cond.any(): omega_cond = omega[cond].unsqueeze(1) so_cond = so[cond].unsqueeze(1) term1 = (torch.sin((1.0 - val) * omega_cond) / so_cond) * low[cond] term2 = (torch.sin(val * omega_cond) / so_cond) * high[cond] res[cond] = term1 + term2 if not cond.all(): res[~cond] = (1.0 - val) * low[~cond] + val * high[~cond] return res.reshape(dims) class LatentHybridInverter: @classmethod def INPUT_TYPES(s): return { "required": { "model": ("MODEL", {"tooltip": "UNet model used for both inversion and diffusion passes."}), "latent_image": ("LATENT", {"tooltip": "Latent tensor to invert and re-noise."}), "positive": ("CONDITIONING", {"tooltip": "Positive prompt conditioning that guides both passes."}), "steps": ("INT", {"default": 20, "min": 1, "max": 1000, "tooltip": "Solver steps executed for inversion and forward diffusion."}), "strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Strength for both inversion and forward diffusion. The target noise level."}), "blend_factor": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "0.0 = 100% Forward Diffusion (Stable), 1.0 = 100% Inverter (Creative)"}), "inverter_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for the creative, perturbed inversion process."}), "forward_diffusion_seed": ("INT", {"default": 1, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for the stable, 'perfect' noise anchor."}), "velocity_amplification": ("FLOAT", {"default": 1.0, "min": -0.9, "max": 2.0, "step": 0.05, "tooltip": "Deterministically scales the predicted velocity each step."}), "velocity_perturb_strength": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Random velocity noise level for the inverter stage."}), "internal_precision": (["bfloat16", "float16", "force_float32"], {"default": "bfloat16", "tooltip": "Precision used for inverter UNet autocast."}), "normalize_output": (["enable", "disable"], {"default": "enable", "tooltip": "Normalizing the inverter output is highly recommended."}), } } RETURN_TYPES = ("LATENT",) FUNCTION = "hybrid_invert" CATEGORY = "Qwen/Sampling" def hybrid_invert(self, model, latent_image, positive, steps, strength, blend_factor, inverter_seed, forward_diffusion_seed, velocity_amplification, velocity_perturb_strength, internal_precision, normalize_output): if strength == 0.0: return (latent_image,) device = comfy.model_management.get_torch_device() native_dtype = comfy.model_management.unet_dtype() inverter_node = QwenRectifiedFlowInverter() inverted_latent, = inverter_node.invert( model=model, latent_image=latent_image, positive=positive, seed=inverter_seed, steps=steps, inversion_strength=strength, velocity_amplification=velocity_amplification, velocity_perturb_strength=velocity_perturb_strength, internal_precision=internal_precision, normalize_output=normalize_output, ) diffusion_node = LatentForwardDiffusion() forward_latent, = diffusion_node.add_scheduled_noise( model=model, latent=latent_image, seed=forward_diffusion_seed, steps=steps, noise_strength=strength, ) inverted_samples = inverted_latent["samples"].to(device, native_dtype) forward_samples = forward_latent["samples"].to(device, native_dtype) if inverted_samples.dim() == 5 and forward_samples.dim() == 4: forward_samples = forward_samples.unsqueeze(2) elif inverted_samples.dim() == 4 and forward_samples.dim() == 5: inverted_samples = inverted_samples.unsqueeze(2) if blend_factor == 0.0: final_latent = forward_samples elif blend_factor == 1.0: final_latent = inverted_samples else: final_latent = slerp(blend_factor, forward_samples, inverted_samples) out = latent_image.copy() out["samples"] = final_latent.cpu() return (out, ) class QwenRectifiedFlowInverter: @classmethod def INPUT_TYPES(s): return { "required": { "model": ("MODEL", {"tooltip": "UNet to integrate during the rectified flow inversion."}), "latent_image": ("LATENT", {"tooltip": "Latent to be inverted toward the clean state."}), "positive": ("CONDITIONING", {"tooltip": "Positive prompt conditioning applied during inversion."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for the random velocity perturbation."}), "steps": ("INT", {"default": 20, "min": 1, "max": 1000, "tooltip": "Number of integration steps available for the inverter."}), "inversion_strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Fraction of the schedule to walk backward during inversion."}), "velocity_amplification": ("FLOAT", { "default": 0.0, "min": -0.9, "max": 2.0, "step": 0.05, "tooltip": "Deterministically amplifies (>0) or dampens (<0) the predicted velocity at each step." }), "velocity_perturb_strength": ("FLOAT", { "default": 0.0, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Adds seeded random noise to the velocity. >1.0 will heavily randomize the path." }), "internal_precision": (["bfloat16", "float16", "force_float32"], {"default": "bfloat16", "tooltip": "Autocast precision used while running the inverter UNet."}), "normalize_output": (["enable", "disable"], {"default": "enable", "tooltip": "Crucial for high inversion strengths. Rescales the output latent to a standard distribution (mean=0, std=1) to prevent the sampler from collapsing."}), } } RETURN_TYPES = ("LATENT",) FUNCTION = "invert" CATEGORY = "Qwen/Sampling" def invert(self, model, latent_image, positive, seed, steps, inversion_strength, velocity_amplification, velocity_perturb_strength, internal_precision, normalize_output): if inversion_strength == 0.0: return (latent_image,) device = comfy.model_management.get_torch_device() native_dtype = comfy.model_management.unet_dtype() autocast_dtype = torch.bfloat16 if internal_precision == "bfloat16" and torch.cuda.is_available() and torch.cuda.is_bf16_supported() else torch.float32 if internal_precision == "force_float32" else torch.float16 clean_latent = latent_image["samples"].clone().to(device, native_dtype) context = positive[0][0].to(device, native_dtype) qwen_transformer = model.model.diffusion_model comfy.model_management.load_models_gpu([model]) inversion_steps = max(1, int(steps * inversion_strength)) end_t = inversion_strength timesteps = torch.linspace(0, end_t, inversion_steps + 1, device=device, dtype=native_dtype) x_t = clean_latent.unsqueeze(2) if clean_latent.dim() == 4 else clean_latent pbar = comfy.utils.ProgressBar(inversion_steps) with torch.no_grad(): for i in range(inversion_steps): t_current = timesteps[i] t_next = timesteps[i+1] t_current_b = torch.full((x_t.shape[0],), t_current, device=device, dtype=native_dtype) with torch.autocast(device_type="cuda", dtype=autocast_dtype): predicted_velocity = qwen_transformer( x=x_t, timestep=t_current_b, context=context, attention_mask=None ) if predicted_velocity.dim() == 4: predicted_velocity = predicted_velocity.unsqueeze(2) if velocity_amplification != 0.0: predicted_velocity = predicted_velocity * (1.0 + velocity_amplification) if velocity_perturb_strength > 0: step_seed = seed + i generator = torch.Generator(device=device).manual_seed(step_seed) velocity_std = torch.std(predicted_velocity) noise = torch.randn( predicted_velocity.shape, generator=generator, device=device, dtype=native_dtype ) perturbation = noise * velocity_std * velocity_perturb_strength predicted_velocity = predicted_velocity + perturbation dt = t_next - t_current x_t = x_t + predicted_velocity * dt if torch.isnan(x_t).any(): raise RuntimeError("NaN detected at step %s. Increase the precision setting." % (i+1)) pbar.update(1) if normalize_output == "enable": for i in range(x_t.shape[0]): latent_item = x_t[i] mean = torch.mean(latent_item) std = torch.std(latent_item) if std > 1e-5: x_t[i] = (latent_item - mean) / std else: x_t[i] = latent_item - mean out = latent_image.copy() out["samples"] = x_t.cpu() return (out, ) class LatentGaussianBlur: @classmethod def INPUT_TYPES(cls): return { "required": { "latent": ("LATENT", {"tooltip": "Latent tensor to blur."}), "sigma": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1, "tooltip": "Standard deviation for the Gaussian kernel."}), "blur_mode": (["Spatial Only", "Spatial and Channel"], {"default": "Spatial Only", "tooltip": "Choose whether to blur across spatial dimensions only or include channels."}), } } RETURN_TYPES = ("LATENT",) FUNCTION = "blur_latent" CATEGORY = "Latent/Filter" def blur_latent(self, latent, sigma, blur_mode): if sigma == 0.0: return (latent,) device = comfy.model_management.get_torch_device() latent_tensor = latent["samples"].clone().to(device) is_5d = latent_tensor.dim() == 5 if is_5d: b, c, t, h, w = latent_tensor.shape latent_4d = latent_tensor.permute(0, 2, 1, 3, 4).reshape(b * t, c, h, w) else: latent_4d = latent_tensor if blur_mode == "Spatial Only": kernel_size = int(sigma * 6) + 1 if kernel_size % 2 == 0: kernel_size += 1 blurred_4d = TF.gaussian_blur(latent_4d, kernel_size=kernel_size, sigma=sigma) else: channels = latent_4d.shape[1] latent_5d_for_conv = latent_4d.unsqueeze(1) k_size_spatial = int(sigma * 6) + 1 if k_size_spatial % 2 == 0: k_size_spatial += 1 k_size_channel = min(channels, k_size_spatial) if k_size_channel % 2 == 0: k_size_channel -= 1 if k_size_channel < 1: k_size_channel = 1 ax_c = torch.linspace(-(k_size_channel - 1) / 2., (k_size_channel - 1) / 2., k_size_channel, device=device) ax_s = torch.linspace(-(k_size_spatial - 1) / 2., (k_size_spatial - 1) / 2., k_size_spatial, device=device) gauss_c = torch.exp(-0.5 * torch.square(ax_c / sigma)) gauss_s = torch.exp(-0.5 * torch.square(ax_s / sigma)) gauss_c = gauss_c.view(k_size_channel, 1, 1) gauss_h = gauss_s.view(1, k_size_spatial, 1) gauss_w = gauss_s.view(1, 1, k_size_spatial) kernel_3d = gauss_c * gauss_h * gauss_w kernel_3d /= torch.sum(kernel_3d) kernel_3d = kernel_3d.view(1, 1, k_size_channel, k_size_spatial, k_size_spatial) padding = (k_size_channel // 2, k_size_spatial // 2, k_size_spatial // 2) blurred_5d = F.conv3d(latent_5d_for_conv, kernel_3d, padding=padding) blurred_4d = blurred_5d.squeeze(1) if is_5d: blurred_tensor = blurred_4d.view(b, t, c, h, w).permute(0, 2, 1, 3, 4) else: blurred_tensor = blurred_4d out = latent.copy() out["samples"] = blurred_tensor.cpu() return (out,) class LatentFrequencySplit: """Splits a latent into low- and high-frequency bands via Gaussian smoothing.""" @classmethod def INPUT_TYPES(cls): return { "required": { "latent": ("LATENT", {"tooltip": "Latent to decompose into low/high frequency bands."}), "sigma": ("FLOAT", { "default": 1.5, "min": 0.0, "max": 20.0, "step": 0.1, "tooltip": "Radius of the Gaussian used for the low-pass. Higher values move detail into the high band." }), } } RETURN_TYPES = ("LATENT", "LATENT") RETURN_NAMES = ("low_pass", "high_pass") FUNCTION = "split" CATEGORY = "Latent/Filter" def split(self, latent, sigma): device = comfy.model_management.get_torch_device() latent_tensor = latent["samples"].clone().to(device) if sigma <= 0.0: low_samples = latent_tensor high_samples = torch.zeros_like(latent_tensor) else: low_samples = self._gaussian_blur(latent_tensor, sigma) high_samples = latent_tensor - low_samples low_latent = latent.copy() low_latent["samples"] = low_samples.cpu() high_latent = latent.copy() high_latent["samples"] = high_samples.cpu() return (low_latent, high_latent) @staticmethod def _gaussian_blur(latent_tensor, sigma): tensor = latent_tensor is_5d = tensor.dim() == 5 if is_5d: b, c, t, h, w = tensor.shape tensor_4d = tensor.permute(0, 2, 1, 3, 4).reshape(b * t, c, h, w) else: tensor_4d = tensor kernel_size = int(sigma * 6) + 1 if kernel_size % 2 == 0: kernel_size += 1 blurred_4d = TF.gaussian_blur(tensor_4d, kernel_size=kernel_size, sigma=sigma) if is_5d: blurred = blurred_4d.view(b, t, c, h, w).permute(0, 2, 1, 3, 4) else: blurred = blurred_4d return blurred class LatentAddNoise: """ Adds a configurable amount of seeded random noise to a latent tensor. The strength is relative to the standard deviation of the input latent. """ @classmethod def INPUT_TYPES(s): return { "required": { "latent": ("LATENT", {"tooltip": "Latent to receive additional Gaussian noise."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for generating repeatable noise."}), "strength": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "tooltip": "Strength of the noise. 1.0 adds noise with the same standard deviation as the latent." }), } } RETURN_TYPES = ("LATENT",) FUNCTION = "add_noise" CATEGORY = "Latent/Noise" def add_noise(self, latent, seed, strength): if strength == 0.0: return (latent,) device = comfy.model_management.get_torch_device() latent_tensor = latent["samples"].clone().to(device) generator = torch.Generator(device=device).manual_seed(seed) noise = torch.randn( latent_tensor.shape, generator=generator, device=device, dtype=latent_tensor.dtype ) latent_std = torch.std(latent_tensor) scaled_noise = noise * latent_std * strength noised_latent = latent_tensor + scaled_noise out = latent.copy() out["samples"] = noised_latent.cpu() return (out,) class ImageAddNoise: """Adds seeded Gaussian noise to image tensors.""" @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE", {"tooltip": "Image to receive additional Gaussian noise."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for generating repeatable noise."}), "strength": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "tooltip": "Strength of the noise. 1.0 adds noise with the same standard deviation as the image.", }), } } RETURN_TYPES = ("IMAGE",) FUNCTION = "add_noise" CATEGORY = "Image/Noise" def add_noise(self, image, seed, strength): if strength == 0.0: return (image,) device = comfy.model_management.get_torch_device() image_tensor = image.clone().to(device) generator = torch.Generator(device=device).manual_seed(seed) noise = torch.randn( image_tensor.shape, generator=generator, device=device, dtype=image_tensor.dtype, ) image_std = torch.std(image_tensor) scaled_noise = noise * image_std * strength noised_image = image_tensor + scaled_noise return (noised_image.cpu(),) class LatentPerlinFractalNoise: """Adds smooth fractal Perlin noise to latent tensors for structured variation.""" @classmethod def INPUT_TYPES(cls): return { "required": { "latent": ("LATENT", {"tooltip": "Latent that will be perturbed with fractal Perlin noise."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed controlling the procedural noise pattern."}), "frequency": ("FLOAT", { "default": 2.0, "min": 0.01, "max": 64.0, "step": 0.01, "tooltip": "Base lattice frequency. Higher values produce finer details." }), "octaves": ("INT", { "default": 4, "min": 1, "max": 12, "tooltip": "Number of noise layers to accumulate." }), "persistence": ("FLOAT", { "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Amplitude multiplier between octaves." }), "lacunarity": ("FLOAT", { "default": 2.0, "min": 1.0, "max": 6.0, "step": 0.1, "tooltip": "Frequency multiplier between octaves." }), "strength": ("FLOAT", { "default": 0.5, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Scales the normalized noise relative to the latent's standard deviation." }), "channel_mode": (["shared", "per_channel"], { "default": "shared", "tooltip": "Shared noise across channels or unique noise per channel." }), } } RETURN_TYPES = ("LATENT",) FUNCTION = "add_perlin_noise" CATEGORY = "Latent/Noise" def add_perlin_noise(self, latent, seed, frequency, octaves, persistence, lacunarity, strength, channel_mode): if strength == 0.0: return (latent,) device = comfy.model_management.get_torch_device() latent_tensor = latent["samples"].clone().to(device) is_video = latent_tensor.dim() == 5 if is_video: dims = (latent_tensor.shape[2], latent_tensor.shape[3], latent_tensor.shape[4]) else: dims = (latent_tensor.shape[-2], latent_tensor.shape[-1]) batch = latent_tensor.shape[0] channels = latent_tensor.shape[1] output = latent_tensor.clone() for batch_index in range(batch): sample = latent_tensor[batch_index] sample_std = torch.std(sample.float()) if channel_mode == "shared": noise = _fractal_perlin(dims, seed + batch_index, frequency, octaves, persistence, lacunarity, device) noise = _normalize_noise_tensor(noise) if is_video: noise = noise.unsqueeze(0).expand(channels, dims[0], dims[1], dims[2]) else: noise = noise.unsqueeze(0).expand(channels, dims[-2], dims[-1]) else: if is_video: noise = torch.zeros((channels, dims[0], dims[1], dims[2]), dtype=torch.float32, device=device) else: noise = torch.zeros((channels, dims[-2], dims[-1]), dtype=torch.float32, device=device) for channel_index in range(channels): channel_seed = seed + batch_index * 997 + channel_index * 1013 channel_noise = _fractal_perlin(dims, channel_seed, frequency, octaves, persistence, lacunarity, device) noise[channel_index] = _normalize_noise_tensor(channel_noise) scale = sample_std * strength scaled_noise = (noise * scale).to(sample.dtype) output[batch_index] = sample + scaled_noise out = latent.copy() out["samples"] = output.cpu() return (out,) class ImagePerlinFractalNoise: """Adds smooth fractal Perlin noise to images for structured variation.""" @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE", {"tooltip": "Image that will be perturbed with fractal Perlin noise."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed controlling the procedural noise pattern."}), "frequency": ("FLOAT", { "default": 2.0, "min": 0.01, "max": 64.0, "step": 0.01, "tooltip": "Base lattice frequency. Higher values produce finer details.", }), "octaves": ("INT", { "default": 4, "min": 1, "max": 12, "tooltip": "Number of noise layers to accumulate.", }), "persistence": ("FLOAT", { "default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Amplitude multiplier between octaves.", }), "lacunarity": ("FLOAT", { "default": 2.0, "min": 1.0, "max": 6.0, "step": 0.1, "tooltip": "Frequency multiplier between octaves.", }), "strength": ("FLOAT", { "default": 0.5, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Scale of the normalized noise relative to the image's standard deviation.", }), "channel_mode": (["shared", "per_channel"], {"default": "shared", "tooltip": "Reuse a single noise field for all channels or reseed per channel."}), "temporal_mode": (["locked", "animated"], {"default": "locked", "tooltip": "locked reruns the same pattern per frame; animated reseeds each frame."}), } } RETURN_TYPES = ("IMAGE",) FUNCTION = "add_perlin_noise" CATEGORY = "Image/Noise" def add_perlin_noise(self, image, seed, frequency, octaves, persistence, lacunarity, strength, channel_mode, temporal_mode): device = comfy.model_management.get_torch_device() image_tensor = image.clone().to(device) generator = lambda size, noise_seed: _fractal_perlin(size, noise_seed, frequency, octaves, persistence, lacunarity, device) output = _apply_image_2d_noise(image_tensor, seed, strength, channel_mode, temporal_mode, generator) return (output.cpu(),) class LatentSimplexNoise: """Applies layered simplex noise for organic latent perturbations.""" @classmethod def INPUT_TYPES(cls): return { "required": { "latent": ("LATENT", {"tooltip": "Latent to perturb with simplex noise."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed controlling the simplex lattice offsets."}), "frequency": ("FLOAT", {"default": 2.0, "min": 0.01, "max": 64.0, "step": 0.01, "tooltip": "Base lattice frequency for the simplex grid."}), "octaves": ("INT", {"default": 4, "min": 1, "max": 12, "tooltip": "Number of simplex layers to accumulate."}), "persistence": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Amplitude multiplier applied between octaves."}), "lacunarity": ("FLOAT", {"default": 2.0, "min": 1.0, "max": 6.0, "step": 0.1, "tooltip": "Frequency multiplier applied between octaves."}), "strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Scale of the normalized simplex noise relative to the latent's standard deviation."}), "channel_mode": (["shared", "per_channel"], {"default": "shared", "tooltip": "Reuse a single noise field for all channels or reseed per channel."}), "temporal_mode": (["locked", "animated"], {"default": "locked", "tooltip": "locked reruns the same pattern per frame; animated reseeds each frame."}), } } RETURN_TYPES = ("LATENT",) FUNCTION = "add_simplex_noise" CATEGORY = "Latent/Noise" def add_simplex_noise(self, latent, seed, frequency, octaves, persistence, lacunarity, strength, channel_mode, temporal_mode): device = comfy.model_management.get_torch_device() latent_tensor = latent["samples"].clone().to(device) generator = lambda size, noise_seed: _fractal_simplex(size, noise_seed, frequency, octaves, persistence, lacunarity, device) output = _apply_2d_noise(latent_tensor, seed, strength, channel_mode, temporal_mode, generator) out = latent.copy() out["samples"] = output.cpu() return (out,) class ImageSimplexNoise: """Applies layered simplex noise for organic image perturbations.""" @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE", {"tooltip": "Image to perturb with simplex noise."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed controlling the simplex lattice offsets."}), "frequency": ("FLOAT", {"default": 2.0, "min": 0.01, "max": 64.0, "step": 0.01, "tooltip": "Base lattice frequency for the simplex grid."}), "octaves": ("INT", {"default": 4, "min": 1, "max": 12, "tooltip": "Number of simplex layers to accumulate."}), "persistence": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Amplitude multiplier applied between octaves."}), "lacunarity": ("FLOAT", {"default": 2.0, "min": 1.0, "max": 6.0, "step": 0.1, "tooltip": "Frequency multiplier applied between octaves."}), "strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Scale of the normalized simplex noise relative to the image's standard deviation."}), "channel_mode": (["shared", "per_channel"], {"default": "shared", "tooltip": "Reuse a single noise field for all channels or reseed per channel."}), "temporal_mode": (["locked", "animated"], {"default": "locked", "tooltip": "locked reruns the same pattern per frame; animated reseeds each frame."}), } } RETURN_TYPES = ("IMAGE",) FUNCTION = "add_simplex_noise" CATEGORY = "Image/Noise" def add_simplex_noise(self, image, seed, frequency, octaves, persistence, lacunarity, strength, channel_mode, temporal_mode): device = comfy.model_management.get_torch_device() image_tensor = image.clone().to(device) generator = lambda size, noise_seed: _fractal_simplex(size, noise_seed, frequency, octaves, persistence, lacunarity, device) output = _apply_image_2d_noise(image_tensor, seed, strength, channel_mode, temporal_mode, generator) return (output.cpu(),) class LatentWorleyNoise: """Generates cellular Worley noise for cracked or biological textures.""" @classmethod def INPUT_TYPES(cls): return { "required": { "latent": ("LATENT", {"tooltip": "Latent to perturb with Worley (cellular) noise."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for the feature point distribution."}), "feature_points": ("INT", {"default": 16, "min": 1, "max": 4096, "tooltip": "Base number of feature points scattered across the plane."}), "octaves": ("INT", {"default": 3, "min": 1, "max": 8, "tooltip": "Number of cellular layers to accumulate."}), "persistence": ("FLOAT", {"default": 0.65, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Amplitude multiplier between Worley octaves."}), "lacunarity": ("FLOAT", {"default": 2.0, "min": 1.0, "max": 6.0, "step": 0.1, "tooltip": "Multiplier for the feature point count between octaves."}), "distance_metric": (["euclidean", "manhattan", "chebyshev"], {"default": "euclidean", "tooltip": "Distance metric used when measuring feature proximity."}), "jitter": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "How far feature points can drift inside each cell."}), "strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Scale of the normalized Worley noise relative to the latent's standard deviation."}), "channel_mode": (["shared", "per_channel"], {"default": "shared", "tooltip": "Shared noise for all channels or reseeded per channel."}), "temporal_mode": (["locked", "animated"], {"default": "locked", "tooltip": "locked reuses a single noise pattern per frame; animated reseeds each frame."}), } } RETURN_TYPES = ("LATENT",) FUNCTION = "add_worley_noise" CATEGORY = "Latent/Noise" def add_worley_noise(self, latent, seed, feature_points, octaves, persistence, lacunarity, distance_metric, jitter, strength, channel_mode, temporal_mode): device = comfy.model_management.get_torch_device() latent_tensor = latent["samples"].clone().to(device) base_points = float(max(1, feature_points)) generator = lambda size, noise_seed: _fractal_worley(size, noise_seed, base_points, octaves, persistence, lacunarity, distance_metric, jitter, device) output = _apply_2d_noise(latent_tensor, seed, strength, channel_mode, temporal_mode, generator) out = latent.copy() out["samples"] = output.cpu() return (out,) class ImageWorleyNoise: """Generates cellular Worley noise for cracked or biological image textures.""" @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE", {"tooltip": "Image to perturb with Worley (cellular) noise."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for the feature point distribution."}), "feature_points": ("INT", {"default": 16, "min": 1, "max": 4096, "tooltip": "Base number of feature points scattered across the plane."}), "octaves": ("INT", {"default": 3, "min": 1, "max": 8, "tooltip": "Number of cellular layers to accumulate."}), "persistence": ("FLOAT", {"default": 0.65, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Amplitude multiplier between Worley octaves."}), "lacunarity": ("FLOAT", {"default": 2.0, "min": 1.0, "max": 6.0, "step": 0.1, "tooltip": "Multiplier for the feature point count between octaves."}), "distance_metric": (["euclidean", "manhattan", "chebyshev"], {"default": "euclidean", "tooltip": "Distance metric used when measuring feature proximity."}), "jitter": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "How far feature points can drift inside each cell."}), "strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Scale of the normalized Worley noise relative to the image's standard deviation."}), "channel_mode": (["shared", "per_channel"], {"default": "shared", "tooltip": "Shared noise for all channels or reseeded per channel."}), "temporal_mode": (["locked", "animated"], {"default": "locked", "tooltip": "locked reuses a single noise pattern per frame; animated reseeds each frame."}), } } RETURN_TYPES = ("IMAGE",) FUNCTION = "add_worley_noise" CATEGORY = "Image/Noise" def add_worley_noise(self, image, seed, feature_points, octaves, persistence, lacunarity, distance_metric, jitter, strength, channel_mode, temporal_mode): device = comfy.model_management.get_torch_device() image_tensor = image.clone().to(device) base_points = float(max(1, feature_points)) generator = lambda size, noise_seed: _fractal_worley(size, noise_seed, base_points, octaves, persistence, lacunarity, distance_metric, jitter, device) output = _apply_image_2d_noise(image_tensor, seed, strength, channel_mode, temporal_mode, generator) return (output.cpu(),) class LatentReactionDiffusion: """Runs a Gray-Scott reaction-diffusion simulation and injects the resulting pattern.""" @classmethod def INPUT_TYPES(cls): return { "required": { "latent": ("LATENT", {"tooltip": "Latent that will receive reaction-diffusion patterns."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for the initial chemical concentrations."}), "iterations": ("INT", {"default": 200, "min": 1, "max": 2000, "tooltip": "Number of Gray-Scott simulation steps."}), "feed_rate": ("FLOAT", {"default": 0.036, "min": 0.0, "max": 0.1, "step": 0.001, "tooltip": "Feed rate (F) controlling how quickly chemical U is replenished."}), "kill_rate": ("FLOAT", {"default": 0.065, "min": 0.0, "max": 0.1, "step": 0.001, "tooltip": "Kill rate (K) regulating removal of chemical V."}), "diffusion_u": ("FLOAT", {"default": 0.16, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Diffusion rate for chemical U."}), "diffusion_v": ("FLOAT", {"default": 0.08, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Diffusion rate for chemical V."}), "time_step": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 5.0, "step": 0.01, "tooltip": "Simulation time step used during integration."}), "strength": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Scale of the normalized pattern relative to the latent's standard deviation."}), "channel_mode": (["shared", "per_channel"], {"default": "shared", "tooltip": "Reuse one simulation for all channels or rerun per channel."}), "temporal_mode": (["locked", "animated"], {"default": "locked", "tooltip": "locked reuses the same pattern for every frame; animated reruns the simulation per frame."}), } } RETURN_TYPES = ("LATENT",) FUNCTION = "add_reaction_diffusion" CATEGORY = "Latent/Noise" def add_reaction_diffusion(self, latent, seed, iterations, feed_rate, kill_rate, diffusion_u, diffusion_v, time_step, strength, channel_mode, temporal_mode): device = comfy.model_management.get_torch_device() latent_tensor = latent["samples"].clone().to(device) generator = lambda size, noise_seed: _gray_scott_pattern(size, noise_seed, iterations, feed_rate, kill_rate, diffusion_u, diffusion_v, time_step, device) output = _apply_2d_noise(latent_tensor, seed, strength, channel_mode, temporal_mode, generator) out = latent.copy() out["samples"] = output.cpu() return (out,) class ImageReactionDiffusion: """Runs a Gray-Scott reaction-diffusion simulation and injects the resulting pattern into an image.""" @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE", {"tooltip": "Image that will receive reaction-diffusion patterns."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for the initial chemical concentrations."}), "iterations": ("INT", {"default": 200, "min": 1, "max": 2000, "tooltip": "Number of Gray-Scott simulation steps."}), "feed_rate": ("FLOAT", {"default": 0.036, "min": 0.0, "max": 0.1, "step": 0.001, "tooltip": "Feed rate (F) controlling how quickly chemical U is replenished."}), "kill_rate": ("FLOAT", {"default": 0.065, "min": 0.0, "max": 0.1, "step": 0.001, "tooltip": "Kill rate (K) regulating removal of chemical V."}), "diffusion_u": ("FLOAT", {"default": 0.16, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Diffusion rate for chemical U."}), "diffusion_v": ("FLOAT", {"default": 0.08, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Diffusion rate for chemical V."}), "time_step": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 5.0, "step": 0.01, "tooltip": "Simulation time step used during integration."}), "strength": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Scale of the normalized pattern relative to the image's standard deviation."}), "channel_mode": (["shared", "per_channel"], {"default": "shared", "tooltip": "Reuse one simulation for all channels or rerun per channel."}), "temporal_mode": (["locked", "animated"], {"default": "locked", "tooltip": "locked reuses the same pattern for every frame; animated reruns the simulation per frame."}), } } RETURN_TYPES = ("IMAGE",) FUNCTION = "add_reaction_diffusion" CATEGORY = "Image/Noise" def add_reaction_diffusion(self, image, seed, iterations, feed_rate, kill_rate, diffusion_u, diffusion_v, time_step, strength, channel_mode, temporal_mode): device = comfy.model_management.get_torch_device() image_tensor = image.clone().to(device) generator = lambda size, noise_seed: _gray_scott_pattern(size, noise_seed, iterations, feed_rate, kill_rate, diffusion_u, diffusion_v, time_step, device) output = _apply_image_2d_noise(image_tensor, seed, strength, channel_mode, temporal_mode, generator) return (output.cpu(),) class LatentFractalBrownianMotion: """Builds fractal Brownian motion from a selectable base noise and injects it into the latent.""" @classmethod def INPUT_TYPES(cls): return { "required": { "latent": ("LATENT", {"tooltip": "Latent to enrich with fractal Brownian motion."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for the base noise generator."}), "base_noise": (["simplex", "perlin", "worley"], {"default": "simplex", "tooltip": "Noise primitive accumulated by the fBm stack."}), "frequency": ("FLOAT", {"default": 2.0, "min": 0.01, "max": 64.0, "step": 0.01, "tooltip": "Fundamental frequency for simplex/perlin bases (acts as a multiplier for Worley)."}), "feature_points": ("INT", {"default": 16, "min": 1, "max": 4096, "tooltip": "Base feature point count (used when base noise is Worley)."}), "octaves": ("INT", {"default": 5, "min": 1, "max": 12, "tooltip": "Number of fBm layers to accumulate."}), "persistence": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Amplitude multiplier between fBm layers."}), "lacunarity": ("FLOAT", {"default": 2.0, "min": 1.0, "max": 6.0, "step": 0.1, "tooltip": "Frequency multiplier between fBm layers."}), "distance_metric": (["euclidean", "manhattan", "chebyshev"], {"default": "euclidean", "tooltip": "Distance metric used when the base noise is Worley."}), "jitter": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Feature jitter amount for Worley base noise."}), "strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Scale of the normalized fBm field relative to the latent's standard deviation."}), "channel_mode": (["shared", "per_channel"], {"default": "shared", "tooltip": "Shared fBm field per sample or reseeded per channel."}), "temporal_mode": (["locked", "animated"], {"default": "locked", "tooltip": "locked reuses the same fBm per frame; animated reseeds each frame."}), } } RETURN_TYPES = ("LATENT",) FUNCTION = "add_fbm_noise" CATEGORY = "Latent/Noise" def add_fbm_noise(self, latent, seed, base_noise, frequency, feature_points, octaves, persistence, lacunarity, distance_metric, jitter, strength, channel_mode, temporal_mode): device = comfy.model_management.get_torch_device() latent_tensor = latent["samples"].clone().to(device) def generator(size, noise_seed): if base_noise == "perlin": return _fractal_perlin(size, noise_seed, frequency, octaves, persistence, lacunarity, device) if base_noise == "worley": points = float(max(1, feature_points)) * max(frequency, 0.01) return _fractal_worley(size, noise_seed, points, octaves, persistence, lacunarity, distance_metric, jitter, device) return _fractal_simplex(size, noise_seed, frequency, octaves, persistence, lacunarity, device) output = _apply_2d_noise(latent_tensor, seed, strength, channel_mode, temporal_mode, generator) out = latent.copy() out["samples"] = output.cpu() return (out,) class ImageFractalBrownianMotion: """Builds fractal Brownian motion from a selectable base noise and injects it into the image.""" @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE", {"tooltip": "Image to enrich with fractal Brownian motion."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for the base noise generator."}), "base_noise": (["simplex", "perlin", "worley"], {"default": "simplex", "tooltip": "Noise primitive accumulated by the fBm stack."}), "frequency": ("FLOAT", {"default": 2.0, "min": 0.01, "max": 64.0, "step": 0.01, "tooltip": "Fundamental frequency for simplex/perlin bases (acts as a multiplier for Worley)."}), "feature_points": ("INT", {"default": 16, "min": 1, "max": 4096, "tooltip": "Base feature point count (used when base noise is Worley)."}), "octaves": ("INT", {"default": 5, "min": 1, "max": 12, "tooltip": "Number of fBm layers to accumulate."}), "persistence": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Amplitude multiplier between fBm layers."}), "lacunarity": ("FLOAT", {"default": 2.0, "min": 1.0, "max": 6.0, "step": 0.1, "tooltip": "Frequency multiplier between fBm layers."}), "distance_metric": (["euclidean", "manhattan", "chebyshev"], {"default": "euclidean", "tooltip": "Distance metric used when the base noise is Worley."}), "jitter": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Feature jitter amount for Worley base noise."}), "strength": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Scale of the normalized fBm field relative to the image's standard deviation."}), "channel_mode": (["shared", "per_channel"], {"default": "shared", "tooltip": "Shared fBm field per sample or reseeded per channel."}), "temporal_mode": (["locked", "animated"], {"default": "locked", "tooltip": "locked reuses the same fBm per frame; animated reseeds each frame."}), } } RETURN_TYPES = ("IMAGE",) FUNCTION = "add_fbm_noise" CATEGORY = "Image/Noise" def add_fbm_noise(self, image, seed, base_noise, frequency, feature_points, octaves, persistence, lacunarity, distance_metric, jitter, strength, channel_mode, temporal_mode): device = comfy.model_management.get_torch_device() image_tensor = image.clone().to(device) def generator(size, noise_seed): if base_noise == "perlin": return _fractal_perlin(size, noise_seed, frequency, octaves, persistence, lacunarity, device) if base_noise == "worley": points = float(max(1, feature_points)) * max(frequency, 0.01) return _fractal_worley(size, noise_seed, points, octaves, persistence, lacunarity, distance_metric, jitter, device) return _fractal_simplex(size, noise_seed, frequency, octaves, persistence, lacunarity, device) output = _apply_image_2d_noise(image_tensor, seed, strength, channel_mode, temporal_mode, generator) return (output.cpu(),) class LatentSwirlNoise: """Swirls latent pixels around randomized centers for vortex-like perturbations.""" @classmethod def INPUT_TYPES(cls): return { "required": { "latent": ("LATENT", {"tooltip": "Latent to deform with vortex-style warps."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for vortex placement and direction randomness."}), "vortices": ("INT", { "default": 1, "min": 1, "max": 16, "tooltip": "Number of independent vortex centres to spawn per latent." }), "channel_mode": (["global", "per_channel"], { "default": "global", "tooltip": "Use a shared swirl grid for all channels or generate unique grids per affected channel." }), "channel_fraction": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Fraction of channels to swirl. The subset is randomly selected per sample." }), "strength": ("FLOAT", { "default": 0.75, "min": 0.0, "max": 6.28, "step": 0.01, "tooltip": "Peak swirl rotation in radians near the vortex center." }), "radius": ("FLOAT", { "default": 0.5, "min": 0.05, "max": 2.0, "step": 0.01, "tooltip": "Normalized radius controlling how far the vortex influence extends." }), "center_spread": ("FLOAT", { "default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "How far the vortex origin drifts from the latent center." }), "direction_bias": ("FLOAT", { "default": 0.0, "min": -1.0, "max": 1.0, "step": 0.05, "tooltip": "Bias toward counter-clockwise (1) or clockwise (-1) swirl." }), "mix": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Blend between original latent (0) and fully swirled result (1)." }), } } RETURN_TYPES = ("LATENT",) FUNCTION = "add_swirl_noise" CATEGORY = "Latent/Noise" def add_swirl_noise(self, latent, seed, vortices, channel_mode, channel_fraction, strength, radius, center_spread, direction_bias, mix): if strength == 0.0 or mix == 0.0: return (latent,) device = comfy.model_management.get_torch_device() latent_tensor = latent["samples"].clone().to(device) is_video = latent_tensor.dim() == 5 if is_video: batch, channels, frames, height, width = latent_tensor.shape working = latent_tensor.permute(0, 2, 1, 3, 4).reshape(batch * frames, channels, height, width) else: batch, channels, height, width = latent_tensor.shape frames = 1 working = latent_tensor base_dtype = torch.float32 base_y, base_x = _normalized_meshgrid(height, width, device, base_dtype) result = torch.empty_like(working) center_spread = max(0.0, min(center_spread, 1.0)) mix = max(0.0, min(mix, 1.0)) bias_threshold = max(0.0, min(1.0, (direction_bias + 1.0) * 0.5)) vortex_count = max(1, int(vortices)) channel_fraction = max(0.0, min(1.0, channel_fraction)) cpu_generator = torch.Generator(device="cpu").manual_seed(seed) total = working.shape[0] for idx in range(total): if channel_fraction <= 0.0: result[idx] = working[idx] continue channel_count = working.shape[1] selected_count = min( channel_count, max(1, math.ceil(channel_count * channel_fraction)) ) channel_indices = torch.randperm(channel_count, generator=cpu_generator)[:selected_count] sample = working[idx:idx + 1] sample_result = sample.clone() if channel_mode == "global": rand_vals = torch.rand((vortex_count, 3), generator=cpu_generator) vortex_params = [] for vortex_idx in range(vortex_count): offsets = (rand_vals[vortex_idx, :2] * 2.0 - 1.0) * center_spread center_y = offsets[0].item() center_x = offsets[1].item() direction = 1.0 if rand_vals[vortex_idx, 2].item() < bias_threshold else -1.0 vortex_params.append((center_x, center_y, float(strength), float(radius), direction)) grid = _swirl_grid(base_y, base_x, vortex_params).unsqueeze(0) warped = F.grid_sample( sample.to(base_dtype), grid, mode="bilinear", padding_mode="reflection", align_corners=True, ).to(sample.dtype) if mix >= 1.0: sample_result[:, channel_indices] = warped[:, channel_indices] else: delta = warped[:, channel_indices] - sample[:, channel_indices] sample_result[:, channel_indices] = sample[:, channel_indices] + delta * mix else: # per_channel for channel_idx in channel_indices.tolist(): rand_vals = torch.rand((vortex_count, 3), generator=cpu_generator) vortex_params = [] for vortex_idx in range(vortex_count): offsets = (rand_vals[vortex_idx, :2] * 2.0 - 1.0) * center_spread center_y = offsets[0].item() center_x = offsets[1].item() direction = 1.0 if rand_vals[vortex_idx, 2].item() < bias_threshold else -1.0 vortex_params.append((center_x, center_y, float(strength), float(radius), direction)) grid = _swirl_grid(base_y, base_x, vortex_params).unsqueeze(0) channel_slice = sample[:, channel_idx:channel_idx + 1].to(base_dtype) warped_channel = F.grid_sample( channel_slice, grid, mode="bilinear", padding_mode="reflection", align_corners=True, ).to(sample.dtype) if mix >= 1.0: sample_result[:, channel_idx:channel_idx + 1] = warped_channel else: delta = warped_channel - sample[:, channel_idx:channel_idx + 1] sample_result[:, channel_idx:channel_idx + 1] = sample[:, channel_idx:channel_idx + 1] + delta * mix result[idx] = sample_result[0] if is_video: result = result.reshape(batch, frames, channels, height, width).permute(0, 2, 1, 3, 4) out = latent.copy() out["samples"] = result.cpu() return (out,) class ImageSwirlNoise: """Swirls image pixels around randomized centers for vortex-like perturbations.""" @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE", {"tooltip": "Image to deform with vortex-style warps."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for vortex placement and direction randomness."}), "vortices": ("INT", { "default": 1, "min": 1, "max": 16, "tooltip": "Number of independent vortex centres to spawn per image.", }), "channel_mode": (["global", "per_channel"], { "default": "global", "tooltip": "Use a shared swirl grid for all channels or generate unique grids per affected channel.", }), "channel_fraction": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Fraction of channels to swirl. The subset is randomly selected per sample.", }), "strength": ("FLOAT", { "default": 0.75, "min": 0.0, "max": 6.28, "step": 0.01, "tooltip": "Peak swirl rotation in radians near the vortex center.", }), "radius": ("FLOAT", { "default": 0.5, "min": 0.05, "max": 2.0, "step": 0.01, "tooltip": "Normalized radius controlling how far the vortex influence extends.", }), "center_spread": ("FLOAT", { "default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "How far the vortex origin drifts from the image center.", }), "direction_bias": ("FLOAT", { "default": 0.0, "min": -1.0, "max": 1.0, "step": 0.05, "tooltip": "Bias toward counter-clockwise (1) or clockwise (-1) swirl.", }), "mix": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Blend between original image (0) and fully swirled result (1).", }), } } RETURN_TYPES = ("IMAGE",) FUNCTION = "add_swirl_noise" CATEGORY = "Image/Noise" def add_swirl_noise(self, image, seed, vortices, channel_mode, channel_fraction, strength, radius, center_spread, direction_bias, mix): if strength == 0.0 or mix == 0.0: return (image,) device = comfy.model_management.get_torch_device() image_tensor = image.clone().to(device) is_video = image_tensor.dim() == 5 if is_video: batch, frames, height, width, channels = image_tensor.shape working = image_tensor.permute(0, 1, 4, 2, 3).reshape(batch * frames, channels, height, width) else: batch, height, width, channels = image_tensor.shape frames = 1 working = image_tensor.permute(0, 3, 1, 2) base_dtype = torch.float32 base_y, base_x = _normalized_meshgrid(height, width, device, base_dtype) result = torch.empty_like(working) center_spread = max(0.0, min(center_spread, 1.0)) mix = max(0.0, min(mix, 1.0)) bias_threshold = max(0.0, min(1.0, (direction_bias + 1.0) * 0.5)) vortex_count = max(1, int(vortices)) channel_fraction = max(0.0, min(1.0, channel_fraction)) cpu_generator = torch.Generator(device="cpu").manual_seed(seed) total = working.shape[0] for idx in range(total): if channel_fraction <= 0.0: result[idx] = working[idx] continue channel_count = working.shape[1] selected_count = min( channel_count, max(1, math.ceil(channel_count * channel_fraction)) ) channel_indices = torch.randperm(channel_count, generator=cpu_generator)[:selected_count] sample = working[idx:idx + 1] sample_result = sample.clone() if channel_mode == "global": rand_vals = torch.rand((vortex_count, 3), generator=cpu_generator) vortex_params = [] for vortex_idx in range(vortex_count): offsets = (rand_vals[vortex_idx, :2] * 2.0 - 1.0) * center_spread center_y = offsets[0].item() center_x = offsets[1].item() direction = 1.0 if rand_vals[vortex_idx, 2].item() < bias_threshold else -1.0 vortex_params.append((center_x, center_y, float(strength), float(radius), direction)) grid = _swirl_grid(base_y, base_x, vortex_params).unsqueeze(0) warped = F.grid_sample( sample.to(base_dtype), grid, mode="bilinear", padding_mode="reflection", align_corners=True, ).to(sample.dtype) if mix >= 1.0: sample_result[:, channel_indices] = warped[:, channel_indices] else: delta = warped[:, channel_indices] - sample[:, channel_indices] sample_result[:, channel_indices] = sample[:, channel_indices] + delta * mix else: # per_channel for channel_idx in channel_indices.tolist(): rand_vals = torch.rand((vortex_count, 3), generator=cpu_generator) vortex_params = [] for vortex_idx in range(vortex_count): offsets = (rand_vals[vortex_idx, :2] * 2.0 - 1.0) * center_spread center_y = offsets[0].item() center_x = offsets[1].item() direction = 1.0 if rand_vals[vortex_idx, 2].item() < bias_threshold else -1.0 vortex_params.append((center_x, center_y, float(strength), float(radius), direction)) grid = _swirl_grid(base_y, base_x, vortex_params).unsqueeze(0) channel_slice = sample[:, channel_idx:channel_idx + 1].to(base_dtype) warped_channel = F.grid_sample( channel_slice, grid, mode="bilinear", padding_mode="reflection", align_corners=True, ).to(sample.dtype) if mix >= 1.0: sample_result[:, channel_idx:channel_idx + 1] = warped_channel else: delta = warped_channel - sample[:, channel_idx:channel_idx + 1] sample_result[:, channel_idx:channel_idx + 1] = sample[:, channel_idx:channel_idx + 1] + delta * mix result[idx] = sample_result[0] if is_video: result = result.reshape(batch, frames, channels, height, width).permute(0, 1, 3, 4, 2) else: result = result.permute(0, 2, 3, 1) return (result.cpu(),) class LatentForwardDiffusion: """ Applies the 'natural' forward diffusion process to a clean latent. This produces a statistically 'perfect' noisy latent that samplers expect. """ @classmethod def INPUT_TYPES(s): return { "required": { "model": ("MODEL", {"tooltip": "Diffusion model that defines the forward noise schedule."}), "latent": ("LATENT", {"tooltip": "Clean latent to push forward along the schedule."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed for the forward diffusion noise."}), "steps": ("INT", {"default": 20, "min": 1, "max": 10000, "tooltip": "Number of steps in the sampler's schedule."}), "noise_strength": ("FLOAT", { "default": 0.8, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "The point in the schedule to noise to. Must match the KSampler's effective start step." }), } } RETURN_TYPES = ("LATENT",) FUNCTION = "add_scheduled_noise" CATEGORY = "Latent/Noise" def add_scheduled_noise(self, model, latent, seed, steps, noise_strength): if noise_strength == 0.0: return (latent,) device = comfy.model_management.get_torch_device() latent_tensor = latent["samples"].clone().to(device) sampler = comfy.samplers.KSampler(model, steps=steps, device=device) sigmas = sampler.sigmas start_step = steps - int(steps * noise_strength) if start_step >= len(sigmas): start_step = len(sigmas) - 1 sigma = sigmas[start_step].to(device) generator = torch.Generator(device=device).manual_seed(seed) noise = torch.randn( latent_tensor.shape, generator=generator, device=device, dtype=latent_tensor.dtype ) noised_latent = latent_tensor + noise * sigma out = latent.copy() out["samples"] = noised_latent.cpu() return (out,) class ConditioningAddNoise: """Adds seeded Gaussian noise to conditioning embeddings and pooled outputs.""" @classmethod def INPUT_TYPES(cls): return { "required": { "conditioning": ("CONDITIONING", {"tooltip": "Conditioning list to perturb with Gaussian noise."}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Seed that drives the conditioning noise."}), "strength": ("FLOAT", { "default": 0.1, "min": 0.0, "max": 5.0, "step": 0.01, "tooltip": "Noise strength relative to each tensor's standard deviation." }), } } RETURN_TYPES = ("CONDITIONING",) FUNCTION = "add_noise" CATEGORY = "conditioning/noise" def add_noise(self, conditioning, seed, strength): if strength == 0.0: return (conditioning,) generator = torch.Generator(device="cpu").manual_seed(seed) result = [] for embedding, metadata in conditioning: if not isinstance(embedding, torch.Tensor): result.append([embedding, metadata]) continue embedding_std = torch.std(embedding) noise = torch.randn( embedding.shape, generator=generator, device=embedding.device, dtype=embedding.dtype, ) noised_embedding = embedding + noise * embedding_std * strength new_metadata = dict(metadata) pooled_output = new_metadata.get("pooled_output") if isinstance(pooled_output, torch.Tensor): pooled_std = torch.std(pooled_output) pooled_noise = torch.randn( pooled_output.shape, generator=generator, device=pooled_output.device, dtype=pooled_output.dtype, ) new_metadata["pooled_output"] = pooled_output + pooled_noise * pooled_std * strength result.append([noised_embedding, new_metadata]) return (result,) class ConditioningGaussianBlur: """Applies Gaussian smoothing along the token dimension of conditioning embeddings.""" @classmethod def INPUT_TYPES(cls): return { "required": { "conditioning": ("CONDITIONING", {"tooltip": "Conditioning list whose token dimension will be blurred."}), "sigma": ("FLOAT", { "default": 0.75, "min": 0.0, "max": 10.0, "step": 0.05, "tooltip": "Standard deviation of the blur kernel along the token axis." }), } } RETURN_TYPES = ("CONDITIONING",) FUNCTION = "blur" CATEGORY = "conditioning/filter" def blur(self, conditioning, sigma): if sigma <= 0.0: return (conditioning,) result = [] for embedding, metadata in conditioning: if not isinstance(embedding, torch.Tensor) or embedding.dim() < 2: result.append([embedding, metadata]) continue needs_batch_dim = embedding.dim() == 2 embedding_tensor = embedding.unsqueeze(0) if needs_batch_dim else embedding conv_dtype = self._select_conv_dtype(embedding_tensor) blur_kernel, padding = self._build_kernel(embedding_tensor.device, conv_dtype, sigma) original_shape = embedding_tensor.shape tokens = original_shape[-2] features = original_shape[-1] batch = math.prod(original_shape[:-2]) if len(original_shape) > 2 else original_shape[0] reshaped = embedding_tensor.reshape(batch, tokens, features).permute(0, 2, 1) if reshaped.dtype != conv_dtype: reshaped = reshaped.to(conv_dtype) kernel = blur_kernel.repeat(features, 1, 1) blurred = F.conv1d(reshaped, kernel, padding=padding, groups=features) blurred = blurred.permute(0, 2, 1).reshape(original_shape) if blurred.dtype != embedding_tensor.dtype: blurred = blurred.to(embedding_tensor.dtype) if needs_batch_dim: blurred = blurred.squeeze(0) result.append([blurred, metadata]) return (result,) @staticmethod def _build_kernel(device, dtype, sigma): kernel_size = int(sigma * 6) + 1 if kernel_size % 2 == 0: kernel_size += 1 half = kernel_size // 2 positions = torch.arange(kernel_size, device=device, dtype=dtype) - half kernel = torch.exp(-0.5 * (positions / max(sigma, 1e-6)) ** 2) kernel /= kernel.sum() kernel = kernel.view(1, 1, kernel_size) return kernel, half @staticmethod def _select_conv_dtype(tensor): if tensor.device.type == "cpu" and tensor.dtype not in (torch.float32, torch.float64): return torch.float32 return tensor.dtype class ConditioningFrequencySplit: """Separates conditioning embeddings into low/high bands via Gaussian smoothing.""" @classmethod def INPUT_TYPES(cls): return { "required": { "conditioning": ("CONDITIONING", {"tooltip": "Conditioning list to separate into low/high bands."}), "sigma": ("FLOAT", { "default": 0.75, "min": 0.0, "max": 10.0, "step": 0.05, "tooltip": "Cutoff for the Gaussian low-pass applied along the token axis." }), } } RETURN_TYPES = ("CONDITIONING", "CONDITIONING") RETURN_NAMES = ("low_pass", "high_pass") FUNCTION = "split" CATEGORY = "conditioning/filter" def split(self, conditioning, sigma): if sigma <= 0.0: low_list = [self._clone_entry(item) for item in conditioning] high_list = [self._zero_entry(item) for item in conditioning] return (low_list, high_list) low_list = [] high_list = [] for embedding, metadata in conditioning: if not isinstance(embedding, torch.Tensor) or embedding.dim() < 2: low_list.append([embedding, dict(metadata)]) high_list.append(self._zero_entry([embedding, metadata])) continue needs_batch_dim = embedding.dim() == 2 embedding_tensor = embedding.unsqueeze(0) if needs_batch_dim else embedding conv_dtype = ConditioningGaussianBlur._select_conv_dtype(embedding_tensor) kernel, padding = ConditioningGaussianBlur._build_kernel(embedding_tensor.device, conv_dtype, sigma) original_shape = embedding_tensor.shape tokens = original_shape[-2] features = original_shape[-1] batch = math.prod(original_shape[:-2]) if len(original_shape) > 2 else original_shape[0] reshaped = embedding_tensor.reshape(batch, tokens, features).permute(0, 2, 1) if reshaped.dtype != conv_dtype: reshaped = reshaped.to(conv_dtype) kernel = kernel.repeat(features, 1, 1) blurred = F.conv1d(reshaped, kernel, padding=padding, groups=features) blurred = blurred.permute(0, 2, 1).reshape(original_shape) if blurred.dtype != embedding_tensor.dtype: blurred = blurred.to(embedding_tensor.dtype) if needs_batch_dim: blurred = blurred.squeeze(0) high_embedding = embedding - blurred low_meta = dict(metadata) high_meta = dict(metadata) pooled_output = low_meta.get("pooled_output") if isinstance(pooled_output, torch.Tensor): high_meta["pooled_output"] = torch.zeros_like(pooled_output) low_list.append([blurred, low_meta]) high_list.append([high_embedding, high_meta]) return (low_list, high_list) @staticmethod def _clone_entry(entry): embedding, metadata = entry return [embedding, dict(metadata)] @staticmethod def _zero_entry(entry): embedding, metadata = entry if isinstance(embedding, torch.Tensor): zero_embedding = torch.zeros_like(embedding) else: zero_embedding = embedding meta_copy = dict(metadata) pooled_output = meta_copy.get("pooled_output") if isinstance(pooled_output, torch.Tensor): meta_copy["pooled_output"] = torch.zeros_like(pooled_output) return [zero_embedding, meta_copy] class ConditioningFrequencyMerge: """Recombines low/high conditioning bands back into a single conditioning list.""" @classmethod def INPUT_TYPES(cls): return { "required": { "low_pass": ("CONDITIONING", {"tooltip": "Low-frequency conditioning list produced by the split node."}), "high_pass": ("CONDITIONING", {"tooltip": "High-frequency conditioning list to recombine."}), "low_gain": ("FLOAT", { "default": 1.0, "min": -5.0, "max": 5.0, "step": 0.05, "tooltip": "Multiplier for the low-pass band before merging." }), "high_gain": ("FLOAT", { "default": 1.0, "min": -5.0, "max": 5.0, "step": 0.05, "tooltip": "Multiplier for the high-pass band before merging." }), } } RETURN_TYPES = ("CONDITIONING",) FUNCTION = "merge" CATEGORY = "conditioning/filter" def merge(self, low_pass, high_pass, low_gain, high_gain): if len(low_pass) != len(high_pass): raise ValueError("Low-pass and high-pass conditioning lists must have the same length") merged = [] for low_item, high_item in zip(low_pass, high_pass): low_embedding, low_meta = low_item high_embedding, high_meta = high_item if not isinstance(low_embedding, torch.Tensor) or not isinstance(high_embedding, torch.Tensor): merged.append([low_embedding, dict(low_meta)]) continue combined_embedding = low_embedding * low_gain + high_embedding * high_gain new_metadata = dict(low_meta) low_pooled = low_meta.get("pooled_output") high_pooled = high_meta.get("pooled_output") if isinstance(high_meta, dict) else None if isinstance(low_pooled, torch.Tensor) or isinstance(high_pooled, torch.Tensor): low_pooled_tensor = low_pooled if isinstance(low_pooled, torch.Tensor) else torch.zeros_like(high_pooled) high_pooled_tensor = high_pooled if isinstance(high_pooled, torch.Tensor) else torch.zeros_like(low_pooled_tensor) new_metadata["pooled_output"] = low_pooled_tensor * low_gain + high_pooled_tensor * high_gain merged.append([combined_embedding, new_metadata]) return (merged,) class ConditioningScale: """Scales conditioning embeddings (and pooled outputs) to amplify or mute prompt influence.""" @classmethod def INPUT_TYPES(cls): return { "required": { "conditioning": ("CONDITIONING", {"tooltip": "Conditioning list to scale."}), "factor": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 10.0, "step": 0.05, "tooltip": "Multiplier applied to embeddings. 0.0 mutes, 1.0 keeps original strength." }), } } RETURN_TYPES = ("CONDITIONING",) FUNCTION = "scale" CATEGORY = "conditioning/filter" def scale(self, conditioning, factor): if factor == 1.0: return (conditioning,) result = [] for embedding, metadata in conditioning: if not isinstance(embedding, torch.Tensor): result.append([embedding, metadata]) continue scaled_embedding = embedding * factor new_metadata = dict(metadata) pooled_output = new_metadata.get("pooled_output") if isinstance(pooled_output, torch.Tensor): new_metadata["pooled_output"] = pooled_output * factor result.append([scaled_embedding, new_metadata]) return (result,) NODE_CLASS_MAPPINGS = { "QwenRectifiedFlowInverter": QwenRectifiedFlowInverter, "LatentGaussianBlur": LatentGaussianBlur, "LatentFrequencySplit": LatentFrequencySplit, "LatentAddNoise": LatentAddNoise, "LatentPerlinFractalNoise": LatentPerlinFractalNoise, "LatentSimplexNoise": LatentSimplexNoise, "LatentWorleyNoise": LatentWorleyNoise, "LatentReactionDiffusion": LatentReactionDiffusion, "LatentFractalBrownianMotion": LatentFractalBrownianMotion, "LatentSwirlNoise": LatentSwirlNoise, "ImageAddNoise": ImageAddNoise, "ImagePerlinFractalNoise": ImagePerlinFractalNoise, "ImageSimplexNoise": ImageSimplexNoise, "ImageWorleyNoise": ImageWorleyNoise, "ImageReactionDiffusion": ImageReactionDiffusion, "ImageFractalBrownianMotion": ImageFractalBrownianMotion, "ImageSwirlNoise": ImageSwirlNoise, "LatentForwardDiffusion": LatentForwardDiffusion, "LatentHybridInverter": LatentHybridInverter, "ConditioningAddNoise": ConditioningAddNoise, "ConditioningGaussianBlur": ConditioningGaussianBlur, "ConditioningFrequencySplit": ConditioningFrequencySplit, "ConditioningFrequencyMerge": ConditioningFrequencyMerge, "ConditioningScale": ConditioningScale, } NODE_DISPLAY_NAME_MAPPINGS = { "QwenRectifiedFlowInverter": "Qwen Rectified Flow Inverter", "LatentGaussianBlur": "Latent Gaussian Blur", "LatentFrequencySplit": "Latent Frequency Split", "LatentAddNoise": "Add Latent Noise (Seeded)", "LatentPerlinFractalNoise": "Latent Perlin Fractal Noise", "LatentSimplexNoise": "Latent Simplex Noise", "LatentWorleyNoise": "Latent Worley Noise", "LatentReactionDiffusion": "Latent Reaction-Diffusion", "LatentFractalBrownianMotion": "Latent Fractal Brownian Motion", "LatentSwirlNoise": "Latent Swirl Noise", "ImageAddNoise": "Add Image Noise (Seeded)", "ImagePerlinFractalNoise": "Image Perlin Fractal Noise", "ImageSimplexNoise": "Image Simplex Noise", "ImageWorleyNoise": "Image Worley Noise", "ImageReactionDiffusion": "Image Reaction-Diffusion", "ImageFractalBrownianMotion": "Image Fractal Brownian Motion", "ImageSwirlNoise": "Image Swirl Noise", "LatentForwardDiffusion": "Forward Diffusion (Add Scheduled Noise)", "LatentHybridInverter": "Latent Hybrid Inverter (Qwen)", "ConditioningAddNoise": "Conditioning (Add Noise)", "ConditioningGaussianBlur": "Conditioning (Gaussian Blur)", "ConditioningFrequencySplit": "Conditioning (Frequency Split)", "ConditioningFrequencyMerge": "Conditioning (Frequency Merge)", "ConditioningScale": "Conditioning (Scale)", }