291 Commits
Author SHA1 Message Date
Æmotion StudioandClaude Opus 5.5 8cf5f3f7cf docs: release 2.2.1
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 07:22:19 -07:00
Æmotion StudioandClaude Opus 5.5 7bed0e37f8 chore: stop tracking CODE_REVIEW.md
It was a working review, not documentation. The two comments that pointed at it
now point at the changelog's 2.0.0 section, which lists the same fixes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 07:20:41 -07:00
Æmotion StudioandClaude Opus 5.5 38773801e5 docs: release 2.2.0
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 07:17:52 -07:00
Æmotion StudioandClaude Opus 5.5 b5186937c5 chore: stop tracking HANDOFF.md
It is a working notebook, not documentation. The changelog no longer points
readers at it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 07:13:30 -07:00
Æmotion StudioandClaude Opus 5.5 dd8b599fd2 feat: measure what the shader does to H3's audio
drive.py saves H3's decoded audio as FLAC, takes --prompt to pick material with a
percussive, tonal or broadband sound, and --shade-non-spatial to paint the audio
stream too. analyze_audio.py reads each run against its own prompt and seed's
strength-0 control and separates what painting the audio latent did from what
reached the sound through the picture alone. Prompt and audio painting now reach
run names, so a rerun under either does not skip a run it already holds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 07:13:19 -07:00
Æmotion StudioandClaude Opus 5.5 18a909b88b docs: bring the README and changelog up to what has shipped
The README still described the deprecated node's per-stage inputs and never
mentioned Shader Noise Walk, stage_progression or shade_non_spatial. The
changelog was missing the live previews, the H3 multi-boundary crash fix, the
metadata-stream skip and the dead noise code removal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 07:12:03 -07:00
Æmotion StudioandClaude Opus 5 ee761fc510 fix: paint each noise stream where it already lives
_paint moved only the paintable streams onto a device taken from the latent
the run started with, but the noise at an interior boundary is recovered from
a segment result on the sampler's device. On MiniMax H3's nested AV latent
that left the video stream on the CPU and the audio stream on CUDA, and
pack_latents refused the pair, so injection_stages and sequential_stages >= 2
crashed on every AV run. Single-stream latents only paid for a needless round
trip, which is why nothing caught it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 18:45:39 -07:00
Æmotion StudioandClaude Fable 5.1 3cb19036d8 docs: what each shader type does on Krea 2, and what each costs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 02:14:01 -07:00
Æmotion StudioandClaude Fable 5.1 266b116fc5 feat: a krea2 target for the blend measurements, one shader type per run
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 02:12:37 -07:00
Æmotion StudioandClaude Fable 5.1 2d0d1e7b7b chore: delete the noise helpers nothing calls
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:50:58 -07:00
Æmotion StudioandClaude Fable 5.1 5c849d6698 feat: live previews for every shader type
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:49:12 -07:00
Æmotion StudioandClaude Fable 5.1 cf42fc020d docs: every shader type is free
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:48:47 -07:00
Æmotion StudioandClaude Fable 5.1 41b7476e70 feat: waves shader type, seeded plane waves summed
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:43:19 -07:00
Æmotion StudioandClaude Fable 5.1 bde7e734aa feat: cellular shader type, Worley cells in four patterns
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:41:52 -07:00
Æmotion StudioandClaude Fable 5.1 3c33550a17 feat: projection_3d shader type, a plane through a 3D field
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:39:06 -07:00
Æmotion StudioandClaude Fable 5.1 c487613832 feat: interference shader type, crossing fringes from two fields
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:37:03 -07:00
Æmotion StudioandClaude Fable 5.1 8da286909a feat: heterogeneous_fbm shader type, detail that varies across the frame
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:31:02 -07:00
Æmotion StudioandClaude Fable 5.1 01d070a856 feat: perlin shader type, classic gradient noise
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:30:38 -07:00
Æmotion StudioandClaude Fable 5.1 2cf55e3a84 feat: fractal shader type, the reference FBM
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:25:36 -07:00
Æmotion StudioandClaude Fable 5.1 f666d4e908 feat: gaussian shader type, white noise as the control
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:23:31 -07:00
Æmotion StudioandClaude Fable 5.1 5c28c42981 fix: let every registered shader name through the parameter whitelist
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:21:37 -07:00
Æmotion StudioandClaude Fable 5.1 7eb7ae3594 refactor: share the four-corner 3D simplex, a lattice hash and an FBM skeleton
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:19:47 -07:00
Æmotion StudioandClaude Fable 5.1 b88adba4a7 test: pin temporal_coherent's output before its 3D simplex moves
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 01:17:33 -07:00
Æmotion StudioandClaude Opus 5 0d7c834d33 feat: shader noise as a NOISE object, for custom sampling
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>
2026-09-22 00:29:09 -07:00
Æmotion StudioandClaude Opus 5 2fed935313 feat: sample part of a schedule, so a run can be split
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>
2026-09-22 00:27:18 -07:00
Æmotion Studio 82f08f5949 docs: record what the measurements suggest next
The interpretations of the blend measurements, marked by how well each is
supported, and what to try for each. None of it is started.

