Skip to content

MiniMax H3 Video Creator Workflow V8.1.7 — Handbook

0. Read this first (for you, and for the next Claude / Cowork session)

Files that matter (source of truth):

file role goes to
SoulCraft Video Creator (MiniMax H3).json the ComfyUI workflow, V8.1.7 — opens in App Mode (§19) RunPod (/workspace/packages)
video-creator-minimax-h3-script.sh the thin package script (V8.1.7): what this workflow needs — 8 packs plus the vendored ComfyUI-H3-App-Controls, 14 files, the hooks — declared and handed to ComfyUI Base, which does the work RunPod, beside the JSON
suite.py + pytest.ini the 14-tier test suite (a heredoc inside the script until V5.8.0), on the base’s basetest plugin RunPod, beside the JSON
ComfyUI-H3-App-Controls/ + h3_install.py + h3-h264-mp4.json + profiles.yaml the package’s own node pack (the form’s controls and the JS that drives the graph from them, §19.2), the installer helper, the VHS format and the profile set — all in the zip via ZIP_EXTRA RunPod, beside the JSON
video-creator-minimax-h3-handbook.md this document: RunPod guide, the design rules a future edit must respect, the review + research behind every version your machine / next Cowork session
successor-plan.md V7 plan, revision 4 (2026-09-06). What the next rebuild changes and why: Ref2VA-only, Engine Profiles, the library down from 110.9 GB to 59.7, Fun ControlNet, the acceleration stack, the licence gate. Read it before editing anything below — several §0 design rules are superseded by it. Not shipped to the pod your machine / next Claude Code session

The only other file is video-creator-minimax-h3-runpod.zip — the files above, zipped for upload by base/comfyui-base/_build/package.py. comfyui-base/ is step one on a fresh pod (its own zip, uploaded once; its handbook is the record for the toolchain and the package contract); this package is step two. _build/layout.py and _build/render.py are the canvas generators (rule 6b) and are not shipped. The build scripts that produced each earlier version were deleted on purpose; this handbook is the record of what changed and why (§11 for the pod record, §12–§15 for the current build). No backup copies are kept.

Starting a future Cowork session: upload the JSON, the script, suite.py, pytest.ini and this handbook; the base lives in the repo beside them. Say what you want changed. The rules below are what an edit must keep true; bash "video-creator-minimax-h3-script.sh" test is the acceptance check (the V8.0.0 result is recorded in §19.6). The suite defines 216 test functions across 14 tiers; those two numbers are asserted against the code by test_config_handbook_numbers_match_reality, so they cannot quietly drift. The skips are the runpod and gpu tiers, which need the pod, plus one config check that only applies when cg-use-everywhere is absent (it is a base pack now). Without the testbed, comfyui and server skip too.

