0.4.1: fix what shipped broken, and record what the first real session measured
Browse files0.4.0 was built with no browser and no GPU. Run in ComfyUI for the first time,
two of its seven features were broken -- both in the gap between "the Python is
correct" and "the graph runs", which is exactly where the offline suite cannot
see.
Fixed:
- The five new widgets were never added to GROUPS in js/editor/run_panel.js.
0.4.0 touched twenty files and not one under js/. They worked, but fell
through to raw dials on the node body because the run panel hides only what
it managed to draw. A Python widget list and a JS widget list are two sources
of truth and nothing checks they agree.
- sheet.placeholder() returned a 1x1 image. libx264 in yuv420p cannot open a
codec context on an odd dimension, so every dry run wired to SaveVideo -- what
the Starter ships -- died in avcodec_open2 naming ComfyUI's video node and
nothing of ours. placeholder() now takes a size, floors at 2x2 and rounds odd
dimensions down; a dry run returns one frame at the resolved geometry.
check_features.py had asserted the exact broken shape, pinning the bug in
place; it now asserts an encodable() contract.
Measured (docs/DEVLOG.md section 21): tone_anchor swept 0.15/0.35/0.60 against
off on one seed and one cache -- 0.35 removes 62% of drift for 1.4/255 of seam,
monotonic with no knee, so the default stays. The seam report node, never
executed before, agrees with an independent mp4 decode to +/-0.07/255. On a
5-hop master no seam appears among the eight largest frame-to-frame luma jumps.
quality=draft is ~7% faster than final in this pack's turbo regime and is a
fidelity lever, not a speed one.
Also:
- Starter ships the seam report wired: HTCSeamReport + PreviewAny + PreviewImage
off the images output. Allowlists in check_workflows.py updated to match.
- Board cards corrected where the session proved them wrong: draft's speed
claim, tone_anchor strength guidance, and the seam report's hops trap -- a
wrong hops does not error, it returns a plausible length and puts every seam
where no join exists.
- CLAUDE.md was 1147 lines and mostly a dated log on a public repo. The log
moves to docs/DEVLOG.md; CLAUDE.md is the architecture brief at 362 lines.
- README carries the measured drift table, the fourth output socket it had
never listed, and HuggingFace card metadata.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MGjcAV8bDy93qJfmLi9kw
- .gitignore +3 -0
- CLAUDE.md +10 -795
- README.md +51 -5
- docs/DEVLOG.md +915 -0
- h3_ref_chain.py +6 -1
- js/editor/run_panel.js +12 -1
- pyproject.toml +1 -1
- sheet.py +15 -3
- tools/check_features.py +24 -4
- tools/check_workflows.py +2 -2
- tools/notes.py +22 -7
- workflows/HandTieClips_Starter.json +154 -17
|
@@ -5,3 +5,6 @@ __pycache__/
|
|
| 5 |
# version control here. They are working files, not releases.
|
| 6 |
*.bak
|
| 7 |
*.bak*
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
# version control here. They are working files, not releases.
|
| 6 |
*.bak
|
| 7 |
*.bak*
|
| 8 |
+
|
| 9 |
+
# Session handover doc -- working file, never ships
|
| 10 |
+
HANDOVER.md
|
|
@@ -350,798 +350,13 @@ hop N-1 ends. Hop N opens by holding what it was handed.
|
|
| 350 |
- Fewer than 3 wired reference stills triggers a warning log; unconnected `Load Image` nodes do not count as wired.
|
| 351 |
- Soundtrack is official H3, not a pack dialect: `(S1)` + `<d>…</d>` for lines, `overall_soundscape` for ambience/physical (or `N/A` for requested silence). Do not invent beat-keywords. “No speech” / “no dialogue” remains negation/gibberish.
|
| 352 |
|
| 353 |
-
##
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
- **`resolve(name)` is the only thing that turns a name into a path**, and it is
|
| 365 |
-
used by the upload route and the loaders alike. Basename only, `normpath`,
|
| 366 |
-
then a prefix check against the reference directory, then an extension
|
| 367 |
-
whitelist. Verified against absolute paths, `../` traversal, and traversal
|
| 368 |
-
carrying a legal extension — all refused.
|
| 369 |
-
- **The loaders return exactly what the sockets delivered** — float `[N,H,W,3]`
|
| 370 |
-
in 0..1 — so `_ref_frames`' resize, `_collect_ref_images`' dense-pack and
|
| 371 |
-
`store.tensor_digest`'s cache keying are all unchanged. That is why this was a
|
| 372 |
-
small diff rather than a rewrite.
|
| 373 |
-
|
| 374 |
-
**Pixels never enter a widget.** Only the basename is stored. PromptMasterLD
|
| 375 |
-
measured 1.68 MB of widget value for nine base64 thumbnails and ComfyUI then
|
| 376 |
-
failed to save the workflow at all; previews are `/view?...&subfolder=h3_refs`
|
| 377 |
-
URLs, which cost nothing to rebuild and survive a reload. `/view` also brings
|
| 378 |
-
Range support, which a `<video>` needs to seek — so no thumbnail route was
|
| 379 |
-
needed.
|
| 380 |
-
|
| 381 |
-
**`IS_CHANGED` is new and load-bearing.** ComfyUI caches a node's output on its
|
| 382 |
-
inputs, and a filename is a stable input even when the bytes behind it change:
|
| 383 |
-
overwrite `face.png` and the previous render would be served. It hashes
|
| 384 |
-
`name:mtime` for every referenced file. Deliberately **not** `float("nan")` —
|
| 385 |
-
that is the blunt version PromptMasterLD's studio node uses, and it would force
|
| 386 |
-
a full re-render of an expensive node on every queue.
|
| 387 |
-
|
| 388 |
-
**Widget order is part of the saved-workflow format.** `widgets_values` is a
|
| 389 |
-
**positional array** -- ComfyUI restores `value[i]` into `widget[i]` and never
|
| 390 |
-
looks at the name. The three `*_file` widgets were first added at the *top* of
|
| 391 |
-
`optional`, which shifted `hop_script`..`tone_compensate` by +3; every workflow
|
| 392 |
-
saved before that change then loaded `audio_pin_frames`' integer into `ref_plan`
|
| 393 |
-
and the editor died with `(text || "").trim is not a function` on load. That
|
| 394 |
-
throw was the lucky part -- `hop_script`, `shot_plan` and `tone_compensate` were
|
| 395 |
-
being silently misassigned too, and only the type mismatch made any of it
|
| 396 |
-
visible.
|
| 397 |
-
|
| 398 |
-
**New widgets go at the bottom of `optional`, always.** Old workflows are then
|
| 399 |
-
*short* rather than *misaligned*, and the new widget takes its default. There is
|
| 400 |
-
an append-only marker comment in `INPUT_TYPES` saying so. Note `seed` costs
|
| 401 |
-
**two** array slots, not one: the frontend appends `control_after_generate`
|
| 402 |
-
right after it.
|
| 403 |
-
|
| 404 |
-
Both plan parsers now coerce (`String(text ?? "")`) and reject non-object JSON
|
| 405 |
-
instead of dereferencing it, so a future misalignment shows an empty JSON tab
|
| 406 |
-
rather than aborting the whole workflow load.
|
| 407 |
-
|
| 408 |
-
**The hop cache needed no change at all.** `chain_salt` already digests the
|
| 409 |
-
actual tensors, so different pixels behind the same filename still move the key.
|
| 410 |
-
|
| 411 |
-
**Legacy plans cannot be migrated automatically.** An old `ref_image_N` held a
|
| 412 |
-
tensor from a `Load Image`; there is no filename to recover. `refs.py` keeps the
|
| 413 |
-
authored slot as `legacy_slot`, `parse_ref_plan` does **not** raise on it, and
|
| 414 |
-
both the rail row and `check()` say *"was wired to ref_image_3 — pick its
|
| 415 |
-
picture"*. Failing the parse would have stopped the editor opening the very plan
|
| 416 |
-
the author needs to repair.
|
| 417 |
-
|
| 418 |
-
## 9. Presentation (2026-08-28)
|
| 419 |
-
|
| 420 |
-
**The node collapsed on first click and stayed collapsed.** Two causes, both in
|
| 421 |
-
`installHeightGuard` (`js/editor/widget_utils.js`), both ported from
|
| 422 |
-
`PromptMasterLD/js/claude_prompt.js:6285-6370`:
|
| 423 |
-
|
| 424 |
-
- `computeLayoutSize().minWidth` reported a **constant**. The layout pass
|
| 425 |
-
re-reads it on every recompute — selecting the node is enough — and
|
| 426 |
-
faithfully re-declares the node at its stated minimum. It now tracks
|
| 427 |
-
`node.size[0]`. Safe against feedback: `max()` against a constant is a
|
| 428 |
-
fixpoint, not an accumulator, and width never feeds height.
|
| 429 |
-
- A stale `widget.width` shadows `node.width` forever after load, because
|
| 430 |
-
ComfyUI's DOM-widget position updater reads `(widget.width ?? node.width)`. A
|
| 431 |
-
live getter with a dropped setter makes the stale value unrepresentable.
|
| 432 |
-
|
| 433 |
-
Two more fixes alongside: `node.computeSize()[0]` is floored at the panel width
|
| 434 |
-
(with no widget declaring one, LiteGraph falls back to `NODE_WIDTH * 1.5 = 210`
|
| 435 |
-
and every resize command is free to crush the panel), and `domWidget.computeSize`
|
| 436 |
-
now answers a **width-passing** caller with the minimum height and a
|
| 437 |
-
**no-argument** caller with the live height. Reporting the live height to both
|
| 438 |
-
pins the resize-drag floor to the current height, so the node could only ever
|
| 439 |
-
grow.
|
| 440 |
-
|
| 441 |
-
**`chrome()`'s memo key was stale by construction.** It keyed on
|
| 442 |
-
`inputs|outputs|widgets.length`, none of which change when a widget is *hidden*
|
| 443 |
-
— so every panel height computed after `applyVisibility` used a chrome
|
| 444 |
-
measurement taken before it. The hidden count is now part of the key.
|
| 445 |
-
|
| 446 |
-
**The reference rail was permanently crushed, and its rescue was dead code.**
|
| 447 |
-
The 7-track grid needs ~536px and the node offers ~510px at `NODE_WIDTH 560`.
|
| 448 |
-
There was a `@container (max-width: 460px)` block written to relieve it — but
|
| 449 |
-
**nothing in the codebase declared `container-type`**, so the query had no
|
| 450 |
-
containment context and never matched. `.h3e-section` now declares
|
| 451 |
-
`container-type: inline-size`, and there are two breakpoints.
|
| 452 |
-
|
| 453 |
-
**The palette committed to one look.** It used to derive surfaces from the host
|
| 454 |
-
theme via `color-mix()` while hardcoding every accent — the intent was
|
| 455 |
-
light-theme safety, but `--h3-bg` mixed toward `#111827` and `--h3-sunken`
|
| 456 |
-
toward `#000`, so a light theme got dark blue-grey islands anyway. It is now the
|
| 457 |
-
PromptMasterLD `.ldp-root` system: `#0a0a0a`, one hairline `#2a2a2a`, zero
|
| 458 |
-
radius, one accent `#e8ff47`. **Changing `--h3-accent` moves the whole panel.**
|
| 459 |
-
Emphasis **inverts** (accent fill, `--h3-on-accent` ink) rather than tinting,
|
| 460 |
-
because an acid accent at 30% behind unchanged text is olive mud.
|
| 461 |
-
|
| 462 |
-
A cascade trap worth remembering: the override block was first inserted *before*
|
| 463 |
-
the reference-rail section, so `.h3e-chip-on`, `.h3e-inactive .h3e-ord` and
|
| 464 |
-
`.h3e-subj-badge` all kept winning on source order. Overrides live at the end of
|
| 465 |
-
the sheet now. Without a browser, a token audit script is the only thing that
|
| 466 |
-
catches this class of bug.
|
| 467 |
-
|
| 468 |
-
**The panel did not fill the node, and the mirror was why.** `installHeightGuard`
|
| 469 |
-
kept an independent `_h`, updated through an `onResize` hook, so that the
|
| 470 |
-
arrange pass could not feed its own growth. Measured during a drag: `onResize`
|
| 471 |
-
and `setSize` each fired 57 times while `_h` sat at 876 and `node.size[1]`
|
| 472 |
-
climbed past 1400. `measuring` was not stuck, which left one gate --
|
| 473 |
-
`Array.isArray(size)`. **This frontend's `node.size` is not a plain Array**, so
|
| 474 |
-
every write was skipped and `_h` held the install-time height forever. A 1911px
|
| 475 |
-
node had a 742px panel.
|
| 476 |
-
|
| 477 |
-
The repair was to delete the mirror, not fix the hook. `_h` existed only to
|
| 478 |
-
break the arrange loop, and
|
| 479 |
-
|
| 480 |
-
panelHeight() = max(minHeight, node.size[1] - chrome() - SLACK) // SLACK 8
|
| 481 |
-
|
| 482 |
-
breaks it outright: the pass wants `panelTop + panelHeight + 4`, and `panelTop`
|
| 483 |
-
is chrome minus the node's bottom padding, so a panel of exactly `size - chrome`
|
| 484 |
-
asks for up to 4px more than the node has on every frame -- the ~130px/frame
|
| 485 |
-
runaway the header comment records. With slack the inequality holds, the loop
|
| 486 |
-
settles, and the node's height simply *is* the panel's height. Nothing to keep
|
| 487 |
-
in sync, no hook to get wrong. `sync()` now only ever grows a too-short node;
|
| 488 |
-
the height is the user's to choose.
|
| 489 |
-
|
| 490 |
-
**RUN is pinned and always open.** `.h3e-root` used to be the scroll container
|
| 491 |
-
with all four sections inside it, which put RUN below the script -- out of view
|
| 492 |
-
on any workflow with more than two shots, and it is the section touched on every
|
| 493 |
-
queue. The root is now a flex column holding `.h3e-scroll` (the authoring
|
| 494 |
-
sections) and RUN outside it.
|
| 495 |
-
|
| 496 |
-
Two flex details, both of which cost a round trip to learn:
|
| 497 |
-
|
| 498 |
-
- **`.h3e-scroll` needs `min-height: 0`.** A flex item defaults to
|
| 499 |
-
`min-height: auto` and refuses to shrink below its content, which pushes RUN
|
| 500 |
-
off the bottom of the node instead of scrolling.
|
| 501 |
-
- **`.h3e-run` must be `flex: 0 0 auto`, never `0 1 auto`.** Flex divides a
|
| 502 |
-
deficit in proportion to each item's content height. The scroller's content is
|
| 503 |
-
far taller, so a shrinkable RUN loses most of the contest and clips its lower
|
| 504 |
-
groups. The scroller absorbs all the shrinking; its `min-height: 160px` is the
|
| 505 |
-
floor that stops RUN owning the panel and RUN's `max-height: 55%` is the
|
| 506 |
-
ceiling.
|
| 507 |
-
|
| 508 |
-
Always-open removed the only moment RUN re-read its widgets, so it now resyncs
|
| 509 |
-
on `api`'s `promptQueued` -- the client-side event that fires after
|
| 510 |
-
`control_after_generate` has bumped the seed, and the same one ComfyUI's own
|
| 511 |
-
change tracker uses. The listener is dropped in `node.onRemoved`.
|
| 512 |
-
|
| 513 |
-
**Not done:** PromptMasterLD's `--fsc` UI-scale multiplier, which makes every
|
| 514 |
-
dimension `calc(Npx * var(--fsc))` and puts a zoom slider on the panel. It is
|
| 515 |
-
mechanical churn across every rule in the sheet and was not worth doing blind.
|
| 516 |
-
|
| 517 |
-
## 10. Ported for public use (2026-08-29)
|
| 518 |
-
|
| 519 |
-
The prompting craft was written down and the pack was made installable by a
|
| 520 |
-
stranger. Four things, in the order they mattered.
|
| 521 |
-
|
| 522 |
-
**The shipped example workflows were broken.** All four in `workflows/` predated
|
| 523 |
-
the 2026-08-28 socket removal: twelve dead media inputs each, three `LoadImage`
|
| 524 |
-
nodes, and 21 widget values against a 28-widget node. Loading one is a new
|
| 525 |
-
user's *first* action, so this outranked any amount of documentation. They are
|
| 526 |
-
quarantined in `_disabled_custom_nodes/h3_legacy_workflows/` (the pack is not
|
| 527 |
-
under its own version control -- deleting would have been unrecoverable) and
|
| 528 |
-
replaced by two built from the verified `H3_Stress_6x7` structure:
|
| 529 |
-
|
| 530 |
-
- **`HandTieClips_Starter.json`** -- two hops, **no references at all**, runs the
|
| 531 |
-
moment the loaders are pointed at files. The empty register is deliberate: an
|
| 532 |
-
`@tag` whose picture is missing is a *hard* error in `resolve_tags`, so a
|
| 533 |
-
starter that shipped with tags would fail on first queue for everyone.
|
| 534 |
-
- **`HandTieClips_Showcase.json`** -- the six-hop continuity test, with the three
|
| 535 |
-
reference filenames generalised.
|
| 536 |
-
|
| 537 |
-
Both use **core ComfyUI plus this pack only**. The dev workflow reaches KJNodes
|
| 538 |
-
(`ModelPreviewOverrideKJ`, `MiniMaxLowVRAMAttention`) and PlagueKind
|
| 539 |
-
(`LTX_lora_loader`, `H3SLAAttention`, `H3AdaLNLoRAFix`); an example that fails
|
| 540 |
-
to load because of a pack the reader never asked for teaches nothing. The
|
| 541 |
-
speed stack is documented, not shipped.
|
| 542 |
-
|
| 543 |
-
**A declared-but-inactive `@tag` reported the wrong cause.** `check()` already
|
| 544 |
-
warned correctly that a picture was missing, but the run then died on
|
| 545 |
-
`resolve_tags` with *"unknown reference '@kitchen'"* -- pointing at the beat's
|
| 546 |
-
spelling, the one thing that was right. `resolve_tags` now takes `declared`
|
| 547 |
-
(every tag in the register, active this hop or not) and separates the two
|
| 548 |
-
failures. The old two-argument behaviour is unchanged when `declared` is
|
| 549 |
-
omitted.
|
| 550 |
-
|
| 551 |
-
**`PROMPTING.md` and `prompt_pack/`.** The craft rules were spread through
|
| 552 |
-
README prose; they are now a standalone guide, and a copy-paste system prompt
|
| 553 |
-
that gets a language model to emit valid plans. Two files under `prompt_pack/`
|
| 554 |
-
are **generated, never hand-written**:
|
| 555 |
-
|
| 556 |
-
- `tools/gen_schema.py` builds `SCHEMA.json` from `directives.VOCAB`,
|
| 557 |
-
`refs.RETENTION` and the duration table, and **asserts** against
|
| 558 |
-
`plan._SHOT_KEYS`, `refs.REF_FIELDS` and `refs.SUBJECT_FIELDS`. Add a camera
|
| 559 |
-
move and the schema follows; add a shot field and the generator fails loudly
|
| 560 |
-
rather than emitting a stale schema. `--check` is the CI form.
|
| 561 |
-
- `tools/gen_example.py` builds `EXAMPLE_6_HOP.md` from the showcase workflow,
|
| 562 |
-
including its hop/reference table, so the worked example and the shipped
|
| 563 |
-
workflow cannot disagree.
|
| 564 |
-
|
| 565 |
-
**Template patterns in the editor.** `js/editor/templates.js` plus a
|
| 566 |
-
**Templates** button in the SCRIPT header. They **append**, never replace --
|
| 567 |
-
replacing would be the one destructive control on the node, and stacking is how
|
| 568 |
-
a chain is actually built. `freeId()` mints the lowest unused `sN` because `id`
|
| 569 |
-
is the hop cache's pointer and two shots sharing one would make `locked` reuse
|
| 570 |
-
the wrong render.
|
| 571 |
-
|
| 572 |
-
No template contains an `@tag`, for the same reason the starter workflow has no
|
| 573 |
-
references. `tools/check_templates.py` extracts the patterns *out of the JS*
|
| 574 |
-
and runs them through `plan.parse_plan` and `plan.check_coherence`, and lints
|
| 575 |
-
every beat for negation -- a template that produced a plan the node rejects
|
| 576 |
-
would be worse than no templates, because a first-time author would blame their
|
| 577 |
-
own writing. It caught two: a beat reading "They stop at the window" (naming a
|
| 578 |
-
cessation, which is law 2) and a line of dialogue containing "did not".
|
| 579 |
-
|
| 580 |
-
That second one is worth recording as an open question: **whether the additive
|
| 581 |
-
prompt bites inside quoted dialogue was never tested.** The templates avoid it,
|
| 582 |
-
and `PROMPTING.md` says plainly that this is untested rather than inventing a
|
| 583 |
-
rule.
|
| 584 |
-
|
| 585 |
-
## 11. What chain_00057 taught (2026-08-29)
|
| 586 |
-
|
| 587 |
-
The six-hop showcase was rendered for the first time. Three failures, and the
|
| 588 |
-
one that mattered was a code gap the documentation had already claimed was
|
| 589 |
-
fixed.
|
| 590 |
-
|
| 591 |
-
**`locked` and `context` reached hop 1 only.** The register's whole promise is
|
| 592 |
-
that a subject's continuity text carries identity across a hop where the
|
| 593 |
-
photograph is absent. It did not. `subject_prose` is called under `if i == 0`,
|
| 594 |
-
and `_identity_lock` returns `""` when no subject-bearing ref is active -- so on
|
| 595 |
-
the showcase, hops 2, 3, 5 and 6 carried **no identity text of any kind**, and
|
| 596 |
-
hop 5, scheduled with no references at all, lost the character entirely.
|
| 597 |
-
Identity was riding on the pinned frames alone, which is the exact failure the
|
| 598 |
-
register exists to prevent.
|
| 599 |
-
|
| 600 |
-
`refs.continuity_line()` now emits that text on every hop 2+, and
|
| 601 |
-
`_assemble_next` injects it between the lock and the live-frame citation. The
|
| 602 |
-
reason it was suppressed in the first place is real and is preserved: naming
|
| 603 |
-
*pictures* on a pin-only hop sent the encoder back to the plates (chain_00034 --
|
| 604 |
-
commercial kitchen, grey shirt, no apron). So the new line carries **no
|
| 605 |
-
`<Picture N>` and no `<Subject N>`**. It is a description of what stays the
|
| 606 |
-
same, not a citation of anything. `<Subject N>` is excluded specifically because
|
| 607 |
-
there is no `subject_definitions` block on a continuation hop to bind it to.
|
| 608 |
-
|
| 609 |
-
Note this changes the assembled block on hop 2+ of every chain with a register,
|
| 610 |
-
so it invalidates the hop cache. That is correct, not a regression.
|
| 611 |
-
|
| 612 |
-
**Dialogue propagated through five hops.** Shot 1 ended on its spoken line with
|
| 613 |
-
`tail: ongoing`. The audio pin carries the previous hop's tail, so the last
|
| 614 |
-
second of hop 1 -- speech -- opened hop 2, and "action is still underway" was
|
| 615 |
-
the closing instruction; the model satisfied it with the action it could hear.
|
| 616 |
-
Nothing in hops 2-6 gave the audio anywhere else to go.
|
| 617 |
-
|
| 618 |
-
This is the guide's own law 3, broken by the plan written to demonstrate it. The
|
| 619 |
-
fix is authorial, not code: land the line **mid-hop** and leave a non-verbal
|
| 620 |
-
action running into the seam (the knife on the board), and give every
|
| 621 |
-
dialogue-free hop a narrowband sound of its own. Both `PROMPTING.md` and the
|
| 622 |
-
authoring prompt now carry this as a named rule, and the shipped templates were
|
| 623 |
-
rewritten to obey it.
|
| 624 |
-
|
| 625 |
-
**`join: continuous` across a location change morphed one room into the other.**
|
| 626 |
-
Hop 6 walked back from the hallway and the kitchen appeared mid-turn. A
|
| 627 |
-
continuous join asks for one unbroken take between two different rooms, which
|
| 628 |
-
is not a thing. `match_cut` is what a walk through a doorway is. The beat also
|
| 629 |
-
read "steps into @kitchen" -- the container phrasing the README warns produces a
|
| 630 |
-
composite of the photograph -- and now names the counter *in* the kitchen.
|
| 631 |
-
|
| 632 |
-
Hop 5's beat is unchanged in the revised plan **on purpose**: it is the
|
| 633 |
-
measurement, and changing it would forfeit the comparison.
|
| 634 |
-
|
| 635 |
-
The revised plan is in the shipped showcase and in
|
| 636 |
-
`user/default/workflows/H3_Stress_6x7_v2.json`; the original that produced
|
| 637 |
-
chain_00057 is left alone so the A/B survives.
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
## 12. Renamed to Hand Tie Clips (2026-08-29)
|
| 641 |
-
|
| 642 |
-
The pack was `ComfyUI-H3-Ref-Chain`. It is now `ComfyUI-Hand-Tie-Clips`, and the
|
| 643 |
-
four registered ids moved with it:
|
| 644 |
-
|
| 645 |
-
| was | is |
|
| 646 |
-
|---|---|
|
| 647 |
-
| `H3RefChain` | `HandTieClips` |
|
| 648 |
-
| `H3ContinuityState` | `HTCContinuityState` |
|
| 649 |
-
| `H3ChainPreview` | `HTCChainPreview` |
|
| 650 |
-
| `H3ToneCompensate` | `HTCToneCompensate` |
|
| 651 |
-
|
| 652 |
-
Display names are unchanged (`H3 Ref2VA Chain`, `H3 Chain Preview`, ...) because
|
| 653 |
-
the pack only drives MiniMax H3 checkpoints and a name that hides that costs
|
| 654 |
-
somebody an afternoon. The menu category is `Hand Tie Clips`. `TAG` -- and so the
|
| 655 |
-
console prefix -- is `[HandTieClips]`.
|
| 656 |
-
|
| 657 |
-
**The old ids are still registered, as `DEPRECATED` subclasses.** A type id is
|
| 658 |
-
what every saved `.json` carries, and an unregistered one is a red missing-node
|
| 659 |
-
box, not a warning. A plain alias in `NODE_CLASS_MAPPINGS` would have worked but
|
| 660 |
-
listed each node twice in search: ComfyUI falls back to the mapping key when
|
| 661 |
-
`NODE_DISPLAY_NAME_MAPPINGS` has no entry for it. Subclassing and setting
|
| 662 |
-
`DEPRECATED = True` gets both -- `server.py:783` publishes `deprecated: True`,
|
| 663 |
-
and the frontend's `Comfy.Node.ShowDeprecated` (off by default) keeps it out of
|
| 664 |
-
search while leaving it fully functional in workflows that name it.
|
| 665 |
-
|
| 666 |
-
**The JS had to learn both ids or the aliases would have been worse than
|
| 667 |
-
useless.** `js/h3_ref_chain_ui.js` and `js/h3_chain_preview.js` each compared
|
| 668 |
-
`nodeData.name` against a single string; a legacy node would have loaded with no
|
| 669 |
-
editor at all, which looks exactly like the rename having broken the pack. Both
|
| 670 |
-
now test membership of a `Set`. `js/h3_chain_preview.js` needs it twice: once
|
| 671 |
-
for its own type, once for `CHAIN_TYPES`, which is how a preview walks back up
|
| 672 |
-
`images` to find the chain feeding it.
|
| 673 |
-
|
| 674 |
-
**What deliberately kept the `h3` naming**, and must not be "finished" later:
|
| 675 |
-
module filenames (`h3_ref_chain.py`), the `h3e-` CSS class prefix and `--h3-`
|
| 676 |
-
tokens (515 occurrences -- one missed class silently breaks styling), the
|
| 677 |
-
`/h3_ref_chain/*` routes, the `h3_refchain_preview` event name, and
|
| 678 |
-
`input/h3_refs`. Renaming that folder would orphan every reference photo already
|
| 679 |
-
on disk.
|
| 680 |
-
|
| 681 |
-
The two shipped workflows moved to `HandTieClips_Starter.json` /
|
| 682 |
-
`HandTieClips_Showcase.json` and were rewritten onto the new ids, with their
|
| 683 |
-
`SaveVideo` prefix now `video/HANDTIECLIPS/chain`. The three under
|
| 684 |
-
`user/default/workflows/` were left on the legacy ids on purpose: re-running
|
| 685 |
-
`H3_Stress_6x7_v2.json` is then a live test of the alias path, and their existing
|
| 686 |
-
renders stay together under `output/video/H3REFCHAIN/`.
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
## 13. The on-canvas board (2026-08-29)
|
| 690 |
-
|
| 691 |
-
`workflows/HandTieClips_Starter.json` carries six `MarkdownNote` cards to the
|
| 692 |
-
left of the loaders, wrapped in a group titled READ ME. The text lives in
|
| 693 |
-
`tools/notes.py` and is written into the workflow by `tools/build_notes.py`.
|
| 694 |
-
|
| 695 |
-
Why on the canvas: the craft was in `PROMPTING.md`, `prompt_pack/` and the
|
| 696 |
-
Templates panel, and all three require leaving the graph. The rules that decide
|
| 697 |
-
whether a first render works are needed while beats are being written, which is
|
| 698 |
-
on the canvas. The cards are a **condensation**, not a copy -- `PROMPTING.md`
|
| 699 |
-
stays the authority and every card says so.
|
| 700 |
-
|
| 701 |
-
Mechanics worth not rediscovering:
|
| 702 |
-
|
| 703 |
-
- `MarkdownNote` is a core virtual node. The frontend renders it with `marked`
|
| 704 |
-
at `gfm: true` and sanitises with DOMPurify, so headings, GFM tables, bold and
|
| 705 |
-
code fences all work. It needs frontend >= ~1.16.
|
| 706 |
-
- Cards are identified by `properties.htc_card`, which is what makes
|
| 707 |
-
`build_notes.py` idempotent -- it drops marked nodes before writing, so
|
| 708 |
-
re-running replaces the board instead of stacking a second copy.
|
| 709 |
-
- `extra.ds` is **restored** on load, not fitted. Without setting it, a board at
|
| 710 |
-
negative x sits off-screen and is never found. Screen is
|
| 711 |
-
`(world + offset) * scale`, so the offset is what brings it into view.
|
| 712 |
-
- Group serialisation is `{id?, title, bounding:[x,y,w,h], color?, font_size?,
|
| 713 |
-
locked?}` -- confirmed against the frontend's own zod schema, not guessed.
|
| 714 |
-
|
| 715 |
-
**`widgets_values_named`, found while doing this.** Both shipped workflows
|
| 716 |
-
carried a stale copy: `chains: 3`, `duration: 10 s`,
|
| 717 |
-
`control_after_generate: randomize`, and a legacy `ref_plan` naming pictures that
|
| 718 |
-
do not ship -- 25 entries against a 28-widget node. It came from the builder
|
| 719 |
-
deep-copying nodes out of the dev workflow and overwriting only
|
| 720 |
-
`widgets_values`. Dormant while `Comfy.Workflow.NamedValuesRestore` stays off
|
| 721 |
-
(experimental, default false), but anyone who turned that on would have loaded a
|
| 722 |
-
Starter that randomizes its seed and dies on a missing reference.
|
| 723 |
-
`build_notes.py` strips it from both, and `tools/check_workflows.py` now fails if
|
| 724 |
-
it ever comes back.
|
| 725 |
-
|
| 726 |
-
`tools/check_workflows.py` is the promoted version of the validator that guarded
|
| 727 |
-
the shipped workflows against socket/widget drift. It also checks the board:
|
| 728 |
-
every card marked and non-empty, exactly one group, the group enclosing every
|
| 729 |
-
card, and no card reaching past x=0 onto the loaders.
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
## 14. The turbo stack ships in the examples (2026-08-29)
|
| 733 |
-
|
| 734 |
-
Both shipped workflows now carry the dev graph's full MODEL wire:
|
| 735 |
-
|
| 736 |
-
UNETLoader -> LTX_lora_loader -> H3AdaLNLoRAFix -> MiniMaxLowVRAMAttention
|
| 737 |
-
-> H3SLAAttention -> ModelPreviewOverrideKJ -> HandTieClips
|
| 738 |
-
|
| 739 |
-
and **CLIP reaches the chain from the LoRA loader, not the encoder** -- that is
|
| 740 |
-
what makes the text half of every LoRA land, and it is the wire most likely to
|
| 741 |
-
get quietly "fixed" back to the encoder by someone tidying the graph.
|
| 742 |
-
|
| 743 |
-
This reverses the earlier "core ComfyUI and this pack only" rule for the
|
| 744 |
-
examples, at the user's direction: `steps` is 7, which only works with a turbo
|
| 745 |
-
LoRA, so an example without the LoRA stack is not a graph anyone can run at the
|
| 746 |
-
settings it ships with. PlagueKind supplies the loader, the AdaLN fix and SLA;
|
| 747 |
-
KJNodes supplies Low VRAM Attention and the preview override.
|
| 748 |
-
|
| 749 |
-
`tools/build_speed_stack.py` inserts and rewires the five nodes idempotently
|
| 750 |
-
(marker `properties.htc_speed`), rebuilding the plain loader -> chain shape
|
| 751 |
-
first so it is reentrant from either state. `tools/check_workflows.py` walks
|
| 752 |
-
**both** wires link by link -- a patch node that is present but bypassed round
|
| 753 |
-
the side looks right on the canvas and does nothing.
|
| 754 |
-
|
| 755 |
-
`H3SLAAttention` widget values are written out in full including
|
| 756 |
-
`reference_protection`, which post-dates the dev workflow's saved values. That
|
| 757 |
-
workflow has 10 entries against an 11-widget node, which is the positional rule
|
| 758 |
-
working as intended: the new widget was appended last, so old values still line
|
| 759 |
-
up and the missing one falls back to its default. The trailing `""` on the LoRA
|
| 760 |
-
loader and the preview override is carried over verbatim for the same reason --
|
| 761 |
-
a value past the last widget is ignored, and dropping one that turns out to
|
| 762 |
-
belong to a widget would shift every value after it.
|
| 763 |
-
|
| 764 |
-
`head_chunks` ships at 4 (the node's own default, safer on unknown VRAM) rather
|
| 765 |
-
than the 2 used here. Two files have to be on disk as well: the turbo LoRA the
|
| 766 |
-
loader names, and `taeh3.safetensors` for the preview override's `tiny_vae`.
|
| 767 |
-
|
| 768 |
-
## 15. What a shipped diagnosis looked like (2026-08-29)
|
| 769 |
-
|
| 770 |
-
The editor UI vanished after the folder rename -- raw widget boxes, no panel.
|
| 771 |
-
Not a code fault. ComfyUI had been restarted while the pack was still
|
| 772 |
-
`ComfyUI-H3-Ref-Chain`, so the running process held
|
| 773 |
-
`python_module: custom_nodes.ComfyUI-H3-Ref-Chain` and served its web assets
|
| 774 |
-
from a path the move had deleted. `/object_info` had all eight node types;
|
| 775 |
-
`/extensions` listed none of the pack's JS and a direct fetch 404'd.
|
| 776 |
-
|
| 777 |
-
Worth knowing for next time: `/object_info` carries `python_module`, which is
|
| 778 |
-
the fastest way to find out **which copy of a pack a running server actually
|
| 779 |
-
loaded**, and `/extensions` plus a direct fetch of one script separates "the JS
|
| 780 |
-
is broken" from "the JS is not being served at all".
|
| 781 |
-
|
| 782 |
-
## 16. What the Rain Kitchen renders taught (2026-08-29)
|
| 783 |
-
|
| 784 |
-
Three six-hop renders of the same 6x7 s chain, each isolating one variable. All
|
| 785 |
-
three findings are now in `PROMPTING.md`, the troubleshooting table, the
|
| 786 |
-
authoring prompt and the on-canvas board.
|
| 787 |
-
|
| 788 |
-
**Identity drift is permanent, and the old advice was backwards.** This file and
|
| 789 |
-
`PROMPTING.md` used to say a face plate riding a later hop *beats the pin* and
|
| 790 |
-
that `shots` should therefore be kept tight. chain_00059 falsified it: hop 4
|
| 791 |
-
carried a face plate photographed in a *different kitchen* through a walking
|
| 792 |
-
medium shot and held cleanly, while hop 5 — scheduled with no references at
|
| 793 |
-
all — came back a different person, and hop 6 never recovered even though a
|
| 794 |
-
place plate rode it and restored the room instantly. `locked` holds a face that
|
| 795 |
-
is still right; only a plate rebuilds one that is gone. **Face refs go on every
|
| 796 |
-
hop.** The old advice survives for *place* plates only.
|
| 797 |
-
|
| 798 |
-
**A beat must survive an over-delivered hop.** A hop routinely does more than it
|
| 799 |
-
was asked. Shot 3 asked for "a first slow step along the counter" and delivered
|
| 800 |
-
the whole walk; shot 4 was then handed an instruction its own live frame had
|
| 801 |
-
already satisfied, and the only way to obey was to reset the scene — a hard
|
| 802 |
-
cut 1.5 s *into* the hop, not at the seam, because `_assemble_next` holds the
|
| 803 |
-
incoming frames for a short beat first. Give one hop the whole movement, and
|
| 804 |
-
write the next beat true from either ending ("she *reaches* the window", "she
|
| 805 |
-
takes up the bowl *again*").
|
| 806 |
-
|
| 807 |
-
**A noun with no adjective drifts.** Three hops said only "the bowl"; it came
|
| 808 |
-
back stainless steel. Naming it "the white bowl" everywhere *and* stating it in
|
| 809 |
-
`context` as a property (never a location — "stays in her hands" fights the
|
| 810 |
-
beat the moment she puts it down) held it for six hops. The re-run then proved
|
| 811 |
-
the rule by accident: `context` read "the apron stays tied over the **grey**
|
| 812 |
-
t-shirt", and the t-shirt held all six hops while the apron, one clause away
|
| 813 |
-
with no colour, had turned denim blue by hop 6.
|
| 814 |
-
|
| 815 |
-
### The code change this bought
|
| 816 |
-
|
| 817 |
-
`refs.resolve_tags` now takes `subject_names` and the node passes it on
|
| 818 |
-
continuation hops only. `<Subject N>` is bound by `subject_definitions:`, which
|
| 819 |
-
is hop-1 material — so on hop 4 the ordinal dangled, exactly the same defect
|
| 820 |
-
as the undescribed bowl. From hop 2 a person tag resolves to the subject's
|
| 821 |
-
`name` instead ("The cook walks down the hallway"), which binds to the
|
| 822 |
-
`continuity_line` sentence every continuation hop already carries. No name means
|
| 823 |
-
the old `<Subject N>` fallback, so nothing regresses.
|
| 824 |
-
|
| 825 |
-
`tools/check_prompts.py` mirrors the new call, and gained the banned-word check
|
| 826 |
-
that previously ran only against `templates.js`. That gap is how the shipped
|
| 827 |
-
Showcase carried "**None** of the kitchen is visible" (law 1 — it *adds* a
|
| 828 |
-
kitchen at cfg 1.0) and "She **stops** at the window" (law 2) through six
|
| 829 |
-
versions. Both are fixed, and the Showcase's face ref now rides all six hops.
|
| 830 |
-
|
| 831 |
-
## 17. The pre-beta audit (2026-08-29)
|
| 832 |
-
|
| 833 |
-
Read for what a stranger hits, not for what we already check. Six findings; the
|
| 834 |
-
first two would each have cost a tester a run.
|
| 835 |
-
|
| 836 |
-
**The docs promised a stop the code never did.** `refs.check()` is *"warn, never
|
| 837 |
-
raise"*, and its caller only printed. A ref naming a file that is not in
|
| 838 |
-
`h3_refs` had its slot skipped and the chain rendered on — all six hops, with
|
| 839 |
-
the reference silently inactive, which is precisely the uncontrolled output the
|
| 840 |
-
register exists to prevent. Four shipped surfaces said otherwise, including the
|
| 841 |
-
Showcase's own note: *"the run stops and names the reference it could not find.
|
| 842 |
-
Nothing guesses."*
|
| 843 |
-
|
| 844 |
-
Fixed in the code rather than the docs, because there is no reading under which
|
| 845 |
-
rendering without a named-but-absent picture is what the author meant. New
|
| 846 |
-
`refs.missing_files()` returns `(tag, file)` for every ref whose named file did
|
| 847 |
-
not load; `h3_ref_chain.py` raises on it, after printing the register table so
|
| 848 |
-
the error arrives with its context. **The distinction that matters:** a ref with
|
| 849 |
-
*no* `file` stays a warning — the Starter ships that way on purpose so it runs
|
| 850 |
-
before any pictures exist. Only a *named* file that is absent is fatal.
|
| 851 |
-
|
| 852 |
-
**The on-canvas board never got §16's corrections.** `tools/notes.py` still
|
| 853 |
-
carried the row chain_00059 falsified — *"a plate riding a hop it does not
|
| 854 |
-
belong on, beating the pin → tighten `shots`"* — and was missing all three rows
|
| 855 |
-
`PROMPTING.md` gained. The board is what a beginner actually reads, so it was
|
| 856 |
-
shipping the inverse of the rule. Lesson: `PROMPTING.md` is the authority, but
|
| 857 |
-
`tools/notes.py` is a **second copy** of the same craft, and a correction is not
|
| 858 |
-
finished until both move. `build_notes.py` regenerates the Starter's cards *and*
|
| 859 |
-
the Showcase's `Note` from `notes.SHOWCASE_NOTE`, so one rebuild covers both.
|
| 860 |
-
|
| 861 |
-
The rest were packaging, not craft: the MIT `LICENSE` named no copyright holder;
|
| 862 |
-
`pyproject.toml` lacks both fields a Registry publish needs (`PublisherId` and a
|
| 863 |
-
`[project.urls] Repository`), now commented in place; the `README` had **no
|
| 864 |
-
install section at all**; `ComfyUI-H3-Motion-Context` was absent from Needs
|
| 865 |
-
despite the intro calling it the primary guidance path, so testers land on the
|
| 866 |
-
`MiniMaxH3AddGuide` fallback without knowing they changed code paths; and the
|
| 867 |
-
example workflows name quantised checkpoints by filename with no note that they
|
| 868 |
-
are one valid set among many, which reads as a broken graph rather than a
|
| 869 |
-
missing file.
|
| 870 |
-
|
| 871 |
-
Clean on the same pass, worth not re-checking: no personal data anywhere, no
|
| 872 |
-
hardcoded local paths in runtime code (only two dev-tool docstrings), 3.10-safe
|
| 873 |
-
syntax, no third-party dependencies, Motion-Context absence handled with fork
|
| 874 |
-
detection, `WEB_DIRECTORY` present, `h3_refs` auto-created.
|
| 875 |
-
|
| 876 |
-
## 18. What a 27B model got wrong, and what the prompt taught it (2026-08-29)
|
| 877 |
-
|
| 878 |
-
Qwen3 27B at temperature 0.3 was given an 8 x 15 s concept. The `ref_plan` came
|
| 879 |
-
back structurally perfect -- both subjects named, face plate on all eight hops,
|
| 880 |
-
places scheduled off the hops they do not belong on, zero register warnings. The
|
| 881 |
-
`shot_plan` carried four defects, and tracing each one back to
|
| 882 |
-
`prompt_pack/AUTHORING_PROMPT.md` found more than four gaps.
|
| 883 |
-
|
| 884 |
-
**The prompt taught one of the bugs.** Rule 9 illustrated a beat as
|
| 885 |
-
``"stands at the counter in `@kitchen`"`` -- backticks and all, because it was
|
| 886 |
-
the one place a tag appeared inside a beat. The model copied the formatting, and
|
| 887 |
-
literal backticks reach the encoder. Fixed, plus a rule that a beat is plain
|
| 888 |
-
prose.
|
| 889 |
-
|
| 890 |
-
**Beat length had no guidance at all.** The only budget in the file was for
|
| 891 |
-
dialogue, so a no-dialogue plan had nothing to size against and every beat came
|
| 892 |
-
back at 22-28 words regardless of a 15 s hop. There is now a word table -- and
|
| 893 |
-
an honest note on it: the shipped plans run 37-39 words at *both* 5 s and 7 s,
|
| 894 |
-
so beat length is near-constant across the only two hop lengths ever rendered.
|
| 895 |
-
The words-per-second reading that gives 70-100 for a 15 s hop is a reasoned
|
| 896 |
-
extrapolation, not a measurement, and the table says so.
|
| 897 |
-
|
| 898 |
-
**Two rules were simply missing.** Nothing told the model to state a visual style
|
| 899 |
-
-- for a "2D anime, Ufotable" concept neither returned block contained the word
|
| 900 |
-
anime, so the text asked for photoreal while the pictures asked for anime. And
|
| 901 |
-
the over-delivery rule from section 16 had never been carried into the prompt at
|
| 902 |
-
all, only into `PROMPTING.md` and the board.
|
| 903 |
-
|
| 904 |
-
**Two statements were wrong.** The prompt said "at most 9 pictures on any one
|
| 905 |
-
hop"; `parse_ref_plan` counts `len(refs)` over the **whole plan**. The code's own
|
| 906 |
-
error message said "on one hop" too, and has been corrected to match what it
|
| 907 |
-
checks. And the `file` bullet still told the model to invent placeholder
|
| 908 |
-
filenames without noting that, as of 0.3.1, a named file absent from `h3_refs`
|
| 909 |
-
**stops the run**.
|
| 910 |
-
|
| 911 |
-
Law 2 is the interesting non-fix. It is stated plainly, with the exact example
|
| 912 |
-
"The cook stops talking", and the model wrote "stops and looks up" anyway. A
|
| 913 |
-
principle is not something a 27B reliably applies to its own output; the literal
|
| 914 |
-
word list `check_prompts.py` enforces now appears in the prompt, because a word
|
| 915 |
-
list is checkable and a principle is not.
|
| 916 |
-
|
| 917 |
-
## 19. What 114 seconds of rendered film and two 27B models taught (2026-08-29)
|
| 918 |
-
|
| 919 |
-
Three sources landed together: a rendered 8 x 15 s anime chain (`chain_00003`,
|
| 920 |
-
2742 frames, 114.25 s), and Qwen and Gemma each answering two test prompts
|
| 921 |
-
written to trip specific rules. Shipped as **0.3.3**.
|
| 922 |
-
|
| 923 |
-
### The node was fighting every stylised plan
|
| 924 |
-
|
| 925 |
-
`directives.py` prepended `ESTABLISH = "Live-action, natural light, one
|
| 926 |
-
continuous take."` to hop 1 **unconditionally**. At cfg 1.0 with no negative
|
| 927 |
-
branch that is additive, and it landed *ahead* of the style declaration rule 12
|
| 928 |
-
requires. A stop-motion puppet plan compiled to "Live-action, natural light,
|
| 929 |
-
one continuous take. ... Hand-drawn stop-motion puppet animation in felt and
|
| 930 |
-
painted wood", and the two fought.
|
| 931 |
-
|
| 932 |
-
It also explains the anime chain's opening: hop 1 rendered as bright
|
| 933 |
-
naturalistic daylight (**mean luma 72**) against a night plan *and* a night
|
| 934 |
-
place plate, then fell to 46 on hop 2 the moment ESTABLISH stopped riding. That
|
| 935 |
-
had been read as drift; it was the node.
|
| 936 |
-
|
| 937 |
-
Never surfaced because both shipped workflows are live-action.
|
| 938 |
-
|
| 939 |
-
Fixed twice over, because either alone leaves a hole: an `establish` widget
|
| 940 |
-
(appended **last** in `optional`, per the positional-widget rule in section 8),
|
| 941 |
-
and `directives.declares_own_medium()` / `establish_for()`, which drop the
|
| 942 |
-
default when shot 1's opening names a medium. A model-authored plan never
|
| 943 |
-
touches a widget, which is why the automatic arm is the one that matters.
|
| 944 |
-
|
| 945 |
-
### Luminance drifts, and it only goes one way
|
| 946 |
-
|
| 947 |
-
Mean luma per hop across the eight: **72, 46, 35, 17, 11, 11, 19, 14**. Setting
|
| 948 |
-
the ESTABLISH artefact aside, hops 2-6 still slide 46 -> 11. The combat, the
|
| 949 |
-
point of the film, plays at the bottom of it.
|
| 950 |
-
|
| 951 |
-
This is the colour law one level up. `locked` holds a face; nothing holds an
|
| 952 |
-
exposure. Each hop inherits the last frame and darkens it slightly and the
|
| 953 |
-
error compounds. **Restate the light as a positive property in every beat** --
|
| 954 |
-
naming a light *source* ("pale moonlight") does not set a level, and at cfg 1.0
|
| 955 |
-
it only adds a moon.
|
| 956 |
-
|
| 957 |
-
### The seams were never the problem
|
| 958 |
-
|
| 959 |
-
7 of 7 hop joins are invisible: largest frame-to-frame difference at any seam is
|
| 960 |
-
13.6, inside the range of ordinary in-shot motion, and two seams score below the
|
| 961 |
-
film's own mean. Identity held 114 seconds on face plates riding every hop.
|
| 962 |
-
|
| 963 |
-
The one hard cut in the film is **inside** hop 4, 3.25 s in, at 7.1 sd -- more
|
| 964 |
-
than double any other jump. Shot 3 ended "ahead the trunks begin to thin toward
|
| 965 |
-
open ground"; shot 4 opened "Across the flat moonlit stone of @arena_clearing
|
| 966 |
-
the two of them square off". Hop 4 was handed a live frame of a man among trees
|
| 967 |
-
and a beat asserting he stood on open stone, held the forest for 3.25 s, then
|
| 968 |
-
reset the scene. The plan was clean under `check_coherence` and the banned-word
|
| 969 |
-
scan. Hence `plan.check_place_handoff()`.
|
| 970 |
-
|
| 971 |
-
### What the two models did
|
| 972 |
-
|
| 973 |
-
Near-identical answers from Qwen and Gemma on the same prompt -- same beats to
|
| 974 |
-
the word in 5 of 6 shots, same defects, same invented justification. The prompt
|
| 975 |
-
is prescriptive enough to collapse two models onto one answer; a shared blind
|
| 976 |
-
spot is then invisible from output alone.
|
| 977 |
-
|
| 978 |
-
**The word table was inert.** Both models, asked for six 15 s hops, returned
|
| 979 |
-
beats averaging **54 words** against a 70-100 band -- every beat under the floor
|
| 980 |
-
-- and the same models on much shorter hops returned 40-48. Beat length is
|
| 981 |
-
near-invariant to hop length in model output, now measured in a third setting.
|
| 982 |
-
A reference table does not move a model with a prior on paragraph length; the
|
| 983 |
-
rule is now an instruction to count, with a worked 74-word example.
|
| 984 |
-
|
| 985 |
-
**The banned-word list beat the principle, and made things worse.** Zero literal
|
| 986 |
-
banned words in all four plans -- and "The storm's roar begins to fade...
|
| 987 |
-
raindrops strike the glass with decreasing force" and "The storm has passed".
|
| 988 |
-
The list taught token avoidance and handed them a box to tick. It is now framed
|
| 989 |
-
as crude examples of an idea, with the test stated as: is this happening, or has
|
| 990 |
-
it finished happening?
|
| 991 |
-
|
| 992 |
-
**Both abandoned the second location.** Each plated the opening place, moved the
|
| 993 |
-
story elsewhere, gave the new place no plate, and justified it with a rule that
|
| 994 |
-
does not exist ("to avoid conflicting with the frame pin of the new space");
|
| 995 |
-
Qwen cited "rule 8/9", which does not say that. In the lighthouse plan that left
|
| 996 |
-
the lamp room -- four of six hops -- on beat text alone.
|
| 997 |
-
|
| 998 |
-
### `check_place_handoff` has two arms, and both were narrowed by real plans
|
| 999 |
-
|
| 1000 |
-
1. **Handoff.** Shot N names a place tag shot N-1 never mentions, and shot N's
|
| 1001 |
-
own beat does not carry the journey. The arrival vocabulary had to widen: the
|
| 1002 |
-
Showcase's shot 6 ("walks back along the hallway and through the doorway to
|
| 1003 |
-
the counter in @kitchen") is correct and was being flagged.
|
| 1004 |
-
2. **Abandonment**, not gaps. Warning on any unplated hop also flagged the
|
| 1005 |
-
Showcase, which deliberately walks her down an unplated hallway on 4-5 and
|
| 1006 |
-
returns the kitchen plate on 6. The rule that survives contact: warn only
|
| 1007 |
-
when the plates stop and **never resume**, so the film ends somewhere no
|
| 1008 |
-
picture describes.
|
| 1009 |
-
|
| 1010 |
-
Both shipped workflows are clean under the final version; the anime plan raises
|
| 1011 |
-
exactly one warning, on shot 4.
|
| 1012 |
-
|
| 1013 |
-
### Audio, for the record
|
| 1014 |
-
|
| 1015 |
-
No background music and no speech, confirmed on the spectrogram: broadband
|
| 1016 |
-
transients and noise, no harmonic bands. Seam levels hold within +/-2 dB on five
|
| 1017 |
-
of seven joins, and the two exceptions are drops the beats themselves ask for.
|
| 1018 |
-
The real audio issue is range, not seams: peaks reach -1.2 dBFS during the fight
|
| 1019 |
-
while hop 8 averages -42 dBFS. There is no audio equivalent of
|
| 1020 |
-
`HTCToneCompensate`. Left open.
|
| 1021 |
-
## 20. Seven features built blind (2026-08-30)
|
| 1022 |
-
|
| 1023 |
-
Built in one pass with no browser and no GPU render available -- the user was
|
| 1024 |
-
away and explicitly asked for the work anyway. Everything below is verified by
|
| 1025 |
-
offline execution; **nothing has been seen in ComfyUI and no chain has been
|
| 1026 |
-
rendered with it.** Read section 19 first for the measurements that motivated
|
| 1027 |
-
most of it.
|
| 1028 |
-
|
| 1029 |
-
### What shipped
|
| 1030 |
-
|
| 1031 |
-
| # | thing | where |
|
| 1032 |
-
|---|---|---|
|
| 1033 |
-
| 1 | `tone_compensate=anchor` + `tone_anchor` strength | `tone.py`, wired in `h3_ref_chain.py` |
|
| 1034 |
-
| 2 | `dry_run` -- compile every prompt, render nothing | `h3_ref_chain.py` |
|
| 1035 |
-
| 3 | `contact_sheet` -- a fourth IMAGE output | new `sheet.py` |
|
| 1036 |
-
| 4 | `render_through` -- stop after hop N | `h3_ref_chain.py` |
|
| 1037 |
-
| 5 | `quality=draft` -- 0.3 MP, 6 steps | `h3_ref_chain.py` |
|
| 1038 |
-
| 6 | **H3 Seam Report** node | new `seam.py` |
|
| 1039 |
-
| 7 | over-delivery lint | `plan.py` |
|
| 1040 |
-
|
| 1041 |
-
Five new widgets, appended LAST (29 -> 34 values). One new output, appended
|
| 1042 |
-
LAST (3 -> 4). Both rules are in section 9; both were obeyed.
|
| 1043 |
-
|
| 1044 |
-
### The anchor, and why it is not just another tone mode
|
| 1045 |
-
|
| 1046 |
-
frame_shift/gain_bias/lut are **seam-local**: they cancel the denoiser's tone
|
| 1047 |
-
bias on the overlap, which makes each join exact. They cannot see the exposure
|
| 1048 |
-
falloff *inside* a hop, and that is what compounds -- hop N darkens across its
|
| 1049 |
-
own frames, hands the darker tail to hop N+1, and every individual seam stays
|
| 1050 |
-
perfect while the film dims. Section 19 measured 46 -> 11 across hops 2-6.
|
| 1051 |
-
|
| 1052 |
-
Worth stating plainly because it is counter-intuitive: **a synthetic 8-hop
|
| 1053 |
-
chain showed frame_shift making the total slide WORSE** (66/255 vs 35/255 with
|
| 1054 |
-
correction off). That is correct behaviour, not a bug. The denoiser's per-hop
|
| 1055 |
-
bias happened to lift; cancelling it removed a lift that had been partly
|
| 1056 |
-
offsetting the falloff. Seam correction fixes seams. It was never a level
|
| 1057 |
-
control and should not be read as one.
|
| 1058 |
-
|
| 1059 |
-
`anchor` = frame_shift + a second stage pulling each hop's mean back toward
|
| 1060 |
-
**hop 1's**. Two properties make it safe to stack:
|
| 1061 |
-
|
| 1062 |
-
- the pull **ramps from zero** over `ANCHOR_RAMP` (48f) frames, so frame 0 of
|
| 1063 |
-
a hop is returned untouched and the seam stays exactly as frame_shift left
|
| 1064 |
-
it. Without the ramp a per-hop constant offset re-introduces precisely the
|
| 1065 |
-
step frame_shift just removed -- this is the whole design, and the trap
|
| 1066 |
-
anyone re-implementing it will fall into;
|
| 1067 |
-
- it is **capped** (`ANCHOR_MAX_SHIFT`, 0.06) and scaled by `tone_anchor`
|
| 1068 |
-
(0.35), so a slide is corrected across several hops instead of one hop
|
| 1069 |
-
snapping back.
|
| 1070 |
-
|
| 1071 |
-
The correction needs no carry variable between hops: because it is applied
|
| 1072 |
-
before `prev_imgs` is taken, the next hop's seam correction matches the
|
| 1073 |
-
already-corrected tail and the offset propagates on its own.
|
| 1074 |
-
|
| 1075 |
-
On the synthetic chain: slide 66 -> 18/255, worst seam step 2.22 -> 2.13/255.
|
| 1076 |
-
The seam did not regress, which is the property that mattered.
|
| 1077 |
-
|
| 1078 |
-
Intent is indistinguishable from drift from the inside, hence the per-shot
|
| 1079 |
-
`tone` field: `"free"` skips one hop's pull, `"rebase"` moves the anchor onto
|
| 1080 |
-
that hop. A deliberate walk into a cellar needs `rebase` or the chain spends
|
| 1081 |
-
the rest of the film brightening it back.
|
| 1082 |
-
|
| 1083 |
-
### dry_run: what it must not touch
|
| 1084 |
-
|
| 1085 |
-
The value is that it costs seconds, so every expensive thing is guarded:
|
| 1086 |
-
`MiniMaxH3SigmaShift`, `KSamplerSelect`, `BasicScheduler`, `_model_fingerprint`
|
| 1087 |
-
(it hashes patched weights), the hop store, and -- the big one -- the master
|
| 1088 |
-
preallocation. `master_imgs` for 8 x 15 s at 1280x736 is 2742 float frames,
|
| 1089 |
-
about **31 GB**. A dry run that allocated it would be worse than useless.
|
| 1090 |
-
|
| 1091 |
-
Hop 2+ needs *a* `prev_imgs` to compute `<Picture N>` ordinals. Content is
|
| 1092 |
-
irrelevant to the compiled text, so a `[overlap, 8, 8, 3]` zero tensor stands
|
| 1093 |
-
in and the text is byte-identical to a real run's.
|
| 1094 |
-
|
| 1095 |
-
The smoke test (`tmp/t_dry.py`) replaces all five sampler entry points with
|
| 1096 |
-
objects that raise on **any** attribute access, so "did not touch the sampler"
|
| 1097 |
-
is asserted rather than assumed. It caught one real bug: the dry block
|
| 1098 |
-
referenced `pin_mech_pred` before its assignment, ~40 lines later. Which pin a
|
| 1099 |
-
hop gets is decided at render time from whether a sampler latent exists, so a
|
| 1100 |
-
dry run genuinely cannot know it -- the sheet reports the `pin_to_qwen`
|
| 1101 |
-
*setting* instead. Reporting AddGuide for every hop would have been a lie.
|
| 1102 |
-
|
| 1103 |
-
### The over-delivery lint
|
| 1104 |
-
|
| 1105 |
-
The one defect class every other check structurally misses: both shots are
|
| 1106 |
-
individually well-formed, the directives are individually legal, and only the
|
| 1107 |
-
JOIN between them is wrong. `tail=settle|hold` promises rest; a following beat
|
| 1108 |
-
that opens "She continues...", "Walking to...", "Mid-sentence..." asks the model
|
| 1109 |
-
to carry on what the hop before was told to stop.
|
| 1110 |
-
|
| 1111 |
-
Narrow on purpose. Trailing spaces in `_MID_ACTION` are load-bearing ("keeps "
|
| 1112 |
-
not "keepsake", "still " not "stillness"), and `_MID_ACTION_LEAD` is only
|
| 1113 |
-
checked at position 0, which is what stops "Morning light..." and "Nothing
|
| 1114 |
-
moves..." from firing. Verified against those exact traps, and both shipped
|
| 1115 |
-
plans stay quiet.
|
| 1116 |
-
|
| 1117 |
-
It will miss a beat that opens mid-action without saying so. That is accepted:
|
| 1118 |
-
a false positive that blocked a render would be worse than the defect.
|
| 1119 |
-
|
| 1120 |
-
### Notes for whoever picks this up
|
| 1121 |
-
|
| 1122 |
-
- `sheet.py` and `seam.py` catch every exception and return a 1x1 placeholder.
|
| 1123 |
-
A picture must never lose a finished chain. Do not "clean up" those handlers.
|
| 1124 |
-
- The contact sheet stores frames through `sheet.small()` (168px tall). Two
|
| 1125 |
-
full frames per hop across eight hops is 180 MB held for the whole render for
|
| 1126 |
-
no reason.
|
| 1127 |
-
- The sheet shows `imgs[overlap_n]` for hops 2+, not `imgs[0]`: the first
|
| 1128 |
-
`overlap` frames are trimmed at the join, so `imgs[0]` is a frame the master
|
| 1129 |
-
never contains.
|
| 1130 |
-
- `tools/check_workflows.py` derives the expected widget list from the live
|
| 1131 |
-
`INPUT_TYPES`, so it needed no edit for the five new widgets -- only
|
| 1132 |
-
`SaveImage` added to `CORE`, for the Starter's new contact-sheet node.
|
| 1133 |
-
- The Starter now ships `contact_sheet=on` with a `SaveImage` wired; the
|
| 1134 |
-
Showcase ships it off. Starter is the teaching graph, so the feature is on
|
| 1135 |
-
the canvas where it will be found.
|
| 1136 |
-
|
| 1137 |
-
### Unverified, in priority order
|
| 1138 |
-
|
| 1139 |
-
1. Everything visual in ComfyUI: the five new widgets rendering in the RUN
|
| 1140 |
-
panel, the fourth output socket, the Starter's new SaveImage.
|
| 1141 |
-
2. `tone_compensate=anchor` on a real chain. The synthetic test models drift as
|
| 1142 |
-
a linear ramp; real drift is not linear and 0.35/0.06 are guesses that
|
| 1143 |
-
looked right on synthetic data.
|
| 1144 |
-
3. Whether the contact sheet is legible at ComfyUI's default zoom.
|
| 1145 |
-
4. `render_through` re-extending: render 3, then 5, and confirm hops 1-3 hit
|
| 1146 |
-
the cache rather than re-rendering.
|
| 1147 |
-
5. `quality=draft` actually being fast enough to be worth it.
|
|
|
|
| 350 |
- Fewer than 3 wired reference stills triggers a warning log; unconnected `Load Image` nodes do not count as wired.
|
| 351 |
- Soundtrack is official H3, not a pack dialect: `(S1)` + `<d>…</d>` for lines, `overall_soundscape` for ambience/physical (or `N/A` for requested silence). Do not invent beat-keywords. “No speech” / “no dialogue” remains negation/gibberish.
|
| 352 |
|
| 353 |
+
## The engineering log
|
| 354 |
+
|
| 355 |
+
Sections 8 onward -- the dated record of what was built, measured, and got
|
| 356 |
+
wrong -- live in [`docs/DEVLOG.md`](docs/DEVLOG.md). They are history, not
|
| 357 |
+
instructions. This file is the brief; the log is why the brief says what it
|
| 358 |
+
says, and it is worth reading before changing any of it.
|
| 359 |
+
|
| 360 |
+
Most recent: **section 21**, the first ComfyUI session, where two of the seven
|
| 361 |
+
features shipped in 0.4.0 turned out to be broken in ways no offline test could
|
| 362 |
+
have caught.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,3 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Hand Tie Clips
|
| 2 |
|
| 3 |
One node. Write a shot plan, drop in your reference stills, queue.
|
|
@@ -9,6 +22,15 @@ One node. Write a shot plan, drop in your reference stills, queue.
|
|
| 9 |
> the rename keeps loading** — they are just hidden from node search. Nothing
|
| 10 |
> needs migrating.
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
**Writing for it:** [PROMPTING.md](PROMPTING.md) is the authoring guide — the rules that come from what this model actually does, not from taste. [prompt_pack/](prompt_pack/) has a copy-paste prompt that gets a language model to write plans for you.
|
| 13 |
|
| 14 |
Each hop is native **MiniMax H3 Reference-to-Video**. Hops after the first are guided by the **previous hop's sampler AV latent** via `ComfyUI-H3-Motion-Context` when that pack is installed (22 picture frames + 24-frame end-aligned audio). Stock `MiniMaxH3AddGuide` is the fallback when Motion-Context is missing or the previous hop was a pixel cache hit. Voice stays as a reference every hop. Identity stills ride hop 1; later hops use the pin for wardrobe and room unless a ref lists those hops in `shots`. A 5 s hop drops the airlock on a continuous join — validate seams at 8 s or 15 s.
|
|
@@ -313,9 +335,14 @@ renders the new hops. The plan is not truncated: shot 4 still knows it is shot
|
|
| 313 |
4, keeps its own seed, and keys the same way it will in the full run.
|
| 314 |
|
| 315 |
`quality=draft` forces 0.3 MP and 6 steps — enough to read blocking, camera and
|
| 316 |
-
whether a join lands
|
| 317 |
-
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
## Contact sheet
|
| 321 |
|
|
@@ -367,6 +394,25 @@ here on stops being fought for the rest of the film.
|
|
| 367 |
|
| 368 |
Set `tone_anchor` to 0 to get plain frame_shift back.
|
| 369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
## The MODEL wire
|
| 371 |
|
| 372 |
Four ordinary nodes, in this order:
|
|
@@ -429,12 +475,12 @@ Three shots at 10 s with a 0.9 s overlap is about 28 s of master after the overl
|
|
| 429 |
|
| 430 |
## Nodes
|
| 431 |
|
| 432 |
-
**H3 Ref2VA Chain** — `images`, `audio`, `info` out. Wire `CreateVideo` + `SaveVideo` as in the example workflow,
|
| 433 |
|
| 434 |
**H3 Chain Preview** — a passthrough panel for the IMAGE (and optionally AUDIO) wire, placed **between the chain and `CreateVideo`**. Images and audio come out unchanged, so adding or removing it changes no pixels. It shows the live sample, the seam (the previous hop's last frame beside this hop's first), a chain-wide progress bar, cache hit / seed / steps per hop, **which pin mechanism each hop actually used** — a latent Motion-Context pin or the AddGuide pixel fallback — and end-of-run A/V drift. Drag the grip to resize the stats panel; double-click it to reset.
|
| 435 |
|
| 436 |
**H3 Tone Compensate** — `images` out. Corrects a generated segment's tone against the previous one, estimated on the overlap they share. **For hand-built chains only.** It cannot fix `H3 Ref2VA Chain`'s output: that node joins its hops internally and drops each hop's first `overlap` frames at the seam, so the regenerated copies this needs are already gone by the time images leave it. Use the chain node's `tone_compensate` widget instead. Estimator ported from [rkfg/ComfyUI-MiniMaxH3-ToneCompensate](https://github.com/rkfg/ComfyUI-MiniMaxH3-ToneCompensate) (MIT).
|
| 437 |
|
| 438 |
-
**H3 Seam Report** — `report` (STRING) + `chart` (IMAGE). Wire the chain's `images` into it and it measures the brightness step at every join, says whether each is invisible / marginal / visible, and totals the chain's cumulative drift. A single reading includes whatever the scene did across the cut — the frames either side are ~0.9 s apart in scene time — so treat one number as an upper bound; to isolate the seam itself, render twice from the same seed and cache changing only `tone_compensate`, and compare.
|
| 439 |
|
| 440 |
**H3 Continuity State** — `continuity_state` (STRING) out. **Setting only**: `setting_locked` / `setting_context` / `setting_mutable`. Characters belong in `ref_plan`.
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
tags:
|
| 4 |
+
- comfyui
|
| 5 |
+
- comfyui-nodes
|
| 6 |
+
- custom-nodes
|
| 7 |
+
- video
|
| 8 |
+
- video-generation
|
| 9 |
+
- text-to-video
|
| 10 |
+
- image-to-video
|
| 11 |
+
- minimax-h3
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
# Hand Tie Clips
|
| 15 |
|
| 16 |
One node. Write a shot plan, drop in your reference stills, queue.
|
|
|
|
| 22 |
> the rename keeps loading** — they are just hidden from node search. Nothing
|
| 23 |
> needs migrating.
|
| 24 |
|
| 25 |
+
> **0.4.1 — 2026-08-30.** The 0.4.0 feature set was built without a browser or a
|
| 26 |
+
> GPU and verified offline only. It has now been run in ComfyUI, and two things
|
| 27 |
+
> were broken: the five new dials were never added to the run panel's widget list
|
| 28 |
+
> (they worked, but rendered as raw dials on the node body), and a dry run
|
| 29 |
+
> returned a 1×1 placeholder image that **libx264 cannot encode** — so every dry
|
| 30 |
+
> run wired to `SaveVideo`, which is what the Starter ships, died in
|
| 31 |
+
> `avcodec_open2`. Both fixed. The Starter now also ships the seam report wired.
|
| 32 |
+
> Measurements are in [`docs/DEVLOG.md`](docs/DEVLOG.md) section 21.
|
| 33 |
+
|
| 34 |
**Writing for it:** [PROMPTING.md](PROMPTING.md) is the authoring guide — the rules that come from what this model actually does, not from taste. [prompt_pack/](prompt_pack/) has a copy-paste prompt that gets a language model to write plans for you.
|
| 35 |
|
| 36 |
Each hop is native **MiniMax H3 Reference-to-Video**. Hops after the first are guided by the **previous hop's sampler AV latent** via `ComfyUI-H3-Motion-Context` when that pack is installed (22 picture frames + 24-frame end-aligned audio). Stock `MiniMaxH3AddGuide` is the fallback when Motion-Context is missing or the previous hop was a pixel cache hit. Voice stays as a reference every hop. Identity stills ride hop 1; later hops use the pin for wardrobe and room unless a ref lists those hops in `shots`. A 5 s hop drops the airlock on a continuous join — validate seams at 8 s or 15 s.
|
|
|
|
| 335 |
4, keeps its own seed, and keys the same way it will in the full run.
|
| 336 |
|
| 337 |
`quality=draft` forces 0.3 MP and 6 steps — enough to read blocking, camera and
|
| 338 |
+
whether a join lands. Resolution and steps are both in the cache key, so a draft
|
| 339 |
+
never overwrites the final it stands in for; the two simply cost two entries.
|
| 340 |
+
|
| 341 |
+
Treat it as a **fidelity** lever rather than a speed one. Measured at ~42 s/hop
|
| 342 |
+
against ~45 s/hop at 7 steps: if you already render at 0.3 MP and 6–8 steps —
|
| 343 |
+
the regime this pack targets — draft saves almost nothing, and `dry_run` is the
|
| 344 |
+
fast button. Draft earns its place when your final is genuinely heavier, 1.0 MP
|
| 345 |
+
at 14 steps.
|
| 346 |
|
| 347 |
## Contact sheet
|
| 348 |
|
|
|
|
| 394 |
|
| 395 |
Set `tone_anchor` to 0 to get plain frame_shift back.
|
| 396 |
|
| 397 |
+
**Measured** on three hops from one seed and one cache — the hop store writes
|
| 398 |
+
before the tone stage runs, so flipping the mode re-grades the same renders and
|
| 399 |
+
only the correction differs:
|
| 400 |
+
|
| 401 |
+
| `tone_anchor` | drift across the chain | worst seam |
|
| 402 |
+
|---|---|---|
|
| 403 |
+
| off | 13.4/255 | 1.9/255 |
|
| 404 |
+
| 0.15 | 7.4 | 1.0 |
|
| 405 |
+
| 0.35 | 5.1 | 1.6 |
|
| 406 |
+
| 0.60 | 2.9 | 2.1 |
|
| 407 |
+
|
| 408 |
+
Monotonic, with no knee: each step of strength removes roughly another 16% of
|
| 409 |
+
the drift and costs about 0.5/255 of seam step. Hop 1 is byte-identical in all
|
| 410 |
+
four. **0.35 is the default and stays.**
|
| 411 |
+
|
| 412 |
+
One caveat the numbers cannot capture: a scene that brightens *for a reason* —
|
| 413 |
+
walking toward a window — is indistinguishable from drift from the inside, and
|
| 414 |
+
anchor will flatten it. That is what the per-shot `tone` field is for.
|
| 415 |
+
|
| 416 |
## The MODEL wire
|
| 417 |
|
| 418 |
Four ordinary nodes, in this order:
|
|
|
|
| 475 |
|
| 476 |
## Nodes
|
| 477 |
|
| 478 |
+
**H3 Ref2VA Chain** — `images`, `audio`, `info`, `contact_sheet` out. Wire `CreateVideo` + `SaveVideo` as in the example workflow, `info` to a Preview Text node, and `contact_sheet` to a Save Image.
|
| 479 |
|
| 480 |
**H3 Chain Preview** — a passthrough panel for the IMAGE (and optionally AUDIO) wire, placed **between the chain and `CreateVideo`**. Images and audio come out unchanged, so adding or removing it changes no pixels. It shows the live sample, the seam (the previous hop's last frame beside this hop's first), a chain-wide progress bar, cache hit / seed / steps per hop, **which pin mechanism each hop actually used** — a latent Motion-Context pin or the AddGuide pixel fallback — and end-of-run A/V drift. Drag the grip to resize the stats panel; double-click it to reset.
|
| 481 |
|
| 482 |
**H3 Tone Compensate** — `images` out. Corrects a generated segment's tone against the previous one, estimated on the overlap they share. **For hand-built chains only.** It cannot fix `H3 Ref2VA Chain`'s output: that node joins its hops internally and drops each hop's first `overlap` frames at the seam, so the regenerated copies this needs are already gone by the time images leave it. Use the chain node's `tone_compensate` widget instead. Estimator ported from [rkfg/ComfyUI-MiniMaxH3-ToneCompensate](https://github.com/rkfg/ComfyUI-MiniMaxH3-ToneCompensate) (MIT).
|
| 483 |
|
| 484 |
+
**H3 Seam Report** — `report` (STRING) + `chart` (IMAGE). **Ships wired on the Starter canvas.** Set `hops` to your shot count: it derives hop length from frames, hops and overlap, so a wrong `hops` does not error — it returns a plausible length and puts every seam where no join exists. Wire the chain's `images` into it and it measures the brightness step at every join, says whether each is invisible / marginal / visible, and totals the chain's cumulative drift. A single reading includes whatever the scene did across the cut — the frames either side are ~0.9 s apart in scene time — so treat one number as an upper bound; to isolate the seam itself, render twice from the same seed and cache changing only `tone_compensate`, and compare.
|
| 485 |
|
| 486 |
**H3 Continuity State** — `continuity_state` (STRING) out. **Setting only**: `setting_locked` / `setting_context` / `setting_mutable`. Characters belong in `ref_plan`.
|
|
@@ -0,0 +1,915 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hand Tie Clips -- engineering log
|
| 2 |
+
|
| 3 |
+
The dated record behind [`CLAUDE.md`](../CLAUDE.md), which is the architecture
|
| 4 |
+
brief. Everything here is **history**: what was built on a given day, what it
|
| 5 |
+
measured, and what it got wrong. Later sections correct earlier ones -- read to
|
| 6 |
+
the end of a thread before trusting the top of it.
|
| 7 |
+
|
| 8 |
+
Numbering starts at 8 because sections 1-7 became the brief.
|
| 9 |
+
|
| 10 |
+
## 8. References are files, not wires (2026-08-28)
|
| 11 |
+
|
| 12 |
+
The node had **16 sockets** and twelve of them were user media: `ref_image_1..9`,
|
| 13 |
+
`reference_video`, `voice`, `start_image`. They occupied roughly 340px down the
|
| 14 |
+
left of the node before the editor started, each needing its own `Load Image`.
|
| 15 |
+
They are gone. The node now has **five**: `model`, `clip`, `vae`, `audio_vae`,
|
| 16 |
+
`continuity_state`.
|
| 17 |
+
|
| 18 |
+
A reference names a **file** under `<ComfyUI input>/h3_refs`. `media.py` owns
|
| 19 |
+
both halves of that:
|
| 20 |
+
|
| 21 |
+
- **`resolve(name)` is the only thing that turns a name into a path**, and it is
|
| 22 |
+
used by the upload route and the loaders alike. Basename only, `normpath`,
|
| 23 |
+
then a prefix check against the reference directory, then an extension
|
| 24 |
+
whitelist. Verified against absolute paths, `../` traversal, and traversal
|
| 25 |
+
carrying a legal extension — all refused.
|
| 26 |
+
- **The loaders return exactly what the sockets delivered** — float `[N,H,W,3]`
|
| 27 |
+
in 0..1 — so `_ref_frames`' resize, `_collect_ref_images`' dense-pack and
|
| 28 |
+
`store.tensor_digest`'s cache keying are all unchanged. That is why this was a
|
| 29 |
+
small diff rather than a rewrite.
|
| 30 |
+
|
| 31 |
+
**Pixels never enter a widget.** Only the basename is stored. PromptMasterLD
|
| 32 |
+
measured 1.68 MB of widget value for nine base64 thumbnails and ComfyUI then
|
| 33 |
+
failed to save the workflow at all; previews are `/view?...&subfolder=h3_refs`
|
| 34 |
+
URLs, which cost nothing to rebuild and survive a reload. `/view` also brings
|
| 35 |
+
Range support, which a `<video>` needs to seek — so no thumbnail route was
|
| 36 |
+
needed.
|
| 37 |
+
|
| 38 |
+
**`IS_CHANGED` is new and load-bearing.** ComfyUI caches a node's output on its
|
| 39 |
+
inputs, and a filename is a stable input even when the bytes behind it change:
|
| 40 |
+
overwrite `face.png` and the previous render would be served. It hashes
|
| 41 |
+
`name:mtime` for every referenced file. Deliberately **not** `float("nan")` —
|
| 42 |
+
that is the blunt version PromptMasterLD's studio node uses, and it would force
|
| 43 |
+
a full re-render of an expensive node on every queue.
|
| 44 |
+
|
| 45 |
+
**Widget order is part of the saved-workflow format.** `widgets_values` is a
|
| 46 |
+
**positional array** -- ComfyUI restores `value[i]` into `widget[i]` and never
|
| 47 |
+
looks at the name. The three `*_file` widgets were first added at the *top* of
|
| 48 |
+
`optional`, which shifted `hop_script`..`tone_compensate` by +3; every workflow
|
| 49 |
+
saved before that change then loaded `audio_pin_frames`' integer into `ref_plan`
|
| 50 |
+
and the editor died with `(text || "").trim is not a function` on load. That
|
| 51 |
+
throw was the lucky part -- `hop_script`, `shot_plan` and `tone_compensate` were
|
| 52 |
+
being silently misassigned too, and only the type mismatch made any of it
|
| 53 |
+
visible.
|
| 54 |
+
|
| 55 |
+
**New widgets go at the bottom of `optional`, always.** Old workflows are then
|
| 56 |
+
*short* rather than *misaligned*, and the new widget takes its default. There is
|
| 57 |
+
an append-only marker comment in `INPUT_TYPES` saying so. Note `seed` costs
|
| 58 |
+
**two** array slots, not one: the frontend appends `control_after_generate`
|
| 59 |
+
right after it.
|
| 60 |
+
|
| 61 |
+
Both plan parsers now coerce (`String(text ?? "")`) and reject non-object JSON
|
| 62 |
+
instead of dereferencing it, so a future misalignment shows an empty JSON tab
|
| 63 |
+
rather than aborting the whole workflow load.
|
| 64 |
+
|
| 65 |
+
**The hop cache needed no change at all.** `chain_salt` already digests the
|
| 66 |
+
actual tensors, so different pixels behind the same filename still move the key.
|
| 67 |
+
|
| 68 |
+
**Legacy plans cannot be migrated automatically.** An old `ref_image_N` held a
|
| 69 |
+
tensor from a `Load Image`; there is no filename to recover. `refs.py` keeps the
|
| 70 |
+
authored slot as `legacy_slot`, `parse_ref_plan` does **not** raise on it, and
|
| 71 |
+
both the rail row and `check()` say *"was wired to ref_image_3 — pick its
|
| 72 |
+
picture"*. Failing the parse would have stopped the editor opening the very plan
|
| 73 |
+
the author needs to repair.
|
| 74 |
+
|
| 75 |
+
## 9. Presentation (2026-08-28)
|
| 76 |
+
|
| 77 |
+
**The node collapsed on first click and stayed collapsed.** Two causes, both in
|
| 78 |
+
`installHeightGuard` (`js/editor/widget_utils.js`), both ported from
|
| 79 |
+
`PromptMasterLD/js/claude_prompt.js:6285-6370`:
|
| 80 |
+
|
| 81 |
+
- `computeLayoutSize().minWidth` reported a **constant**. The layout pass
|
| 82 |
+
re-reads it on every recompute — selecting the node is enough — and
|
| 83 |
+
faithfully re-declares the node at its stated minimum. It now tracks
|
| 84 |
+
`node.size[0]`. Safe against feedback: `max()` against a constant is a
|
| 85 |
+
fixpoint, not an accumulator, and width never feeds height.
|
| 86 |
+
- A stale `widget.width` shadows `node.width` forever after load, because
|
| 87 |
+
ComfyUI's DOM-widget position updater reads `(widget.width ?? node.width)`. A
|
| 88 |
+
live getter with a dropped setter makes the stale value unrepresentable.
|
| 89 |
+
|
| 90 |
+
Two more fixes alongside: `node.computeSize()[0]` is floored at the panel width
|
| 91 |
+
(with no widget declaring one, LiteGraph falls back to `NODE_WIDTH * 1.5 = 210`
|
| 92 |
+
and every resize command is free to crush the panel), and `domWidget.computeSize`
|
| 93 |
+
now answers a **width-passing** caller with the minimum height and a
|
| 94 |
+
**no-argument** caller with the live height. Reporting the live height to both
|
| 95 |
+
pins the resize-drag floor to the current height, so the node could only ever
|
| 96 |
+
grow.
|
| 97 |
+
|
| 98 |
+
**`chrome()`'s memo key was stale by construction.** It keyed on
|
| 99 |
+
`inputs|outputs|widgets.length`, none of which change when a widget is *hidden*
|
| 100 |
+
— so every panel height computed after `applyVisibility` used a chrome
|
| 101 |
+
measurement taken before it. The hidden count is now part of the key.
|
| 102 |
+
|
| 103 |
+
**The reference rail was permanently crushed, and its rescue was dead code.**
|
| 104 |
+
The 7-track grid needs ~536px and the node offers ~510px at `NODE_WIDTH 560`.
|
| 105 |
+
There was a `@container (max-width: 460px)` block written to relieve it — but
|
| 106 |
+
**nothing in the codebase declared `container-type`**, so the query had no
|
| 107 |
+
containment context and never matched. `.h3e-section` now declares
|
| 108 |
+
`container-type: inline-size`, and there are two breakpoints.
|
| 109 |
+
|
| 110 |
+
**The palette committed to one look.** It used to derive surfaces from the host
|
| 111 |
+
theme via `color-mix()` while hardcoding every accent — the intent was
|
| 112 |
+
light-theme safety, but `--h3-bg` mixed toward `#111827` and `--h3-sunken`
|
| 113 |
+
toward `#000`, so a light theme got dark blue-grey islands anyway. It is now the
|
| 114 |
+
PromptMasterLD `.ldp-root` system: `#0a0a0a`, one hairline `#2a2a2a`, zero
|
| 115 |
+
radius, one accent `#e8ff47`. **Changing `--h3-accent` moves the whole panel.**
|
| 116 |
+
Emphasis **inverts** (accent fill, `--h3-on-accent` ink) rather than tinting,
|
| 117 |
+
because an acid accent at 30% behind unchanged text is olive mud.
|
| 118 |
+
|
| 119 |
+
A cascade trap worth remembering: the override block was first inserted *before*
|
| 120 |
+
the reference-rail section, so `.h3e-chip-on`, `.h3e-inactive .h3e-ord` and
|
| 121 |
+
`.h3e-subj-badge` all kept winning on source order. Overrides live at the end of
|
| 122 |
+
the sheet now. Without a browser, a token audit script is the only thing that
|
| 123 |
+
catches this class of bug.
|
| 124 |
+
|
| 125 |
+
**The panel did not fill the node, and the mirror was why.** `installHeightGuard`
|
| 126 |
+
kept an independent `_h`, updated through an `onResize` hook, so that the
|
| 127 |
+
arrange pass could not feed its own growth. Measured during a drag: `onResize`
|
| 128 |
+
and `setSize` each fired 57 times while `_h` sat at 876 and `node.size[1]`
|
| 129 |
+
climbed past 1400. `measuring` was not stuck, which left one gate --
|
| 130 |
+
`Array.isArray(size)`. **This frontend's `node.size` is not a plain Array**, so
|
| 131 |
+
every write was skipped and `_h` held the install-time height forever. A 1911px
|
| 132 |
+
node had a 742px panel.
|
| 133 |
+
|
| 134 |
+
The repair was to delete the mirror, not fix the hook. `_h` existed only to
|
| 135 |
+
break the arrange loop, and
|
| 136 |
+
|
| 137 |
+
panelHeight() = max(minHeight, node.size[1] - chrome() - SLACK) // SLACK 8
|
| 138 |
+
|
| 139 |
+
breaks it outright: the pass wants `panelTop + panelHeight + 4`, and `panelTop`
|
| 140 |
+
is chrome minus the node's bottom padding, so a panel of exactly `size - chrome`
|
| 141 |
+
asks for up to 4px more than the node has on every frame -- the ~130px/frame
|
| 142 |
+
runaway the header comment records. With slack the inequality holds, the loop
|
| 143 |
+
settles, and the node's height simply *is* the panel's height. Nothing to keep
|
| 144 |
+
in sync, no hook to get wrong. `sync()` now only ever grows a too-short node;
|
| 145 |
+
the height is the user's to choose.
|
| 146 |
+
|
| 147 |
+
**RUN is pinned and always open.** `.h3e-root` used to be the scroll container
|
| 148 |
+
with all four sections inside it, which put RUN below the script -- out of view
|
| 149 |
+
on any workflow with more than two shots, and it is the section touched on every
|
| 150 |
+
queue. The root is now a flex column holding `.h3e-scroll` (the authoring
|
| 151 |
+
sections) and RUN outside it.
|
| 152 |
+
|
| 153 |
+
Two flex details, both of which cost a round trip to learn:
|
| 154 |
+
|
| 155 |
+
- **`.h3e-scroll` needs `min-height: 0`.** A flex item defaults to
|
| 156 |
+
`min-height: auto` and refuses to shrink below its content, which pushes RUN
|
| 157 |
+
off the bottom of the node instead of scrolling.
|
| 158 |
+
- **`.h3e-run` must be `flex: 0 0 auto`, never `0 1 auto`.** Flex divides a
|
| 159 |
+
deficit in proportion to each item's content height. The scroller's content is
|
| 160 |
+
far taller, so a shrinkable RUN loses most of the contest and clips its lower
|
| 161 |
+
groups. The scroller absorbs all the shrinking; its `min-height: 160px` is the
|
| 162 |
+
floor that stops RUN owning the panel and RUN's `max-height: 55%` is the
|
| 163 |
+
ceiling.
|
| 164 |
+
|
| 165 |
+
Always-open removed the only moment RUN re-read its widgets, so it now resyncs
|
| 166 |
+
on `api`'s `promptQueued` -- the client-side event that fires after
|
| 167 |
+
`control_after_generate` has bumped the seed, and the same one ComfyUI's own
|
| 168 |
+
change tracker uses. The listener is dropped in `node.onRemoved`.
|
| 169 |
+
|
| 170 |
+
**Not done:** PromptMasterLD's `--fsc` UI-scale multiplier, which makes every
|
| 171 |
+
dimension `calc(Npx * var(--fsc))` and puts a zoom slider on the panel. It is
|
| 172 |
+
mechanical churn across every rule in the sheet and was not worth doing blind.
|
| 173 |
+
|
| 174 |
+
## 10. Ported for public use (2026-08-29)
|
| 175 |
+
|
| 176 |
+
The prompting craft was written down and the pack was made installable by a
|
| 177 |
+
stranger. Four things, in the order they mattered.
|
| 178 |
+
|
| 179 |
+
**The shipped example workflows were broken.** All four in `workflows/` predated
|
| 180 |
+
the 2026-08-28 socket removal: twelve dead media inputs each, three `LoadImage`
|
| 181 |
+
nodes, and 21 widget values against a 28-widget node. Loading one is a new
|
| 182 |
+
user's *first* action, so this outranked any amount of documentation. They are
|
| 183 |
+
quarantined in `_disabled_custom_nodes/h3_legacy_workflows/` (the pack is not
|
| 184 |
+
under its own version control -- deleting would have been unrecoverable) and
|
| 185 |
+
replaced by two built from the verified `H3_Stress_6x7` structure:
|
| 186 |
+
|
| 187 |
+
- **`HandTieClips_Starter.json`** -- two hops, **no references at all**, runs the
|
| 188 |
+
moment the loaders are pointed at files. The empty register is deliberate: an
|
| 189 |
+
`@tag` whose picture is missing is a *hard* error in `resolve_tags`, so a
|
| 190 |
+
starter that shipped with tags would fail on first queue for everyone.
|
| 191 |
+
- **`HandTieClips_Showcase.json`** -- the six-hop continuity test, with the three
|
| 192 |
+
reference filenames generalised.
|
| 193 |
+
|
| 194 |
+
Both use **core ComfyUI plus this pack only**. The dev workflow reaches KJNodes
|
| 195 |
+
(`ModelPreviewOverrideKJ`, `MiniMaxLowVRAMAttention`) and PlagueKind
|
| 196 |
+
(`LTX_lora_loader`, `H3SLAAttention`, `H3AdaLNLoRAFix`); an example that fails
|
| 197 |
+
to load because of a pack the reader never asked for teaches nothing. The
|
| 198 |
+
speed stack is documented, not shipped.
|
| 199 |
+
|
| 200 |
+
**A declared-but-inactive `@tag` reported the wrong cause.** `check()` already
|
| 201 |
+
warned correctly that a picture was missing, but the run then died on
|
| 202 |
+
`resolve_tags` with *"unknown reference '@kitchen'"* -- pointing at the beat's
|
| 203 |
+
spelling, the one thing that was right. `resolve_tags` now takes `declared`
|
| 204 |
+
(every tag in the register, active this hop or not) and separates the two
|
| 205 |
+
failures. The old two-argument behaviour is unchanged when `declared` is
|
| 206 |
+
omitted.
|
| 207 |
+
|
| 208 |
+
**`PROMPTING.md` and `prompt_pack/`.** The craft rules were spread through
|
| 209 |
+
README prose; they are now a standalone guide, and a copy-paste system prompt
|
| 210 |
+
that gets a language model to emit valid plans. Two files under `prompt_pack/`
|
| 211 |
+
are **generated, never hand-written**:
|
| 212 |
+
|
| 213 |
+
- `tools/gen_schema.py` builds `SCHEMA.json` from `directives.VOCAB`,
|
| 214 |
+
`refs.RETENTION` and the duration table, and **asserts** against
|
| 215 |
+
`plan._SHOT_KEYS`, `refs.REF_FIELDS` and `refs.SUBJECT_FIELDS`. Add a camera
|
| 216 |
+
move and the schema follows; add a shot field and the generator fails loudly
|
| 217 |
+
rather than emitting a stale schema. `--check` is the CI form.
|
| 218 |
+
- `tools/gen_example.py` builds `EXAMPLE_6_HOP.md` from the showcase workflow,
|
| 219 |
+
including its hop/reference table, so the worked example and the shipped
|
| 220 |
+
workflow cannot disagree.
|
| 221 |
+
|
| 222 |
+
**Template patterns in the editor.** `js/editor/templates.js` plus a
|
| 223 |
+
**Templates** button in the SCRIPT header. They **append**, never replace --
|
| 224 |
+
replacing would be the one destructive control on the node, and stacking is how
|
| 225 |
+
a chain is actually built. `freeId()` mints the lowest unused `sN` because `id`
|
| 226 |
+
is the hop cache's pointer and two shots sharing one would make `locked` reuse
|
| 227 |
+
the wrong render.
|
| 228 |
+
|
| 229 |
+
No template contains an `@tag`, for the same reason the starter workflow has no
|
| 230 |
+
references. `tools/check_templates.py` extracts the patterns *out of the JS*
|
| 231 |
+
and runs them through `plan.parse_plan` and `plan.check_coherence`, and lints
|
| 232 |
+
every beat for negation -- a template that produced a plan the node rejects
|
| 233 |
+
would be worse than no templates, because a first-time author would blame their
|
| 234 |
+
own writing. It caught two: a beat reading "They stop at the window" (naming a
|
| 235 |
+
cessation, which is law 2) and a line of dialogue containing "did not".
|
| 236 |
+
|
| 237 |
+
That second one is worth recording as an open question: **whether the additive
|
| 238 |
+
prompt bites inside quoted dialogue was never tested.** The templates avoid it,
|
| 239 |
+
and `PROMPTING.md` says plainly that this is untested rather than inventing a
|
| 240 |
+
rule.
|
| 241 |
+
|
| 242 |
+
## 11. What chain_00057 taught (2026-08-29)
|
| 243 |
+
|
| 244 |
+
The six-hop showcase was rendered for the first time. Three failures, and the
|
| 245 |
+
one that mattered was a code gap the documentation had already claimed was
|
| 246 |
+
fixed.
|
| 247 |
+
|
| 248 |
+
**`locked` and `context` reached hop 1 only.** The register's whole promise is
|
| 249 |
+
that a subject's continuity text carries identity across a hop where the
|
| 250 |
+
photograph is absent. It did not. `subject_prose` is called under `if i == 0`,
|
| 251 |
+
and `_identity_lock` returns `""` when no subject-bearing ref is active -- so on
|
| 252 |
+
the showcase, hops 2, 3, 5 and 6 carried **no identity text of any kind**, and
|
| 253 |
+
hop 5, scheduled with no references at all, lost the character entirely.
|
| 254 |
+
Identity was riding on the pinned frames alone, which is the exact failure the
|
| 255 |
+
register exists to prevent.
|
| 256 |
+
|
| 257 |
+
`refs.continuity_line()` now emits that text on every hop 2+, and
|
| 258 |
+
`_assemble_next` injects it between the lock and the live-frame citation. The
|
| 259 |
+
reason it was suppressed in the first place is real and is preserved: naming
|
| 260 |
+
*pictures* on a pin-only hop sent the encoder back to the plates (chain_00034 --
|
| 261 |
+
commercial kitchen, grey shirt, no apron). So the new line carries **no
|
| 262 |
+
`<Picture N>` and no `<Subject N>`**. It is a description of what stays the
|
| 263 |
+
same, not a citation of anything. `<Subject N>` is excluded specifically because
|
| 264 |
+
there is no `subject_definitions` block on a continuation hop to bind it to.
|
| 265 |
+
|
| 266 |
+
Note this changes the assembled block on hop 2+ of every chain with a register,
|
| 267 |
+
so it invalidates the hop cache. That is correct, not a regression.
|
| 268 |
+
|
| 269 |
+
**Dialogue propagated through five hops.** Shot 1 ended on its spoken line with
|
| 270 |
+
`tail: ongoing`. The audio pin carries the previous hop's tail, so the last
|
| 271 |
+
second of hop 1 -- speech -- opened hop 2, and "action is still underway" was
|
| 272 |
+
the closing instruction; the model satisfied it with the action it could hear.
|
| 273 |
+
Nothing in hops 2-6 gave the audio anywhere else to go.
|
| 274 |
+
|
| 275 |
+
This is the guide's own law 3, broken by the plan written to demonstrate it. The
|
| 276 |
+
fix is authorial, not code: land the line **mid-hop** and leave a non-verbal
|
| 277 |
+
action running into the seam (the knife on the board), and give every
|
| 278 |
+
dialogue-free hop a narrowband sound of its own. Both `PROMPTING.md` and the
|
| 279 |
+
authoring prompt now carry this as a named rule, and the shipped templates were
|
| 280 |
+
rewritten to obey it.
|
| 281 |
+
|
| 282 |
+
**`join: continuous` across a location change morphed one room into the other.**
|
| 283 |
+
Hop 6 walked back from the hallway and the kitchen appeared mid-turn. A
|
| 284 |
+
continuous join asks for one unbroken take between two different rooms, which
|
| 285 |
+
is not a thing. `match_cut` is what a walk through a doorway is. The beat also
|
| 286 |
+
read "steps into @kitchen" -- the container phrasing the README warns produces a
|
| 287 |
+
composite of the photograph -- and now names the counter *in* the kitchen.
|
| 288 |
+
|
| 289 |
+
Hop 5's beat is unchanged in the revised plan **on purpose**: it is the
|
| 290 |
+
measurement, and changing it would forfeit the comparison.
|
| 291 |
+
|
| 292 |
+
The revised plan is in the shipped showcase and in
|
| 293 |
+
`user/default/workflows/H3_Stress_6x7_v2.json`; the original that produced
|
| 294 |
+
chain_00057 is left alone so the A/B survives.
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
## 12. Renamed to Hand Tie Clips (2026-08-29)
|
| 298 |
+
|
| 299 |
+
The pack was `ComfyUI-H3-Ref-Chain`. It is now `ComfyUI-Hand-Tie-Clips`, and the
|
| 300 |
+
four registered ids moved with it:
|
| 301 |
+
|
| 302 |
+
| was | is |
|
| 303 |
+
|---|---|
|
| 304 |
+
| `H3RefChain` | `HandTieClips` |
|
| 305 |
+
| `H3ContinuityState` | `HTCContinuityState` |
|
| 306 |
+
| `H3ChainPreview` | `HTCChainPreview` |
|
| 307 |
+
| `H3ToneCompensate` | `HTCToneCompensate` |
|
| 308 |
+
|
| 309 |
+
Display names are unchanged (`H3 Ref2VA Chain`, `H3 Chain Preview`, ...) because
|
| 310 |
+
the pack only drives MiniMax H3 checkpoints and a name that hides that costs
|
| 311 |
+
somebody an afternoon. The menu category is `Hand Tie Clips`. `TAG` -- and so the
|
| 312 |
+
console prefix -- is `[HandTieClips]`.
|
| 313 |
+
|
| 314 |
+
**The old ids are still registered, as `DEPRECATED` subclasses.** A type id is
|
| 315 |
+
what every saved `.json` carries, and an unregistered one is a red missing-node
|
| 316 |
+
box, not a warning. A plain alias in `NODE_CLASS_MAPPINGS` would have worked but
|
| 317 |
+
listed each node twice in search: ComfyUI falls back to the mapping key when
|
| 318 |
+
`NODE_DISPLAY_NAME_MAPPINGS` has no entry for it. Subclassing and setting
|
| 319 |
+
`DEPRECATED = True` gets both -- `server.py:783` publishes `deprecated: True`,
|
| 320 |
+
and the frontend's `Comfy.Node.ShowDeprecated` (off by default) keeps it out of
|
| 321 |
+
search while leaving it fully functional in workflows that name it.
|
| 322 |
+
|
| 323 |
+
**The JS had to learn both ids or the aliases would have been worse than
|
| 324 |
+
useless.** `js/h3_ref_chain_ui.js` and `js/h3_chain_preview.js` each compared
|
| 325 |
+
`nodeData.name` against a single string; a legacy node would have loaded with no
|
| 326 |
+
editor at all, which looks exactly like the rename having broken the pack. Both
|
| 327 |
+
now test membership of a `Set`. `js/h3_chain_preview.js` needs it twice: once
|
| 328 |
+
for its own type, once for `CHAIN_TYPES`, which is how a preview walks back up
|
| 329 |
+
`images` to find the chain feeding it.
|
| 330 |
+
|
| 331 |
+
**What deliberately kept the `h3` naming**, and must not be "finished" later:
|
| 332 |
+
module filenames (`h3_ref_chain.py`), the `h3e-` CSS class prefix and `--h3-`
|
| 333 |
+
tokens (515 occurrences -- one missed class silently breaks styling), the
|
| 334 |
+
`/h3_ref_chain/*` routes, the `h3_refchain_preview` event name, and
|
| 335 |
+
`input/h3_refs`. Renaming that folder would orphan every reference photo already
|
| 336 |
+
on disk.
|
| 337 |
+
|
| 338 |
+
The two shipped workflows moved to `HandTieClips_Starter.json` /
|
| 339 |
+
`HandTieClips_Showcase.json` and were rewritten onto the new ids, with their
|
| 340 |
+
`SaveVideo` prefix now `video/HANDTIECLIPS/chain`. The three under
|
| 341 |
+
`user/default/workflows/` were left on the legacy ids on purpose: re-running
|
| 342 |
+
`H3_Stress_6x7_v2.json` is then a live test of the alias path, and their existing
|
| 343 |
+
renders stay together under `output/video/H3REFCHAIN/`.
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
## 13. The on-canvas board (2026-08-29)
|
| 347 |
+
|
| 348 |
+
`workflows/HandTieClips_Starter.json` carries six `MarkdownNote` cards to the
|
| 349 |
+
left of the loaders, wrapped in a group titled READ ME. The text lives in
|
| 350 |
+
`tools/notes.py` and is written into the workflow by `tools/build_notes.py`.
|
| 351 |
+
|
| 352 |
+
Why on the canvas: the craft was in `PROMPTING.md`, `prompt_pack/` and the
|
| 353 |
+
Templates panel, and all three require leaving the graph. The rules that decide
|
| 354 |
+
whether a first render works are needed while beats are being written, which is
|
| 355 |
+
on the canvas. The cards are a **condensation**, not a copy -- `PROMPTING.md`
|
| 356 |
+
stays the authority and every card says so.
|
| 357 |
+
|
| 358 |
+
Mechanics worth not rediscovering:
|
| 359 |
+
|
| 360 |
+
- `MarkdownNote` is a core virtual node. The frontend renders it with `marked`
|
| 361 |
+
at `gfm: true` and sanitises with DOMPurify, so headings, GFM tables, bold and
|
| 362 |
+
code fences all work. It needs frontend >= ~1.16.
|
| 363 |
+
- Cards are identified by `properties.htc_card`, which is what makes
|
| 364 |
+
`build_notes.py` idempotent -- it drops marked nodes before writing, so
|
| 365 |
+
re-running replaces the board instead of stacking a second copy.
|
| 366 |
+
- `extra.ds` is **restored** on load, not fitted. Without setting it, a board at
|
| 367 |
+
negative x sits off-screen and is never found. Screen is
|
| 368 |
+
`(world + offset) * scale`, so the offset is what brings it into view.
|
| 369 |
+
- Group serialisation is `{id?, title, bounding:[x,y,w,h], color?, font_size?,
|
| 370 |
+
locked?}` -- confirmed against the frontend's own zod schema, not guessed.
|
| 371 |
+
|
| 372 |
+
**`widgets_values_named`, found while doing this.** Both shipped workflows
|
| 373 |
+
carried a stale copy: `chains: 3`, `duration: 10 s`,
|
| 374 |
+
`control_after_generate: randomize`, and a legacy `ref_plan` naming pictures that
|
| 375 |
+
do not ship -- 25 entries against a 28-widget node. It came from the builder
|
| 376 |
+
deep-copying nodes out of the dev workflow and overwriting only
|
| 377 |
+
`widgets_values`. Dormant while `Comfy.Workflow.NamedValuesRestore` stays off
|
| 378 |
+
(experimental, default false), but anyone who turned that on would have loaded a
|
| 379 |
+
Starter that randomizes its seed and dies on a missing reference.
|
| 380 |
+
`build_notes.py` strips it from both, and `tools/check_workflows.py` now fails if
|
| 381 |
+
it ever comes back.
|
| 382 |
+
|
| 383 |
+
`tools/check_workflows.py` is the promoted version of the validator that guarded
|
| 384 |
+
the shipped workflows against socket/widget drift. It also checks the board:
|
| 385 |
+
every card marked and non-empty, exactly one group, the group enclosing every
|
| 386 |
+
card, and no card reaching past x=0 onto the loaders.
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
## 14. The turbo stack ships in the examples (2026-08-29)
|
| 390 |
+
|
| 391 |
+
Both shipped workflows now carry the dev graph's full MODEL wire:
|
| 392 |
+
|
| 393 |
+
UNETLoader -> LTX_lora_loader -> H3AdaLNLoRAFix -> MiniMaxLowVRAMAttention
|
| 394 |
+
-> H3SLAAttention -> ModelPreviewOverrideKJ -> HandTieClips
|
| 395 |
+
|
| 396 |
+
and **CLIP reaches the chain from the LoRA loader, not the encoder** -- that is
|
| 397 |
+
what makes the text half of every LoRA land, and it is the wire most likely to
|
| 398 |
+
get quietly "fixed" back to the encoder by someone tidying the graph.
|
| 399 |
+
|
| 400 |
+
This reverses the earlier "core ComfyUI and this pack only" rule for the
|
| 401 |
+
examples, at the user's direction: `steps` is 7, which only works with a turbo
|
| 402 |
+
LoRA, so an example without the LoRA stack is not a graph anyone can run at the
|
| 403 |
+
settings it ships with. PlagueKind supplies the loader, the AdaLN fix and SLA;
|
| 404 |
+
KJNodes supplies Low VRAM Attention and the preview override.
|
| 405 |
+
|
| 406 |
+
`tools/build_speed_stack.py` inserts and rewires the five nodes idempotently
|
| 407 |
+
(marker `properties.htc_speed`), rebuilding the plain loader -> chain shape
|
| 408 |
+
first so it is reentrant from either state. `tools/check_workflows.py` walks
|
| 409 |
+
**both** wires link by link -- a patch node that is present but bypassed round
|
| 410 |
+
the side looks right on the canvas and does nothing.
|
| 411 |
+
|
| 412 |
+
`H3SLAAttention` widget values are written out in full including
|
| 413 |
+
`reference_protection`, which post-dates the dev workflow's saved values. That
|
| 414 |
+
workflow has 10 entries against an 11-widget node, which is the positional rule
|
| 415 |
+
working as intended: the new widget was appended last, so old values still line
|
| 416 |
+
up and the missing one falls back to its default. The trailing `""` on the LoRA
|
| 417 |
+
loader and the preview override is carried over verbatim for the same reason --
|
| 418 |
+
a value past the last widget is ignored, and dropping one that turns out to
|
| 419 |
+
belong to a widget would shift every value after it.
|
| 420 |
+
|
| 421 |
+
`head_chunks` ships at 4 (the node's own default, safer on unknown VRAM) rather
|
| 422 |
+
than the 2 used here. Two files have to be on disk as well: the turbo LoRA the
|
| 423 |
+
loader names, and `taeh3.safetensors` for the preview override's `tiny_vae`.
|
| 424 |
+
|
| 425 |
+
## 15. What a shipped diagnosis looked like (2026-08-29)
|
| 426 |
+
|
| 427 |
+
The editor UI vanished after the folder rename -- raw widget boxes, no panel.
|
| 428 |
+
Not a code fault. ComfyUI had been restarted while the pack was still
|
| 429 |
+
`ComfyUI-H3-Ref-Chain`, so the running process held
|
| 430 |
+
`python_module: custom_nodes.ComfyUI-H3-Ref-Chain` and served its web assets
|
| 431 |
+
from a path the move had deleted. `/object_info` had all eight node types;
|
| 432 |
+
`/extensions` listed none of the pack's JS and a direct fetch 404'd.
|
| 433 |
+
|
| 434 |
+
Worth knowing for next time: `/object_info` carries `python_module`, which is
|
| 435 |
+
the fastest way to find out **which copy of a pack a running server actually
|
| 436 |
+
loaded**, and `/extensions` plus a direct fetch of one script separates "the JS
|
| 437 |
+
is broken" from "the JS is not being served at all".
|
| 438 |
+
|
| 439 |
+
## 16. What the Rain Kitchen renders taught (2026-08-29)
|
| 440 |
+
|
| 441 |
+
Three six-hop renders of the same 6x7 s chain, each isolating one variable. All
|
| 442 |
+
three findings are now in `PROMPTING.md`, the troubleshooting table, the
|
| 443 |
+
authoring prompt and the on-canvas board.
|
| 444 |
+
|
| 445 |
+
**Identity drift is permanent, and the old advice was backwards.** This file and
|
| 446 |
+
`PROMPTING.md` used to say a face plate riding a later hop *beats the pin* and
|
| 447 |
+
that `shots` should therefore be kept tight. chain_00059 falsified it: hop 4
|
| 448 |
+
carried a face plate photographed in a *different kitchen* through a walking
|
| 449 |
+
medium shot and held cleanly, while hop 5 — scheduled with no references at
|
| 450 |
+
all — came back a different person, and hop 6 never recovered even though a
|
| 451 |
+
place plate rode it and restored the room instantly. `locked` holds a face that
|
| 452 |
+
is still right; only a plate rebuilds one that is gone. **Face refs go on every
|
| 453 |
+
hop.** The old advice survives for *place* plates only.
|
| 454 |
+
|
| 455 |
+
**A beat must survive an over-delivered hop.** A hop routinely does more than it
|
| 456 |
+
was asked. Shot 3 asked for "a first slow step along the counter" and delivered
|
| 457 |
+
the whole walk; shot 4 was then handed an instruction its own live frame had
|
| 458 |
+
already satisfied, and the only way to obey was to reset the scene — a hard
|
| 459 |
+
cut 1.5 s *into* the hop, not at the seam, because `_assemble_next` holds the
|
| 460 |
+
incoming frames for a short beat first. Give one hop the whole movement, and
|
| 461 |
+
write the next beat true from either ending ("she *reaches* the window", "she
|
| 462 |
+
takes up the bowl *again*").
|
| 463 |
+
|
| 464 |
+
**A noun with no adjective drifts.** Three hops said only "the bowl"; it came
|
| 465 |
+
back stainless steel. Naming it "the white bowl" everywhere *and* stating it in
|
| 466 |
+
`context` as a property (never a location — "stays in her hands" fights the
|
| 467 |
+
beat the moment she puts it down) held it for six hops. The re-run then proved
|
| 468 |
+
the rule by accident: `context` read "the apron stays tied over the **grey**
|
| 469 |
+
t-shirt", and the t-shirt held all six hops while the apron, one clause away
|
| 470 |
+
with no colour, had turned denim blue by hop 6.
|
| 471 |
+
|
| 472 |
+
### The code change this bought
|
| 473 |
+
|
| 474 |
+
`refs.resolve_tags` now takes `subject_names` and the node passes it on
|
| 475 |
+
continuation hops only. `<Subject N>` is bound by `subject_definitions:`, which
|
| 476 |
+
is hop-1 material — so on hop 4 the ordinal dangled, exactly the same defect
|
| 477 |
+
as the undescribed bowl. From hop 2 a person tag resolves to the subject's
|
| 478 |
+
`name` instead ("The cook walks down the hallway"), which binds to the
|
| 479 |
+
`continuity_line` sentence every continuation hop already carries. No name means
|
| 480 |
+
the old `<Subject N>` fallback, so nothing regresses.
|
| 481 |
+
|
| 482 |
+
`tools/check_prompts.py` mirrors the new call, and gained the banned-word check
|
| 483 |
+
that previously ran only against `templates.js`. That gap is how the shipped
|
| 484 |
+
Showcase carried "**None** of the kitchen is visible" (law 1 — it *adds* a
|
| 485 |
+
kitchen at cfg 1.0) and "She **stops** at the window" (law 2) through six
|
| 486 |
+
versions. Both are fixed, and the Showcase's face ref now rides all six hops.
|
| 487 |
+
|
| 488 |
+
## 17. The pre-beta audit (2026-08-29)
|
| 489 |
+
|
| 490 |
+
Read for what a stranger hits, not for what we already check. Six findings; the
|
| 491 |
+
first two would each have cost a tester a run.
|
| 492 |
+
|
| 493 |
+
**The docs promised a stop the code never did.** `refs.check()` is *"warn, never
|
| 494 |
+
raise"*, and its caller only printed. A ref naming a file that is not in
|
| 495 |
+
`h3_refs` had its slot skipped and the chain rendered on — all six hops, with
|
| 496 |
+
the reference silently inactive, which is precisely the uncontrolled output the
|
| 497 |
+
register exists to prevent. Four shipped surfaces said otherwise, including the
|
| 498 |
+
Showcase's own note: *"the run stops and names the reference it could not find.
|
| 499 |
+
Nothing guesses."*
|
| 500 |
+
|
| 501 |
+
Fixed in the code rather than the docs, because there is no reading under which
|
| 502 |
+
rendering without a named-but-absent picture is what the author meant. New
|
| 503 |
+
`refs.missing_files()` returns `(tag, file)` for every ref whose named file did
|
| 504 |
+
not load; `h3_ref_chain.py` raises on it, after printing the register table so
|
| 505 |
+
the error arrives with its context. **The distinction that matters:** a ref with
|
| 506 |
+
*no* `file` stays a warning — the Starter ships that way on purpose so it runs
|
| 507 |
+
before any pictures exist. Only a *named* file that is absent is fatal.
|
| 508 |
+
|
| 509 |
+
**The on-canvas board never got §16's corrections.** `tools/notes.py` still
|
| 510 |
+
carried the row chain_00059 falsified — *"a plate riding a hop it does not
|
| 511 |
+
belong on, beating the pin → tighten `shots`"* — and was missing all three rows
|
| 512 |
+
`PROMPTING.md` gained. The board is what a beginner actually reads, so it was
|
| 513 |
+
shipping the inverse of the rule. Lesson: `PROMPTING.md` is the authority, but
|
| 514 |
+
`tools/notes.py` is a **second copy** of the same craft, and a correction is not
|
| 515 |
+
finished until both move. `build_notes.py` regenerates the Starter's cards *and*
|
| 516 |
+
the Showcase's `Note` from `notes.SHOWCASE_NOTE`, so one rebuild covers both.
|
| 517 |
+
|
| 518 |
+
The rest were packaging, not craft: the MIT `LICENSE` named no copyright holder;
|
| 519 |
+
`pyproject.toml` lacks both fields a Registry publish needs (`PublisherId` and a
|
| 520 |
+
`[project.urls] Repository`), now commented in place; the `README` had **no
|
| 521 |
+
install section at all**; `ComfyUI-H3-Motion-Context` was absent from Needs
|
| 522 |
+
despite the intro calling it the primary guidance path, so testers land on the
|
| 523 |
+
`MiniMaxH3AddGuide` fallback without knowing they changed code paths; and the
|
| 524 |
+
example workflows name quantised checkpoints by filename with no note that they
|
| 525 |
+
are one valid set among many, which reads as a broken graph rather than a
|
| 526 |
+
missing file.
|
| 527 |
+
|
| 528 |
+
Clean on the same pass, worth not re-checking: no personal data anywhere, no
|
| 529 |
+
hardcoded local paths in runtime code (only two dev-tool docstrings), 3.10-safe
|
| 530 |
+
syntax, no third-party dependencies, Motion-Context absence handled with fork
|
| 531 |
+
detection, `WEB_DIRECTORY` present, `h3_refs` auto-created.
|
| 532 |
+
|
| 533 |
+
## 18. What a 27B model got wrong, and what the prompt taught it (2026-08-29)
|
| 534 |
+
|
| 535 |
+
Qwen3 27B at temperature 0.3 was given an 8 x 15 s concept. The `ref_plan` came
|
| 536 |
+
back structurally perfect -- both subjects named, face plate on all eight hops,
|
| 537 |
+
places scheduled off the hops they do not belong on, zero register warnings. The
|
| 538 |
+
`shot_plan` carried four defects, and tracing each one back to
|
| 539 |
+
`prompt_pack/AUTHORING_PROMPT.md` found more than four gaps.
|
| 540 |
+
|
| 541 |
+
**The prompt taught one of the bugs.** Rule 9 illustrated a beat as
|
| 542 |
+
``"stands at the counter in `@kitchen`"`` -- backticks and all, because it was
|
| 543 |
+
the one place a tag appeared inside a beat. The model copied the formatting, and
|
| 544 |
+
literal backticks reach the encoder. Fixed, plus a rule that a beat is plain
|
| 545 |
+
prose.
|
| 546 |
+
|
| 547 |
+
**Beat length had no guidance at all.** The only budget in the file was for
|
| 548 |
+
dialogue, so a no-dialogue plan had nothing to size against and every beat came
|
| 549 |
+
back at 22-28 words regardless of a 15 s hop. There is now a word table -- and
|
| 550 |
+
an honest note on it: the shipped plans run 37-39 words at *both* 5 s and 7 s,
|
| 551 |
+
so beat length is near-constant across the only two hop lengths ever rendered.
|
| 552 |
+
The words-per-second reading that gives 70-100 for a 15 s hop is a reasoned
|
| 553 |
+
extrapolation, not a measurement, and the table says so.
|
| 554 |
+
|
| 555 |
+
**Two rules were simply missing.** Nothing told the model to state a visual style
|
| 556 |
+
-- for a "2D anime, Ufotable" concept neither returned block contained the word
|
| 557 |
+
anime, so the text asked for photoreal while the pictures asked for anime. And
|
| 558 |
+
the over-delivery rule from section 16 had never been carried into the prompt at
|
| 559 |
+
all, only into `PROMPTING.md` and the board.
|
| 560 |
+
|
| 561 |
+
**Two statements were wrong.** The prompt said "at most 9 pictures on any one
|
| 562 |
+
hop"; `parse_ref_plan` counts `len(refs)` over the **whole plan**. The code's own
|
| 563 |
+
error message said "on one hop" too, and has been corrected to match what it
|
| 564 |
+
checks. And the `file` bullet still told the model to invent placeholder
|
| 565 |
+
filenames without noting that, as of 0.3.1, a named file absent from `h3_refs`
|
| 566 |
+
**stops the run**.
|
| 567 |
+
|
| 568 |
+
Law 2 is the interesting non-fix. It is stated plainly, with the exact example
|
| 569 |
+
"The cook stops talking", and the model wrote "stops and looks up" anyway. A
|
| 570 |
+
principle is not something a 27B reliably applies to its own output; the literal
|
| 571 |
+
word list `check_prompts.py` enforces now appears in the prompt, because a word
|
| 572 |
+
list is checkable and a principle is not.
|
| 573 |
+
|
| 574 |
+
## 19. What 114 seconds of rendered film and two 27B models taught (2026-08-29)
|
| 575 |
+
|
| 576 |
+
Three sources landed together: a rendered 8 x 15 s anime chain (`chain_00003`,
|
| 577 |
+
2742 frames, 114.25 s), and Qwen and Gemma each answering two test prompts
|
| 578 |
+
written to trip specific rules. Shipped as **0.3.3**.
|
| 579 |
+
|
| 580 |
+
### The node was fighting every stylised plan
|
| 581 |
+
|
| 582 |
+
`directives.py` prepended `ESTABLISH = "Live-action, natural light, one
|
| 583 |
+
continuous take."` to hop 1 **unconditionally**. At cfg 1.0 with no negative
|
| 584 |
+
branch that is additive, and it landed *ahead* of the style declaration rule 12
|
| 585 |
+
requires. A stop-motion puppet plan compiled to "Live-action, natural light,
|
| 586 |
+
one continuous take. ... Hand-drawn stop-motion puppet animation in felt and
|
| 587 |
+
painted wood", and the two fought.
|
| 588 |
+
|
| 589 |
+
It also explains the anime chain's opening: hop 1 rendered as bright
|
| 590 |
+
naturalistic daylight (**mean luma 72**) against a night plan *and* a night
|
| 591 |
+
place plate, then fell to 46 on hop 2 the moment ESTABLISH stopped riding. That
|
| 592 |
+
had been read as drift; it was the node.
|
| 593 |
+
|
| 594 |
+
Never surfaced because both shipped workflows are live-action.
|
| 595 |
+
|
| 596 |
+
Fixed twice over, because either alone leaves a hole: an `establish` widget
|
| 597 |
+
(appended **last** in `optional`, per the positional-widget rule in section 8),
|
| 598 |
+
and `directives.declares_own_medium()` / `establish_for()`, which drop the
|
| 599 |
+
default when shot 1's opening names a medium. A model-authored plan never
|
| 600 |
+
touches a widget, which is why the automatic arm is the one that matters.
|
| 601 |
+
|
| 602 |
+
### Luminance drifts, and it only goes one way
|
| 603 |
+
|
| 604 |
+
Mean luma per hop across the eight: **72, 46, 35, 17, 11, 11, 19, 14**. Setting
|
| 605 |
+
the ESTABLISH artefact aside, hops 2-6 still slide 46 -> 11. The combat, the
|
| 606 |
+
point of the film, plays at the bottom of it.
|
| 607 |
+
|
| 608 |
+
This is the colour law one level up. `locked` holds a face; nothing holds an
|
| 609 |
+
exposure. Each hop inherits the last frame and darkens it slightly and the
|
| 610 |
+
error compounds. **Restate the light as a positive property in every beat** --
|
| 611 |
+
naming a light *source* ("pale moonlight") does not set a level, and at cfg 1.0
|
| 612 |
+
it only adds a moon.
|
| 613 |
+
|
| 614 |
+
### The seams were never the problem
|
| 615 |
+
|
| 616 |
+
7 of 7 hop joins are invisible: largest frame-to-frame difference at any seam is
|
| 617 |
+
13.6, inside the range of ordinary in-shot motion, and two seams score below the
|
| 618 |
+
film's own mean. Identity held 114 seconds on face plates riding every hop.
|
| 619 |
+
|
| 620 |
+
The one hard cut in the film is **inside** hop 4, 3.25 s in, at 7.1 sd -- more
|
| 621 |
+
than double any other jump. Shot 3 ended "ahead the trunks begin to thin toward
|
| 622 |
+
open ground"; shot 4 opened "Across the flat moonlit stone of @arena_clearing
|
| 623 |
+
the two of them square off". Hop 4 was handed a live frame of a man among trees
|
| 624 |
+
and a beat asserting he stood on open stone, held the forest for 3.25 s, then
|
| 625 |
+
reset the scene. The plan was clean under `check_coherence` and the banned-word
|
| 626 |
+
scan. Hence `plan.check_place_handoff()`.
|
| 627 |
+
|
| 628 |
+
### What the two models did
|
| 629 |
+
|
| 630 |
+
Near-identical answers from Qwen and Gemma on the same prompt -- same beats to
|
| 631 |
+
the word in 5 of 6 shots, same defects, same invented justification. The prompt
|
| 632 |
+
is prescriptive enough to collapse two models onto one answer; a shared blind
|
| 633 |
+
spot is then invisible from output alone.
|
| 634 |
+
|
| 635 |
+
**The word table was inert.** Both models, asked for six 15 s hops, returned
|
| 636 |
+
beats averaging **54 words** against a 70-100 band -- every beat under the floor
|
| 637 |
+
-- and the same models on much shorter hops returned 40-48. Beat length is
|
| 638 |
+
near-invariant to hop length in model output, now measured in a third setting.
|
| 639 |
+
A reference table does not move a model with a prior on paragraph length; the
|
| 640 |
+
rule is now an instruction to count, with a worked 74-word example.
|
| 641 |
+
|
| 642 |
+
**The banned-word list beat the principle, and made things worse.** Zero literal
|
| 643 |
+
banned words in all four plans -- and "The storm's roar begins to fade...
|
| 644 |
+
raindrops strike the glass with decreasing force" and "The storm has passed".
|
| 645 |
+
The list taught token avoidance and handed them a box to tick. It is now framed
|
| 646 |
+
as crude examples of an idea, with the test stated as: is this happening, or has
|
| 647 |
+
it finished happening?
|
| 648 |
+
|
| 649 |
+
**Both abandoned the second location.** Each plated the opening place, moved the
|
| 650 |
+
story elsewhere, gave the new place no plate, and justified it with a rule that
|
| 651 |
+
does not exist ("to avoid conflicting with the frame pin of the new space");
|
| 652 |
+
Qwen cited "rule 8/9", which does not say that. In the lighthouse plan that left
|
| 653 |
+
the lamp room -- four of six hops -- on beat text alone.
|
| 654 |
+
|
| 655 |
+
### `check_place_handoff` has two arms, and both were narrowed by real plans
|
| 656 |
+
|
| 657 |
+
1. **Handoff.** Shot N names a place tag shot N-1 never mentions, and shot N's
|
| 658 |
+
own beat does not carry the journey. The arrival vocabulary had to widen: the
|
| 659 |
+
Showcase's shot 6 ("walks back along the hallway and through the doorway to
|
| 660 |
+
the counter in @kitchen") is correct and was being flagged.
|
| 661 |
+
2. **Abandonment**, not gaps. Warning on any unplated hop also flagged the
|
| 662 |
+
Showcase, which deliberately walks her down an unplated hallway on 4-5 and
|
| 663 |
+
returns the kitchen plate on 6. The rule that survives contact: warn only
|
| 664 |
+
when the plates stop and **never resume**, so the film ends somewhere no
|
| 665 |
+
picture describes.
|
| 666 |
+
|
| 667 |
+
Both shipped workflows are clean under the final version; the anime plan raises
|
| 668 |
+
exactly one warning, on shot 4.
|
| 669 |
+
|
| 670 |
+
### Audio, for the record
|
| 671 |
+
|
| 672 |
+
No background music and no speech, confirmed on the spectrogram: broadband
|
| 673 |
+
transients and noise, no harmonic bands. Seam levels hold within +/-2 dB on five
|
| 674 |
+
of seven joins, and the two exceptions are drops the beats themselves ask for.
|
| 675 |
+
The real audio issue is range, not seams: peaks reach -1.2 dBFS during the fight
|
| 676 |
+
while hop 8 averages -42 dBFS. There is no audio equivalent of
|
| 677 |
+
`HTCToneCompensate`. Left open.
|
| 678 |
+
## 20. Seven features built blind (2026-08-30)
|
| 679 |
+
|
| 680 |
+
Built in one pass with no browser and no GPU render available -- the user was
|
| 681 |
+
away and explicitly asked for the work anyway. Everything below was verified by
|
| 682 |
+
offline execution only. **Section 21 is what happened when it was finally
|
| 683 |
+
opened in ComfyUI**: two of the seven shipped broken in ways no offline test
|
| 684 |
+
could have caught, and the rest measured out. Read section 19 first for the
|
| 685 |
+
measurements that motivated most of it, and 21 for what survived contact.
|
| 686 |
+
|
| 687 |
+
### What shipped
|
| 688 |
+
|
| 689 |
+
| # | thing | where |
|
| 690 |
+
|---|---|---|
|
| 691 |
+
| 1 | `tone_compensate=anchor` + `tone_anchor` strength | `tone.py`, wired in `h3_ref_chain.py` |
|
| 692 |
+
| 2 | `dry_run` -- compile every prompt, render nothing | `h3_ref_chain.py` |
|
| 693 |
+
| 3 | `contact_sheet` -- a fourth IMAGE output | new `sheet.py` |
|
| 694 |
+
| 4 | `render_through` -- stop after hop N | `h3_ref_chain.py` |
|
| 695 |
+
| 5 | `quality=draft` -- 0.3 MP, 6 steps | `h3_ref_chain.py` |
|
| 696 |
+
| 6 | **H3 Seam Report** node | new `seam.py` |
|
| 697 |
+
| 7 | over-delivery lint | `plan.py` |
|
| 698 |
+
|
| 699 |
+
Five new widgets, appended LAST (29 -> 34 values). One new output, appended
|
| 700 |
+
LAST (3 -> 4). Both rules are in section 9; both were obeyed.
|
| 701 |
+
|
| 702 |
+
### The anchor, and why it is not just another tone mode
|
| 703 |
+
|
| 704 |
+
frame_shift/gain_bias/lut are **seam-local**: they cancel the denoiser's tone
|
| 705 |
+
bias on the overlap, which makes each join exact. They cannot see the exposure
|
| 706 |
+
falloff *inside* a hop, and that is what compounds -- hop N darkens across its
|
| 707 |
+
own frames, hands the darker tail to hop N+1, and every individual seam stays
|
| 708 |
+
perfect while the film dims. Section 19 measured 46 -> 11 across hops 2-6.
|
| 709 |
+
|
| 710 |
+
Worth stating plainly because it is counter-intuitive: **a synthetic 8-hop
|
| 711 |
+
chain showed frame_shift making the total slide WORSE** (66/255 vs 35/255 with
|
| 712 |
+
correction off). That is correct behaviour, not a bug. The denoiser's per-hop
|
| 713 |
+
bias happened to lift; cancelling it removed a lift that had been partly
|
| 714 |
+
offsetting the falloff. Seam correction fixes seams. It was never a level
|
| 715 |
+
control and should not be read as one.
|
| 716 |
+
|
| 717 |
+
`anchor` = frame_shift + a second stage pulling each hop's mean back toward
|
| 718 |
+
**hop 1's**. Two properties make it safe to stack:
|
| 719 |
+
|
| 720 |
+
- the pull **ramps from zero** over `ANCHOR_RAMP` (48f) frames, so frame 0 of
|
| 721 |
+
a hop is returned untouched and the seam stays exactly as frame_shift left
|
| 722 |
+
it. Without the ramp a per-hop constant offset re-introduces precisely the
|
| 723 |
+
step frame_shift just removed -- this is the whole design, and the trap
|
| 724 |
+
anyone re-implementing it will fall into;
|
| 725 |
+
- it is **capped** (`ANCHOR_MAX_SHIFT`, 0.06) and scaled by `tone_anchor`
|
| 726 |
+
(0.35), so a slide is corrected across several hops instead of one hop
|
| 727 |
+
snapping back.
|
| 728 |
+
|
| 729 |
+
The correction needs no carry variable between hops: because it is applied
|
| 730 |
+
before `prev_imgs` is taken, the next hop's seam correction matches the
|
| 731 |
+
already-corrected tail and the offset propagates on its own.
|
| 732 |
+
|
| 733 |
+
On the synthetic chain: slide 66 -> 18/255, worst seam step 2.22 -> 2.13/255.
|
| 734 |
+
The seam did not regress, which is the property that mattered.
|
| 735 |
+
|
| 736 |
+
Intent is indistinguishable from drift from the inside, hence the per-shot
|
| 737 |
+
`tone` field: `"free"` skips one hop's pull, `"rebase"` moves the anchor onto
|
| 738 |
+
that hop. A deliberate walk into a cellar needs `rebase` or the chain spends
|
| 739 |
+
the rest of the film brightening it back.
|
| 740 |
+
|
| 741 |
+
### dry_run: what it must not touch
|
| 742 |
+
|
| 743 |
+
The value is that it costs seconds, so every expensive thing is guarded:
|
| 744 |
+
`MiniMaxH3SigmaShift`, `KSamplerSelect`, `BasicScheduler`, `_model_fingerprint`
|
| 745 |
+
(it hashes patched weights), the hop store, and -- the big one -- the master
|
| 746 |
+
preallocation. `master_imgs` for 8 x 15 s at 1280x736 is 2742 float frames,
|
| 747 |
+
about **31 GB**. A dry run that allocated it would be worse than useless.
|
| 748 |
+
|
| 749 |
+
Hop 2+ needs *a* `prev_imgs` to compute `<Picture N>` ordinals. Content is
|
| 750 |
+
irrelevant to the compiled text, so a `[overlap, 8, 8, 3]` zero tensor stands
|
| 751 |
+
in and the text is byte-identical to a real run's.
|
| 752 |
+
|
| 753 |
+
The smoke test (`tmp/t_dry.py`) replaces all five sampler entry points with
|
| 754 |
+
objects that raise on **any** attribute access, so "did not touch the sampler"
|
| 755 |
+
is asserted rather than assumed. It caught one real bug: the dry block
|
| 756 |
+
referenced `pin_mech_pred` before its assignment, ~40 lines later. Which pin a
|
| 757 |
+
hop gets is decided at render time from whether a sampler latent exists, so a
|
| 758 |
+
dry run genuinely cannot know it -- the sheet reports the `pin_to_qwen`
|
| 759 |
+
*setting* instead. Reporting AddGuide for every hop would have been a lie.
|
| 760 |
+
|
| 761 |
+
### The over-delivery lint
|
| 762 |
+
|
| 763 |
+
The one defect class every other check structurally misses: both shots are
|
| 764 |
+
individually well-formed, the directives are individually legal, and only the
|
| 765 |
+
JOIN between them is wrong. `tail=settle|hold` promises rest; a following beat
|
| 766 |
+
that opens "She continues...", "Walking to...", "Mid-sentence..." asks the model
|
| 767 |
+
to carry on what the hop before was told to stop.
|
| 768 |
+
|
| 769 |
+
Narrow on purpose. Trailing spaces in `_MID_ACTION` are load-bearing ("keeps "
|
| 770 |
+
not "keepsake", "still " not "stillness"), and `_MID_ACTION_LEAD` is only
|
| 771 |
+
checked at position 0, which is what stops "Morning light..." and "Nothing
|
| 772 |
+
moves..." from firing. Verified against those exact traps, and both shipped
|
| 773 |
+
plans stay quiet.
|
| 774 |
+
|
| 775 |
+
It will miss a beat that opens mid-action without saying so. That is accepted:
|
| 776 |
+
a false positive that blocked a render would be worse than the defect.
|
| 777 |
+
|
| 778 |
+
### Notes for whoever picks this up
|
| 779 |
+
|
| 780 |
+
- `sheet.py` and `seam.py` catch every exception and return a placeholder
|
| 781 |
+
image. A picture must never lose a finished chain. Do not "clean up" those
|
| 782 |
+
handlers. The placeholder was 1x1 until section 21 -- see there for why an
|
| 783 |
+
inert-looking image is not inert.
|
| 784 |
+
- The contact sheet stores frames through `sheet.small()` (168px tall). Two
|
| 785 |
+
full frames per hop across eight hops is 180 MB held for the whole render for
|
| 786 |
+
no reason.
|
| 787 |
+
- The sheet shows `imgs[overlap_n]` for hops 2+, not `imgs[0]`: the first
|
| 788 |
+
`overlap` frames are trimmed at the join, so `imgs[0]` is a frame the master
|
| 789 |
+
never contains.
|
| 790 |
+
- `tools/check_workflows.py` derives the expected widget list from the live
|
| 791 |
+
`INPUT_TYPES`, so it needed no edit for the five new widgets -- only
|
| 792 |
+
`SaveImage` added to `CORE`, for the Starter's new contact-sheet node.
|
| 793 |
+
(Section 21 added `PreviewAny`, `PreviewImage` and `HTCSeamReport` to those
|
| 794 |
+
allowlists when the seam report was wired into the Starter.)
|
| 795 |
+
- The Starter now ships `contact_sheet=on` with a `SaveImage` wired; the
|
| 796 |
+
Showcase ships it off. Starter is the teaching graph, so the feature is on
|
| 797 |
+
the canvas where it will be found.
|
| 798 |
+
|
| 799 |
+
### Unverified, in priority order
|
| 800 |
+
|
| 801 |
+
All five items that stood here were closed on 2026-08-30. See section 21.
|
| 802 |
+
|
| 803 |
+
## 21. What the first ComfyUI session measured (2026-08-30)
|
| 804 |
+
|
| 805 |
+
Section 20's seven features, opened in a browser and run on a GPU for the first
|
| 806 |
+
time. Five measured out. **Two were broken, and neither could have been caught
|
| 807 |
+
by any offline test that existed** -- both failures lived in the gap between
|
| 808 |
+
"the Python is correct" and "the graph runs".
|
| 809 |
+
|
| 810 |
+
### Bug 1: 0.4.0 never touched `js/`
|
| 811 |
+
|
| 812 |
+
`git show --stat` on the 0.4.0 commit lists twenty files and not one under
|
| 813 |
+
`js/`. The five new widgets were declared in `INPUT_TYPES` and never added to
|
| 814 |
+
`GROUPS` in `js/editor/run_panel.js`, so the run panel did not draw them.
|
| 815 |
+
|
| 816 |
+
It did not *look* broken, which is the interesting part. The panel hides only
|
| 817 |
+
the widgets it successfully drew -- a deliberate design so an undrawable dial
|
| 818 |
+
never vanishes from the node -- so all five fell through to native dials and
|
| 819 |
+
worked fine. `tools/check_workflows.py` passed throughout, because it derives
|
| 820 |
+
from `INPUT_TYPES` and `GROUPS` is display-only.
|
| 821 |
+
|
| 822 |
+
**The lesson: a Python-side widget list and a JS-side widget list are two
|
| 823 |
+
sources of truth, and nothing checks that they agree.** Adding a widget means
|
| 824 |
+
editing both. There is still no checker for this.
|
| 825 |
+
|
| 826 |
+
### Bug 2: a 1x1 image is not inert
|
| 827 |
+
|
| 828 |
+
A dry run returned `sheet.placeholder()` on `images` -- a 1x1 black frame. The
|
| 829 |
+
Starter wires `images` into `CreateVideo` -> `SaveVideo`. libx264 in yuv420p
|
| 830 |
+
subsamples chroma by 2 and **cannot open a codec context on an odd dimension**,
|
| 831 |
+
so every dry run died in `avcodec_open2` before writing a frame, with a
|
| 832 |
+
traceback naming ComfyUI's video node and nothing of ours.
|
| 833 |
+
|
| 834 |
+
Reproduced in isolation: 1x1 throws, 2x2 encodes. `placeholder()` now takes a
|
| 835 |
+
width and height, floors at 2x2, and rounds odd dimensions down to even; the
|
| 836 |
+
dry run passes the geometry the plan resolved to, so a dry run yields one black
|
| 837 |
+
frame at the real resolution and downstream video nodes are happy.
|
| 838 |
+
|
| 839 |
+
`tools/check_features.py` had asserted `shape == (1, 1, 1, 3)` -- the offline
|
| 840 |
+
suite was *pinning the bug in place*. It now asserts an `encodable()` contract
|
| 841 |
+
instead. **A test that encodes an exact wrong value is worse than no test.**
|
| 842 |
+
|
| 843 |
+
### `tone_compensate=anchor`, measured on a real chain
|
| 844 |
+
|
| 845 |
+
Three hops, 8 s, 0.3 MP, 6 steps, one seed, one cache. Because the hop store
|
| 846 |
+
writes *before* the tone block runs, flipping tone modes re-grades cached
|
| 847 |
+
renders in ~14 s instead of 164 s -- so the whole sweep is nearly free. Drift is
|
| 848 |
+
hop 3 mean minus hop 1 mean; seams are the step across the join.
|
| 849 |
+
|
| 850 |
+
| `tone_anchor` | drift | seam @192 | seam @362 | flicker |
|
| 851 |
+
|---|---|---|---|---|
|
| 852 |
+
| off | 13.4/255 | 1.2 | 1.9 | 0.4675 |
|
| 853 |
+
| 0.15 | 7.4 (-45%) | 0.9 | 1.0 | 0.4566 |
|
| 854 |
+
| 0.35 | 5.1 (-62%) | 1.4 | 1.6 | 0.4618 |
|
| 855 |
+
| 0.60 | 2.9 (-78%) | 2.1 | 1.8 | 0.4661 |
|
| 856 |
+
|
| 857 |
+
Monotonic, no knee: each step of strength buys ~16pp of drift removal and costs
|
| 858 |
+
~0.5/255 of seam. Hop 1 is byte-identical across all four, as the design
|
| 859 |
+
requires. **The shipped 0.35 default is defensible and stays.**
|
| 860 |
+
|
| 861 |
+
The propagation claim in section 20 -- that no carry variable is needed --
|
| 862 |
+
is visible in the logs: hop 3's `frame_shift` grew with anchor strength
|
| 863 |
+
(`r+0.0185` -> `r+0.0217` -> `r+0.0256`) because hop 3 measures against hop 2's
|
| 864 |
+
already-corrected tail. It self-propagates, exactly as designed.
|
| 865 |
+
|
| 866 |
+
**Caveat that limits this measurement**: the test scene walks the subject toward
|
| 867 |
+
a bright window, so some of that 13.4/255 is light a real camera would also
|
| 868 |
+
produce. Anchor cannot distinguish motivated light from drift -- that is what
|
| 869 |
+
the per-shot `tone` field is for. A `camera=hold` scene is the run that would
|
| 870 |
+
justify moving off 0.35.
|
| 871 |
+
|
| 872 |
+
### The seam report node, validated
|
| 873 |
+
|
| 874 |
+
Never executed before this session. Wired into the Starter and checked against
|
| 875 |
+
an independent decode of the mp4: agreement to **±0.07/255** across six seam
|
| 876 |
+
readings under two different hop geometries. Its arithmetic is right.
|
| 877 |
+
|
| 878 |
+
A better result came out of the cross-check. On a 5-hop master the eight largest
|
| 879 |
+
frame-to-frame luma jumps were at f289, f368, f409, f412, f463, f464, f482,
|
| 880 |
+
f497 -- **not one of them a seam**. Every join is quieter than ordinary scene
|
| 881 |
+
motion. That is the pack's central claim, measured.
|
| 882 |
+
|
| 883 |
+
**Trap, and it cost a wrong diagnosis.** 5 hops x 124f and 3 hops x 192f both
|
| 884 |
+
total 532 frames at overlap 22. The node derives hop length from
|
| 885 |
+
`frames`, `hops` and `overlap`, so a wrong `hops` yields a plausible length and
|
| 886 |
+
four confidently wrong seam positions. There is no way for it to know better
|
| 887 |
+
from `images` alone -- but the chain's `info` output carries the real geometry,
|
| 888 |
+
so an optional `info` input that cross-checks would turn this class of mistake
|
| 889 |
+
into an error message. Worth doing.
|
| 890 |
+
|
| 891 |
+
### `quality=draft` is close to a no-op here
|
| 892 |
+
|
| 893 |
+
Draft's two levers are resolution -> 0.3 MP and steps -> 6. In the turbo regime
|
| 894 |
+
this pack targets, a "final" run is *already* 0.3 MP at 6-8 steps, so the first
|
| 895 |
+
lever does nothing and the second saves one step:
|
| 896 |
+
|
| 897 |
+
- draft, 6 steps: **42.1 s/hop**
|
| 898 |
+
- final, 7 steps: **43.3 / 45.2 / 50.2 / 44.1 s/hop**
|
| 899 |
+
|
| 900 |
+
About 7% apart. Draft only earns its place if final is genuinely heavier --
|
| 901 |
+
1.0 MP at 14 steps. Kept, because that configuration exists, but it is not the
|
| 902 |
+
fast-preview button it sounds like. `dry_run` is the fast-preview button.
|
| 903 |
+
|
| 904 |
+
### The rest
|
| 905 |
+
|
| 906 |
+
- **Contact sheet**: correct and genuinely useful on real renders -- first/last
|
| 907 |
+
thumbnails per hop, directives, beat, tone line, seed, `cached`, pin
|
| 908 |
+
mechanism. Legible at 1:1; **not** legible in a node preview, where a 1280px
|
| 909 |
+
sheet scales to ~0.25 and 15px body text renders at ~4px. It is a
|
| 910 |
+
click-to-enlarge document. Fonts left alone deliberately.
|
| 911 |
+
- **`render_through`**: truncates correctly (`rendering hops 1-1 of 2`), and
|
| 912 |
+
re-extending works -- a 3-hop chain extended to 5 loaded hops 1-3 from cache
|
| 913 |
+
and started rendering at hop 4.
|
| 914 |
+
- **Over-delivery lint**: exercised offline; never fired in ComfyUI because both
|
| 915 |
+
shipped plans and the test plan stay clean. Unproven against a real positive.
|
|
@@ -2026,7 +2026,12 @@ class HandTieClips:
|
|
| 2026 |
meta={"dry_run": True, "hops": int(n),
|
| 2027 |
"would_be_frames": int(total_frames),
|
| 2028 |
"done": True})
|
| 2029 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2030 |
{"waveform": torch.zeros((1, 2, 1024), dtype=torch.float32),
|
| 2031 |
"sample_rate": 44100},
|
| 2032 |
info,
|
|
|
|
| 2026 |
meta={"dry_run": True, "hops": int(n),
|
| 2027 |
"would_be_frames": int(total_frames),
|
| 2028 |
"done": True})
|
| 2029 |
+
# Sized to the geometry the chain WOULD have produced, not 1x1:
|
| 2030 |
+
# the images output usually lands in a video encoder, and one black
|
| 2031 |
+
# frame at the real resolution both encodes cleanly and shows the
|
| 2032 |
+
# dimensions the plan resolved to. One frame is not the master --
|
| 2033 |
+
# the 31 GB allocation this mode exists to avoid is untouched.
|
| 2034 |
+
return (_sheet.placeholder(width, height),
|
| 2035 |
{"waveform": torch.zeros((1, 2, 1024), dtype=torch.float32),
|
| 2036 |
"sample_rate": 44100},
|
| 2037 |
info,
|
|
@@ -38,8 +38,19 @@ const GROUPS = [
|
|
| 38 |
"shift_video", "shift_audio"]],
|
| 39 |
["join & pin", ["hop_script", "pin_to_qwen", "ref_image_size",
|
| 40 |
"audio_pin_frames", "pin_renorm", "pin_noise",
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
["cache", ["cache_hops", "cache_budget_gb"]],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
];
|
| 44 |
|
| 45 |
/**
|
|
|
|
| 38 |
"shift_video", "shift_audio"]],
|
| 39 |
["join & pin", ["hop_script", "pin_to_qwen", "ref_image_size",
|
| 40 |
"audio_pin_frames", "pin_renorm", "pin_noise",
|
| 41 |
+
// tone_anchor sits next to the mode it modifies: it is
|
| 42 |
+
// read only when tone_compensate is `anchor`, and split
|
| 43 |
+
// across groups nothing says so. Deliberately NOT
|
| 44 |
+
// suppressed in the other modes -- suppression only stops
|
| 45 |
+
// the panel drawing a dial, and an undrawn dial reappears
|
| 46 |
+
// as a raw native widget, which is worse than an inert one.
|
| 47 |
+
"tone_compensate", "tone_anchor"]],
|
| 48 |
["cache", ["cache_hops", "cache_budget_gb"]],
|
| 49 |
+
// Added 2026-08-30. 0.4.0 shipped these five on the Python side and never
|
| 50 |
+
// touched js/, so all five fell through to native dials -- the documented
|
| 51 |
+
// fallback doing its job, not a break. Order is the order you reach for
|
| 52 |
+
// them: prove the plan compiles, pick the fidelity, stop short, look.
|
| 53 |
+
["preview", ["dry_run", "quality", "render_through", "contact_sheet"]],
|
| 54 |
];
|
| 55 |
|
| 56 |
/**
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
[project]
|
| 2 |
name = "comfyui-hand-tie-clips"
|
| 3 |
-
version = "0.4.
|
| 4 |
description = "Native MiniMax H3 Ref2VA chain: persistent references, previous-segment clip+audio pin, one queue."
|
| 5 |
readme = "README.md"
|
| 6 |
license = { file = "LICENSE" }
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "comfyui-hand-tie-clips"
|
| 3 |
+
version = "0.4.1"
|
| 4 |
description = "Native MiniMax H3 Ref2VA chain: persistent references, previous-segment clip+audio pin, one queue."
|
| 5 |
readme = "README.md"
|
| 6 |
license = { file = "LICENSE" }
|
|
@@ -100,9 +100,21 @@ def _font(size, bold=False):
|
|
| 100 |
return f
|
| 101 |
|
| 102 |
|
| 103 |
-
def placeholder():
|
| 104 |
-
"""The
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
|
| 108 |
def _wrap(draw, text, font, max_w):
|
|
|
|
| 100 |
return f
|
| 101 |
|
| 102 |
|
| 103 |
+
def placeholder(width=16, height=16):
|
| 104 |
+
"""The black IMAGE returned when no sheet was built.
|
| 105 |
+
|
| 106 |
+
Even dimensions, minimum 2, are a hard requirement rather than a default.
|
| 107 |
+
This used to be 1x1, which is inert to `SaveImage` but detonates any video
|
| 108 |
+
encoder downstream: libx264 in yuv420p subsamples chroma by 2 and cannot
|
| 109 |
+
open a context whose width or height is odd, so a dry run wired to
|
| 110 |
+
`SaveVideo` -- exactly what the Starter ships -- died in `avcodec_open2`
|
| 111 |
+
before a frame was written, with a traceback naming ComfyUI's video node
|
| 112 |
+
and nothing of ours. A placeholder whose whole job is to keep a graph alive
|
| 113 |
+
must survive the nodes the graph actually contains.
|
| 114 |
+
"""
|
| 115 |
+
w = max(2, int(width) & ~1)
|
| 116 |
+
h = max(2, int(height) & ~1)
|
| 117 |
+
return torch.zeros((1, h, w, 3), dtype=torch.float32)
|
| 118 |
|
| 119 |
|
| 120 |
def _wrap(draw, text, font, max_w):
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
r"""Offline regression tests for the 2026-08-30 feature set (
|
| 2 |
|
| 3 |
None of these features could be tested in a browser or against a GPU when they
|
| 4 |
were written, so this is the only thing standing between them and a silent
|
|
@@ -34,6 +34,16 @@ sys.path.insert(0, COMFY)
|
|
| 34 |
FAIL = []
|
| 35 |
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
def ck(name, cond, detail=""):
|
| 38 |
print(" %-4s %-52s %s" % ("ok" if cond else "FAIL", name, detail))
|
| 39 |
if not cond:
|
|
@@ -175,9 +185,13 @@ def main():
|
|
| 175 |
ck("builds text-only (dry run shape)",
|
| 176 |
SH.build([{"hop": 1, "first": None, "last": None, "beat": "x",
|
| 177 |
"directives": {}}], "t").shape[1] > 20)
|
| 178 |
-
ck("empty -> placeholder",
|
| 179 |
ck("hostile row -> placeholder, no raise",
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
ck("small() shrinks a full frame",
|
| 182 |
tuple(SH.small(torch.rand(736, 1280, 3)).shape)[0] == SH.THUMB_H)
|
| 183 |
|
|
@@ -231,7 +245,13 @@ def main():
|
|
| 231 |
out, _ = run(tone_compensate="anchor", cache_hops="on")
|
| 232 |
ck("dry run returns four values", len(out) == 4)
|
| 233 |
ck("dry run never reached the sampler", True, "asserted by the traps above")
|
| 234 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
ck("audio is silent, not None", out[1]["waveform"].abs().max() == 0)
|
| 236 |
ck("info carries every compiled prompt", out[2].count("===== hop") == 8)
|
| 237 |
ck("contact sheet is built on a dry run", out[3].shape[1] > 100)
|
|
|
|
| 1 |
+
r"""Offline regression tests for the 2026-08-30 feature set (docs/DEVLOG.md section 20).
|
| 2 |
|
| 3 |
None of these features could be tested in a browser or against a GPU when they
|
| 4 |
were written, so this is the only thing standing between them and a silent
|
|
|
|
| 34 |
FAIL = []
|
| 35 |
|
| 36 |
|
| 37 |
+
def encodable(t):
|
| 38 |
+
"""An IMAGE a video encoder can actually open: 4-D, one or more frames,
|
| 39 |
+
both dimensions even and >= 2. Checked instead of an exact 1x1 shape
|
| 40 |
+
because the exact shape is what shipped broken."""
|
| 41 |
+
sh = tuple(t.shape)
|
| 42 |
+
return (len(sh) == 4 and sh[0] >= 1 and sh[3] == 3
|
| 43 |
+
and sh[1] >= 2 and sh[2] >= 2
|
| 44 |
+
and sh[1] % 2 == 0 and sh[2] % 2 == 0)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
def ck(name, cond, detail=""):
|
| 48 |
print(" %-4s %-52s %s" % ("ok" if cond else "FAIL", name, detail))
|
| 49 |
if not cond:
|
|
|
|
| 185 |
ck("builds text-only (dry run shape)",
|
| 186 |
SH.build([{"hop": 1, "first": None, "last": None, "beat": "x",
|
| 187 |
"directives": {}}], "t").shape[1] > 20)
|
| 188 |
+
ck("empty -> placeholder", encodable(SH.build([])))
|
| 189 |
ck("hostile row -> placeholder, no raise",
|
| 190 |
+
encodable(SH.build([{"hop": 1, "first": "nope", "beat": "x"}])))
|
| 191 |
+
ck("placeholder floors at 2x2, never 1x1",
|
| 192 |
+
encodable(SH.placeholder(1, 1)) and encodable(SH.placeholder(0, 0)))
|
| 193 |
+
ck("placeholder rounds odd dimensions down to even",
|
| 194 |
+
tuple(SH.placeholder(737, 415).shape) == (1, 414, 736, 3))
|
| 195 |
ck("small() shrinks a full frame",
|
| 196 |
tuple(SH.small(torch.rand(736, 1280, 3)).shape)[0] == SH.THUMB_H)
|
| 197 |
|
|
|
|
| 245 |
out, _ = run(tone_compensate="anchor", cache_hops="on")
|
| 246 |
ck("dry run returns four values", len(out) == 4)
|
| 247 |
ck("dry run never reached the sampler", True, "asserted by the traps above")
|
| 248 |
+
# Was `== (1, 1, 1, 3)`. A 1x1 frame is inert to SaveImage and fatal to
|
| 249 |
+
# every video encoder -- libx264 cannot open a yuv420p context on an odd
|
| 250 |
+
# dimension -- so the Starter's own SaveVideo died on a dry run. The
|
| 251 |
+
# contract is now "encodable, at the geometry the plan resolved to".
|
| 252 |
+
ck("dry-run images is one encodable frame", encodable(out[0]))
|
| 253 |
+
ck("dry-run images carries the planned geometry",
|
| 254 |
+
tuple(out[0].shape) == (1, 736, 1280, 3), str(tuple(out[0].shape)))
|
| 255 |
ck("audio is silent, not None", out[1]["waveform"].abs().max() == 0)
|
| 256 |
ck("info carries every compiled prompt", out[2].count("===== hop") == 8)
|
| 257 |
ck("contact sheet is built on a dry run", out[3].shape[1] > 100)
|
|
@@ -25,8 +25,8 @@ from h3p import plan as PL # noqa: E402
|
|
| 25 |
from h3p import h3_ref_chain as H3 # noqa: E402
|
| 26 |
|
| 27 |
CORE = {"UNETLoader", "CLIPLoader", "VAELoader", "CreateVideo", "SaveVideo",
|
| 28 |
-
"SaveImage", "Note", "MarkdownNote"}
|
| 29 |
-
OURS = {"HandTieClips", "HTCChainPreview"}
|
| 30 |
# Declared dependencies, not accidents: the turbo stack this node is actually
|
| 31 |
# run with. Anything outside these three sets is a pack the reader never asked
|
| 32 |
# for and must not appear in a shipped example.
|
|
|
|
| 25 |
from h3p import h3_ref_chain as H3 # noqa: E402
|
| 26 |
|
| 27 |
CORE = {"UNETLoader", "CLIPLoader", "VAELoader", "CreateVideo", "SaveVideo",
|
| 28 |
+
"SaveImage", "Note", "MarkdownNote", "PreviewAny", "PreviewImage"}
|
| 29 |
+
OURS = {"HandTieClips", "HTCChainPreview", "HTCSeamReport"}
|
| 30 |
# Declared dependencies, not accidents: the turbo stack this node is actually
|
| 31 |
# run with. Anything outside these three sets is a pack the reader never asked
|
| 32 |
# for and must not appear in a shipped example.
|
|
@@ -25,7 +25,7 @@ GREEN = ("#232", "#353") # ComfyUI's green -- card 1 only, so the entry
|
|
| 25 |
GROUP = {
|
| 26 |
"id": 1,
|
| 27 |
"title": "READ ME -- writing for this node",
|
| 28 |
-
"bounding": [-990, -465, 980,
|
| 29 |
"color": "#3f789e",
|
| 30 |
"font_size": 24,
|
| 31 |
}
|
|
@@ -260,14 +260,29 @@ still keys exactly as it will in the full run.
|
|
| 260 |
0.3 MP, 6 steps. Enough to read blocking, camera and whether a join lands.
|
| 261 |
Both values are in the cache key, so a draft never overwrites its final.
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
---
|
| 264 |
|
| 265 |
Afterwards: **`contact_sheet = on`** gives one row per hop -- first and last
|
| 266 |
delivered frame, beat, directives, seed, cache hit, tone correction. Wire it
|
| 267 |
to a Save Image.
|
| 268 |
|
| 269 |
-
And **H3 Seam Report**
|
| 270 |
-
invisible / marginal / visible, plus the
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
"""
|
| 272 |
|
| 273 |
TROUBLE = """\
|
|
@@ -285,7 +300,7 @@ TROUBLE = """\
|
|
| 285 |
| Two characters' faces merge | Both declared as the same `subject` | One subject number per person |
|
| 286 |
| The face becomes a different person partway through | A hop scheduled with no face plate. `locked` holds a face that is still right; only a plate rebuilds one that is gone, and the drift never self-corrects | Put the face ref on **every** hop |
|
| 287 |
| A stylised plan renders photoreal | The node's hop-1 establishing line asserts live action | Name the medium in shot 1's first sentence, or clear the `establish` widget |
|
| 288 |
-
| The film gets darker every hop | Each hop dims across its own frames and hands the darker tail on. Seam correction cannot see it | `tone_compensate=anchor`. Also restate the light as a positive property in every beat |
|
| 289 |
| A location introduced mid-plan drifts | It has no place plate of its own | Plate it, on the hop it arrives and every hop after |
|
| 290 |
| A prop or garment changes colour or material | Named with no adjective, so each hop's encode is free to invent one | Repeat the adjective in every beat, and state it as a property in `context` |
|
| 291 |
| A hard cut ~1.5 s into a hop, mid-scene | The previous hop over-delivered, so this beat instructs what its own live frame already did | One movement per hop; write the next beat so it is true from either ending |
|
|
@@ -430,7 +445,7 @@ CARDS = [
|
|
| 430 |
("rules", "The rules", [-960, 600], [440, 760], YELLOW, RULES),
|
| 431 |
("refs", "References and @tags", [-960, 1420], [440, 640], YELLOW, REFERENCES),
|
| 432 |
("directives", "Directives", [-480, -400], [440, 640], YELLOW, DIRECTIVES),
|
| 433 |
-
("trouble", "When it goes wrong", [-480, 280], [440,
|
| 434 |
-
("check", "Check before you render", [-480,
|
| 435 |
-
("author", "Let a model write it", [-480,
|
| 436 |
]
|
|
|
|
| 25 |
GROUP = {
|
| 26 |
"id": 1,
|
| 27 |
"title": "READ ME -- writing for this node",
|
| 28 |
+
"bounding": [-990, -465, 980, 3200],
|
| 29 |
"color": "#3f789e",
|
| 30 |
"font_size": 24,
|
| 31 |
}
|
|
|
|
| 260 |
0.3 MP, 6 steps. Enough to read blocking, camera and whether a join lands.
|
| 261 |
Both values are in the cache key, so a draft never overwrites its final.
|
| 262 |
|
| 263 |
+
It is a **fidelity** lever more than a speed one. Measured: ~42 s/hop against
|
| 264 |
+
~45 s/hop at 7 steps. If you already render at 0.3 MP and 6-8 steps it saves
|
| 265 |
+
almost nothing -- `dry_run` is the fast button. Draft earns its place when your
|
| 266 |
+
final is genuinely heavier, 1.0 MP at 14 steps.
|
| 267 |
+
|
| 268 |
---
|
| 269 |
|
| 270 |
Afterwards: **`contact_sheet = on`** gives one row per hop -- first and last
|
| 271 |
delivered frame, beat, directives, seed, cache hit, tone correction. Wire it
|
| 272 |
to a Save Image.
|
| 273 |
|
| 274 |
+
And **H3 Seam Report** -- already wired on this canvas -- takes the chain's
|
| 275 |
+
`images` and measures every join: invisible / marginal / visible, plus the
|
| 276 |
+
chain's cumulative drift.
|
| 277 |
+
|
| 278 |
+
**Set `hops` to your shot count.** It derives hop length from frames, hops and
|
| 279 |
+
overlap, so a wrong `hops` does not error -- it returns a plausible length and
|
| 280 |
+
puts every seam at a frame where no join exists.
|
| 281 |
+
|
| 282 |
+
A single reading includes whatever the scene did across the cut, so it is an
|
| 283 |
+
upper bound. To isolate the seam, A/B two renders from the same seed and cache
|
| 284 |
+
changing only `tone_compensate`; the hop store writes before tone is applied,
|
| 285 |
+
so the second run re-grades from cache in seconds.
|
| 286 |
"""
|
| 287 |
|
| 288 |
TROUBLE = """\
|
|
|
|
| 300 |
| Two characters' faces merge | Both declared as the same `subject` | One subject number per person |
|
| 301 |
| The face becomes a different person partway through | A hop scheduled with no face plate. `locked` holds a face that is still right; only a plate rebuilds one that is gone, and the drift never self-corrects | Put the face ref on **every** hop |
|
| 302 |
| A stylised plan renders photoreal | The node's hop-1 establishing line asserts live action | Name the medium in shot 1's first sentence, or clear the `establish` widget |
|
| 303 |
+
| The film gets darker every hop | Each hop dims across its own frames and hands the darker tail on. Seam correction cannot see it | `tone_compensate=anchor`. `tone_anchor=0.35` removed ~60% of measured drift; 0.6 removes ~78% and costs ~0.5/255 more seam step. Also restate the light as a positive property in every beat |
|
| 304 |
| A location introduced mid-plan drifts | It has no place plate of its own | Plate it, on the hop it arrives and every hop after |
|
| 305 |
| A prop or garment changes colour or material | Named with no adjective, so each hop's encode is free to invent one | Repeat the adjective in every beat, and state it as a property in `context` |
|
| 306 |
| A hard cut ~1.5 s into a hop, mid-scene | The previous hop over-delivered, so this beat instructs what its own live frame already did | One movement per hop; write the next beat so it is true from either ending |
|
|
|
|
| 445 |
("rules", "The rules", [-960, 600], [440, 760], YELLOW, RULES),
|
| 446 |
("refs", "References and @tags", [-960, 1420], [440, 640], YELLOW, REFERENCES),
|
| 447 |
("directives", "Directives", [-480, -400], [440, 640], YELLOW, DIRECTIVES),
|
| 448 |
+
("trouble", "When it goes wrong", [-480, 280], [440, 900], YELLOW, TROUBLE),
|
| 449 |
+
("check", "Check before you render", [-480, 1220], [440, 800], GREEN, CHECK),
|
| 450 |
+
("author", "Let a model write it", [-480, 2060], [440, 620], YELLOW, AUTHOR),
|
| 451 |
]
|
|
@@ -1,8 +1,8 @@
|
|
| 1 |
{
|
| 2 |
"id": "h3-ref2va-chain",
|
| 3 |
"revision": 1,
|
| 4 |
-
"last_node_id":
|
| 5 |
-
"last_link_id":
|
| 6 |
"nodes": [
|
| 7 |
{
|
| 8 |
"id": 2,
|
|
@@ -529,7 +529,8 @@
|
|
| 529 |
"type": "IMAGE",
|
| 530 |
"slot_index": 0,
|
| 531 |
"links": [
|
| 532 |
-
5
|
|
|
|
| 533 |
]
|
| 534 |
},
|
| 535 |
{
|
|
@@ -1088,6 +1089,118 @@
|
|
| 1088 |
"HandTieClips/contact_sheet"
|
| 1089 |
]
|
| 1090 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1091 |
{
|
| 1092 |
"id": 20,
|
| 1093 |
"type": "MarkdownNote",
|
|
@@ -1100,7 +1213,7 @@
|
|
| 1100 |
940
|
| 1101 |
],
|
| 1102 |
"flags": {},
|
| 1103 |
-
"order":
|
| 1104 |
"mode": 0,
|
| 1105 |
"inputs": [],
|
| 1106 |
"outputs": [],
|
|
@@ -1126,7 +1239,7 @@
|
|
| 1126 |
760
|
| 1127 |
],
|
| 1128 |
"flags": {},
|
| 1129 |
-
"order":
|
| 1130 |
"mode": 0,
|
| 1131 |
"inputs": [],
|
| 1132 |
"outputs": [],
|
|
@@ -1152,7 +1265,7 @@
|
|
| 1152 |
640
|
| 1153 |
],
|
| 1154 |
"flags": {},
|
| 1155 |
-
"order":
|
| 1156 |
"mode": 0,
|
| 1157 |
"inputs": [],
|
| 1158 |
"outputs": [],
|
|
@@ -1178,7 +1291,7 @@
|
|
| 1178 |
640
|
| 1179 |
],
|
| 1180 |
"flags": {},
|
| 1181 |
-
"order":
|
| 1182 |
"mode": 0,
|
| 1183 |
"inputs": [],
|
| 1184 |
"outputs": [],
|
|
@@ -1201,10 +1314,10 @@
|
|
| 1201 |
],
|
| 1202 |
"size": [
|
| 1203 |
440,
|
| 1204 |
-
|
| 1205 |
],
|
| 1206 |
"flags": {},
|
| 1207 |
-
"order":
|
| 1208 |
"mode": 0,
|
| 1209 |
"inputs": [],
|
| 1210 |
"outputs": [],
|
|
@@ -1213,7 +1326,7 @@
|
|
| 1213 |
"htc_card": "trouble"
|
| 1214 |
},
|
| 1215 |
"widgets_values": [
|
| 1216 |
-
"## When it goes wrong\n\n| symptom | cause | fix |\n|---|---|---|\n| The clip cuts to the reference photo in its last seconds | The beat finished before the frames did | Set `tail`, give the beat enough to do |\n| A stray gesture or line in the closing second | `tail: ongoing` on the final shot | `settle` or `hold` |\n| She keeps talking after you asked for quiet | You named the ending | Pose plus a sound |\n| Dialogue continues into hops that have none written | The hop before ended on speech | Land the line early; non-verbal action into the seam; a sound bed on every quiet hop |\n| A character walks between two rooms and one morphs into the other | `continuous` across a location change | `match_cut` |\n| Silence renders as speech | Silence written as an absence | Name room tone, a fridge, a distant car |\n| Ambience is a five-second hiss | Broadband wording | Narrowband, or one discrete event |\n| Two characters' faces merge | Both declared as the same `subject` | One subject number per person |\n| The face becomes a different person partway through | A hop scheduled with no face plate. `locked` holds a face that is still right; only a plate rebuilds one that is gone, and the drift never self-corrects | Put the face ref on **every** hop |\n| A stylised plan renders photoreal | The node's hop-1 establishing line asserts live action | Name the medium in shot 1's first sentence, or clear the `establish` widget |\n| The film gets darker every hop | Each hop dims across its own frames and hands the darker tail on. Seam correction cannot see it | `tone_compensate=anchor`. Also restate the light as a positive property in every beat |\n| A location introduced mid-plan drifts | It has no place plate of its own | Plate it, on the hop it arrives and every hop after |\n| A prop or garment changes colour or material | Named with no adjective, so each hop's encode is free to invent one | Repeat the adjective in every beat, and state it as a property in `context` |\n| A hard cut ~1.5 s into a hop, mid-scene | The previous hop over-delivered, so this beat instructs what its own live frame already did | One movement per hop; write the next beat so it is true from either ending |\n| A continuous join reads as a cut | Framing change with `camera: hold` | Earn it on the move, or `framing: keep` |\n| The run stops, naming a reference | That row's picture is not in `h3_refs` | Drop the file on the row, or clear its picture to run without it |\n| A reference has no effect on some hop | Its `shots` list leaves that hop out | List every hop the picture should ride |\n| A deliberately dark scene keeps being brightened | `anchor` cannot tell intent from drift | `\"tone\": \"rebase\"` on that scene's first shot |\n| You cannot tell which hop broke | 114 s is a lot to scrub | `contact_sheet=on` -- one row per hop, first and last frame |\n| A pasted plan is rejected as invalid JSON | Escaped double quotes mangled in transit | Single quotes around dialogue |\n\nEvery error message names the shot or the reference it came from. Nothing\nguesses."
|
| 1217 |
],
|
| 1218 |
"color": "#432",
|
| 1219 |
"bgcolor": "#653"
|
|
@@ -1223,14 +1336,14 @@
|
|
| 1223 |
"type": "MarkdownNote",
|
| 1224 |
"pos": [
|
| 1225 |
-480,
|
| 1226 |
-
|
| 1227 |
],
|
| 1228 |
"size": [
|
| 1229 |
440,
|
| 1230 |
-
|
| 1231 |
],
|
| 1232 |
"flags": {},
|
| 1233 |
-
"order":
|
| 1234 |
"mode": 0,
|
| 1235 |
"inputs": [],
|
| 1236 |
"outputs": [],
|
|
@@ -1239,7 +1352,7 @@
|
|
| 1239 |
"htc_card": "check"
|
| 1240 |
},
|
| 1241 |
"widgets_values": [
|
| 1242 |
-
"## Check before you render\n\nA chain is minutes to hours. These three dials cost seconds and catch almost\neverything.\n\n### `dry_run = on`\n\nCompiles every hop's prompt and stops. No model, no sampler. Read the result\non **`info`**, or as a page on **`contact_sheet`**.\n\nWhat you wrote is not what the encoder gets -- directives, continuation\nscaffolding, the identity lock and the `<Picture N>` citations are all\nassembled at render time. This is the only way to see the real thing first.\nEvery plan warning prints on the way through, too.\n\n### `render_through = N`\n\nStop after N hops. With `cache_hops=on`, 3 then 5 then 8 builds the chain up\nand only ever renders the new hops. The plan is **not** truncated -- shot 4\nstill keys exactly as it will in the full run.\n\n### `quality = draft`\n\n0.3 MP, 6 steps. Enough to read blocking, camera and whether a join lands.\nBoth values are in the cache key, so a draft never overwrites its final.\n\n---\n\nAfterwards: **`contact_sheet = on`** gives one row per hop -- first and last\ndelivered frame, beat, directives, seed, cache hit, tone correction. Wire it\nto a Save Image.\n\nAnd **H3 Seam Report** takes the chain's
|
| 1243 |
],
|
| 1244 |
"color": "#232",
|
| 1245 |
"bgcolor": "#353"
|
|
@@ -1249,14 +1362,14 @@
|
|
| 1249 |
"type": "MarkdownNote",
|
| 1250 |
"pos": [
|
| 1251 |
-480,
|
| 1252 |
-
|
| 1253 |
],
|
| 1254 |
"size": [
|
| 1255 |
440,
|
| 1256 |
620
|
| 1257 |
],
|
| 1258 |
"flags": {},
|
| 1259 |
-
"order":
|
| 1260 |
"mode": 0,
|
| 1261 |
"inputs": [],
|
| 1262 |
"outputs": [],
|
|
@@ -1407,6 +1520,30 @@
|
|
| 1407 |
35,
|
| 1408 |
0,
|
| 1409 |
"IMAGE"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1410 |
]
|
| 1411 |
],
|
| 1412 |
"groups": [
|
|
@@ -1417,7 +1554,7 @@
|
|
| 1417 |
-990,
|
| 1418 |
-465,
|
| 1419 |
980,
|
| 1420 |
-
|
| 1421 |
],
|
| 1422 |
"color": "#3f789e",
|
| 1423 |
"font_size": 24
|
|
|
|
| 1 |
{
|
| 2 |
"id": "h3-ref2va-chain",
|
| 3 |
"revision": 1,
|
| 4 |
+
"last_node_id": 38,
|
| 5 |
+
"last_link_id": 22,
|
| 6 |
"nodes": [
|
| 7 |
{
|
| 8 |
"id": 2,
|
|
|
|
| 529 |
"type": "IMAGE",
|
| 530 |
"slot_index": 0,
|
| 531 |
"links": [
|
| 532 |
+
5,
|
| 533 |
+
20
|
| 534 |
]
|
| 535 |
},
|
| 536 |
{
|
|
|
|
| 1089 |
"HandTieClips/contact_sheet"
|
| 1090 |
]
|
| 1091 |
},
|
| 1092 |
+
{
|
| 1093 |
+
"id": 36,
|
| 1094 |
+
"type": "HTCSeamReport",
|
| 1095 |
+
"pos": [
|
| 1096 |
+
2991.7,
|
| 1097 |
+
1100.0
|
| 1098 |
+
],
|
| 1099 |
+
"size": [
|
| 1100 |
+
340,
|
| 1101 |
+
130
|
| 1102 |
+
],
|
| 1103 |
+
"flags": {},
|
| 1104 |
+
"order": 107,
|
| 1105 |
+
"mode": 0,
|
| 1106 |
+
"title": "H3 Seam Report - measure the joins",
|
| 1107 |
+
"inputs": [
|
| 1108 |
+
{
|
| 1109 |
+
"localized_name": "images",
|
| 1110 |
+
"name": "images",
|
| 1111 |
+
"type": "IMAGE",
|
| 1112 |
+
"link": 20
|
| 1113 |
+
}
|
| 1114 |
+
],
|
| 1115 |
+
"outputs": [
|
| 1116 |
+
{
|
| 1117 |
+
"localized_name": "report",
|
| 1118 |
+
"name": "report",
|
| 1119 |
+
"type": "STRING",
|
| 1120 |
+
"slot_index": 0,
|
| 1121 |
+
"links": [
|
| 1122 |
+
21
|
| 1123 |
+
]
|
| 1124 |
+
},
|
| 1125 |
+
{
|
| 1126 |
+
"localized_name": "chart",
|
| 1127 |
+
"name": "chart",
|
| 1128 |
+
"type": "IMAGE",
|
| 1129 |
+
"slot_index": 1,
|
| 1130 |
+
"links": [
|
| 1131 |
+
22
|
| 1132 |
+
]
|
| 1133 |
+
}
|
| 1134 |
+
],
|
| 1135 |
+
"properties": {
|
| 1136 |
+
"Node name for S&R": "HTCSeamReport"
|
| 1137 |
+
},
|
| 1138 |
+
"widgets_values": [
|
| 1139 |
+
2,
|
| 1140 |
+
22,
|
| 1141 |
+
6
|
| 1142 |
+
]
|
| 1143 |
+
},
|
| 1144 |
+
{
|
| 1145 |
+
"id": 37,
|
| 1146 |
+
"type": "PreviewAny",
|
| 1147 |
+
"pos": [
|
| 1148 |
+
3400.0,
|
| 1149 |
+
1100.0
|
| 1150 |
+
],
|
| 1151 |
+
"size": [
|
| 1152 |
+
400,
|
| 1153 |
+
300
|
| 1154 |
+
],
|
| 1155 |
+
"flags": {},
|
| 1156 |
+
"order": 108,
|
| 1157 |
+
"mode": 0,
|
| 1158 |
+
"title": "Seam numbers",
|
| 1159 |
+
"inputs": [
|
| 1160 |
+
{
|
| 1161 |
+
"localized_name": "source",
|
| 1162 |
+
"name": "source",
|
| 1163 |
+
"type": "*",
|
| 1164 |
+
"link": 21
|
| 1165 |
+
}
|
| 1166 |
+
],
|
| 1167 |
+
"outputs": [],
|
| 1168 |
+
"properties": {
|
| 1169 |
+
"Node name for S&R": "PreviewAny",
|
| 1170 |
+
"cnr_id": "comfy-core"
|
| 1171 |
+
},
|
| 1172 |
+
"widgets_values": []
|
| 1173 |
+
},
|
| 1174 |
+
{
|
| 1175 |
+
"id": 38,
|
| 1176 |
+
"type": "PreviewImage",
|
| 1177 |
+
"pos": [
|
| 1178 |
+
3400.0,
|
| 1179 |
+
1450.0
|
| 1180 |
+
],
|
| 1181 |
+
"size": [
|
| 1182 |
+
400,
|
| 1183 |
+
320
|
| 1184 |
+
],
|
| 1185 |
+
"flags": {},
|
| 1186 |
+
"order": 109,
|
| 1187 |
+
"mode": 0,
|
| 1188 |
+
"title": "Seam chart",
|
| 1189 |
+
"inputs": [
|
| 1190 |
+
{
|
| 1191 |
+
"localized_name": "images",
|
| 1192 |
+
"name": "images",
|
| 1193 |
+
"type": "IMAGE",
|
| 1194 |
+
"link": 22
|
| 1195 |
+
}
|
| 1196 |
+
],
|
| 1197 |
+
"outputs": [],
|
| 1198 |
+
"properties": {
|
| 1199 |
+
"Node name for S&R": "PreviewImage",
|
| 1200 |
+
"cnr_id": "comfy-core"
|
| 1201 |
+
},
|
| 1202 |
+
"widgets_values": []
|
| 1203 |
+
},
|
| 1204 |
{
|
| 1205 |
"id": 20,
|
| 1206 |
"type": "MarkdownNote",
|
|
|
|
| 1213 |
940
|
| 1214 |
],
|
| 1215 |
"flags": {},
|
| 1216 |
+
"order": 110,
|
| 1217 |
"mode": 0,
|
| 1218 |
"inputs": [],
|
| 1219 |
"outputs": [],
|
|
|
|
| 1239 |
760
|
| 1240 |
],
|
| 1241 |
"flags": {},
|
| 1242 |
+
"order": 111,
|
| 1243 |
"mode": 0,
|
| 1244 |
"inputs": [],
|
| 1245 |
"outputs": [],
|
|
|
|
| 1265 |
640
|
| 1266 |
],
|
| 1267 |
"flags": {},
|
| 1268 |
+
"order": 112,
|
| 1269 |
"mode": 0,
|
| 1270 |
"inputs": [],
|
| 1271 |
"outputs": [],
|
|
|
|
| 1291 |
640
|
| 1292 |
],
|
| 1293 |
"flags": {},
|
| 1294 |
+
"order": 113,
|
| 1295 |
"mode": 0,
|
| 1296 |
"inputs": [],
|
| 1297 |
"outputs": [],
|
|
|
|
| 1314 |
],
|
| 1315 |
"size": [
|
| 1316 |
440,
|
| 1317 |
+
900
|
| 1318 |
],
|
| 1319 |
"flags": {},
|
| 1320 |
+
"order": 114,
|
| 1321 |
"mode": 0,
|
| 1322 |
"inputs": [],
|
| 1323 |
"outputs": [],
|
|
|
|
| 1326 |
"htc_card": "trouble"
|
| 1327 |
},
|
| 1328 |
"widgets_values": [
|
| 1329 |
+
"## When it goes wrong\n\n| symptom | cause | fix |\n|---|---|---|\n| The clip cuts to the reference photo in its last seconds | The beat finished before the frames did | Set `tail`, give the beat enough to do |\n| A stray gesture or line in the closing second | `tail: ongoing` on the final shot | `settle` or `hold` |\n| She keeps talking after you asked for quiet | You named the ending | Pose plus a sound |\n| Dialogue continues into hops that have none written | The hop before ended on speech | Land the line early; non-verbal action into the seam; a sound bed on every quiet hop |\n| A character walks between two rooms and one morphs into the other | `continuous` across a location change | `match_cut` |\n| Silence renders as speech | Silence written as an absence | Name room tone, a fridge, a distant car |\n| Ambience is a five-second hiss | Broadband wording | Narrowband, or one discrete event |\n| Two characters' faces merge | Both declared as the same `subject` | One subject number per person |\n| The face becomes a different person partway through | A hop scheduled with no face plate. `locked` holds a face that is still right; only a plate rebuilds one that is gone, and the drift never self-corrects | Put the face ref on **every** hop |\n| A stylised plan renders photoreal | The node's hop-1 establishing line asserts live action | Name the medium in shot 1's first sentence, or clear the `establish` widget |\n| The film gets darker every hop | Each hop dims across its own frames and hands the darker tail on. Seam correction cannot see it | `tone_compensate=anchor`. `tone_anchor=0.35` removed ~60% of measured drift; 0.6 removes ~78% and costs ~0.5/255 more seam step. Also restate the light as a positive property in every beat |\n| A location introduced mid-plan drifts | It has no place plate of its own | Plate it, on the hop it arrives and every hop after |\n| A prop or garment changes colour or material | Named with no adjective, so each hop's encode is free to invent one | Repeat the adjective in every beat, and state it as a property in `context` |\n| A hard cut ~1.5 s into a hop, mid-scene | The previous hop over-delivered, so this beat instructs what its own live frame already did | One movement per hop; write the next beat so it is true from either ending |\n| A continuous join reads as a cut | Framing change with `camera: hold` | Earn it on the move, or `framing: keep` |\n| The run stops, naming a reference | That row's picture is not in `h3_refs` | Drop the file on the row, or clear its picture to run without it |\n| A reference has no effect on some hop | Its `shots` list leaves that hop out | List every hop the picture should ride |\n| A deliberately dark scene keeps being brightened | `anchor` cannot tell intent from drift | `\"tone\": \"rebase\"` on that scene's first shot |\n| You cannot tell which hop broke | 114 s is a lot to scrub | `contact_sheet=on` -- one row per hop, first and last frame |\n| A pasted plan is rejected as invalid JSON | Escaped double quotes mangled in transit | Single quotes around dialogue |\n\nEvery error message names the shot or the reference it came from. Nothing\nguesses."
|
| 1330 |
],
|
| 1331 |
"color": "#432",
|
| 1332 |
"bgcolor": "#653"
|
|
|
|
| 1336 |
"type": "MarkdownNote",
|
| 1337 |
"pos": [
|
| 1338 |
-480,
|
| 1339 |
+
1220
|
| 1340 |
],
|
| 1341 |
"size": [
|
| 1342 |
440,
|
| 1343 |
+
800
|
| 1344 |
],
|
| 1345 |
"flags": {},
|
| 1346 |
+
"order": 115,
|
| 1347 |
"mode": 0,
|
| 1348 |
"inputs": [],
|
| 1349 |
"outputs": [],
|
|
|
|
| 1352 |
"htc_card": "check"
|
| 1353 |
},
|
| 1354 |
"widgets_values": [
|
| 1355 |
+
"## Check before you render\n\nA chain is minutes to hours. These three dials cost seconds and catch almost\neverything.\n\n### `dry_run = on`\n\nCompiles every hop's prompt and stops. No model, no sampler. Read the result\non **`info`**, or as a page on **`contact_sheet`**.\n\nWhat you wrote is not what the encoder gets -- directives, continuation\nscaffolding, the identity lock and the `<Picture N>` citations are all\nassembled at render time. This is the only way to see the real thing first.\nEvery plan warning prints on the way through, too.\n\n### `render_through = N`\n\nStop after N hops. With `cache_hops=on`, 3 then 5 then 8 builds the chain up\nand only ever renders the new hops. The plan is **not** truncated -- shot 4\nstill keys exactly as it will in the full run.\n\n### `quality = draft`\n\n0.3 MP, 6 steps. Enough to read blocking, camera and whether a join lands.\nBoth values are in the cache key, so a draft never overwrites its final.\n\nIt is a **fidelity** lever more than a speed one. Measured: ~42 s/hop against\n~45 s/hop at 7 steps. If you already render at 0.3 MP and 6-8 steps it saves\nalmost nothing -- `dry_run` is the fast button. Draft earns its place when your\nfinal is genuinely heavier, 1.0 MP at 14 steps.\n\n---\n\nAfterwards: **`contact_sheet = on`** gives one row per hop -- first and last\ndelivered frame, beat, directives, seed, cache hit, tone correction. Wire it\nto a Save Image.\n\nAnd **H3 Seam Report** -- already wired on this canvas -- takes the chain's\n`images` and measures every join: invisible / marginal / visible, plus the\nchain's cumulative drift.\n\n**Set `hops` to your shot count.** It derives hop length from frames, hops and\noverlap, so a wrong `hops` does not error -- it returns a plausible length and\nputs every seam at a frame where no join exists.\n\nA single reading includes whatever the scene did across the cut, so it is an\nupper bound. To isolate the seam, A/B two renders from the same seed and cache\nchanging only `tone_compensate`; the hop store writes before tone is applied,\nso the second run re-grades from cache in seconds.\n"
|
| 1356 |
],
|
| 1357 |
"color": "#232",
|
| 1358 |
"bgcolor": "#353"
|
|
|
|
| 1362 |
"type": "MarkdownNote",
|
| 1363 |
"pos": [
|
| 1364 |
-480,
|
| 1365 |
+
2060
|
| 1366 |
],
|
| 1367 |
"size": [
|
| 1368 |
440,
|
| 1369 |
620
|
| 1370 |
],
|
| 1371 |
"flags": {},
|
| 1372 |
+
"order": 116,
|
| 1373 |
"mode": 0,
|
| 1374 |
"inputs": [],
|
| 1375 |
"outputs": [],
|
|
|
|
| 1520 |
35,
|
| 1521 |
0,
|
| 1522 |
"IMAGE"
|
| 1523 |
+
],
|
| 1524 |
+
[
|
| 1525 |
+
20,
|
| 1526 |
+
7,
|
| 1527 |
+
0,
|
| 1528 |
+
36,
|
| 1529 |
+
0,
|
| 1530 |
+
"IMAGE"
|
| 1531 |
+
],
|
| 1532 |
+
[
|
| 1533 |
+
21,
|
| 1534 |
+
36,
|
| 1535 |
+
0,
|
| 1536 |
+
37,
|
| 1537 |
+
0,
|
| 1538 |
+
"STRING"
|
| 1539 |
+
],
|
| 1540 |
+
[
|
| 1541 |
+
22,
|
| 1542 |
+
36,
|
| 1543 |
+
1,
|
| 1544 |
+
38,
|
| 1545 |
+
0,
|
| 1546 |
+
"IMAGE"
|
| 1547 |
]
|
| 1548 |
],
|
| 1549 |
"groups": [
|
|
|
|
| 1554 |
-990,
|
| 1555 |
-465,
|
| 1556 |
980,
|
| 1557 |
+
3200
|
| 1558 |
],
|
| 1559 |
"color": "#3f789e",
|
| 1560 |
"font_size": 24
|