Also ignores *.local.md, so a personal reference sheet can sit beside the
handoff without being tracked.
2026-09-22 00:24:32 -07:00
Æmotion StudioandClaude Opus 5 33a57ae7b2 fix: draw a clip's first frame from the same field as the rest
domain_warp chose between a 2D field and a 3D one with time as the third axis
by testing `time != 0`. core.shader_noise ramps time from 0 to 1 across a
clip, so frame 0 is the only frame whose time is exactly 0, and it alone took
the 2D path. Every video drawn with the default shader had a first frame that
came from a different function than the five, or thirty-six, after it.

With temporal coherence on, adjacent frames correlate at about 0.95. Frame 0
against frame 1 measured -0.01, which is what uncorrelated looks like.
Nudging the base time to 0.001 made it +0.95, which is what confirmed the
cause.

The choice is now made once per draw, from the latent's frame count, and
passed down as `time_axis`. A single image still takes the 2D path -- frames
== 1 -- so no image fixture moved. In a video draw only frame 0 changes;
frames 1 onward are byte-identical, checked. video_nested was re-captured.

Where `time_axis` is absent, because something called the generator directly,
the old test still applies, so nothing outside this pipeline changes
behaviour.

Also corrects the CHANGELOG's claim that domain_warp shows no frame-to-frame
correlation under temporal coherence. That number came from measuring the one
frame pair that straddled this bug. It is the most coherent of the five, not
the least: 0.96 against curl_noise's 0.96, tensor_field's 0.80, spectral's
0.68 and temporal_coherent's 0.55.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:34:14 -07:00
Æmotion StudioandClaude Opus 5 298e8bdc72 perf: batch tensor_field's channel axis too
The last generator still drew one channel at a time, and it drew them
differently from the others: each channel takes one of four visualisations,
its own scale, warp and time, and its own random perturbation of the
coordinate grid. The expensive part -- five simplex evaluations inside
compute_tensor_properties -- does not depend on the visualisation, so it now
runs once for the whole channel axis and each channel takes the cheap
visualisation its index asks for. Computing all four and selecting is less
work than grouping the channels and making four calls.

At 1344x768/124 frames, measured against the pre-batched-noise tag in one
sitting: 5.37s -> 2.08s. That completes the set -- domain_warp 6.76 -> 1.90,
curl_noise 6.54 -> 1.94, temporal_coherent 5.76 -> 1.66 -- with effective
channel rank unchanged in all four.

Two things this cost, both recorded in tests:

- Its fixtures moved. Exact at vector-aligned shapes, up to 1.3e-05 at
  others. The amplification is larger than domain_warp's for a reason worth
  knowing: tensor_field perturbs the coordinates per channel, and an ulp of
  coordinate can push a point across a simplex cell boundary and flip a
  gradient, so a 1e-07 input difference is not a 1e-07 output difference.
  It was 5.6e-04 until the per-channel `time` was held in float64 and cast at
  the point of use, which is where the scalar path's Python float was rounded.
  Rounding it any earlier moves the coordinate.
- Channel 0 must stay on the scalar path. Batching it along with the rest
  broke the identity between channel 0 of a wide draw and a one-channel draw
  at 22x38, by 6e-07 -- small, but that identity is what every travel-mode
  basis rests on, and jump and stamp are pinned byte-identical to
  pre-collapse-fix through it. The other three generators get this for free by
  handing channel 0 to fill_channels as `base`; tensor_field does not use
  fill_channels, so it needed saying explicitly.

The new test that found the second one now runs all four generators across
nine latent shapes, ragged and aligned, so the invariant is checked where it
actually breaks rather than only where it holds. 32 jump/drift draws remain
byte-identical to the tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:06:33 -07:00
Æmotion StudioandClaude Opus 5 2c7047a0e3 perf: draw the channel axis in one call, not one per channel
At MiniMax H3's default latent the draw performed 37 frames x 24 channels =
888 separate renders of 4032 pixels each, and cProfile put the time in per-op
dispatch rather than arithmetic -- a 4032-element torch.floor was costing
0.3ms. Generators now offer fill_channels a batched draw and it takes it
whenever more than one extra channel is wanted.

At 1344x768/124 frames: domain_warp 5.31s -> 1.97s, temporal_coherent
6.52s -> 1.86s, curl_noise -> 2.11s.

curl_noise and temporal_coherent are byte-identical; their fixtures did not
move. domain_warp is not, and the reason is worth recording. It turns its
coordinates by an angle drawn from the seed, so unlike the others the
coordinates genuinely differ per channel and the batched draw has to
materialise them. Every downstream elementwise op then runs over N times as
many elements, and where the per-slice count does not suit the vector width
the tail is handled differently. Five fixtures moved by 1.2e-07 to 2.4e-07 --
float32 rounding, with effective channel rank identical to three decimals --
and the previous behaviour is tagged pre-batched-noise. Exact at 22x38, 48x84
and 64x64; not at 22x39, 23x38, 37x53. tests/test_simplex.py pins both sides
of that boundary so it cannot quietly get worse.

jump and stamp are unaffected by construction: fill_channels takes the
batched path only for more than one channel, and both travel modes build from
one-channel draws. 32 draws across four generators and four latent shapes
were checked byte-identical against the tag.

