Top blurb claimed smooth areas were left at the normal sigma and that the
node preserves those regions outright. The mask is a soft weighting, so
smooth regions receive an attenuated shift rather than full protection.
The examples section further down already says this; align the blurb.
Triggers on push to master when pyproject.toml changes (so version
bumps publish automatically) plus a manual workflow_dispatch escape
hatch. Uses the official Comfy-Org/publish-node-action with the
REGISTRY_ACCESS_TOKEN repo secret.
Crops a square focal point and a 21:9 strip from the anime strength
sweep so the Registry listing has visible artwork instead of falling
back to the no-image placeholder.
Publish three example workflows (simple, hiresfix, debug) under
example_workflows/ and link them from the README, in preparation for
ComfyUI Manager / Comfy Registry registration. Replace the single
Juggernaut sweep pair with realistic and anime (Illustrious) pairs to
show the wrapper is architecture-level rather than tuned to one style,
and soften the mask-effect wording to reflect that selectivity is a
soft weighting, not a binary gate.
Drops in two SDXL/Juggernaut portrait sweeps showing the coverage and
strength knobs against a stylized prompt. The strength sweep is held
at coverage=0.5 to demonstrate the selective behavior: outlines and
fine detail sharpen while the low-density background stays clean.
Restores the IMAGE socket on the preview node so the rendered mask can
be wired into image grids alongside outputs. Also seeds mask_ref with
an all-ones placeholder when the Debug sampler short-circuits at
strength=0 + coverage>=1, matching the non-zero-strength full-coverage
behavior and avoiding a black gap in mask comparisons.
Post-session audit. Comment drift from the CFG++ reorder:
- Top docstring and _MIN_MASK_ACTIVITY comment implied normal-first
ordering and forward-saving short-circuits; neither is true after the
reorder. Both forwards now run before the mask is available.
- build_sampler's coverage<=0 docstring described a branch that's never
reached (both nodes short-circuit at go() for coverage<=0).
- _mask_delta's coverage<=0 check was dead code with a matching stale
comment claiming it lets the wrapper avoid the second model call.
Also: the debug preview rendered an empty mask on the coverage=1 fast
path because full_coverage skips the mask_fn entirely and never writes
to mask_ref. Populate mask_ref with a ones tensor once at sampler_function
entry so the preview reflects what the wrapper is actually doing.
CFG++ samplers capture uncond_denoised via a post_cfg_function hook and
use it as a separate side-channel for the noise direction. The wrapper's
two forwards both fire the hook, leaving the captured uncond tied to
whichever sigma ran last. Reorder so the original sigma runs last, which
keeps the side-channel aligned with the sigma the sampler uses in its
step formula. For non-CFG++ samplers the order is irrelevant (blend math
is commutative) so this is safe as the single path. Cost: the activity
and first-step short-circuits no longer save a forward.
Reorder alone isn't sufficient because our returned denoised still mixes
x0 across two sigmas while the uncond stays at one, which introduces a
cross-sigma term in the implicit guidance signal. Empirically this
amplifies effective strength ~6-7x at mid-mask on CFG++ samplers.
Detect the hook at run start and attenuate the per-step adjustment by
0.15 when present. Calibration is exact only at mid-mask; full-strength
detail on CFG++ is better served by coverage=1.
Also drops the mask_sharpness experiment. Sharpening mid-mask values
toward hard {0,1} was tried as a fix for the same pathology before we
understood it was a CFG++ side-channel issue; it didn't help and isn't
useful as a standalone knob.
The schedule's active region is now [start, end] with start=0.1, end=0.9 on
the main node. make_schedule enforces a clean step at both head and tail
regardless of the passed values, giving the sampler composition headroom
and room to clean residual noise after the last detail step.
Debug node exposes start and end as sliders. _TAIL_TAPER_START/END collapse
to a single _TAIL_TAPER_WIDTH derived relative to end.
strength is the peak per-step sigma-reduction fraction applied directly,
replacing the intensity/_INTENSITY_REFERENCE/_REFERENCE_PER_STEP_SHIFT
indirection. Breaking change: old intensity=16 maps to strength=0.1.
Add fast paths so degenerate configurations skip work: strength==0 or
coverage<=0 returns the input sampler unwrapped; coverage>=1 saturates the
mask and runs only the adjusted-sigma forward per active step.
The previous README described a variance-based mask and a schedule-curve
parameter set that no longer exist. Replaces it with a walkthrough of the
current delta-mask two-pass design, an explanation of why spatial
selectivity matters (clean backgrounds, bokeh, flat colors breaking under
global sigma modulation), a realistic picture of the 2x inference cost
and its mitigations, and listings of the three current nodes.
Linearly decays intensity across the last ~30% of steps and zeroes the
final ~15%, so those steps skip the second model call via the existing
adjustment==0 fast path. Saves 14-17% wallclock on typical workloads and
softens the over-sharpening that showed up at higher step counts.
Also adds a per-run stats line logging detail vs. skip counts so the
impact of future schedule/threshold tweaks is visible. Includes a
mask-activity floor (currently 0.02) as a secondary skip path; not
tripping in practice but kept as a safety net for degenerate masks.
Splits _PER_STEP_ADJUSTMENT_SCALE into _INTENSITY_REFERENCE and
_REFERENCE_PER_STEP_SHIFT so the calibration anchor (intensity=16) is
explicit at the call site. Adds module/function docstrings and targeted
inline comments explaining the two-pass blend, why the shift is not
divided by step count, the coverage post-EMA ordering, and other
non-obvious invariants.
After the CFG decoupling and step-count normalization fixes the old
default of 2.0 produces very mild output. 16 works across 8/16/32
step runs as a sensible starting point.
The V2 suffix came from earlier iterations that also had V1 variants.
Only one SAMPLER node remains; the version marker is noise.
Breaking change: saved workflows referencing the old key
SelectiveSigmaDetailerDeltaV2Node will need to be re-wired to
SelectiveSigmaDetailerNode.
The preview node wrote a fresh timestamped PNG per run without ever
deleting the previous ones, so the temp directory would accumulate a
file per preview call over a session. Delete prior ssd_mask_*.png
files at the start of each preview so at most one mask image exists
on disk at a time. Keep the timestamped filename so the browser does
not cache a stale image.
ComfyUI caches the SAMPLER object across runs with matching inputs,
so its captured mask_ref dict persists. If a later run skipped the
mask path (short schedule, coverage=0, etc.) the debug preview would
display the prior run's mask. Clear at the top of sampler_function so
stale masks never survive into a new run.
At coverage=0 the mask shift zeroes out the blend factor, so the
detail call's output is discarded anyway. Return None early so the
sampler wrapper skips the second apply_model call entirely.
The patch node hooked set_model_unet_function_wrapper to inject sigma
adjustments, but that wrapper sits below apply_model's input scaling
and timestep transformation. The second forward pass therefore ran
with input prepped for the original sigma while the outer layer's
calculate_denoised also used the original sigma, so the adjustment
produced near-zero visible effect regardless of intensity.
Keep only the SAMPLER variant (used via SamplerCustom), which operates
at the correct layer.
The SAMPLER path multiplied the per-step adjustment by the model's
cfg_scale, which made intensity a moving target across CFG values and
diverged from the patch path's behavior. Drop the cfg_scale factor so
intensity maps directly to sigma shift.
Per-step sigma adjustment was divided by active_count, which
double-counted the step-size normalization the sampler's integration
already applies. Net effect scaled inversely with step count, so short
runs felt much stronger than long runs. Replace the per-run divisor
with a fixed 16-step reference constant, matching the patch variant's
calibration. Removes the now-unused normalize_by_active_steps flag
from build_sampler.
Main SAMPLER and model-patch nodes now return only their primary
output. A new Debug node exposes the hardcoded constants (start, ema,
mask_clip_percentile) plus the mask_ref output; the preview node is
renamed to "Debug Preview" and stripped of its upscale/image outputs.
The existing node outputs a SAMPLER and only works with SamplerCustom.
The new SelectiveSigmaDetailerPatchModelNode takes MODEL in and out,
attaching the mask-and-blend logic via set_model_unet_function_wrapper
so the same behavior works inside any sampler node.
Tradeoffs vs the SAMPLER variant: progress is measured in sigma-space
(1 - sigma/sigma_max) instead of step index since the full schedule is
not visible at the model layer; intensity is calibrated against a
fixed 16-active-step reference instead of the actual active-step
count; the cfg_scale coupling is dropped since it is not cleanly
accessible from the unet wrapper. Behavior should match the SAMPLER
variant closely near 20 steps on standard schedulers.
Parameter search converged on narrow working values across SDXL
workloads, so the previously-exposed knobs now live as hardcoded
constants: schedule start at 0.2 (skip composition-setting phase, min
1 step enforced), mask clip percentile at 0.2, and EMA at 0.9.
Coverage replaces the removed mask_floor/invert_mask experiments. It
is an additive threshold shift applied after EMA and state storage so
it does not compound across steps: 0 zeroes the mask, 0.5 is the raw
delta mask, 1.0 saturates to full. The `end` knob is also removed
since 1.0 was always the natural value.
Cosine ramping existed to dampen sigma modification at the start/end
of the active window, a defensive measure against composition drift.
With the delta mask providing spatial selectivity derived from the
emerging composition, this temporal dampening is redundant.
Schedule is now a flat rectangle of detail_amount across [start,
end]. Defaults updated to observed working values: detail_amount=2.0,
start=0.0, end=1.0, ema=0.75, mask_clip_percentile=0.2.
Remove v1 delta node, redundant schedule parameters (bias, exponent,
smooth, start/end_offset, fade), cfg_scale_override, and the mask
blur/threshold/gamma post-processing. Defaults bake in the observed-
static values: cosine curve with bias=0.5, exponent=1.0, smooth=on,
offsets/fade at 0.
Node surface is now sampler + detail_amount + start + end + ema +
mask_clip_percentile. Renames postprocess_mask to normalize_mask
since only the percentile-clipped stretch remains.
Percentile-clipped min/max normalization on the delta mask replaces
min/max and rank normalization in the V2 node. Default 0.1; 2%
proved too mild, 10% is where the knob starts producing visible
change. A median-filter experiment was tried and reverted after the
resulting smoother mask produced no meaningful image difference.
Remove variance-snapshot, dynamic-variance, and edges experimental
nodes along with their unused helpers. Keep the delta node as v1.
Add SelectiveSigmaDetailerDeltaV2Node: divides the per-step sigma
adjustment by the count of active schedule steps, and rank-normalizes
the delta mask so detail_amount has direct leverage regardless of
mask distribution shape. Detail_amount cap raised to 100 to allow
for the larger values v2 requires.
Extract shared sampler-wrapping and mask postprocessing into ssd_core.py.
Add three experimental variants alongside the original (variance snapshot):
dynamic variance with EMA, delta (inter-step prediction difference) with
EMA, and edges (Sobel). Each sampler node now also emits a mask handle
consumed by a new mask-preview node that renders the captured mask inline
after sampling runs.
Use reflect-padding in variance, Sobel, and blur ops to eliminate the
border-aura artifact produced by zero-padded convolutions.