Sandpies Claude Opus 5 commited on
Commit
a94c062
·
1 Parent(s): 543c752

1.1.0: docs, version, and the devlog for all of it

Browse files

README gets the 1.1.0 block and its resolution references move to the new
rungs. The hop-cache section now states the thing that changed underneath it:
changing a reference picture re-renders only the hops that picture rides.

CLAUDE.md's verified-size instruction follows the top rung rather than naming
1.0 MP, and the iteration-canvas note moves off 0.5 MP -- a label that still
RESOLVES, because retired labels are parsed, but is no longer on the dropdown.

Both prompt_pack prompts, together as the rule requires, note that the
nine-reference limit is per hop and that a whole list of nine can never breach
it. The numbered rules are untouched and still number 13.

Devlog 37-40: the uncited-reference failure and its three appearances; the
canvas formula against the table it replaced, including the two cells I got
wrong by hand and the grid search that was not worth it; the first check the
JavaScript has ever had, and the bug it found in itself on its first run; and
why render_from replays through the loop instead of seeding prev_* up front.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014VB4Dw6F79643vySkf19KV

CLAUDE.md CHANGED
@@ -320,7 +320,7 @@ Offline results (`frame_shift`, synthetic): recovers a planted additive shift to
320
  1. **Cache fingerprint.** LoRA strength 1.0 → 0.9, re-queue. Both hops re-render. (Before the latent sidecar, a hop-1 hit made hop 2 log `AddGuide pixel pin`; that line now means the entry predates the sidecar or its latent failed to load.)
321
  **Note ComfyUI's own node cache sits in front of this one:** re-queueing with *nothing* changed skips `run()` entirely (no `[HandTieClips]` lines at all, ~9 s), so it tests nothing. Nudge `cache_budget_gb` to force re-execution -- it is in neither `chain_salt` nor the hop key, so every key stays byte-identical.
322
  2. **Chained re-roll.** Revert strength. Change one word in hop 2 only. Hop 1 cache hit, hop 2 renders.
323
- 3. Confirm the same join at **1.0 MP** (`1280×736`) before calling it shipped. Keep `seed_per_shot`. 777777 was join-hostile on the old 5 s pin-only graph; do not treat that seed as the only one.
324
 
325
  **chain_00030_.mp4 / chain_00031_.mp4 / the keyframe-completion re-queue.** Hard cut at hop 2, f124, onto the outfit still’s commercial kitchen. Drink beat ran; pin did not. Official hop 2 cannot join at 8 steps.
326
 
@@ -330,7 +330,7 @@ Offline results (`frame_shift`, synthetic): recovers a planted additive shift to
330
 
331
  **H3 soundtrack (official methods, user 2026-08-26):** do not invent a pack dialect for quiet vs speech. Dialogue belongs in `detailed_description` as `(S1)` + `<d>[English] …</d>` with the actual words. Ambience/physical/non-verbal belong in `overall_soundscape`. Requested silence throughout a hop is `overall_soundscape: N/A` (the official complete-silence token), not the English word “Silence” stuffed into a beat. “No speech” / “no dialogue” is still negation and still gibberish. `_assemble_next` currently does not emit these fields; when the beat already contains them, leave them alone.
332
 
333
- **Iteration canvas (user 2026-08-26):** further tests run at **0.5 MP** (`960×544` landscape) for speed. 8 steps, seed 777777, 4×8 s, overlap 0.9 s stay. Seam times do not move (still master frames 192 / 362 / 532). Resolution is in `chain_salt`, so the 1.0 MP cache will not hit. A 0.5 MP pass validates join / speech / cache behaviour, not 1.0 MP texture. Confirm anything that ships at 1.0 MP (`1280×736`) on that canvas before calling it done.
334
 
335
  **chain_00028_.mp4 (0.5 MP, 960×544, 702 f / 29.25 s, 8 step).** Same stress plan. Frame-walked.
336
 
 
320
  1. **Cache fingerprint.** LoRA strength 1.0 → 0.9, re-queue. Both hops re-render. (Before the latent sidecar, a hop-1 hit made hop 2 log `AddGuide pixel pin`; that line now means the entry predates the sidecar or its latent failed to load.)