Also folded in, because the batching needed it:

- One simplex primitive instead of seven. The 2D hash was duplicated across
  three generators and the 3D across four, with real differences hidden
  between them -- three of the 3D copies sum a single corner rather than
  four, and one of those computed three more corner hashes and threw them
  away. shaders/simplex.py names them for what they do and keeps the
  differences rather than unifying them. temporal_coherent's four-corner
  version stays with that generator: it picks corners by the real simplex
  ordering and reads gradients from a table, so it is a different function
  and not a parameterisation of the others. I had written it as one, guessed
  wrong, and the equivalence check caught it.
- The accumulators in get_velocity_field, fbm_noise and temporal_spectral_noise
  no longer preallocate a fixed [B,H,W,C] and write into it in place, so the
  same code serves a shared coordinate grid and a per-channel one. The adds
  happen in the same order, so the values are unchanged.
- tensor_field draws its shape mask once instead of once per channel -- 127
  identical masks at LTXV's 128 channels -- and stops cloning the coordinate
  grid per channel. Byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 18:54:02 -07:00
Æmotion StudioandClaude Opus 5 40882604ee feat: a shader type that builds its field from a frequency band
The other four generators evaluate a procedural field per pixel per channel,
so a wide latent costs one render per channel and a long video multiplies
that by the frame count. `spectral` draws the whole channel stack's Fourier
coefficients at once, shapes their amplitude by a radial envelope and takes
one inverse real FFT. Against domain_warp on the same latents: 0.02s against
1.16s at H3's 608x352/56 frames, 0.13s against 5.31s at 1344x768/124 frames,
0.01s against 0.61s at LTXV's 128 channels -- where it also spans more of
them, 115 of 128 against 84.

It is a different instrument, not a faster domain_warp. A shaped-Gaussian
field is a cloud: no filaments, no swirls, and colour schemes mean nothing to
it because there is no vector field to map onto a palette. What it has
instead is direct control over the one property the model actually reads.
HANDOFF's measurements found the model settles composition from the noise's
large-scale structure and that the shader's effect is mostly large-scale
structure; the video-diffusion literature arrives at the same place from the
other side. Here that band is a parameter rather than a side effect:
noise_scale sets it, octaves sets the roll-off, warp_strength the anisotropy.

Temporal coherence falls out for free. Holding the seed and advancing time
turns each mode at its own rate instead of redrawing the field, measured as
0.27 frame-to-frame correlation against temporal_coherent's 0.26 -- and
domain_warp's 0.00, which is worth knowing on its own.

Two things that are not obvious and are commented where they live:

- The envelope is a soft shelf, `(1 + (|k|/corner)^2)^(-beta/2)`, not a
  `|k|^-beta` power law. A power law has a pole at DC, so the zero-frequency
  coefficient swamps everything and every channel comes out a flat wash.
- Bandwidth and channel rank are the same dial. Every channel is an
  independent draw but they all live in whatever subspace the envelope leaves
  open, so a band narrow enough to look good can hand the sampler a draw
  spanning far fewer directions than it has channels -- the exact failure the
  channel-rank work was about. `_corner_floor` keeps the band wide enough to
  carry CHANNEL_BASIS directions. It takes the grid size and *not* the
  requested channel count: an envelope that varied with target_channels would
  draw channel 0 of a wide request differently from a one-channel request at
  the same seed, and every travel-mode basis is built from one-channel draws.

Each channel is seeded separately, the way shaders/base.py seeds its extra
channels. Drawing the whole stack from one generator looks equivalent and is
not: torch's CPU normal fill works in blocks of 16, so the first channel of a
wide draw only matches a one-channel draw when the per-channel element count
is a multiple of 16. It was for the coefficients and was not for the phase
drift rates, which made channel 0 agree on frame 0 and diverge after it.

Two golden fixtures, one image and one video, and tests/test_spectral.py for
the properties the fixtures cannot express -- that the field is large-scale
rather than white, that noise_scale moves the band, that the rank holds at
five latent shapes, and that it stays far cheaper than drawing per channel.
Also added to ALL_SHADERS, so it runs the whole generator battery.

Its strengths are not calibrated against real prompts the way the other four
are, and the tooltip says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 18:23:19 -07:00
Æmotion StudioandClaude Opus 5 ee7c7bc31d perf: stop rendering the draw a collapse throws away
None of this changes what a seed produces. All eleven golden fixtures are
byte-identical and the suite passes untouched; that was the acceptance
criterion for every change here.

`travel_mode: jump` -- and the `jump` and `stamp` presets -- rebuilds every
channel from one-channel draws and never reads the wide draw, but the wide
draw was rendered first anyway and dropped on the next line. The guard that
discards it turns only on values known before rendering, so it moves up into
generate(). Measured on domain_warp at MiniMax H3's latent: 5.99s to 0.22s
at 1344x768/124 frames, 1.44s to 0.05s at 608x352/56 frames. `drift` is
unaffected -- it needs the wide draw to measure its rank, so it still pays.

