A standing request asked for a SamplerCustomAdvanced version of the node, so a
custom guider and scheduler could be used. The scheduler half already shipped:
custom_sigmas takes a SIGMAS input and the schedule is sampled, not just
counted. The gap was the guider, and it is the half that matters -- a guider is
the only way to reach BasicGuider with no negative and no CFG, DualCFGGuider,
or anything another pack provides.
This does it with a node that outputs NOISE rather than a second sampler.
SamplerCustomAdvanced consumes one, and so does AddNoise.
A second sampler node was the obvious shape and would have worked: the segment
loop already routes through CFGGuider.sample, because comfy.sample.sample
builds a guider and calls it, so accepting one would have been a substitution
rather than a new mechanism. It was not worth it. A NOISE object is forty lines
against a second wall of shader widgets, five frontend files that name nodes
explicitly, and a sampling seam through the pipeline -- and it composes with
everything in the custom sampling family instead of only with itself.
What it covers is the node's default configuration exactly: one shader stage,
painted into the noise a run starts from. That is pinned bit for bit against
the Direct node across eps, flow and AV latents, presets, batch_index and
progressions, so a seed means the same thing on both. What it cannot do is
stages, because a NOISE object is asked for noise once, before any sampling --
the sampler's stages re-enter the shader at segment boundaries partway through.
The node says so, and so do the README and the tooltips.
Two pieces were lifted out rather than copied: build_shader_params, next to the
reader that owns the params dict, and _paint/starting_noise in the pipeline,
where the painting loop was already written twice. The Direct node is sixty-odd
lines shorter for it. Both extractions were checked against the golden suite
before anything was built on them.
The equivalence test earned its keep immediately. Presets set stage_progression,
and stage_progression shapes a single stage -- it sits at the start of the
trajectory and is drawn from there -- so ignoring it made preset: roam mean two
different things on the two nodes. It is now an input here, with its own wording
because a ramp across stages is not what it does with one. That also showed the
Direct node's tooltip has always been wrong to say it needs more than one stage
to do anything; the tooltip is corrected and the sampling is untouched, which is
how roam and video have always behaved.
Also dropped a refusal check copied into starting_noise: unlike run, there is no
sampling to protect there, and the generator raises the same error one call
down. And _STREAM_SEED_STRIDE turned out to have nothing pinning it anywhere --
the nested golden case paints only the spatial stream, so zeroing the offset
changed nothing in the suite. It has a test now, feeding two identical streams
so the inputs cannot explain the difference.
Verified on H3 through BasicGuider, KSamplerSelect and BasicScheduler into
SamplerCustomAdvanced: seven steps, coherent output.
example_workflows/MiniMaxH3_CustomSampling_SNK_Source.json is that run, and
needs core nodes and this pack and nothing else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MiniMax H3 fixes faces with a latent upscale partway through the run, which
means splitting the generation: some steps, the upscaler, then the rest. The
sampler could only ever run a schedule end to end, so getting the upscaler in
meant dropping shader noise out of the workflow entirely.
Four inputs, the same ones KSampler (Advanced) has: add_noise, start_at_step,
end_at_step and return_with_leftover_noise. They land at the tail of the
optional block, because ComfyUI maps saved widget values by position.
The machinery was already there. The pipeline builds one sigma schedule and
samples it in segments, slicing sigmas[start:end + 1] for each; a window is
that same slicing applied once more at the outer edges. Indices stay absolute,
so the per-segment seed still comes out as seed + start -- which means a split
run draws the same ancestral and SDE noise as the unsplit one it came from.
Decisions worth naming:
- Stages spread across the steps the node actually samples, not the whole
schedule. Two stages over a three-step window are two stages in those three
steps; spread over the schedule, one would land outside the window and never
fire. merge_boundaries takes the window's own edges for this, and its first
parameter is renamed end_step to say so -- measuring the tail against the
schedule length instead would leave a 1-step tail inside a long window.
- stage_progression keeps measuring position in the whole trajectory, so a node
running the last three steps of seven gets the fine end of coarse_to_fine
rather than a fresh coarse-to-fine sweep of its own.
- With add_noise off the opening boundary is not painted. The latent already
carries its noise from whatever ran before, and the shader on a zero tensor
would add back exactly what was turned off. Later boundaries still paint,
since their noise is recovered from the latent rather than made here. The
refusal for an unpaintable latent weighs only the stages that will actually
be painted, so it no longer rejects a latent on behalf of noise nobody makes.
Two things fixed on the way:
- force_full_denoise was dead. KSampler.sample only reads it alongside
last_step, and this code passes sigmas= with last_step unset, so it was
discarded on every call. The window's zeroed sigma tail is what does that job
now. The golden fixtures recorded the flag, so they are re-captured; every
sigma, noise tensor, seed and output across all thirteen is unchanged, which
was checked before blessing them.
- The progress bar counted the schedule rather than the steps being run.
ProgressBar takes its total from the callback, so a windowed run would have
started part-filled and stopped short of the end. At the defaults the numbers
are identical to before.
A schedule with fewer than two sigmas now returns the latent instead of being
handed to the sampler, which is what denoise 0.0 builds.
Verified against comfy.samplers.KSampler itself across seven start/end/leftover
combinations, requiring the sigmas to match bit for bit -- the parity that lets
this node sit on one side of a split and a stock KSampler (Advanced) on the
other. Two golden fixtures cover the two halves. Also run for real on H3:
4 steps, latent upscale, 3 steps, 896x896x124 with audio, in the new
example_workflows/MiniMaxH3_Split_Upscale_SNK_Direct.json.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Fix absolute imports in shaders/ to use relative paths
- Correct indentation in shader files
- Refactor video_comparer.py and nodes/video_comparer.py
- Add new example workflows
- Update gitignore and package.json