321
  **Note ComfyUI's own node cache sits in front of this one:** re-queueing with *nothing* changed skips `run()` entirely (no `[HandTieClips]` lines at all, ~9 s), so it tests nothing. Nudge `cache_budget_gb` to force re-execution -- it is in neither `chain_salt` nor the hop key, so every key stays byte-identical.
322
  2. **Chained re-roll.** Revert strength. Change one word in hop 2 only. Hop 1 cache hit, hop 2 renders.
323
+ 3. Confirm the same join at the top rung -- **0.98 MP** (`1312×736`) as of 1.1, **1.0 MP** (`1280×736`) before it -- before calling it shipped. Keep `seed_per_shot`. 777777 was join-hostile on the old 5 s pin-only graph; do not treat that seed as the only one.
324
 
325
  **chain_00030_.mp4 / chain_00031_.mp4 / the keyframe-completion re-queue.** Hard cut at hop 2, f124, onto the outfit still’s commercial kitchen. Drink beat ran; pin did not. Official hop 2 cannot join at 8 steps.
326
 
 
330
 
331
  **H3 soundtrack (official methods, user 2026-08-26):** do not invent a pack dialect for quiet vs speech. Dialogue belongs in `detailed_description` as `(S1)` + `<d>[English] …</d>` with the actual words. Ambience/physical/non-verbal belong in `overall_soundscape`. Requested silence throughout a hop is `overall_soundscape: N/A` (the official complete-silence token), not the English word “Silence” stuffed into a beat. “No speech” / “no dialogue” is still negation and still gibberish. `_assemble_next` currently does not emit these fields; when the beat already contains them, leave them alone.
332
 
333
+ **Iteration canvas (user 2026-08-26):** further tests run at **0.45 MP** (`896×512` landscape) for speed. (Was 0.5 MP / `960×544`; 1.1 computes the canvas from an area budget and the rungs moved. `0.5 MP` still resolves -- retired labels are parsed -- but it is off the dropdown.) 8 steps, seed 777777, 4×8 s, overlap 0.9 s stay. Seam times do not move (still master frames 192 / 362 / 532). Resolution is in `chain_salt`, so the top-rung cache will not hit. A low-rung pass validates join / speech / cache behaviour, not full-resolution texture. Confirm anything that ships at the top rung on that canvas before calling it done.
334
 
335
  **chain_00028_.mp4 (0.5 MP, 960×544, 702 f / 29.25 s, 8 step).** Same stress plan. Frame-walked.
336
 
PROMPTING.md CHANGED
@@ -504,7 +504,7 @@ Two related dials:
504
  - **`render_through=N`** stops after N hops. With `cache_hops=on`, 3 → 5 → 8
505
  builds a chain up in stages and only ever renders the new hops. The plan is
506
  not truncated; shot 4 still keys exactly as it will in the full run.
507
- - **`quality=draft`** forces 0.3 MP and 6 steps. Enough to read blocking,
508
  camera and whether a join lands. Both values are in the cache key, so a draft
509
  never overwrites the final it stands in for.
510
 
 
504
  - **`render_through=N`** stops after N hops. With `cache_hops=on`, 3 → 5 → 8
505
  builds a chain up in stages and only ever renders the new hops. The plan is
506
  not truncated; shot 4 still keys exactly as it will in the full run.
507
+ - **`quality=draft`** forces 0.30 MP and 6 steps. Enough to read blocking,
508
  camera and whether a join lands. Both values are in the cache key, so a draft
509
  never overwrites the final it stands in for.
510
 
README.md CHANGED
@@ -80,6 +80,34 @@ of a second, separate generation. The pack exists so that you cannot tell which.
80
  > quiet buys a quiet pin, not a quiet opening. Sections 31 and 32 of the devlog
81
  > have the measurements.
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  > **1.0.1 — 2026-09-02.** Three fixes, all found by the first people to use
84
  > 1.0.0. The **WRITE** panel never saved your model on a fresh install: with
