Commit Graph
1684 Commits
Author SHA1 Message Date
digital-garbageandClaude Sonnet 5 ee94e5851c feat: show per-file upload progress in the media bin
Uploads now go through XHR instead of fetch so byte-level progress is
visible; the drop zone shows a bar, percent, and filename instead of
looking stale while the backend receives the file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:14:58 +03:00
digital-garbageandClaude Sonnet 5 27a82eead1 feat: server-side media thumbnails, cached, instead of the full original file
The media bin's grid always requested the FULL original file for a
thumbnail (an <img>/video-frame decode of the whole thing, only
cropped down visually via CSS/canvas afterward). A 4K photo or a
multi-MB clip got fully downloaded and decoded just to show a 96px
square, every render.

Added GET /api/media/{mid}/thumb: generates a small JPEG on first
request (Pillow resize for images, one ffmpeg frame-grab for video),
caches it under media/thumbnails/, and serves that instead. Deleting
a media item removes its cached thumbnail too. mediabrowser.js now
loads from this endpoint for both image and video kinds -- the same
canvas-drawing path as before, just fed a file that's already small
instead of the original. Measured live: a 1.57MB test clip's cached
thumbnail is 1.6KB.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 18:05:26 +03:00
digital-garbageandClaude Sonnet 5 b301d40592 fix: Generate no longer waits on a separate autosave, and closes the resulting write race
generate()/generateMontage()/generateSelected() awaited a full project
save (and a separate models save) before doing anything visible --
both are plain network round-trips that can retry for tens of seconds
on a flaky connection (_retryOnTunnel), and nothing set a gen state
until they finished. Clicking Generate on bad internet looked exactly
like the app had frozen: no disabled button, no message, no feedback,
for however long those saves took.

