Files
olivv-cs-ComfyUI-FunPack/core/patching.py
T
digital-garbageandClaude Opus 5 a81ac48083 model support is a folder, and a modifier you can watch working
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>
2026-08-29 13:33:09 +03:00

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