Decode now takes the MODEL, so a provider answers "is this mine" by identity instead of by shape. H3 was claiming any latent that happened to have two parts -- right today because it is the only thing producing one, wrong the moment a second AV model exists, and wrong in the way that reads as an unrelated fault rather than an error. It declines when the model is not wired, and the node says that is what is missing. That completes the shape a model module should have: one folder saying how this model behaves at each stage -- what it is, how its latent is built, how its output is decoded -- instead of that knowledge spreading through the conditioning and sampling code as special cases. Added a modifier that runs on SD1.5, because a feature nobody can watch is a feature nobody can check. It declares no traits, so it applies everywhere, which is the other half of the rule ALG demonstrates by narrowing. Measured on this machine at a fixed seed: off and on differ, 0.4 gives a visibly richer picture that still reads correctly, and 1.2 blows the highlights -- so the range is -1..1, set from what was measured rather than from a round number. A slider whose top half destroys the image lies about its range. Two things found while building it. ComfyUI has a real circular import between nodes_latent and nodes_post_processing that resolves only because init_extra_nodes reaches one first; a custom node has to break it the same way. And the accumulation bug returned through a third door: stripping handled keyed wrappers but not the anonymous function lists on model_options, so chaining doubled the effect. Tagging is now generic -- it looks for our mark rather than for a list of hook names that would need keeping current. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82 lines
3.1 KiB
Python
82 lines
3.1 KiB
Python
"""Tagging what we install, so it can be removed again.
|
|
|
|
ComfyUI offers two shapes of hook. Some are keyed -- `add_wrapper_with_key` takes
|
|
a name and removing it is exact. Others are plain lists on `model_options`, where
|
|
`set_model_sampler_pre_cfg_function` appends an anonymous callable and nothing
|
|
records who put it there.
|
|
|
|
For the second kind, install-then-forget means a model run through the loader
|
|
twice carries two copies and applies the effect at double strength, reporting
|
|
once. That is the accumulation this project has already paid for: v4 registered
|
|
hooks on shared blocks and only a restart cleared them.
|
|
|
|
So everything we install is tagged, and everything tagged is stripped before the
|
|
next install. The walk below is generic -- it looks for our tag rather than for a
|
|
list of known hook names, because a list of hook names is a thing to keep current
|
|
and the day it falls behind is the day a hook stops being removable.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
TAG = "_funpack_key"
|
|
|
|
|
|
def tag(fn, key: str):
|
|
"""Mark a callable as ours, so `strip` can find it later."""
|
|
try:
|
|
setattr(fn, TAG, key)
|
|
except AttributeError:
|
|
# A builtin or a slotted object cannot carry the mark, and something
|
|
# unremovable must not be installed silently.
|
|
raise TypeError(
|
|
f"{fn!r} cannot be tagged, so it could never be removed again. "
|
|
f"Install a plain function or a closure.")
|
|
return fn
|
|
|
|
|
|
def _ours(value: Any, prefix: str) -> bool:
|
|
key = getattr(value, TAG, None)
|
|
return isinstance(key, str) and key.startswith(prefix)
|
|
|
|
|
|
def strip(patcher, prefix: str) -> int:
|
|
"""Remove every tagged hook under `prefix`, and nothing else.
|
|
|
|
Covers both shapes: keyed wrappers and callbacks, and the anonymous function
|
|
lists on model_options. Returns how many were removed, so a caller can say
|
|
that it replaced something rather than leaving it to be inferred.
|
|
"""
|
|
removed = 0
|
|
|
|
for holder in (getattr(patcher, "wrappers", None), getattr(patcher, "callbacks", None)):
|
|
if not isinstance(holder, dict):
|
|
continue
|
|
for by_key in holder.values():
|
|
if not isinstance(by_key, dict):
|
|
continue
|
|
for key in [k for k in by_key if isinstance(k, str) and k.startswith(prefix)]:
|
|
by_key.pop(key, None)
|
|
removed += 1
|
|
|
|
options = getattr(patcher, "model_options", None)
|
|
if isinstance(options, dict):
|
|
removed += _strip_options(options, prefix)
|
|
return removed
|
|
|
|
|
|
def _strip_options(options: dict, prefix: str) -> int:
|
|
"""Walk model_options for tagged callables, in lists or on their own."""
|
|
removed = 0
|
|
for name, value in list(options.items()):
|
|
if isinstance(value, list):
|
|
keep = [item for item in value if not _ours(item, prefix)]
|
|
if len(keep) != len(value):
|
|
removed += len(value) - len(keep)
|
|
options[name] = keep
|
|
elif callable(value) and _ours(value, prefix):
|
|
options.pop(name, None)
|
|
removed += 1
|
|
elif isinstance(value, dict):
|
|
removed += _strip_options(value, prefix)
|
|
return removed
|