85
  > nothing stored, no entry in the dropdown was ever *selected* and the browser
@@ -235,7 +263,7 @@ Two modes:
235
 
236
  Only one of them is on screen at a time, so there is never a text box quietly doing nothing.
237
 
238
- Under the script sits **RUN**, collapsed, holding everything that is not per-shot: output size and length, sampling, the join and pin controls, and the hop cache. Its title line summarises the run — `1.0 MP 16:9 · 10s ×3 · 14 steps res_multistep · cache off` — so you can read the setup without opening it. In Shots mode `chains` and `hop_script` are not offered there, because the shot list already decides both.
239
 
240
  **`tone_compensate`** lives in that panel's *join & pin* group. The H3 denoiser biases each hop's tone, so a chain gets steadily brighter; this measures the bias on the overlap each hop regenerated and undoes it, correcting each hop against the previous **corrected** one so the whole chain lands on hop 1's tone. `frame_shift` is the mode to reach for: all three modes remove the drift equally well (within 0.4/255 of each other), but `gain_bias` and `lut` pair pixels between a frame and its *regeneration*, which fits a slope that is not really there and flattens contrast a little more with every hop. `frame_shift` uses frame averages only, so it can shift but never distort. **Measured on a 3-hop render: chain drift 5.6/255 without it, 0.3/255 with it.** Worth turning on for anything past two hops. It ships off because enabling it also clamps the master to 0..1, and because the correction grows with hop count — by hop 10 it is subtracting ~23/255 and will start crushing blacks. Switching modes never invalidates the hop cache, so it is free to A/B. Do **not** judge it by whether the seams flatten to zero: real scene brightness changes across a cut should survive, and they do.
241
 
@@ -411,7 +439,8 @@ The key **chains** — each hop's key includes the previous hop's — because ho
411
 
412
  - edit shot 3 and re-queue → shots 1 and 2 load from cache, only 3 renders;
413
  - edit shot 1 → all three re-render, which is correct, not a bug;
414
- - change resolution, sampler, a reference still, a LoRA, or an attention setting → the whole chain re-renders.
 
415
 
416
  That last one is worth knowing about. The node cannot read the settings on your LoRA and attention nodes, so instead it fingerprints what they *did* to the model — which weight keys were patched, at what strengths, and the attention overrides. Change a LoRA strength and the cache correctly invalidates. Two different LoRAs touching exactly the same keys at exactly the same strengths would look identical to it; that is the one gap.
417
 
@@ -433,14 +462,14 @@ rendered stay on disk, so 3 → 5 → 8 builds a chain up in stages and only eve
433
  renders the new hops. The plan is not truncated: shot 4 still knows it is shot
434
  4, keeps its own seed, and keys the same way it will in the full run.
435
 
436
- `quality=draft` forces 0.3 MP and 6 steps — enough to read blocking, camera and
437
  whether a join lands. Resolution and steps are both in the cache key, so a draft
438
  never overwrites the final it stands in for; the two simply cost two entries.
439
 
440
  Treat it as a **fidelity** lever rather than a speed one. Measured at ~42 s/hop
441
- against ~45 s/hop at 7 steps: if you already render at 0.3 MP and 6–8 steps —
442
  the regime this pack targets — draft saves almost nothing, and `dry_run` is the
443
- fast button. Draft earns its place when your final is genuinely heavier, 1.0 MP
444
  at 14 steps.
445
 
446
  ## Contact sheet
@@ -556,7 +585,7 @@ The DiT pin is the previous hop’s **sampler latent** through Motion-Context wh
556
 
557
  | | |
558
  |---|---|
559
- | resolution | 1.0 MP (1280×736 landscape) |
560
  | duration | 10 s (243 frames) |
561
  | overlap | 0.9 s (22 frames) |
562
  | steps | 8, with a 4-step turbo LoRA — the regime this node targets |
 
80
  > quiet buys a quiet pin, not a quiet opening. Sections 31 and 32 of the devlog
81
  > have the measurements.
82
 
