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()