Commit History
Today
-
Remove the SeamAB/SeamFix lab workflows and old beta/handoff notes
Sandpies04e3fdc -
build_notes: the Starter's one READ ME card is the source; keeps card ids; board opens in view
Sandpiesff80a37 -
Starter: READ ME links the three custom node packs the graph needs
Sandpies41de115 -
Starter: READ ME links every model file the graph loads, with its folder
Sandpies3737ef9 -
Hand-tie v3 (Previs Motion-Context, chained spine, audio floor, solo previs reuse), 00138 example & interactive demo
Sandpies46ee5aa
Yesterday
-
Video Upsampler node runs the chain's re-draw; standalone pack 0.2.0
SandpiesHTCVideoUpsampler drops steps/start_sigma/tile_overlap and calls upsample() exactly as upsample_chain does (2 steps on the denoise schedule, 192 px overlap, no detail merge, AdaLN port on any model). check_video_upsampler pins the node to the chain's values. Pack rebuilt and published as 0.2.0.3f935ab
2 days ago
-
feat: built-in Motion-Context, Refine v2.4, hand_tie v2.6, guided SWAP, upsampler & WRITE fixes
Sandpies- Built-in _HTCMotionContext (zero external ComfyUI-H3-Motion-Context dependency, credited to NikoDemon80) - Refine v2.4 (_pin_inpaint_head, _calibrate_overlap_latent, _bridge_motion_seam, audio_refine [99..104]) - hand_tie v2.6 (Qwen3-VL previs compiler, cond-only mod5 phase alignment, ViT3DDecoder warm-up, post-deflicker seam_feather) - Standalone guided SWAP (swap_script [97], swap_refs [98], guide_latent_only, dual Swap LoRAs, SLA ref pinning) - Tiled Upsampler schedule & VRAM fix (start_sigma=0.0, detail_sigma=0.0, H3AdaLNLoRAFix, TE offload) - WRITE, Music Video (Song/Lay out), SWAP & UI fixes (scenePrompt, Edit shots, write_propose JSON contract, .h3e-hopbar, Vue Nodes 2.0)2f7f1e4
4 days ago
-
Hand-tie: ht_seam_fix v2 -- 24-frame tail discard (bright/griddy last 22 frames never delivered or handed on), tone to clean frames, mean anchor 0.3 off the carried overlap, sound draft continues the carried overlap audio (frozen), line at 1.3 s, no-line hops reject invented speech, early drafts keep the latest start, picture-only Motion-Context (audio keyframe stripped); SeamFix_v2 workflow
Sandpiesf69fbce -
Hand-tie: ht_seam_fix (off by default) -- sound lock carries the previous hop's audio over the overlap, draft opens on the previous hop's tail, Whisper word-timing check on GPU, no previs end guide on prev_hop hops, prev-hop tone match, per-block-position latent mean anchor to hop 1; one-hop chain skips the previs; SeamFix_all_on workflow
Sandpiesee2b299 -
Hand-tie: seam A/B workflow pair (prev_hop vs previs, Storyboard4, seed 5150, 8 steps)
Sandpiesba8cae2 -
Hand-tie: ht_seam_source -- a continuing hop opens on the previous hop's latent (prev_hop), previs head guide kept behind the switch
Sandpiesba78f5a
11 days ago
-
Remove HTCSeamReport, HTCContinuityState and the soundtrack / continuity_state sockets
Sandpies +1: Claude Opus 5.5 (1M context)The seam report read every frame of the film and stalled long chains; judge joins by eye. HTCContinuityState could not express two people and duplicated the register (ref_plan covers identity). The wired soundtrack AUDIO socket is covered by soundtrack_file. Starter loses its seam-report branch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>750b0fe
12 days ago
-
H3 Video Upsampler node, upsample tile budget; split_finish_aug removed
Sandpies +1: Claude Opus 5.5 (1M context)HTCVideoUpsampler ("H3 Video Upsampler (finished clip)"): the chain's upsampler as one standalone node for a finished clip. Frames (+ optional soundtrack and audio VAE) in; it fits them to 32 px, pads to the next 17g+5 length, encodes, conditions on `prompt` (default: the h3upscale caption) as Ref2VA with no refs, runs tiled_upsampler.upsample -- an h3upscale guide LoRA detected from its header gets its guide, as on the chain -- decodes and trims. The soundtrack comes back untouched. Example: workflows/ HandTieClips_Upsample_Video.json (plain UNETLoader as the model), built by tools/build_video_upsample_workflow.py. The tile budget is now a switch: `budget` threaded through upsample_chain / upsample / redraw_chunk / tile_grid, defaulting to the recipe's 70000 so the MMH3 identity holds. The node exposes it as tile_tokens; the chain as upsample_tile_tokens, the last widget. Lower = more, smaller tiles, less VRAM. Delivery only, in no hop key. split_finish_aug is removed: at 0.85 the l…458f618
13 days ago
-
hop_refine=split: the refine as the hop's own finish; split_finish_aug
Sandpies +1: Claude Opus 5.5 (1M context)hop_refine gains `split` (appended to the combo). No second pass: the hop samples its own schedule to the start of its last K = refine_steps steps (0 -> 2), then finishes those K steps, each halved, from the same latent with DisableNoise -- on refine_model (sigma-shifted like the hops) or the MODEL wire, with refine_sampler. Both stages carry the hop's conditioning, the Motion-Context pin included, which is why it continues the way a boosted hop does. One latent is decoded, cached and pinned; there is no raw/refined pair to blend. At 8 steps K=2 is 10 model calls. Refused beside realism_boost, which already splits. refine_denoise / scheduler / cond / audio / blend / head / align do not apply, and the queue says so. Its own hop-key field. First GPU A/B (chain_00097 split, finish on the bare Singularity base, vs 00098 off; 3 x 8 s, turbo, boost off): joins identical (x2.14 vs x2.16), hop 1 -> 3 face mid band +11.8% vs +16.8%, shirt -0.3% vs +4.0%; by eye "split looks a little less textu…469f6cb
14 days ago
-
Upsample: guided re-draw with the h3upscale LoRA, upsample_lora, upsample_dit
Sandpies +1: Claude Opus 5.5 (1M context)upsample_lora / upsample_lora_strength (appended last) stack a LoRA on the upsample re-draw model only. When that LoRA's header says it is an h3upscale guide LoRA (reference_downscale_factor > 1, target_crop_v2 / target_grid_stride_v1), the SAME one re-draw step also carries its guide: each chunk's canvas frames at 1/factor of the output, packed on every factor-th patch of each tile, under the LoRA's training caption instead of the hop prompt. No extra steps; the recipe's numbers do not move. Output and tiles snap to 32 x factor and the guide's rows count against the token budget. Without a guide LoRA the recipe path is unchanged and still matches MMH3 exactly (check_tiled_upsampler). upscale_guide.py is HTC's own: the header reader, the guide pixel prep, and the PackedLayout patch that lets core build strided keyframe rows (same marker as the HTC-Lab prototype, so only one installs it). The guide encode pads each chunk to a whole 17-frame clip and keeps exactly its steps: H3's VAE dr…f103f03
17 days ago
-
Chain node: upsample controls, prompt wrap, fourth boost LoRA
Sandpies +1: Claude Opus 5.5 (1M context)Upsample - Under realism_boost the re-draw runs on the boost model, not the MODEL wire. - upsample_denoise (default 0.1). The recipe's 0.2 starts the re-draw at sigma 0.75 under shift 12 and repaints skin texture; 0.1 textured least by eye, 0.075 smeared mid-distance faces. 0 skips the re-draw. - upsample_model picks the latent upscaler file. h3_upscaler_lms_v0.1 (Alissonerdx, the LBH network fine-tuned on the LMS sharpness data) loads as-is. - The HTCTiledUpsampler side node keeps the recipe's 0.2 and stays identical to MMH3; check_tiled_upsampler passes. prompt_prefix / prompt_suffix wrap every hop's finished prompt, outside everything the chain adds, so a LoRA trigger is the first thing the encoder reads. Empty leaves every block byte-identical. boost_lora_4 joins the realism boost. It enters the hop key only when set, so boosted hops cached before it existed still hit. All new widgets are appended last; both shipped workflows carry the new slots. Co-Authored-By: Cla…6b173d9
20 days ago
-
Chain node: upsample the finished film with the tiled upsampler recipe
Sandpies +1: Claude Opus 5.5 (1M context)Two widgets, appended last: `upsample` (off/on, default off) and `upsample_megapixels` (2). With it on, after the last hop the recipe runs ONCE over the whole film -- not per hop, which would put a fresh texture step at every join; over the joined film the recipe's own 17-frame chunk crossfades run across the joins. Delivery only. Every pin has been handed on before it starts, it is in no hop key, and changing the size on a cached chain re-runs only the upsample. Each hop's delivered sampler latent is kept (under pin_only, the delivered one, not the refined pin) and stitched on the latent grid; tiled_upsampler .stitch refuses an overlap that is not whole latent steps at the running VAE phase rather than slipping the timing. A restart splits the film into separate runs. Each chunk is re-drawn with the conditioning of the hop at its middle; a cache-hit hop re-encodes its Ref2VA conditioning for this. With tone correction on, each upsampled frame's level is matched to the corrected canva…8419c65 -
Remove the realism boost's latent upscale
Sandpies +1: Claude Opus 5.5 (1M context)Reverts e2d30cf: the boost_upscale / boost_upscale_by widgets, the two-canvas guide build (_pin_on_canvas), latent_upscale.py, the HTCLatentUpscale side node, its THIRD_PARTY_NOTICES entry and the README section. It served no purpose for how the pack is used. The boost renders at the canvas again. Never released (landed after 2.2.0), so no published workflow uses it. The two widgets were the last appended, so a saved graph only carries two unused trailing values. The hop key only included the upscale when it ran, so chains rendered with it off keep their cached hops. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>b8605ba
21 days ago
-
Shipped workflows: keep the turbo LoRA through a load
Sandpies +1: Claude Opus 5.5 (1M context)PlagueKind's LTX_lora_loader rebuilds its stack in configure() from node.properties.stack_data and ignores the saved widget value. Both shipped workflows stored the stack only in widgets_values, so on load it came back empty: the turbo LoRA silently dropped and every hop sampled 8-10 steps on the base model ("0 patches attached" in the log, stack_data '[]' in the queued prompt). Found because a bench built from the Starter flickered on its camera-move hop. check_workflows now fails a workflow whose stack does not survive a load. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>d2c632d -
Realism boost: the recipe's latent upscale, on our own runner
Sandpies +1: Claude Opus 5.5 (1M context)boost_upscale / boost_upscale_by (None / 1.25, appended last): stage 1's denoised video is upscaled in latent space before the re-noise, where The AI Brief's graph has its Optional Latent Upscaler. Nothing else in the recipe moves. It serves the low-VRAM route: 0.5 MP canvas, base 3 with turbo, stage 3 at 1.25x. In a chain the delivered video, the pin and the cache are at the upscaled size; the Ref2VA encode stays at the canvas. H3 keyframes sit on the target's grid and Motion-Context refuses a pin of another size, so each hop builds its guides twice: stage 1 on the canvas with the previous hop area-shrunk, stage 3 at full size. Keyed only when it runs. latent_upscale.py runs LBH-123-AI's weights (MIT code, Apache-2.0 weights) with HTC's own runner: layout and latent statistics are theirs, float32 output identical to their module on both installed checkpoints; memory via free_memory, whole-clip inference when it fits, safetensors only. Also as the side node HTCLatentUpscale. THIRD_PA…e2d30cf
22 days ago
-
Realism boost: a third LoRA slot
Sandpies +1: Claude Opus 5.5 (1M context)boost_lora_3 / boost_lora_3_strength, appended last so saved workflows keep their indices. Ships None, so a boosted graph renders as before. README now says the boost LoRAs go on top of the MODEL wire's own stack, turbo included, as in the original recipe. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>9ecff38 -
READ ME cards: link the two realism boost LoRAs
Sandpies +1: Claude Opus 5.5 (1M context)LMS (Alissonerdx) and Realism People (fal), both on HuggingFace. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>f6c0fa8 -
Shipped workflows: one short READ ME card instead of eight
Sandpies +1: Claude Opus 5.5 (1M context)The Starter carried eight markdown cards (~18,000 characters) and the Showcase one 4,600-character note, restating what PROMPTING.md covers. Each now has one card: links to GitHub and HuggingFace, the few settings to check before queueing, the MODEL chain in its current order (the old Showcase note still showed SLA near the end), the dialogue rule, and the realism boost LoRAs. The Showcase keeps its three required pictures. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>864c8e7
25 days ago
-
realism_boost: Singularity dual sampling inside the hop sampler
Sandpies +1: Claude Opus 5.5The AI Brief's recipe, per hop, with no extra nodes on the graph: the schedule is doubled (ExtendIntermediateSigmas 2) and split after its first base step. Two steps on the MODEL wire set the composition; the video is re-noised from that stage's denoised x0 at the shared hand-off sigma, audio carries on from its output, and the boost model (same model + two boost LoRAs, AdaLN-ported when H3AdaLNLoRAFix is present) paints the rest under DisableNoise. Base 6 = 2 + 10, hand-off ~0.984. Five widgets appended last; off keys byte-identically. Refused with hop_refine and master_audio_file. tools/check_boost.py pins the schedule and hand-off arithmetic offline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>0dd48de
27 days ago
-
2.2.0: refine_align, refine_blend=auto, and SLA at the front of the wire
Sandpies +1: Claude Opus 5refine_align picks the sigmas the refine pass re-runs. The published refine_denoise=0.50 + refine_steps=2 pair was a schedule hard-coded to a 4-step base, whatever the hop actually sampled; hop_tail re-runs the hop's own last sigmas instead. The default stays denoise, so a 2.1 graph reloads on the schedule it rendered under. refine_blend=auto derives the blend ramp from this run's overlap instead of hard-coding 22 frames, and resolves to a literal before the hop key is built, so auto at the shipped overlap keys byte-identically and cached hops still hit. SPEED_MODES is one row. A preset may move a field only if the field is a property of the checkpoint; sampler, scheduler and step count are community fashion and a stamping preset would overwrite a deliberate choice. SLA moves to the front of the MODEL wire in both shipped workflows, pinned by tools/check_workflows.py. That changes the keys _model_fingerprint hashes, so the first run after updating re-renders every hop even with cach…4bf5daf
-
Credit PlagueKind in H3 Cache node header and Starter note.
Sandpiesbda5248 -
2.1.0: refine pass, H3 Cache, and a prompt-pack trust boundary
SandpiesHop refine ships off with frozen defaults; silveroxides block cache is vendored with notices; new widgets are appended so 2.0 graphs render identically. CLAUDE.md and .claude/ stay local and no longer ship.f131421
-
Let the voice reference ride every hop that speaks
Sandpies +1: Claude Opus 5`voice_file` 1-3 were cited as <Audio 1..3> on hop 1 and nowhere else whenever a shot plan was in play, because a shot plan forces `hop_script=next`. Later hops inherited the timbre only through the audio pin, which drifts, so a chain came out with one voice in shot 1 and a stranger in shot 2. Users have reported this as the pack's most visible defect; `master_audio_file` answers a different question (bring your own take), and nothing answered this one. `voice_every_hop` (RUN, default `off`): - `off` is 2.0.0 exactly. Byte-identical keys, byte-identical renders. - `speaking` rides every hop whose block carries a spoken line and skips the quiet ones. This is the setting to use. - `on` rides every hop regardless. `speaking` is not a hedge. The gate existed for a measured failure -- chain_00038 put a 1.35 s male take into the last second of a hop while the written line still followed the woman's face -- and that failure needs a QUIET hop, because what an uncited timbre clip fills is …bb3f9fa
-
The canvas boards still described the 1.0 node
Sandpies +1: Claude Opus 5Both shipped workflows carry their documentation on their own canvas, and neither had been revisited since 1.0. The laws on them are unchanged and still correct -- what was missing is most of what this release is. Starter, seven cards to eight: - **New card, "New in 2.0".** Master audio, `anchor: "restart"` paired with `last_frame_guide=before_restart`, `tone_anchor_ref=still` with the measurement that motivates it, and SWAP -- including the two things the GPU tests established: do not describe the clip on MEDIA before a swap (that caption reaches the encoder and asks for the person being replaced), and drop the clip to about 0.3 MP, because a clip's decode area is its token count and its token count is its influence. - **"References and @tags"** gains per-shot `refs`, which overrides every `shots` list for one hop -- and says that `[]` is what a restart hop usually wants, so the photograph supplies the room instead of a portrait being put back in front of it. - **"…944cb85 -
Add last_frame_guide: opt-in last-pixel AddGuide
Sandpies +1: GrokDefault off is byte-identical and omitted from the hop key. still pins start_image at AddGuide frame_idx=-1 (last PIXEL frame, not latent T-1 -- FRAME_PER_TOKEN is 1,4,4,4,4). Does not become the next hop's frame 0. Queue-fails without a start image. DiT treating the pin as a bound is untested (GPU). Co-Authored-By: Grok <noreply@x.ai>7b313c2 -
Add master_audio_file: lock every hop to one take
Sandpies +1: GrokEmpty default is off and does not move cache keys. Window function is 0-based, pure, and table-checked for 8 s and 15 s hops. noise_mask is 1 on video / 0 on audio, asserted. Delivered audio is a passthrough. Beat transcript is not auto-cut -- unmatched <d> can still pull the mouth. Sampler honouring NestedTensor noise_mask is untested (GPU). Co-Authored-By: Grok <noreply@x.ai>3721260 -
Merge media-3x3 into v2
Sandpies +1: GrokExtra video/voice slots appended AFTER pin_mech and tone_anchor_ref so existing v2 workflows keep those two values. Workflows updated with 12 empty trailing widgets. Incoming DEVLOG as §55. _dense_media kept; pin_mech still takes mode=. Co-Authored-By: Grok <noreply@x.ai>5ab0db5 -
Merge cache-key-batch into v2
Sandpies +1: GrokKeep both hop-key fields (tone from trunk, pin_qwen from the branch). pin_qwen uses hop_is_start, not i>0, so a restart is keyed like hop 1. check_cache_keys.py added to check_all next to check_core_calls. Incoming DEVLOG kept as §50. Co-Authored-By: Grok <noreply@x.ai>6150e60 -
Anchor on the photograph, and let a restart actually be a chain start
Sandpies +1: Claude Opus 5Two changes, landed together because the second was found while wiring the first and they meet in the same cache key. tone_anchor_ref=still points the anchor at start_image instead of hop 1. The ten-run study measured hop 1 already missing the still before any relay (chroma 30 against 33.6, b* 22 against 26.6), so a chain held on hop 1 was converging on the first casualty. `still` also pulls hop 1 itself, which is the only way that gap ever closes. Default is unchanged. Hop 1 and restart hops now take the correction from frame 0 (ramp=0). The ramp protects a join; neither has one, and ramping them fades into the look over the two seconds a viewer is deciding what the shot is. anchor=restart was being assembled as a CONTINUATION. `hop_restart` was computed beside the sampler, two hundred lines after the code that decides which references ride a hop, whether the previous frame is pinned as <Picture 1>, and whether the prompt says "opens already in progress". So a restart hop got the p…0365453
-
Three reference videos and three voices, and the clips keep their sound
Sandpies +1: Claude Opus 5H3 takes 9 reference images, 3 reference videos and 3 standalone reference audios (comfy_extras/nodes_minimax_h3.py:265-280). The pack matched the 9 and hardcoded exactly one of each of the others, two lines apart: base_videos = {"ref_video_1": reference_video} ref_audios = {"ref_audio_1": voice} So two whole channels ran at a third of capacity. This adds slots 2 and 3 for both -- twelve widgets, APPENDED, because widgets_values is positional. Numbering is DENSE. Core numbers reference blocks by iteration order and the prompt cites those ordinals, so filling slots 1 and 3 must produce <Video 1> and <Video 2>, never a gap -- otherwise a beat naming "the second clip" cites something else and nothing reports it. _dense_media() does that and is tested. ref_video_audios was never passed at all, so a reference clip reached the model SILENT even when the file had a soundtrack. Each clip's own audio is now decoded and paired to the same ordinal, trimmed to the same window as its p…44f76f4 -
Add pin_mech: choose the hop-2+ pin mechanism
Sandpies +1: Claude Opus 5Detection already existed -- _motion_context_cls() resolves the upstream class and rejects incompatible forks, run() logs the path per hop, the preview shows it. Only the choice was missing, which made one question unanswerable: does the AddGuide pixel path ratchet less than Motion-Context? Its VAE round trip is itself a projection and may scrub the latent structure that accumulates, at the cost of the join quality Motion-Context was chosen for. Asking that used to mean uninstalling a pack. pin_mech is auto / motion_context / addguide, default auto = current behaviour. The override is applied in BOTH _pin_mech_for and _pin_continue. The predictor exists to put the mechanism in the per-hop cache key before the pin runs, and its docstring already says every condition mirrors the executor. If the two disagree the key stops describing what is on disk -- the failure that made cache_hops=on worse than off before the latent sidecar landed. Both now carry a comment saying not to change one w…9672605 -
Close two more bindings the fingerprint could not see
Sandpies +1: Claude Opus 5Review of 5298a45 found the object fix covered one of four shapes a node can use to hand a configured callable to the model. Both gaps were verified against the real _model_fingerprint before fixing, and both produced identical fingerprints for different settings -- the silent-wrong-frames class the batch exists to close. A BOUND METHOD loses everything. vars() on one proxies to the underlying function's __dict__, which is empty, not to the instance, so only __qualname__ survived. A node registering self.forward rather than self is the object bug one attribute away. A functools.partial loses everything too: no __name__, no __qualname__, no __closure__, empty __dict__. It collapsed to the same "fn()" constant a bare instance did, with its settings sitting untouched in func/args/keywords. That is the SLA bug one binding away. _callable_scalars now unwraps all four shapes -- closure, instance, bound method, partial -- depth-bounded because a partial can wrap a partial. Corrects a clai…0730c62
-
Video input size is an area budget, not a short edge
Sandpies +1: Claude Opus 5The output canvas is a short-edge ladder because H3 is a 768-short-edge model and that is what core's adapt_canvas pins. A reference clip is the opposite case: it arrives at whatever aspect it was shot at, and this control exists to bound MEMORY. A short edge does not bound memory across aspects. At a 640 short edge a 21:9 clip is 0.96 MP and a 1:1 clip is 0.41 MP -- a factor of more than two for the same label, on a dial whose entire job is telling you what it will cost. An area budget is the same number at every aspect, which is why the request to express it in megapixels was the right one. So: 0.3 through 1.0 MP, then MAX, which is what "match H3" was called before. Decimal megapixels, 10^6. Note the output ladder's parenthetical is MEBIpixels because "0.98 MP" for 1344x768 is a community figure computed against 1024^2 and people search for it -- different quantities, and the comment says so in both places. The retired short-edge rungs still parse, so a graph saved while v1.1 was…5aa7eeb -
Open at 1200x900; the 4:3 request was about ratio, not scale
Sandpies +1: Claude Opus 5640x480 was the wrong reading of "4:3 broad ratio". The ratio was right and the scale was not: at 640 wide the script cards wrap, the RUN panel's four groups stack into a scroll, and the first thing anyone does is drag the node bigger. A default that has to be resized before use is not a default. 1200x900 is the same ratio at the size the panel was actually laid out for. The minimum is now a separate constant. NODE_WIDTH was doing double duty as the default size AND as installHeightGuard's minWidth, so raising the default would have silently raised the floor to 1200 and made the node impossible to narrow -- worse on a small screen than the old default ever was on a large one. NODE_MIN_WIDTH stays at 560. Both shipped workflows carry the new size, and check_workflows.py asserts it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014VB4Dw6F79643vySkf19KVe6eb52a -
The 4:3 default never applied to anything
Sandpies +1: Claude Opus 5Two reasons the node still opened as a tall column, and the release fixed neither because both live outside the code that was rewritten. Saved size wins. configure() runs after onNodeCreated, so a workflow's stored `size` overrides whatever the JS asked for. Both shipped workflows carried 968x1426 -- the pre-1.1 column height, from when the panel really was one long downward scroll. The tabs went in, the default became a 4:3 box, every check passed, and the graphs users actually open still came up as a column. And on a fresh node, Math.max only grows. LiteGraph sizes a node from its widget count and this one has 48, so the computed height was already far past 480 and the max() never brought anything down. The editor hides those widgets, so the height they imply is not a floor worth respecting -- it is set exactly now, on creation only. A node from a saved workflow keeps whatever size its author dragged it to; resizing somebody's graph on load would be worse than the wrong default eve…3347123 -
Decode the reference clip at the size it will actually be used at
Sandpies +1: Claude Opus 5Section 40 deferred this on the grounds that core already scales reference video down, so the only cost was disk and load time. Core does scale it down -- in execute(), after load_video has decoded every frame at source resolution, stacked them, and cast the stack to float32. A 10 s 4K plate spends ~36 GB of system RAM to hand over ~4.5 GB of pixels core immediately discards. load_video now computes the target before the loop and passes it to frame.reformat, so libswscale scales inside the decoder and the full-resolution RGB array is never built. The default, "match H3", asks core's adapt_canvas what it would have resized to and never scales up -- a clip already below the tier is left alone, exactly as core leaves it. Same pixels, different resampling kernel, none of the memory. reference_video_size adds the rungs below that (640p..384p) for trading reference fidelity against memory, which is the feature that was asked for. Measured on a 226-frame 960x544 plate -- already under the t…271a0f9 -
The canvas is a short edge, not an area; and prose reaches the scanner
Sandpies +1: Claude Opus 5Two corrections, both from evidence that arrived after the fact. CANVAS. Core's adapt_canvas pins the short edge at 768 and derives the long edge from the ratio -- its own docstring says so. v1.1 derived both axes from a megapixel budget and claimed to mirror it. The two agree at no aspect ratio: 16:9 gave 1312x736 against core's 1344x768, 4:3 gave 1152x864 against 1024x768. The root error was reading "0.98 MP" as 980,000 pixels. 1344x768 = 1,032,192, and 1,032,192 / 1024^2 = 0.984 -- the quoted number is a mebipixel count of a short-edge tier. The label was describing exactly the thing I had decided it could not describe. _fit_canvas is now core's function with the short edge as an argument, so the draft tiers run the identical path. The ladder is 768/640/576/512/448, labelled "768p (0.98 MP)" and so on, with the MP figure being that tier's 16:9 cell. check_canvas.py imports adapt_canvas and compares against it, rather than against numbers copied out of it -- the old "area within 5…0b085ca -
The Starter previewed its seam chart and saved its contact sheet
Sandpies +1: Claude Opus 5Node 35 was a SaveImage wired to `contact_sheet`, so every run of the shipped Starter wrote output/HandTieClips/contact_sheet_NNNNN_.png. `contact_sheet` defaults to off and the node returns `sheet.placeholder()` when it is, so the default configuration wrote a 16x16 black PNG to the output directory on every single render. With it on, it wrote a real sheet nobody asked to keep. Worse, ComfyUI's queue lists saved outputs, so that PNG -- not the video -- became what the queue showed for the job. Reported as "contact_sheet is still being autosaved after video finishes rendering, it should show the video instead of the contact sheet in the queue window". Not an ordering problem: the seam chart's PreviewImage sits at order 109, later than the SaveImage's 99, and appears in neither output/ nor the queue. The difference is Save versus Preview. Node 35 becomes a PreviewImage, titled and sized to match the seam chart beside it -- the sheet is still there to look at, it just stops being filed…717b144
-
The reference clip can say what it is for
Sandpies +1: Claude Opus 5Feedback asked for two things here: resize the clip on import, and give it a description alongside the other references. This is the second. The clip has always gone to the encoder as <Video 1> with nothing anywhere in the prompt naming it. That is the same uncited-reference problem the stills had on hops after the first, with the same consequence -- footage handed to a Ref2VA model with no stated role tends to be rendered as the shot. _live_cite covers the PINNED tail, which is a different video and already explained. The author's clip is reliably <Video 1>: _attach_pin_to_qwen APPENDS the pinned tail rather than prepending it, so unlike the stills nothing shifts it. One line, built once outside the loop, emitted on hop 1 directly and folded into _assemble_next's inject on continuations. Gated on the field being filled, and asserted as such: an empty reference_video_desc produces a byte-identical continuation prompt, so no 1.0.x workflow has its prompt quietly rewritten by upgradin…543c752 -
render_from, and a cache that stops re-rendering hops nothing touched
Sandpies +1: Claude Opus 5Three things, all aimed at the same complaint: changing one shot in the middle of a chain cost a whole chain. `render_from` is the other end of the range `render_through` already had. It truncates nothing -- the hops before it still run through the loop, they are just required to come out of the hop cache instead of the sampler. Doing it that way rather than seeding prev_imgs / prev_audio / prev_sampled / prev_key from the store up front means there is still exactly ONE piece of code that carries continuity forward, and it is the cache-hit branch that was already carrying it. The sampler latent is the part that would have been got wrong: it decides whether the next hop joins by Motion-Context or falls back to AddGuide. Every misconfiguration is refused before the master tensor is allocated, which on an 8x15s plan is ~31 GB. An inverted range is checked against the ORIGINAL render_through, because `n` has already been truncated to it by then and testing against `n` reported "past the …8d66692 -
1.0.0: the writer on the node, and the docs to match
Sandpies +1: Claude Opus 5Version 1.0.0. The README front door never mentioned WRITE at all -- the headline feature of this release was reachable only by finding it on the node -- so it now has its own section, and the "what v1.0 changed" table a row. Docs: - Four screenshots from a real 3-hop render: the editor panels, the WRITE bar, the MEDIA strip and a contact sheet. All LFS, all referenced through media.githubusercontent.com as .gitattributes requires. - New "Voice, music and trims" section. The audio in/out windows were documented only in a tooltip, and the reason they matter -- H3 encodes the whole voice file with no cap and attends over it every step of every hop -- was nowhere a reader would find it. - The starter workflow's notes gained the sub-hop sound rule and its symptom row, and its writer note now leads with the built-in panel rather than the copy-paste flow. Its context guidance said 16384 and ~4,700 tokens; the system prompt measures ~6,000 and wants 32768. - The old contact-s…ff9b717
-
trim windows on every media input, and a per-reference pixel budget
SandpiesUsing the soundtrack for the first time exposed the gap: a 173 s track under a 9.4 s chain, and `loop` silently takes the first 9.4 s -- which for a mastered track is the intro. There was no way to say WHICH 9.4 seconds. The same hole was on the other two inputs, and on one of them it is not a convenience issue. MiniMaxH3ReferenceToVideo hands the whole voice file to `_encode_ref_audio` with no cap, and every latent frame that produces is a token the DiT attends over on every step of every hop. An untrimmed three-minute voice reference is a large, silent, permanent tax on the render. The reference clip is truncated to the hop length, but only from frame 0, so you could not point at the motion you wanted. Six FLOAT widgets, appended last: voice / reference_video / music, start+end. An end of 0.0 means "to the end of the file", so the default pair is untrimmed and a longer replacement file still plays out. `media.clip_window` is the ONE definition of what a window means -- four readers…a2cfa42 -
soundtrack: put it in the panel, where the other media already lives
SandpiesThe AUDIO socket was the odd one out. Every other piece of media in this pack is a filename under h3_refs -- start_image_file, reference_video_file, voice_file -- because collapsing sixteen sockets to five is what made the node readable. Adding a sixth walked that back, and left the music input somewhere nobody looks for media. So `soundtrack_file` joins the MEDIA strip beside the voice reference, and the four dials become a `soundtrack` group in the RUN panel. Neither needed new machinery: the strip's SLOTS table and the panel's GROUPS table already do this, and MEDIA_WIDGETS derives its hide-list from SLOTS, so the widget stops rendering raw on the body by itself. The socket stays, and wins when both are set. A wire is a deliberate act; a filename left in the panel from an earlier take is not, and quietly preferring the stale one would be the worse guess -- so the console says which one played. IS_CHANGED gains soundtrack_file for the reason the other three are in it: overwrite th…6c5d3ea -
soundtrack: a music bed under the chain, ducked under the dialogue
Sandpies +1: Claude Opus 5A user asked for a track over the whole generation -- not an audio reference, which H3 already takes as a voice, but a bed under the finished chain. Applied once, after the last hop is joined, immediately before master_audio is built. That places it downstream of every latent, every pin and every cache key, so it cannot move a generated frame or sample; it only decides what is laid over them. With nothing wired it returns the master object itself. music.py carries the DSP and imports no ComfyUI, so tools/check_music.py runs it on the CPU with no server -- every defect here is inaudible in a still and invisible in a frame count, so a checker that cannot run is a checker that never catches one. Three non-obvious choices, each explained where it lives: resample explicitly (48k into a 44.1k master plays a semitone sharp and reads as a bad model, not a bug); crossfade the loop wrap (a butt join clicks once per period); duck against the 95th percentile rather than the peak (one shouted wo…7d5c81d
-
0.4.5: the hop cache stops shelling out to ffmpeg
Sandpies +1: Claude Opus 5store.py ran an ffmpeg binary through subprocess.Popen to write and read its lossless FFV1 cache. That cost two things, and the registry flag was the smaller one. _ffmpeg() raised if no ffmpeg was on PATH, and ComfyUI never requires one -- so the feature that makes a tone A/B cost 14 s instead of 164 s hard-failed for a large share of users, on the pack's fastest path. The Comfy registry's YARA scan also flagged all three published versions for it: rule python_command_injection_risk, "detects ALL os.system and subprocess usage", severity info, recommendation null. It does no taint analysis, so a static argument list built from shutil.which and run with shell=False matches as hard as a shell injection. The reason is not in the web UI -- it is behind api.comfy.org/versions?nodeId=<id>&include_status_reason=true. Now encoded in process with PyAV, a hard dependency of ComfyUI itself (SaveVideo and CreateVideo are built on it). The format is deliberately unchanged: ffv1 / rgb48le / level…d07b19b