File size: 12,723 Bytes
e488ce7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
"""H3 capability probe and block-patch collision report (alpha, 2026-08-23).

Two questions a graph cannot answer from a ComfyUI version string, because
users run stable, stale nightlies and same-day checkouts interchangeably:

1. What can the RUNNING core do for H3? Per-token latent masks (#15375),
   arbitrary guide positions and clip guides (#15439), references that
   compose with keyframes, the tokenizer special tokens (#15808), the two
   condition-row noise-aug dials, the TAE decoder, the 2 fps Qwen view.
   Every answer here is read from the installed source, never from a
   version number, so a nightly that carries the commit answers yes and a
   tagged release that does not answers no.

2. Who else has a hand on the model? Several packs replace H3's double
   blocks through ``set_model_patch_replace`` (this pack's Streamed Blocks,
   first/last-block caches), several replace ``_forward`` or the final layer
   through object patches (Sol packs, this pack's trim_forward), and some
   rewrite the core classes at import time. Comfy keeps ONE replacement per
   block key, so stacking is last-writer-wins and silent. This module lists
   the owners before Streamed Blocks installs so the collision is loud.

Nothing here changes a render. The node passes MODEL through untouched and
prints; the hook in Streamed Blocks only logs.
"""
from __future__ import annotations

import inspect
import logging
import os
import subprocess
import sys

log = logging.getLogger("MAINodes.h3_capabilities")

MINIMAX_EXTRA_TOKENS = ("<d>", "</d>", "<|cutoff|>", "<|lyrics_start|>",
                        "<|lyrics_end|>", "<|caption_start|>", "<|caption_end|>")


def _src(mod):
    try:
        return inspect.getsource(mod)
    except Exception:  # noqa: BLE001
        return ""


def _import(name):
    try:
        return __import__(name, fromlist=["*"])
    except Exception:  # noqa: BLE001
        return None


def _comfy_root():
    m = _import("comfy.model_base")
    if m is None:
        return None
    return os.path.dirname(os.path.dirname(os.path.abspath(m.__file__)))


def _git_head(root):
    if not root or not os.path.isdir(os.path.join(root, ".git")):
        return None
    try:
        out = subprocess.run(["git", "-C", root, "log", "-1", "--format=%h %cs"],
                             capture_output=True, text=True, timeout=5)
        return out.stdout.strip() or None
    except Exception:  # noqa: BLE001
        return None


def probe_core() -> dict:
    """Capabilities of the running ComfyUI for H3, read from source.

    Values: True / False / "unknown" (module missing or source unreadable).
    Keys are stable; new keys append.
    """
    caps = {}
    root = _comfy_root()
    caps["comfy_root"] = root
    caps["comfy_commit"] = _git_head(root)

    mb = _import("comfy.model_base")
    s = _src(mb)
    if not s:
        caps.update(per_token_masks="unknown", keyframe_plus_refs="unknown",
                    visual_cond_noise_aug="unknown", audio_cond_noise_aug="unknown")
    else:
        # #15375: masks pooled to the 2x2 token grid and audio latent frames
        caps["per_token_masks"] = "_pool_masks_to_token_grid" in s
        # refs APPEND to the keyframe list (fixed); the old code overwrote it
        if 'payload.get("cond_video_latents", []) +' in s:
            caps["keyframe_plus_refs"] = "native"
        elif "cond_video_latents" in s:
            caps["keyframe_plus_refs"] = "BROKEN (refs overwrite keyframes)"
        else:
            caps["keyframe_plus_refs"] = "unknown"
        caps["visual_cond_noise_aug"] = "minimax_visual_cond_noise_aug" in s
        caps["audio_cond_noise_aug"] = "minimax_audio_cond_noise_aug" in s

    h3m = _import("comfy.ldm.minimax.model")
    s = _src(h3m)
    caps["arbitrary_keyframe_position"] = ("resolved_frame_index" in s) if s else "unknown"
    caps["mask_rows_fractional"] = ("def mask_row_values" in s) if s else "unknown"

    nodes = _import("comfy_extras.nodes_minimax_h3")
    s = _src(nodes)
    if not s:
        caps.update(clip_guide="unknown", audio_guide="unknown", qwen_ref_video_2fps="unknown",
                    length_rounds_up="unknown")
    else:
        caps["clip_guide"] = "guide_frames % 17 != 5" in s or "guide_frames -= 1" in s
        caps["audio_guide"] = 'Input("audio"' in s and "AddGuide" in s
        caps["qwen_ref_video_2fps"] = "FPS // 2" in s
        caps["length_rounds_up"] = "def align_frame_count" in s

    tok = _import("comfy.text_encoders.minimax")
    s = _src(tok)
    caps["tokenizer_special_tokens"] = ("MINIMAX_EXTRA_TOKENS" in s) if s else "unknown"

    sd = _import("comfy.sd")
    s = _src(sd)
    caps["tae_h3_decoder"] = ("decoder.22.bias" in s and "MiniMax H3" in s) if s else "unknown"

    caps["sageattention"] = _import("sageattention") is not None
    return caps


