Detail declared no traits, which read as "works on everything" and meant "is offered on everything and drops itself on half of them". Core's unsharp mask pads in two dimensions and raises on anything with more, so on a model with a time axis the toggle was present, switchable, and inert -- a knob that does nothing, which is the fault this project has shipped once already. It now declares spatial_latent and is absent where it cannot work. The guard's regression case survives, because the guard is not about a module being wrong about itself: a hook meets the tensor it was handed. The stub model now announces a latent rank, without which every trait-declaring modifier was filtered out and those tests passed by installing nothing at all. Round seven, one finding, confirmed in part -- the maths is deliberately not being fixed, the advertisement was the defect.
108 lines
4.5 KiB
Python
108 lines
4.5 KiB
Python
"""Detail: sharpen or soften what the model is producing, while it produces it.
|
|
|
|
`REQUIRES` is `spatial_latent`, which is a statement about the KERNEL and not
|
|
about what the picture is for: core's unsharp mask pads in two dimensions and
|
|
raises on anything with more, so on a latent with a time axis this modifier
|
|
cannot do the thing its label says. It used to declare no traits at all, which
|
|
read as "works everywhere" and meant "is offered everywhere and silently drops
|
|
itself on half of them" -- a knob that is present and inert, which is the one
|
|
thing this project does not ship. Offered where it works; absent where it does
|
|
not.
|
|
|
|
It is also the modifier that can be SEEN on a laptop. Ratings, anchors and audio
|
|
clocks all need hardware or history before they mean anything; this changes the
|
|
picture on a 512x512 image in ten seconds, so "the setting did what it said" is
|
|
something to look at rather than infer.
|
|
|
|
The maths is ComfyUI's own `LatentOperationSharpen`, not a copy of it. The
|
|
technique here is FunPack's -- v4 measured this and settled on it -- but the
|
|
kernel is core's, tested by core, and a second implementation of a gaussian
|
|
unsharp mask would only be a second thing to keep correct.
|
|
"""
|
|
|
|
from ..._core import log, patching
|
|
|
|
ID = "sharpen"
|
|
TITLE = "Detail"
|
|
MOUNT = "generation.sampling"
|
|
STAGE = "sampling"
|
|
STATUS = "proven"
|
|
REQUIRES = ["spatial_latent"]
|
|
|
|
SETTINGS = {
|
|
"enabled": {
|
|
"type": "bool", "default": False,
|
|
"label": "Adjust detail",
|
|
"hint": "Sharpens or softens as the picture forms, not afterwards.",
|
|
},
|
|
# Range set from measurement, not from what looked like a round number:
|
|
# 0.4 gives a visibly richer picture on SD1.5 that still reads correctly,
|
|
# and 1.2 already blows out the highlights and smears the subject. A slider
|
|
# whose top half destroys the image is a slider that lies about its range.
|
|
"amount": {
|
|
"type": "float", "default": 0.4, "min": -1.0, "max": 1.0, "step": 0.05,
|
|
"label": "Amount", "ui": "slider",
|
|
"hint": "Above zero sharpens, below zero softens.",
|
|
"when": {"enabled": True},
|
|
},
|
|
"radius": {
|
|
"type": "int", "default": 5, "min": 1, "max": 31, "step": 2,
|
|
"label": "Spread", "ui": "slider",
|
|
"hint": "How far the effect reaches from each detail.",
|
|
"when": {"enabled": True},
|
|
},
|
|
}
|
|
|
|
|
|
def _operation(radius, amount):
|
|
"""Core's sharpen kernel, asked for by name rather than reimplemented.
|
|
|
|
`nodes_post_processing` is imported FIRST on purpose. ComfyUI has a genuine
|
|
cycle between it and `nodes_latent` -- each imports from the other -- which
|
|
resolves only because init_extra_nodes happens to reach post_processing
|
|
first. Importing nodes_latent on its own raises ImportError, so a custom
|
|
node that reaches for it has to break the cycle the same way.
|
|
"""
|
|
import comfy_extras.nodes_post_processing # noqa: F401 (breaks the cycle)
|
|
from comfy_extras.nodes_latent import LatentOperationSharpen
|
|
return LatentOperationSharpen.execute(
|
|
sharpen_radius=int(radius), sigma=1.0, alpha=float(amount)).result[0]
|
|
|
|
|
|
def install(patcher, values, key):
|
|
if not values.get("enabled"):
|
|
return None
|
|
|
|
amount = float(values.get("amount", 0.4))
|
|
if amount == 0.0:
|
|
# Zero is a request for nothing, and saying so beats reporting that a
|
|
# modifier ran when it could not have changed a single pixel.
|
|
log.alert("FunPack Detail", "enabled at amount 0, which changes nothing")
|
|
return None
|
|
|
|
radius = int(values.get("radius", 5))
|
|
operation = _operation(radius, abs(amount))
|
|
sharpening = amount > 0
|
|
|
|
def pre_cfg(args):
|
|
conds_out = args["conds_out"]
|
|
# Applied to the guidance DIFFERENCE when there is one, exactly as
|
|
# core's LatentApplyOperationCFG does -- operating on the raw
|
|
# conditioned output instead would fight the guidance rather than
|
|
# shape it.
|
|
if len(conds_out) == 2:
|
|
adjusted = operation(latent=(conds_out[0] - conds_out[1]))
|
|
changed = adjusted + conds_out[1]
|
|
else:
|
|
changed = operation(latent=conds_out[0])
|
|
conds_out[0] = changed if sharpening else (2 * conds_out[0] - changed)
|
|
return conds_out
|
|
|
|
# Tagged, because this hook is an anonymous entry in a list: without a
|
|
# mark there is nothing to identify it by when it has to come off again.
|
|
patcher.set_model_sampler_pre_cfg_function(patching.tag(pre_cfg, key))
|
|
return f"{'sharpening' if sharpening else 'softening'} by {abs(amount)} at spread {radius}"
|
|
|
|
|
|
PROVIDES = {"modifier": install}
|