Two pieces of the "port v4's exact UI onto v5's backend" plan: - core/contract.py: new optional ModuleSpec.category field (closed set: continuity, guidance, conditioning, sampling, post, system), validated in core/schema.py, threaded through core/registry.py's FIELDS and to_manifest(). Presentation grouping, independent of STAGE (execution order) -- a module can be stage="sampling" and category="guidance" at once. Only the 4 existing modules that actually render a settings panel (alg, sharpen, appearance, full_control) got a category assigned; the other 12 have none to categorize (no SETTINGS block) and stay uncategorised, which is valid. - v4's frontend (movie_editor/frontend/*, ~29k lines, 61 files) copied verbatim into app/legacy/. core/routes.py's "/" route now serves legacy/index.html instead of the composer entry point; a <base> tag in that file resolves its own relative script/link tags against /funpack/app/legacy/ regardless of what URL it's served at, so every file moved as a unit with zero per-file path edits. Verified live: all 61 assets load 200 OK, the real v4 onboarding screen renders pixel-for- pixel (it's the actual DOM/CSS, not a reimplementation), and several v4 API calls (/api/media, /api/projects, /api/git/status) already succeed against v5's real backend by coincidence of shared route naming -- the rest 404/405 as expected, since api.js's ~90 functions haven't been rewired to v5's actual routes yet (next piece of this plan). The old composer entry point (app/index.html, app/composer/**) stays in the tree, unreferenced, until a later cleanup pass once nothing imports it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
109 lines
4.5 KiB
Python
109 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"
|
|
CATEGORY = "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}
|