# ---- who else patches H3 ---------------------------------------------------

_KNOWN_OWNERS = {
    # module-path fragment -> human label (kept coarse: the pack directory)
}


def _owner_of(fn):
    """custom_nodes/<pack> that a callable lives in, or 'comfy core', or module name."""
    try:
        code = getattr(fn, "__code__", None) or getattr(getattr(fn, "__call__", None), "__code__", None)
        path = code.co_filename if code else inspect.getfile(fn)
    except Exception:  # noqa: BLE001
        path = getattr(fn, "__module__", "?")
    path = str(path).replace("\\", "/")
    if "/custom_nodes/" in path:
        return path.split("/custom_nodes/", 1)[1].split("/", 1)[0]
    if "/comfy/" in path or "/comfy_extras/" in path:
        return "comfy core"
    return path


def block_patch_report(model=None) -> dict:
    """Every hand on the H3 model: block replacements, object patches,
    transformer_options keys, and import-time rewrites of the core classes.

    ``model`` is a ModelPatcher (optional). Without it only the import-time
    rewrites and loaded packs are reported.
    """
    rep = {"double_block_owners": {}, "object_patches": {}, "transformer_options": {},
           "class_rewrites": {}, "loaded_h3_packs": []}

    if model is not None:
        try:
            to = model.model_options.get("transformer_options", {})
            blocks = to.get("patches_replace", {}).get("dit", {})
            for key, fn in blocks.items():
                rep["double_block_owners"].setdefault(_owner_of(fn), []).append(key)
            for k, v in to.items():
                if k in ("patches_replace", "patches"):
                    continue
                rep["transformer_options"][k] = _owner_of(v) if callable(v) else type(v).__name__
            for name, obj in getattr(model, "object_patches", {}).items():
                rep["object_patches"][name] = _owner_of(obj)
        except Exception as e:  # noqa: BLE001
            rep["error"] = f"{type(e).__name__}: {e}"

    h3m = _import("comfy.ldm.minimax.model")
    if h3m is not None:
        for cls_name, attrs in (("MiniMaxH3Model", ("forward", "_forward")),
                                ("DiTBlock", ("forward",)),
                                ("Attention", ("forward",)),
                                ("FinalLayer", ("forward",))):
            cls = getattr(h3m, cls_name, None)
            if cls is None:
                continue
            for a in attrs:
                fn = getattr(cls, a, None)
                if fn is None:
                    continue
                owner = _owner_of(fn)
                if owner != "comfy core":
                    rep["class_rewrites"][f"{cls_name}.{a}"] = owner

    for name in list(sys.modules):
        if "custom_nodes" not in name and not name.startswith("custom_nodes"):
            continue
        low = name.lower()
        if any(t in low for t in ("minimax", "h3", "sol")):
            top = name.split(".")[1] if name.startswith("custom_nodes.") else name.split(".")[0]
            if top not in rep["loaded_h3_packs"]:
                rep["loaded_h3_packs"].append(top)
    return rep


def collision_warnings(report: dict, me: str = "ComfyUI-MAINodes") -> list:
    """Lines worth shouting about before this pack installs its own patches."""
    out = []
    for owner, keys in report.get("double_block_owners", {}).items():
        if owner != me:
            out.append(f"double_block replacement already owned by {owner} on {len(keys)} block(s) "
                       f"(e.g. {keys[0]}); Comfy keeps ONE replacement per block, the last install wins")
    for name, owner in report.get("object_patches", {}).items():
        if owner != me and ("_forward" in name or "final_layer" in name or "diffusion_model" in name):
            out.append(f"object patch {name} already owned by {owner}")
    for name, owner in report.get("class_rewrites", {}).items():
        out.append(f"core class {name} was rewritten at import time by {owner}; every model in this process sees it")
    return out