The wait existed because /api/projects/{pid}/generate read the
project BY ID from disk (_project_or_404) -- if the disk copy was
stale, Generate would silently build a graph from old data. Fixed at
the actual source instead of papering over the wait: the endpoint now
also accepts the current project inline in the request body (the same
shape /api/projects/{pid}'s own PUT already takes) and persists
exactly that payload before generating from it, atomically, in one
step. The client sends its live in-memory snapshot instead of relying
on whatever the last autosave wrote. body["project"] stays optional
so any other caller keeps the old disk-read behavior.

Adversarial review (two rounds) found this reopened a narrower race:
the generate handler's own existing tail already saved the project a
SECOND time afterward (to record the queued prompt's hash), so an
ambient autosave landing between this request's two internal writes
would get silently clobbered by the second one. Closed by suspending
the client's ambient autosave (suspendSave/resumeSave, an existing
primitive) for the exact span of the generate request, and by waiting
out any autosave that was already in flight before generate started.
Also moved _markGenInFlight to fire right before the actual request
instead of before that wait, so a scene deleted during a brief
already-in-flight wait isn't ghosted for a request that hadn't
actually been sent yet.

Verified live end-to-end: an edit made only in memory (never
autosaved) reached disk through the generate request itself and
survived a concurrent pending edit through the suspend/resume window,
confirmed via direct reads against the running dev server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 17:53:49 +03:00
digital-garbageandClaude Sonnet 5 530a45df2e fix: media bin thumbnails now cache a downscaled bitmap, not full-res
Image thumbnails appended the raw <img> (full-resolution decoded
bitmap held in memory for the card's lifetime, just to be shown at
56-96px via CSS crop). Video thumbnails already snapshotted to a
canvas but sized it at the source's native resolution. Neither ever
downscaled anything -- a bin of real photos/4K clips kept every one
of them fully decoded in memory at once.

Both paths now draw down to a capped 320px long edge before caching
(_thumbScale), same visual result, far smaller resident bitmaps. Also
drops the img.draggable=false workaround the old <img>-based image
thumb needed to stop the browser's native image drag from hijacking
card drag-and-drop -- a <canvas> was never draggable to begin with.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:52:17 +03:00
digital-garbageandClaude Sonnet 5 14ae90c9be fix: explore_first_step candidate scoring crashed on every real GPU run
_select_best_seed called value_fn.forward()/compress() directly on a
candidate's captured prediction with no device handling -- the value
function loads CPU-only (map_location='cpu'), but the candidate tensor
lives on whatever device the model ran on (cuda in production). Every
candidate failed to score on every real GPU generation since this
feature shipped; _select_best_seed always fell back to the unchanged
seed, silently a no-op the entire time despite passing all tests
(the suite is CPU-only, so a mismatch never had two devices to hit).

Every other value_function.py call site (gradient/ascend/search) moves
its input onto next(self.parameters()).device first -- this was the
one path that skipped that. Mirrored it: compress on the candidate's
own device (cheap reduction), then move the small compressed vector
onto the value function's device before scoring.

Added a regression test using a real second torch device (mps on this
box, cuda in CI/production) to reproduce the exact cross-device
failure, and fixed three pre-existing value_fn test doubles that were
missing parameters() and had silently started hitting this same
crash branch once the fix started calling it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:20:18 +03:00
digital-garbageandClaude Sonnet 5 9e3a8278a5 fix: pin the media bin's action row (Clear references etc) while scrolling
Clear references only ever sat above the grid, so clearing refs on a
large bin meant scrolling all the way back to the top first. Made
.media-bin-actions sticky within the bin's scroll container, same
pattern already used for the (currently unused) bin-tabs-sticky class.
z-index bumped to 4 to clear each card's kind badge (z-index 2), which
otherwise ties and wins on DOM order, painting over the sticky bar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:50:11 +03:00
digital-garbageandClaude Sonnet 5 1c59012470 docs: combo sweep hint/placeholder catch up to existing av: syntax
parseComboSweepConfig in store.js already parses an av:penalty segment
(h3_av_decouple attention penalty) but the panel's own hint text and
placeholder never mentioned it -- looked like an undocumented, maybe
broken feature.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:30:41 +03:00
digital-garbageandClaude Sonnet 5 37c9c501bc fix: shadow-negative compose remedy + explore_first_step silent failures
Compose-mode message and Editor tooltip were stating a false remedy
(turn REINS down to passive-capture-only/strength 0 to free blocks) --
REINS claims all 50 blocks the moment it's on at all, strength only
gates injection not installation. Corrected to say only fully
disabling REINS frees any blocks.

_select_best_seed had three silent failure paths (no usable
prediction, a scoring exception, zero candidates scored) that burned
real GPU compute and reported nothing. Added prints to all three.
Added _output_value_fn_sample_count so "not ready yet" messages show
the actual banked sample count instead of leaving the user blind on
how close they are to the threshold.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 15:27:40 +03:00
digital-garbageandClaude Sonnet 5 1ea5bf179e test: output value function snapshot fires on H3 too
Adversarial review of 77cf37e found a real, cheaply-closable test gap: this
file already calls node.sample() end-to-end with a monkeypatched value
function for the explore_first_step tests, so the same pattern (plus
spoofing minimax_h3.is_h3_model) proves the snapshot save is no longer
skipped for H3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 14:49:58 +03:00
digital-garbageandClaude Sonnet 5 77cf37e18d fix: output value function -- train on H3 too, not just LTXAV
explore_first_step reads the same output-space value function
output_guidance trains, but training/snapshotting was gated off for H3
entirely -- a decision made before explore_first_step existed, when
output_guidance (still correctly forced off there) was its only consumer.
Result: the value function could never reach MIN_SAMPLES on H3 no matter how
many generations were rated, so explore_first_step printed "not ready yet"
forever. Un-gate the snapshot save and the training call for H3; leave
output_guidance's own application (and the separate conditioning-payload VF
for embed_guidance/value_guidance) forced off as before -- that decision is
unaffected.

