Commit Graph
1635 Commits
Author SHA1 Message Date
digital-garbageandClaude Sonnet 5 e164e1ef70 feat: wire a Media Bin reference into a placed node's file-upload widget
VHS_LoadVideo's "video" field (and LoadImage/LoadAudio/LoadVideo's own
file fields) are plain combo widgets, not wireable sockets, so a marked
reference video had no way in except a manual upload or path -- which
meant no way to force VHS_LoadVideo's own framerate/frame-cap controls
onto a reference clip without re-uploading it outside the Editor.

The node panel's combo fields for these known upload widgets now offer
the marked Media Bin references of the matching kind, using the same
"ref:<id>" values as Input sources. The builder resolves them to the
reference's filename at build time; an unresolvable or wrong-kind
reference is reported instead of silently defaulting.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 16:06:53 +03:00
digital-garbageandClaude Sonnet 5 063fa5a55b fix: media bin thumbnails no longer reload on every reference toggle
The panel rebuilds its whole DOM on any relevant store change,
including marking/unmarking a reference -- which only actually
changes one card's badge. That recreated every <img>/<video> thumb
from scratch each time, so every preview visibly reloaded and every
video redecoded its first frame for an unrelated single-item toggle.

Cache the built preview node per media id and re-append (move, not
recreate) it across rebuilds instead. Verified live: toggling a
reference mark on/off now leaves the same DOM node instances in
place. Adversarially reviewed; holds up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 20:36:43 +03:00
digital-garbageandClaude Sonnet 5 59f91a9e24 feat: video reference wins over image reference at the same slot index
For nodes with parallel image/video autogrow reference lists (MiniMax
H3's ref_images/ref_videos and anything following the same naming
convention): when both end up filled at the same index, only the
video is sent -- letting you switch between image-as-reference and
video-as-reference runs without manually clearing the one you're not
using. Per-node opt-out toggle, on by default.

Adversarial review (three rounds) found and fixed a real gap: dropping
an image reference from the middle of a multi-reference list left a
non-contiguous index sequence, which the V3 autogrow expansion this
project already relies on cannot parse. Surviving entries are now
re-indexed to close the gap.

Also fixes an unrelated bug the same review surfaced in the Linked
Inputs picker: clicking "Save link"/"Add" while an unrelated node-page
edit sat buffered in the deferSave queue silently persisted that edit
too, bypassing the normal unsaved-changes confirm. It now runs through
the same confirm/discard path as leaving the page any other way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 20:14:53 +03:00
digital-garbageandClaude Sonnet 5 3022a341b0 fix: gate add-to-link picker on kind and combo choices compatibility
Adversarial review of the previous commit found two ways an
incompatible node input could silently corrupt an existing linked
input's shared value: no kind check at all, and (after fixing that) a
same-kind combo with a different choices list still slipping through
and getting overwritten via the widget's own stale-value self-heal,
which then broadcast to every other member of the link.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 20:03:03 +03:00
digital-garbageandClaude Sonnet 5 1f663a3ce1 feat: allow adding inputs to an already-created linked input
Models > Linked inputs could only create a new link; there was no way
to add more node inputs to one that already existed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 19:53:40 +03:00
digital-garbageandClaude Sonnet 5 7a75d93423 test: prevvideo -- assert same-class sockets still share one loader node
Round 2 of review confirmed the class-scoping fix closes the cross-wire
bug with no new defect, but flagged that the new test only exercised
two DIFFERENT loader classes -- it couldn't have caught an over-fix
into a unique node per socket, losing the original one-loader
shared-by-multiple-sockets design the class-scoped id is supposed to
preserve. Added the missing case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 03:09:54 +03:00
digital-garbageandClaude Sonnet 5 6571543212 fix: prevvideo -- scope loader node id by class, don't drop a second notice
Adversarial review found two real bugs in the new prevvideo feature:

1. The loader node id was a bare "prevvideo_load", copied from
   prevframe's pattern -- but unlike prevframe (always LoadImage),
   prevvideo's loader class varies per destination socket type
   (LoadVideo vs VHS_LoadVideo). Two prevvideo sockets needing
   different loaders in the same run would share one node via
   graph.setdefault, silently wiring a mismatched-type output into
   the second socket while the builder reported both as cleanly
   wired. Fixed by scoping the id to the loader class, matching the
   sibling ref#/ref: bin-reference path's own ref_load_{id}_{cls}
   pattern -- prevvideo should have followed that one, not
   prevframe's single-class shortcut.

2. store.js's declared-failure notice used .find() across
   report.unsatisfied, so a run where BOTH a prevframe socket and a
   prevvideo socket failed to retrieve the predecessor's render would
   silently drop one of the two messages. Now collects every match.

Added a two-socket regression test proving the loader-scoping fix
(distinct node ids, correct class per socket).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 03:06:32 +03:00
digital-garbageandClaude Sonnet 5 198318db3e feat: auto-wire previous scene's whole video into a ref-video slot
Same idea as "prevframe" but passing the entire previous scene's
rendered clip through instead of extracting one frame from it: a new
"prevvideo" input source wires whatever LoadVideo-family node the
destination socket needs, pointed straight at the predecessor's own
TEMP file via ComfyUI's "[temp]" annotation -- no ffmpeg, no copy, no
media-bin entry.

Refactored the shared "resolve and sanitize prev_scene_media into a
safe TEMP path" logic out of _extract_prev_scene_frame into
_resolve_prev_scene_media so both paths share one place that trusts
none of the client-supplied filename/subfolder as literal paths.
Reuses the R2V pipeline's own _reference_loader/_widget_defaults to
pick LoadVideo vs VHS_LoadVideo by what the destination socket wants,
same as the existing "ref#"/"ref:" sources. Missing predecessor stays
silent; a predecessor that rendered but can't be retrieved is declared
through the same report.unsatisfied -> state.notice path as prevframe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 02:59:22 +03:00
digital-garbageandClaude Sonnet 5 0f6709f5a0 fix: prevframe round 3/4 -- sanitize output filename, surface the declared warning
Round 3 found the extraction's OUTPUT filename was built from the
requesting scene id with no sanitization at all -- unlike the input
side, which round 2 already locked down. A scene_ids-driven request
(bypassing _solo()'s only_scene validation) or a project imported with
a crafted Scene.id could write the extracted frame outside ComfyUI's
TEMP directory. Fixed at the sink: scene_id is now stripped to a safe
character set before it ever reaches a path, regardless of where it
came from, plus a scene_scope membership check as defense in depth.

Also found: the "declared, not silent" warning for a failed extraction
(added in round 1) was returned by the API in report.unsatisfied, but
store.js never read that field -- it reached no one. Now surfaced
through the same state.notice path as the existing "ignored settings"
warning, concatenated rather than overwriting it when both fire in the
same run (round 4 caught the two clobbering each other).

Added a regression test proving the old code would have written outside
tempdir with this exact payload, and that the fix keeps it contained.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 02:47:05 +03:00
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