Commit Graph
1626 Commits
Author SHA1 Message Date
digital-garbageandClaude Sonnet 5 094694bcc1 fix: prevframe round 2 — non-dict payload crash, generative-unit scoping bug
Second adversarial pass on the previous fix found two real regressions
it introduced: a non-dict prev_scene_media (malformed client JSON) threw
AttributeError inside the request's try block, failing the ENTIRE
generate call instead of gracefully leaving the slot unconnected; and
the extraction filename was scoped to target.scenes[0].id, which is
the first scene of whatever generative unit _solo() expanded the
request into — not the scene actually being generated — so two
concurrent solo requests for different scenes in the same unit would
clobber each other's extracted frame.

Also swapped the hand-rolled tempdir-containment check for
folder_paths.is_within_directory, which the codebase already has and
resolves symlinks before comparing (the hand-rolled version did not).

Added direct tests for _extract_prev_scene_frame itself (malformed
input, traversal, missing ffmpeg, scene-scoped naming) — round 1's
fix had none, only builder-level tests of its output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 02:36:11 +03:00
digital-garbageandClaude Sonnet 5 cc7e06893b fix: prevframe extraction — no shared-folder leak, path traversal, silent failure
Adversarial review of the previous commit found: the extracted frame was
written into ComfyUI's global input/ folder and never cleaned up (an
actual visible asset, contradicting the point of not touching the media
bin); the client-supplied filename/subfolder round-trip through the saved
project JSON unsanitized, allowing path traversal into ffmpeg's input;
and a missing/garbage-collected predecessor render failed identically
to "scene 1", giving the user no way to tell the two apart.

Now extracts into ComfyUI's TEMP folder (loaded via LoadImage's "[temp]"
annotation — wiped on restart like any other preview, never input/ or
the bin), sanitizes filename/subfolder against traversal, and reports a
distinct "could not be retrieved" warning when a predecessor existed but
its render is gone. Output filename is scene-scoped instead of a
process-random hash, closing the accumulation and cross-request-race
angles as a side effect. Added builder tests for all three prevframe
outcomes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 02:28:22 +03:00
digital-garbageandClaude Sonnet 5 f64b0659eb feat: auto-wire previous scene's last frame into R2V reference slot
New "prevframe" input source: server extracts the preceding scene's
last rendered frame via ffmpeg into ComfyUI's input folder (never the
media bin) and the builder wires it through a plain LoadImage. Scene 1
or a solo re-run with nothing rendered before it leaves the socket
unconnected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 02:18:18 +03:00
digital-garbage 53d7be9c68 test: cover mixed-mechanism dit-patch leak unwinding + optimized_attention_override slot
Adversarial review of the leak fix found the new tests only exercised REINS
stacked on itself, leaving 6 of 7 install mechanisms and the entire
optimized_attention_override chain (av_decouple/attn_temperature/q_steering)
untested. Add a test stacking REINS -> av_decouple -> q_steering -- different
mechanisms chaining onto each other, the realistic multi-run leak shape -- and
confirm one strip call unwinds both the per-block dit dict and the single-slot
attention override chain.
2026-09-10 23:37:33 +03:00
digital-garbage 310a2be912 fix: H3 dit-patch hooks (REINS/query steering/decouple/temperature/repeat/span/probe) leaked on interrupt
patches_replace["dit"] and optimized_attention_override installs had no tag+strip
protection, unlike the scene-wrapper and block-hook mechanisms fixed for the same bug
class earlier. An interrupted run left these hooks on the shared model; the next run
chained a new layer onto the leaked one instead of starting clean, growing unbounded
across interrupted runs -- the likely cause of the live "potential memory leak...
circular references" warnings and slow full-GC pauses.
2026-09-10 23:28:59 +03:00
digital-garbage a6fbca1588 feat: raise h3_q_steer_strength ceiling to 10.0
REINS' equivalent strength dial caps at 2.0, but Q-steering only reaches the
output through softmax rather than a direct hidden-state addition, and live
testing this session found strength 1.0 barely moved the output on a small
block range. Raised well past REINS' number to give room to search before
concluding the mechanism itself is weak.
2026-09-07 05:21:11 +03:00
digital-garbage 50218f8f67 feat: REINS passive capture, and wire H3 query steering into the Editor
REINS ("h3_repr_steering") only ever captured while its own toggle was on --
turning it off also stopped it from learning, with no way to bank rating
history separately. New h3_repr_steering_passive_capture keeps the existing
unconditional per-block capture running (forcing strength to 0 so nothing is
ever injected) even while the main toggle is off. Exposed a real dormant bug
in REINS' own console print: it claimed "applying learned direction(s)"
whenever a direction existed, regardless of strength -- harmless before
because the toggle and strength widget always moved together, not once
passive capture legitimately runs at strength 0 with real data available.
Fixed to report "capturing only" in that case, mirroring h3_q_steering's own
already-fixed version of the same print.

Also: h3_q_steer_block/h3_q_steer_strength (from the H3 query steering
feature) were never wired into the Movie Editor's Settings UI, only exposed
as raw ComfyUI node widgets -- missing this project's own rule that every
new sampler toggle lands in the Editor in the same change. Added both,
plus the new passive-capture checkbox, to Engine Settings > Chain Guidance
alongside REINS' existing controls.
2026-09-07 04:43:26 +03:00
digital-garbage 676edbfb18 fix: H3 query steering -- NaN strength disagreement, and capture leaking temperature scaling
Two real faults found by a third adversarial review round on 4154746:

