Files
EnragedAntelope 04d56deed2 v4.0.0: behind-subject occlusion, a schema that says what it means, and only-live controls
- 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
2026-09-04 14:50:33 -04:00

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)