Design rules the graph relies on (do not break):

  1. The radios are the form’s controls (V8.0.0; V5–V7: rgthree Fast Groups Bypassers). 🎬 flow, 🎚️ quality (🎧 follows it), 🔗 scene, the photo/video/audio counts and the character toggle are widgets on 🎛️ Setup (1487); the assisted toggle and the one prompt field on ✍️ Prompt (1488) — both nodes of the package’s own pack ComfyUI-H3-App-Controls. The pack’s JavaScript maps every value onto frame modes from ONE table (web/controls.json, read from Python by h3_app_controls/controls.py) and then fires the frontend event that re-renders the App panel, so the form and the canvas cannot disagree. Membership is still group bounds (rgthree’s centre rule), so presets still work by geometry: 🎚️3 Master’s bounds enclose the shared column that 🎚️2 Deliver also encloses, the 🔗 rows nest, and moving a node across a group edge changes behaviour. The table is applied every OFF prefix first, the chosen row last, which is what makes a shared stage end up right — the half-selected radio of the V7 rule is unexpressible now. Only two rgthree panels remain: ✅ Toggles (Fidelity Rules, Continuity Editor, Analyzer, Audio Anchor) and 🔧 Advanced (Aspect override); a group title may still carry exactly one panel marker.
  2. Continue rows overlap on purpose: Motion Context (1018-1021, 1114-1115) is only inside 🔗2/🔗3; the shared core (1044-1046, 1059, 1064) is inside all three. 🔗 selects IMG 1’s SOURCE, 🎬 selects whether it is pinned (V5.7): the old 🔗1 still-anchor pinned frame 0 from the 🔗 axis, so 🔗1 + 🎬 I2VA/FL2VA anchored two keyframes at the same index. That anchor is retired (its id is listed in rule 4); 995/1357/1358 now read the chain-aware switch 1048 the references already used, so the collision cannot be expressed.
  3. Bypass dead-ends: every optional chain feeding a switch starts at a primitive inside the toggled group (1099 for the editor; the brief is the ✍️ Prompt node 1488, whose text feeds both the writer and the manual path and never dead-ends), so a bypassed group resolves to nothing and the Any Switch falls through to the manual/default input. Widget-inputs fall back to their widget value when their link is dead.
  4. Node id ranges: ≤ 1077 original, 1078-1107 V5.0.1, 1108-1136 V5.1.0, 1137-1234 V5.2.0, 1235-1322 V5.3.0, 1323-1325 V5.4.0, 1326-1356 V5.5.0, 1357-1409 V5.6.0, 1410-1411 V5.6.2, 1412-1423 V5.7.0, 1424-1429 V5.8.0, 1430 V5.9.1, 1431-1458 V6.0.0 (21 fold instances 1431-1451, the 🧩 user LoRA rows 1452-1457, the 🧩 panel 1458), 1459-1464 V6.0.1 (the 📖 notes above the six text fields; 1116, 960, 961, 1027 and 1028 retired), 1465-1486 V7, 1487-1498 V8.0.0 (1487 🎛️ Setup, 1488 ✍️ Prompt, 1489 🧬 Character LoRA, 1490-1492 🧩 LoRA rows, 1493-1495 the photos = 0 gates, 1497-1498 the captioner name and its adapter; retired in V8: 16, 1035, 1459, 1460, 1437 with 1421-1429, 1452-1457, the panels 990/1010/1131/1338/1184/1458/747/1384 and the 18 cut switches 1366-1383; retired in V8.1.0: 1466-1472, 1474-1481 and 1496, §21); links ≥ 9500 are new. The test suite names nodes by id — keep ids stable, add new ones from 1499, and let _build/restructure.py allocate them (it is deterministic; re-run it after a rebase rather than merging JSON).
  5. Never --use-sage-attention for H3 (ComfyUI issue #15263); the script removes it from comfyui_args.txt. Sol-Attn (kijai pack) is the accelerator, Iterate only.
  6. Keep SoulCraft Video Creator (MiniMax H3).json pristine next to the script; a frontend re-save reorders widgets and the layout tests auto-skip on such a copy (H3_LAYOUT=0). 6b. Geometry belongs to _build/layout.py (V5.7.0). Node positions, node sizes, group bounds, order and the saved view are written by that tool and by nothing else. Dragging a box in the frontend is a lost edit: re-run python3 _build/layout.py and it is overwritten. The tool never types a group bound — it computes each one from the members and then re-derives membership and refuses to write if any group gained or lost a node it was not told about, which is the only reason rule 1 is safe to automate. --verify proves a run changed nothing outside geometry. 6d. Look at it before shipping it (V5.7.0; V6.0.0: look at the REAL thing). uv run "base/comfyui-base/_build/canvas/snapshot.py" --pkg . photographs the canvas in the testbed’s own frontend at the display it is designed for (_build/snapshots/opening-*.png), exports every node’s computeSize() to _build/sizes.json (which layout.py treats as a floor: on V5.9.1, 138 nodes were stored smaller than the frontend drew them and 42 of them collided on the pod’s screen), and the snapshot tier asserts what the first screen must show. python3 "base/comfyui-base/_build/canvas/render.py" --pkg . --review is the offline stand-in: it draws the canvas to scale from the JSON with the frontend’s widgets, slots and bypass tints, numbers the frames a person touches in the order they touch them, prints every budget the uiux tier asserts, and exits non-zero if any of them fails. Run it after any change to the graph and actually look at the picture. Two defects in V5.7.0 were invisible to the whole suite and obvious in one glance: the banner labels painted their 40px text straight across the canvas because a Label ignores its node box, and the reading path doubled back on itself. Both are now tests, because a defect found by eye should only need finding once. python3 "base/comfyui-base/_build/canvas/page.py" --pkg . builds the before/after page from two snapshot folders (_build/snapshots/canvas.html, served by the canvas-review entry in .claude/launch.json); that page, not the SVG, is what a release is judged on. 6c. The saved view must clear ComfyUI’s level-of-detail cliff (V5.7.0). The frontend sets low_quality when ds.scale < min_font_size_for_lod / (NODE_TEXT_SIZE * sqrt(devicePixelRatio)) — 8 and 14 as shipped, so 0.571 at dpr 1 and 0.404 at dpr 2 — and below it draws no widget text at all and hides DOM widgets outright. V5.6.2 shipped 0.20 and therefore opened with every box blank. Ship above 0.571 so it renders on any display.
  7. The assisted toggle MUTES the 🧠1 frame (V8.0.0; V6–V7: a Fast Groups Muter panel with a 🧠0 row). Muted nodes are dropped from the prompt with no passthrough, so the 🔀 Prompt→REF switch 1033 sees None from the engine and falls through to the ✍️ Prompt text — which is why 1033 and its readout 1032 live outside the frame (in V7 rgthree muted the switch itself, a defect the suite’s model never saw). 🧠1 holds the engine instance, the task and rider switches and the captions readout; the caption rules and the house rules sit in their own frames and stay live whichever way the toggle is set. There is no 🧠0 frame: the manual prompt is the same field with the toggle off.
  8. Bypass gates need a primitive root: a bypassed StringConcatenate passes string_a through, so a mode-side gate is StringConcatenate(string_a ← stub primitive inside the mode group, string_b ← the live text); an image gate is ImageBatch(image1 ← EmptyImage inside the group, image2 ← the batch) → ImageFromBatch(1, 64).
  9. IMG groups carry the picture chain: LoadImage → ResizeAndPadImage 768² → ImageBatch(previous, this). A bypassed slot passes the previous batch through, so the 32B sees pictures numbered exactly as the engine compacts them. The frames follow the form’s counts (V8): Photo 1 ships live, Photo 2-9 / Video 1-3 / Audio 1-3 ship OFF and the pack’s JS lights 🖼️ IMG k (loader + Vision Prep), 🎞️ VID k, 🎵 AUDIO k up to the count. Every media loader ships a PUT-YOUR-… name that fails validation naming the field, never an empty value (an empty LoadImage that runs resolves to the input directory and dies mid-render). Photos = 0 is a state: 1494 (an Any Switch, ladder ▸ blank) feeds the batch consumers a blank and 1495 hands the 32B None.
  10. Settings sentences live with their settings: every 📋 sentence is a primitive (or SomethingToString fed by the live value) inside the group that owns the setting. The concat chain and its header are in 📋 (always on). Add a setting = add a sentence in its group and one concat in the chain.
  11. The model library is a contract: every loader widget reads <category>/<Family>/<Purpose>/<upstream filename>, the setup list fetches to exactly that path, and test_model_library_layout fails if either side drifts. There are no optional or alternate models (V5.5.0): every listed file is required and every required file is loaded by a live node. No control swaps a model for another (V8.1.0, §21): SoulCraft is SFW only.
  12. Node packs are read, not remembered: a widget list is only right if the comfyui tier says so against the packs’ current source; the fix for a drift is to re-order widgets_values (positional!) — never to guess from a README.
  13. The frontend adds a seed control widget (V5.5.0): ComfyUI’s useIntWidget.ts adds control_after_generate to any INT input named seed/noise_seed unless the pack sets the flag to False. A node whose pack declares nothing (the 32B TextGen nodes) therefore serialises value + control string; the comfyui tier applies the same rule. rgthree’s Seed node is the one exception (its JS strips the widget and adds buttons).
  14. Sampling follows the higher of 🎚️ VIDEO and 🎧 AUDIO (V5.5.0): H3 makes picture and sound in one pass, so every sampling switch (steps 1000, shift_audio 1335, sampler 1336, shift_video 1337, unet checkpoint 1086 — all inside the Tier Routing Bus subgraph; V5.7 re-tasked 1335 and 1086 when the turbo LoRA left the bus, and added the Master rows 1412/1413) takes its inputs in the order [🎚️3, 🎧3, 🎚️2, 🎧2, ⚡ default] and rgthree’s Any Switch picks the first live one. VIDEO Iterate + AUDIO Master = 0.4 MP canvas sampled at the official 20 steps.
  15. No fallbacks (user principle, V5.5.0): one RIFE implementation, one download tool, no “if it fails use X” node paths, no optional models. Installer safety rollbacks (venv, server restart) are not fallbacks and stay.

RunPod / ComfyUI quick start

Pod. An NVIDIA B200 (180 GB) first, else a B300 (B300 SXM6 AC, 288 GB, not the 34 GB MIG slice), CUDA 13.0 in RunPod’s filters (§23). On those two, ⚡ Draft runs and the form refuses 💎 Deliver and 🏆 Master at Run, because they end in RTX Video Super Resolution, which needs an RTX GPU: for those tiers use the RTX PRO 6000 Blackwell 96 GB, which runs all three. A network volume of 250 GB+ — this package alone is ≈ 63 GB models + 6.2 GB GGUF = 69 GB. The regular CUDA 12.8 template also works: the base builds its own venv with a CUDA 13 torch either way (host driver ≥ 580).

VRAM. 46 GB minimum (estimated, ComfyUI swapping models between stages), 73 GB comfortable (everything resident).

Step one — ComfyUI Base, once per pod. Upload comfyui-base-runpod.zip, extract it, run it. It installs itself to /workspace/comfy-base and brings up everything every workflow shares: uv, the newest Python and torch cu130, ComfyUI at its newest release tag, a venv whose interpreter lives on the volume (so a pod stop/start cannot brick it), rgthree / KJNodes / VideoHelperSuite / cg-use-everywhere pinned, the model library layout, one deletion prompt, one ledger. comfyui-base-handbook.md is the record for that part.

Terminal window
cd /workspace/packages
python3 -m zipfile -e "comfyui-base-runpod.zip" .
bash "comfyui-base/comfyui-base-script.sh"

Step two — this package, one command. Upload video-creator-minimax-h3-runpod.zip beside it:

Terminal window
cd /workspace/packages
python3 -m zipfile -e "video-creator-minimax-h3-runpod.zip" .
bash "video-creator-minimax-h3-script.sh" # 8 packs → nvidia-vfx → 16 files → workflow paths → hooks → import check → hygiene → suite → summary

Safe to re-run; a second run is a no-op. It asks two things only: a token on the first run (Hugging Face, optional: every file is on the Hub or GitHub, ungated; saved once to /workspace/comfy-base/state/tokens.env, mode 600, and a V5.5–V5.8 pod’s user/h3/tokens.env is migrated) and a y/N before deleting duplicate or superseded files it found on the volume (default N; BASE_YES=1 answers yes for unattended runs). Exit 0 = everything present and verified; 1 = something required is missing, and the summary names it.

What it does, in order: the base reads the pod (ComfyUI dir, venv, torch/CUDA, driver, free space), checks out ComfyUI’s newest release tag on comfy-base-stable, reuses its venv when it is current (else rebuilds it with backup and rollback), clones or pins the packs — the base’s four plus the eight declared here — installs their requirements and nvidia-vfx under the torch constraint, indexes every model file on the volume and MOVES known ones into the library (exact name + exact byte size; never symlinks), downloads what is missing (hf-xet for the Hub), offers duplicates and superseded files for deletion, syncs the workflow’s widget paths, runs this package’s hooks (the 320 kbps VHS format registered through extra_model_paths.yaml, the output/MiniMaxH3/chain folder, the rewriter’s models.json), import-checks every pack plus comfy_kitchen / triton / comfy_aimdo / nvidia-vfx in the venv, applies hygiene (--preview-method auto, --disable-api-nodes, Manager security_level normal, --use-sage-attention stripped — H3 renders noise with it), writes the ledger, runs the suite, prints a summary ending in the first-render checklist.

It never restarts ComfyUI on its own — a shared pod may be mid-render. The summary prints the exact launch line to run; BASE_RESTART=1 makes it stop and start the server itself. After a restart, bash "video-creator-minimax-h3-script.sh" test reads the live /object_info.

Stopping and restarting a pod. The volume keeps ComfyUI, the packs, the models, the base, the workflow and the script. The container disk keeps nothing — it is restored from the image at every start. The base keeps the Python interpreter on the volume (/workspace/comfy-base/python; defect 39 was an interpreter on the container disk, and it bricked the pod), so a stop/start brings ComfyUI straight back on 8188 with no script re-run; re-running is how you update, not how you boot. Keep the .pre-comfy-base-<ts> venv backup (a V5.7–V5.8 pod’s .pre-h3-<ts> counts too) until you have stopped, started and rendered — a render alone proves nothing about a restart. If the pod ever stops seconds after starting and JupyterLab never appears, run bash "video-creator-minimax-h3-script.sh" rescue from a shell on the volume (a pod on the same network volume with a plain Jupyter template, or the Container Start Command overridden to sleep infinity): your models are safe and deleting the network volume is never the answer.

The slim template has no unzip — python3 -m zipfile -e does it. The workflow JSON must sit beside the script: the run refuses to start without it, because the sync step rewrites its widget paths and the suite reads it.

bash … --check is the dry run (same report, changes nothing). bash … test [tier] runs the suite alone — tiers: unit functional flows automation uiux config install comfyui server e2e runpod gpu. bash … --latest is the same as a plain run: since base 3.0.0 every run takes the eight packs to their remotes’ HEAD and prints their last-tested rows. bash … help prints the base-generated usage.

Then in ComfyUI: load SoulCraft Video Creator (MiniMax H3).json (it is also in the workflow browser after the first run).

Finding your way around the canvas (V6.0.1, the cockpit — §17). The file opens on one screen that holds everything you touch every run, read like a page: the nine 🖼️ IMG slots across the top; below them the 0 · Start here card, ⚙️ GEN SETTINGS (length, scene, seed), 🧠1 with the 🎯 brief — the one big text box on screen 1, with a 📖 note above it saying what it does and showing an example — and the 🎛️ CONTROL PANEL with the five radios. Every text field you type in has such a note above it (V6.0.1, §17.5). It is laid out for a MacBook display at the saved 55% and fits 1512 × 982 points with ComfyUI’s own toolbars measured out (on a non-Retina monitor, zoom to 60% once: below 57% that frontend stops drawing text). Scroll down for the set-once band: 🧠0 the manual prompt (used when the 🧠 radio says 🧠0), ⚙️ Processing (key 3: the base save and 📝 LIVE SETTINGS), 📐 Aspect, the 🎞️ VID and 🎵 AUDIO slots, 🎛️ ADVANCED (✅ Toggles, Reference Toggles, the 🎞️ REFERENCES cut muter, 🧩 LoRAs, 🔧 Advanced), 🗃 Reference cuts, ✅ Audio Anchor and 🔧 Aspect override; then a row of the guideline texts you set once — ✅ Fidelity Rules (house rules), 👁 Caption rules, ✅ Continuity Editor (editor rules), ✅ Passion (the rider) — each with its 📖 note; then the six 🧩 LoRA rows, ✅ Analyzer and the 📋 SETTINGS block. Everything right of the gutter runs itself; its rows are switched from the control panel. Keys 1 / 2 / 3 jump to the start, the controls and the output; seven 📖 notes sit beside the sections they explain. The machinery is folded into 39 subgraphs (double-click a stub to look inside).

First render (the real smoke test).

  1. Control panel: 🎬 Ref2VA · 🎚️ ⚡ Iterate · 🎧 🎧1 · 🔗 New scene · 🧠 🧠1 · 🎬 SCENE # = 1 (all already set; ✅ Continuity Editor and ✅ Analyzer ship ON, ✅ Audio Anchor OFF).
  2. Upload stills into IMG 1-9 (IMG 1 = face; the canvas aspect follows it). Every slot ships ON and empty: turn off any slot you leave empty, or the queue stops with that node named.
  3. Write the 🎯 brief, one line each: Action / Camera / Cuts / Dialogue / Sound / Refs / Fidelity: nothing else happens. Queue.
  4. Read, in this order: 📝 reference captions (Picture 1 must describe the person in IMG 1), 📝 LIVE SETTINGS, the 🔎 Prompt Check findings, the console ([Sol-Attn] active, the tail loader’s load/unload lines, no RTX VSR unavailable) — and after the render the 📝 ANALYZER REPORT with its AUDIO score, TOP FIXES mapped to the 🎚️/🎧/🎙 knobs, the Note: rider and Retry: seed advice.
  5. Happy → 💎 Quality + 🎧2, Seed → fixed, queue again (STAGES: base, H3 2×, RTX; a FLAC master lands beside the MP4). Publishing → 🏆 Master + 🎧3: the official 20-step recipe without turbo, RIFE 4.26 → 60 fps, RTX 4K, −14 LUFS (STAGES: base, H3 2×, RIFE, RTX).
  6. Speech from a recording (ElevenLabs etc.): put it in AUDIO 1, turn ✅ Audio Anchor on, keep the video length ≥ the clip — the soundtrack is your file and H3 animates to it. Listen for doubled speech (AUDIO 1 also stays a timbre reference); if you hear it, turn the AUDIO 1 slot off while ✅ Audio Anchor is on.

Scene 2: SCENE # = 2, 🔗 Last frame → start, new brief, queue. Retry = same number. Details in the 🔗 Scene chaining note on the canvas.

If something is off.

  • The pod stops seconds after you start it and JupyterLab never loads: the venv at the template path cannot boot (defect 39). Get a shell on the volume — deploy a pod on the same network volume using a plain base/Jupyter template, or override the Container Start Command to sleep infinity — then run bash "video-creator-minimax-h3-script.sh" rescue. It reports which venv is dead and why, and puts a working one back. Your ~119 GB of models are on the volume and are never touched. Do not delete the network volume.
  • The script stops early: read the ERR line it prints (function + line) and the log path; --check reproduces the plan without changing anything.
  • test fails after you edit the graph in the UI: expected — it pins wiring and layout; re-run against the pristine file to confirm, or update the suite.
  • A node shows red “missing”: the import check before the restart names the pack and its traceback in the log; fix, re-run the script.
  • Queue stops on an IMG/VID/AUDIO node: that slot is ON and empty — upload or turn it off (rule 9).
  • Never launch ComfyUI with --use-sage-attention for H3 (core bug → pure noise); the script removes it.
  • 🧠1 errors with “needs the 50-layer MiniMax H3 CLIP” or “tail … not found”: node 2 must load the stock encoder and the tail must sit in models/text_encoders/MiniMax-H3/ — the script fetches both; refresh the model list.
  • Flipping the engine row re-arms ✅ Fidelity Rules (house rules) because it is nested in the band — click it off again if you had it off.
  • Audio sounds muffled on Iterate/Quality: that is the turbo LoRA (§11.6). Use 🎧3 (or 🎚️3) for 20-step sampling; the analyzer’s AUDIO score says so too.

bash "video-creator-minimax-h3-script.sh" help prints the same checklist on the pod.


1–10. Changelog, V5.0.1 → V5.4.1 (history)

The long-form record of these releases was compressed on 2026-09-03. Each row is what changed and why; the invariants they established are stated as live rules in §0, and anything still operative is restated in §11 (the pod record) or §12 (the current build). The build scripts that produced each version were deleted on purpose — this handbook is the record.

Version Date What changed, and why
V5.0.1 2026-09-01 The fidelity upgrade. Introduced the 🔒 HOUSE RULES primitive (node 1089) and the fidelity contract: the render may not invent anything the brief did not ask for. Everything since is built on that node.
V5.1.0 2026-09-01 Plug-and-play. The rgthree radio panels (🎬 / 🎚️ / 🎧 / 🔗) with toggleRestriction: always one, membership decided by group bounds. First embedded test suite (44 tests), including the full radio grid.
V5.2.0 2026-09-02 The writer sees the graph. The 32B vision engine (H3’s own text encoder + generation tail) reads IMG 1-9 and writes the prompt; the 📋 LIVE SETTINGS block was added, one sentence per live setting, each inside the group that owns it.
V5.3.0 2026-09-03 The analyzer. A post-render 32B pass judges references, every stage’s frames, the prompt, the settings and the audio, and writes a report + paper trail beside the render. Ships ON.
V5.4.0 2026-09-03 Latest models, and a library you can read. Established the model-library contract — <category>/<Family>/<Purpose>/<upstream filename> on both the widget and the download side — enforced by test_model_library_layout.
V5.4.1 2026-09-03 The tiered test suite. Split the suite into tiers so a failure names its own layer, and added the pod-only tiers. That structure is what §12.6 later grew to thirteen tiers.

| V5.7.1 | 2026-09-04 | The pod comes back. A stop/start had been bricking the pod outright — the Python 3.13 interpreter lived on the container disk, so the venv on the volume pointed at a path the next boot deleted, start.sh activated it, PID 1 exited and JupyterLab went with it. Interpreter moved onto the volume, the venv verify now fails on any off-volume startup path so the rollback keeps the pod bootable, and rescue is a way back in when it does not. Defects 39-41; §11.1b, “Fifth run”. |

11. V5.5.0 — one command, one library, and what the failed pod run taught us (2026-09-03)

Sections 1-10 are the record of V5.0.1 → V5.4.1 as they shipped. Where V5.5.0 changed a fact (the LTX stage, the 🧠2 engine, the Ref-Delta / Ref-noise toggles, the script’s options, the library tree, the test counts), this section is authoritative.

11.1 Why the first pod run failed (toolchain)

The user ran the script’s all stage on the official RunPod ComfyUI template and every stage failed: the script errored, nodes were missing on load, models were missing, the queue errored, and no token was ever asked for. No log survived, so the causes were reconstructed from the template’s source (runpod-workers/comfyui-base: start.sh, Dockerfile, docker-bake.hcl) and a line-by-line read of the V5.4.1 script. All thirteen are fixed in the V5.5.0 toolchain.

# V5.4.1 defect Effect on the pod V5.5.0
1 no venv detection: python3 / bare pip while the template runs ComfyUI from ComfyUI/.venv-cu1xx pack deps landed in the wrong interpreter → nodes missing on load; torch “upgraded” where ComfyUI never looks every install is "$PY" -m pip in the venv ComfyUI actually runs; a test fails on any bare pip
2 the image exports PIP_CONSTRAINT=/opt/comfyui-runtime-constraints.txt (pins torch) system-wide every pip install torch==… conflicted → update failed unset PIP_CONSTRAINT PIP_INDEX_URL PIP_EXTRA_INDEX_URL first thing
3 eager PyPI -r requirements.txt ran before the cu130 torch step pulled a cu12 torch, bumped numpy/transformers torch trio first from the cu130 index, then ComfyUI + pack requirements with a constraints file pinning that torch, only-if-needed
4 torch version from PyPI JSON, empty-generator fallback picked the oldest release; pipelines ended in | grep | tail, no set -e could pin a missing wheel; failures printed green newest cu130 release ≥ 14 days old that exists on the index; set -Eeuo pipefail + ERR trap naming function and line
5 backticks inside double quotes bash: all: command not found exactly when the core nodes were missing none (tested)
6 git stash never popped; newest-tag checkout left detached HEAD template edits lost; Manager’s updater refuses detached HEAD git checkout -B h3-stable <tag>; the stash pop command is printed in the summary
7 -s accepted partial downloads; sizes never checked a Ctrl-C’d 34 GB file was “✔” forever → models missing at queue time size verified against the Hub (Content-Length / x-linked-size); wrong size → .partial, re-fetched
8 downloads staged in /tmp (container disk), mv -f across filesystems unchecked, then rm -rf doubled I/O, could fill the container disk staging in models/.h3-staging on the volume; move only after the size check
9 model index: find over huge roots without -xdev, substring match, “largest file wins” could relocate a same-named different model one indexed pass with -xdev; exact basename and expected size, else it is not that file
10 PRUNE=1 / DEDUPE=1 deleted by folder destructive with no name check one y/N prompt listing exactly which duplicates / superseded files, default No; unknown files are never touched
11 test / smoke hardcoded the ComfyUI path; all ignored an update failure a “green” run proved little discovery by main.py; the import check (--quick-test-for-ci) gates the restart; exit code carries model / venv / restart failures
12 UPDATE_COMFY meant two things, WORKFLOW= half-implemented, GGUFs missing from the disk gate, nvidia-vfx import name differed three ways confusion, false disk pass no options; one disk gate over every required byte; the import name is read from pip show -f
13 no token prompt, HF_TOKEN env only; CivitAI never needed but never said so (the template’s Civicomfy is why a CivitAI key was expected) “never asked for keys” the optional Hugging Face prompt on the first run, saved once; CivitAI token: not needed printed

11.1b Second pod run, 2026-09-03 — defects 14-28

The first real pod run (log h3_run_20260903-181144.log) completed the venv and the full 123 GB pull, then failed the import check: comfy_kitchen has no attribute 'int8_attention_is_available'. Root cause was defect 16 — the ComfyUI fetch failed, the checkout stayed on 0.30.0, and pip resolved a comfy_kitchen wheel whose API that release does not use. Everything downstream (missing ModelAttentionBackend, the KJNodes widget-count mismatch, 26 node types absent from /object_info) followed from that plus defect 17.

# Defect Effect on the pod Fix
14 HF_HUB_ENABLE_HF_TRANSFER=1 set and huggingface_hub[hf_transfer] installed hf_transfer is deprecated (the Hub is fully Xet-backed) and the extra does not exist on hub 1.x — pip warned and installed nothing, so downloads ran unaccelerated while appearing tuned HF_XET_HIGH_PERFORMANCE=1, the [hf-xet] extra, HF_HUB_DOWNLOAD_TIMEOUT=60, HF_HUB_DISABLE_UPDATE_CHECK=1
15 uv/pip/Xet caches under $HOME on the container disk a pod restart wiped them; the 7.5 GB torch trio was re-fetched every run caches default onto /workspace/.cache/*; UV_CACHE_DIR then shares a filesystem with the venv, so uv hardlinks instead of copying. The caches moved; the venv’s own interpreter did not, because it had been filed mentally as a cache too — that gap is defect 39, and it cost a pod rather than a download
16 git fetch … -q | tail -2 discarded the reason and the run continued stayed on ComfyUI 0.30.0 → the import-check blocker names the remote, unshallows a --depth 1 template, prints the real error, escalates to H3_FAILED
17 template-baked packs sit on a branch with no upstream pull --ff-only could never work; KJNodes silently never updated on any run _h3_pack_sync ignores the checkout’s own tracking config and re-derives everything from the URL in PACKS: remote (added if missing) → the branch the remote publishes as HEAD → checkout → --set-upstream-to → merge --ff-only. Handles a branch with no upstream, no remote at all, and a detached/shallow HEAD; a genuinely diverged pack is reported, never rewritten
18 every install went through pip the torch step took ~6 min uv pip install --python "$PY" for the torch trio (uv has no --upgrade-strategy, so the only-if-needed calls stay on pip)
19 -q plus | tail -N zero output for minutes; a working run looked hung _h3_run_watched prints a heartbeat every 60 s and shows output only on failure
20 _run inherited COMFY_DIR, and _h3_discover honoured it even under H3_FAKE_ROOT every fake-pod test on the pod operated on the real ComfyUI — including the H3_YES=1 one, which auto-confirms deletions the runner scrubs COMFY_DIR/H3_NODE_SRC/H3_SEARCH_ROOTS/H3_YES; discovery refuses a COMFY_DIR outside the fake root
21 an out-of-date ComfyUI was a warning the run spent ~40 min and 123 GB before failing COMFY_MIN="0.34.0" + h3_comfy_gate immediately after the ComfyUI step: nothing downloaded, venv untouched, exit 1
22 a missing pristine workflow JSON degraded silently sync only fixed the ComfyUI copy; the suite fell back to the frontend-resaved file (2 failures, 13 skipped layout checks) h3_require_workflow fails in the first second with an upload instruction
23 rife426.pth declared 0.05 GB the release asset is 24,620,531 bytes; only the disk estimate and the on-disk test use the declared figure, so the test failed while the download “verified” (it HEADs the real length) declared 0.025
24 hf CLI update/skills hint in every log noise HF_HUB_DISABLE_UPDATE_CHECK=1
25 the summary reprinted the whole pytest tail tier table, every SKIPPED line and the count all twice the summary captures the tier table only
26 taeh3.safetensors asserted to be graph-referenced it is loaded by --preview-method auto, named by no node — the test failed on a correct list vae_approx/ exempted alongside LLM/ and custom_nodes/
27 index searched two directories with -xdev and pruned custom_nodes indexed 0 model files; rife426.pth lives inside a pack and was unfindable for ever every mounted filesystem, custom_nodes no longer pruned, site-packages/dist-packages pruned instead (a venv is full of *.pth path files that are not weights)
28 nothing looked for installs already on the pod strays stayed invisible to ComfyUI; duplicates accumulated h3_consolidate runs before clone/pull: moves stray packs into custom_nodes, reports second ComfyUI trees and foreign venvs, queues duplicate packs into the one existing deletion prompt. Our own .pre-h3-* venv backup is reported, never queued — it is the rollback for the venv now in place, and only the run that builds a replacement removes it (_h3_drop_stale_venv_backups), so at most one exists and an unchanged re-run rewrites nothing
29 h3_consolidate as first written did one whole-filesystem walk per pack thirteen walks, and the same tree reported twice through macOS firmlinks / RunPod bind mounts (/System/Volumes/Data/… and /private/…, or /workspace/… and /runpod-volume/…) one _h3_dirscan walk for every directory question, deduplicated with cd && pwd -P, bounded by H3_SCAN_SECS (240 s) so a hung network mount cannot stall the run

Third run, 2026-09-03 20:01 — --check on a fresh pod (defects 30-33)

The volume did not survive the pod being spun down: new host, template venv back at 3.12/torch 2.10, one baked pack, no models. The dry run cost nothing and found four more defects, three of them introduced by the 14-29 batch.

# Defect Effect Fix
30 sed 's#/*$##' normalising the index roots it collapses the root / to the empty string and awk 'NF' drops it, so the widened index searched nothing — the giveaway is indexed 0 model files under: with a blank root list sed -E 's#(.)/+$#\1#', which strips trailing slashes only after a character
31 _h3_git_diag was grep … | head -1 head closes the pipe, grep dies of SIGPIPE, pipefail makes it fatal and the ERR trap printed unexpected failure … head -1 over the real report. And git fetch opens with a From <url> banner, so “first line” reported the banner — the same class of bug as the original tail -1 prefers an explicit error line (fatal:, error:, !, [rejected], would clobber); falls back to the last line only when a From banner is present (fetch shape) and the first otherwise (pull shape); no head in a pipe; always returns 0
32 git fetch --tags --prune without --force a template that ships tags upstream has since moved is refused wholesale with would clobber existing tag, exit 1, after the banner — the most likely cause of the original defect 16 --tags --force --prune on both the normal and the unshallow path; we author no tags, so clobbering local ones is safe
33 consolidation queued any duplicate pack for deletion it offered to delete /opt/comfyui-baked/custom_nodes/ComfyUI-KJNodes, which is the image’s seed copy, not user data only duplicates under the volume are queued; anything outside is reported and left alone

Fourth run, 2026-09-03 — first full install on a fresh pod (defects 34-38)

It completed — ComfyUI 0.34.3, venv rebuilt, all 18 files downloaded and verified, every pack imported, server up, smoke green — and the suite still reported 4 failed / 6 warnings.

# what was wrong why it mattered fix
34 defect 17 was only half fixed the repair ran after pull --ff-only had already failed and needed origin/<current branch> to exist, so KJNodes still never updated _h3_pack_sync ignores the checkout’s tracking config entirely and re-derives remote → remote-HEAD branch → upstream from the URL in PACKS; verified against six git shapes
35 ModelPreviewOverrideKJ: 6 values, source wants 5 the graph was right and the pack was three months old — it predated the tiny_vae widget fixed by 34; no workflow change
36 the pack-name test derived the name as src.split("/")[0] that is custom_nodes on a live tree; it only worked when H3_NODE_SRC was a flat folder of clones extract_schemas stores repo from its own repo_of() and the test reads that — one source of truth
37 the suite ran before h3_restart its runpod tier read /object_info off the OLD venv’s server and reported 26 phantom missing node types while h3_smoke found all 66 h3_test runs after h3_smoke, so the suite is last in h3_run
38 h3_consolidate queued our own .pre-h3-* venv backup for deletion the next run silently destroyed the rollback the summary told the user to keep, and made an otherwise unchanged re-run rewrite the tree backups are reported only; _h3_drop_stale_venv_backups removes the predecessor when a replacement is built, so exactly one exists

The 6 pytest SyntaxWarnings were \( escapes in Frame-Interpolation’s unselected cupy_ops backend, unreachable in this install; pruned from the schema walk rather than suppressed.

Never write a requirements.txt into a pack checkout to fix a missing dep — it dirties the tree, and h3_packs then refuses to update that pack for ever. That is the exact KJNodes failure mode above.

Confirmed working on this run: the banner’s (via …/.venv-cu128/bin/python) annotation, the version gate (→ would abort here: ComfyUI 0.30.0 is below the 0.34.0 floor), the single-walk consolidation finding /opt/comfyui-baked, and the workflow-JSON requirement. After the fixes the index was re-verified on a fake tree: 2 files found where it previously reported 0, the legacy LoRA relocated, and rife426.pth found inside custom_nodes — the un-prune (defect 27) and the corrected 0.025 GB size (defect 23) both proven.

Suite: 122 passed / 0 failed on the non-pod tiers.

Root cause of defect 16 — confirmed on the pod, 2026-09-03 20:0x:

$ git -C /workspace/runpod-slim/ComfyUI fetch --tags --prune origin
From https://github.com/comfyanonymous/ComfyUI
e9b373d5..da0e7f3f deepme987/comfyui/comfy-cloud-node/BE-9870 -> origin/...
+ 2052ea54...9c5ea0bf feat/partner-nodes/tripo-nodes-fixes -> origin/... (forced update)
! [rejected] v0.30.0 -> v0.30.0 (would clobber existing tag)

Upstream moved the v0.30.0 tag after the RunPod image was built, so the image’s local v0.30.0 no longer matches the remote one. Branches fetched successfully; the single rejected tag made the whole fetch exit 1. The script read that as “no network” and stayed on 0.30.0 — and every later failure of that run followed from it:

local v0.30.0 ≠ remote v0.30.0 → fetch --tags exits 1 → checkout stays at 0.30.0 → pip resolves the newest comfy_kitchen against 0.30.0’s attention.py → AttributeError: module 'comfy_kitchen' has no attribute 'int8_attention_is_available' → import check blocks the restart → the run fails after the 123 GB pull.

Defect 32’s --force is therefore the correct and complete fix: we author no tags, so replacing a stale local tag with the remote one is safe. Defect 21’s gate is the backstop — if a fetch ever fails again for a different reason, the run now stops before spending anything.

Acceptance status. 119 passed / 0 failed on the non-pod tiers off-pod (was 111; +8 toolchain tests locking defects 14-28). Six pod-tier failures off-pod are expected. §11.10 still has to be completed on the pod.

Fifth run, 2026-09-04 — the pod that would not restart (defects 39-41)

Reported symptom: deploy a pod, install, make videos, stop the pod, start it again — and neither JupyterLab nor ComfyUI comes up. The only recovery anyone had found was deleting the network volume and re-pulling 119 GB, which had happened more than once.

Nothing in the suite could have caught it, and that is the interesting part. Every venv test runs inside the boot that created the venv. test_runpod_venv_is_python313_at_template_path executes bin/python and is satisfied — because right after an install the interpreter is still there. It can only fail after a restart, which is exactly when nobody can run it. A test that shares a lifetime with the thing it validates cannot see lifetime bugs.

# what was wrong why it mattered fix
39 uv venv --python 3.13 downloaded its managed CPython into $HOME/.local/share/uv/python — container disk. _h3_env_setup had already moved the uv, pip and Xet caches onto the volume for exactly this reason (defect 15) but never set UV_PYTHON_INSTALL_DIR, because the interpreter had been filed mentally as a cache. uv writes an absolute symlink at $VENV/bin/python and an absolute home = in pyvenv.cfg pointing into it RunPod restores the container disk ($HOME, /root, /usr) from the image at every start and keeps only /workspace. After one stop/start the interpreter was gone, .venv-cu130/bin/python dangled, and start.sh — which creates the venv only when the path is missing — activated the corpse instead of rebuilding it. python main.py failed, and because the template runs it as & wait rather than exec, PID 1 exited and the pod stopped, taking JupyterLab with it. There was no way in. A deleted venv would have been repaired automatically; a present-but-dead one never is — that asymmetry is the whole defect UV_PYTHON_INSTALL_DIR defaults to /workspace/h3/python (not under a cache dir: uv cache clean is a documented, safe operation and would otherwise brick the pod), plus UV_MANAGED_PYTHON=1 so there is one source of 3.13. The reuse test now rebuilds a venv whose interpreter is off-volume instead of reusing it — without that, the fixed script would have printed “reusing our 3.13 venv” on an already-poisoned pod and changed nothing. _h3_venv_verify fails on any off-volume base interpreter, pyvenv.cfg home, sys.path root or .pth target, so the existing rollback fires and the pod is left bootable
40 _h3_discover’s loop globs "$COMFY"/.venv*, which also matches our own .venv-cu130.pre-h3-<ts> backup, and picks the first entry with an executable bin/python if the primary venv is dangling and unstamped — the window between uv venv and the stamp write, which spans the torch install, the requirements and the verify — discovery adopts the backup as the live venv. The run rebuilds 3.13 at the backup path and reports success while start.sh still activates the dead primary. The pod test had the same hole (glob(".venv*/.h3-venv-3.13")[0]) .pre-h3-* is excluded from live-venv candidates and can only ever be a restore source; quarantined venvs move to /workspace/h3/dead-venvs/, outside $COMFY, so nothing that is not a live venv can match the glob; /venv and /opt/venv are dropped as candidates on the pod, being container disk and the same brick by another route
41 _h3_drop_stale_venv_backups deleted the previous .pre-h3-* at the top of every rebuild, and the summary told the user to rm -rf the backup “once the pod renders” replacing a poisoned venv therefore deleted the pristine template venv — the only proven-bootable venv on the volume — and backed up the corpse in its place, so a failed rebuild had nothing to roll back to. And a render proves nothing about a restart: the failure only appears on the next boot, by which time the advice has been followed and the escape hatch is gone when the venv being replaced cannot boot, the existing backup is kept as the rollback target and the corpse is quarantined instead. The summary now says “once the pod has been stopped, started and rendered”

rescue, the sixth subcommand. The other five are conveniences; this one is the only way back from a pod that will not boot, so it never executes $PY — the interpreter of the venv it is repairing, which on a bricked pod is exactly the dangling one. Diagnosis is test, readlink and cat; repair is mv. The only interpreter it runs is a candidate backup’s, behind an [ -x ] guard, to prove that one works before putting it back. It reports every venv beside ComfyUI with a verdict (on the volume / container disk), says which one the run would use and warns if that is a backup, writes $COMFY/user/h3/boot-facts.txt with the PID 1 command line, and offers to restore the pristine backup or quarantine the dead venv so start.sh rebuilds a clean 3.12 one. It never touches models/.

Getting a shell on a pod that will not boot. The models are fine; only the venv is dead. Deploy a pod on the same network volume using a plain base/Jupyter template whose start command has nothing to do with ComfyUI — that depends only on “attach an existing network volume”, a first-class RunPod feature. Alternatively override the Container Start Command at deploy time to sleep infinity, but note that then start.sh never runs, so JupyterLab and sshd never start and whether RunPod’s web terminal still works is unconfirmed — record the original value of that field before changing it. Then run rescue, clear the override, and start the pod normally. Do not delete the volume.

Acceptance status. 240 passed / 10 skipped / 0 failed against the local testbed (was 231 + 9; +10 tests locking defects 39-41). The 10 skips are the runpod and gpu tiers. §11.10 now has a restart step, and it is the only test that would have caught defect 39.

Sixth run, 2026-09-05/06 — the first run on ComfyUI Base V2, on a HearmemanAI pod (defects 42-43)

Pod cl90brwiw1pm0x, image hearmeman/comfyui-minimax-template:v8, RTX PRO 6000 Blackwell (95 GB), a new network volume, driven from the Mac with podctl over SSH (base Handbook §1). Step one (base 2.0.4 → 2.0.11 across the fix loop) materialised ComfyUI v0.34.5 at /workspace/ComfyUI, built the venv on Python 3.14.7 / torch 2.14.0+cu130 / sm_120 with the interpreter on the volume, installed the six base packs and the base’s own boot; the base suite was green on the pod (123 passed) with the runpod tier skipping until the first base boot. Step two, BASE_RESTART=1 … --latest, took three attempts — every red was the base’s, fixed test-first on the Mac and re-uploaded (base Handbook §10, 2.0.9-2.0.11: the CivitAI redirect, the launch line’s HF_HOME, pytest in our venv, the opt-in start). The third attempt: 14 packs, models ok 16 (110.89 GB) · failed 0 (the two Hub engines at ~700 MB/s, the Master engine from CivitAI at 160 MB/s, the template’s own VAEs and rife426.pth moved into the family layout, 22 template files reported unclaimed), workflow fixed=0 active_missing=0 bypassed_missing=0 placeholders=3, import ok, restart started (46 s), smoke ok (ComfyUI 0.34.5 · python 3.14.7 · cudaMallocAsync), combos ok, suite 201 passed · 11 skipped (server, e2e, runpod and gpu tiers all real; snapshot skipped on the pod by design), STEP2 RC=0. Over ssh -N runpod: item 12 panels.py 0 failures / 0 page errors; item 13 the snapshot tier 7/7 and the server tier 6/6 against the pod; snapshot.py rendered the opening screen with no missing nodes and no page errors (the 15 console errors are the testbed’s known family plus the headless browser’s media policy). Item 9’s proofs: readlink -f of the venv’s python and pyvenv.cfg’s home both under /workspace/comfy-base/python, state/boot.env written, rescue healthy. Logs and manifests: _build/podruns/2026-09-05/. Item 3 from the Browser pane through the tunnel: the workflow loads with no unregistered type and no node error; the first queue was refused by the server (defect 42, fixed in V6.0.3 — base 2.0.13’s positional pass now guards it); on V6.0.3 the queue is accepted and fails at execution in 0.7 s on the first empty IMG slot, node 920 (LoadImage: the empty path resolves to the input folder) — the slot named, no GPU work. Item 9, the reboot with nothing re-run (02:53Z): the base’s boot.sh became the boot (state/boot.pid alive, its argv boot.sh), sshd up keys-only, JupyterLab answering token-protected from the tools venv, ComfyUI answering after 68 s from /workspace/ComfyUI/.venv-cu130 with the base’s launch line; the base suite on the pod 126 passed with the runpod tier 4/4 (the two boot tests real), and the V6.0.3 workflow loads through the tunnel with no missing type and no node error. Items 4-8 and 11 (renders) are Paul’s; the seed line of item 3 is the suite’s (the four 32B nodes carry no seed widget since V6; node 1199’s 42 / fixed is the captioner’s).

# Defect Effect on the pod Fix
42 Node 1199 (the Omni captioner inside 🧠 Vision engine) stored its widgets_values in an OLDER pack’s order — the retired options string first, the mask last. The frontend restores widget values positionally into /object_info’s order the server refused every queue: 1435:1199 — model: '{}' not in [...] (item 3), and the run said combos ok: the base’s combo check judged values by membership, which cannot see a shift. First suspected as --latest moving the Prompt Rewriter to 0.20.0; pinning it back (a59888df, the tested commit, where it stays) changed nothing — the stored order was stale at both commits. Base 2.0.12’s first positional model then also named 1195 and 819, wrongly: a multi-type STRING,COMBO input and a dynamic combo’s sub-widget are widgets it had skipped (base 2.0.13) V6.0.3: 1199’s list in the server’s order; base 2.0.13’s positional pass (combofix.py) names any such shift on every run. The rewriter at its 0.20.0 HEAD (5a265ac3) was then tested on the pod (2026-09-06, podctl install --latest): restart, smoke, combos and the 203-test suite green, the browser’s queue accepted by the server and failing only at the empty IMG slot — the pin moved to HEAD
43 suite.py’s own ON_POD fallback tested /dev/nvidia0; this pod’s GPU node is /dev/nvidia3 harmless under the base’s runner (it exports BASE_ON_POD), wrong under bare pytest on such a pod any /dev/nvidia[0-9]* (the base’s rule since 2.0.4)

Seventh run, 2026-09-06 — the one command (base 2.0.14 → 2.0.16, H3 V6.0.4)

uv run "base/comfyui-base/_build/pod/podctl.py" install cl90brwiw1pm0x --base --pkg "video-creator-minimax-h3" --no-latest, unattended: the volume size recorded (400 GB), each zip checked current, uploaded and verified, extracted, --check ok, the base run and then this package’s run (BASE_RESTART=1) detached and polled, the pod’s logs fetched into _build/podruns/<date>/pod-logs/, the summary printed, the pins compared (unchanged). On base 2.0.15: base suite 133 passed, this package models ok 16 · failed 0, restart restarted, smoke ok, combos ok (the positional pass), suite 202 passed · 12 skipped (server, e2e, runpod, gpu real; snapshot on the pod by design), STEP RC=0, exit 0. The base’s sweep behind it (base Handbook §10, 2.0.14–2.0.16): nothing tolerated — every warning either fixed or accounted for with its reason, a red suite fails the run, the suite never inherits the run’s tokens or mode variables.

11.2 RunPod template facts (verified 2026-09-03; RunPod’s official image)

Since base V2.0.0 (2026-09-05) the base owns the boot on every image and none of the start.sh facts below is load-bearing any more — see the base Handbook, design rule 7 and its section 6 (the contract every image gets). They stay as the record of why.

  • Two filesystems, and only one of them survives — but the container disk is restored, not blanked. /workspace is the network volume and persists across a pod stop/start. Everything else — $HOME, /root, /usr, /opt — is the container disk, which is reset to the image at every start. That distinction matters in both directions: /usr/bin/python3.12 comes back every time, so the template’s own 3.12 venv is sound and must not be treated as doomed; what does not come back is anything written at runtime onto that disk — $HOME, /root, /tmp. $HOME/.local/share/uv/python, where uv puts managed interpreters by default, is therefore volatile, and anything on the volume that points into it is dead the moment the pod is stopped (defect 39). The rule the venv has to satisfy: everything it needs in order to start must be under /workspace.
  • ComfyUI lives at /workspace/runpod-slim/ComfyUI; the template starts it with python main.py --listen 0.0.0.0 --port 8188 --enable-cors-header $(cat /workspace/runpod-slim/comfyui_args.txt) in the background (& + wait, not exec), so the script can stop and restart it without ending the pod. That is the benign half of the fact. The other half: because start.sh ends in wait rather than exec, a python that dies immediately makes start.sh return, and PID 1 exiting stops the container — so a broken venv does not merely break ComfyUI, it takes JupyterLab and the terminal with it and leaves no way in.
  • ComfyUI runs from ComfyUI/.venv-cu128 (regular template) or .venv-cu130 (Blackwell Edition), Python 3.12, --system-site-packages. start.sh creates that venv only when the path is missing — which is why the script builds its Python 3.13 venv at the same path: the next pod boot activates it unchanged. Note the asymmetry that defect 39 turned on: a missing venv is rebuilt automatically, a present but dead one never is.
  • To confirm on a pod: where start.sh actually lives, and therefore whether a volume-resident boot guard is even possible. tr '\0' ' ' < /proc/1/cmdline and readlink /proc/1/exe settle it; rescue captures both into $COMFY/user/h3/boot-facts.txt.
  • PIP_CONSTRAINT, PIP_INDEX_URL and PIP_EXTRA_INDEX_URL are exported system-wide by the image (defect 2).
  • On an image bump start.sh rsyncs the baked ComfyUI core over the workspace copy (excluding .venv*, models, input, output, user, custom_nodes, extra_model_paths.yaml); the baked packs (Manager, KJNodes, Civicomfy, RunpodDirect) are re-synced with --delete. Re-running the script restores our checkout and hygiene; that is why hygiene is idempotent and re-applied every run.
  • The four baked packs’ requirements lived in system site-packages, invisible to a self-contained venv — the script installs them into the new venv too.

11.3 The library (V7.0.2) — one list, everything required

Drawn from the script’s MODELS rows (14 rows, 68.85 GB), not by hand. flat = the consumer scans the folder root, so the row has no Family and the widget names the bare file.

models/
├── diffusion_models/ MiniMax-H3/ minimax_h3_ref2va_pruned_int8_convrot
├── text_encoders/ MiniMax-H3/ qwen3vl_32b_minimax_h3_int8_convrot (encoder) · qwen3vl_32b_h3_instruct_generation_tail_50_63_int8_convrot (tail)
├── vae/ MiniMax-H3/ minimax_h3_video_vae_int8_convrot · minimax_h3_audio_vae_fp32
├── vae_approx/ flat taeh3 (ComfyUI's preview loader expects it here)
├── loras/ MiniMax-H3/Turbo/ minimax_h3_ref2v_turbo_8step_v1.0_768p_comfyui_bf16
│ MiniMax-H3/Style/ h3-realism-people-t2v-i2v-r2v (r34l1sm) · camera_motion_h3_lora_v1_3000_pruned · wushu_spatial_physics_clean_3000_pruned
├── latent_upscale_models/ flat minimax_h3_latent_upscaler_3d_bf16 (the pack globs the root — base 2.0.37)
└── LLM/ Qwen2.5-Omni-7B-GGUF/ Qwen2.5-Omni-7B-Q4_K_M · mmproj-Qwen2.5-Omni-7B-Q8_0 (VID / AUDIO captions, the analyzer's soundtrack caption)
custom_nodes/ComfyUI-Frame-Interpolation/ckpts/rife/rife426.pth (RIFE 4.26 — the pack loads from its own folder)

In this list since V8.1.1: minimax_h3_video_vae_fp16, which this graph left for the int8 ConvRot VAE and which no package ships now that MiniMax H3 Video Replacer V5.1.0 has moved too. Not in this list: minimax_h3_ref2v_turbo_4step_v0.1_comfyui_bf16, which this graph never loads but the Replacer’s 🅵 Fast and 🅸 Iterate modes still use (V8.1.0 listed it by mistake).

No LTX-2.5/, no Experimental/, no optional entries: every file is required and every required file is read by a live node (test_setup_required_models_are_used_by_workflow checks both directions). ≈ 112 GB of models + 6 GB GGUF. The superseded list (offered for deletion after the y/N, never touched otherwise): the five LTX-2.5 files, the CQ / IC LoRAs, the Ref-Delta LoRA, flownet.pkl (old RIFE), the HauhauCS 27B / 9B writer GGUFs, Omni-3B, and the pre-V5.4 wushu_spatial_physics_v2_1000 — ≈ 68 GB reclaimable on a volume that ran V5.4.1. Legacy folders (loras_H3, text_encoders/H3, diffusion_H3, LTX2.5) are emptied only of files whose basename and size match a list entry; anything else is reported as “unknown, left alone” — a private LoRA in loras_H3/Quality survives.

11.4 Graph defects fixed

# Defect Fix
B1 the four 32B TextGen nodes (1198, 1200, 1204, 1315) carried 19 widgets_values; the frontend builds 20 "fixed" inserted after seed (index 13). Design rule 13: useIntWidget.ts adds control_after_generate to any INT input named seed/noise_seed unless the pack declares the flag False; the suite’s _expected_widgets applies the same rule and rgthree’s Seed node is the exemption. The V5.4.1 “19 values” fix was wrong on this point.
B2 VHS_LoadImagesPath 1114 stored widget names as values dict form {directory, image_load_cap 0, skip_first_images 0, select_every_nth 1}, input order aligned with 1042/1044
B3 group 🎬 Scene paths was captured by the 🎬 MODE radio (a third row that bypassed the SaveImage prefixes) renamed 🗂 Scene paths (from SCENE #); a test asserts no non-radio group carries a panel marker
B5 all nine IMG slots shipped on placeholder files (example.png, two personal photos) every media widget empty (LoadImage, LoadAudio, VHS_LoadVideo); slots stay ON; an unfilled slot fails validation with the node named
B6 Motion Context nodes pointed Manager at the moved ethanfel/ComfyUI-H3-Motion-Context aux_id = seitanism/ComfyUI-H3-Motion-Context-MultiRef on 1018-1022
B7 label 749 said V5.1.0; 770 titled “Image 1”; widgets_values_named drift on 32/39/1024/1046; node 16 example used the FL2VA key; 732 showed 8 steps all corrected; node 16 rewritten in the Ref2VA six-section format

Not bugs (checked): the regex keys on 1201/1202 match the pack’s sockets; frame math rounds up to the 17k+5 grid by design; empty groups 🔗0 / 🧠0 are the radios’ “off” rows.

11.5 Removed subsystems and why

LTX 2.5 refine stage (subgraph + 717, 782, 1013-1016, 1263-1274). The only post stage that changed content: H3 frames were re-encoded through a different 22B model’s VAE and re-denoised by a model that never saw the reference stills — an identity-drift risk the house rules exist to prevent. H3’s latent 2× upscaler (trained on H3 latents) and RTX VSR (deterministic) cover resolution; RIFE covers frame rate. Removed with it: 5 LTX files + CQ LoRA + gated IC-LoRA (~41 GB), the Easy-Use pack (used only by the subgraph’s easy cleanGpuUsed), a 35-node subgraph and two model loads per Master render. It is not recoverable from this repo — the backups were removed; re-adding it means rebuilding the subgraph and its model list from upstream.

🧠2 27B text engine (1060, 1062, 1087, 1088, 1090-1094, 1187-1189, 1233, 1234). The Prompt-Rewriter README says its Guided Writer nodes “read text, not pixels”, that the format “is carried by ~4 000 tokens of instructions the model has to follow”, and that the trained LoRA rewriter’s prose is “denser and its formatting more reliable” — 🧠2 used neither the trained LoRA nor a vision model and ran the “Aggressive” abliteration the model audit in §11.7 flagged for weak instruction following. 🧠1 runs the exact Qwen3-VL-32B weights H3 conditions on, sees all nine stills, and (in V5.5) used the measured Heretic abliteration (KL 0.042, 4/100 refusals); V7 moved to the stock encoder. No public head-to-head exists; on architecture and the pack’s own statements 🧠1 is the better engine. The 🧠 panel now has two rows. Future A/B if wanted: lightx2v’s image-conditioned 8B VL rewriter LoRA (2026-08-18).

🔧 Ref-Delta (1119, 1134-1136, 1180) and ➕ Ref noise (764, 770, 771, 911-921, 1118, 1181). Kijai labels the ref LoRA “completely experimental”; the noise trick has no documented basis. Every consumer of an ImageAddNoise node (the encoder’s ref_image_N, the frames, the anchors, Prompt Check, the pad/batch chain) now reads its LoadImage directly. 🔧 rows: 7 → 5.

11.6 Audio — research and the new flow

Why the audio “seemed off”, from primary sources:

  • The turbo LoRAs are the main cause. A human-validated comparison found 8-step turbo speech “clearly muffled with noise mixed in” against the official 20-step sampling while automated checks passed; Comfy’s docs concede turbo gives “slightly lower audio and motion quality”. Every V5.4.1 preset ran turbo.
  • Upstream bugs: blown-microphone distortion with ancestral/SDE samplers (ComfyUI #15283, fixed by PR #15243); EasyCache halving audio amplitude (#15326, not used here); an open static/popping report (#15614). Comfy’s own 20-step R2V template uses res_multistep + simple.
  • The H3 2× pass re-samples the joint AV latent (σ 0.9, three manual sigmas), so the soundtrack was generated a second time in Quality/Master and muxed from the 2× decode (822). Rule now: every save muxes the base-pass audio (734 → Trim 1020) and 822 is gone; the 2× subgraph’s sampler is res_multistep (SDE avoided on the audio-bearing latent).
  • Encoding (V8.1.2: libx264 everywhere, §23): VHS’s stock h264-mp4 runs -c:a aac with no bitrate → ffmpeg’s 128 kbps default. The script writes h3-h264-mp4.json (the same libx264 pass, -b:a 320k) to /workspace/h3/video_formats/ and registers it through VHS_video_formats in extra_model_paths.yaml; saves 781 (RTX) and 830 (H3 2×) use it, 757 (Iterate base) keeps the stock format.
  • Pinning real audio: core MiniMaxH3AddGuide accepts audio and stores it as a keyframe audio latent — the output soundtrack is the supplied track and H3 animates to it. That is the ✅ Audio Anchor toggle (node 1354, ships OFF): AUDIO 1 pinned at frame 0 in the conditioning chain after 996; sentence 1355 tells the writer the dialogue must match the file word for word. AUDIO 1 keeps feeding ref_audio_0 as a timbre reference; if a pod run hears doubled speech, turn the AUDIO 1 slot off while ✅ Audio Anchor is on.
  • Post-processing is deterministic DSP only (ComfyUI_MusicTools: Music_LufsNormalizer −14 LUFS per BS.1770 via pyloudnorm, Music_AudioRepair “Auto (All)” de-click / DC-offset via pedalboard). No neural “enhancers” — they re-synthesise the voice.

The paired radios. 🎧 AUDIO (panel 1338, a second Fast Groups Bypasser, matchTitle 🎧, always one) has three rows beside 🎚️ VIDEO: 🎧1 nothing extra · 🎧2 320 kbps AAC + a lossless FLAC master (SaveAudioAdvanced 1343 → MiniMaxH3/<date>/H3Audio_) · 🎧3 = 🎧2 + LUFS 1344 → Repair 1345 on the base audio before every mux. Because H3 makes sound and picture in one pass, sampling follows the higher row (design rule 14): each sampling switch takes [🎚️3, 🎧3, 🎚️2, 🎧2, ⚡ default] — steps 1000 (20 / 20 / 8 / 8 / 4), turbo strength 1335 (0 / 0 / 1 / 1 / 1), sampler 1336 (res_multistep ×2, euler ×3), shift 1337 (12 / 12 / 12 / 12 / 3), turbo file 1325 / 1086 (none for the Master rows). So VIDEO ⚡ + AUDIO 🎧3 = the 0.4 MP canvas sampled at the official 20 steps without turbo — a fast way to hear clean speech. 📋 LIVE SETTINGS carries “Sampling: …”, “Audio: …” and “Audio anchor: …” sentences (1332, 1342, 1350, 1355) so the writer and the analyzer see the effective state.

Master = the official recipe. 🏆 no longer uses a turbo LoRA: 20 steps, res_multistep, simple, shift 12, strength 0 on both turbo loaders (980/981), then H3 2× → RIFE 4.26 (RIFE VFI 1326, ×5, VHS_SelectEveryNthImage 1327 every 2nd → exactly 60 fps with the unchanged 24·310/124 Auto-FPS math) → RTX VSR 4K → 60 fps MP4 at 320 kbps + FLAC. Stage order in the analyzer: base → H3 2× → RIFE → RTX. Destination is social / web, hence −14 LUFS / −1 dBTP (a widget on 1344) and H.264 MP4.

11.7 Model audit (Hugging Face listings and READMEs, 2026-09-03)

Everything the graph loads is the newest file in its repo. The only weight swap is RIFE (flownet.pkl → rife426.pth, RIFE 4.26, the newest Practical-RIFE model; Fannovel16’s pack is the only maintained one shipping it, so ComfyUI-VFI is dropped — no fallback implementation).

Role File Verdict
engines Comfy-Org minimax_h3_{ref2va,fl2va}_int8_convrot (34 GB each) current; Comfy-Org: prefer int8_convrot on cu130
encoder + tail ethanfel Heretic int8 + …generation_tail_50_63_int8_convrot current then; retired (V7 ships the stock encoder and its instruct tail; V8.1.0 removed the rest, §21)
turbo LoRAs lightx2v ref2v 4-step v0.1 / 8-step v1.0 768p, fl2v 4-step v1.1 / 8-step v1.0 768p current
style LoRAs fal realism, Jojocodex camera v1_3000, physics clean_3000 current
upscaler, TAE, Omni-7B as listed current
CivitAI 2821932 “H3 (Comfy)” re-upload of the Comfy-Org bytes under generic names, needs a token not adopted
CivitAI 2830065 pruned int8 + w4a8 int4 for small GPUs lower quality than what runs now — not adopted
seen, not added Kijai’s comfy conversion of Alibaba Acc-8Step, Asirus “conservative” upscaler, MATLOWAI fused turbo (needs MAINodes + SLA), VDN future A/Bs

Under the no-alternates rule the stock Comfy-Org encoder is no longer fetched. To A/B it by hand: download qwen3vl_32b_h3_int8_convrot from Comfy-Org’s repo into text_encoders/MiniMax-H3/ and swap node 2’s widget.

11.8 Privacy boundary and the template-pack review

Every model runs on the pod: the H3 engines, the 32B encoder + tail, Omni-7B (llama.cpp, local), RIFE, RTX VSR, the DSP. Outbound traffic is downloads only (Hugging Face, GitHub, PyPI, the cu130 torch index, pypi.nvidia.com) plus ComfyUI-Manager’s public pack-list fetch (user decision). --disable-api-nodes removes Comfy’s cloud API nodes and the frontend’s own internet traffic. Stills, audio and renders never leave the pod and the user’s Mac; no third-party inference API anywhere. The rewriter pack fetches nothing per run (its prompt guide ships in the repo).

Template packs, source scanned for external hosts: Civicomfy (CivitAI browser; hosts civitai.com, search.civitai.com, image.civitai.com, cdnjs.cloudflare.com for a UI library; network only when its panel is used; API key in plain text in custom_nodes/Civicomfy/root_settings.json) — kept for the user’s occasional CivitAI LoRA; only the search / download requests go out. ComfyUI-RunpodDirect (“Download to Pod” in the missing-models dialog; only huggingface.co) — kept. ComfyUI-Manager — kept online with security_level = normal written idempotently, network_mode = public untouched.

11.9 The toolchain (V5.5.0)

One invocation does everything, in this order: banner (ComfyUI dir by main.py, template venv, torch/CUDA, driver, GPU, free space) → tokens → ComfyUI newest tag on branch h3-stable → Python 3.13 venv (uv; resolvability dry-run over ComfyUI + 15 packs + torch cu130 + nvidia-vfx + pytest before the old venv is moved aside; torch first; constraints; verify torch.version.cuda 13.x, sm_120, comfy_kitchen/triton/comfy_aimdo import; automatic rollback to .pre-h3-<ts> on failure) → packs (clone, or fetch + merge --ff-only onto the branch the remote publishes as HEAD; local edits left alone and reported; requirements always installed, and a pack that ships none has its real imports checked against the venv) → models (one index, moves, size checks, staging on the volume, hf download resumable — one tool) → the y/N for duplicates / superseded → sync (pristine JSON + the copy in user/default/workflows; a timestamped .bak whenever a file actually changed, so a second run is a no-op) → import check (--quick-test-for-ci; a failing pack stops the run before the live server is touched) → hygiene (--preview-method auto, --disable-api-nodes, no --use-sage-attention, Manager security_level, VHS format + yaml merge) → restart (TERM/KILL, nohup "$PY" main.py …, ≤ 180 s for /system_stats; on failure: log tail, venv rollback offer) → smoke (/object_info covers every node type) → suite (after the restart: its runpod tier reads the live /object_info, which before the restart still belonged to the old venv’s server and knew nothing about the packs this run installed) → summary + exit code.

Other invocations: --check (dry run, changes nothing), test [tier], help. Env: HF_TOKEN (optional), H3_YES=1 (unattended yes to the deletion prompt), COMFY_DIR (only if discovery fails). Exit codes: 0 complete · 1 the summary lists FAILED items · 2 usage · 3 no ComfyUI found · 4 an unexpected command failed (the ERR trap names function, line and log). Every run logs to /workspace/h3_run_<timestamp>.log. uv comes from PyPI into the system interpreter (no installer script); rife426.pth is the one non-Hub file and is fetched from the pack’s GitHub release with curl -C - through the same staging and size check. Removed: UPDATE, UPDATE_COMFY, TORCH, TORCH_MIN_AGE_DAYS, INCLUDE_OPTIONAL, DEDUPE, PRUNE, WORKFLOW, FAST_DL, PYTHON, H3_TIER, all, setup, update, sync, smoke.

Rollback matrix: venv verify fails → the old venv is restored automatically, exit 1 with the log path · a pack fails the import check → the old server keeps running, the venv rollback is offered (default yes, automatic without a TTY — a broken venv at the template path would otherwise be activated on the next boot) · ComfyUI silent on 8188 → log tail, the same rollback offer, restart with the old venv · checkout fails → stay on the current version, report, continue · required model missing after every source → continue, exit non-zero at the summary · suite / smoke fail → reported, never blocks. --use-sage-attention is removed without asking: it only breaks H3.

Offline testing: H3_FAKE_ROOT=<dir> makes <dir>/ComfyUI the tree and <dir>/runpod-slim/comfyui_args.txt the args file, H3_SEARCH_ROOTS confines the index, H3_NO_NET=1 trusts the declared sizes and “downloads” by allocating the declared size (H3_FAKE_DL=fail makes every download fail). The toolchain tests build such a pod with sparse files: --check touches nothing; a known legacy file moves, an unknown one stays; a truncated file is re-fetched; the deletion prompt defaults to No; H3_YES=1 deletes only duplicates / superseded; the second run is a no-op; a missing required file fails the run; the token lands in user/h3/tokens.env mode 600. The old integration snippet hardcoded /workspace; never run the V5.4.1 integration tier on a populated pod.

Wheels verified for Python 3.13 / cu130: torch 2.13.0 (2.14.0 becomes eligible 2026-09-16), triton 3.8.0 cp313, comfy-kitchen 0.2.32 cp312-abi3, comfy-aimdo 0.5.1 cp39-abi3, nvidia-vfx 0.1.0.1 cp312-abi3, pedalboard 0.9.24 cp313, pyloudnorm 0.2.0 pure Python.

11.10 Verification

Offline (2026-09-03). The V5.5.0 graph was built by a Python build script from its predecessor and checked test-first: 21 acceptance tests written from the plan failed against V5.4.1 and pass against the build; the embedded suite, patched for the new graph, ran 118 passed / 8 skipped (the two pod tiers) against the pack clones; the toolchain contract tests (18: 12 text-contract + 6 fake-pod runs) failed against the V5.4.1 script and pass against the new one; bash -n and every embedded Python compile. Build result: 365 nodes, 496 links, 53 groups (V5.4.1: 383 / 571 / 52). Design deviations from the plan, all deliberate: no extra “RTX source” Any Switch (the existing 901 → RIFE → 902 → 779 → 36 wiring already makes RTX read RIFE’s output in Master and pass the 2× decode through otherwise); the 320 kbps format is unconditional on the Quality/Master saves (the 🎧 rows decide LUFS and FLAC, not the bitrate); LUFS runs before Repair so the ceiling is applied last; the FLAC writer is core SaveAudioAdvanced.

On the pod (the acceptance test — run it and keep the log).

  1. Any supported image (RunPod’s ComfyUI, HearmemanAI’s templates, a bare GPU image — base Handbook §6), a network volume ≥ 250 GB at /workspace. The one command (base 2.0.14): uv run "base/comfyui-base/_build/pod/podctl.py" install <pod> --base --pkg "video-creator-minimax-h3" does everything below in order and stops at the first red. By hand: the two zips in /workspace/packages (podctl upload), base V2.0.14 or newer as step one, then BASE_RESTART=1 bash "video-creator-minimax-h3-script.sh" --latest; the tokens come from the pod’s own environment or state/tokens.env (no prompt over ssh).
  2. Summary must show: venv at $COMFY/.venv-cu130 on the newest CPython uv resolves (3.14.7 on 2026-09-06), torch 2.13+cu130 or newer (2.14.0), sm_120, ComfyUI v0.34.3 or newer, 14 packs (the base’s six plus the eight declared here), every model at the Hub’s size, every tier green (snapshot skips on the pod by design), smoke: every node type registered — against the server this run started (BASE_RESTART=1).
  3. Load the workflow: no red nodes; the four 32B nodes show seed 42 / fixed; queueing with an empty IMG slot fails with that node named.
  4. Likeness gate: real stills in IMG 1-9, a real brief, ⚡ · 🎬 Ref2VA · 🔗0 New scene · 🧠1 — Picture 1’s caption describes IMG 1, the face is IMG 1’s, the action is the brief’s; console shows three 32B passes, [Sol-Attn] active, no RTX VSR unavailable; the ✅ Analyzer report lands in output/MiniMaxH3/<date>/.
  5. All presets on a fixed seed: 💎 + 🎧2 (8-step turbo; STAGES base, H3 2×, RTX; FLAC beside the MP4), 🏆 + 🎧3 (console: 20 steps, res_multistep, no turbo LoRA; STAGES base, H3 2×, RIFE, RTX; 60 fps, 320 kbps, −14 LUFS), then ⚡ + 🎧3 — listen for clean speech.
  6. Audio anchor: a recorded line in AUDIO 1, ✅ Audio Anchor on, length ≥ the clip: the soundtrack is the file and the mouth follows it; no doubled speech (else turn the AUDIO 1 slot off while 🎙 is on and report it).
  7. Chaining: SCENE # 2, 🔗 Last frame → start, then 🔗 + Motion carry-over (exercises 1114 and the MultiRef pack). Scene 1 + 2 on Master is the publishable clip.
  8. V5.6.0 rows nothing has rendered yet: repeat step 4 once on each of 🎬 T2VA (no pictures pinned), 🎬 I2VA (IMG 1 as first frame) and 🎬 L2VA (IMG 1 as last frame), then mute one slot on the 🎞️ REFERENCES panel and confirm that reference drops out of the caption pass while the file stays loaded.
  9. Stop the pod. Start it again. Run nothing. ComfyUI must answer on 8188, the workflow must load, and JupyterLab must come up — without re-running the script. This is the only step that would have caught defect 39, and it went unwritten for five versions because “it worked” was always measured inside the boot that built it. Before restarting, the two commands that prove the fix directly:
    Terminal window
    readlink -f "$COMFY/.venv-cu130/bin/python" # $COMFY as the banner names it: /workspace/runpod-slim/ComfyUI on RunPod's image, /workspace/ComfyUI elsewhere
    grep '^home' "$COMFY/.venv-cu130/pyvenv.cfg"
    Both must be under /workspace. Then confirm the .pre-h3-<ts> backup still exists (defect 41), and run rescue once on the healthy pod to capture boot-facts.txt — the PID 1 command line is the only evidence of where start.sh actually lives, and it can only be read on a pod that is up.
  10. Anything fails → /workspace/comfy-base/state/logs/h3_<timestamp>.log is the deliverable for the next iteration (Claude’s fix loop: diagnose, test, fix locally, bump, re-upload, re-run).
  11. V6.0.0 bypass cases (Handbook §16): with 🔗 New scene selected, 📝 LIVE SETTINGS rows 14/15 must be empty and the writer’s chain marker (no chain) — not a scene_N/ path; switch IMG 2 off on Reference Toggles and queue: the run must proceed and Picture 2 in the captions must be IMG 3’s; in ⚡ Iterate the 📝 ANALYZER REPORT must describe the reference batch, not raw frames.
  12. Every row on every panel lights: click each row on 🎬 MODE, 🎚️ QUALITY, 🎧 AUDIO, 🔗 CONTINUE, 🧠 PROMPT ENGINE, ✅ Toggles, Reference Toggles, 🔧 Advanced and 🧩 LoRAs (six rows) — the frame it names lights and the others of a radio go dark; the 🎞️ REFERENCES muter lists 18 cuts and muting one greys only that switch inside 🗃 Reference cuts. Expect the shared-band rows to light together (🎚️2 with 🎚️3, 🔗2 with 🔗3, 🎧2 with 🎧3) and a click on the chosen row to switch it off — rule 1 says why; uv run "base/comfyui-base/_build/canvas/panels.py" --pkg . is the same check on the testbed and prints those as HAZARD lines. Then switch 🧩 LoRA · User 1 on with the placeholder still selected and queue: it must fail with the loader named. (The suite emulates rgthree; this is the only check that runs the real thing, and the 🧠1 band switches 1033/1360/1364/1365 are the case most likely to differ.)
  13. The opening screen: on the MacBook at the saved view, steps ①-⑥ of the start card, all nine IMG slots, GEN SETTINGS, the brief and the control panel are visible without scrolling, and no node is drawn as a blank box (the level-of-detail cliff).

Risks to watch on that run: an image bump re-syncs the baked core (re-run the script); a 3.13 venv on the network volume is slow on cold import torch (the script warms it up and prints the timing) — and since V5.7.1 the interpreter itself is on the volume too, so that timing now covers the whole startup path and is worth reading; Sol-Attn’s Triton kernels, nvidia-vfx and RIFE 4.26 under Python 3.13 / torch 2.13 have not been exercised by anyone we know of — the first ⚡, 💎 and 🏆 renders are the test.

11.11 Sources added for V5.5.0

  • runpod-workers/comfyui-base — start.sh, Dockerfile, docker-bake.hcl (venv path, PIP_CONSTRAINT, args file, rsync excludes)
  • ComfyUI src/composables/widgets/useIntWidget.ts (the seed control-widget rule); comfy/cli_args.py (--quick-test-for-ci, --disable-api-nodes, --preview-method)
  • Comfy-Org/MiniMax-H3 README (int8_convrot on cu130); lightx2v turbo repos; Kijai/MiniMax-H3-experimental (“completely experimental”)
  • pytraveler/MiniMax-H3-Prompt-Rewriter-ComfyUI README (Guided Writer “read text, not pixels”); ethanfel/ComfyUI-H3-Qwen3VL-TextGen
  • zenn.dev validation write-up on turbo vs 20-step speech; ComfyUI issues #15283, #15326, #15614 and PR #15243; Comfy’s R2V template (res_multistep / simple, 20 steps)
  • Fannovel16/ComfyUI-Frame-Interpolation (rife426.pth release asset); jeankassio/ComfyUI_MusicTools (pyloudnorm, pedalboard); VHS VHS_video_formats folder path
  • PyPI / download.pytorch.org/whl/cu130 wheel listings (2026-09-03); Civicomfy, ComfyUI-RunpodDirect, ComfyUI-Manager sources (host scan)

12. V5.6.0 — encapsulation, one engine, five anchor rows (2026-09-03)

V5.6.0 is two changes at once: the graph was encapsulated into subgraphs, and the two engine paths were merged into one. V5.6.1 (this pass) repaired what the first cut of that work left behind. Read §12.4 before editing anything.

12.1 Encapsulation — what moved and what it costs

165 of 380 nodes now live inside 26 subgraph definitions, leaving 215 on the canvas. Groups that used to hold a dozen nodes now hold the single instance that stands in for them.

Subgraph What it holds
SETTINGS Builder (31) · Analyzer Report Builder A (32) / B (8) the 📋 sentence chain and the ✅ Analyzer report
Tier Routing Bus (9) every 🎚️/🎧 sampling switch (steps, sampler, shift_video, shift_audio, unet checkpoint, MP, ref size)
Aspect Math (10) IMG 1 → ratio → 32-aligned canvas
Chain · 🔗1 (5) / 🔗2 (4) the continue rows’ loaders and riders
Analyzer Tap · Base / Upscale / RTX / RIFE (6 each) per-stage frame sampling for the report
Vision Prep · 🖼️ IMG 1-9 (1-2 each) resize-and-pad + batch per picture slot
Prompt Assembly A (4) / B (2) · Editor Assembly (4) · Ref2VA Prep (6) · Scene Path Builder (5) · H3 Upscale Pass (4) prompt band, editor, task stub, scene paths, the 2nd sampling pass

Three rules follow from this, and every one of them was violated by the first cut:

  1. A group toggle reaches the instance, never the interior. rgthree sets mode on the nodes a group’s bounds capture — on the canvas that is the instance. So an interior node must ship at mode 0 and let the instance carry the row’s on/off. Encapsulation froze 28 interiors at the mode 4 they happened to have when their group was off, which made 🔗1 / 🔗2 / 🔗3 and the Quality/Master analyzer taps impossible to switch back on.
  2. An optional chain still needs its primitive on the canvas (§0 rule 3). 🔧 Aspect override had its override primitive absorbed into Aspect Math, so both aspect groups ended up with identical bounds around the same instance and the toggle bypassed the whole aspect computation. The primitive is back on the canvas behind an override_ratio subgraph input.
  3. Interior pos is subgraph-local. Comparing it to a main-canvas group box compares two coordinate spaces. byid() annotates each interior with its instance’s canvas box; nbound() reads that annotation.

12.2 One engine

V5.7: the merge is baked, not performed. Node 940 is a stock UNETLoader again, and the file it loads is picked by the 🎚️ tier — DaSiWa’s pre-merged hybrid, which is the same fl2va+ref2va idea the old MiniMaxH3HybridLoader did at load time, done once by its author and shipped as one file. A pre-merged checkpoint cannot go back through that node: it validates that its two inputs have identical key sets and refuses anything else, and it discards the safetensors header that carries _quantization_metadata. The pack left PACKS with it.

Retired earlier, in V5.6.0, when the two engines merged: node 14 (the FL2VA ReferenceToVideo), 981 (its turbo loader), 1082 (its shift), and the whole duplicated FL2VA half of the prompt, editor and analyzer bands.

🎚️ tier checkpoint steps shift video / audio
⚡ Iterate DasiwaMinimaxH3_dasiwaHybrid4turboV1 4 9 / 4
💎 Quality DasiwaMinimaxH3_dasiwaHybrid8turboV1 8 9 / 4
🏆 Master DasiwaMinimaxH3_dasiwaHybridV1 (no turbo) 20 11 / 3

All three are the pruned lineage: MiniMax’s ~13B-parameter AdaLN modulation projection is replaced by a curve derived from those same weights (adaln_t_table), which Comfy Org’s maintainers describe as functionally equivalent — same seed, different output, no better or worse. The 🏆 Master file has no Hugging Face mirror, so it is the one CivitAI download; its version ships int4 and int8 under one basename, so the manifest pins fileId.

🎬 now selects which frames are PINNED, never which engine runs. Five rows on one radio:

Row Pins
🎬 T2VA nothing — and, from V5.7, every image reference is gated off (see below)
🎬 Ref2VA nothing — references only (the shipped row; its group is deliberately empty)
🎬 I2VA IMG 1 as the first frame
🎬 L2VA IMG 1 as the last frame
🎬 FL2VA IMG 1 first, IMG 2 last

The anchors are a cascade of MiniMaxH3AddGuide nodes, one per row, each bypassed unless its row is chosen: 13 → 1357 → 1358 → 995 → 996. A bypassed AddGuide passes conditioning straight through (slot 0 → slot 0, same type), which is why the radio needs no switch node — and also why 🎬 MODE SELECT must stay a Bypasser: a muted AddGuide would return None and break the chain.

🎬 T2VA really is text only (V5.7). Because bypass passes values through and only mute blocks them, a Bypasser panel cannot cut a reference on its own — before V5.7, picking T2VA stopped the pinning but every loaded IMG still reached the engine, which is Ref2VA by another name. The fix is a node that returns nothing while live: nine MiniMaxH3OptionalReferenceImage gates (1415-1423, enabled = false) sit inside the 🎬 T2VA group between each IMG loader and its 🎞️ cut. On every other row they are bypassed and pass the image through; on T2VA they go live and return None, which MiniMaxH3ReferenceToVideo ignores. VID 1-3 and AUDIO 1-3 ship bypassed, so they need no gate — but if you enable one by hand and then pick T2VA, cut it on its own 🎞️ row.

The turbo LoRA is a manual override. Both distilled tiers now carry their turbo in the weights, so stacking a LoRA on top would distil twice — and the old one was fl2v-trained, on a ref2va -derived hybrid. Node 980 stays in the graph on the 🔧 row at strength 0, carrying the one turbo LoRA built for this checkpoint (…DasiwaREF2VAHybridV1_0_curveproj1025_compat…); raise its strength to make 🏆 Master fast. It is live rather than bypassed because LoraLoader.load_lora returns before it opens the file at strength 0, so nothing is loaded and the widget path never has to resolve.

🎞️ REFERENCES. A per-slot Any Switch (1366-1374 images, 1375-1377 video, 1378-1380 video audio, 1381-1383 audio) sits between each loader and the engine, driven by a Fast Groups Muter (node 1384). V5.7 split the one container group into 18 per-slot groups, so the muter renders 18 rows and a single reference really can be cut without unloading the file — until then its ^🎞️ REFERENCES regex matched one group and the single toggle cut all eighteen at once. The Prompt Check (1096) reads through these switches too, so a cut slot is invisible to the grader as well as to the engine, and its task follows the 🎬 row instead of a frozen widget.

12.3 Control reference (absorbed from the canvas)

V5.6.0 removed ~1,055 words of on-canvas prose. Every control now carries a one-line title; this is the long form.

  • 🎬 MODE — see the table in §12.2.
  • 🎚️ VIDEO — 💎 and 🏆 share the H3 2×/RTX stages, so both rows light when either is chosen; to move between them click ⚡ first (rule 1). The tier picks the checkpoint as well as the canvas (V5.7): ⚡ Iterate 0.4 MP / 4-turbo checkpoint at 4 steps / Sol-Attn · 💎 Quality 0.98 MP / 8-turbo checkpoint at 8 steps / refs max / H3 2× / RTX 4K · 🏆 Master = the Quality canvas on the non-turbo checkpoint at 20 steps
    • RIFE 4.26 → 60 fps. No tier stacks a turbo LoRA any more.
  • 🎧 AUDIO — 🎧1 as generated · 🎧2 320 kbps AAC + FLAC master · 🎧3 + −14 LUFS / −1 dBFS. Sampling follows the higher of the two radios — H3 makes picture and sound in one pass, so VIDEO Iterate + AUDIO Master gives a fast 0.4 MP canvas with clean 20-step speech.
  • 🔗 CONTINUE — 🔗0 new scene · 🔗1 IMG 1 = the previous scene’s last frame (a source; pin it with 🎬 I2VA/FL2VA) · 🔗2 + motion carry-over (22 frames + audio) · 🔗3 + tail video → VID 1.
  • 🧠 PROMPT ENGINE — 🧠0 manual (the ✍️ Positive node, which now lives in that group; it ships muted, so the panel shows one lit row — V6.0.0) · 🧠1 the 32B vision engine (captions + writer + editor read IMG 1-9 through H3’s own text encoder).
  • ✅ Toggles — Fidelity Rules · Passion (retired in V7) · Continuity Editor · Analyzer (ships ON) · Audio Anchor (AUDIO 1 becomes the exact soundtrack; ships OFF). The old ✅7b / ✅8 / ✅9 / ✅A / ✅🎙 numbering is gone — there was never a ✅1-7.
  • 🧩 LoRAs (V6.0.0) — one panel for every LoRA: the three shipped style rows Realism / Physics / Camera (strength on the row, all ON) and three rows for your own files, 🧩 LoRA · User 1-3 (ship OFF). Loading your own LoRA: put the file under models/loras/MiniMax-H3/Custom/ (or let Civicomfy save it there), pick it in the row’s loader, switch the row on in the 🧩 LoRAs panel, set the strength. A user row ships with the placeholder PUT-YOUR-LORA-HERE.safetensors selected — the installer knows it as a placeholder (PLACEHOLDERS in the script) and never fetches it; switching a row on with the placeholder still selected fails at queue with the loader named, which is the intended error, not a fallback.
  • 🔧 Advanced — Turbo (manual, ships 0) · Aspect override.
  • Reference Toggles — IMG 1-9, VID 1-3, AUDIO 1-3. IMG 1-9 ship ON and empty; VID and AUDIO ship bypassed. A slot that ships ON and empty needs a file or a mute: upload a file or mute the slot, or the queue stops naming that node.
  • Text fields (V6.0.1) — the six fields you type in (🎯 brief, ✍️ manual prompt, 👁 caption rules, 🔒 house rules, 🧑‍⚖️ editor rules, the passion rider) are 620 px wide (520 for the rider), 240-320 px tall, and each has a 📖 note directly above it: what the field does, who reads it and when, and an example. The notes are nodes 1459-1464; _build/restructure.py (FIELD_NOTES) owns their text and keeps each centred on its field.
  • Brief format (🎯) — Action: · Camera: · Cuts: · Dialogue: · Sound: · Refs: (Picture 4 = location, Audio 1 = voice) · Fidelity: nothing else happens.
  • 🎲 Seed — −1 = a new seed each queue; ♻️ reuses the last one for a retry. It appears in LIVE SETTINGS and in the report.
  • Scene chaining — scene 1: 🔗0, 🎬 SCENE # = 1, stills in IMG 1-9, queue. Scene 2: SCENE # = 2, pick a 🔗 row, write the next brief, queue. The cache is output/MiniMaxH3/chain/scene_N/.

12.4 What V5.6.1 repaired

Found by re-running the embedded suite (which was red at 42 failed / 87 passed) and by an independent graph validator.

# Defect Fix
1 the RunPod zip shipped the previous pair — the quick start deployed the old version rebuilt from the current files; the suite now asserts the hashes match
2 28 interiors across 5 subgraphs frozen at mode 4 — 🔗1/🔗2/🔗3 and the Upscale/RTX/RIFE taps could never be switched on interiors → mode 0; the instance carries the row
3 🔧 Aspect override bypassed the whole aspect computation override primitive promoted to the canvas behind a new override_ratio input
4 H3 Upscale Pass instance was missing noise_seed, shifting every later slot by one (pre-existing since V5.5.0) input restored at slot 1, wired to the master seed #1235; the stale [444, …] widget pair dropped
5 five dead nodes left by the engine merge (1067, 1102, 1103, 1107, 1202) — a complete duplicate prompt path whose output went nowhere removed
6 📋 anchor · start / · end sat outside every 🎬 group, so the settings block could never report the pinned frame moved into 🎬 I2VA / 🎬 L2VA
7 widgets_values_named had drifted from widgets_values on 3 nodes — #1314 was missing the entire ===== AUDIO ===== analyzer section added in V5.5.0 mirrored from the canonical list
8 nodes 757 / 781 / 830 carried G:\StabilityMatrix-win-x64\… absolute paths from the original author’s machine removed
9 _all_nodes and src were each defined twice in the suite; the later, main-canvas-only copies won, so the subgraph-aware helpers written for V5.6.0 were dead code duplicates removed / renamed
10 the runner exported H3_ON_POD=1 on any machine without H3_FAKE_ROOT, so the nine @pod_only tests ran and failed on workstations gated on Linux + /dev/nvidia0 + a real custom_nodes
11 ~20 node ids, the group table, the panel row counts and the model list were stale retargeted; expected_members regenerated for the 53 groups
12 MrWeaz / EZ H3 branding, and output/MrWeaz/ as the on-disk tree renamed to MiniMax H3 Video Creator Workflow and output/MiniMaxH3/ — writers, readers, path builders, script and handbook together

Output path change. A pod carrying an existing output/MrWeaz/chain cache will not find it after this release; scene 1 simply regenerates. Move it with mv output/MrWeaz output/MiniMaxH3 if you want the history.

12.5 Verification (2026-09-03, offline)

bash "video-creator-minimax-h3-script.sh" test → 216 passed, 7 skipped, 0 failed against the local testbed (180 passed, 20 skipped bare). The ten non-pod tiers pass, including the e2e tier’s full 768-state radio sweep; comfyui, runpod and gpu skip because there is no ComfyUI tree, pod or GPU on the workstation — a skip there is the correct result, and it is what defect 10 above was hiding.

Independently of the suite: link endpoint/slot/type integrity is clean across the main graph and all 26 subgraphs; all 26 instances match their definition’s IO; every group captures the same set under all three rules (overlap / containment / center); no group has mixed member modes; the only three groups capturing nothing are the deliberate “off” rows 🔗0, 🎧1 and 🎬 Ref2VA; last_node_id 1409 and last_link_id 10546 bound every id in use.

Still to do: the pod acceptance run in §11.10, now unblocked (the zip finally contains this version). Extend step 4 to exercise T2VA / I2VA / L2VA and the 🎞️ REFERENCES mute, which are the V5.6.0 features nothing has rendered yet.

12.6 Three new tiers (V5.6.1)

The suite went from nine tiers to thirteen. The additions answer questions the old tiers did not ask: the radio grid swept 768 states but only checked “is anything dead?”, and nothing at all covered the install as a thing that can fail before a single frame renders.

flows (33 tests) — one 🎬 row at a time, which is how the workflow is actually used. Per row: exactly one anchor set may execute, the other rows’ AddGuide nodes must be bypassed so the cascade passes conditioning through, and the sampler’s conditioning must trace to the expected node (13 for T2VA/Ref2VA, 1357 I2VA, 1358 L2VA, 996 FL2VA). Ref2VA additionally proves the full loadout — 9 stills through their per-slot 🎞️ cuts, 3 videos and 3 audio into the Omni captioner. I2VA / L2VA / FL2VA each prove which loader is pinned and that the matching anchor sentence rides the same row. A 5 × 3 sweep re-runs every row against every 🎚️/🎧 tier. A cut on 🎞️ REFERENCES must mute (2), never bypass, and must not change any loader’s mode.

automation (16 tests) — one per automated subsystem, testing behaviour rather than wiring: the auto prompter (captioner → Omni → writer → editor → Prompt Check → engine, plus what changes with the editor off and on the manual engine, where the 32B band must be muted not bypassed); the auto reviewer (which taps run per preset, and that turning it off actually stops the 32B judge and the report file); scene chaining (the write path of scene N and the read path of scene N+1 must agree — a contract between two separate runs); the audio anchor, aspect override, tier routing (steps follow the higher of the two radios), and LIVE SETTINGS (a sentence appears exactly when its setting is on).

install (7 tests) — what has to be true for a fresh pod install to work, checked offline: every model’s download folder matches the category the loading node reads from (a file in the wrong folder is invisible to the node); no two downloads share a basename (the relocation pass matches on basename); nothing is downloaded that no node opens; declared sizes are plausible and total ~111 GB; the free-space gate runs before anything is spent; --use-sage-attention is stripped from the launch args; and the venv is rebuilt at the template path start.sh actually activates.

Also added to config: the shipping zip must contain the files beside it, by sha256. That one exists because V5.6.0 shipped a zip holding the previous release — and because the script is inside the zip, it re-fails on every script edit until the zip is rebuilt, which is the point.

What the tiers still cannot see. comfyui needs the node packs’ source (bash testbed.sh; the suite finds it as BASE_NODE_SRC), runpod needs the pod, gpu needs the GPU. Those 20 skips are the honest remainder: widget order against each pack’s real signature, the live /object_info, and whether Sol-Attn’s Triton kernels, nvidia-vfx and RIFE 4.26 import under Python 3.13 / torch 2.13. Nothing offline can answer those — the pod acceptance run in §11.10 is still the gate.

12.7 The ComfyUI testbed — and the bug it caught immediately

testbed.sh, beside the project folders, provisions a real ComfyUI and all 11 node packs that every project in this directory can test against:

bash testbed.sh # clone ComfyUI at its newest release tag + the 14 packs (~190 MB)
bash testbed.sh --server # + a CPU venv, and start ComfyUI on port 8199 (~2.0 GB)
bash testbed.sh --status # what is here and what is running
bash testbed.sh --stop # stop the server

Then, from any project folder:

cd "MiniMax H3 Video Creator" && bash "video-creator-minimax-h3-script.sh" test # finds ../ComfyUI and the server on 8199 by itself (V5.9.0)

It lives at Projects/ComfyUI/ComfyUI, not ~/ComfyUI, on purpose: the toolchain’s pod discovery scans $HOME/ComfyUI, /workspace/* and /ComfyUI, so a tree at the home path would be found by a plain bash "…-script.sh" run and have its venv rebuilt. This path is invisible to that scan.

What it found on the first run. Three caption nodes (1064, 1199, 1295) still stored the bare "Qwen2.5-Omni-7B". The rewriter pack has since moved to descriptive option labels, and its resolver does an exact dict lookup:

'Qwen2.5-Omni-7B' is not in the captioner list any more. Pick another entry, …

Node 1295 is the analyzer’s audio caption, and the analyzer ships ON — so the first render on a fresh pod would have failed at queue time. Nothing offline could have caught this: it is not a graph defect, it is the pack changing under us. All three now store the full label, and test_server_combo_widget_values_are_offered fails if any stored COMBO value stops being offered.

A second, quieter find. ComfyUI 0.34.x reports combos as ["COMBO", {"options": [...]}]; the older shape put the option list in the type slot. The pod’s own widget-count test only read the old shape, so on a current ComfyUI it silently counted every combo as absent. That test now routes through a reader that understands both — and, because the server tier runs against any reachable ComfyUI, it exercises on the pod too instead of only there.

Honest limits. This host has no NVIDIA GPU, so SolAttnPatch and RTXVideoSuperResolution cannot register; they are listed in CUDA_ONLY and the test demands they be present when the server reports a CUDA device. Widget order for V3 nodes with autogrow inputs (MiniMaxH3MultiReferenceCaption and friends) is built by each pack’s own JS, so the server’s declared order is not authoritative for them — they sit in DYNAMIC_INPUT_TYPES. And 63 of 65 node types register, which is strong evidence the graph will load, but it is not a render: the weights and an sm_120 GPU are still only on the pod.

12.8 V5.6.1 — the ship-ready pass (2026-09-03)

Cleanup before the pod run, plus two defects that would have cost a render each.

Two one-shot-install blockers, both found by auditing against the real packs:

The GGUF pair went to the wrong folder. The script fetched models/LLM/*.gguf flat, but the rewriter pack resolves its captioner to models/LLM/<repo last segment>/ (nodes.py::_ensure_pair). The prefetched 6.2 GB was invisible to it, so the pod would have downloaded the same pair again, silently, blocking the analyzer’s first run. Dests are now LLM/Qwen2.5-Omni-7B-GGUF/…. Script.sh MODELS + 3 tests
/workspace vs /workspace/runpod-slim. Step 1 of the acceptance run named the wrong directory — ComfyUI’s parent is runpod-slim. Following it verbatim meant the script could not find ComfyUI. §11.10 step 1

Two leftovers that would have shipped:

  • Node 746 (🎞️ VID 1) carried skip_first_frames: 160 and a previous clip’s filename, from a test session. Any video dropped into VID 1 would have silently lost its first 160 frames. Reset to 0.
  • The analyzer rules (node 1314) named knobs that no longer exist — “LTX polish”, 🔧 Ref-Delta on/off, ➕ ref noise, ✅0/✅0b anchors. The rules tell the 32B “Each must name a knob in THIS graph”, so it was being steered to recommend controls the user cannot find.

New: the combo self-heal step (h3_combos, after the restart). A pack can rename its dropdown options between our last test and your install; ComfyUI rejects a COMBO value that is not in the list, so the graph loads and the render dies. This step compares every stored value against the live /object_info, repairs one whose prefix still identifies it uniquely, reports the change, and blocks on anything it cannot resolve. It matches by value, never by widget index — a V3 autogrow node stores its widgets in the order its own JS builds them, offset from the order the server declares, so an index-based rewrite would put the model into length.

Its limit, stated plainly: widgets_values mixes combo values with free text, so the step repairs renames reliably but cannot flag an arbitrary bad value. That is the suite’s job — test_server_combo_widget_values_are_offered checks by index with exemptions, and runs after it.

Cleanup. 20 dead constants and helpers removed (the FL2VA prep chain, the two-engine switches, the ref2v turbo trio, main_nodes, _wf_json, a duplicate TEXTGEN, a dead ai= kwarg, an unused tempfile import); 26 stale comments corrected (tier counts said nine, pack counts said eleven, node counts said ~190, five places still described the current build as V5.5.0); the CAT node-type→folder map had drifted between its two copies and is now consistent; §§1–10 compressed from 309 lines of prose to a changelog table; both backup folders deleted.


13. V5.7.0 — the canvas becomes an interface (2026-09-04)

Historical since V6.0.0: this is the record of the V5.7 canvas and the tools and tests it introduced, which are still in force. The cockpit that replaced that canvas is §17.

Six versions of nudging boxes in the frontend produced a canvas of 12,180 × 16,828 px holding 217 nodes at 5.5% fill, with the 70 controls a person touches scattered across all of it — the mode panel at x=6511, the image slots strung from x=1240 to x=12180, the settings block 7,000 px below both. The eight layout tests in §12.6 passed throughout, because they check that nothing collides and nothing clips a group edge, not that the canvas is a place anyone can work.

13.1 What the measurements said

Three sources, none of them taste.

ComfyUI’s own renderer. comfyui_frontend_package 1.51.x computes lowQualityZoomThreshold = min_font_size_for_lod / (NODE_TEXT_SIZE * sqrt(devicePixelRatio)), which with the shipped defaults 8 and 14 is 0.571 at dpr 1 and 0.404 at dpr 2. Under it the canvas draws no widget text and DomWidgets hides the DOM widgets entirely. V5.6.2 shipped ds.scale = 0.20. Confirmed live against the testbed: canvas.low_quality was true on load. The workflow did not merely open zoomed out — it opened blank.

The 492 official Comfy-Org templates in comfyui_workflow_templates_json. Canvas height max 5,770 px; area max 24.6 Mpx; fill min 0.192, median 0.598; backward-link fraction median 0.000, p90 0.053; saved scale median 0.59, which is authors parking just above the cliff. H3 was outside every one of them.

The graph itself. Groups covered 4.6× the node area; 204 of 217 nodes were off any grid; 17 belonged to no group; the longest group title was 91 characters, rendered at font-size 24 and truncated anyway in the 462 px rgthree panel it was written for.

13.2 What changed

V5.6.2 V5.7.0
canvas 12,180 × 16,828 (205 Mpx) 12,080 × 3,866 (46.7 Mpx)
opens at 0.20 — below the cliff, widgets blank 0.58 — above it
fill 0.055 0.256
frame bloat (union) 4.6× 2.1×
nodes off the 10 px grid 204 of 217 0
nodes in no group 17 2, both by design
genuine backward links — 4.6%
longest group title 91 chars 28
section notes 1 for 217 nodes 7

Node ids, links, widgets_values and bypass modes are untouched by the geometry pass, which layout.py --verify proves and the 768-state radio sweep, the flows tier and the live testbed each confirm independently. In the running testbed all nine panels list exactly the groups they listed before.

Two new tools, in _build/. render.py draws the canvas to scale from the JSON alone and reports every number this tier asserts; it needs no server, so it runs in CI and on the pod, and it cannot flatter the workflow because the shipped file is all it reads. layout.py owns the geometry (rule 6b), orders the frames by greedy feedback-arc-set minimisation, shortens the titles, and creates the section notes.

The panels split. The five radios touched every queue stay in 🎛️ CONTROL PANEL; the four set once and forgotten — ✅ Toggles, 🔧 Advanced, the reference cuts, the 🎞️ REFERENCES muter — moved to 🎛️ ADVANCED beside it. The three viewport bookmarks were stacked in one corner, so all three pointed at one place; they now sit at 0%, 51% and 94% along the path, and are retitled 🔖 1 · Start, 🔖 2 · Controls, 🔖 3 · Output.

The titles gave the gloss back. V5.6.0 pushed the control-panel guide into the group titles so every radio row would explain itself. The half that worked is kept — no card explains a control that can explain itself, and no subsystem essay returns to the canvas — but with nowhere else to put it the gloss grew to 91 characters. It now lives in six 📖 notes beside the sections they describe, which is what the Comfy-Org template guide recommends, and the titles are labels again. Rows that must explain themselves still do: test_uiux_control_panel_documents_every_radio requires a · or — in every 🎬 / 🎚️ / 🎧 / 🔗 / 🧠 title.

13.3 Fourteen new test_uiux_* checks, and three arguments with them

The tier is what stops this happening again. Every threshold traces to the frontend’s constants or to the corpus. Three had to be argued with rather than simply written, and the arguments are the useful part of this section.

“No orphans” contradicted a rule. Nodes 1047/1048 must touch no group — inside a toggled one they are bypassed with it and the auto▸manual fallthrough dies (asserted independently; search 1047). The exemption is two named ids with the reason attached, not a predicate, because a rule like “switches may float” would quietly re-admit the other fifteen nodes that were floating.

“Canvas under 24.6 Mpx” compared different things. That maximum belongs to a 66-node template. Holding a 217-node graph to an absolute area is not a like-for-like comparison, so the budget is per node, where the corpus agrees: 0.25 to 0.37 Mpx per node. Height stays absolute, because height is what makes a canvas unnavigable.

“Every link runs left to right” measures something this graph cannot have. The group-level graph is cyclic: 24 of 49 frames sit in a mutually dependent pair, because a group here is an rgthree toggle set rather than a pipeline stage, and toggle sets legitimately feed each other (GEN SETTINGS ↔ Processing, the hybrid engine ↔ the analyzer, and ten more). No arrangement puts both members of such a pair first. Counting only pairs that do have an order gives 4.6%, inside the corpus, where the raw 12.2% would have read as failure.

A fourth is a judgement call rather than a correction, and is flagged as one in the code. Fill declines with frame count — 0.598 at the corpus median, where the median template has no groups at all, and 0.480 among those with twelve or more. The most-grouped official template has sixteen frames; this workflow has fifty-five, because the radios need one per option, and three of them are Venn bands whose frames must cover cells they hold no content in. Balanced lanes, a skyline packer and a swept lane height took fill from 0.055 to 0.256 and stopped there. The thresholds protect what was reached rather than describing an ambition, and say so.

13.4 What the guard caught

layout.py re-derives group membership after placing and refuses to write on any change it was not told to expect. It earned that on the first four runs: 🔧 Turbo orphaned out of both parent frames because the nesting was built one level deep; 📦 Models emptied because alphabetical order let ⚙️ H3 Hybrid engine claim itself as top-level first; a frame laid down on top of the two nodes that must touch none, because “leave them alone” is not the same as leaving them where they were when everything else moves; and adoptions that silently did not happen. In a 58,000-line diff none of these would have been visible.

Two more were caught by the suite rather than the guard: the skyline packer reserved its gap vertically but not horizontally, so frames sat flush and their padding overlapped; and the preset spans were drawn from cell arithmetic rather than from where the nodes actually landed, so grid snapping left 9 px of padding where 10 is required.

13.6 The visual pass, and the three things only it caught

Rule 6d exists because of this section. python3 "base/comfyui-base/_build/canvas/render.py" --pkg . --review was run against the canvas and the canvas was then opened in a real ComfyUI and looked at, region by region. Three defects turned up that the whole 211-test suite could not see, and all three are now tests.

The banner labels painted across the canvas. A rgthree Label draws its text at its own fontSize and takes no notice of its node box. All five banners were 30-40px type in nodes 26px tall and a third of the width the text needed, so they painted straight over everything to their right — a smear across the entire top-left of the graph. Nothing in the suite could detect it, because the nodes did not overlap; only the ink did. test_uiux_labels_fit_their_text now sizes every label against its own text. The four section banners also still described CONTROL DECK / PIPELINE / REFERENCE INPUTS / CONTINUE & CHAIN, four regions that stopped existing in this version, and now read ① WHAT YOU TOUCH and ② WHAT RUNS ITSELF.

The reading path doubled back. Ordering the whole canvas by dataflow gives excellent wiring and a hostile interface: the two control panels came out 2,100 px apart, the fifteen reference slots split across three columns, and the brief landed at x=9,700 when it is the first thing you type. The frames a person touches now claim the leftmost lanes in the order the first-render flow uses, and test_uiux_the_user_path_reads_in_order fails if the sequence goes backwards.

Ordering by dataflow and ordering by use are not the same thing here and cannot both be had. The price is written into FLOW_MAX: genuine backward links went from 4.6% to about 14%, because the SETTINGS block feeds the prompt writer but you read it after queueing, so wherever it sits one of those two relationships runs backwards. Where the two orders can agree they were made to — length, scene, seed and the SETTINGS block now come before the brief, which removed several backward links and is truer anyway: you choose how long the clip is before describing it.

The layout tool was not reproducible. Two runs on the same input produced different canvases. The feedback-arc-set pass iterated a set of group titles, Python seeds string hashing per process, and the ties it breaks are common, so the result moved every run. Every diff would have looked like ordinary churn and no run could be trusted to be the one that was tested. Every iteration in the ordering pass is now over a sorted sequence, --verify proves a second run moves nothing at all, and test_config_the_layout_tool_reproduces_itself runs it.

One thing the visual pass confirmed rather than caught: ComfyUI’s Comfy.EnableWorkflowViewRestore has defaultValue: true, so extra.ds really is applied on open and rule 6c bites.

13.5 Still open

Subgraphing the automatic interiors. 223 top-level nodes is a lot to look at, and the three H3QwenVLGenerateText nodes alone are 0.8 Mpx that nobody reads — only their Display Any outputs. Moving them into subgraph instances is a real simplification and the mechanism is proven here (this workflow already ships 42 promoted widgets across 7 instances; a promoted widget is a subgraph input carrying widget: {name}, not a widgets array).

It is worth doing for its own sake and not for density: measured, collapsing the eight largest automatic nodes removes 1.33 Mpx of node area while every group frame stays the same size, so fill would go 0.256 → 0.211. Density came from lanes, packing and the strips, not from node count.

14. V5.8.0 — the baked hybrid, and four defects it exposed (2026-09-04)

DaSiWa published the fl2va+ref2va merge V5.6.0 was performing at load time as a pre-merged checkpoint, in three distillation grades. Taking it retires the merge, retires a turbo LoRA that was trained on the wrong branch, and lets the 🎚️ tier pick weights matched to its own step count.

14.1 One loader, three checkpoints

Node 940 is a stock UNETLoader again; its unet_name is tier-switched the way the turbo LoRA’s lora_name used to be. See the table in §12.2 for the three files and their settings.

Why the pre-merged file cannot go through MiniMaxH3HybridLoader: that node validates its two inputs have identical key sets and raises otherwise — a check that runs even when the overlay preset is none, before it knows the overlay will never be read — and it hardcodes metadata = {}, discarding the safetensors header that carries _quantization_metadata. It is a merger; a merged file is its output, not its input. The pack left PACKS (12 → 11).

A latent bug this retired. The pack’s own minimax_h3_analysis.md derived the shipped block_range_adaln 25-49 preset from the pruned checkpoints — its table lists adaln_t_table at 8,200 params and blocks.adaln_proj at 43,545,600 — while MODELS downloaded the unpruned pair, where those same keys carry ~300× more parameters and no curve table exists. The preset had been moving ~6.5B params on files it was never characterised against.

14.2 What freeing node 980 paid for

Moving the turbo LoRA off the tier bus freed exactly two bus outputs — slot 2 (FLOAT) and slot 5 (COMBO) — which are precisely the two shapes the change needed. Slot 5 now drives 940.unet_name; slot 2 drives 788.shift_audio, which was never tier-switched before. Turbo rows run 9/4, Master 11/3. No new bus outputs were added; switch 1086 grew from three inputs to five so it takes the same [🎚️3, 🎧3, 🎚️2, 🎧2, ⚡] shape as the steps switch (design rule 14).

14.3 CivitAI as a second source

The 🏆 Master checkpoint has no Hugging Face mirror, so the installer’s Hub-only rule was lifted: CIVITAI_TOKEN is prompted and stored exactly like HF_TOKEN (same tokens.env, mode 600), the key rides an Authorization header rather than a ?token= query string, and aria2c is installed when missing so the 21 GB single-stream pull runs 16-way. Hugging Face stays first: the guard at test_setup_required_models_are_used_by_workflow now allows exactly one CivitAI row and requires it to pin a fileId — DaSiWa’s non-turbo version ships int4 and int8 under one basename, so a URL without the fileId can silently fetch a 12 GB file that will not load. This is a source fallback, not a model fallback: a missing file still fails the run loudly.

14.4 The four defects

  1. 🎬 T2VA never cut references. Bypass passes a value through; only mute blocks it — and 🎬 MODE SELECT must stay a Bypasser, because the AddGuide cascade relies on bypassed nodes passing conditioning along. Fixed with nine MiniMaxH3OptionalReferenceImage gates that return nothing while live, inside the 🎬 T2VA group. See §12.2.
  2. 🎞️ REFERENCES could not cut one slot. Its ^🎞️ REFERENCES regex matched a single container group, so the muter rendered one toggle over all 18 switches. Split into 18 per-slot groups.
  3. Prompt Check graded references the engine would never see — it read the loaders directly and its task was a frozen widget. Both now follow the graph.
  4. 🔗1 and 🎬 I2VA/FL2VA could both pin frame 0. The 🔗 still-anchor pinned from the wrong axis. Retired: 🔗 selects IMG 1’s source, 🎬 selects whether it is pinned (design rule 2).

14.5 Verification

bash "video-creator-minimax-h3-script.sh" test — 233 passed, 23 skipped off-pod, and 246 passed, 10 skipped against the local ComfyUI testbed with a live server (§12.7), with every assertion above rewritten rather than deleted. Still outstanding, and only provable on the pod: one render per tier confirming each loads its own checkpoint, and a 🎬 I2VA / FL2VA render with references filled — identity carrying from the references while the pinned frames land where asked is the combination this whole change rests on, and the one thing the suite cannot prove.


15. V5.9.0 — onto ComfyUI Base (2026-09-04)

No graph change. The 6929-line script that carried its own toolchain became a 169-line declaration on the shared base, and the suite left the script for suite.py. Node ids, geometry (_build/layout.py), the model library dests and the VHS format are byte-for-byte what V5.8.0 shipped; the workflow’s only edits are the two version strings.

15.1 What moved, and what stayed

Moved to comfyui-base/ (once, for every workflow on the pod): discovery, tokens, the ComfyUI tag update, the Python / torch picks and the venv build with backup, rollback, persistence check and rescue (defects 39–41 are structural there: the interpreter lives at /workspace/comfy-base/python), pack clone / pin / --latest, the model index over every filesystem, exact-size verification, hf-xet / aria2c / curl downloads, the one deletion prompt, the workflow path sync (py/sync_workflow.py, from this package’s PYEOF_SYNC), the import check, comfyui_args.txt hygiene (--preview-method auto, --disable-api-nodes, --use-sage-attention stripped — this package declares HYGIENE_ARGS_REMOVE=( --use-sage-attention ), so it goes even on a pod where the Video Replacer put sageattention in the venv for its ⚡ node), Manager security_level, extra_model_paths.yaml, the restart (now opt-in, BASE_RESTART=1 — V5.8.0 restarted unconditionally, which is wrong on a pod another workflow may be rendering on), smoke, combos, the ledger, the summary and help. The three packs every workflow shares — rgthree, KJNodes, VideoHelperSuite — are the base’s pins.

Stayed here, as tables and hooks: the eight H3-only packs; the sixteen files with exact byte sizes (base.sh gen-models filled them from the Hub’s x-linked-size; the CivitAI Master engine’s from api/v1/model-versions/3251526, fileId 3156811 = the int8 file, 20 967 669 024 bytes); SUPERSEDED, LEGACY_DIRS, DROPPED_PACKS; PIP_EXTRA=( nvidia-vfx ); and four hooks — pkg_hygiene (the 320 kbps AAC / NVENC VHS format at /workspace/h3/video_formats, the same path V5.5–V5.8 pods already have registered, through the base’s _base_yaml_register h3 VHS_video_formats), pkg_post_models (output/MiniMaxH3/chain; the rewriter’s models.json minus the retired 27B writer), pkg_import_check (comfy_kitchen / triton / comfy_aimdo, and nvidia-vfx under the import name its wheel declares, defect 12), pkg_summary (the first-render checklist). The script cites the defects it still embodies and test_config_every_cited_defect_has_a_handbook_row still checks each has a row here.

Identity of what was replaced: video-creator-minimax-h3-script.sh at commit fb32173, 6929 lines, sha256 e564328517e81207df30cf3264dc9e58b7c12a5cde562359b3507f798fcc1b9c. It is in git history, not in this folder.

15.2 The suite

suite.py is the PYEOF_TEST heredoc cut out verbatim, then edited by name: 69 test functions deleted — every test_unit_toolchain_* (43), every test_integration_* (12: the fake-pod runs, rescue, the poisoned venv, the torch pin), five test_install_* about the venv path, the disk gate and the sage flag, two test_runpod_* and three test_gpu_* about the venv and torch, the update step, the torch picker, and the three list-format tests. Each has an equivalent in the base’s suite, run against the stub package and against every converted package’s tables. Kept: 154, unchanged in what they assert; where one read the script’s tables or heredocs it now reads the same tables through the base’s basetest.load_package (PKG.models, PKG.packs, OWN_PACKS for the eight that are ours) and the base’s loader_cats map — the very map the install’s sync stage is handed, so suite and installer cannot disagree about which folder a loader reads. New: six — the manifest covers every ACTIVE loader in the shipped graph; no two rows share a basename; every declared URL is on the privacy list; a dry run of the base’s sync tool over this package’s rows fixes nothing (fixed=0 active_missing=0: the shipped JSON is already canonical); --check on a fake pod changes nothing; a real fake run lands all sixteen files, the VHS format, the yaml key, the chain folder and the workflow copy, and a second run downloads nothing. test_setup_script_is_lean now means thin: under 250 lines, no heredoc suite, no base_* function, the right eight packs, no stale model names. test_config_versions_agree checks PKG_VERSION, extra.h3_version, node 749’s title, this handbook’s title and the suite’s own docstring. The tiers are the twelve in pytest.ini (integration retired with its tests); basetest supplies the prefix→marker hook and the per-tier table the old conftest drew.

15.3 What a pod built by V5.5–V5.8 sees

Nothing to move by hand. The library dests are identical, so the index pass finds every file “in place, verified”. user/h3/tokens.env is migrated once into /workspace/comfy-base/state/tokens.env. The .h3-venv-3.13 stamp is adopted (probe → re-stamp .comfy-base-venv) if the venv is current for the base’s Python and torch picks; if a newer torch has shipped, the venv is rebuilt with backup, about ten minutes, and .pre-h3-<ts> is recognised as a rollback source. The VHS format path and the yaml entry are the same. /workspace/h3/python is left where it is (anything under /workspace is durable); the base’s interpreter is /workspace/comfy-base/python. Logs move from /workspace/h3_run_<ts>.log to /workspace/comfy-base/state/logs/h3_<ts>.log.

15.4 Verification (2026-09-04, offline)

bash "video-creator-minimax-h3-script.sh" test against the testbed with a live server: 216 passed, 7 skipped, 0 failed. Bare (no testbed): 173 passed, 20 skipped. The base’s own suite, which now also checks this package’s tables against Flux 2’s and Krea 2’s (no two packages give one basename two dests, no package supersedes what another loads, no shared pack pinned to two commits): green. python3 "base/comfyui-base/_build/package.py" --check confirms both zips match their trees.

Still outstanding, and only provable on the pod — the acceptance sequence in comfyui-base-handbook.md: base zip → comfyui-base-script.sh → this zip → video-creator-minimax-h3-script.sh (venv reported current and reused; the sixteen files found or fetched; test runpod green) → stop/start the pod → ComfyUI comes back → test runpod still green. The V5.8.0 open items in §14.5 (one render per tier, a 🎬 I2VA / FL2VA render with references) stand as well.

16. V6.0.0 step 0 — what the frontend actually queues (2026-09-05)

The suite emulated bypass by pushing a subgraph instance’s mode onto every interior node and resolving node by node (push_instance_modes + resolve). The frontend does something else. executionUtil.ts:70-78 never expands a root node at mode 2 or 4, and ExecutableNodeDTO.ts:279-293 resolves a consumer’s link into a bypassed node through that node’s own slots with _getBypassSlotIndex: the same index if its type is compatible, else the first input of the exact type, else the first compatible input, else nothing. For a subgraph instance those are the instance’s promoted slots, whatever the interior does.

The port (bypass_slot_index, resolve_frontend, _resolve_input_f in suite.py) was checked against the real thing: _build/probe.py loads the shipped file into the testbed’s page and dumps app.graphToPrompt() for seven radio states (default, IMG 2 off, 💎 Quality, 🏆 Master, Master I2VA, analyzer off, 🔗1). Every prompt input agrees with the port in every state — 291 to 354 inputs compared per state, 0 divergent. The old model differed in 129 of them on the default state.

16.1 Frontend vs suite — the shipped file, V5.9.0

Path The suite’s model said The frontend queues Consequence
Chain · 🔗1 (1390) and · 🔗2 (1398) bypassed — the shipped default their STRING outputs resolve to nothing; the writer’s chain marker falls through to (no chain) and SETTINGS rows 14/15 stay empty _getBypassSlotIndex finds no same-index input for a STRING output and takes the first STRING input: directory. 1055 chain + passion.string_a ← 1112 output/MiniMaxH3/chain/scene_N-1/last; 1227.string_b ← 1112; 1228.string_b ← 1113 the writer and the SETTINGS block are told a file path as if it were the chain sentence, on every default run. With 🔗1 on, row 15 still leaks 1113
Vision Prep · IMG 2-9 with a slot switched off (Reference Toggles bypass the 🖼️ frame: LoadImage + the prep instance) the previous batch passes through (rule 9) the instance’s IMAGE output pairs with input 0, which is the slot’s own picture; the bypassed LoadImage has no inputs, so the result is nothing. 1402:1150 ImageBatch is queued with no image1 ImageBatch requires both images: the server rejects the prompt. Switching any of IMG 2-8 off cannot render
Analyzer taps in ⚡ Iterate (1394 Upscale, 1396 RIFE bypassed; 1395 RTX bypassed) the batch chain passes image1 through each bypassed tap: analyzer image = base tap batch 1243 1394’s IMAGE output pairs with input 0 = image (the stage’s own frames) and 1396’s with image1 only by luck of position; the chain resolves to 901 📏 Pre-RIFE Count the 32B analyzer in ⚡ Iterate is shown the raw pre-RIFE frames, without the nine reference pictures or the 768² pads it was designed to compare against. 💎/🏆 are correct (1315.image ← 1255)
Music_LufsNormalizer 1344 .audio in 🏆 (1020 Auto-trim bypassed) live live: 1020’s AUDIO output has no same-index match and takes the exact-type audio input ← 734 no defect — the suite’s first port reported it dead only because the caller named no consumer type
Base tap frames (1393.image ← 1021 bypassed ← 900 ← 733) live live: same-index images IMAGE no defect

The first three are defects in the shipped file and shipped as V5.9.1 before the cockpit (_build/restructure.py, id-keyed and idempotent): the Vision Prep and tap inputs are reordered so the pass-through input sits at the IMAGE output’s index, and the chain rows take an INT prev input with the path formatted inside (1112 moved into 🔗1; 🔗2 formats its own with node 1430), so a STRING output has no STRING input to fall back on and dies as intended. Re-probed after the surgery: 1315.image ← 1243 in ⚡, 1055.string_a ← 1050 (no chain), 1150.image1 ← 1146 with IMG 2 off, and the port still agrees with the frontend on every input in all seven states.

16.2 The rule that comes out of it

For an instance inside a toggled row, every consumed output slot k must satisfy bypass_slot_index(k, consumer type) → the declared pass-through input, or -1. Two moves satisfy it: put the pass-through input at the same index as its output with the identical type, and keep must-die outputs type-disjoint from every input (feed INT scene numbers, not STRING paths). The suite carries the frontend’s resolver and nothing else; test_toggled_instances_honour_the_bypass_contract declares, per instance in a toggled row, which input each consumed output passes through, so a fold that breaks the rule fails before it ships.

16.3 How to reproduce

bash testbed.sh --server
uv run "base/comfyui-base/_build/canvas/probe.py" --pkg "video-creator-minimax-h3" # writes _build/snapshots/probe/prompt-<state>.json
bash "video-creator-minimax-h3-script.sh" test

17. V6.0.0 — the cockpit (2026-09-05)

The complaint that started it: the canvas kept being redesigned against a stand-in picture, and it was not simple to use. Both had one root — no picture from the real frontend — so the release is in two halves: a ground-truth pipeline (§16, Part A of the plan) and a canvas rebuilt on it.

17.1 What changed

  • The picture is the frontend’s. _build/snapshot.py loads the file into the testbed’s ComfyUI (1.49.6) in headless Chromium at the display the canvas is designed for (1512 × 982 at dpr 2, the MacBook), writes opening-*.png and full-*.png, exports every node’s computeSize() to _build/sizes.json (committed, keyed to a structure hash), and measures the toolbars the frontend draws OVER the canvas: the left icon strip (76 css px), the tab bar (52), the Run bar top-right and the zoom widget bottom-right. layout.py treats the computed sizes as a floor — V5.9.1 stored 137 nodes smaller than the page drew them — and lays the origin out from under the strip.
  • 109 machinery nodes folded into 21 more subgraphs (39 in all) by _build/restructure.py, every instance in a toggled row satisfying the bypass contract of §16.2; the test declares, per instance, which input each consumed output passes through. Big instances inside frames a person reads (Sampler, Analyzer, Vision engine, Editor, SETTINGS Builder) ship collapsed.
  • The 18 one-node cut frames are gone: a node-level Fast Muter (rgthree) (1384) wired from the 18 switches lists them by name; the switches sit in one 🗃 Reference cuts frame.
  • 🧩 LoRAs: one panel for the six LoRA rows, three of them yours (§12.3).
  • The cockpit (COCKPIT in layout.py, twelve lines that read like the table): band 1 = the IMG row, then [Start + 🧠0] · [GEN SETTINGS] · [🧠1 ∋ Fidelity Rules] · [CONTROL PANEL], 2,410 × 1,440 units inside the 2,581 × 1,539 the MacBook shows at 0.55; band 2 = the set-once frames; the machinery right of a 220-unit gutter. The saved scale moved from 0.58 to 0.55 (the dpr-2 cliff is 0.404; a non-Retina display zooms to 60% once). The three bookmarks are collapsed stubs so the panel column ends above the zoom widget.
  • Notes are owned by restructure.py (id, title, width, text; height from the suite’s own wrap estimate): the start card is six steps ①-⑥ naming the cockpit’s frames; the ADVANCED note covers the set-once panels and your own LoRAs; the machinery notes say every row there is switched from the control panel.

17.2 Measured, before and after (the frontend’s numbers, _build/page.py)

V5.9.1 V6.0.0
Canvas extent (units) 12,750 × 4,626 7,610 × 4,220
Top-level nodes / inside subgraphs / definitions 234 / 166 / 26 161 / 255 / 39
Frames / frames around a single node 73 / 33 58 / 17
Median / longest link (units) 2,017 / 10,116 1,864 / 7,623
Nodes the frontend draws larger than stored 137 0
Stops of a render on the opening screen (pictures · settings · brief · radios · output) 1 of 5 4 of 5 (the output is one scroll below, by design)
Saved scale / usable opening area (units) 0.58 / 2,476 × 1,488 0.55 / 2,581 × 1,539
Suite 171 fn, 203 passed 172 fn, 204 passed, 7 skipped

17.3 The critique (design-critique rubric against principles P1-P7 of the plan)

  • First impression: the nine slots read as a filmstrip, the eye drops to the start card, then right along settings → brief → radios. The purpose is legible in two seconds (P2). Frame colour is semantics — rose slots, blue structure, blue-grey modes, magenta models — and the one 18 px label is the only display type (P5).
  • Usability, 🟡 moderate: the links from the machinery back into 🧠1 and the control panel (the batch into the Vision engine stub, the panels’ OPT_CONNECTION outputs) draw over the cockpit — ComfyUI paints links above groups and below nodes, so they cross the frames, not the controls. The remedy on the pod is the frontend setting Link Render Mode: Hidden (or Straight); a structural remedy (Set/Get or use-everywhere broadcasts) changes execution and the suite’s model, so it is a V6.1 question, not a V6.0.0 fix. Each slot’s own LoadImage → Vision Prep link crosses its frame diagonally because the prep sits under the 200-unit preview reserve; cosmetic, same list.
  • Usability, 🟡 moderate, pre-existing, measured 2026-09-05 (base/comfyui-base/_build/canvas/panels.py, every row of every panel clicked in the real frontend): rgthree lights a row when any node in its frame is active, so the rows that share stages by design (🎚️2/🎚️3, 🔗2/🔗3, 🎧2/🎧3) light together, and a click on the lit-but-not-chosen row switches the shared stages off instead of choosing it; clicking the chosen row of any radio leaves it with no row at all. The ADVANCED note and rule 1 say how to move between them (through ⚡ / 🔗0 / 🎧1). The 🧠 panel had both rows lit because node 16 shipped live: it now ships muted, and the muter behaves as a radio. The 🎬 panel, the four toggle panels and the 18-row cut muter match the model on every click.
  • Usability, 🟢 minor: on a 1512-wide window the Run bar covers the frame titles of IMG 8 and 9; the node titles beneath stay visible.
  • Hierarchy and consistency: one column of 340-wide panels in the order you set them, notes in one colour, bookmarks as three identical stubs. Nothing is drawn blank at the saved view (P6): the snapshot tier asserts low_quality is false and that every band-1 frame is on screen (P1, P7).
  • Accessibility: node text at 0.55 on dpr 2 is 15 device px; the markdown notes are ≈12 and rely on the Retina display — the reason the scale is not lower and the non-Retina zoom is documented.

17.4 Still open (V6.1 candidates)

Promoted widgets on the engine subgraphs (the official-template move; deletes primitives the suite names by id); hiding the cross-cockpit links structurally; the Vision Prep placement; disjoint radio rows — give 🏆 its own copies of the stages it shares with 💎 (and 🔗3 of 🔗2’s, 🎧3 of 🎧2’s) behind a switch, so rgthree’s rows are truthful and a click never half-selects a preset (the shared band was the V5.7 answer to “one frame per preset”; it is what lights two rows at once). The pod acceptance run on V6.0.0 (§11.10, items 1-13) is the outstanding proof; base V2.0.0 (2026-09-05) made the install template-agnostic and Claude-driven (podctl, the base’s own boot), so the run happens on the HearmemanAI pod Paul deployed — still outstanding.

17.5 V6.0.1 — the text fields (2026-09-05)

Paul’s note after V6.0.0: the places where longer text is typed — prompts, rules, guidelines — must be wider and taller, and must say what the field does and show an example. Six fields qualify (the other string primitives are auto riders and settings sentences): the 🎯 brief, the ✍️ manual prompt, the 👁 caption rules, the 🔒 house rules, the 🧑‍⚖️ editor rules and the passion rider.

  • Bigger boxes: 620 × 320 (brief), 620 × 300 (manual prompt, house rules), 620 × 240 (caption rules, editor rules), 520 × 160 (rider). TEXT_BOXES/SHRINK in restructure.py agree, so the layout neither grows nor shrinks them.
  • A 📖 note above each field (FIELD_NOTES, ids 1459-1464): what it does, who reads it and when, and an example in a fenced block so the lines keep their shape. A note is a member of its field’s frame (rgthree’s centre rule), so it dims with the frame: the manual prompt’s note is grey until 🧠0 is chosen, which is the truth. layout.py sorts a note just before its field (h3_for).
  • Band 1 keeps one screen: the brief and its note are the one big text box on screen 1 (🧠1 is 1,170 × 890 units; row 2 ends at x = 2,520 of the 2,581 usable). The manual prompt moved from under the start card to the first frame of band 2, and the two guideline fields left 🧠1: ✅ Fidelity Rules is its own frame one screen down (no longer nested) and 👁 Caption rules is a new frame beside it, with ✅ Continuity Editor and ✅ Passion completing a row of set-once texts. Functionally nothing moved: both fields feed the 🧠 Vision engine instance, and it is the instance the 🧠 radio mutes; the ✅ Toggles rows still bypass them. apply_combo mirrors it (RULES, VLM_PRIV, MANUAL).
  • The cards cap (test_removed_nodes_are_gone) is 14: seven section notes and six field notes.

17.6 V6.0.2 — the canvas kit moves to ComfyUI Base (2026-09-05)

Paul: “whatever can be used to test the other workflows that isn’t unique to this MiniMax H3 workflow should be part of the overall testing ComfyUI workflow.” Everything in §16-§17 that knows no H3 id now lives in the base, V1.1.0, and this package is its first consumer:

  • base/comfyui-base/py/canvas.py — the graph helpers (byid through subgraphs, links, the frontend’s bypass resolver of §16, nbound/gb/contains, rgthree’s membership rule), the size floor, the structure hash, note_height, the corpus budgets (Budgets) and the declaration (CanvasSpec).
  • base/comfyui-base/py/canvastest.py — the 41 generic tests: the uiux budgets, the snapshot tier and the shape-only config guards. suite.py declares CANVAS = CanvasSpec(...) (reading order, markers, the CUDA-only pair, the start card, the display, the nest/partial pairs, tier gates) and star-imports them; its own 132 tests are what is H3’s (radios, folds, flows, the model library).
  • base/comfyui-base/_build/canvas/ — snapshot.py, render.py, page.py, panels.py, probe.py and canvaslayout.py (the layout engine), every one taking --pkg <package dir> and reading the same declaration by ast. _build/layout.py here is now 160 lines of tables (the COCKPIT tree, ADOPT, STRIPS, the fold and panel homes) ending in run(globals()); the engine reproduced this canvas byte for byte through the move. _build/restructure.py, _build/sizes.json and _build/snapshots/ stay here: they are this graph’s.
  • Suite: 173 collected (132 own + 41 generic), 13 tiers. Rule 6d’s picture is unchanged; only the commands moved (they are the base’s now, and the same commands photograph any package).

Adoption by the other packages (Krea 2, Replacer, Flux 2 Dev, Klein, Continuer) is one short session each: declare a CANVAS, run the base’s snapshot.py once for a sizes.json, fix what the budgets find.

17.7 V6.0.2 on the pod (2026-09-06)

The first acceptance run on ComfyUI Base V2 (§11.10, sixth run): step one and step two green on a HearmemanAI pod, every automated item of §11.10 met, the Prompt Rewriter pinned back to its tested commit (defect 42), and item 9 proven by a reboot with nothing re-run (the base’s boot.sh boots the pod; ComfyUI up from the base venv in 68 s). What is left is Paul’s: the renders of items 4-8 and 11.

17.8 V6.0.3 — three stored widget orders (2026-09-06)

The first queue from the browser on the pod was refused (defect 42): node 1199 carried widgets_values in the order of an older pack version. The list is in the server’s order now, checked by base 2.0.13’s positional pass; the Prompt Rewriter was pinned back at a59888df until its 0.20.0 HEAD was tested green the same day (defect 42) and the pin moved. Nothing else changed.

17.9 V6.0.4 — on ComfyUI Base 2.0.14 (2026-09-06)

The base’s sweep after the sixth run (base Handbook §10, 2.0.14). Here: BASE_MIN rises to 2.0.14 (the suite uses basetest.parse_foreign_source, and the one-command path podctl install is what runs this package now); the suite’s 12 “warnings” — Python’s SyntaxWarnings about the PACKS’ source it parses — are accounted for by a test that fails the day one is ours. Graph unchanged but for the version. Suite: 175 collected (133 own + 42 generic), 13 tiers.

17.10 V6.0.5 — node source means node classes (2026-09-06)

Inside the base’s rehearsal (three fake pod layouts) this package’s comfyui tier ran against the fake pod’s clones — no Python in them — and failed three tests per run, masked until base 2.0.15 made a red suite a failed run. _node_src_root now accepts a tree only when it carries node classes (nodes.py, or clones declaring NODE_CLASS_MAPPINGS); a fake tree is “no node source” and the tier skips, saying so. Suite: 176 collected (134 own + 42 generic), 13 tiers.

18. V7.0.0 — one checkpoint, one template, one toggle (2026-09-07)

The rebuild the successor plan (successor-plan.md) specifies. Five things changed at once, and each is a deletion as much as an addition.

One checkpoint. The three baked DaSiWa grades are gone. 940 is a stock UNETLoader holding minimax_h3_ref2va_pruned_int8_convrot and the tier never picks a file again — because the tier never needed to: MiniMaxH3AddGuide already pins frames, which is the merge problem those three grades were solving. Five rows carried a unet name, not three: 1084 ⚡, 1085 💎, 1418 🏆 and — easily missed — 1341 🎧2 and 1419 🎧3, because design rule 14 gives the 🎧 rows their own copy of every sampling switch.

The freed bus slot was retyped, not swapped. Collapsing the checkpoints frees bus output 5, but it frees as a COMBO and the returning turbo LoRA needs a FLOAT — and slot 2, the FLOAT that came free when the LoRA left in V5.8, is now 788.shift_audio and cannot be given back. So the slot changes type, the five row primitives become PrimitiveFloat in place (which keeps every link they already had), and 1086 is the 🎚 turbo strength switch: [0, 0, 1, 1, 1] in rule 14’s order. 980 is back on the bus and stays live — LoraLoaderModelOnly.load_lora returns before it opens the file at strength 0, so 🏆 never resolves the path.

One template. The public/private build split is deleted — no generator script, no divergence test, one JSON. (V7 also added a toggle that swapped the models for an alternative set; V8.1.0 removed it, because SoulCraft is SFW only with no switch that turns anything else on, §21.)

Sol-Attn on every tier. It moved out of 🎚️1 into ⚙️ Processing, beside 815, its own upstream. Twelve widgets are now asserted by name; five changed. tau was 0.1 against the pack’s default of 1.3 — whose own tooltip reads “higher is sparser: 1.0 ≈ 16% of blocks kept exact” — so Sol-Attn was running near-dense and the 1.64× it is adopted for could never have appeared. int8_qk shipped true: an INT8 QK path on H3’s DiT, which is the hazard class rule 5 bans the global Sage flag for, never once run on a GPU here. Both are off now, and int8_pv with them, so the rule reads as one sentence rather than a rule plus a dormant exception.

✅ Passion is deleted and the tiers are renamed on both radios — ⚡ Draft · 💎 Deliver · 🏆 Master and 🎧 As generated · Deliver · Master. Renaming one and not the other would have split the vocabulary across a pair the higher-of-two rule binds together.

18.1 The library

55.19 GB, from 110.88. One engine (20.97), the ref2v 8-step turbo back on the bus (1.96), a stock NVFP4 encoder (15.69) with the instruct NVFP4 tail (5.40), Kijai’s int8-convrot video VAE (3.17), and the rest unchanged. §0 rule 16 is new and is why the encoder and tail move together: they share a lineage and a precision.

18.1a D19 — why the radio rows are still not disjoint, and what closed instead

The plan scheduled disjoint rows in this phase: give the higher row its own copy of every node it shares with the row below, then merge the copies with an rgthree Any Switch at each consumer. Measured, that remedy does not work here, and the measurement is worth keeping because it will be proposed again.

The merge relies on the bypassed copy resolving to nothing, so the switch falls through to the live row. The frontend’s bypass model instead resolves a bypassed node through its own slots: where an output type also appears among the inputs, the input is passed straight through. Over the twelve shared nodes, eight are pass-throughs — both analyzer taps, the RTX pass, the caption-last-frame node, Motion Context, both auto-trims, and the FLAC save all carry a type on both sides. A bypassed copy of any of them emits its input, the Any Switch takes that first, and the merge silently selects the dead row. Only 🎚️ shared settings, 🎚️ H3 2× pass, Chain · 🔗1 and Chain · 🔗2 are safe to duplicate, and splitting only those leaves the rows still sharing everything else — more nodes, no independence.

The premise had also moved. The reason to want the split was “🏆 cannot be given its own H3 2× setting, because there is only one” — but the tier bus now carries every tier-varying setting (steps, ref size, both shifts, sampler, megapixels and turbo strength) to the sampler, the Ref2VA node and the SETTINGS builder, and the shared stages take no per-tier setting at all: 🎚️ H3 2× pass’s inputs are vae/latent/audio/model/seed/conditioning — data, not settings.

So what is left of D19 is the click, and that is what the pack’s tier widget fixes by setting all three group modes atomically. Rule 1’s workaround paragraph therefore stays (the plan expected to delete it), and §17.3’s 🟡 usability entry stays with it. One duplication had been made before this was measured — 1473, a second FLAC master for 🎧3 — and it has been removed: 1343 stayed in the shared cell, so selecting 🎧3 wrote the same master twice. That is the shape of this whole class of change, and the reason to measure first.

18.1b App Mode — what it is, what it is not, and the one control it cannot have

extra.linearData gives the workflow a form view: open the JSON, fill in a brief, drop in pictures, press Run, never see the graph. It is a separate view in the frontend — switch it with the Graph/App control in the top-left — so it changes nothing about the canvas. A canvas that looks like a canvas is not evidence that App Mode is missing.

The flag, not just the fields. linearData is only the form’s contents; extra.linearMode is what makes the workflow open in the app view. Through V6 we shipped the first and not the second, so all 17 fields existed and were never seen — the file opened on the canvas exactly as before. That is a one-boolean defect that four versions of “App Mode is done” hid, and test_comfyui_app_mode_every_input_resolves_to_a_live_widget now asserts the flag as well as the fields.

Two measured facts about frontend 1.49.6, both from loading the real thing rather than reading the schema. First, the tuple is (nodeId, inputName) with an optional passthrough config — not (widgetId, label), which is where our labels had been sitting; slot 1 is zNodeInputName. Second, the app panel labels every field by its widget name, so the form reads value, image, image … and a label in the config object is ignored. The labels are carried in that config anyway, for a frontend that honours them. Making the form read 🎯 Brief — what happens? needs the host to be a node whose widget is named that, which is our own pack’s SoulCraftBrief — so nicer labels arrive with the pack, alongside the tier selector, and not before.

A red “N errors found” banner on the testbed is expected and is not a graph defect. SolAttnPatch only registers on a machine with an NVIDIA GPU, so node 775 comes back with class_type: null and POST /prompt answers missing_node_type (§12.7). V7 put Sol-Attn on every tier, so this now hits every state rather than the ⚡ one — the plan predicted exactly that. The same graph is accepted and renders on the pod.

The form’s design, read off AppModeWidgetList.vue rather than guessed (V7.0.1). Four facts decide everything a field can look like in frontend 1.49.6, and the form is built to them:

  • the label is widget.label || widget.name, drawn on one truncated line — so it lives on the host node’s input slot (label), which is where the frontend’s own Rename command writes it;
  • the description is one line, h-5, truncated at roughly forty characters. It is a hint, not a sentence: “One line per heading. The 32B tu…” was the first draft, cut mid-word. An empty description hides the line entirely (v-if="description || builderMode"), which is cleaner than filler;
  • height in the config is honoured as the textarea height. Without it the brief showed four lines of a seven-heading template, so the person could not see what they were meant to fill in; it is 240 px now, the three set-once rules 120;
  • the node title is drawn only in builder mode, so it never doubles the label for a user; outputs render by media type, and isText is one of them, so the analyzer report shows in the results panel.

Eight fields, not seventeen. IMG 2-9 left the form for a reason that took a render to find: they ship bypassed (an empty LoadImage that runs resolves to the input directory and dies), and App Mode draws only a live node’s widgets — a bypassed host’s field is declared, resolved, kept by pruneLinearData, and simply absent from the panel. Eight invisible entries help nobody. IMG 1 stays, required, and ships as PUT-YOUR-PHOTO-HERE.png rather than empty, so a forgotten photo fails at validation with a message naming the field instead of mid-run with Is a directory. More references are added in the graph view through ✅ Reference Toggles until the pack’s media loader gives the form a real multi-slot control.

Two facts about the panel, measured on the pod (2026-09-07). The placeholder does better than designed: with IMG 1 still PUT-YOUR-PHOTO-HERE.png the panel shows “Media input missing — Load Image is missing a required media file” before Run is pressed, so a forgotten photo is named in the form itself rather than at /prompt. And the output panel is per browser session: a fresh tab that opens the workflow sees none of the earlier runs, even though /history still holds them — results appear only in the session that pressed Run. Any check of “the video shows up in the form” has to fill, run, wait and look inside one session; _build/snapshots/appmode-session-output.png is that capture.

What the form covers, and what it cannot. It drives the shipped default — 🎬 Ref2VA, ⚡ Draft, 🎧 As generated, 🔗 New scene — end to end, and the configurer fills every setting the panel does not expose. It cannot switch the 🎬 / 🎚️ / 🎧 / 🔗 rows: those are rgthree group panels, which build their rows in JS and serialise widgets_values: null, so no WidgetId can ever name them. The other four 🎬 rows and clip chaining are graph-view flows until the pack ships its tier widget.

It ships 8 inputs and 3 outputs, in reading order, because App Mode has no conditional visibility and ordering is therefore the only disclosure mechanism it has: the brief, then IMG 1-9, then the three things you set per run (length, scene number, seed), then the four you set once.

This is now tested, and it was not before. test_comfyui_app_mode_every_input_resolves_to_a_live_widget checks every WidgetId against the frontend’s own parser (/^(?<graphId>[^:]+):(?<nodeId>[^:]+):(?<name>[^:]+)$/u), that the graphId is the root graph’s, that each host is a root node, and that each widget name exists on that node’s type in the node source. That matters more than it sounds: pruneLinearData drops entries it cannot resolve silently on load, so a wrong id does not raise — the field just disappears and the form quietly gets shorter. Nothing else in the suite reads linearData, so the entire app view could have gone missing between releases with every other tier still green.

The one control the form cannot have is the quality tier — and the reason is structural. 🎚️, 🎧, 🔗 and 🎬 are rgthree Fast Groups panels, and a panel builds its rows in JS at runtime: its widgets_values serialises as null. A WidgetId needs a widget name that exists at load time, so a panel row can never be an app input. Giving the form a tier selector needs a real node with a real widget — which is exactly what the pack’s SoulCraftTier is for, and so it arrives with the pack (see 18.1a’s blocker). Until then the form ships on the default tier, which is the right behaviour for a front door anyway: north-star point 5 is to remove the guess work, not to add a decision.

18.1c V7.0.1 — NVFP4 produces NaN, and the first real render is what found it

Measured on the pod, 2026-09-07, RTX PRO 6000 Blackwell / ComfyUI 0.34.5. V7.0.0 shipped the NVFP4-AWQ encoder and instruct tail. Queued for real, H3QwenVLGenerateText stopped at the first generated token:

H3 text generation produced NaN or Inf at generated token 1 in base language layers 0-49 (overlay=none). Sampling was stopped before torch.multinomial to avoid a fatal CUDA device-side assertion. Verify that the base encoder and generation tail use the same format.

The pack’s advice does not apply here: both halves were NVFP4-AWQ, so §0 rule 16 held and the pairing was not the fault. Layers 0-49 are the encoder’s own range — the tail owns 50-63 — so the NaN is produced before the tail is reached at all. Same brief, same seed, one variable changed to the int8-convrot pair: status=success, video and frames written.

V7.0.1 therefore ships int8-convrot on both halves: qwen3vl_32b_minimax_h3_int8_convrot (27,141,342,152 B, Comfy-Org) with qwen3vl_32b_h3_instruct_generation_tail_50_63_int8_convrot (7,609,128,659 B, ethanfel). Still one lineage and one precision, so rule 16 is unchanged — only the precision it lands on. The library goes 55.19 → 68.85 GB; NVFP4’s 15.69 GB encoder was the smallest of the three and it does not work here.

Why no test could have caught it. Every structural fact was right: both files present, both the same format, the graph correct, 216 tests green. NVFP4 numerics are a property of the weights meeting this GPU, and only a render sees them. This is the third defect in this package found exclusively by rendering — after 43 (the captioner with nothing to caption) and the empty-slot crash below.

18.1d V7.0.2 — the first 💎 Deliver render, and three defects older than V7

Every render before 2026-09-07 was ⚡ Draft, where the whole post chain is pruned. The first 💎 Deliver prompt was rejected at validation with four node_errors that blocked the analyzer, both post-stage saves and the report file — so a Deliver “success” would have produced the base video and nothing else. All three causes are present, unchanged, in V6.0.6:

  1. A cycle through the RIFE fold whenever it is bypassed. 1448.images ← 902 (📏 Post-RIFE Count) while 902.image ← 1448. images is input index 0 — the slot the bypass contract passes through — so with RIFE off the frontend resolves 902.image → 902 and ComfyUI reports dependency_cycle. With RIFE on (🏆) the fold’s inner graph never consumes images, so this one did not block Master — defects 2 and 3 did, and the sweep showed every 🏆 state failing on them. Fix: both of the fold’s IMAGE inputs take the pre-RIFE frames from 901, as frames already did. Guarded by test_v702_no_node_resolves_to_itself_under_any_preset, which asks the suite’s own resolver the question nobody had asked it — test_v702_the_resolved_prompt_has_no_cycle_under_any_preset builds the prompt the frontend would post and DFSes it, the server’s own check. It is red on V6.0.6 and green here.
  2. The 320 kbps VHS format was keyed wrong. VideoHelperSuite keys external formats by the full filename, extension included (get_video_formats() iterates get_filename_list("VHS_video_formats")), so the file the installer writes is offered as video/h3-nvenc-h264-mp4.json. The widgets said …-mp4.
  3. The H3 2× upscaler could not see its model. The pack’s scan_models() globs *.safetensors at the root of latent_upscale_models/ and returns basenames, so the file the library convention put under MiniMax-H3/ did not exist to it — the server offered only a stray V6 _fp16 at the root. The base has a list of freeform categories for exactly this shape of pack (SAM3, SeedVR2, RMBG os.listdir their folders); base 2.0.37 adds latent_upscale_models to it, so the row carries no Family, the file lands at the folder root, the widget names it bare, and the stray V6 fp16 is superseded.

Why no test saw them. All three are facts about what a pack or the server does with a value — a key format, a glob, a cycle only the resolver can see — and the installer’s combo check exempts .safetensors values. The guard that catches this whole class in seconds is a validation sweep: every 🎬 row × 🎚️ tier posted to a GPU server and deleted from its queue unrun, asserting zero node_errors. That is _build/validate_states.py, run by test_server_every_mode_and_tier_validates when the server registers SolAttnPatch (the local testbed cannot; the pod can).

--dump <dir> keeps each accepted prompt as prompt-<slug>.json (ref2va-t2, fl2va-t3-c1), the file base/comfyui-base/_build/pod/render.py --prompt takes — so the state that validated is the state that renders, and _build/snapshots/probe/states.json, which still names V6 node ids, is not the route to a V7 💎/🏆 prompt.

18.2 What is now shipped beside the workflow

profiles.yaml (four profiles since V8.1.0), App Mode’s extra.linearData — 8 inputs in V7, 36 in V8 (§19.1) — LICENCE_MODE emitting the NOTICE and modified-file notices mechanically, a Dockerfile carrying no weights and no packs, the RunPod Secure Cloud template, the CivitAI listing, and our own pack — renamed and rebuilt in V8 as ComfyUI-H3-App-Controls, publisher Generate-AI, vendored into the package (§19.2); the media tray, brief compiler, tier widget and resource-metadata nodes this paragraph once promised are gone with the SoulCraft name.

Eight packs in V7, not the nine the plan projected. Two corrections, both found by the suite rather than by reading: ComfyUI_MusicTools is still a pack — its LUFS normaliser and audio repair are used from it and the vendoring the plan called for has not happened — and ComfyUI-MiniMaxH3-AdaptiveCache has been taken back out, because test_comfyui_packs_are_exactly_what_the_graph_needs is right that a pack installing no node the graph uses is dead weight. It returns with its node, pinned at 868ecc5e, once a pod can name its classes.

Fun ControlNet is vendored and built, but withheld from the shipped graph — and AdaptiveCache is out for the identical reason. Both were gated on one thing: the pack had no published git URL (V8 vendors the pack into the package instead, so the remaining gate on Fun ControlNet is only rule 15: its 2.30 GB controlnet has no consumer yet). Every PACKS row is dir|git-url|sha||what, so until our own pack is a repo a user’s pod can clone, its node types do not exist there — the graph would name classes nobody can install and the download would be read by nothing. test_comfyui_packs_are_exactly_what_the_graph_needs and rule 15 both say so, and they are right. The row is one constant away: FUNCONTROL_SHIPS = True in _build/restructure.py, in the same commit that publishes the pack and adds its PACKS row. What follows describes the vendoring, which is done.

The vendoring itself. wyzborrero/ComfyUI-H3-FunControl is Apache-2.0; its control.py and the H3FunControlLoader / H3FunControlApply pair are vendored into our pack verbatim, with the licence at LICENSES/ComfyUI-H3-FunControl.LICENSE. Vendored rather than reimplemented because its README documents eight measured silent-failure traps. Reading the source corrected the plan twice: the loader reads folder_paths.get_filename_list("controlnet"), so the file lands in controlnet/, not the model_patches/ Comfy-Org publishes it under; and it raises on the full-width AdaLN release rather than loading something subtly wrong, so the curve-form requirement is enforced by the node, not by a note. The 🎛 CONTROL row ships bypassed, and needs no merge switch: Apply takes MODEL and returns MODEL, so bypassed the frontend passes the model straight through — the very pass-through that made §18.1a’s merges impossible is the right behaviour here.

AdaptiveCache is the one P3 item still out. It is pinned at 868ecc5e in this handbook but not in PACKS, because a pack that installs no node the graph uses fails test_comfyui_packs_are_exactly_what_the_graph_needs — the same rule, one level up, that would have caught the FunControl download. Wiring it needs its class names from a first-party source, and the pack could not be fetched (github.com is off this session’s network allow-list) nor read from the pod, where our packs are not yet installed. It goes in the moment either is available.

17.12 V6.0.7 — the first render, and the two defects it found (2026-09-06)

The first time this workflow was ever driven to completion on a pod it failed twice, and neither failure was visible to the 202-test suite. Both are runtime behaviour of a pack rather than facts about the graph, which is the one class of defect a green suite cannot speak to. They are also why §11.10’s render items (4-8, 11) stayed outstanding for four versions: the workflow could not complete a render.

Defect 43 — every render died at the captioner. MiniMaxH3MultiReferenceCaption (1199, inside the 🧠 Vision engine) raised “Nothing to describe. Connect a reference to one of the four groups…” on every 🎬 row and every 🎚️ tier. Its only source is 1047, the 🔀 VID1: auto ▸ manual Any Switch, whose two inputs are the 🔗 chain path and the VID 1 loader — both dead with 🔗0 New scene and the VID slots shipped bypassed, which is the shipped state. The pack raises only when nothing is connected; it handles “connected but switched off” gracefully. Its own bypass widget returns the block unchanged with empty captions, which is the right default when no VID or AUDIO slot is in use. Switch it off when you turn one on. Reproduced on 🎬 T2VA (115 s, models cold) and on the shipped 🎬 Ref2VA ⚡ default (0.27 s, warm).

Defect 44 — the report save killed a finished render. SaveStringKJ (1322) wrote to output/MiniMaxH3/<date>/MiniMaxH3/<date>/H3Report_… and died on the missing directory — after 514 seconds of sampling had produced the video. This one is upstream: KJNodes’ save_string builds its filename from filename_prefix (directories included) and then joins it onto full_output_folder, which get_save_image_path has already pointed at those same directories; it should use filename. Any prefix containing a subdirectory doubles. The node resolves its output_folder widget as a subpath of output/ and validates it stays inside, so the folder moves there and the prefix stays flat. Reports now land in output/MiniMaxH3/, beside the dated folders rather than inside them — the one exemption to design rule “outputs land in dated folders”, recorded in test_e2e_output_files_land_in_dated_folders with its reason.

What the suite gains. Being structurally right is not the same as being runnable. The frontend port is exact — proved 0-divergent against graphToPrompt() in §16 — but nothing asserted that a node which raises on empty input actually receives one. That assertion belongs across the whole graph, not on these two nodes.

How this was found, and how to repeat it. base/comfyui-base/_build/pod/render.py takes an API prompt from canvas/probe.py, applies widget overrides by node id, queues it on the pod over ssh, polls /history and brings the frames back, timing per §11. Dump the prompts from the pod, never the local testbed: the testbed has no NVIDIA GPU, so SolAttnPatch never registers there (§12.7) and graphToPrompt() emits node 775 with class_type: null and UNKNOWN_n widget names — a prompt the pod refuses with “Node not found”, which reads like a missing pack and is nothing of the kind.

17.11 V6.0.6 — the package is MiniMax H3 Video Creator (2026-09-06)

A rename, nothing else. The folder and its four files were MiniMax H3 Create Video; they are MiniMax H3 Video Creator now, which is the house nomenclature — model name · model number · Video/Image/Audio/Character · action — already carried by the two siblings, H3 Video Continuer and H3 Video Replacer. git mv kept the history.

Every live reference moved with it: this handbook, the script (PKG_NAME, WF_NAME, the header and the first-render block), suite.py, the workflow’s title label, _build/layout.py and _build/restructure.py, the base’s handbook and canvas kit, both siblings’ handbooks/scripts/suites (their shared-dest contract tests name this package), README.md and .claude/launch.json. ComfyUI’s own CreateVideo node class is a different thing and was left alone.

Two things deliberately keep the old name, because they are records rather than references: _build/podruns/**, the logs of runs that really did happen under the old name, and _build/snapshots/*, whose absolute paths are the provenance of a photograph taken before the rename (nothing reads those paths back; the tools locate the file from --pkg).

The title label is in the graph’s structure hash, so _build/sizes.json was re-keyed to match and one layout.py run settled the pinned Label (rgthree) 749 at 460 px — one character wider title, one node, no other geometry moved. The hash was re-keyed by hand, not by a fresh photograph — the canvas has not been re-photographed since the rename. Do that (snapshot.py --pkg "video-creator-minimax-h3" against the testbed, base Handbook §17.6) at the next convenient point; it must run on the display the canvas is designed for, 1512x982@2, since what it records is that frontend’s text metrics.

18.3 Where V7.0.2 stands at the end of 2026-09-07

Locally: complete. 221 passed / 0 failed with the server tiers on; the zip matches the shipped files; the base is 2.0.37 (205 / 0) and the Continuer, caught by the same freeform rule, is 1.0.1 (29 / 0). App Mode was proven in the real frontend in one browser session — brief typed, IMG 1 uploaded, Run pressed, and the session’s own results panel showed H3Base__00011-audio.mp4 after 506 s (_build/snapshots/appmode-session-output.png).

On the pod: half. Base 2.0.37 is installed and green there (143 / 0). The V7.0.2 package step has not run — the pod’s ledger still reads 7.0.1 on base 2.0.35 and the bf16 upscaler still sits under a Family folder the pack cannot see, so 💎 Deliver and 🏆 Master are still unrenderable on the pod until it does. It was queued behind another package’s install on the same pod when the session was wound down. The order for the next session is in successor-plan.md (closing section): the package step, then _build/pod-renders.sh — the sweep with prompt dumps and the six renders that decide the audio finding and D33.

19. V8.0.0 — the App Mode front door (2026-09-07)

Paul’s ask, on the evening of 2026-09-07: start using H3 through App Mode — a flow selector, a scene-chain control, Photos/Videos/Audio counts after which that many upload rows appear, a character LoRA, a manual-or-assisted prompt with one field, extra LoRA rows. The plan is ~/.claude/plans/i-would-like-to-calm-book.md (thirteen decisions, all Paul’s or measured); this section is the record of what was built.

19.1 The form

Thirty-six rows, in this order — reading order is App Mode’s only disclosure, plus the pack’s JS lighting rows from the counts. Every label lives on the host’s converted-widget input slot (app_labels), the one-line help in the tuple’s description (≤ 40 characters, asserted), the long help in each pack widget’s tooltip (drawn on hover).

rows host control
1–4 1487 🎛️ Setup 🎬 Flow (Reference · First frame · Last frame · First + last · Text only) · 🎚️ Quality (⚡ 💎 🏆; 🎧 follows) · 🔗 Scene (New · last frame · + motion carry-over · + tail video → Video 1)
5 1108 🎬 Scene #
6–15 1487 + 741…920 🖼️ Photos (0–9) and Photo 1 (the face) … Photo 9 — rows 2–9 appear with the count
16–23 1487 + 746/922/923 + 743/744/924 🎞️ Videos (0–3) and Video 1–3; 🎵 Audio clips (0–3) and Audio 1–3
24–26 1487 + 1489 🧬 Character reference (toggle), then 🧬 Character (a LoRA under loras/MiniMax-H3/Characters/, — none — first) and its strength
27–28 1488 ✍️ Prompt 🧠 AI-assisted prompt (toggle) · ✍️ Prompt (200 px; ships empty, the seven headings in the tooltip — decision 4)
29–34 1490–1492 🧩 LoRA 1–3 (— none — = off, any file under loras/) and strengths
35–36 34, 1235 ⏱ Video length · 🎲 Seed

Outputs unchanged: 757 the video, 1316 the analyzer report, 1322 the report file. The three set-once rule texts (1089, 1194, 1098) left the form (decision 3): they are the local LLMs’ system prompts, package-authored, and a person has no reason to edit them per run. The setup node validates impossible states at /prompt with a message naming the fix — First/Last frame with no photo, First + last with fewer than two, Text only with any reference or a chain, Reference with nothing at all under New scene, the tail video with a counted clip (it takes the Video 1 slot).

19.2 The pack, and the measured facts it stands on

ComfyUI-H3-App-Controls/ (MIT, publisher Generate-AI, category MiniMax H3/App controls, vendored beside the script and copied into custom_nodes by pkg_post_venv before the base’s import check; the repo-root ComfyUI-SoulCraft-H3 was git mv’d here and no SoulCraft string survives). Four nodes: H3AppSetup, H3AppPrompt, H3AppLoRA, H3AppCharacterLoRA (a LoRA row with a real — none —: the model passes through, no placeholder file). Fun ControlNet stays vendored and unshipped.

One table. web/controls.json says, per control value, which group-title prefixes go live and which go off: a radio family (flow, quality, scene) lists every row and the chosen one; a toggle lists what it lights; a count names a prefix and a maximum. h3_app_controls/controls.py reads it from Python — _build/restructure.py ships the modes the table gives at the defaults, suite.py models every state from it, _build/validate_states.py writes the real widgets and lets the JS apply it — and web/h3_app_controls.js reads the same file in the browser. The two prefix_modes functions are line-for-line equal on purpose: OFF entries first, ON last.

Three frontend facts — two measured before anything was built (T0, the local testbed, frontend 1.49.6; the App Mode sources are byte-identical in 1.51.9), one found by the first live run. (a) The App panel draws only a live host’s rows, and the node.mode !== ALWAYS gate in AppModeWidgetList.vue is not reactive — setting a bypassed host live changes nothing on screen; but app.rootGraph.trigger('node:slot-label:changed', {nodeId, slotType: 1}) (or events.dispatch) re-runs useResolvedSelectedInputs and the row appears with its drop-zone tile (8 → 9 → 8 rows in the spike). That event is what apply() fires after setting modes: “Photos: 3” shows three drop-zones. (b) The form calls the live litegraph widget.callback(value, canvas, node) on every edit, so a callback wrapped in onNodeCreated sees form edits (node.onWidgetChanged does not fire on that path). Pinia stores are not exposed to extensions; nothing here touches one. afterConfigureGraph applies the table on load, so a saved file is normalised to its own widget values. (c) Group membership in afterConfigureGraph must come from pos/size, never from group.recomputeInsideNodes(): that method reads each node’s draw-time boundingRect, which is [0,0,0,0] until the first frame is painted, so every node’s centre sits at the origin and a frame that contains the origin (🖼️ IMG 1 lives at [0,0]) claims the whole graph. The first live run lit all 36 rows that way — every offline test was green, because the suite and rgthree use pos/size. membersOf in the JS is now canvas.py’s nbound/center_in (node box including the title bar, centre inside, inclusive edges), line-for-line.

Two facts about the tools, found while building. The layout engine births a new frame at [0, 0] before it measures membership, and IMG 1’s frame sits at the origin — so the character frame is created by restructure.py, parked with its node, never by NEW_GROUPS. And snapshot.py decided “missing node type” from has_errors, which the frontend also raises from its missing-model and missing-media scans (useNodeErrorFlagSync.ts): a placeholder photo wore the same red halo as an uninstalled pack. It now reads last_serialization, which only a missing-type placeholder carries (base record, 2.0.37 tools-only entry).

19.3 The graph

restructure.py’s v8_front_door (one idempotent pass keyed on 1487) and v8_controls; every id pinned, every link id-keyed:

  • Eight rgthree panels deleted (990, 1010, 1131, 1338, 1184, 1458, 747, 1384) and the 18 single-input cut switches 1366–1383 with them: every loader now feeds node 13’s optional autogrow slots, the prompt check’s references.ref_k, and 1354 directly (decision 10). ✅ Toggles and 🔧 Advanced stay.
  • One prompt. 1488 feeds 1435.string_a (the brief) and 1033.any_02 (the manual path); 16, 1035 and their 📖 notes are gone; 1032 and 1434.string_b_3 read 1033’s output, so the readout and the report show the prompt that actually reached the encoder in either mode; 1032/1033 moved out of the muted frame (rule 7).
  • Photos = 0 is a state. 741 and its Vision Prep 1409 are frame members like every other slot. 1494 Any Switch [ladder ▸ blank 1493] feeds 1431.image2 and 1434.image (both required inside); 1495 MiniMaxH3OptionalReferenceImage, enabled by 1487’s has_photos, hands the 32B None instead of a black square. The T2VA gates subgraph 1437 (+1421–1429) is redundant and gone. 1120’s fallback canvas is 1344×768 (decision 7). Core’s ComfySoftSwitchNode would have been the obvious gate; it sits in nodes_logic.py but the 0.34.x server never registers it (# SoftSwitchNode, in the extension’s list), so the graph’s own §0 rule 3 idiom carries it.
  • The Omni captioner wakes with the counts. 1199’s bypass is a promoted input of 🧠 Vision engine, linked from 1487’s captions_off (= no clip and no audio counted), which is what defect 43 asked for.
  • LoRA rows with a real none. 1143 → 1489 (character, bypassed until the toggle) → 1490 → 1491 → 1492 → 815; the PUT-YOUR-LORA-HERE placeholder file and its PLACEHOLDERS row are gone.
  • The captioner’s name. 1498 (the stock label) through 1497 into the model widget-input of 1064 and the promoted model inputs of 1199 (🧠) and 1295 (✅ Analyzer). (V8.0 also routed an alternative captioner through 1497; removed in V8.1.0, §21.)
  • The L2VA rider 1176 said “IMG 2” while the row pins IMG 1: fixed by text.
  • The validation tap: 741 → 1487.photo_1. Measured on the pod (ComfyUI 0.34.6): a node whose only consumer is an rgthree Any Switch is never validated — FlexibleOptionalInputType declares no input names, so the server’s validator never recurses into it. Photo 1 reaches the graph only through the 🔗 source switch 1048, so a placeholder passed /prompt and died at execution. The setup node’s optional photo_1 input is a node the validator does visit; through it LoadImage 741 is checked and a placeholder is refused before the run, naming the file.

19.4 Installer

VENDORED_PACKS, ZIP_EXTRA naming every pack file plus h3_install.py, h3-nvenc-h264-mp4.json (the VHS format, a shipped file now, not a heredoc) and profiles.yaml; pkg_pre_models creates loras/MiniMax-H3/Characters and Custom; pkg_post_models runs h3_install.py models-json over the rewriter’s models.json (the old 27B-writer removal; V8.0 also registered a captioner there and a package command fetched an alternative model set, both removed in V8.1.0, §21). PLACEHOLDERS = the three PUT-YOUR-… media names. The script is 241 lines (cap 250).

19.5 The suite

213 test functions, 14 tiers (app is new). apply_combo no longer writes modes by hand: it turns a state into setup-node values and applies the pack’s table over the centre rule, so the 768-state sweep and every flow test model exactly what the JS does. New: the table’s prefixes each match one frame (byte-exact emoji), slot frames hold loader + prep, the shipped modes equal the table at the defaults, the form’s rows are the declared rows in the declared order with labels ≤ 28 and help ≤ 40, hidden hosts are exactly the rows beyond the counts, manual mode runs no LLM before the sampler (the Analyzer’s own judge follows its toggle), Text only reaches the engine with no reference, counts light exactly their slots and wake the captioner, the character toggle lights one frame, each continue level lights the chain up to it, the setup node refuses impossible flows, — none — is a pass-through, the vendored pack ships whole and SoulCraft-free, and — app_live, against the testbed — the real panel opens on the flow row, shows no raw widget names, and grows two photo rows when Photos goes 1 → 3.

19.6 Verification

Local (the testbed, frontend 1.49.6, no GPU). The one test phase began with the whole build done: the first run gave 17 failures, all stale expectations from the V8 rewiring plus three tooling gaps (the schema scanner did not follow the testbed’s symlinked pack; the LoRA unit test needed comfy importable; a 32-hex “key” check matched a 40-hex pack sha). Fixed as one batch → 7 → 2 → green: 275 passed, 14 skipped (runpod/gpu need the pod; the server sweep needs SolAttnPatch). The one real defect the offline suite could never see came from the first live open: group.recomputeInsideNodes() read the draw-time bounding rects before anything was drawn and lit all 36 rows (§19.2 (c)); after the fix the real panel opens with the 20 declared rows and Photos 1 → 3 grows it to 22.

The pod (Generate-migration 5lgll2pfr93u3j, RTX PRO 6000, ComfyUI 0.34.6, 2026-09-08). podctl install --pkg … --no-latest — the pod’s pre-install --check runs the suite before the vendored pack is copied in, which the suite now treats as pending, not missing (§19.5); the install’s own run is green and the ledger records 8.0.0. Then, through the pod’s own frontend:

  • The state sweep (_build/validate_states.py, the pack’s JS doing the switching): 23/23 states validate clean — 5 flows × 3 tiers, the three continue levels, character, manual, a clip, an audio, and one more state V8.1.0 removed (§21).
  • The form, driven as a person would (_build/appe2e.py, the real rows and the real Run button): it opens in the App view (canvas 0×0) on 🎬 Flow with the 20 declared rows and no raw widget names; Photos 3 → 22 rows with three drop-zones; Videos 1 + Audio 1 → their two rows; 🧬 Character reference on → the two character rows, through the form’s own switch; Text only → the photo row goes. First frame with Photos 0 and Text only with a photo are refused before the run with the setup node’s own sentence naming the fix (ComfyUI repeats it once per input the validator names — its behaviour, not ours).
  • Finding, fixed: a photo row left on PUT-YOUR-PHOTO-HERE.png was accepted and queued. Cause measured on the server: a node whose only consumer is an rgthree Any Switch is never validated (§19.3, the validation tap). With 741 → 1487.photo_1 the same run is refused naming the file.
  • Tooling findings, fixed in the same batch: the sweep wrote assisted onto the setup node, read LoadAudio’s V3 combo as the string "COMBO", and took V7’s UPLOAD-YOUR-PHOTO-HERE.png for a real file; the driver’s toggle clicked the row’s tooltip button instead of its switch (so “character rows did not appear” was the driver, not the form); pod-renders.sh split the base’s path on its space.
  • Renders (pod-renders.sh, seed 424242, e2e_person.png, the seven-line brief): render A (Draft, Reference) wrote its video (H3Base__00001-audio.mp4, 18 min cold) but ended in error at the ✅ Analyzer’s Omni audio captioner (1434:1295): llama-mtmd-cli produced nothing for 180 s and the rewriter’s watchdog stopped it, while another session’s Krea 2 job shared the GPU. Contention or defect is undecided: the remaining five renders were paused (Paul, 2026-09-08) so the Krea 2 Character Creator and Photo Auditor work could finish; resume with pod-renders.sh on a free GPU, and read render B’s Analyzer first.
  • App Mode runs to a video in the panel (Reference Draft manual; Reference Deliver assisted with three photos; Text only; First / Last / First + last; Master; clip + audio; the three continue levels on scene 2; character — none —): pending — after the re-install that carries the tap.

Two commands, and the Mac goes first.

bash _build/build.sh is the whole build chain: restructure, --verify, snapshot, layout twice (the second must be quiet), restructure again (must find nothing to do), the canvas review, and package.py last. The handbook and the suite ship inside the zip, so any edit at all makes the zip stale and test_config_the_shipping_zip_is_the_shipped_pair fails for that reason alone; ending the chain with the zip is what stops that happening again.

bash _build/pass.sh local then runs, against the Mac testbed, everything that does not need a GPU — and that is nearly all of it. The sweep bypasses the two node types a Mac cannot register (SolAttnPatch, RTXVideoSuperResolution, both pass-through shaped) because a missing type makes /prompt answer missing_node_type and report nothing else; with them bypassed, every other input of all 23 states is validated here. That is exactly the check that would have caught the placeholder-photo defect without spending a pod cycle. The four row-count drives and the three negatives run here too, three browsers at a time, since a refused prompt never reaches a queue. --strict (used on the pod) turns any bypass back into a failure, so the pod still proves the real thing.

What the Mac cannot judge, and why (all measured 2026-09-08 on ComfyUI 0.34.6, and each one had to be learned before the local venue meant anything):

  • A missing node type short-circuits the whole prompt. /prompt answers missing_node_type with an empty node_errors — not one other input is checked. Hence the bypass above; without it a local run proves nothing at all.

  • A 200 can still carry node_errors. ComfyUI accepts a prompt when at least one output validates and reports the failed branches beside the prompt_id. The same refusal therefore reads 400 on the pod, where everything depends on the models, and 200 on a Mac, where the text-only branches survive. A negative is judged by its message, never by the status code.

  • The App panel will not submit at all while models are missing (“Resolve them before running the workflow”), so the button path for the three impossible states can only be proven on the pod. Locally the same three go straight to the validator (validate_states --negatives), which is the same judge; the pod then proves the wording reaches the panel a person is looking at.

  • The testbed serves whatever is in user/default/workflows, and appe2e opens the workflow by name from that list exactly as a person does. pass.sh local copies the current build in first: without it the whole local pass silently drives the previous version, which is precisely what happened the first time it ran (V7.0.2, every case reporting “NO ROW for 1487.flow”).

  • A label and a help line are cut to ONE line at 262 px, and the limit is pixels, not characters. “Reference · first · last · both · text” (38 characters) fits; a 37-character help line led by a wide emoji did not — and what it cut was the only actionable part, the command at its end. The ellipsis is drawn by a grandchild, div.truncate inside the row’s description element, and that child is 40 px narrower than its parent: measuring the row element reports nothing at all while three lines are visibly cut. Four help lines were over by 5 to 20 px and now read in full; test_app_live_nothing_is_clipped walks the descendants, which is the only way to see it, and appview.py --json reports the overrun per row.

Model files, real renders and anything CUDA remain the pod’s to prove.

bash _build/pass.sh (no argument) is the same list against the pod, and it now takes the base’s GPU lease (2.0.39) for the whole run and releases it on the way out, so a peer session waits instead of restarting ComfyUI under a render. It guards (refusing while another session’s work is on the pod — this pod is shared, and it never kills anything), installs once, repeats the validation-only checks against the real CUDA server, and only then spends the GPU, serially: the two identical-seed renders the audio A/B needs (RENDERS=all adds the other four) and the thirteen form drives, at Draft and 4 seconds wherever the tier is not the subject. Every case writes _build/podruns/<date>/cases/<tag>.txt and one line to FINDINGS.txt, so a cycle ends with the whole list rather than the first failure — which is what §2 of the repo’s rules asks for and what the V8 phase kept getting wrong.

20. V8.0.1: wording for ComfyUI Base 3.0.0 (2026-09-25)

Base 3.0.0 is always the newest: every run takes every pack to its remote’s HEAD, a row’s sha is the last-tested record, and --latest is a plain run. The script’s header and the quick start’s command list said --latest moved pinned packs to HEAD; both now say what 3.0.0 does, and the “come pinned from the base” comments lose “pinned”. No row change. The rebuild (bash _build/build.sh) also caught two canvas notes (🚀 Start here, Set once) still naming the pre-kebab-case MiniMax H3 Video Creator Script.sh: the builder already wrote video-creator-minimax-h3-script.sh, but the committed JSON had not been rebuilt since the rename. Same length, so the layout is unchanged.

The HANDOFF’s owed LOADER_CATS rows land with this bump: MinimaxH3LatentUpscaler3D|latent_upscale_models and H3QwenVLGenerationTailLoader|text_encoders. Base 2.3.0’s loader map covers ComfyUI’s core loaders and the shared packs only, so without them the sync stage stopped rewriting those two loaders’ paths.

Owed: re-photograph _build/sizes.json. The version lives in node 749’s title and structure_sha hashes titles, so the bump leaves the size cache keyed to V8.0.0’s graph and the three size-cache tests red. No SoulCraft testbed existed on 2026-09-25 to photograph against (bash base/comfyui-base/testbed.sh --server, then bash _build/build.sh). That photograph will also measure the upstream drift already red on the committed tree under base 3.0.0 (seen against another session’s ComfyUI 0.37.2, not a SoulCraft testbed): node 1095 (MiniMaxH3RewriterOptions) now has 20 widgets against the graph’s 19, and four nodes draw larger than stored.

21. V8.1.0: SFW only (2026-10-15)

Why. SoulCraft is SFW only, repo-wide, and the rule is never switched off: no nudity, no sexual content, no brutal gore, and no switch, field, toggle, flag or model row that turns any of these on (the workspace’s CLAUDE.md). V7 and V8 shipped one: a toggle on the 🎛️ Setup node, its App Mode row, a package command that fetched an alternative engine, encoder, tail and captioner (one of them from CivitAI with a token), their rows in profiles.yaml, and four sampling overrides for that engine. All of it is gone, as a minor version because the form loses a field.

The graph (_build/restructure.py, pass v810_sfw_only, idempotent). Every consumer reads what ships again: the 32B encoder (2) and its tail (1185) feed 1431, 1434, 1435 and 1436 directly; 940’s own widget names the one checkpoint and nothing links unet_name; the tier bus feeds 1432.sampler_name, shift_video, shift_audio and 980.strength_model with nothing spliced in front; 1497 keeps one input, the stock captioner name 1498, as the adapter a STRING primitive needs to reach a COMBO. Retired: 1466-1472, 1474-1481, 1496 and their frame. The setup node loses its widget, so the form has 35 rows. p2_uncensored and p2_eros_overlay are deleted rather than left dormant (the first would have rebuilt the frame on a graph without it). v8_front_door, which never runs again, keeps id 1496 reserved so a replay would still pin 1497 and 1498.

The pack (ComfyUI-H3-App-Controls 1.1.0): the widget leaves H3AppSetup, the family leaves web/controls.json, the default leaves controls.py. The script: no package command, no rows, no CIVITAI_TOKEN. The four files are listed in SUPERSEDED, so a pod that fetched them is offered the deletion, and h3_install.py models-json removes the captioner entry V8.0 wrote into the rewriter’s catalog (that and the SUPERSEDED names are the only places the package still names them). The suite: the tests that asserted the toggle are replaced by test_flow_sfw_only_every_state_reads_the_shipped_models and test_install_sfw_only_no_command_row_or_field_turns_on_an_alternative_set, which keep it out.

The rule texts. The same rule holds for what the local LLMs are told. The 👁 caption rules (1194) asked the 32B to describe nudity “plainly and clinically”; they now ask for people, clothing and action described plainly and factually, and still never to refuse an ordinary picture. The 🔒 house rules (1089) and the 🧑‍⚖️ editor rules (1098) still listed the Intimacy style: rider line that ✅ Passion wrote before V7 retired it; the name is gone from both. The pass rewrites every stored copy, including the stale ones inside the TextGen nodes.

Also in this version (the orchestrator’s request, 2026-10-15): the script’s repo-discovery line still looked for base/testbed.sh, a monorepo path gone since the base became a submodule. In the one-repository-per-package layout (2026-10-16) the line is removed: the script reaches the base through $COMFY_BASE alone, and the suite’s by-hand line reads $COMFY_BASE/py.

A workflow saved from V8.0.x carries one more Setup widget value than the V8.1.0 node declares, so its later values shift by one. Open the shipped file again rather than an old copy.

22. V8.1.1: the SUPERSEDED list corrected (2026-10-15)

minimax_h3_ref2v_turbo_4step_v0.1_comfyui_bf16.safetensors leaves SUPERSEDED: this graph never loads it, but MiniMax H3 Video Replacer (V5.1.0, modes 🅵 Fast and 🅸 Iterate, loras/MiniMax-H3/Turbo/) ships and uses it, and on a shared pod this package’s install offered it for deletion. minimax_h3_video_vae_fp16.safetensors joins the list: this graph left it for the int8 ConvRot VAE, the Replacer has moved to the same VAE, and no package ships the fp16 file any more.

One graph change, from the package gate (2026-10-16): the prompt rewriter pack’s 0.21.0 appended a 20th widget to MiniMaxH3RewriterOptions, downloader (default built-in, the pack’s own transfer). The base installs every pack at its HEAD, so ⚙️ Writer options (1095, in 🧠 Vision engine) saved 19 values against 20 widgets. _build/restructure.py now writes all 20, built-in last; _build/sizes.json was photographed again (frontend 1.55.17, base 3.8.2’s platform field) and the layout re-run.

23. V8.1.2: B200 first, B300 second, and every save on the CPU (2026-10-16)

Paul’s decision (2026-10-16): the video packages run on NVIDIA’s datacenter Blackwell, B200 first and B300 second. Two first-party facts decided what this graph had to change:

  • No NVENC. NVIDIA’s Video Encode and Decode support matrix gives the HGX B200 and the HGX B300 0 NVENC engines (7 NVDEC), like the H100. Four saves asked for h264_nvenc: ▶ Base save (757, stock nvenc_h264-mp4), the RIFE preview (970, the same), and the two post-stage saves 830 (H3 2×) and 781 (RTX 4K) through the package’s 320 kbps format. On a B200 every render would have died at its first save. All four now encode with libx264: 757 and 970 take VHS’s stock video/h264-mp4, 830 and 781 take h3-h264-mp4.json (the file the script writes and registers, renamed from h3-nvenc-h264-mp4.json; the same libx264 pass plus -b:a 320k). The pass is v812_cpu_encode in _build/restructure.py. Quality: the old saves ran NVENC at 15 Mbit/s; FFmpeg’s own H.264 encoding guide calls CRF 17-18 “visually lossless or nearly so”, and x264 at a constant rate factor spends what each frame needs, so every save is CRF 17 at the default preset (medium), yuv420p, bt709 as before. The swap is two widgets for two (bitrate and megabit out, crf and trim_to_audio in), so the canvas’s cached sizes stay valid. A pod that ran an earlier version keeps the old format file in /workspace/h3/video_formats/; nothing uses it.
  • RTX Video Super Resolution names an RTX GPU (“An Nvidia RTX GPU”, the Comfy-Org/Nvidia_RTX_Nodes_ComfyUI README), and a B200 or B300 is not one. 💎 Deliver and 🏆 Master end in it, so on a non-RTX GPU the setup node refuses those two tiers when Run is pressed, before anything renders, naming the GPU and the way out (⚡ Draft here, or an RTX PRO 6000 for all three); the Quality tooltip says so too, and the install’s SUMMARY warns once. H3_ALLOW_NON_RTX_VSR=1 in ComfyUI’s environment lets them try, for the test this section owes. On an RTX GPU nothing changed. A failed nvidia-vfx import is a failure on an RTX GPU, as before, and a warning on any other (only those two tiers use it).

The script declares GPU_PREFER="B200 B300": base 3.8.0 warns at install, and in podctl install, on any other GPU, and podctl create asks RunPod for that order with a CUDA 13.0 floor. RunPod, read 2026-10-15/16 (stock moves): B200 offered in EU-RO-1, US-CA-2, US-NC-2, US-NE-1 and US-TX-6; B300 with network volumes in EUR-IS-1; a network volume pins its datacenter.

Owed, only a real B200/B300 can show it: a ⚡ Draft render on each (time, output, the libx264 save’s size and look against an old NVENC save of the same seed); whether RTX VSR runs there at all (H3_ALLOW_NON_RTX_VSR=1, one 💎 render), and if it does, the refusal goes; whether nvidia-vfx and Sol-Attn import there.

24. V8.1.3: the upscaler link works again (2026-10-16)

On 2026-09-17 LBH-123-AI moved minimax_h3_latent_upscaler_3d_bf16.safetensors into minimax_h3_latent_upscaler_3d_conv_v1/ and renamed it minimax_h3_latent_upscaler_3d_conv_v1_bf16.safetensors (commit 6749d7b), so the row’s resolve/main/ URL was 404 and a fresh pod could not install the package. The Hub reports the same 690 592 992 bytes and the same LFS sha256 (4f57821f…46a5e6). The row now names commit 3f941d5 and the new path, and keeps the file name the H3 2× pass loads, so the graph is unchanged but for the version label. Base 3.9.0 drops the resolve/<revision> part and fetches that path at main, where it is today; a base that honours the revision (3.11.0) fetches it at the commit, so a later move upstream cannot break it. A contract test holds the pin.

25. V8.1.4: every Hub row is pinned to a commit (2026-10-16)

ComfyUI Base 3.12.0 honours the revision in a row and checks each Hub file against the sha256 the Hub states for that commit, once, at download, so a repo that moves or renames a file upstream can no longer 404 a row or change what installs (the H3 latent upscaler broke that way on 2026-09-17). The twelve Hub rows that still named main now name the commit base.sh gen-models <dir> --pin printed on 2026-10-16 (the upscaler row was pinned in V8.1.3), each file’s size and Hub sha256 unchanged. rife426.pth is a GitHub release asset and stays as it is. Files already on a volume are kept as they are: no download and no re-hash. BASE_MIN is 3.12.0, because an older base drops the revision and fetches main. No graph change but the version stamp.

26. V8.1.5: BASE_MIN 3.17.1, coach_tour, and a test that skips instead of branching (2026-10-16)

BASE_MIN is 3.17.1: the base marks the App Mode tour seen on install, so the form opens clear. _build/appe2e.py calls the base’s coach_tour instead of its own Skip/Close loop, and stops when a tour opens on a seeded server (the testbed or a base install), because there that means the seed missed. test_config_no_dangling_section_reference checked README.md behind an if, so a run from the extracted zip passed without it; it now SKIPS when no README.md is beside the package, and CI’s VERIFY_SUITE=zip runs it again from the repo (error log row 106). The size cache is photographed again for the 8.1.5 title. No graph change but the version stamp.

27. V8.1.6: taeh3 is madebyollin’s TAEH3, the file the other H3 packages install (2026-10-17)

The vae_approx/taeh3.safetensors row is madebyollin’s TAEH3 (MIT, 22,709,752 bytes, madebyollin/taehv pinned to commit 62f7591f), the row the Video Replacer already ships, in place of Kijai’s MiniMax-H3-TAE (9,791,388 bytes). Both files sat at the same dest, so a pod with two H3 packages kept whichever came first, and the workspace’s integration run 39045535460 failed the Replacer’s and the Continuer’s sibling comparisons on exactly this row. The Video Swapper’s decision (2026-10-17): madebyollin’s file, which the author of the TAEHV family publishes and which ComfyUI’s own H3 latent preview and KJNodes’ ModelPreviewOverrideKJ both load. Core reads vae_approx/taeh3* only under the TAESD preview method: the base’s launch line passes --preview-method auto, and auto resolves to Latent2RGB in latent_preview.py (read in the testbed’s ComfyUI of 2026-09-30), so this package’s previews use the file when the TAESD method is chosen. Released together with Video Continuer 1.0.9, which makes the same move. No node names the file, so the graph is unchanged but for the version label; a new setup test holds the row on its own, since the Replacer’s byte-for-byte test sees it only with this package beside it.

How _build/sizes.json is photographed from now on. structure_sha hashes every node’s title, so each version bump re-keys the cache, and the photograph needs a ComfyUI server and a browser, which the Mac no longer runs (2026-10-17). It is a CI job: gh workflow run photograph.yml -R SoulCraft-Org/video-creator-minimax-h3 --ref <branch> -f branch=<branch>. On the free runner it provisions the testbed as package CI does, at the ComfyUI release sizes.json records, runs bash _build/build.sh, checks the zip, commits sizes.json, the workflow and the zip back to the branch and starts ci.yml on it. That push is made with GITHUB_TOKEN, which starts no pull_request run, and the dispatched run is not the pull request’s required check, so the gate runs on the next push to the branch (or a close and reopen of the pull request). A chain with any failure commits nothing and uploads what it made. 8.1.6’s cache is the first one taken there (Linux Chromium; earlier ones were taken on the Mac).

The preview decoder, checked on the CPU. The same job runs _build/preview_check.py: the taeh3 row fetched from its URL and size-checked, then loaded by core’s latent_preview.get_previewer for MiniMaxH3Video (TAESD method, the TAEHV previewer) and by KJNodes’ load_tiny_vae_decoder (the loader ModelPreviewOverrideKJ calls), each decoding a random 24-channel latent to an image of the latent’s size times 16, finite and not flat. It proves the file and both readers; a random latent says nothing about how a real preview looks.

28. V8.1.7: GPU_MIN_GB for the site’s catalog (2026-10-17)

GPU_MIN_GB="46", estimated: the smallest card the default flow runs on, for the site’s catalog. It is the heaviest stage’s weights at the precision the loader picks, plus ComfyUI’s own memory_required activations and its 1.2 GiB inference reserve, with ComfyUI swapping models between stages; the script’s comment holds the arithmetic. Comfortable: 73 GB. A measured peak replaces the estimate at the next free GPU smoke. No graph change but the version stamp.