- _install_h3_av_decouple's own bail check (`_strength <= 0.0`) disagreed with
  _sample_chunk's install guard (`h3_av_decouple > 0.0`) for a NaN strength:
  every ordered comparison is False for NaN under IEEE 754, so `<= 0.0` let a
  NaN through as "on" while the guard treated the same value as "off" --
  reproducing the exact av_decouple-defeats-q_steering bug the last fix
  closed, just reached through a different value. Both now use the same
  `not (x > 0.0)` predicate so they can't disagree.
- h3_q_steering was installed before h3_explore_temperature, so if both
  target the same block, temperature's Q-scaling reached q_steering's
  capture before it ran -- banking a temperature-distorted "natural Q"
  descriptor, contradicting the mechanism's own documented ordering promise
  (capture reflects the network's natural Q, same claim h3_repr_steering
  makes for hidden state). q_steering is now installed last (outermost),
  after both av_decouple and attn_temperature, so it always sees Q first.
  av_decouple still can't compose with q_steering at all regardless of
  order (unrelated masking issue, already handled by skipping install).

Regression tests for both, plus a NaN case added to h3_av_decouple's own
existing zero-strength no-op test.
2026-09-07 04:26:12 +03:00
digital-garbage 41547469a8 fix: H3 query steering silently defeated by h3_av_decouple, and a misreported commit outcome
Two real faults found by a second adversarial review round on 608a97a:

- h3_av_decouple, installed after q_steering, becomes the OUTER
  optimized_attention_override and always calls its inner override with a
  non-None mask for the video-row partition -- exactly the case q_steering's
  own override treats as "already handled, back off". With both enabled,
  q_steering captured and injected nothing while still printing "applying"
  and paying the clone/hook cost. The two can't currently compose, so
  q_steering is now skipped entirely (with a declared reason) whenever
  h3_av_decouple is also active, rather than silently no-op.
- conditioning.py's q_steer commit call was silent on no_pending/no_key,
  justified by a comment claiming REINS' own commit is silent there too --
  that was wrong, REINS is loud on exactly those outcomes. Fixed to match.

Also tightened the shape-mismatch warning's wording: it checks total element
count, not the heads/head_dim split, so a same-total-different-split
mismatch would still reshape onto the wrong head boundaries undetected --
documented as a known narrow gap rather than fixed, since a real fix would
mean changing the persistence format REINS itself also depends on.