No new automated test: no existing fixture exercises this rating-commit code
path (FunPackVideoRefiner.run's post-scene training block) cheaply enough to
extend honestly; verified by full suite pass (1491/1491) and direct tracing
that no other call site re-introduces the gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 14:43:33 +03:00
digital-garbageandClaude Sonnet 5 68d1657a50 feat: H3 shadow negative -- optional coexistence with REINS/Q-steer
h3_shadow_negative's non-composing install was silently starving REINS/
Q-steer of their own per-block capture, so ratings never trained them while
shadow negative was on (0/0 liked/disliked no matter how much was rated).

Add h3_shadow_negative_compose ("Allow when REINS/Q-steer are active"): when
on, shadow negative skips any block another dit-patch mechanism already
claimed, leaving it to keep capturing/steering there, and only pushes on the
rest. Structural coexistence, not per-block math blending -- disclosed as
such in the docstring and tooltip. Off by default, unchanged behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 14:37:01 +03:00
digital-garbageandClaude Sonnet 5 38bdf9f674 feat: H3 shadow negative -- give the negative prompt a job at CFG 1
Native port of the community "H3 Shadow Negative" pack's Basic scope: runs the
negative CONDITIONING (already threaded through the sampler, otherwise dead
weight since H3's CFG is fixed at 1.0) through H3's DiT blocks as a second
shadow stream and pushes the real stream's attention output away from it,
NAG-style. Non-composing with REINS/Q-steer/av_decouple/block-repeat/
attn_temperature by design -- replaces a block's forward outright and prints
which existing patches it superseded.

Full Editor (Cutting Room) UI exposure lands in this same change, along with
explore_first_step's own missing Editor UI from an earlier session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 13:22:08 +03:00
digital-garbageandClaude Sonnet 5 99c784921d feat: explore_first_step -- branch-then-commit seed search at step 1
Tries N candidate seeds with one throwaway denoising step each, scores
each with the trained output-space value function, commits to the
best-scoring seed for the real full pass. Candidates are scored fully
isolated from the scene's own guidance stack (model_function_wrapper
chain, context_windows, v2a_grad_scale, identity_overlap -- each torn
down and reinstalled around the candidate-scoring window) against the
scene's fully-finalized conditioning. Any failure in the value
function, schedule, or the isolation cycle itself degrades to a
silent no-op, declared in the run report, never a crash.

Also hardens _install_context_windows/_install_v2a_scale to roll back
partial state on failure (a leaked context_handler key was silently
re-enabling windowing for later scenes) and fixes the original
non-explore v2a install call site to degrade like its sibling
mechanisms instead of aborting the render.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 01:00:34 +03:00
digital-garbageandClaude Sonnet 5 3052037519 fix: phrase blend -- position-by-position average, not a mean-vector blur
Corrected the actual design: encode two FULL conditionings ($style
SHORTCUT1 $postfix and $style SHORTCUT2 $postfix) and average them row by
row -- (1-strength)*A + strength*B per position, strength=0.5 giving
exactly (A+B)/2 as originally specified. The prior mean-vector version
collapsed phraseB's whole sentence into one flat direction applied
identically everywhere, discarding its own word-by-word content.

Resamples the shorter side (linear interpolation along the token axis)
when the two shortcuts tokenize to different lengths, so both sides line
up for the average without changing the output's token count.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 23:03:01 +03:00
digital-garbageandClaude Sonnet 5 dde02f5cd7 fix: phrase blend -- shift the whole prompt, not just the phrase's own rows
Confirmed on the rental: the previous version (phrase-scoped shift) showed
zero visible effect even at strength 1.5. Qwen's rows are contextual, so
every word in "a cat sits on a mat" already carries "sits", not just the
verb's own row -- editing 1-4 rows out of the sentence left the rest still
pointing at phraseA's action. Now shifts the whole prompt-token range toward
the alt sentence's own whole-prompt mean.

Multiple [a|b] spans in one prompt are additive: each span's delta is
measured against the original conditioning, not against a prior span's
already-shifted result, so they don't compound order-dependently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:33:00 +03:00
digital-garbageandClaude Sonnet 5 8a6ab12ce6 feat: phrase blend -- per-blend strength override, [a|b:0.8]
Default 0.5 shift left phraseA dominating the video. [phraseA|phraseB:N]
overrides strength per blend. Fixed a shared regex gap along the way: every
weight/strength markup in this file (not just blend) silently rejected
leading-dot decimals like ".8" -- fell through to literal text with no
error, reproducing the exact "override did nothing" bug this feature exists
to fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:05:22 +03:00
digital-garbageandClaude Sonnet 5 1f54ad4519 fix: phrase blend -- an ordinary colon in a shortcut silently broke the whole construct
A real shortcut expanding to "wide shot: character walks" made [a|b]
never match at all, no error, because the previous fix banned ':' and
'@' anywhere in either phrase to stop one dangerous shape (an alt
phrase ending in ":1.5"). Two narrower shape-guards were tried and each
let a different shape through (colon-then-@, an em dash) while still
blocking ordinary prose. Reverted to fully permissive: a markup-shaped
alt phrase just reaches Qwen as literal punctuation in its own separate
re-encode, the same accepted trade-off this file already makes for a
weighted phrase's own stray parentheses -- a quality footnote, not a
leak, confirmed by tracing the encode path and its existing exception
handling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 21:35:37 +03:00
digital-garbageandClaude Sonnet 5 38f059b889 fix: phrase blend -- alt phrase can't smuggle markup, can't double-apply on the wired path
Adversarial review of the previous commit found two real leaks and fixed
them: a colon in an alt phrase reached the real text encoder unstripped
(no bracket survives to re-catch it after substitution), and a wired
conditioning re-blended a second time because the re-tag guard only
checked for tag absence. Also reports per-span placement failures instead
of only counting successes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 21:05:18 +03:00
digital-garbageandClaude Sonnet 5 c6eab10c25 feat: [phraseA|phraseB] blends two H3 actions -- mean-vector shift, both encode paths
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 20:40:07 +03:00
digital-garbageandClaude Opus 5 01c0a2b9c2 feat: H3 phrase probe -- where the picture reads a bracketed phrase, and where hiding it changes the picture
Measurement only, opt-in from Settings > Refinement & Taste. Reading map is free
(attention share on the phrase's keys per block); response map costs one extra
forward per phrase per model call. Phrases are numbered, never quoted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 03:18:34 +03:00
digital-garbageandClaude Opus 5 3cb10d3375 feat: [phrase:1.5] is a weight too -- brackets are the universal prompt markup
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 02:26:13 +03:00
digital-garbageandClaude Opus 5 8b680dc4aa revert: latent sharpness -- visible but bad at every strength on the rental
A latent unsharp on H3's 24-channel latent does not translate to picture
sharpness. Wrong mechanism, not wrong number; removed rather than tuned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 02:19:12 +03:00
digital-garbageandClaude Opus 5 0b4ca1eb46 feat: latent sharpness -- spatial unsharp on the late x0 predictions (KSampler branch)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 02:08:19 +03:00
digital-garbageandClaude Opus 5 bc0da79ffc feat: typed (phrase:1.5) weights reach the H3 attention bias
The parser existed but nothing called it; the syntax went to Qwen as text.
One pass strips both markups so timed and weighted spans share one clean text;
both encode paths carry them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 01:46:26 +03:00
digital-garbageandClaude Opus 5 af5bf69eb0 test: follow the phrase-emphasis log's new wording
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 01:36:11 +03:00
digital-garbageandClaude Opus 5 d6e68a2b79 fix: the phrase-emphasis log counted timed windows as zero spans
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 01:35:41 +03:00
digital-garbageandClaude Opus 5 b3a4af3b39 feat: Studio liked/bad/axis directions fade old ratings the same way
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 01:16:52 +03:00
digital-garbageandClaude Opus 5 84aaf7084d feat: REINS direction fades old ratings (0.9 per rating, read-time)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 01:16:52 +03:00
digital-garbageandClaude Opus 5 5bdd259b52 fix: shortcut autocomplete works inside ( ) and [ ]
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 01:07:11 +03:00
digital-garbageandClaude Opus 5 9a3ea8c35c fix: timed phrases reach the sampler when a wired conditioning owns the prompt
The editor strips the brackets before the encoding node sees them and hands the
windows to Studio as char spans on the text it gave that node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:58:02 +03:00
digital-garbageandClaude Opus 5 25e5cc8de0 feat: timed phrases use square brackets so shortcut parentheses can sit inside
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:49:16 +03:00
digital-garbageandClaude Opus 5 aa4efe3248 fix: timed phrases may contain colons and @; only :number before the window is a weight
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:43:26 +03:00
digital-garbageandClaude Opus 5 64a6b93713 fix: rating from the action bar no longer pops the hover-timeline open
A hold keeps the timeline open; it does not open it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:36:05 +03:00
digital-garbageandClaude Opus 5 1a1ca7a31e feat: H3 timed phrases -- (phrase@t0-t1) shapes the video only inside its window
Query rows are attended in chunks split at window edges, each with its own key
bias: exact, no SxS mask, no extra model call. Outside the window the phrase is
masked from video rows (H3-World single-egress); audio and text rows never see a
window. Composer lists the windows under the prompt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:34:24 +03:00
digital-garbageandClaude Sonnet 5 5284cc8229 feat: move scene rating to the top action bar, rate without selecting on a single scene
The rate-this-render button lived at the far end of the bottom clip-editing
toolbar and required an explicit clip selection. Moved it next to Auto
Montage in the top action bar, and when the project has exactly one scene
it now resolves that scene automatically instead of requiring a click-select
first (with 2+ scenes, selection is still required -- no ambiguity to
resolve without one).

The rating block used to live in the timeline zone, whose fingerprint
already tracked selection/renders/ratings; moving it into the actionbar
zone needed those added to fpActionbar too, plus two more the button reads
indirectly: usesFunpackStudio() depends on project.conditioning_slot (not
previously tracked anywhere in this zone), and S.isGenerativeScene() can
flip on a video-clip conversion without the scene's rating changing.
Verified live (not just read) that toggling conditioning_slot off and back
onto "funpack" now correctly hides/reshows the button.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 23:36:04 +03:00
digital-garbageandClaude Sonnet 5 0f77d937e5 docs: confirm the compiler-disable scope is per-call, not session-wide
Confirmed on a real rental: the ~20s a REINS-injecting/block-repeat
generation now takes to start (no aimdo dynamic-VRAM streaming for that one
call) does not carry over to the next generation -- every other generation
this session keeps the compiler's normal speed. No behavior change; this
documents that the per-call scope (not a session-wide sticky disable, which
was drafted and reverted while chasing an initial guess about the 20s cause)
is the right one to keep.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 23:17:21 +03:00
digital-garbageandClaude Sonnet 5 54e49884a4 feat: allow H3 attention temperature to sharpen, not just flatten
h3_explore_temperature was positive-only (flattens the softmax -- more
exploratory, pulled away from the prompt's own weighting). Same knob now
also takes negative values (widget min -0.9) to do the opposite: sharpen
attention so it commits harder to what the prompt already made confident,
instead of only ever being pulled away from it. Same code path (temperature
= 1 + strength, dividing Q before the dot product), sign decides direction.

Floored at 0.05 so an out-of-widget-range strength can't drive temperature
to zero or negative. Fixed a NaN gap the floor doesn't cover: `!= 0.0`
(needed so negative values install at all) is also true for NaN, and
max(1+nan, 0.05) is nan, not 0.05 -- excluded explicitly.

Editor UI widget (engine_settings.js) updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 23:14:28 +03:00
digital-garbageandClaude Sonnet 5 c929004573 fix: disable Comfy's model compiler around REINS-injecting/block-repeat forward calls
aimdo's malloc-graph compiler assumes every block-scope forward allocates the
same pattern call to call. REINS while actually injecting (not passive
capture) and block repeat both patch what a block's forward does, which
crashed on a real rental with "aimdo memory compile error" -- confirmed via
instance log: identical workflow succeeds in passive-capture-only mode
(injection branch never entered) and fails the moment strength > 0. This is
a known upstream conflict class (Comfy-Org/comfy-aimdo #16144, #16342), not
specific to REINS; their fix is --disable-comfy-compiler, read live per
malloc-graph scope rather than cached at startup, so toggling it in Python
around just the affected sample_custom call works without touching the
(templated, rental-fixed) launch command.

Gated on actual install success (model identity before/after each
installer), not on the raw request flags -- a silently-refused install
(non-contiguous span, no block list, zero strength) leaves a block's
forward genuinely unpatched, so forcing the compiler off there would be a
pure, needless perf/VRAM regression instead of a fix.

Also: FUNPACK_AIMDO_LOG env var to raise comfy_aimdo's own native log level
to DEBUG, for diagnosing this class of failure directly from aimdo's side
when needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 23:08:25 +03:00
digital-garbageandClaude Sonnet 5 4960fd2178 test: cover the real-direction cache path round 3 of review found was untested
The previous caching test only exercised the zero-direction torch.zeros()
fallback -- round 1's actual bug lived in the real-direction branch
(direction.to(out.dtype).to(out.device)), which a torch.zeros count can't
see at all. Adds a second test using a deliberately non-matching dtype
(float64 direction against a float32 harness) so .to() is never a
coincidental no-op, then counts .to() calls on the direction tensor itself.

Verified by mutation: temporarily reverted dir_t_cache to round-1's buggy
shape (real-direction path uncached) and confirmed this new test fails
(3 calls instead of 1) while the old zero-direction-only test still passes
-- proving the old test alone would have let that exact regression through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 19:51:16 +03:00
digital-garbageandClaude Sonnet 5 54706eebe6 test: cover the dir_t_cache reuse round 2 of review flagged as untested
The caching logic itself (a block's second+ call reusing the built
direction/zero tensor instead of reallocating) had no regression test --
only the single-call math was covered. Counts torch.zeros calls across
three calls to the same block; only the first call's mask-cache-miss and
dir_t-cache-miss should allocate, the rest should be pure cache hits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 19:47:03 +03:00
digital-garbageandClaude Sonnet 5 04f66dbc48 fix: cache the cast direction too, not just the zero fallback
Adversarial review caught this: zero_dir_cache only cached the zero-vector
path. The real-steered block's direction.to(out.dtype) ran fresh every call
with no caching -- a genuine allocation every time, since direction is
always float32 (h3_repr_steering.capture()) while H3 runs bf16/fp16, so the
cast is never a no-op. From step 2 of a scene onward that put the steered
block back to allocating one more tensor per call than the other 49 --
exactly the per-call allocation-count asymmetry the uniform-clone commit
was meant to close, just narrowed rather than eliminated.

Renamed zero_dir_cache -> dir_t_cache and cache the real cast-direction
result the same way the zero fallback already was: built once per
(dtype, device) combo, reused every later call for that block.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 19:42:35 +03:00
digital-garbageandClaude Sonnet 5 3f459038ab fix: make REINS clone/inject on every candidate block, not just the steered one
Real crash report from the rental, not speculation this time: "aimdo memory
compile error" inside comfy_aimdo's malloc_graph (comfy/model_prefetch.py's
prefetch_queue_pop(..., malloc_scope="block")), dying before the first
denoising step completes. That scope string is passed identically for every
one of H3's 50 blocks within a single forward pass -- aimdo records one
allocation sequence across all 50 block-scope entries per pass and breaks
when that sequence isn't uniform block to block.

REINS's hook only ever cloned+indexed+injected on the block(s) actually
being steered (direction is not None), typically 1 of 50 -- the other 49
did nothing extra. That's a block-to-block allocation-shape mismatch within
a single pass, present from the very first forward call, which matches
"crashes before step 1" better than the cross-step timing race this file's
last several commits were fixing.

Now every candidate block runs the identical clone -> index -> torch.where
sequence whenever REINS is steering anywhere, using a zero direction vector
for blocks with no learned direction (or not enough rated data) so the math
is a genuine no-op (bump=0) while the allocation shape stays uniform. Real,
accepted cost: up to 50 full clones of the block's hidden state per step
instead of ~1, whenever REINS is active -- the user explicitly chose this
over cheaper, less certain options after the first several attempts (host
sync removal, unconditional single-block clone) didn't fix the actual
crash.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 19:38:43 +03:00
digital-garbageandClaude Sonnet 5 8f98f46dac fix: cache a real .any(), not an unenforced construction guarantee
Round 3 of adversarial review, executed and confirmed live: the "mask is
not None implies non-empty" reasoning behind has_rows=True was false.
_mask_from_mod_segments' scalar branch sets found=True without checking the
segment has nonzero length, so a zero-length scalar-tagged segment produces
an all-False mask that is not None -- has_rows=True would then make
capture() silently return an all-NaN descriptor instead of None, poisoning
that block's learned direction() on disk with no error anywhere. No
reachable path into a zero-length segment was found in this repo's own code
(the real video segment is never zero-length; keyframe/ref segments come
from real encoded latents here), so this doesn't fire today, but the
invariant it depended on isn't enforced or asserted anywhere, so a future
upstream ComfyUI change could trip it silently.

Fixed the right way, which was available at the same cost av_decouple
already pays (h3_repr_steering.capture()'s has_rows param exists for
exactly this): cache a REAL bool(mask.any()) once per unique seq_len,
alongside the mask itself, in both h3_repr_steering's hook and
h3_q_steering's override -- same single-sync-per-generation cost class as
av_decouple's has_v/has_a/has_other, zero per-block/per-step syncs, and
correct regardless of what video_mask_from_mod_segments actually guarantees.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 18:55:41 +03:00
digital-garbageandClaude Sonnet 5 d50c33aff0 fix: actually eliminate capture()'s per-block host sync, not just re-add .clone()
Round 2 of adversarial review: my earlier "fix" still called bool(mask.any())
inside h3_repr_steering.capture() and h3_q_steering's _override on every one
of the 50 blocks, every step -- despite the mask itself being cached per
seq_len, the .any() reduction was recomputed and host-synced every call. The
av_decouple mechanism already had the right pattern (cache .any() once per
seq_len) and its own comment falsely claimed capture() had been fixed the
same way; it hadn't.

Both real callers (h3_repr_steering's hook, h3_q_steering's override) build
their mask via video_mask_from_mod_segments, which by construction only ever
returns non-None when it found at least one True row -- so both can pass
has_rows=True and skip capture()'s internal check entirely, with zero syncs
in the hot path. capture()'s own default (has_rows=None) still self-checks
for any other/future/test caller that can't make that guarantee.

Also dropped the .cpu() added in the first commit: a 50-descriptor-per-step
[hidden_size] vector is a few hundred KB, not a real VRAM pressure source,
and forcing that transfer synchronously was itself a sync in the exact loop
this whole fix exists to keep sync-free. Reverted to leaving it on GPU
(original behavior) -- torch.save/load already handles device-tagged tensors
fine downstream.

q_steering's unconditional q.clone() was flagged this round as likely pure
waste under ComfyUI's single-owner attention-tensor-container contract, but
left in place: verifying that contract myself with confidence is out of
scope here, and the cost of being wrong (reintroducing the aliasing bug just
fixed for h3_repr_steering's out) outweighs the small clone it would save.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 18:50:37 +03:00
digital-garbageandClaude Sonnet 5 7598769ac7 fix: restore h3_repr_steering's clone -- out can alias the block loop's own tensor
Adversarial review round 1 caught this: out is not provably this hook's own
private tensor. A chained h3_block_repeat span_loop passthrough returns
args["img"] verbatim with no copy for non-head blocks in its span, and
h3_repr_steering installs as the OUTER wrapper around it (_install_block_
influence then _install_h3_repr_steering in _sample_chunk) -- so `out` here
can be the H3 model loop's own live `h`. Removing the clone entirely (as the
previous commit did) reintroduced exactly the aliasing hazard block_repeat's
own _repeat() explicitly guards against one function away. Restored the
clone, kept unconditional (not gated behind the safe/unsafe branch) so it
still doesn't need the host sync this fix exists to remove.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 18:42:05 +03:00
digital-garbageandClaude Sonnet 5 f5729a96df fix: stop REINS/q-steering/av-decouple forcing host syncs in the H3 block loop
comfy_aimdo's dynamic-VRAM block prefetch pipelines weight cast/unpin against
CUDA stream timing; a CPU sync (bool()/item() of a CUDA tensor) or a surprise
allocation mid-loop can race it into reclaiming a weight buffer a still-queued
kernel needs -- a silent segfault, no Python exception possible. All three H3
rating-driven dit hooks did this on every block, every step:

- h3_repr_steering's injection guard and h3_q_steering's injection guard both
  did `if torch.isfinite(x) and x > 0:` (forces a host sync) before writing
  the steered rows, and h3_repr_steering cloned the whole block output first.
  Replaced with a GPU-only torch.where guard; dropped the now-unnecessary
  clone in h3_repr_steering (out is this hook's own private tensor already).
- h3_repr_steering.capture() now .cpu()s its descriptor immediately instead
  of leaving it on GPU competing for VRAM headroom for the rest of the run.
- h3_av_decouple's attention override re-checked `.any()` on its video/audio/
  other masks every single attention call; now decided once per unique
  seq_len alongside the mask itself (typically once per generation).

Unvalidated against a real GPU (none available here) -- worth confirming on
the rental that REINS no longer crashes silently under dynamic VRAM offload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 18:34:50 +03:00
digital-garbageandClaude Sonnet 5 c70e16cde8 feat: numbered "Reference video N" slot for widget-level references, plus a review chain of fixes
The direct "ref:<id>" picker for a widget-level reference (VHS_LoadVideo's
"video", etc.) forced reselecting the field by hand every time the
underlying reference file changed. Adds the same numbered indirection
Input Sources already gives sockets: "Reference video 1" is whatever is
marked first among video references, not a specific file, so re-marking
in the Media Bin re-points the widget with no node-config change.

Adversarial review of this addition, run to convergence, found and fixed
in order:
- a real installed file whose name collided with the sentinel pattern
  ("ref#video:1", "ref:whatever") could be silently hijacked instead of
  used as-is -- fixed by having a literal installed choice always win,
  identically in the builder and the picker;
- the self-heal fallback for a stale plain value could reach into
  reference-slot space and silently attach a live Media Bin dependency
  the user never picked;
- fixing that reopened the underlying onChange(undefined) hole the
  fallback chain had been papering over, wiping a stale plain value
  outright when nothing was installed -- restored the original
  choices-only self-heal invariant explicitly;
- a reference-capable widget could be added to a Linked Inputs group,
  whose own value editor builds a spec with no ref_kind -- silently
  wiping the shared reference across every member on next render.
  Simplest safe fix: that kind of field doesn't link.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 16:44:00 +03:00
digital-garbageandClaude Sonnet 5 516660bbb1 fix: hide the raw-filename duplicate of a Media Bin reference in widget pickers
A reference's file lives in ComfyUI's own input folder (so the node can
load it), which means it was also showing up a second time in the
combo's plain choice list under its on-disk name
(funpack_movie_<id>.mp4) -- indistinguishable from any other reference,
right next to the friendly "R1 - name" entry for the same file. Drop
the raw duplicate; the friendly one is always the better pick.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 16:23:43 +03:00
digital-garbageandClaude Sonnet 5 e17482e2b5 fix: don't silently overwrite an unresolved widget reference, report which kind it is
Adversarial review of the previous commit found two real gaps in the
"reference into a file-upload widget" feature: unmarking a wired
reference in the Media Bin silently rewrote and persisted a plain
default over it on the node panel's next render (the exact silent
substitution the feature was meant to avoid, just via a path the
builder's own report never sees), and a wrong-kind reference reported
the same "no longer in the media bin" message as a genuinely missing
one, sending the user to check the wrong thing.

A second pass on the fix itself found the new "is this a reference"
check wasn't scoped to reference-capable fields, so an ordinary combo
with an installed file literally named "ref:..." would misreport.
Scoped it to spec.ref_kind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 16:16:43 +03:00
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