def format_report(caps: dict, rep: dict) -> str:
    lines = ["H3 capabilities (read from the installed source, not a version number)"]
    if caps.get("comfy_commit"):
        lines.append(f"  comfy: {caps['comfy_commit']}  ({caps.get('comfy_root')})")
    order = ("per_token_masks", "mask_rows_fractional", "clip_guide", "audio_guide",
             "arbitrary_keyframe_position", "keyframe_plus_refs", "length_rounds_up",
             "tokenizer_special_tokens", "visual_cond_noise_aug", "audio_cond_noise_aug",
             "qwen_ref_video_2fps", "tae_h3_decoder", "sageattention")
    notes = {
        "per_token_masks": "#15375 (2026-08-18): pin existing content with a latent mask; preferred over guides for continuation",
        "tokenizer_special_tokens": "#15808 (2026-08-22): <d> and six other tags tokenize as ONE token; dialogue/audio readings before it are suspect",
        "keyframe_plus_refs": "references and keyframe anchors in one graph; BROKEN on v0.33.1 and earlier",
        "mask_rows_fractional": "the H3 path accepts fractional mask rows; the generic mask path rounds to binary first",
        "length_rounds_up": "generation length rounds UP to 17k+5; clip guides and ref video round DOWN",
    }
    for k in order:
        v = caps.get(k, "unknown")
        tag = {True: "yes", False: "NO"}.get(v, str(v))
        n = notes.get(k, "")
        lines.append(f"  {k:28s} {tag:10s} {n}")
    lines.append("who has a hand on this model")
    if rep.get("double_block_owners"):
        for owner, keys in rep["double_block_owners"].items():
            lines.append(f"  double_block x{len(keys):<3d} {owner}")
    else:
        lines.append("  double_block       none (stock blocks)")
    for name, owner in rep.get("object_patches", {}).items():
        lines.append(f"  object_patch       {name} <- {owner}")
    for name, owner in rep.get("transformer_options", {}).items():
        lines.append(f"  transformer_option {name} <- {owner}")
    for name, owner in rep.get("class_rewrites", {}).items():
        lines.append(f"  CLASS REWRITE      {name} <- {owner}")
    if rep.get("loaded_h3_packs"):
        lines.append("  loaded H3 packs    " + ", ".join(sorted(rep["loaded_h3_packs"])))
    warns = collision_warnings(rep)
    if warns:
        lines.append("WARNINGS")
        lines += ["  " + w for w in warns]
    return "\n".join(lines)


class H3CapabilityProbe:
    """Print what the running core can do for H3 and who else patches the model."""

    @classmethod
    def INPUT_TYPES(cls):
        return {"required": {},
                "optional": {"model": ("MODEL", {"tooltip": "optional: list the block replacements and object patches already on this model"})}}

    RETURN_TYPES = ("MODEL", "STRING")
    RETURN_NAMES = ("model", "report")
    FUNCTION = "probe"
    CATEGORY = "MAINodes/alpha"
    OUTPUT_NODE = True
    DESCRIPTION = (
        "EXPERIMENTAL (alpha), new 2026-08-23. Reads the installed ComfyUI source to say "
        "whether per-token masks (#15375), clip/audio guides and arbitrary positions (#15439), "
        "references alongside keyframes, the tokenizer special-token fix (#15808), the "
        "condition noise-aug dials and the H3 TAE are present, and lists every pack that has "
        "replaced a double block, patched _forward or the final layer, or rewrote a core class. "
        "Passes MODEL through untouched. Nothing here changes a render.")

    def probe(self, model=None):
        caps = probe_core()
        rep = block_patch_report(model)
        text = format_report(caps, rep)
        log.info("\n" + text)
        return {"ui": {"text": [text]}, "result": (model, text)}


NODE_CLASS_MAPPINGS = {"H3CapabilityProbe": H3CapabilityProbe}
NODE_DISPLAY_NAME_MAPPINGS = {"H3CapabilityProbe": "H3 Capability Probe (alpha)"}