83
+ > **1.1.0 — 2026-09-03.** The panel is a 4:3 box with tabs instead of one long
84
+ > scrolling column, and RUN stays pinned at the bottom. The **WRITE** draft is
85
+ > readable — it used to clip every beat at 110 characters and throw away the
86
+ > duration, seed, directives and per-shot references before rendering, so what
87
+ > you were asked to Accept was a row of sentences ending in an ellipsis — and it
88
+ > now survives leaving the tab or reloading the page. Shot cards carry `⏵` and
89
+ > `lock` buttons driving `render_from` / `render_through`, the new one being the
90
+ > other end of a range `render_through` has had since 0.4: everything before the
91
+ > start is replayed from the hop cache rather than re-rendered.
92
+ >
93
+ > The bug worth upgrading for: **a reference pinned to any hop but the first
94
+ > tended to be rendered as the shot.** `retention_analysis:` — the text that
95
+ > tells the encoder what a picture is *for* — was emitted on hop 1 only, so a
96
+ > still scheduled onto hop 3 arrived uncited; and the identity lock never read
97
+ > `retention`, so a wardrobe plate carrying a subject was announced as *"the
98
+ > only identity … that face, bone structure, and hairstyle match the photograph
99
+ > exactly"* about a photograph of an apron, while the closer simultaneously said
100
+ > clothing follows the live frame. Both fixed. The reference *clip* had the same
101
+ > gap — it went in as `<Video 1>` with nothing naming it — and now has a
102
+ > description field.
103
+ >
104
+ > Output size is computed rather than tabulated: eleven aspect ratios from 21:9
105
+ > to 9:21 across five megapixel rungs, every one on H3's 32 px grid and under
106
+ > its 768×1344 cap. **The top rung is 0.98 MP, and 16:9 there is 1312×736** —
107
+ > the hop cache invalidates once because of it. Saved 1.0.x workflows keep the
108
+ > exact pixels they were built with. And changing a reference picture now
109
+ > re-renders only the hops that picture rides, instead of the whole chain.
110
+
111
  > **1.0.1 — 2026-09-02.** Three fixes, all found by the first people to use
112
  > 1.0.0. The **WRITE** panel never saved your model on a fresh install: with
113
  > nothing stored, no entry in the dropdown was ever *selected* and the browser
 
263
 
264
  Only one of them is on screen at a time, so there is never a text box quietly doing nothing.
265
 
266
+ Under the script sits **RUN**, collapsed, holding everything that is not per-shot: output size and length, sampling, the join and pin controls, and the hop cache. Its title line summarises the run — `1312x736 · 10s ×3 · 14 steps res_multistep · cache off` — so you can read the setup without opening it. In Shots mode `chains` and `hop_script` are not offered there, because the shot list already decides both.
267
 
268
  **`tone_compensate`** lives in that panel's *join & pin* group. The H3 denoiser biases each hop's tone, so a chain gets steadily brighter; this measures the bias on the overlap each hop regenerated and undoes it, correcting each hop against the previous **corrected** one so the whole chain lands on hop 1's tone. `frame_shift` is the mode to reach for: all three modes remove the drift equally well (within 0.4/255 of each other), but `gain_bias` and `lut` pair pixels between a frame and its *regeneration*, which fits a slope that is not really there and flattens contrast a little more with every hop. `frame_shift` uses frame averages only, so it can shift but never distort. **Measured on a 3-hop render: chain drift 5.6/255 without it, 0.3/255 with it.** Worth turning on for anything past two hops. It ships off because enabling it also clamps the master to 0..1, and because the correction grows with hop count — by hop 10 it is subtracting ~23/255 and will start crushing blacks. Switching modes never invalidates the hop cache, so it is free to A/B. Do **not** judge it by whether the seams flatten to zero: real scene brightness changes across a cut should survive, and they do.
269
 
 
439
 
440
  - edit shot 3 and re-queue → shots 1 and 2 load from cache, only 3 renders;
441
  - edit shot 1 → all three re-render, which is correct, not a bug;
442
+ - change resolution, sampler, a LoRA, or an attention setting → the whole chain re-renders;
443
+ - change a reference picture → only the hops that picture rides re-render. Swapping the file behind `@outfit` when it rides hop 5 leaves hops 1-4 on cache. Before 1.1 this invalidated everything.
444
 