The draw now runs under torch.inference_mode(), worth about a tenth of it,
and clones on the way out. That clone is not optional: an inference tensor
raises "Inference tensors cannot be saved for backward" the moment it
reaches a grad-recording region, and this noise is handed to whatever the
workflow does next.

domain_warp, curl_noise and temporal_coherent stopped reseeding the global
RNG. All three are coordinate hashes of their seed argument, so the calls
changed nothing -- verified by neutering torch.manual_seed and re-drawing --
and domain_warp made one per channel render, 888 times per draw at H3's
default latent, each reseeding every CUDA device as well as the CPU.
tensor_field keeps its reseed: it draws torch.randn_like inside its channel
loop and that value reaches its output.

Also here, because the batching work needs it:

- shaders/simplex.py holds one copy of the 2D primitive, which was duplicated
  across three generators, and takes a tensor of seeds as well as an int so a
  whole channel axis can be drawn in one call. domain_warp delegates to it,
  bit-identical and at performance parity. The variants are kept rather than
  unified -- domain_warp rotates its coordinates by the seed and the others do
  not -- because collapsing them would silently change three generators.
- tests/test_simplex.py pins how far batching stays exact. Without the
  rotation it is exact everywhere. With it, the coordinates differ per slice
  and have to be materialised, every downstream op then runs over N times as
  many elements, and where the per-slice count does not suit the vector width
  the tail differs by 3e-08 to 1.2e-07. So batching domain_warp will move the
  fixtures, and batching the other three will not.
- verification/benchmark_draw.py, because none of the above was reproducible:
  the only per-draw timing in the repo predated the per-channel fill and
  understated the cost roughly tenfold. It refuses to let you raise the thread
  count quietly -- 8 threads on this 4-core box is 2.5x slower than 4.
- SNK_TEST_CPU=1 pins the suite to the CPU, so it runs while a ComfyUI server
  on the same box is holding the GPU.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 18:08:46 -07:00
Æmotion StudioandClaude Opus 5 049b34d60b docs: record the blend-statistics fix in the handoff
The near-end section described the rescale in mix_noise as an open question. It now
records the fix and its before/after measurements on SD 1.5 and MiniMax H3: SD 1.5's
first step away from strength 0 falls from 0.32 to 0.03 image towns, H3's from 0.48
to 0.38, with the reason H3 keeps a step. Tables measured before the fix are marked
as such, and the analysis cache bug that briefly corrupted the H3 comparison is noted.

Left to do: the changelog item is done; the legacy migration item notes that the
sampling_mode tooltip still claims seeds reproduce; and a new item records that the
golden suite never injects shader noise at a stage boundary, because its stub sampler
never calls the progress callback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 16:07:12 -07:00
Æmotion StudioandClaude Opus 5 c13e4dc030 docs: changelog and README for the unreleased blend work
The changelog had no entry for anything since 2.1.0, and its 2.1.0 section still
described decorrelate_channels, which travel_mode replaced before it shipped. It now
has an Unreleased section: the generators filling every channel, blending that keeps
the base noise's statistics, normalize_strength on by default, travel modes and
presets, presets writing the widgets, the drift fix and the measurement tools. It
opens by saying what a user needs first: any strength above 0 now renders
differently, in both sampling modes. A known-issues entry says plainly that legacy no
longer reproduces every earlier seed.

The README gets the same news as a recent update, turns the park-the-seed metaphor
into what the node actually does with a seed, strength and the other settings, adds
presets, travel modes and normalize_strength to the features, usage and
configuration table, and corrects the three places that promised legacy reproduces
pre-2.0 seeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 16:07:12 -07:00
Æmotion StudioandClaude Opus 5 839ca1f7f1 fix: cache video frames per manifest in the blend analysis
Frames extracted from videos were cached by run name, in one folder per model.
Runs in different manifests share names, so analysing a second manifest silently
reused the first one's frames: the first analysis of the blend-statistics fix on
H3 compared the old renders' frames for most runs. Latent measurements were
unaffected. Each manifest now gets its own frame folder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 16:05:21 -07:00
Æmotion StudioandClaude Opus 5 f44aeb5c50 fix: keep the base noise's own statistics when blending
mix_noise rescaled its result to mean 0 and deviation 1 per channel. The base
noise ComfyUI generates is only approximately that, so the moment strength left 0
the starting noise moved by 1 to 3 per cent before the shader contributed anything:
at strength 0.001 that rescale was the whole change. Strength 0 returns the base
untouched, so the road away from the seed's own image started with a jump.

The blend now takes the base noise's own per-channel mean and deviation. Strength 0
is still bit-identical, and as strength approaches 0 the result approaches the base.
Measured on real renders, from strength 0 to 0.001 the image now moves 0.03 of the
typical distance between two seeds' images on SD 1.5, where it moved 0.32. On
MiniMax H3 it moves 0.38, where it moved 0.48; the rest is H3 responding to a 0.06
per cent change in its noise. At 0.05, SD 1.5 lands 0.28 of that distance from the
seed's image instead of 0.41. From 0.25 up SD 1.5 is unchanged, and H3 sits nearer
its seed's image across most of the range.

