From 0ad14698d8e0ea60eb1d729002be3fe9929b8564 Mon Sep 17 00:00:00 2001 From: IAMCCS Date: Mon, 26 Oct 2026 01:20:48 +0100 Subject: [PATCH] bugs fixed --- iamccs_minimax_h3_face_detailer.py | 16 +++ iamccs_minimax_h3_pixel_refine_variant.py | 11 +- tests/test_h3_facerefine_optional.py | 118 ++++++++++++++++++++++ 3 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 tests/test_h3_facerefine_optional.py diff --git a/iamccs_minimax_h3_face_detailer.py b/iamccs_minimax_h3_face_detailer.py index a651833..fb6c2e7 100644 --- a/iamccs_minimax_h3_face_detailer.py +++ b/iamccs_minimax_h3_face_detailer.py @@ -26,6 +26,15 @@ CATEGORY = "IAMCCS/MiniMax H3/Face Detailer" _EXTERNAL_FACE_NODE_PATH = Path(__file__).resolve().parents[1] / "ComfyUI-H3-FaceRefine" / "nodes.py" +def _face_nodes_available(*node_names: str) -> bool: + """Probe optional registration without importing the external backend.""" + if _EXTERNAL_FACE_NODE_PATH.is_file(): + return True + registered = getattr(sys.modules.get("nodes"), "NODE_CLASS_MAPPINGS", {}) + external = getattr(sys.modules.get("iamccs_external_h3_face_refine"), "NODE_CLASS_MAPPINGS", {}) + return all(name in registered or name in external for name in node_names) + + def _face_node_class(node_name: str): """Resolve the installed FaceRefine node without duplicating its algorithms.""" try: @@ -224,3 +233,10 @@ NODE_DISPLAY_NAME_MAPPINGS = { "IAMCCS_MiniMaxH3FaceTransformInfo": "IAMCCS H3 Face Transform Info", "IAMCCS_MiniMaxH3FaceDetailerRouter": "IAMCCS H3 Optional Face Detailer Router", } + +# ComfyUI queries every registered INPUT_TYPES on startup/frontend reload. +# Missing optional backends must not turn that discovery into repeated errors. +for _name, _class in list(NODE_CLASS_MAPPINGS.items()): + if issubclass(_class, _FaceRefineAlias) and not _face_nodes_available(_class.EXTERNAL_NODE): + del NODE_CLASS_MAPPINGS[_name] + del NODE_DISPLAY_NAME_MAPPINGS[_name] diff --git a/iamccs_minimax_h3_pixel_refine_variant.py b/iamccs_minimax_h3_pixel_refine_variant.py index dbcb5e1..07d51eb 100644 --- a/iamccs_minimax_h3_pixel_refine_variant.py +++ b/iamccs_minimax_h3_pixel_refine_variant.py @@ -722,7 +722,10 @@ from .iamccs_minimax_h3_face_delivery_variant import ( IAMCCS_MiniMaxH3FaceDeliveryR38B, IAMCCS_MiniMaxH3WideCharacterDetailer12GB, ) -NODE_CLASS_MAPPINGS["IAMCCS_MiniMaxH3FaceDeliveryR38B"] = IAMCCS_MiniMaxH3FaceDeliveryR38B -NODE_DISPLAY_NAME_MAPPINGS["IAMCCS_MiniMaxH3FaceDeliveryR38B"] = "MiniMax H3 · Optional FaceRefine · Before Upscale" -NODE_CLASS_MAPPINGS["IAMCCS_MiniMaxH3WideCharacterDetailer12GB"] = IAMCCS_MiniMaxH3WideCharacterDetailer12GB -NODE_DISPLAY_NAME_MAPPINGS["IAMCCS_MiniMaxH3WideCharacterDetailer12GB"] = "IAMCCS WIDE CHARACTER DETAILER · 12 GB" +from .iamccs_minimax_h3_face_detailer import _face_nodes_available + +if _face_nodes_available("H3FaceTrackCrop", "H3FaceStitch"): + NODE_CLASS_MAPPINGS["IAMCCS_MiniMaxH3FaceDeliveryR38B"] = IAMCCS_MiniMaxH3FaceDeliveryR38B + NODE_DISPLAY_NAME_MAPPINGS["IAMCCS_MiniMaxH3FaceDeliveryR38B"] = "MiniMax H3 · Optional FaceRefine · Before Upscale" + NODE_CLASS_MAPPINGS["IAMCCS_MiniMaxH3WideCharacterDetailer12GB"] = IAMCCS_MiniMaxH3WideCharacterDetailer12GB + NODE_DISPLAY_NAME_MAPPINGS["IAMCCS_MiniMaxH3WideCharacterDetailer12GB"] = "IAMCCS WIDE CHARACTER DETAILER · 12 GB" diff --git a/tests/test_h3_facerefine_optional.py b/tests/test_h3_facerefine_optional.py new file mode 100644 index 0000000..b68ccc9 --- /dev/null +++ b/tests/test_h3_facerefine_optional.py @@ -0,0 +1,118 @@ +"""Exercise optional FaceRefine registration without loading models or ComfyUI.""" +import ast +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import patch + +import torch + + +ROOT = Path(__file__).resolve().parents[1] +ALIAS_NAMES = ( + "H3FaceTrackCrop", "H3FaceStitch", "H3InjectVideoLatent", + "H3PerFrameDenoise", "H3FaceMaskSAM", "H3FaceTransformInfo", +) + + +class FaceRefineOptionalTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.modules = patch.dict(sys.modules) + self.modules.start() + self.addCleanup(self.modules.stop) + self.package = "_iamccs_optional_face_test" + package = types.ModuleType(self.package) + package.__path__ = [str(ROOT)] + sys.modules[self.package] = package + for name in ("iamccs_prompter", "iamccs_minimax_h3_atomic_backend"): + module = types.ModuleType(f"{self.package}.{name}") + module.SUPERNODE_LINX_TYPE = "SUPERNODE_LINX" + module._resolve_shotplan = lambda linx: linx + sys.modules[module.__name__] = module + comfy_nodes = types.ModuleType("nodes") + comfy_nodes.NODE_CLASS_MAPPINGS = {} + sys.modules["nodes"] = comfy_nodes + sys.modules.pop("iamccs_external_h3_face_refine", None) + + def load_detailer(self): + name = f"{self.package}.iamccs_minimax_h3_face_detailer" + module = types.ModuleType(name) + module.__file__ = str(Path(self.temp.name) / "IAMCCS-nodes" / "iamccs_minimax_h3_face_detailer.py") + module.__package__ = self.package + sys.modules[name] = module + source = ROOT / "iamccs_minimax_h3_face_detailer.py" + exec(compile(source.read_text(encoding="utf-8"), str(source), "exec"), module.__dict__) + return module + + def delivery_registration(self): + # Execute the real registration tail, excluding unrelated pixel inference code. + source = ROOT / "iamccs_minimax_h3_pixel_refine_variant.py" + tree = ast.parse(source.read_text(encoding="utf-8")) + start = next(i for i, node in enumerate(tree.body) if isinstance(node, ast.ImportFrom) + and node.module == "iamccs_minimax_h3_face_delivery_variant") + tail = ast.Module(body=tree.body[start:], type_ignores=[]) + namespace = {"__package__": self.package, "NODE_CLASS_MAPPINGS": {}, "NODE_DISPLAY_NAME_MAPPINGS": {}} + exec(compile(tail, str(source), "exec"), namespace) + return namespace + + def test_absent_dependency_registers_only_independent_router(self): + detailer = self.load_detailer() + self.assertEqual(set(detailer.NODE_CLASS_MAPPINGS), {"IAMCCS_MiniMaxH3FaceDetailerRouter"}) + for node in detailer.NODE_CLASS_MAPPINGS.values(): + self.assertIn("required", node.INPUT_TYPES()) + delivery = self.delivery_registration() + self.assertEqual(delivery["NODE_CLASS_MAPPINGS"], {}) + self.assertEqual(delivery["NODE_DISPLAY_NAME_MAPPINGS"], {}) + + def test_disabled_router_passes_native_objects_without_dependency(self): + detailer = self.load_detailer() + router = detailer.IAMCCS_MiniMaxH3FaceDetailerRouter() + frames, audio = object(), {"waveform": object()} + result = router.route({}, frames, audio) + self.assertIs(result[0], frames) + self.assertIs(result[1], audio) + self.assertEqual(router.check_lazy_status({}, frames, audio), []) + + def test_explicit_use_still_reports_missing_dependency(self): + detailer = self.load_detailer() + with self.assertRaisesRegex(RuntimeError, "before using IAMCCS H3 Face Detailer"): + detailer.IAMCCS_MiniMaxH3FaceTrackCrop().run(images=object()) + + def test_registered_provider_preserves_alias_schema_and_execution(self): + schema = {"required": {"images": ("IMAGE",), "detector": (["installed-detector"],)}} + class Provider: + @classmethod + def INPUT_TYPES(cls): + return schema + + def run(self, **kwargs): + return (kwargs["images"],) + + sys.modules["nodes"].NODE_CLASS_MAPPINGS = dict.fromkeys(ALIAS_NAMES, Provider) + detailer = self.load_detailer() + self.assertEqual(len(detailer.NODE_CLASS_MAPPINGS), 7) + for name in ALIAS_NAMES: + alias = detailer.NODE_CLASS_MAPPINGS[f"IAMCCS_MiniMax{name}"] + result = alias.INPUT_TYPES() + self.assertEqual(result, schema) + self.assertIsNot(result, schema) + marker = object() + self.assertIs(alias().run(images=marker)[0], marker) + self.assertEqual(len(self.delivery_registration()["NODE_CLASS_MAPPINGS"]), 2) + + def test_installed_package_registers_aliases_without_eager_import(self): + provider = Path(self.temp.name) / "ComfyUI-H3-FaceRefine" / "nodes.py" + provider.parent.mkdir() + provider.write_text("raise AssertionError('Provider imported during registration')", encoding="utf-8") + detailer = self.load_detailer() + self.assertEqual(len(detailer.NODE_CLASS_MAPPINGS), 7) + self.assertEqual(len(self.delivery_registration()["NODE_CLASS_MAPPINGS"]), 2) + self.assertNotIn("iamccs_external_h3_face_refine", sys.modules) + + +if __name__ == "__main__": + unittest.main()