445
  That last one is worth knowing about. The node cannot read the settings on your LoRA and attention nodes, so instead it fingerprints what they *did* to the model — which weight keys were patched, at what strengths, and the attention overrides. Change a LoRA strength and the cache correctly invalidates. Two different LoRAs touching exactly the same keys at exactly the same strengths would look identical to it; that is the one gap.
446
 
 
462
  renders the new hops. The plan is not truncated: shot 4 still knows it is shot
463
  4, keeps its own seed, and keys the same way it will in the full run.
464
 
465
+ `quality=draft` forces 0.30 MP and 6 steps — enough to read blocking, camera and
466
  whether a join lands. Resolution and steps are both in the cache key, so a draft
467
  never overwrites the final it stands in for; the two simply cost two entries.
468
 
469
  Treat it as a **fidelity** lever rather than a speed one. Measured at ~42 s/hop
470
+ against ~45 s/hop at 7 steps: if you already render at 0.30 MP and 6–8 steps —
471
  the regime this pack targets — draft saves almost nothing, and `dry_run` is the
472
+ fast button. Draft earns its place when your final is genuinely heavier, 0.98 MP
473
  at 14 steps.
474
 
475
  ## Contact sheet
 
585
 
586
  | | |
587
  |---|---|
588
+ | resolution | 0.98 MP (1312×736 landscape) |
589
  | duration | 10 s (243 frames) |
590
  | overlap | 0.9 s (22 frames) |
591
  | steps | 8, with a 4-step turbo LoRA — the regime this node targets |
docs/DEVLOG.md CHANGED
@@ -1954,3 +1954,177 @@ The general shape, and section 34's lesson one level down: a control that
1954
  displays a value it has not committed is bad, and a generator that emits a
1955
  value it could not compute is the same bug wearing a different hat. Neither
1956
  one fails; both produce something that looks like an answer.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1954
  displays a value it has not committed is bad, and a generator that emits a
1955
  value it could not compute is the same bug wearing a different hat. Neither
1956
  one fails; both produce something that looks like an answer.