Two tests pin it: a vanishingly small strength returns the base for every blend
mode, and a base with non-unit statistics keeps them. The golden fixtures are
re-captured, since every non-zero-strength case changes by design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 16:05:21 -07:00
Æmotion StudioandClaude Opus 5 eb1dcd9d59 docs: record how the shader blends, across the whole strength range
The handoff judged the shader by ceilings: how much of it a picture can hide. The
project's intent is a real blend of the seed's noise and shader noise, explored from
a fixed seed, so the handoff now measures that, on SD 1.5 and MiniMax H3.

The shader is in the result on both, far more directly on SD 1.5 (field correlation
0.28 to 0.75 over strengths 0.25 to 1.0) than on H3 (0.05 to 0.26). Results stay
nearest their own seed, and phase_shift and noise_scale are real levers whose effect
traces back to the field. jump imprints its field more strongly than walk on both;
a first pass reported otherwise because of a capture bug, now corrected.

The near end turns up a pipeline discontinuity. Above strength 0, mix_noise rescales
the base noise to mean 0 and deviation 1 before blending, and at 0.001 that rescale
alone moves the image as far as a whole 0.05 step (0.32 image towns on SD 1.5, 0.48
on H3) with no trace of the shader. Past that the road moves in seed-dependent hops.
Recorded with a proposed fix, not applied.

Also documents how to rerun the measurements from verification/blend/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 15:32:22 -07:00
Æmotion StudioandClaude Opus 5 2f3592315e feat: add tools that measure how the shader blends into a model's output
verification/blend/ answers the question the node exists for: with the seed held
fixed, is the shader really in the result, how far do its settings take you, and are
you still in the seed's neighbourhood.

drive.py runs a matrix of seeds, strengths, travel modes and street variants
(phase_shift, noise_scale) through a running ComfyUI server, for SD 1.5 or MiniMax
H3, and records where each latent and image or video landed. analyze.py reads those
manifests and reports distances in towns -- the distance between two seeds'
strength-0 results -- and the step between neighbouring strengths, plus the
correlation between each latent change and the shader field that caused it, against
other seeds' fields as chance. field_capture.py reconstructs that field by running the
Direct node with a stub sampler and no weights; it matches the CUDA render to about
3e-6.

Two traps found while building it, both handled. A travel mode that remixes calls
shader_noise.generate again for its one-channel basis draws, so only the outermost
call is the field the sampler received; recording an inner one gave wrong jump
numbers in a first pass. And runs recorded twice are compared, which showed the
server's output is bit-identical across sessions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 15:32:22 -07:00
Æmotion StudioandClaude Opus 5 6685fccddd docs: describe what the shader does at each strength, not how much to avoid
The tooltips judged the shader by how much of it the picture could hide: "start
low and climb", "roam: as far as the picture reliably holds", modes ranked by how
much strength they "tolerate". That is not what the node is for. The point is a
real blend of the seed's noise and shader noise, with a fixed seed as the place
you explore from, and a shader that takes over the picture is a destination, not
a failure.

shader_strength now reads as distance from the seed, with what was observed
along the way on SD 1.5 and MiniMax H3, and says the pace belongs to the model,
the seed and noise_scale rather than to the number. noise_scale says what
measurement showed: it changes how the shader shows as much as strength does.
blend_mode describes the share of shader each mode hands over instead of ranking
them. shader_type, shape_type, use_temporal_coherence, shade_non_spatial and the
preset descriptions lose their warning framing but keep their measured facts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 15:07:59 -07:00
Æmotion StudioandClaude Opus 5 4dc9cb71f4 docs: record the upgrade and what real weights showed
The handoff still said the agreed upgrade was decided and not started, that
normalize_strength ships off, and that rank topped out near 60% because the basis
draws are not independent. All three are now false.

That section is rewritten as done: what the plan got wrong (three collapse
sources, seven goldens rather than one, the 60% diagnosis, drift already broken
for the wide generators), what was built, and what is left.

It also records the before/after measurements on real weights, and what each one
rests on. SD 1.5 gained modestly, a ceiling of about 0.30 becoming 0.35-0.45, and
the distance between two prompts at the same strength backs that with a number.
On MiniMax H3 under a real prompt, domain_warp's ceiling moved from about 0.5 to
about 0.75 and temporal_coherent's did not move; that rests on judging one middle
frame per strength, and no metric separates the versions. Finding (1)'s ceilings
are marked as coming from a zero-conditioning sweep and running optimistic, and
audio level is recorded as an unreliable ceiling signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:04:27 -07:00
Æmotion StudioandClaude Opus 5 f91c1e4954 feat: choosing a preset writes the widgets it controls
A preset was applied at execution time, so the panel lied: select explore and
shader_strength kept showing its old value while the run used 0.30.

web/src/preset_widgets.ts wraps the preset widget's callback and writes the
bundle into the widgets it names, running each widget's own callback so the
shader preview follows. Editing one of those widgets to a value the preset does
not hold drops the preset back to custom -- otherwise the panel would just lie
the other way. The Walk node keeps the parameter it ramps, and a saved workflow is
never rewritten on load.

