- Fix "Behind Subject" not occluding: blur the two halves separately and re-apply the silhouette after, add a traced cast shadow (shadow_strength, shadow_length), and give the background glow real radial falloff - Replace use_colored_lights with lighting_mode, adding "Both"; three presets set a colour and a grading block, so 12 values in each were inert - Rename use_gradient_mode to mask_shape, and collapse apply_3d_lighting plus light_direction into subject_interaction - Remove show_debug_info: wiring debug_image is the whole gesture, tracked through a hidden debug_output_connected input so one run is enough - Scale everything drawn on the debug view with the frame, and flip a light's label to the left of the marker when it would run off the right edge - Show only the controls that are doing something; grey out what a preset owns and put the preset's value in the label - Ship a working "Fix node (recreate)" and remove ComfyUI-Manager's broken one - Remap pre-v4 saves by name in web/relight_migrate.js - Make "Rim Light (Behind)" a neutral white rim instead of (200,255,200) - Drop requirements.txt; numpy, Pillow, scipy and torch come with ComfyUI - Add a frontend test suite driving the real web/*.js outside a browser, and a CI job for it - Bump to 4.0.0
124 lines
3.5 KiB
Python
124 lines
3.5 KiB
Python
import os
|
|
import sys
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
# The stub `comfy_api` must win over anything a real ComfyUI checkout might put
|
|
# on the path, so it goes first.
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "stubs"))
|
|
sys.path.insert(1, os.path.dirname(os.path.dirname(__file__)))
|
|
|
|
from relight import ReLight # noqa: E402
|
|
|
|
|
|
def _shallow_clone(cls):
|
|
"""Copy of ComfyUI's ``comfy_api.internal.shallow_clone_class``."""
|
|
return type(f"{cls.__name__}Clone", (cls,) + cls.__bases__, dict(cls.__dict__))
|
|
|
|
|
|
def _lock(cls):
|
|
"""Copy of ComfyUI's ``comfy_api.internal.lock_class``.
|
|
|
|
Kept byte-for-byte in behaviour with the real thing: any write to a class
|
|
attribute, or to an attribute of an instance, raises AttributeError.
|
|
"""
|
|
|
|
def locked_instance_setattr(self, name, value):
|
|
raise AttributeError(
|
|
f"Cannot set attribute '{name}' on immutable instance of {type(self).__name__}"
|
|
)
|
|
|
|
class LockedMeta(type(cls)):
|
|
def __setattr__(cls_, name, value):
|
|
raise AttributeError(
|
|
f"Cannot modify class attribute '{name}' on locked class '{cls_.__name__}'"
|
|
)
|
|
|
|
locked_dict = dict(cls.__dict__)
|
|
locked_dict["__setattr__"] = locked_instance_setattr
|
|
return LockedMeta(cls.__name__, cls.__bases__, locked_dict)
|
|
|
|
|
|
@pytest.fixture
|
|
def node():
|
|
"""The node exactly as ComfyUI hands it to ``execute``: a locked clone.
|
|
|
|
ComfyUI never calls the class you define - it calls a locked shallow clone
|
|
whose metaclass rejects every class-attribute write. Testing against the
|
|
bare class hid a crash on the first mask of every single run (3.0.0/3.1.0),
|
|
because the tests could write class state the real runtime forbids.
|
|
"""
|
|
return _lock(_shallow_clone(ReLight))
|
|
|
|
|
|
class _HiddenHolder:
|
|
"""Stand-in for ``comfy_api``'s HiddenHolder.
|
|
|
|
The real one returns None for anything it was not given, which the node
|
|
relies on, so this does too.
|
|
"""
|
|
|
|
def __init__(self, prompt=None, unique_id=None):
|
|
self.prompt = prompt
|
|
self.unique_id = unique_id
|
|
|
|
def __getattr__(self, name):
|
|
return None
|
|
|
|
|
|
@pytest.fixture
|
|
def node_with_prompt():
|
|
"""A locked clone carrying hidden inputs, the way ComfyUI builds one.
|
|
|
|
``PREPARE_CLASS_CLONE`` sets ``hidden`` on the clone *before* the class is
|
|
locked; a test cannot assign it afterwards, because the lock rejects every
|
|
class-attribute write (that is the whole point of the fixture above).
|
|
"""
|
|
|
|
def _make(prompt=None, unique_id=None):
|
|
clone = _shallow_clone(ReLight)
|
|
clone.hidden = _HiddenHolder(prompt, unique_id)
|
|
return _lock(clone)
|
|
|
|
return _make
|
|
|
|
|
|
@pytest.fixture
|
|
def defaults():
|
|
"""Every widget input at its schema default, keyed by input id.
|
|
|
|
Derived from ``define_schema()`` rather than hardcoded, so adding or
|
|
renaming an input never silently desynchronises the tests.
|
|
"""
|
|
|
|
def _defaults(**overrides):
|
|
params = {
|
|
spec.id: spec.default
|
|
for spec in ReLight.define_schema().inputs
|
|
if spec.id not in ("image", "mask")
|
|
}
|
|
params.update(overrides)
|
|
return params
|
|
|
|
return _defaults
|
|
|
|
|
|
@pytest.fixture
|
|
def run(node, defaults):
|
|
"""Execute the node with schema defaults plus overrides."""
|
|
|
|
def _run(image, mask=None, **overrides):
|
|
params = defaults(**overrides)
|
|
if mask is not None:
|
|
params["mask"] = mask
|
|
return tuple(node.execute(image, **params))
|
|
|
|
return _run
|
|
|
|
|
|
@pytest.fixture
|
|
def image():
|
|
torch.manual_seed(0)
|
|
return torch.rand(1, 64, 96, 3)
|