1957
+
1958
+ ## 37. Two ways to hand a model a photograph and not say why (2026-09-03)
1959
+
1960
+ A user: *"pinning outfit to anything but first hop throws the actual image
1961
+ in."* The obvious suspect was the image ordering -- something prepending the
1962
+ still ahead of the continuity frame. It was not. `_attach_pin_to_qwen` builds
1963
+ `[last_frame] + refs`, and `still_shift` moves the text ordinals to match. The
1964
+ pictures arrive in the right order with the right numbers.
1965
+
1966
+ What was wrong was that nothing said what the picture was *for*.
1967
+
1968
+ `retention_analysis:` is the block that does that -- `<Picture 2> (denim apron
1969
+ over a grey tee): the garment and its cut carry over`. It is produced by
1970
+ `subject_prose`, and `subject_prose` is gated on `i == 0`. That gate is correct
1971
+ for its other half: `subject_definitions:` introduces `<Subject N>`, and a
1972
+ subject token first introduced on hop 4 has no antecedent in its own encode.
1973
+ But retention has no such problem, and it went out with the gate. A still
1974
+ scheduled onto hop 3 therefore reached the encoder as `ref_image_2` with no
1975
+ retention line, no `desc`, and no subject entry. If it also had no subject
1976
+ number, `id_ords` came back empty, `_identity_lock` returned `""`, and the
1977
+ photograph was never named anywhere in the prompt at all.
1978
+
1979
+ The second defect is worse because it is confidently wrong rather than silent.
1980
+ `_identity_lock` decides who is a person by `subject is not None` and has never
1981
+ read `retention`. The canonical outfit reference in the README carries
1982
+ `subject: 1` *and* `retention: partially_copy` -- it is a garment belonging to
1983
+ person 1, not a photograph of their face. So hop 3 opened with *"`<Picture 2>`
1984
+ is the only identity. That face, bone structure, and hairstyle match the
1985
+ photograph exactly"*, asserted about an apron. And because `lock` was then
1986
+ non-empty, the closer became *"Faces and hair follow the identity photographs.
1987
+ Clothing follows whatever is already on them in the live frame"* -- which
1988
+ contradicts the wardrobe plate riding that very hop. At cfg 1.0 there is no
1989
+ negative branch, so both instructions are additive and the encoder gets each at
1990
+ full weight.
1991
+
1992
+ Three changes. `retention_prose` splits out of `subject_prose` and takes an
1993
+ explicit ordinal map, because on a continuation hop the live frame is `<Picture
1994
+ 1>` and every still shifts up -- deriving the map a second time is exactly the
1995
+ drift the `hop_ords` comment warns about. The identity filter requires
1996
+ `fully_preserved`, which `refs.py` already defaults subject-bearing refs to, so
1997
+ ordinary face plates are untouched and only garment and setting plates stop
1998
+ being called people. And the closer points clothing at the plate when one is
1999
+ actually present.
2000
+
2001
+ Hop 1 escaped all of this because `subject_prose` runs there, which is
2002
+ precisely why the report said "anything but the first hop".
2003
+
2004
+ The same shape turned up a third time while writing this. The author's
2005
+ reference *clip* has always gone in as `<Video 1>` with nothing naming it --
2006
+ `_live_cite` covers the pinned tail, which is a different video. Same failure
2007
+ mode, same fix: a description field, and a note printed when a clip is wired
2008
+ without one. It is gated on the field being filled and asserted as such, so no
2009
+ existing workflow has its prompt quietly rewritten by upgrading.
2010
+
2011
+ The lesson is not about pinning. It is that an uncited reference is not
2012
+ neutral. A Ref2VA model handed a photograph and no reason for it will find a
2013
+ reason, and the reason it finds is "render this".
2014
+
2015
+ ## 38. Fifteen tuples, of which the formula gets nine (2026-09-03)
2016
+
2017
+ The output canvas was a hand-authored table: five resolution labels by three
2018
+ aspects. Adding eight more aspects would have made it fifty-five, so it became
2019
+ a function -- mirroring core's `adapt_canvas`, nearest 32 per axis, under the
2020
+ `768*1344` cap.
2021
+
2022
+ The interesting part was checking it against what it replaced. I predicted the
2023
+ formula would reproduce thirteen of the fifteen old tuples and wrote that into
2024
+ the plan. It reproduces nine. Two cells I had done in my head were simply wrong
2025
+ -- `sqrt(200000/1.7778)` is 335.4, which rounds to ten 32-blocks and not eleven
2026
+ -- and I had also forgotten that every landscape divergence has a portrait
2027
+ twin.
2028
+
2029
+ Six diverge, all in the 16:9/9:16 column, and the pattern says why: that column
2030
+ was hand-tuned for ratio fidelity rather than derived from area. 1280x736 is
2031
+ 1.739:1, closer to 16:9 than the 1344x736 (1.826:1) the area allows. So the old
2032
+ table was not sloppy; it was optimising something else.
2033
+
2034
+ They are pinned rather than recomputed, because width and height are in
2035
+ `chain_salt`: resolving them differently would re-render every chain a 1.0.x
2036
+ user has on disk *and* change the pixels of a graph they already signed off.
2037
+ `check_canvas.py` asserts both halves -- that the six stay pinned, and that the
2038
+ other nine still need no pin. A compat shim nobody re-checks becomes a bug the
2039
+ moment the thing it was shimming moves.
2040
+
2041
+ I also tried a small grid search to beat per-axis rounding on ratio fidelity.
2042
+ Weighting ratio twice as heavily as area took the worst ratio error from 3.3%
2043
+ to 2.1% and pushed the worst area error from 2.7% to 3.8%, and produced
2044
+ identical sizes for 16:9 at every rung. Not worth diverging from core's
2045
+ arithmetic for. Matching `adapt_canvas` exactly is worth more than a point of
2046
+ ratio error on one cell, because the two agreeing is a property somebody can
2047
+ rely on.
2048
+
2049
+ Separately: `media._mp_cap_size` rounded reference stills to a multiple of 16
2050
+ on the stated grounds that "that is H3's canvas grid". It is not.
2051
+ `CANVAS_MULTIPLE` is 32; 16 is the VAE's spatial factor, a different number
2052
+ that happens to divide it. Core re-snaps every reference to 32 on the way in,
2053
+ so nothing wrong ever reached the model -- the only casualty was that the size
2054
+ printed in the log was not the size the encoder saw, which is the one job that
2055
+ rounding has. `check_waveform.py` asserted the wrong number and had been
2056
+ passing on it since it was written.
2057
+
2058
+ ## 39. Four thousand lines with nothing in front of them (2026-09-03)
2059
+
2060
+ There is no Node in this environment and no build step in this pack, so `js/`
2061
+ had no check of any kind between an edit and a browser. That matters more than
2062
+ it sounds: the browser reports a bad import as a node with no editor, which
2063
+ looks exactly like the pack failing to load, and the console line is one
2064
+ `console.warn` among ComfyUI's own.
2065
+
2066
+ `tools/check_ui.py` is not a parser and does not try to be. It checks four
2067
+ things that have gone wrong or could go wrong silently: delimiters balance per
2068
+ file; every named import resolves to a real export in the file it names; every
2069
+ `var(--h3-*)` the stylesheet reads is declared in it; and no rule targets a
2070
+ class the JS never sets.
2071
+
2072
+ It found three things on its first run, before it had checked any new code.
2073
+ `--h3-line` was read in two rules and declared nowhere, so the trim bar's and
2074
+ the draft list's borders had been resolving to nothing. `--h3-fg` fell back to
2075
+ a literal `#fff` that no longer matched the palette. And two classes had rules
2076
+ but no setter -- `.h3e-drag-over`, left behind when the drop target was renamed
2077
+ `.h3e-drop`, and `.h3e-card.h3e-dragging { opacity: .45 }`, which is feedback
2078
+ the stylesheet was clearly written for and the drag reorder was never wired to.
2079
+ The first is deleted; the second is now wired.
2080
+
2081
+ Its own first run also caught a bug in itself, which is the honest way to
2082
+ calibrate one of these: the import scan ran against the string-stripped source,
2083
+ and the module specifier of an import *is* a string literal, so it found zero
2084
+ imports and reported success. The "something was actually imported" assertion
2085
+ exists because of that, and it is the more valuable line -- a checker that
2086
+ silently checks nothing is the failure mode every checker has.
2087
+
2088
+ ## 40. The range that only had one end (2026-09-03)
2089
+
2090
+ `render_through` has stopped a chain after N hops since 0.4. There has never
2091
+ been a way to start at one, so re-rendering shot 5 of an eight-shot chain meant
2092
+ re-rendering shots 1 through 4 first, or accepting a chain that began at 5 with
2093
+ nothing before it.
2094
+
2095
+ The obvious implementation is to seed `prev_imgs`, `prev_audio`, `prev_sampled`
2096
+ and `prev_key` from the hop store before the loop starts. `HopStore.get`
2097
+ already returns all four, so it is maybe fifteen lines.
2098
+
2099
+ It is the wrong fifteen lines. The cache-hit branch inside the loop already
2100
+ carries exactly those four forward, and a second implementation is a second
2101
+ thing to get subtly wrong -- the sampler latent especially, which is what
2102
+ decides whether the next hop joins by Motion-Context or falls back to the
2103
+ weaker AddGuide pin. Getting that wrong produces a chain that renders fine and
2104
+ joins badly, which is the hardest class of bug to attribute.
2105
+
2106
+ So `render_from` truncates nothing. The leading hops run through the loop like
2107
+ any other hop and are simply *required* to hit the cache; a miss names the hop
2108
+ and says why it might be gone. One code path carries continuity, and it is the
2109
+ one that was already carrying it.
2110
+
2111
+ Two things fell out of writing the guards. Every misconfiguration is refused
2112
+ before the master tensor is allocated, which on an 8x15s plan is ~31 GB -- the
2113
+ first draft checked `hop_store is None`, which meant the check sat after the
2114
+ allocation, so a typo cost 31 GB before being told it was a typo. And the
2115
+ inverted-range check has to run against the *original* `render_through`,
2116
+ because `n` has already been truncated to it by then: testing `start_at > n`
2117
+ first reported "past the end of the plan" for `from=5, through=2` and then
2118
+ rendered the whole thing.
2119
+
2120
+ The cache change shipped alongside is the one that will be felt more.
2121
+ `chain_salt` digested every wired reference chain-wide, so swapping the file
2122
+ behind `@outfit` moved hop 1's key even when `@outfit` rides only hop 5 -- and
2123
+ the key is chained, so that re-rendered everything. References are keyed per
2124
+ hop now, from `base_images`, which is the right set on both paths: this hop's
2125
+ scheduled stills when there is a ref plan, every wired reference when there is
2126
+ not, and the pin frame it excludes is already covered by `prev_key`.
2127
+
2128
+ And the nine-reference ceiling turns out to be per *encode*, not per plan,
2129
+ while the check counted the whole rail. Twelve references at three per shot
2130
+ across four shots was refused against a limit no shot came near.
prompt_pack/AUTHORING_PROMPT.md CHANGED
@@ -444,7 +444,7 @@ Before you answer, check every one of these:
444
  and the wardrobe plate is `partially_copy`.