The table comes from GET /shader_noise_ksampler/presets rather than a copy in
TypeScript, because preset values get recalibrated and a copy would drift. The
Python override stays the authority: API submissions and the Walk node never run
the JS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:04:01 -07:00
Æmotion StudioandClaude Opus 5 883ae7097c feat: turn normalize_strength on by default
One strength value meant eight different things depending on blend mode: at 0.5,
normal hands the sampler 0.71 of the shader and difference 0.03. The calibration
that fixes this shipped off, so nobody got it unless a preset turned it on. It is
now on. multiply, the default blend mode, is the calibration reference and is
unchanged, so a workflow that never left the defaults samples exactly as before.

The shader_strength, shader_type and travel_mode tooltips are rewritten from what
was measured once the generators filled their own channels. Two of them still
described decorrelate_channels, an input replaced before it shipped. Under real
prompts, domain_warp holds to about 0.75 on MiniMax H3 and to about 0.35-0.45 on
SD 1.5 at 512x512; temporal_coherent gives way past about 0.5 on H3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:03:30 -07:00
Æmotion StudioandClaude Opus 5 1a4b3a8808 fix: fill every latent channel at the source, not downstream
The generators handed back noise spanning about one channel however many the
latent had, and travel_mode walk worked around it by remixing 64 renders after
the fact. The plan was to fix shaders/base.py::expand_channels. That alone would
have changed nothing for SD 1.5 or SDXL: the collapse had three sources, and the
one that mattered at four channels was somewhere else.

- domain_warp copied one field four times with repeat(1, 4, 1, 1), so
  expand_channels returned early at four channels and never ran.
- temporal_coherent broadcast one field to every channel with .expand(): rank
  1.00 at any channel count, and it is the shader the video preset picks.
- curl_noise padded its colour path with copies of one magnitude field.

BaseNoiseGenerator.fill_channels replaces expand_channels. Every channel up to
CHANNEL_BASIS (64) is its own render at seed + 6151*c; channels past that are
QR-orthogonalised mixtures of those renders. Channel 0 stays the generator's own
draw, so a one-channel request is unchanged -- and jump, which is built from
one-channel draws, is byte-identical to before for all four generators. The
extra renders run inside a forked RNG. 6151 has no collisions among the first 64
channels under the mod-10000 seed hashing curl_noise does internally.

Effective rank for domain_warp at 4 / 24 / 128 channels goes from
1.00 / 2.13 / 2.43 to 3.91 / 22.72 / 69.56.

The travel-mode guard had to become direction-aware. Only a basis of 1 skipped
it, so once the generators were wide, drift (basis 4) would have been treated as
a remix that could not help and silently returned walk's noise. It already did
that for tensor_field and curl_noise. A basis below the widest now narrows
unconditionally. Widening is skipped once a draw exceeds 0.65 of its target,
just above the best a random remix reached (0.52-0.64, measured across all four
generators at 16, 24 and 128 channels), so walk passes the generator's noise
through instead of rendering 64 more draws and discarding them. The old 0.9
threshold would still have paid for that on 16-channel models.

Seven of the eleven goldens moved, not the one the handoff predicted; the four
that should not move held byte-identical. The suite is now pinned to the
standard pipeline, re-captured, with every standard-only input set explicitly in
NODE_DEFAULTS. The old legacy output is at the pre-collapse-fix tag.
test_legacy_golden.py becomes test_golden.py.

test_blend_calibration_is_current no longer compares difference on an absolute
tolerance: its whole curve sits below the tolerance, and after this change it
passed by 0.002. Every other mode stays within 0.049 of the table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:02:59 -07:00
Æmotion StudioandClaude Opus 5 d1a90cfc0a docs: correct handoff claims invalidated by travel modes
The travel_mode change made several statements in the handoff false, including
one that mattered.

"Every new capability is an optional input defaulting to off, so no saved
workflow changes behaviour unless asked" is no longer true: travel_mode defaults
to walk, so the standard pipeline decorrelates unless told otherwise. That is
deliberate and safe -- legacy never reaches this code, so the golden suite is
untouched -- but a reader trusting that line would have concluded nothing had
changed by default, which is the opposite of the case.

Also renames decorrelate_channels throughout, since the input no longer exists,
and restates the two ceilings tables in the terms that replaced it: the "off"
numbers are the noise the generators natively produce, the "on" numbers are
travel_mode walk.

Step 2 of the planned upgrade is marked half done. Widening is already the
default; normalize_strength still ships off and is only turned on by a preset.

Adds travel modes and presets to the status table, which listed only the five
original directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 10:34:24 -07:00
Æmotion StudioandClaude Opus 5 ce4b1ecdf9 fix: calibrate roam and stamp against real prompts
Both preset values were wrong on first contact with a real generation.

roam shipped at 0.60, taken from a sweep run with zero conditioning at 448x256
where decorrelated domain_warp held to 0.75. Under an actual prompt at 608x352 it
produced rainbow colour bands. Sweeping 0.35 / 0.45 / 0.55 puts the boundary
between 0.45 and 0.55, so roam is now 0.45. The number had not survived the
change of conditions it was measured under -- which is the exact mistake the
handoff warns about two sections above where I made it.

