File size: 7,456 Bytes
7c2113f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | """Wire the turbo stack onto both shipped workflows, matching the dev graph.
UNETLoader -> LoRA Loader Stack -> H3 AdaLN LoRA Fix
-> MiniMax H3 Low VRAM Attention
-> H3 SLA Attention
-> Model Preview Override (KJ) -> Hand Tie Clips
and, off the same LoRA loader, **CLIP goes to the chain from the loader, not
from the encoder** -- that is what makes the text half of every LoRA land.
None of these five nodes belong to this pack. They are here because this is the
graph the node is actually run with: `steps` is 7, which only works with a turbo
LoRA, and the AdaLN fix exists because the LoRA needs it. Shipping the examples
without them ships a graph nobody uses. Every dependency is named on the START
HERE card, in the Showcase note and in the README.
Idempotent: inserted nodes carry `properties.htc_speed`, so a re-run rewires
rather than stacking a second copy. Widget values are POSITIONAL (CLAUDE.md) and
are copied from the proven dev workflow rather than reconstructed.
"""
import io
import json
import os
import shutil
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WORKFLOWS = ["HandTieClips_Starter.json", "HandTieClips_Showcase.json"]
MARKER = "htc_speed"
TURBO_LORA = "minimax_h3_ref2v_turbo_4step_v0.1_comfyui_bf16.safetensors"
LORA_STACK = json.dumps([{"on": True, "lora": TURBO_LORA,
"str": 1, "v": 1, "a": 1, "t": 1}],
separators=(",", ":"))
# id, type, title, pos, size, widgets
#
# Widget order is INPUT_TYPES order with sockets skipped. The trailing "" on the
# LoRA loader and the preview override is carried over from the dev workflow --
# a value past the last widget is ignored, and dropping one that turns out to
# belong to a widget would silently shift every value after it.
STACK = [
("lora", "LTX_lora_loader", "LoRA Loader Stack (turbo)",
[520, -40], [420, 240], ["minimax", LORA_STACK, ""]),
("adaln", "H3AdaLNLoRAFix", "H3 AdaLN LoRA Fix",
[520, 240], [340, 150], ["port"]),
("lowvram", "MiniMaxLowVRAMAttention", "MiniMax H3 Low VRAM Attention",
[520, 430], [330, 58], [4]),
# sparsity_ratio, block_size, min_seq_len, dense_last_steps, protect_audio,
# enabled, dense_steps, dense_backend, disable_fp16_accum, stabilize_motion,
# reference_protection -- the last one post-dates the dev workflow's saved
# values, so it is written out explicitly here.
("sla", "H3SLAAttention", "H3 SLA Attention",
[520, 530], [340, 322],
[0.9, "64", 8192, 0, True, True, "0", "comfy_kitchen", True, True, "Light"]),
("preview", "ModelPreviewOverrideKJ", "Model Preview Override",
[520, 900], [360, 480], [512, 80, True, 100, 8, "taeh3.safetensors", ""]),
]
FIRST_ID = 30
MODEL_PATH = ["UNETLoader"] + [t for _, t, _, _, _, _ in STACK] + ["HandTieClips"]
def main():
for fn in WORKFLOWS:
p = os.path.join(HERE, "workflows", fn)
bak = p + ".bak-speed"
if not os.path.exists(bak):
shutil.copyfile(p, bak)
wf = json.load(io.open(p, encoding="utf-8"))
# Drop any previous stack and every link that touched it.
old = {n["id"] for n in wf["nodes"]
if MARKER in (n.get("properties") or {})}
wf["nodes"] = [n for n in wf["nodes"] if n["id"] not in old]
wf["links"] = [l for l in wf["links"]
if l[1] not in old and l[3] not in old]
unet = next(n for n in wf["nodes"] if n["type"] == "UNETLoader")
clipl = next(n for n in wf["nodes"] if n["type"] == "CLIPLoader")
chain = next(n for n in wf["nodes"] if n["type"] == "HandTieClips")
lid = max([l[0] for l in wf["links"]] or [0])
# Removing the stack also removed the wires that fed it, so rebuild the
# plain loader -> chain shape first. That makes this reentrant from
# either state: a fresh workflow or one already wired.
wf["links"] = [l for l in wf["links"]
if not (l[3] == chain["id"] and l[4] in (0, 1))]
lid += 1
model_link = [lid, unet["id"], 0, chain["id"], 0, "MODEL"]
lid += 1
clip_link = [lid, clipl["id"], 0, chain["id"], 1, "CLIP"]
wf["links"] += [model_link, clip_link]
ids = {}
for i, (key, ntype, title, pos, size, widgets) in enumerate(STACK):
ids[key] = FIRST_ID + i
wf["nodes"].append({
"id": FIRST_ID + i, "type": ntype,
"pos": list(pos), "size": list(size),
"flags": {}, "order": 0, "mode": 0,
"inputs": [], "outputs": [],
"title": title,
"properties": {MARKER: key, "Node name for S&R": ntype},
"widgets_values": list(widgets),
})
byid = {n["id"]: n for n in wf["nodes"]}
def link(src, src_slot, dst, dst_slot, kind):
"""Append one link and record it on both endpoints."""
nonlocal lid
lid += 1
wf["links"].append([lid, src, src_slot, dst, dst_slot, kind])
return lid
# MODEL, straight down the column.
model_link[3], model_link[4] = ids["lora"], 0
chain_model = model_link[0]
prev = ids["lora"]
for key in ("adaln", "lowvram", "sla", "preview"):
chain_model = link(prev, 0, ids[key], 0, "MODEL")
prev = ids[key]
chain_model = link(prev, 0, chain["id"], 0, "MODEL")
# CLIP through the LoRA loader, so the text half of every LoRA lands.
clip_link[3], clip_link[4] = ids["lora"], 1
chain_clip = link(ids["lora"], 1, chain["id"], 1, "CLIP")
# Sockets and slots, now that every link id exists.
byid[ids["lora"]]["inputs"] = [
{"name": "model", "type": "MODEL", "link": model_link[0]},
{"name": "clip", "type": "CLIP", "link": clip_link[0]},
]
for key in ("adaln", "lowvram", "sla", "preview"):
byid[ids[key]]["inputs"] = [
{"name": "model", "type": "MODEL", "link": None}]
byid[ids["preview"]]["inputs"].append(
{"name": "vae", "type": "VAE", "link": None})
outs = {"lora": [("model", "MODEL"), ("clip", "CLIP")],
"adaln": [("MODEL", "MODEL")],
"lowvram": [("model", "MODEL")],
"sla": [("MODEL", "MODEL")],
"preview": [("MODEL", "MODEL")]}
for key, spec in outs.items():
byid[ids[key]]["outputs"] = [
{"name": nm, "type": ty, "links": []} for nm, ty in spec]
# Walk the finished link table back onto the endpoints.
for n in wf["nodes"]:
for o in n.get("outputs", []):
o["links"] = []
for l in wf["links"]:
src, sslot, dst, dslot = l[1], l[2], l[3], l[4]
byid[src]["outputs"][sslot]["links"].append(l[0])
byid[dst]["inputs"][dslot]["link"] = l[0]
wf["last_link_id"] = lid
wf["last_node_id"] = max(wf["last_node_id"], FIRST_ID + len(STACK) - 1)
io.open(p, "w", encoding="utf-8", newline="\n").write(
json.dumps(wf, indent=2) + "\n")
print("%s\n MODEL: %s\n CLIP : CLIPLoader -> LoRA Loader Stack -> "
"Hand Tie Clips (link %d)"
% (fn, " -> ".join(MODEL_PATH), chain_clip))
if __name__ == "__main__":
main()
|