Added regression tests for the av_decouple interaction (both directions:
guard fires when it should, stays quiet when it shouldn't).
2026-09-07 04:10:21 +03:00
digital-garbage 608a97a48d fix: H3 query steering was on by default, and could crash or misreport
Three real faults found by adversarial review of 299941a:

- sample()'s own default for h3_q_steer_block was "0,1" while INPUT_TYPES
  declared "" -- any saved workflow predating this widget omits the key
  entirely, so ComfyUI falls back to sample()'s default, silently turning on
  capture/clone/hook for every H3 generation with a refinement key. Now both
  default to blank, matching every sibling knob in this file.
- The "applying learned direction(s)" console line fired whenever a direction
  was available, not when it was actually applied -- strength=0 still printed
  it, which is the exact silent-success-message trap this project has already
  been burned by. Now split into an "applying" line (strength>0) and a
  "capturing only" line (strength=0).
- A direction banked under a different heads*head_dim layout (model swap on
  the same refinement_key) hit an unguarded .view() and would hard-crash
  generation instead of degrading. Now checked and skipped with a one-time
  notice, same pattern as the existing batch>1 guard.

Added regression tests for all three, plus two coverage-gap tests the review
flagged (kind isolation against REAL persistence, not mocks; two named blocks
steering with independent directions in one run).
2026-09-07 03:55:56 +03:00
digital-garbage 299941a22e feat: H3 query steering -- REINS-style direction learned and applied on Q, not hidden state
Same liked-minus-disliked mean-difference as h3_repr_steering, captured off Q inside
optimized_attention_override instead of a block's hidden state, own kind="q_steer" sidecar
so the two vector spaces never collide. Only Q is biased: a uniform additive bias on K is a
provable softmax no-op, and on V it's mathematically identical to h3_repr_steering's own
mechanism, so neither is a genuinely different lever. Off by default (blank block spec).

h3_repr_steering.py's persistence functions take an optional kind= param (default unchanged)
so this reuses capture()/direction()/commit() verbatim instead of a parallel implementation.
2026-09-07 03:43:04 +03:00
digital-garbageandClaude Sonnet 5 8804744a36 feat: chainable attention overrides + a rating-free H3 attention randomizer
optimized_attention_override was a single dict key that h3_av_decouple claimed
outright -- a second mechanism on the same slot would silently replace it with
no error, the exact bug class dit_patches chaining already guards against
elsewhere. Fixed so any install chains through whatever's already there.

New h3_explore_temperature (+ block spec): flattens attention's own softmax at
named blocks instead of adding ungated latent noise -- variety stays bounded
by the prompt since it can't attend to anything not already in context.
Deliberately NOT wired to REINS or any rating data: neither mechanism's
behavior is understood well enough yet to make coupling them debuggable. A
plain manual dial, same as block repeat. Off by default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 03:00:57 +03:00
digital-garbageandClaude Sonnet 5 a60b0e981f fix: h3_av_decouple was a 0-1 dial over a guessed conversion constant
No way to read H3's actual trained attention-logit scale from this machine
(checkpoint isn't here) -- so normalizing the knob to 0-1 against a hidden
made-up multiplier (12.0) was a second guess wearing a calibrated costume,
and exactly the kind of thing that produces another flat live-tested curve.
The knob is now the raw softmax logit penalty directly (0-50, step 0.5), so
a live sweep searches the actual unit instead of my conversion factor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 02:29:26 +03:00
digital-garbageandClaude Sonnet 5 1e4553e658 fix: H3 attention decoupling was scaling K magnitude, which barely moves softmax
Live 0-1 strength sweep came back flat -- confirmed the mechanism, not just the
value, was wrong. H3's Q/K get RMSNorm'd before attention, so scaling a key
toward zero pushes its logit toward the *mean* of an already near-zero-centered
distribution, not toward excluded. Switched to what actually suppresses a
softmax entry: an additive negative bias on the targeted logits, still passed
through the same per-query-group split (still no S^2 mask, still same total
FLOPs -- the bias is a per-key vector broadcast over the group's queries).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 02:23:30 +03:00
digital-garbageandClaude Sonnet 5 9bbdfc8b4b feat: H3 attention decoupling -- damp video<->audio joint self-attention, no S^2 mask
H3 has no cross-attn to mask (unlike LTXAV's Bounded Attention), so a dense
attention mask would be GB-scale over its ~37k-token packed sequence. Uses
comfy's own optimized_attention_override extension point instead: runs the
same joint attention as separate video-query/audio-query passes against a K
where the other modality's rows are scaled down, at the same total FLOPs as
one pass. New h3_av_decouple sampler input + Engine Settings entry, off by
default (0 installs nothing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 02:09:15 +03:00
digital-garbage e9174c257c fix: sweep gallery ratings were single-slot, not per-item -- rebuilt on independent slots
The sweep's rating feature was wired against REINS' single 'pending'
capture (one slot per key, overwritten by whatever generates next),
which meant only the MOST RECENT result in a whole batch could ever be
rated -- the opposite of what batch training actually does: rate every
result you generated, independently, in any order.

New h3_repr_capture_slot sampler input (blank by default = unchanged
ordinary one-at-a-time flow) saves a run's capture under an explicit
slot instead of the single pending. New h3_repr_steering.save_capture_slot
/ commit_slot / discard_slot mirror save_pending/commit but keyed, so a
whole batch stays independently rateable the same way batch_training.py's
per-item .cond.pt files already do for value-function training.

New server routes commit/discard a slot directly -- no extra generation
needed, unlike the old 'fire another render with rating attached'
workaround that existed only because of the single-slot limitation.
Every gallery card is now rateable (Like/Dislike/Skip), not just the
last one.
2026-09-05 01:22:23 +03:00
digital-garbage 9a45229e85 feat: log liked/disliked counts per steered block, not just when data is short
The 'applying learned direction(s)' line read the same whether a block
was steering off the bare 2/2 minimum or twenty rated runs -- and those
are not equally trustworthy. Now prints each steered block's own
(liked/disliked) count alongside it, so a style shift from a
just-barely-qualified block is distinguishable from one with real
history behind it.
2026-09-05 00:30:54 +03:00
digital-garbage 1203315aff feat: Like/Dislike/Skip buttons on the sweep gallery instead of a label dropdown
REINS only ever reads a rating's SIGN (direction() splits liked vs disliked
on weight > 0/< 0, magnitude unused), so the full nuanced rating taxonomy
was overkill here. Like/Dislike send the plain H3-scale digit ratings
('9'/'2') the pipeline already accepts, well clear of the sign-flip
midpoint. Skip costs nothing -- no follow-up generation, just stops the
row asking.

Also fixes the rated state resetting to blank on the next re-render: the
outcome (liked/disliked/skipped) is now tracked per result and the row
shows it instead of the buttons again.
2026-09-05 00:10:24 +03:00
digital-garbage 6c9b0a91aa feat: REINS steers multiple blocks at once, block picker covers all 50
h3_repr_steering_block goes from a fixed combo (18 candidate blocks) to
free text -- a single block, a range, or a comma list, same syntax as
Repeat block(s) (31-40 / 4,5,6). Each named block steers with its OWN
learned direction from its OWN liked/disliked history at that block --
there is no shared 'the direction', just N independent ones applied at
their own positions in the same forward pass. A block short on rated
data at itself just captures, same as any unnamed candidate block.

CANDIDATE_BLOCKS widened from 18 curated blocks to all 50 -- every block
is now both capturable and steerable, not just the ones on the old grid.

Fixed a pre-existing sync gap: h3_block_repeat_last_steps was never added
to settings_card.py's ownership/defaults tables, breaking two golden-file
tests as soon as they ran (test_settings_card_noise.py).
2026-09-05 00:03:56 +03:00
digital-garbage 7d46171327 feat: H3 Combo Sweep — REINS + block-repeat combined test lines, ratings, seed toggle
Renamed the sweep panel (H3 Block Repeat Sweep -> H3 Combo Sweep) and the
REINS settings labels (now lead with 'REINS' instead of 'Representation
steering'), per-request.

New combinable line format: reins:strength;block and/or sweep:blocks;
seam|noseam;times[;laststeps], joined with '|' -- e.g.
'reins:0.15;49|sweep:40-41;noseam;1;0' or just 'reins:0.1;49' alone.
Whichever half is omitted is explicitly OFF for that line, not whatever
Engine Settings has -- every line fully states what it tests.

Seed checkbox, default OFF: each line gets its own fresh random seed
regardless of the project's Engine Settings seed. On: one seed picked
once for the whole sweep (the old behaviour).

Rate button on the gallery: only the MOST RECENT card is rateable, since
REINS' pending capture is a single slot overwritten by the next run that
samples -- rating any earlier card would silently commit against the
wrong run's capture. Fires one more generation (same config, rating
attached via node_overrides on the 'studio' node) since a rating isn't a
free action, it rides the next generation the same way it does
everywhere else in this codebase.

Removed Picture detail (h3_video_detail) from Engine Settings -- unused,
backend default (1.0) is already a no-op so hiding it is safe.
2026-09-04 23:56:33 +03:00
digital-garbage d7d8f7f01d fix: Engine Settings' own copy of h3_block_repeat_last_steps was still floored at 0
The node widget's min went to -50 for the head-window feature, but the
Editor's Engine Settings field metadata is a SEPARATE declaration and
still had min:0 -- so the UI silently refused negative values the node
itself already accepted. Widened to match, updated label/hint for both
directions.
2026-09-04 23:09:43 +03:00
digital-garbage 226e0983f5 feat: h3_block_repeat_last_steps goes negative for 'apply early, then stop'
Same widget, sign now carries direction: positive N = final N steps
(tail, refine only -- what every laststeps test so far has used).
Negative N = first |N| steps instead -- apply while structure is still
forming, then get out of the way and let the untouched rest of the
schedule resolve it, instead of the perturbed step being close to the
last word on the output.

Widget min widened 0 -> -50 (backward compatible: 0 still means every
step, existing positive values unchanged). Sweep panel accepts negative
laststeps in the config line (e.g. 40-41;noseam;1;-2).
2026-09-04 23:01:27 +03:00
digital-garbage 8f6637be6d feat: sweep render files are named from their config, not funpack_preview_<uuid>
Overrides VHS_VideoCombine's filename_prefix per run (same node_overrides
mechanism as everything else in the sweep) to funpack_sweep_<config>_<short>,
so the temp file on disk is self-identifying without needing the manual
Save-to-Media-bin step at all.
2026-09-04 22:08:41 +03:00
digital-garbage 4103d10df0 feat: remove sweep config checkbox, persist config text, name saved videos by config
The checkbox gated the config panel that IS the whole point of a dedicated
settings entry -- pointless now that it lives in its own section instead of
being folded into something else. Config text also moves to a module-level
variable so it survives closing and reopening the panel instead of resetting
on every mount.

Each gallery result gets a 'Save to Media bin' button that names the file
from its own sweep config (sweep_10-11-13-seam-5_<ts>.mp4) via the same
import-clip route Save Video uses -- server-side copy, no re-upload.
2026-09-04 22:06:25 +03:00
digital-garbage adf4673501 fix: sweep gallery videos were cropped to 56px thumbnail height
Reused media-card/tmp-card/media-thumb classes, which are sized for a
clickable thumbnail strip, not for watching. New dedicated sw-sweep-grid/
sw-sweep-card classes give each result a real 16:9 player instead of a
sliver you had to open in a separate tab to see.
2026-09-04 22:01:25 +03:00
digital-garbage 4eec7ba5b8 fix: block-repeat sweep checkbox rendered as a giant blue square
The generic input{width:100%; min-height:...} rule (meant for text fields)
was also catching the raw checkbox, since nothing scoped it down the way
every other checkbox in the app does (.field-toggle, .eng-field). Same fix
as those: width auto, plus a small flex label so it sits inline with its
text instead of centered alone on its own line.
2026-09-04 21:53:40 +03:00
digital-garbage 39be0455b5 feat: last-steps field in the block-repeat sweep config
Fourth optional ;laststeps field wires h3_block_repeat_last_steps per config
line, so the sweep can test the tail-window against the full-schedule
default in the same run instead of needing a separate mechanism.
2026-09-04 21:50:17 +03:00
digital-garbage e10ed05a20 feat: Save video button + H3 block-repeat sweep panel in Editor
Save video (transport bar, next to Save frame): copies the current clip's
render straight into the Media bin server-side via the existing import-clip
route -- no download-then-reupload round trip through the local machine.

H3 Block Repeat Sweep (Settings > Learning): runs one scene through several
block-repeat configs back to back on one fixed seed, gallery of labelled
results. One config per line: blocks;seam|noseam;times. Each run is a
one-off node_overrides on the built graph (same mechanism as the existing
anchor-guide i2v bypass) -- project Engine Settings are never touched, so
nothing needs restoring after and a crash mid-sweep leaves no residue.
2026-09-04 21:47:02 +03:00
digital-garbage f8e777e696 feat: confine H3 block repeat/span loop to the last N schedule steps
Every step was being repeated, including the earliest near-pure-noise ones
where a block is still deciding layout, not detail -- the likely reason
repeat re-rolls the shot instead of sharpening it. New h3_block_repeat_last_steps
(0 = every step, unchanged default) windows the repeat to the tail via the
exact step index off transformer_options[sample_sigmas/sigmas].
2026-09-04 21:34:05 +03:00
digital-garbage fec02975c0 docs: 31-40 downgraded from "best result" to "style lever"
More runs: it reliably brings the preferred style (skin texture,
outlines — replicated across two reference images) and guarantees
nothing about the rest. Motion, anatomy and coherence are still a
re-roll. Calling it the best result was over-read from the first few
runs, and the constraint map and tooltip both said so.

Also retracts the span-loop "dead" verdict from the previous commit:
10-21 as a span is visually better than the other loops while ruining
audio, so the loop is dead at mid-depth and alive shallow. That also
breaks the displacement model on its own — 10-21 is a WIDER span (+12)
than 31-40 (+10) and fares better, so depth modulates tolerance on top
of displacement, as the per-block sweep already found.

One finding hardened rather than softened: the ~10-21 band is where
audio is fragile, and it now replicates across two INDEPENDENT
mechanisms (per-block @2x replaced the soundtrack; a span loop ruins it
too). Different operation, same band, same casualty, video fine or
better in both.
2026-09-04 20:12:09 +03:00
digital-garbage e8b96c436d docs: span loop tested and dead — seams were not the mechanism
Built on the hypothesis that seam count explained why 31,40 survives two
extra passes while 31-40 collapses. Tested across nine spans: the loop
is worse than per-block repeat everywhere. 31-40 per-block @1x is the
sweep's best result; the identical blocks as a span loop at identical
cost are awful. Fewer seams made it WORSE, so the hypothesis is dead.

What fits instead is input displacement — how far each block's input is
from what it was trained on. Per-block repeat: a block's second call
sees the stream advanced by ONE block, regardless of how many blocks are
selected. Span loop: the first block of the span sees it advanced by
SPAN_LENGTH. 31-35 (+5) tolerable, 31-40 (+10) awful, 0-49 (+50)
destroyed — monotonic in width. Displacement also suppresses motion
specifically: "reference refuses to move" appears in every span-loop
result and in none of the per-block ones.

Also corrects an overclaim in the video-only docs. It restores audio
rows at the exit of the repeated blocks, but the blocks AFTER them still
process audio while attending to twice-processed video, so protection
ends at the span exit and not at the output. That is why the checkbox
did not save audio under a span loop.

The toggle stays — it is a real mechanism, cheap, and 31-35 was usable.
It is documented as losing to per-block rather than removed.
2026-09-04 20:10:51 +03:00
digital-garbage e3d63a99a3 feat: span loop — 31,32..40,31,32..40 instead of 31,31,32,32..40,40
A different operation, not a re-spelling. Per-block repeat hands every
block its OWN doubled output, which is a distribution the NEXT block has
never seen, so a ten-block span creates ten seams. A span loop leaves
every block receiving input from its normal predecessor and creates
exactly ONE, at the wrap from last back to first.

That is the obvious candidate explanation for the sweep's central
puzzle: 31,40 and 35,40 survive two extra passes while 31-40 collapses
at the same count. Same total extra block evaluations, so the same cost
— only the seam count differs.

The span's first block does the work (walking
model.model.diffusion_model.blocks, which the hook cannot reach since
original_block closes over one block only) and the rest of the span
becomes a pass-through, because the model's own loop still calls them.

Refuses a non-contiguous selection with a note rather than silently
looping min..max, which would run the blocks deliberately left out.
Under video-only, text and audio rows get the span ONCE — not zero
times, which would skip those blocks entirely for audio and is a
different, worse intervention than "do not repeat them".
2026-09-04 19:56:38 +03:00
digital-garbage b9a570dd0e feat: repeat video rows only, leaving the soundtrack on one pass
Band 31-40 is the best video result in the sweep and invents unprompted
dialogue on every single run of it — a trade that the bisection proved
cannot be dropped by narrowing, because neither half reproduces either
effect.

H3 packs video, text and audio into ONE sequence, so a plain repeat
doubles the block for the audio rows too. This restores text and audio
rows to their single-pass value after the extra passes, keeping the
repeat for video only.

It does NOT undo audio's influence on the second pass — attention had
already mixed the twice-processed audio rows into the video rows before
they were restored — so it fixes the audio that comes OUT, not the video
path. Whether the video gain survives that is the experiment.

Off by default so the measured sweep stays reproducible, and it falls
back to repeating everything when the mask code meets a shape it has
never seen, rather than silently dropping the feature.
2026-09-04 19:45:51 +03:00
digital-garbage f88fb1c947 docs: constraint map — the 31-40 audio cost is constant, and inert != safe
Two corrections from continued testing:

- 31-40's unprompted dialogue has appeared on EVERY run of that band,
  including the cross-image replication. It is a trade (better video
  texture/outlines/motion, paid for in audio), not a win with an
  occasional blemish, and calling it "the best result" without that
  clause oversells it.
- Everything past 40 dislikes a second pass, INCLUDING blocks that did
  nothing at all on the first. So "no visible effect" is not headroom —
  a null result at one pass must not be read as permission to push
  harder. That is now a stated rule, since the opposite reading is the
  natural one.
2026-09-04 19:30:02 +03:00
digital-garbage 38100d2a55 docs: block-repeat constraint map from the 2026-09-04 sweep
The Details panel on h3_block_repeat now carries what was actually
measured instead of a description of the idea, because the field is free
text and the dead zones are not guessable:

  0-10    destroys everything — never
  10, 15  indistinguishable from plain — pointless
  11-20   video good, AUDIO AWFUL; survives 2 passes on video only
  20-30   mild and mostly positive — safe band
  31-41   best results, ONE PASS ONLY (collapses at 2, seen twice)
  42-46   no significant difference
  47-49   destroys detail / noise / broken audio — never

Two rules run through it: repeat tolerance falls off with depth, and
audio breaks shallower than video. Also states the headline the sweep
actually produced — this re-rolls the shot, it does not sharpen (detail
stayed within ±5% everywhere useful while the picture moved 17-60%).

Nothing is clamped or refused. Most cells are one observation by eye and
the map is a note to the person typing, not a gate.
2026-09-04 19:26:44 +03:00
digital-garbage 6bbf8550cf fix: the detail verdict never actually asked the sharpness question
dpVerdict gated on structure >= 0.85 and returned "the shot moved"
otherwise — so `detail` and `edge_aligned` were never consulted. The
block-repeat sweep's best row was structure 0.831, which means all 19
comparisons short-circuited and the "sharper" branch was unreachable for
the entire experiment. Reporting that as "not one row read sharper" was
true and misleading: sharpness was never evaluated at all.

The threshold was also wrong on its own terms. Blocks 10/15/44/45 were
judged indistinguishable from plain by eye and measured 0.73-0.83, so
the cliff was rejecting pairs a person calls the same shot.

Now: how far the shot moved is a PREFIX, not a verdict, and the detail
half is always reported alongside it ("shot moved 17% · detail
unchanged"), which is strictly more than either half alone. Only below
0.35 — where the two runs share almost nothing — is a detail comparison
refused outright.

Tested against the sweep's real rows, including a hypothetical genuine
sharpen to prove the branch is now reachable.
2026-09-04 19:20:48 +03:00
digital-garbage 38b00ff8e9 feat: record the attention backend in the run fingerprint
It is not an argument of the sampler node, but it decides whether two
runs can be identical at all: comfy_kitchen_int8 costs ~10% of the
picture between runs with everything else fixed (quantisation flips
buckets, which shows up as eyes and mouth changing while the motion
stays put), and pytorch is bit-exact. An A/B unknowingly spanning a
backend switch compares two different machines.

The override function carries its own name now, so the fingerprint can
report which backend was live rather than "something overrode it".
2026-09-04 18:45:47 +03:00
digital-garbage 175b08ff32 fix: panel threw on every detail row (temporal dead zone)
`const moved` sat below the repeatability branch that uses it, so that
branch hit "can't access lexical declaration 'moved' before
initialization" the moment a row rendered. Valid syntax, so node --check
passed it — the failure only exists at runtime.

Declarations moved to the top of dpVerdict, and the panel logic now has
a test that RUNS every branch instead of parsing it. That is the actual
lesson: this file had no runtime coverage at all, and a syntax check
cannot see a TDZ.

Also: the block panel showed a cumulative count beside a green dot while
recording was off, which reads as "still recording". It now says
"Recorded earlier (not recording now)" and drops the dot. Turning
recording off has never cleared stored profiles and still doesn't — the
count is history.
2026-09-04 18:37:40 +03:00
digital-garbage a187dc055a feat: detail probe gets its own switch, so it can run without the block probe
They were sharing one toggle on the argument that a second switch for
the same decision is worse UI. That was wrong for a specific reason: the
two probes cost wildly different amounts — the detail probe copies one
latent per run, the block probe gathers ~17k x 5120 rows per block per
step across 18 blocks — and allocator churn on that scale can change
which GPU kernels get selected, so a probe can move the numbers it is
measuring. Answering "is the measurement causing the variation?" needs
one running while the other is off, which a shared toggle made
impossible to perform.

Also: the noise floor is now measured from your own repeat rows (median
of 1 - structure across pairs where nothing changed) instead of graded
against a fixed 0.98 that no real rig reaches. Other rows report their
movement as a multiple of it, so "structure 0.42" reads as "6x your
noise floor" — which is the part that means something.
2026-09-04 18:31:34 +03:00
digital-garbage 24f147418a fix: "prompt/scenes changed" fired on runs with an identical prompt
The conditioning fingerprint was a formatted string, so it was only ever
equal or not — encoder float wobble read exactly like a new prompt, and
every row claimed the prompt changed on four runs where it demonstrably
had not.

It is numbers now (numel, mean, std per entry), compared with a
tolerance, and the row reports the SIZE of the move. That separates
three cases the old label collapsed into one:

  under 0.1%   encoder wobble — not reported at all
  a few %      "conditioning drifted 2.1%" — something rating-driven
               moved it (h3_phrase_emphasis lives on the conditioning
               and updates as ratings land), worth naming but it does
               not invalidate the comparison
  token count  "prompt/scenes changed" — structurally a different
  or entry     generation, and only this still voids the A/B
  count differs
2026-09-04 18:27:09 +03:00
digital-garbage 24f7cec8dd feat: REINS can steer anywhere in 40-49, one block at a time
The influence probe measured the stack as heavily back-loaded — blocks
48 and 49 alone carry about a third of all residual movement, and the
busiest twelve are almost all in the 40s — while REINS defaults to block
25 and the picker stopped at 40, so it could not reach where the model
does most of its work.

CANDIDATE_BLOCKS now covers 40-49 individually (the quiet early half
stays on the coarse grid). Capture has to cover them too: direction()
reads descriptors at the exact block being steered, so a picker entry
outside the capture set would say "not enough data yet" forever. A test
pins that the two sets cannot drift apart — the same silent-substitution
class as the rating combo that discarded every digit.

Node and Editor choice lists verified byte-identical: a mismatch there
makes ComfyUI's widget validation quietly swap in the default.
2026-09-04 18:23:58 +03:00
digital-garbage 8a670fdf60 fix: a prompt change was reported as "nothing changed"
The run fingerprint captured only scalar arguments, and the prompt
arrives as conditioning tensors — so two runs of completely different
text compared as a repeatability check, and the resulting structure drop
(0.44-0.93) read as "generation is not reproducible here" when one row
in the same batch showed a bit-identical pair (structure 1.000, detail
x1.000, edge +0.000), proving it demonstrably is.

Fingerprint now includes a conditioning stand-in: per-entry shape + mean
+ std, which differs between any two real prompts and costs two
reductions on a tensor already resident. The row says "prompt/scenes
changed" and refuses to call the pair a noise-floor reading.
2026-09-04 18:18:41 +03:00
digital-garbage be5579f1f9 fix: rank blocks by share of movement, not by a ratio depth decides
Live data showed block 1 at 6.01 and block 0 at 1.83 against ~0.2-0.35
everywhere else, with block 1's liked/disliked gap (+5.82) as large as
its whole measurement. That is not "the first two blocks do 30x the
work" — ||f_i||/||x_i|| divides by a residual stream that GROWS with
depth, so early blocks score high because the denominator is barely
formed. Ranking on it just sorts the earliest blocks to the top, and a
high-variance early block fakes a large taste correlation at low n.

The hook now stores both norms instead of their ratio, and profile()
adds `share` — each block's raw ||f_i|| over every block's, one
denominator for the whole stack. The panel ranks and displays that
("2.3% of the movement"). The ratio is kept, since it is still the right
answer to "how much did this block move the stream relative to what the
stream was".

Checked against data shaped like the report: ratio ranks 0,1,2 (all
artifact), share ranks the blocks that actually move more.
2026-09-04 18:10:55 +03:00
digital-garbage b6841cbbd7 fix: the detail row now says what the A/B actually was
Every row read "no repeat → no repeat" because the label only ever
tracked one knob — the block-repeat spec — so a comparison where
anything else changed, or nothing did, described itself as unchanged and
told you nothing about what it compared.

sample() now snapshots its scalar arguments (locals() at the top IS the
argument list, so no name list to go stale), and each row keeps the DIFF
against the previous run. The row titles itself:

  h3_block_repeat: — → 40      sharper — +204% detail, on existing edges
  nothing changed              stable — two identical runs match, so a
                               real A/B can be trusted
  seed 111 → 222               different seed — not comparable

The "nothing changed" case is the valuable one and was previously
unreachable: two identical runs measure how much generation varies on
its own. If that comes back unstable, no A/B on the setup means
anything, and the row says so instead of letting every later comparison
quietly inherit the noise.
2026-09-04 18:07:08 +03:00
digital-garbage 7125c1ba4d fix: block-influence probe read "before" AFTER running the block
A block that writes its residual in place leaves args["img"] already
holding the post-block values, so every delta came out exactly zero —
and a zero delta gives a zero cosine, so magnitude AND novelty both read
0.0000 / +0.00 on every block. That looks like "this model has no block
structure", which is a finding, when it actually meant the probe
measured nothing. The exact silent-no-op class this probe exists to
catch, shipped inside the probe.

Snapshot the sampled rows before calling the block. Advanced indexing
copies, so the snapshot is not a view onto src.

Regression test pins it: a block mutating img in place must still report
|delta| = 2x|before|. Verified it fails with 0.0 against the old code.

Also: the readout used toFixed(4), which hides exactly the range these
values live in — the intervention map measured block activity varying in
the 4th decimal, so 0.0000 was ambiguous between "tiny" and "nothing".
Now toPrecision(3).

Detail probe: records the seed with each run, so a low `structure` can
say which of its two causes applies — different seeds (two separate
generations, never an A/B; a reference pins the subject, not the sample)
or the same seed whose shot the change actually moved, which is a
result. The numbers cannot separate those; the seed can.
2026-09-04 18:01:07 +03:00
digital-garbage b1f80ef45f feat: block repeat + the detail measurement that can falsify it
h3_block_repeat runs the named block(s) on their own output before
passing it on — a second pass that never leaves latent space. To first
order this doubles that block's contribution; the nonlinear part is a
genuine second refinement of its own result. ~2% of a step per repeat.

detail_probe scores whether that (or anything else) actually sharpened
the picture, by comparing a run against the previous one on three
numbers:
  detail        high-frequency energy — did fine structure increase
  structure     low-frequency survival — same shot, or a different one
  edge-aligned  did the EXTRA detail land on existing edges, or spread

The third is the one that earns its place: noise is high-frequency too,
so `detail` alone cannot tell sharpening from grain — and grain is
exactly what an out-of-distribution stream would produce. Verified on
video-latent-shaped data: unsharp-mask reads detail x3.04 / edge +0.885
("sharper"), uniform noise reads detail x1.12 / edge -0.248 ("grain").
Both raised detail; only edge-alignment separated them.

Measured in latent space deliberately — the tensor is already in hand at
the call site DynaShift banks from, so no VAE decode and no new
plumbing. A real approximation, valid for the A/B it is for (same seed,
identical decoder both sides) and documented as ratio-only.

Reuses block_influence's collection switch rather than adding a second
research toggle, and pairs CONSECUTIVE runs, which is the workflow
anyway: generate with it off, generate with it on, read the row.

Also scores second_pass sharpen, quality_sharpness and segmented
detailing — every mechanism here that claims detail and is currently
judged by eye.
2026-09-04 17:51:01 +03:00
digital-garbage e987f6036f feat: block probe also measures NOVELTY — what each block adds that the
previous one didn't

Magnitude alone cannot distinguish a block contributing something nothing
before it contributed from a block pushing harder in the direction the
last one already went. Records cos(f_i, f_i-1) on the same strided video
rows: ~1 means it amplifies its predecessor (little new per block), ~0
means orthogonal (a genuinely new component), negative means it partly
UNDOES the previous block — which real stacks do, and which would mean
the effective work is far smaller than the sum of the magnitudes.

This is the number that speaks to whether 50 blocks are doing 50
different things or one thing 50 times.

Step boundaries are respected: a block index that does not exceed the
last one seen starts a new step, so block 0 is never compared against
block 49 of the previous step — that pairs the end of one denoise step
with the start of the next, which is not a depth relationship.

Reads out in Refinement & Taste beside flatness, with a plain-language
verdict and a per-block column. Verified live on seeded data: amplifying
(+0.81), undoing (-0.49) and orthogonal (+0.06) regimes all round-trip
through the API and render. Seed data cleared.
2026-09-04 17:37:26 +03:00
digital-garbage 4aad3d7a09 refactor: block-influence data lives in Settings, not the console
Research data belongs in Settings > Refinement & Taste — readable,
downloadable, clearable, with its own collection switch — not in a run
report that already carries more than anyone but us needs.

- Removed the sampler widget and its Editor knob entirely. Collection is
  driven by a global switch (env var live for this process + on-disk copy
  so a fresh rental comes back recording), the same two-layer mechanism
  trajectory_probe uses.
- Removed the per-rating console readout. Findings go to the panel;
  failures still speak through funpack_log.
- New section in Refinement & Taste: toggle, recorded count, Download,
  Clear, a plain-language flatness verdict, and the busiest blocks ranked
  with their liked-vs-disliked delta.
- Routes: GET/POST /api/block_influence, /export, /clear.

The switch gates whether the PROBE is installed and nothing else —
save_pending/commit stay unconditional, with a test pinning that, since a
toggle that quietly stops recording ratings is a mistake this project has
already made once.

Verified live: routes round-trip, the panel renders a seeded 6-run
profile, and a planted rating-correlated bump at block 31 surfaces as
+0.0107 in the UI. Seed data cleared afterwards.
2026-09-04 17:13:35 +03:00
digital-garbage d6981036d0 feat: per-block influence probe for H3 (measurement only)
Records ||f_i(x_i)|| / ||x_i|| on video rows for every block, averaged
over each run's steps, and pairs the profile with that run's rating.
Answers the question that gates rating-driven block weighting: is H3's
depth profile flat (nothing to aim at) or structured? Reports a
scale-invariant flatness number, and once a key has 2+ liked and 2+
disliked runs, which blocks run hotter on the liked ones.

Steers nothing. Installed BEFORE any steering hook so the delta it
records is the block's own, not one already containing an injection.
Bounded cost: two norms per block per step on ~512 strided rows, no
extra forward pass, results accumulated as device tensors and synced
once per run rather than 50x per step. Says so explicitly when it
records nothing, since an empty capture and a flat profile are
otherwise indistinguishable downstream.

Adjacent to per-block residual steering, removed in b7ed4f9 for
finding no rating signal above the 4th decimal -- that pass had no
per-step resolution, which its own removal note named as the gap.
This either reproduces that null definitively or it doesn't.
2026-09-04 17:01:21 +03:00
digital-garbage 8e31dcbff6 fix: report when DynaShift's positive pull is silently inactive
The status line at generation start only counted bank sizes, not
whether entries actually match this run's latent resolution -- so
"positive pull active (N liked)" could be true by that rough count
while the real per-resolution check inside the wrapper found nothing
usable, with no way to tell from the log. Negatives already had this
warning; positives didn't. Now both report a resolution mismatch.
2026-09-03 23:56:43 +03:00