stamp shipped at 0.70 with a spiral mask and did not visibly draw the mask at
all; it read as a stylised horseshoe. At 0.90 the mask becomes legible, and
hexgrid is far clearer than spiral -- a grid of glowing orange cells in the
forge prompt's own material, which is what the preset promises.

Handoff updated: the travel-mode proposal is marked built with what it needed
(bypassing the widening guards, forcing rank 1 for every generator so shader_type
stays a usable coordinate), presets get their own section, and the calibration
lesson is written down next to the values it applies to.

Two things left open on presets: the override happens at execution time so the
widgets still show stale values, which wants a JS fix like the one
shader_renderer.js already does for the preview; and `preset` is appended last
because ComfyUI maps saved widget values by position, when it should be the first
thing on the node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 09:45:23 -07:00
Æmotion StudioandClaude Opus 5 5434791700 feat: travel modes and presets
Two controls aimed at the gap between "pick one and go" and "read nine tooltips".

TRAVEL MODES. decorrelate_channels was a boolean over something that is really a
range, so it becomes travel_mode: walk / drift / jump, over the same
DECORRELATION_BASIS underneath (64 / 4 / 1).

The two ends are not better and worse, they are different primitives:

                    walk                    jump
    destination     the seed                the shader's parameters
    seed            anchors throughout      fades as strength rises
    coherent range  wide, about 0.75        narrow
    push per unit   gentle                  strong
    output          a scene                 texture in the prompt's material

jump is the behaviour the generators produce for free -- rank 1.00 for
domain_warp at four channels -- which is why high strength used to land on the
same green quilt whatever the prompt: the destination had stopped depending on
where you started. That is a defect when you wanted to explore around a seed and
a coordinate system when you wanted to name a destination, so it is kept as a
mode rather than deleted.

It had to bypass the widening guards, which exist precisely to prevent a remix
narrowing the noise. It also forces rank 1 for every generator, including
tensor_field which natively spans 90 of 128 channels -- otherwise shader_type
would not be a usable coordinate in jump-space.

Measured rank for domain_warp: native 2.06, jump 1.00, drift 3.73, walk 14.36 at
MiniMax H3's 24 channels.

Note this makes widening the default. walk is the default mode, so the standard
pipeline now decorrelates unless told otherwise. Legacy is untouched -- it never
reached this code -- so the golden suite is unaffected.

PRESETS. shader_type, shader_strength, blend_mode, travel_mode,
stage_progression and shape_type only mean anything in combination; choosing them
one at a time is how you end up at 0.6 with a shape mask and a picture made of
hexagons. Seven presets set them together: nudge, explore, roam, video, jump,
stamp, and custom which overrides nothing. Values come from the measurements in
HANDOFF.md rather than taste, and every preset turns normalize_strength on so its
strength number means the same thing in any blend mode.

apply_preset takes an exclude set, and the Walk node passes the parameter it is
ramping -- otherwise a preset pinning shader_strength would flatten a strength
ramp into identical frames.

An unknown preset name is treated as custom rather than raising, so a workflow
saved against a later version still runs.

21 tests. custom and explore produce identical noise from the default widgets,
which is the intended relationship: explore is the defaults, named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 09:34:37 -07:00
Æmotion StudioandClaude Opus 5 85cdc52547 docs: expand the upgrade rationale, and propose keeping the collapse as a travel mode
Expands the agreed upgrade with the reasoning and three consequences that were
discussed but not written down:

- Step 2, flipping the defaults, is the step that delivers. Both fixes ship off,
  so a user installing today still gets rank-1 noise and a strength dial that
  means eight things. The project knows better and does not act on it.
- web/sampling_mode_migration.js silently routes pre-2.0 workflows into legacy
  "so their seeds keep reproducing". After the fix it still fires and no longer
  does that. Drop it, or redefine legacy as the old pipeline structure rather
  than the old output.
- A golden failure changes meaning, from "you broke backwards compatibility" to
  "you changed the sampler", and re-capturing gives up the only bit-exact record
  of pre-2.0 behaviour.

Also notes the widget count: 25 required and 8 optional inputs on the Direct
node, each individually justified, trending away from the compass the README
sells. Prefer presets over the next toggle.

And proposes not deleting the rank-1 behaviour when expand_channels is fixed.
It is a second navigational primitive rather than only a defect: with collapsed
noise the shader parameters set the destination and the seed fades, which is the
inverse of the decorrelated case. It is controllable -- at strength 0.6 the shape
masks rendered spiral, hexgrid, rays and vignette as four distinct recognisable
images in the prompt's own palette -- but the destinations are texture fields
rather than scenes, because the model is being fed out-of-distribution input.

Sketches it as one rank control (walk / drift / jump) over the existing
DECORRELATION_BASIS rather than a boolean, and lists what to test first. The
question worth answering before building anything: whether jump destinations are
recognisably similar across models given the same shader parameters. If they
are, it is a prompt- and model-independent coordinate system. If not, a
per-model curiosity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 09:23:40 -07:00
Æmotion StudioandClaude Opus 5 6144b04851 docs: record the agreed upgrade, and correct the audio-model claim
Legacy compatibility is no longer a constraint: the pack is upgrading and
pre-2.0 workflows changing output is accepted. That supersedes commit 1b21bcb,
the byte-exact promise in tests/golden_cases.py, and the auto-switch in
web/sampling_mode_migration.js. Recorded rather than executed, to be picked up
later.

