225 lines
7.6 KiB
Python
225 lines
7.6 KiB
Python
"""P3.2 / P4.1 - the mock-integration execution of the node classes.
|
|
|
|
The loader node is a V3 node (P4.1) - its ``execute`` is a classmethod that
|
|
returns an ``io.NodeOutput``. The tests drive it with a **stubbed loader**
|
|
(the model build is stubbed out - no weights are needed) and assert the
|
|
contract of the node: the pack comes back, the widgets reach the loader
|
|
unchanged and the second call is served from the loader's cache.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def node_module():
|
|
return importlib.import_module("ComfyUI_KEEP.nodes.keep_face_upscale_image")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def loader_module():
|
|
return importlib.import_module("ComfyUI_KEEP.src.keep_model_loader")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def loader_node_module():
|
|
return importlib.import_module("ComfyUI_KEEP.nodes.keep_model_loader")
|
|
|
|
|
|
@pytest.fixture
|
|
def stub_pack(loader_module, monkeypatch):
|
|
"""A real ``KEEPModelPack`` (the node checks the type) with stubs."""
|
|
pack = loader_module.KEEPModelPack(
|
|
keep_net=None,
|
|
face_helper=None,
|
|
bg_upscale_model=None,
|
|
face_upscale_model=None,
|
|
model_type_str="KEEP",
|
|
)
|
|
calls: list[str] = []
|
|
monkeypatch.setattr(pack, "load_device", lambda: calls.append("load_device"))
|
|
monkeypatch.setattr(pack, "offload", lambda: calls.append("offload"))
|
|
pack.calls = calls
|
|
return pack
|
|
|
|
|
|
@pytest.fixture
|
|
def stubbed_loader(loader_module, monkeypatch, tmp_path):
|
|
"""A real ``KEEPModelLoader`` whose model build is stubbed out.
|
|
|
|
The stub replaces the four heavy parts of the build (the architecture, the
|
|
checkpoint download, the checkpoint load and the face helper) - the cache
|
|
logic of the loader itself stays **real**.
|
|
"""
|
|
class _StubArch:
|
|
def __init__(self, **kwargs):
|
|
self.kwargs = kwargs
|
|
self.loaded = None
|
|
|
|
def to(self, device):
|
|
return self
|
|
|
|
def load_state_dict(self, state_dict, strict=True):
|
|
self.loaded = state_dict
|
|
return self
|
|
|
|
def eval(self):
|
|
return self
|
|
|
|
class _StubHelper:
|
|
def __init__(self, **kwargs):
|
|
self.kwargs = kwargs
|
|
|
|
monkeypatch.setattr(loader_module, "ARCH_REGISTRY", {"KEEP": _StubArch})
|
|
monkeypatch.setattr(
|
|
loader_module,
|
|
"load_file_from_url_comfy",
|
|
lambda **kwargs: str(tmp_path / "checkpoint.pth"),
|
|
)
|
|
monkeypatch.setattr(
|
|
loader_module.torch,
|
|
"load",
|
|
lambda *args, **kwargs: {"params_ema": {"weight": torch.zeros(1)}},
|
|
)
|
|
monkeypatch.setattr(loader_module, "_face_restore_helper", lambda: _StubHelper)
|
|
monkeypatch.setattr(
|
|
loader_module.keep_paths, "resolve_model_root", lambda *args, **kwargs: str(tmp_path)
|
|
)
|
|
return loader_module.KEEPModelLoader()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# P4.1 - the model loader node
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.mock_integration
|
|
def test_the_loader_node_returns_the_pack(loader_node_module, stubbed_loader, monkeypatch):
|
|
"""The node returns the pack of the loader as a ``NodeOutput``."""
|
|
from comfy_api.latest import io
|
|
|
|
monkeypatch.setattr(loader_node_module, "GLOBAL_KEEP_MODEL_LOADER", stubbed_loader)
|
|
|
|
output = loader_node_module.KEEP_ModelLoaderNode.execute(
|
|
model="KEEP",
|
|
detection_model="retinaface_resnet50",
|
|
)
|
|
|
|
assert isinstance(output, io.NodeOutput)
|
|
assert len(output.result) == 1
|
|
pack = output.result[0]
|
|
assert pack.model_type_str == "KEEP"
|
|
assert pack.keep_net is not None
|
|
|
|
|
|
@pytest.mark.mock_integration
|
|
def test_the_loader_node_serves_the_second_call_from_the_cache(
|
|
loader_node_module, stubbed_loader, monkeypatch
|
|
):
|
|
"""The second call with the same widgets reuses the cached models."""
|
|
monkeypatch.setattr(loader_node_module, "GLOBAL_KEEP_MODEL_LOADER", stubbed_loader)
|
|
|
|
first = loader_node_module.KEEP_ModelLoaderNode.execute(
|
|
model="KEEP", detection_model="retinaface_resnet50"
|
|
).result[0]
|
|
second = loader_node_module.KEEP_ModelLoaderNode.execute(
|
|
model="KEEP", detection_model="retinaface_resnet50"
|
|
).result[0]
|
|
|
|
assert first is not second, "every call returns its own pack"
|
|
assert first.keep_net is second.keep_net, "the models are cached"
|
|
assert first.face_helper is second.face_helper, "the models are cached"
|
|
assert len(stubbed_loader.loaded_models) == 1, stubbed_loader.loaded_models
|
|
|
|
|
|
@pytest.mark.mock_integration
|
|
def test_the_loader_node_passes_the_widgets_to_the_loader(
|
|
loader_node_module, monkeypatch
|
|
):
|
|
"""The widget values reach the loader unchanged."""
|
|
received: list[dict] = []
|
|
|
|
class _RecordingLoader:
|
|
def load_keep_model_pack(self, **kwargs):
|
|
received.append(kwargs)
|
|
return "pack"
|
|
|
|
monkeypatch.setattr(loader_node_module, "GLOBAL_KEEP_MODEL_LOADER", _RecordingLoader())
|
|
|
|
output = loader_node_module.KEEP_ModelLoaderNode.execute(
|
|
model="KEEP",
|
|
detection_model="retinaface_mobile0.25",
|
|
bg_upscale_model="bg",
|
|
face_upscale_model="face",
|
|
)
|
|
|
|
assert output.result == ("pack",)
|
|
assert received == [
|
|
{
|
|
"model_type_str": "KEEP",
|
|
"detection_model_str": "retinaface_mobile0.25",
|
|
"bg_upscale_model": "bg",
|
|
"face_upscale_model": "face",
|
|
}
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# P3.2 - the single image node (V1 until P4.2)
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.mark.mock_integration
|
|
def test_upscale_face_image_returns_the_processed_image(node_module, stub_pack, monkeypatch):
|
|
"""The node returns the processed image and uses the pack once."""
|
|
processed = np.full((4, 6, 3), 128, dtype=np.uint8)
|
|
processor_calls: list[dict] = []
|
|
|
|
class _StubProcessor:
|
|
def __init__(self, model_pack):
|
|
processor_calls.append({"pack": model_pack})
|
|
|
|
def process_image(self, **kwargs):
|
|
processor_calls.append({"kwargs": kwargs})
|
|
return processed
|
|
|
|
monkeypatch.setattr(node_module, "KEEPFaceProcessor", _StubProcessor)
|
|
|
|
image = torch.rand((1, 4, 6, 3), dtype=torch.float32)
|
|
result = node_module.KEEP_FaceUpscaleImageNode().upscale_face_image(
|
|
image,
|
|
stub_pack,
|
|
final_upscale_factor=1.0,
|
|
has_aligned_face=True,
|
|
only_center_face=True,
|
|
draw_bounding_box=False,
|
|
)
|
|
|
|
assert len(result) == 1
|
|
output = result[0]
|
|
assert tuple(output.shape) == (1, 4, 6, 3), output.shape
|
|
# the stub's image (128/255) came back
|
|
assert torch.allclose(output, torch.full((1, 4, 6, 3), 128 / 255), atol=1e-3), output
|
|
|
|
# the processor was built once and called once
|
|
assert len(processor_calls) == 2, processor_calls
|
|
assert processor_calls[0]["pack"] is stub_pack
|
|
assert processor_calls[1]["kwargs"]["final_upscale_factor"] == 1.0
|
|
# the pack was loaded and offloaded exactly once each
|
|
assert stub_pack.calls == ["load_device", "offload"], stub_pack.calls
|
|
|
|
|
|
@pytest.mark.mock_integration
|
|
def test_upscale_face_image_rejects_a_foreign_pack(node_module):
|
|
"""A pack of the wrong type is refused (and the error is logged)."""
|
|
result = node_module.KEEP_FaceUpscaleImageNode().upscale_face_image(
|
|
torch.rand((1, 4, 4, 3), dtype=torch.float32),
|
|
object(),
|
|
final_upscale_factor=1.0,
|
|
has_aligned_face=True,
|
|
only_center_face=True,
|
|
draw_bounding_box=False,
|
|
)
|
|
assert result == (None,)
|