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):
- 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 byh3_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. - 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.
- 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.
- 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.pyallocate them (it is deterministic; re-run it after a rebase rather than merging JSON). - Never
--use-sage-attentionfor H3 (ComfyUI issue #15263); the script removes it fromcomfyui_args.txt. Sol-Attn (kijai pack) is the accelerator, Iterate only. - Keep
SoulCraft Video Creator (MiniMax H3).jsonpristine 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,orderand the saved view are written by that tool and by nothing else. Dragging a box in the frontend is a lost edit: re-runpython3 _build/layout.pyand 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.--verifyproves 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’scomputeSize()to_build/sizes.json(whichlayout.pytreats 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 thesnapshottier asserts what the first screen must show.python3 "base/comfyui-base/_build/canvas/render.py" --pkg . --reviewis 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 theuiuxtier 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 thecanvas-reviewentry 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 setslow_qualitywhends.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. - 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
Nonefrom 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. - Bypass gates need a primitive root: a bypassed StringConcatenate passes
string_athrough, so a mode-side gate isStringConcatenate(string_a ← stub primitive inside the mode group, string_b ← the live text); an image gate isImageBatch(image1 ← EmptyImage inside the group, image2 ← the batch) → ImageFromBatch(1, 64). - 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 kup to the count. Every media loader ships aPUT-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 32BNone. - 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.
- The model library is a contract: every loader widget reads
<category>/<Family>/<Purpose>/<upstream filename>, the setup list fetches to exactly that path, andtest_model_library_layoutfails 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. - 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. - The frontend adds a seed control widget (V5.5.0): ComfyUI’s
useIntWidget.tsaddscontrol_after_generateto any INT input namedseed/noise_seedunless the pack sets the flag toFalse. 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). - 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.
- 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.
cd /workspace/packagespython3 -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:
cd /workspace/packagespython3 -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 → summarySafe 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).
- Control panel: 🎬 Ref2VA · 🎚️ ⚡ Iterate · 🎧 🎧1 · 🔗 New scene · 🧠 🧠1 · 🎬 SCENE # = 1 (all already set; ✅ Continuity Editor and ✅ Analyzer ship ON, ✅ Audio Anchor OFF).
- 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.
- Write the 🎯 brief, one line each:
Action / Camera / Cuts / Dialogue / Sound / Refs / Fidelity: nothing else happens. Queue. - 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, noRTX VSR unavailable) — and after the render the 📝 ANALYZER REPORT with its AUDIO score, TOP FIXES mapped to the 🎚️/🎧/🎙 knobs, theNote:rider andRetry:seed advice. - 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). - 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 runbash "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;
--checkreproduces the plan without changing anything. testfails 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-attentionfor 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 originFrom 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.
/workspaceis 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.12comes 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 withpython main.py --listen 0.0.0.0 --port 8188 --enable-cors-header $(cat /workspace/runpod-slim/comfyui_args.txt)in the background (&+wait, notexec), so the script can stop and restart it without ending the pod. That is the benign half of the fact. The other half: becausestart.shends inwaitrather thanexec, apythonthat dies immediately makesstart.shreturn, 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.shcreates 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.shactually lives, and therefore whether a volume-resident boot guard is even possible.tr '\0' ' ' < /proc/1/cmdlineandreadlink /proc/1/exesettle it;rescuecaptures both into$COMFY/user/h3/boot-facts.txt. PIP_CONSTRAINT,PIP_INDEX_URLandPIP_EXTRA_INDEX_URLare exported system-wide by the image (defect 2).- On an image bump
start.shrsyncs 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-mp4runs-c:a aacwith no bitrate → ffmpeg’s 128 kbps default. The script writesh3-h264-mp4.json(the same libx264 pass,-b:a 320k) to/workspace/h3/video_formats/and registers it throughVHS_video_formatsinextra_model_paths.yaml; saves 781 (RTX) and 830 (H3 2×) use it, 757 (Iterate base) keeps the stock format. - Pinning real audio: core
MiniMaxH3AddGuideacceptsaudioand 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 feedingref_audio_0as 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).
- 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, thenBASE_RESTART=1 bash "video-creator-minimax-h3-script.sh" --latest; the tokens come from the pod’s own environment orstate/tokens.env(no prompt over ssh). - Summary must show: venv at
$COMFY/.venv-cu130on 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 (snapshotskips on the pod by design), smoke: every node type registered — against the server this run started (BASE_RESTART=1). - 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. - 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, noRTX VSR unavailable; the ✅ Analyzer report lands inoutput/MiniMaxH3/<date>/. - 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. - 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).
- 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.
- 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.
- 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:
Both must be under
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 elsewheregrep '^home' "$COMFY/.venv-cu130/pyvenv.cfg"/workspace. Then confirm the.pre-h3-<ts>backup still exists (defect 41), and runrescueonce on the healthy pod to captureboot-facts.txt— the PID 1 command line is the only evidence of wherestart.shactually lives, and it can only be read on a pod that is up. - Anything fails →
/workspace/comfy-base/state/logs/h3_<timestamp>.logis the deliverable for the next iteration (Claude’s fix loop: diagnose, test, fix locally, bump, re-upload, re-run). - 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 ascene_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. - 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.) - 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.pthrelease asset); jeankassio/ComfyUI_MusicTools (pyloudnorm, pedalboard); VHSVHS_video_formatsfolder 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:
- A group toggle reaches the instance, never the interior. rgthree sets
modeon the nodes a group’s bounds capture — on the canvas that is the instance. So an interior node must ship atmode 0and let the instance carry the row’s on/off. Encapsulation froze 28 interiors at themode 4they happened to have when their group was off, which made 🔗1 / 🔗2 / 🔗3 and the Quality/Master analyzer taps impossible to switch back on. - An optional chain still needs its primitive on the canvas (§0 rule 3).
🔧 Aspect overridehad its override primitive absorbed intoAspect 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 anoverride_ratiosubgraph input. - Interior
posis 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 placeholderPUT-YOUR-LORA-HERE.safetensorsselected — the installer knows it as a placeholder (PLACEHOLDERSin 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 isoutput/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 runningbash testbed.sh --stop # stop the serverThen, 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: 160and 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
- 🎬 T2VA never cut references. Bypass passes a value through; only mute blocks it — and 🎬 MODE
SELECT must stay a Bypasser, because the
AddGuidecascade relies on bypassed nodes passing conditioning along. Fixed with nineMiniMaxH3OptionalReferenceImagegates that return nothing while live, inside the 🎬 T2VA group. See §12.2. - 🎞️ REFERENCES could not cut one slot. Its
^🎞️ REFERENCESregex matched a single container group, so the muter rendered one toggle over all 18 switches. Split into 18 per-slot groups. - Prompt Check graded references the engine would never see — it read the loaders directly and
its
taskwas a frozen widget. Both now follow the graph. - 🔗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 --serveruv run "base/comfyui-base/_build/canvas/probe.py" --pkg "video-creator-minimax-h3" # writes _build/snapshots/probe/prompt-<state>.jsonbash "video-creator-minimax-h3-script.sh" test17. 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.pyloads 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), writesopening-*.pngandfull-*.png, exports every node’scomputeSize()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.pytreats 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 (
COCKPITinlayout.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
snapshottier assertslow_qualityis 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/SHRINKin 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.pysorts 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_combomirrors 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 (byidthrough 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: theuiuxbudgets, thesnapshottier and the shape-onlyconfigguards.suite.pydeclaresCANVAS = 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.pyandcanvaslayout.py(the layout engine), every one taking--pkg <package dir>and reading the same declaration by ast._build/layout.pyhere is now 160 lines of tables (theCOCKPITtree,ADOPT,STRIPS, the fold and panel homes) ending inrun(globals()); the engine reproduced this canvas byte for byte through the move._build/restructure.py,_build/sizes.jsonand_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; heightin 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
isTextis 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:
- A cycle through the RIFE fold whenever it is bypassed.
1448.images ← 902 (📏 Post-RIFE Count)while902.image ← 1448.imagesis input index 0 — the slot the bypass contract passes through — so with RIFE off the frontend resolves902.image → 902and ComfyUI reportsdependency_cycle. With RIFE on (🏆) the fold’s inner graph never consumesimages, 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, asframesalready did. Guarded bytest_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_presetbuilds the prompt the frontend would post and DFSes it, the server’s own check. It is red on V6.0.6 and green here. - The 320 kbps VHS format was keyed wrong. VideoHelperSuite keys external formats by the full
filename, extension included (
get_video_formats()iteratesget_filename_list("VHS_video_formats")), so the file the installer writes is offered asvideo/h3-nvenc-h264-mp4.json. The widgets said…-mp4. - The H3 2× upscaler could not see its model. The pack’s
scan_models()globs*.safetensorsat the root oflatent_upscale_models/and returns basenames, so the file the library convention put underMiniMax-H3/did not exist to it — the server offered only a stray V6_fp16at the root. The base has a list of freeform categories for exactly this shape of pack (SAM3, SeedVR2, RMBGos.listdirtheir folders); base 2.0.37 addslatent_upscale_modelsto 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) and1033.any_02(the manual path); 16, 1035 and their 📖 notes are gone; 1032 and1434.string_b_3read 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]feeds1431.image2and1434.image(both required inside); 1495MiniMaxH3OptionalReferenceImage, enabled by 1487’shas_photos, hands the 32BNoneinstead 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’sComfySoftSwitchNodewould have been the obvious gate; it sits innodes_logic.pybut 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
bypassis a promoted input of🧠 Vision engine, linked from 1487’scaptions_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-HEREplaceholder file and itsPLACEHOLDERSrow are gone. - The captioner’s name. 1498 (the stock label) through 1497 into the
modelwidget-input of 1064 and the promotedmodelinputs 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 —
FlexibleOptionalInputTypedeclares 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/promptand died at execution. The setup node’s optionalphoto_1input is a node the validator does visit; through itLoadImage741 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🎬 Flowwith 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 referenceon → 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.pngwas 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). With741 → 1487.photo_1the same run is refused naming the file. - Tooling findings, fixed in the same batch: the sweep wrote
assistedonto the setup node, read LoadAudio’s V3 combo as the string"COMBO", and took V7’sUPLOAD-YOUR-PHOTO-HERE.pngfor 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.shsplit 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 withpod-renders.shon 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.
/promptanswersmissing_node_typewith an emptynode_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 theprompt_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, andappe2eopens the workflow by name from that list exactly as a person does.pass.sh localcopies 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.truncateinside 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_clippedwalks the descendants, which is the only way to see it, andappview.py --jsonreports 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, stocknvenc_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 stockvideo/h264-mp4, 830 and 781 takeh3-h264-mp4.json(the file the script writes and registers, renamed fromh3-nvenc-h264-mp4.json; the same libx264 pass plus-b:a 320k). The pass isv812_cpu_encodein_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=1in ComfyUI’s environment lets them try, for the test this section owes. On an RTX GPU nothing changed. A failednvidia-vfximport 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.