Files
nawka12andClaude Sonnet 4.6 d1c1930989 sync: port WebUI changes — remove EQ-VAE mode, refactor AkashicEQFlow
- Remove eqvae_mode parameter from AkashicSolverSampler and sample_akashic_solver
- Remove eqvae_enabled branching (tau, eta, s_noise, NDB paths)
- Remove compute_eqvae_tau/noise_scale/ndb helper functions from utils.py
- Remove eqvae_mode/eqvae_blur_sigma params from sa_solver_step
- Refactor AkashicEQFlow to pure CDF-based schedule (removes Karras blend,
  asymmetric widths, ratio safety nets — matches WebUI commit fa06979)
- Bump version 1.3.0 → 1.4.0
- Update README to reflect eqvae_mode removal and EQFlow changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 05:58:15 +07:00

591 lines
21 KiB
Python

"""
Utility functions for Adept Sampler ComfyUI nodes.
Ported from Stable Diffusion WebUI reForge extension.
"""
import math
import torch
try:
from torchvision.transforms.functional import gaussian_blur
TORCHVISION_AVAILABLE = True
except ImportError:
TORCHVISION_AVAILABLE = False
def to_d(x, sigma, denoised):
"""Convert denoised prediction to derivative with robust numerical stability."""
diff = x - denoised
safe_sigma = torch.clamp(sigma, min=1e-4)
derivative = diff / safe_sigma
# Normalize derivative by sigma to handle different prediction types
sigma_adaptive_threshold = 1000.0 * (1.0 + sigma / 10.0)
derivative_max = torch.abs(derivative).max()
if derivative_max > sigma_adaptive_threshold:
derivative = torch.clamp(derivative, -sigma_adaptive_threshold, sigma_adaptive_threshold)
return derivative
def to_d_enhanced_ancestral(x, sigma, denoised, eta, progress, generator=None):
"""
Enhanced derivative computation optimized for ancestral sampling.
Provides ancestral-specific derivative corrections that adapt
based on the sampling progress and eta value.
"""
diff = x - denoised
safe_sigma = torch.clamp(sigma, min=1e-4)
base_derivative = diff / safe_sigma
def safe_randn_like(tensor, generator=None):
if generator is None:
return torch.randn_like(tensor)
try:
return torch.randn(tensor.shape, device=tensor.device, dtype=tensor.dtype, generator=generator)
except (TypeError, AttributeError):
return torch.randn_like(tensor)
# Ancestral-specific enhancements (conservative to reduce noisiness)
if eta > 1.0:
eta_correction = 0.02 * (eta - 1.0) * safe_randn_like(diff, generator) * progress
base_derivative = base_derivative + eta_correction
elif eta < 1.0:
eta_correction = 0.015 * (1.0 - eta) * safe_randn_like(diff, generator) * (1.0 - progress)
base_derivative = base_derivative - eta_correction
# Progress-based phase corrections
if progress < 0.3:
phase_correction = 0.01 * safe_randn_like(diff, generator)
base_derivative = base_derivative + phase_correction
elif progress > 0.7:
phase_correction = 0.008 * safe_randn_like(diff, generator)
base_derivative = base_derivative - phase_correction
# Final safety check
sigma_adaptive_threshold = 500.0 * (1.0 + sigma / 10.0)
derivative_max = torch.abs(base_derivative).max()
if derivative_max > sigma_adaptive_threshold:
base_derivative = torch.clamp(base_derivative, -sigma_adaptive_threshold, sigma_adaptive_threshold)
return base_derivative
def get_noise_sampler(x):
"""Get proper noise sampler with working fallback."""
try:
import comfy.k_diffusion.sampling as k_sampling
if hasattr(k_sampling, 'default_noise_sampler'):
return k_sampling.default_noise_sampler(x)
except ImportError:
pass
# Fallback noise sampler
def simple_noise_sampler(sigma_from, sigma_to):
noise = torch.randn_like(x)
if abs(sigma_to - sigma_from) > 1e-6:
scale = (sigma_to / sigma_from.clamp(min=1e-6)).sqrt()
noise = noise * scale
return noise
return simple_noise_sampler
def apply_dynamic_thresholding(x, percentile=0.995, clamp_range=1.0):
"""Optimized dynamic thresholding with better stability."""
if percentile >= 1.0:
return x
try:
batch_size = x.shape[0]
x_flat = x.view(batch_size, -1)
abs_max = torch.abs(x_flat).max(dim=1, keepdim=True)[0]
if abs_max.max() < 5.0:
return x
k = max(1, int(x_flat.shape[1] * (1.0 - percentile)))
topk_vals = torch.topk(torch.abs(x_flat), k=k, dim=1, largest=True)[0]
s = topk_vals[:, -1:].clamp(min=1.0)
threshold = s * 2.5
mask = torch.abs(x_flat) > threshold
x_flat = torch.where(mask, torch.sign(x_flat) * threshold, x_flat)
x_flat = x_flat * 0.98
return x_flat.view(x.shape)
except Exception as e:
print(f"⚠️ Dynamic thresholding failed: {e}")
return x
def compute_compensation_ratio(r, step_idx, total_steps, base_ratio=1.0):
"""
Compute dynamic compensation ratio inspired by DC-Solver.
Adapts interpolation based on step position.
"""
progress = step_idx / max(total_steps - 1, 1)
if progress < 0.3:
phase_weight = 1.5
elif progress < 0.7:
phase_weight = 1.0
else:
phase_weight = 1.3
compensation = base_ratio * phase_weight * (1.0 + 0.1 * math.tanh(r - 1.0))
return compensation
def compute_tau_eqvae(progress, base_tau=0.5, phase_strength=0.5):
"""
Phase-aware tau function for SA-Solver style stochasticity control.
Optimized for EQ-VAE's smooth latent space.
"""
if progress < 0.30:
phase_factor = 1.0 + 0.2 * phase_strength
elif progress < 0.60:
phase_factor = 1.0 - 0.15 * phase_strength
else:
phase_factor = 1.0 - 0.3 * phase_strength
return min(1.0, max(0.0, base_tau * phase_factor))
def compute_smea_factor(progress, smea_strength=0.5):
"""
SMEA (Sinusoidal Multipass Euler Ancestral) inspired interpolation.
Uses sine-based schedule to improve coherency at high resolutions.
"""
if smea_strength <= 0:
return 1.0
smea_interp = 0.5 * (1 + math.sin(math.pi * (progress - 0.5)))
return 1.0 - smea_strength * (1.0 - smea_interp)
def compute_native_detail_boost(progress, ndb_strength=0.0):
"""
Native Detail Boost (NDB): Enhances detail emergence at native resolution.
Returns:
Tuple of (base_scale, high_freq_boost)
"""
if ndb_strength <= 0:
return 1.0, 0.0
if progress < 0.30:
phase_progress = progress / 0.30
high_freq_boost = 0.03 * ndb_strength * phase_progress
elif progress < 0.60:
phase_progress = (progress - 0.30) / 0.30
high_freq_boost = (0.03 + 0.07 * phase_progress) * ndb_strength
else:
phase_progress = (progress - 0.60) / 0.40
high_freq_boost = (0.10 + 0.08 * phase_progress) * ndb_strength
return 1.0, high_freq_boost
def sa_solver_step(x, d_history, sigma, sigma_next, tau, s_noise=1.0, noise_sampler=None, order=2, ndb_strength=0.0, progress=0.0):
"""
SA-Solver inspired step with controlled stochasticity.
Uses Adams-Bashforth coefficients for multi-step integration.
Args:
x: Current latent tensor
d_history: List of (sigma, derivative) tuples from previous steps
sigma: Current sigma value
sigma_next: Next sigma value
tau: Stochasticity control (0=ODE, 1=full SDE)
s_noise: Noise scaling factor
noise_sampler: Function to generate scaled noise
order: Multi-step order (1, 2, or 3)
ndb_strength: Native Detail Boost strength (0=disabled)
progress: Sampling progress (0.0 to 1.0) for NDB phase calculation
Returns:
Tuple of (next latent, sigma_up used for noise)
"""
dt = sigma_next - sigma
# Compute interpolated derivative based on order and history
if len(d_history) >= 2 and order >= 2:
sigma_cur, d_cur = d_history[-1]
sigma_prev, d_prev = d_history[-2]
h_prev = sigma_cur - sigma_prev
r = abs(dt / (h_prev + 1e-8)) if abs(h_prev) > 1e-8 else 1.0
r = min(r, 2.0)
if len(d_history) >= 3 and order >= 3:
sigma_0, d_0 = d_history[-3]
h_0 = sigma_prev - sigma_0
h_1 = h_prev
if abs(h_0) > 1e-6 and abs(h_1) > 1e-6:
r0 = min(abs(h_1 / h_0), 2.0)
r1 = min(abs(dt / (h_1 + 1e-8)), 2.0)
tau_blend = 1.0 - tau
c0_ab3 = 1.0 + (1.0 + r0) * r1 / 2.0
c1_ab3 = -(1.0 + r0) * r1 / 2.0
c2_ab3 = r0 * r1 / 2.0
c0 = tau_blend * c0_ab3 + (1.0 - tau_blend) * 1.0
c1 = tau_blend * c1_ab3
c2 = tau_blend * c2_ab3
c_sum = c0 + c1 + c2
if abs(c_sum) > 1e-8:
c0 /= c_sum
c1 /= c_sum
c2 /= c_sum
else:
c0, c1, c2 = 1.0, 0.0, 0.0
d_interp = c0 * d_cur + c1 * d_prev + c2 * d_0
else:
tau_blend = 1.0 - tau
c1_ab2 = 1.0 + 0.5 * r
c2_ab2 = -0.5 * r
c1 = tau_blend * c1_ab2 + (1.0 - tau_blend) * 1.0
c2 = tau_blend * c2_ab2
c_sum = c1 + c2
if abs(c_sum) > 1e-8:
c1 /= c_sum
c2 /= c_sum
d_interp = c1 * d_cur + c2 * d_prev
else:
tau_blend = 1.0 - tau
c1_ab2 = 1.0 + 0.5 * r
c2_ab2 = -0.5 * r
c1 = tau_blend * c1_ab2 + (1.0 - tau_blend) * 1.0
c2 = tau_blend * c2_ab2
c_sum = c1 + c2
if abs(c_sum) > 1e-8:
c1 /= c_sum
c2 /= c_sum
d_interp = c1 * d_cur + c2 * d_prev
elif len(d_history) >= 1:
d_interp = d_history[-1][1]
else:
d_interp = torch.zeros_like(x)
# Compute sigma_up based on tau (controls stochasticity)
sigma_up = 0.0
if tau > 0 and sigma_next > 0 and noise_sampler is not None:
sigma_ancestral_sq = sigma_next ** 2 * (sigma ** 2 - sigma_next ** 2) / (sigma ** 2 + 1e-8)
sigma_ancestral = sigma_ancestral_sq ** 0.5 if sigma_ancestral_sq > 0 else 0.0
sigma_up = tau * sigma_ancestral
sigma_down = (sigma_next ** 2 - sigma_up ** 2) ** 0.5
dt_adjusted = sigma_down - sigma
x_det = x + d_interp * dt_adjusted
noise = noise_sampler(sigma, sigma_next) * s_noise * sigma_up
# Apply Native Detail Boost if enabled
if ndb_strength > 0 and TORCHVISION_AVAILABLE:
_, high_freq_boost = compute_native_detail_boost(progress, ndb_strength)
blur_sigma = 0.5
# Extract high-frequency component from noise using Gaussian blur
try:
low_freq_noise = gaussian_blur(noise, kernel_size=3, sigma=blur_sigma)
high_freq_noise = noise - low_freq_noise
noise = noise + high_freq_noise * high_freq_boost
except Exception:
pass # Fallback: use original noise if blur fails
x_next = x_det + noise
else:
x_next = x + d_interp * dt
return x_next, sigma_up
def compute_adaptive_noise_scale(excess_samples, base_s_noise,
correction_power=0.5,
dampen_floor=0.80, boost_ceiling=1.15):
"""
Adaptive noise scale using calibrated sigma-relative excess.
After collecting excess ratio samples during a warmup window inside the
texture phase, computes a single global correction factor based on the
median excess.
The excess ratio measures how much noise is slowing down convergence:
excess = (change_i / change_{i-1}) / (sigma_{i+1} / sigma_i)
When excess > 1.0, the model converges slower than sigma predicts.
Args:
excess_samples: List of excess ratio values from warmup window
base_s_noise: The effective s_noise to modulate
correction_power: How aggressively to correct (0.5 = square root)
dampen_floor: Minimum correction multiplier (default 0.80)
boost_ceiling: Maximum correction multiplier (default 1.15)
Returns:
Tuple of (correction_factor, median_excess)
"""
if not excess_samples:
return 1.0, 0.0
sorted_samples = sorted(excess_samples)
median_excess = sorted_samples[len(sorted_samples) // 2]
if median_excess > 0:
correction = 1.0 / (median_excess ** correction_power)
else:
correction = 1.0
correction = max(dampen_floor, min(boost_ceiling, correction))
return correction, median_excess
def compute_binned_corrections(excess_bins, global_correction,
correction_power=0.5, dampen_floor=0.80,
boost_ceiling=1.15, min_samples=3):
"""
Compute per-phase corrections from binned excess samples.
Bins with fewer than min_samples fall back to the global correction.
Returns:
Dict mapping bin name to correction factor.
"""
bin_corrections = {}
for bin_name, samples in excess_bins.items():
if len(samples) >= min_samples:
correction, _ = compute_adaptive_noise_scale(
samples, 1.0, correction_power, dampen_floor, boost_ceiling
)
bin_corrections[bin_name] = correction
else:
bin_corrections[bin_name] = global_correction
return bin_corrections
def get_phase_correction(sigma_val, bin_corrections, global_correction):
"""Look up the appropriate correction for the current sigma phase."""
if bin_corrections is None:
return global_correction
if sigma_val > 5.0:
return bin_corrections.get('structural', global_correction)
elif sigma_val > 0.5:
return bin_corrections.get('texture', global_correction)
else:
return bin_corrections.get('cleanup', global_correction)
def adaptive_noise_step(denoised_raw, prev_denoised_raw, prev_change_norm,
sigma, sigma_next, excess_samples, adaptive_correction,
base_s_noise, step_idx, total_steps, warmup=5,
binned_enabled=False, excess_bins=None,
adaptive_bin_corrections=None):
"""
Process one step of adaptive noise calibration.
Call after getting raw model output and before noise injection.
Handles warmup collection, calibration trigger, and correction application.
Returns:
(effective_s_noise, change_norm, adaptive_correction, should_restart,
adaptive_bin_corrections)
"""
# Compute prediction change magnitude
if prev_denoised_raw is not None:
change_norm = torch.norm((denoised_raw - prev_denoised_raw).flatten(1), dim=1).mean().item()
else:
change_norm = None
effective_s_noise = base_s_noise
should_restart = False
if sigma_next > 0:
sigma_val = sigma.item() if torch.is_tensor(sigma) else float(sigma)
sigma_next_val = sigma_next.item() if torch.is_tensor(sigma_next) else float(sigma_next)
in_texture_phase = 0.5 < sigma_val < 5.0
if adaptive_correction is not None:
# Post-restart: apply correction
if binned_enabled and adaptive_bin_corrections is not None:
correction = get_phase_correction(sigma_val, adaptive_bin_corrections, adaptive_correction)
effective_s_noise *= correction
else:
effective_s_noise *= adaptive_correction
elif change_norm is not None and prev_change_norm is not None:
change_ratio = change_norm / (prev_change_norm + 1e-8)
sigma_ratio = sigma_next_val / (sigma_val + 1e-8)
excess = change_ratio / (sigma_ratio + 1e-8)
# Bin the excess sample by sigma phase
if binned_enabled and excess_bins is not None:
if sigma_val > 5.0:
excess_bins['structural'].append(excess)
elif sigma_val > 0.5:
excess_bins['texture'].append(excess)
else:
excess_bins['cleanup'].append(excess)
# Texture-phase samples drive the warmup trigger
if in_texture_phase:
excess_samples.append(excess)
if len(excess_samples) >= warmup:
adaptive_correction, median_excess = compute_adaptive_noise_scale(
excess_samples, effective_s_noise
)
if binned_enabled and excess_bins is not None:
adaptive_bin_corrections = compute_binned_corrections(
excess_bins, adaptive_correction
)
bin_info = {k: f"{v:.3f}" for k, v in adaptive_bin_corrections.items()}
print(f" Adaptive Noise calibrated: global={adaptive_correction:.3f}, binned={bin_info}")
else:
print(f" Adaptive Noise calibrated: correction={adaptive_correction:.3f}")
should_restart = True
return effective_s_noise, change_norm, adaptive_correction, should_restart, adaptive_bin_corrections
def get_ancestral_step(sigma, sigma_next, eta=1.):
"""Calculate ancestral step sizes."""
sigma_up = min(sigma_next, eta * (sigma_next ** 2 * (sigma ** 2 - sigma_next ** 2) / sigma ** 2) ** 0.5)
sigma_down = (sigma_next ** 2 - sigma_up ** 2) ** 0.5
return sigma_down, sigma_up
def apply_spectral_modulation_clybius(noise_pred, multiplier=1.0, percentile=5.0):
"""
Clybius Spectral Modulation: Apply frequency-domain corrections to noise prediction.
Based on ComfyUI-Latent-Modifiers. Applied to noise_pred (cond - uncond),
NOT to the denoised latent directly.
Args:
noise_pred: The noise prediction tensor (cond - uncond)
multiplier: Modulation strength (0=none, 1=full Clybius effect). Default: 1.0
percentile: Upper/lower percentile threshold. Default: 5.0
Returns:
Spectrally modulated noise prediction
"""
if multiplier == 0 or percentile <= 0:
return noise_pred
try:
fourier = torch.fft.fft2(noise_pred, dim=(-2, -1))
log_amp = torch.log(torch.sqrt(fourier.real ** 2 + fourier.imag ** 2) + 1e-8)
log_amp_flat = log_amp.abs().flatten(2)
quantile_low = torch.quantile(log_amp_flat, percentile * 0.01, dim=2)
quantile_high = torch.quantile(log_amp_flat, 1 - percentile * 0.01, dim=2)
quantile_low = quantile_low.unsqueeze(-1).unsqueeze(-1).expand(log_amp.shape)
quantile_high = quantile_high.unsqueeze(-1).unsqueeze(-1).expand(log_amp.shape)
mask_low = ((log_amp < quantile_low).float() + 1).clamp_(max=1.5)
mask_high = ((log_amp < quantile_high).float()).clamp_(min=0.5)
filtered_fourier = fourier * ((mask_low * mask_high) ** multiplier)
result = torch.fft.ifft2(filtered_fourier, dim=(-2, -1)).real
return result
except Exception as e:
print(f"⚠️ Spectral modulation failed: {e}")
return noise_pred
def apply_combat_cfg_drift(latent, method='mean', intensity=1.0):
"""
Combat CFG Drift: Reduce mean drift from high CFG values.
Based on ComfyUI-Latent-Modifiers. As CFG increases, the latent mean
can drift away from 0, causing color shifts and other artifacts.
Note: Disable for inpaint/ADetailer passes to prevent patchy composites.
Args:
latent: The latent tensor to correct
method: 'mean' or 'median'. Default: 'mean'
intensity: How much drift to remove (0=none, 1=full). Default: 1.0
Returns:
Drift-corrected latent
"""
if intensity <= 0:
return latent
try:
if method == 'median':
center = latent.view(latent.shape[0], -1).median(dim=-1, keepdim=True)[0]
center = center.view(latent.shape[0], 1, 1, 1)
else:
center = latent.mean(dim=(1, 2, 3), keepdim=True)
return latent - center * intensity
except Exception as e:
print(f"⚠️ Combat CFG drift failed: {e}")
return latent
def apply_cfg_techniques(denoised, settings):
"""
Apply enabled post-hoc CFG techniques to the denoised prediction.
Spectral Modulation is handled separately via the AdeptSpectralModulation
model patch node. This function handles Combat CFG Drift only.
Args:
denoised: The denoised prediction from the model
settings: Dict with 'combat_cfg_drift' (bool) and 'combat_drift_intensity' (float)
Returns:
Enhanced denoised result
"""
if not settings.get('combat_cfg_drift', False):
return denoised
intensity = settings.get('combat_drift_intensity', 0.5)
return apply_combat_cfg_drift(denoised, method='mean', intensity=intensity)
def create_detail_enhanced_model(model, x, sigmas, settings):
"""Creates a model wrapper with detail enhancement."""
if not TORCHVISION_AVAILABLE:
return model
base_strength = settings.get('detail_enhancement_strength', 0.05)
radius = settings.get('detail_separation_radius', 0.5)
total_steps = len(sigmas) - 1
class DetailEnhancer:
def __init__(self):
self.current_step = 0
def __call__(self, x_current, sigma, **kwargs):
denoised = model(x_current, sigma, **kwargs)
try:
low_freq = gaussian_blur(denoised, kernel_size=3, sigma=radius)
high_freq = denoised - low_freq
progress = min(self.current_step / max(total_steps, 1), 1.0)
strength = base_strength * (0.5 + progress)
enhanced = denoised + high_freq * strength
self.current_step += 1
return enhanced
except Exception as e:
print(f"⚠️ Detail enhancement failed: {e}")
return denoised
return DetailEnhancer()