The plan: fix shaders/base.py::expand_channels at source so domain_warp stops
meaning two different things depending on sampling mode, flip
decorrelate_channels and normalize_strength to default on, and re-purpose the
golden suite as regression pins for the standard pipeline instead of legacy.

The investigation behind it is written down so it does not need redoing. Only
domain_warp calls expand_channels. Exactly one of the eleven goldens changes --
video_nested -- because the eight image cases call it but return early with
nothing to grow at four channels, and the rest use other generators. That is a
trap rather than good news: the suite under-covers the change badly, since the
real population is every legacy workflow using domain_warp on five or more
channels, which is Flux, SD3, WAN, Hunyuan, H3 and LTXV. A small golden diff
would not mean a small change.

Also corrects a mischaracterisation. The handoff said the audio work was
untested on a real audio model. MiniMax H3 *is* an audio model -- it generates
music, speech and sound jointly with video -- so the spectral flatness result is
a real audio-model measurement. What is untested is the narrower rank-3
[B, C, L] sequence-latent path that Stable Audio and ACE-Step use, because no
such checkpoint is installed here.

Removing legacy mode altogether was considered and left out of scope as a
separate, larger decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 09:03:55 -07:00
Æmotion StudioandClaude Opus 5 f16390cff7 docs: correct the ceilings for decorrelation, which inverts the ordering
The tooltips quoted ceilings measured before decorrelate_channels existed, so
they were understating the usable range by roughly three times for the default
generator. Re-derived on real H3 at the current basis of 64:

    generator           without    with
    domain_warp           ~0.25    ~0.75   (degrades at 1.0)
    temporal_coherent     ~0.35    ~0.75
    tensor_field           ~0.5     ~0.5   unchanged, skipped
    curl_noise             ~0.2     ~0.2   unchanged, skipped

This reverses the earlier finding that tensor_field tolerated the most strength.
Rank was the only thing limiting domain_warp; once that is fixed it overtakes the
generators decorrelation skips, which are limited by their spatial character
instead and gain nothing.

The sweep ran with zero conditioning, so content comes from the model's prior
rather than a prompt. Coherence is still unambiguous, and the control in the same
batch reproduced the green quilt seen earlier under real prompts, so the
comparison holds.

Two handoff items close with this; a third opens. Rank still tops out near 60% of
channels because the basis draws are not independent of each other either, so
mixing genuinely orthogonal fields would close the remaining gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 08:32:10 -07:00
Æmotion StudioandClaude Opus 5 c938137645 fix: never paint a metadata stream, even with shade_non_spatial on
shade_non_spatial painted every stream, which is right for MiniMax H3 and LTXAV
where the second stream is audio, and wrong for TripoSplat where it is a
[B, 1, 5] camera. Writing shader noise into that moves the viewpoint rather than
varying the subject -- the tooltip warned about it, which is not the same as
handling it.

Streams carrying fewer than 64 cells per batch item are now skipped. The gap is
wide: TripoSplat's camera is 5, and H3's audio, the smallest real content stream
in the roster, is 414. The first stream is never skipped, so the refusal for a
genuinely unpaintable primary stream still comes from require_spatial_latent
with its named error rather than from silence here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 08:25:05 -07:00
Æmotion StudioandClaude Opus 5 7661d7cf17 fix: raise DECORRELATION_BASIS from 8 to 64, which was set by guess
The basis shipped at 8 with the comment that it bounded cost "which matters at
LTXV's 128 channels". Both halves of that were wrong.

The quality claim, measured on real H3 at strength 0.75 where stock noise is a
solid quilted green texture:

    stock       destroyed
    basis 8     still mostly destroyed
    basis 16    coherent
    basis 32    clean

So 8 was leaving most of the benefit on the table. Achieved rank runs at about
85-90% of the basis until it reaches the channel count, so a basis of 64 gives
full independence to every model up to 64 channels and takes LTXV's 128 from
rank 7.6 to 43.5.

The cost it was guarding against does not exist. Seconds per draw, worst case
across shapes:

    model            stock   basis 8   basis 16   basis 32
    SD 1.5 512        0.01      0.03       0.03       0.03
    Flux 768          0.01      0.09       0.18       0.16
    H3 608x352x56     0.13      0.45       0.79       1.12
    LTXV              0.03      0.07       0.09       0.14

One draw happens per stage boundary, on runs that take thirty to fifty seconds.
At the new default the worst measured case is 1.22s.

Effective rank at the new default:

    (1, 4,   64, 64)      1.00 -> 2.43     (capped by 4 channels)
    (1, 24,  17, 22, 38)  2.03 -> 14.39    MiniMax H3
    (1, 48,  5, 24, 24)   2.15 -> 28.55    Wan 2.2
    (1, 128, 32, 32)      2.37 -> 43.48    LTXV

The no-regression guarantee still holds across all sixteen shape/generator
combinations: turning decorrelation on never narrows the noise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 08:24:13 -07:00