445
  - [ ] Every subject in `subjects` is claimed by a ref, and has both a `name`
446
  and a `locked`.
447
- - [ ] `refs` holds at most 9 entries in total.
448
  - [ ] Dialogue uses single quotes.
449
  - [ ] Both blocks are valid JSON: no trailing commas, no comments, no smart
450
  quotes, plain ASCII, and no field outside the lists above.
 
444
  and the wardrobe plate is `partially_copy`.
445
  - [ ] Every subject in `subjects` is claimed by a ref, and has both a `name`
446
  and a `locked`.
447
+ - [ ] `refs` holds at most 9 entries in total. (The encoder's real limit is 9 on any ONE hop, so a whole list of 9 can never breach it.)
448
  - [ ] Dialogue uses single quotes.
449
  - [ ] Both blocks are valid JSON: no trailing commas, no comments, no smart
450
  quotes, plain ASCII, and no field outside the lists above.
prompt_pack/SYSTEM_PROMPT.md CHANGED
@@ -431,7 +431,7 @@ Before you answer, check every one of these:
431
  and the wardrobe plate is `partially_copy`.
432
  - [ ] Every subject in `subjects` is claimed by a ref, and has both a `name`
433
  and a `locked`.
434
- - [ ] `refs` holds at most 9 entries in total.
435
  - [ ] Dialogue uses single quotes.
436
  - [ ] Both blocks are valid JSON: no trailing commas, no comments, no smart
437
  quotes, plain ASCII, and no field outside the lists above.
 
431
  and the wardrobe plate is `partially_copy`.
432
  - [ ] Every subject in `subjects` is claimed by a ref, and has both a `name`
433
  and a `locked`.
434
+ - [ ] `refs` holds at most 9 entries in total. (The encoder's real limit is 9 on any ONE hop, so a whole list of 9 can never breach it.)
435
  - [ ] Dialogue uses single quotes.
436
  - [ ] Both blocks are valid JSON: no trailing commas, no comments, no smart
437
  quotes, plain ASCII, and no field outside the lists above.
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
  [project]
2
  name = "comfyui-hand-tie-clips"
3
- version = "1.0.1"
4
  description = "Native MiniMax H3 Ref2VA chain: persistent references, previous-segment clip+audio pin, one queue."
5
  readme = "README.md"
6
  # SPDX string, not `{ file = "LICENSE" }`. The registry does not resolve the file
 
1
  [project]
2
  name = "comfyui-hand-tie-clips"
3
+ version = "1.1.0"
4
  description = "Native MiniMax H3 Ref2VA chain: persistent references, previous-segment clip+audio pin, one queue."
5
  readme = "README.md"
6
  # SPDX string, not `{ file = "LICENSE" }`. The registry does not resolve the file