diff --git a/README.md b/README.md index d43487d..ca7a248 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ ## UPDATES +**2024/01/07** @1.0.3: +* relative imports + **2024/01/04** @1.0.2: * updated project description for registry diff --git a/__init__.py b/__init__.py index f9a6a20..709c14e 100644 --- a/__init__.py +++ b/__init__.py @@ -16,13 +16,13 @@ @description: SPOUT support for ComfyUI. @node list: SpoutReaderNode, SpoutWriterNode -@version: 1.0.2 +@version: 1.0.3 """ __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] __author__ = """Alexander G. Morano""" __email__ = "amorano@gmail.com" -__version__ = "1.0.2" +__version__ = "1.0.3" import os import sys @@ -30,6 +30,7 @@ import inspect import importlib from pathlib import Path +from types import ModuleType from typing import Any from loguru import logger @@ -48,21 +49,7 @@ JOV_INTERNAL = os.getenv("JOV_INTERNAL", 'false').strip().lower() in ('true', '1', 't') -JOV_PACKAGE = "Jovi_Spout" - -# ============================================================================== -# === THERE CAN BE ONLY ONE === -# ============================================================================== - -class Singleton(type): - _instances = {} - - def __call__(cls, *arg, **kw) -> Any: - # If the instance does not exist, create and store it - if cls not in cls._instances: - instance = super().__call__(*arg, **kw) - cls._instances[cls] = instance - return cls._instances[cls] +JOV_PACKAGE = "JOV_SPOUT" # ============================================================================== # === CORE NODES === @@ -70,7 +57,7 @@ def __call__(cls, *arg, **kw) -> Any: class JOVBaseNode: NOT_IDEMPOTENT = True - CATEGORY = f"{JOV_PACKAGE.upper()} 📺" + CATEGORY = f"{JOV_PACKAGE} 📺" RETURN_TYPES = () FUNCTION = "run" @@ -99,21 +86,9 @@ def INPUT_TYPES(cls, prompt:bool=False, extra_png:bool=False, dynprompt:bool=Fal data["hidden"]["dynprompt"] = "DYNPROMPT" return data -class JOVImageNode(JOVBaseNode): - RETURN_TYPES = ("IMAGE", "IMAGE", "MASK") - RETURN_NAMES = ("RGBA", "RGB", "MASK") - - @classmethod - def INPUT_TYPES(cls) -> dict: - d = super().INPUT_TYPES() - d = deep_merge(d, { - "outputs": { - 0: ("IMAGE", {"tooltip":"Full channel [RGBA] image. If there is an alpha, the image will be masked out with it when using this output."}), - 1: ("IMAGE", {"tooltip":"Three channel [RGB] image. There will be no alpha."}), - 2: ("MASK", {"tooltip":"Single channel mask output."}), - } - }) - return d +# ============================================================================== +# === TYPE === +# ============================================================================== class AnyType(str): """AnyType input wildcard trick taken from pythongossss's: @@ -124,32 +99,32 @@ def __ne__(self, __value: object) -> bool: return False JOV_TYPE_ANY = AnyType("*") -JOV_TYPE_IMAGE = JOV_TYPE_ANY - -def deep_merge(d1: dict, d2: dict) -> dict: - """ - Deep merge multiple dictionaries recursively. - - Args: - *dicts: Variable number of dictionaries to be merged. - - Returns: - dict: Merged dictionary. - """ - for key in d2: - if key in d1: - if isinstance(d1[key], dict) and isinstance(d2[key], dict): - deep_merge(d1[key], d2[key]) - else: - d1[key] = d2[key] - else: - d1[key] = d2[key] - return d1 # ============================================================================== # === NODE LOADER === # ============================================================================== +def load_module(name: str) -> None|ModuleType: + module = inspect.getmodule(inspect.stack()[0][0]).__name__ + try: + route = str(name).replace("\\", "/") + route = route.split(f"{module}/core/")[1] + route = route.split('.')[0].replace('/', '.') + except Exception as e: + logger.warning(f"module failed {name}") + logger.warning(str(e)) + return + + try: + module = f"{module}.core.{route}" + module = importlib.import_module(module) + except Exception as e: + logger.warning(f"module failed {module}") + logger.warning(str(e)) + return + + return module + def loader(): global NODE_DISPLAY_NAME_MAPPINGS, NODE_CLASS_MAPPINGS NODE_LIST_MAP = {} @@ -158,27 +133,13 @@ def loader(): if fname.stem.startswith('_'): continue - try: - route = str(fname).replace("\\", "/").split(f"{JOV_PACKAGE}/core/")[1] - route = route.split('.')[0].replace('/', '.') - module = f"{JOV_PACKAGE}.core.{route}" - module = importlib.import_module(module) - except Exception as e: - logger.warning(f"module failed {fname}") - logger.warning(str(e)) + if (module := load_module(fname)) is None: continue - # check if there is a dynamic register function.... - try: - for class_name, class_def in module.import_dynamic(): - setattr(module, class_name, class_def) - except Exception as e: - pass - classes = inspect.getmembers(module, inspect.isclass) for class_name, class_object in classes: if not class_name.endswith('BaseNode') and hasattr(class_object, 'NAME') and hasattr(class_object, 'CATEGORY'): - name = class_object.NAME + name = f"{class_object.NAME} ({JOV_PACKAGE})" NODE_DISPLAY_NAME_MAPPINGS[name] = name NODE_CLASS_MAPPINGS[name] = class_object desc = class_object.DESCRIPTION if hasattr(class_object, 'DESCRIPTION') else name @@ -189,7 +150,7 @@ def loader(): keys = NODE_CLASS_MAPPINGS.keys() for name in keys: - logger.debug(f"✅ {name} :: {NODE_DISPLAY_NAME_MAPPINGS[name]}") + logger.debug(f"✅ {name}") logger.info(f"{len(keys)} nodes loaded") # only do the list on local runs... diff --git a/core/__init__.py b/core/__init__.py index e69de29..38f1074 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -0,0 +1,50 @@ +""" +Jovi_Spout - http://www.github.com/Amorano/Jovi_Spout +Core +""" + +from .. import JOVBaseNode + +# ============================================================================== +# === CORE NODES === +# ============================================================================== + +class JOVImageNode(JOVBaseNode): + RETURN_TYPES = ("IMAGE", "IMAGE", "MASK") + RETURN_NAMES = ("RGBA", "RGB", "MASK") + + @classmethod + def INPUT_TYPES(cls) -> dict: + d = super().INPUT_TYPES() + d = deep_merge(d, { + "outputs": { + 0: ("IMAGE", {"tooltip":"Full channel [RGBA] image. If there is an alpha, the image will be masked out with it when using this output."}), + 1: ("IMAGE", {"tooltip":"Three channel [RGB] image. There will be no alpha."}), + 2: ("MASK", {"tooltip":"Single channel mask output."}), + } + }) + return d + +# ============================================================================== +# === SUPPORT === +# ============================================================================== + +def deep_merge(d1: dict, d2: dict) -> dict: + """ + Deep merge multiple dictionaries recursively. + + Args: + *dicts: Variable number of dictionaries to be merged. + + Returns: + dict: Merged dictionary. + """ + for key in d2: + if key in d1: + if isinstance(d1[key], dict) and isinstance(d2[key], dict): + deep_merge(d1[key], d2[key]) + else: + d1[key] = d2[key] + else: + d1[key] = d2[key] + return d1 diff --git a/core/spout.py b/core/spout.py index 2262839..30dbeb3 100644 --- a/core/spout.py +++ b/core/spout.py @@ -19,7 +19,11 @@ from comfy.utils import ProgressBar -from Jovi_Spout import JOV_TYPE_IMAGE, JOVBaseNode, JOVImageNode, deep_merge +from .. import JOV_TYPE_ANY, \ + JOVBaseNode + +from ..core import JOVImageNode, \ + deep_merge # ============================================================================== # === GLOBAL === @@ -33,6 +37,8 @@ TYPE_PIXEL = Union[int, float, TYPE_iRGB, TYPE_iRGBA, TYPE_fRGB, TYPE_fRGBA] TYPE_IMAGE = Union[np.ndarray, torch.Tensor] +JOV_TYPE_IMAGE = JOV_TYPE_ANY + # ============================================================================== # === ENUMERATION === # ============================================================================== @@ -202,7 +208,7 @@ def cv2tensor_full(image: TYPE_IMAGE, matte:TYPE_PIXEL=(0,0,0,255)) -> Tuple[tor # ============================================================================== class SpoutReaderNode(JOVImageNode): - NAME = "SPOUT READER (JOV_SP) 📺" + NAME = "SPOUT READER" SORT = 50 DESCRIPTION = """ Capture frames from Spout streams. It supports batch processing, allowing multiple frames to be captured simultaneously. The node provides options for configuring the source and number of frames to gather. The captured frames are returned as tensors, enabling further processing downstream. @@ -267,7 +273,7 @@ def run(self, **kw) -> Tuple[torch.Tensor, ...]: return [torch.stack(i) for i in zip(*frames)] class SpoutWriterNode(JOVBaseNode): - NAME = "SPOUT WRITER (JOV_SP) 🎥" + NAME = "SPOUT WRITER" RETURN_TYPES = () OUTPUT_NODE = True SORT = 90 diff --git a/node_list.json b/node_list.json index 8f10ea2..1dc6fff 100644 --- a/node_list.json +++ b/node_list.json @@ -1,4 +1,4 @@ { - "SPOUT READER (JOV_SP) \ud83d\udcfa": "Capture frames from Spout streams", - "SPOUT WRITER (JOV_SP) \ud83c\udfa5": "Sends frame(s) to a specified Spout receiver application for real-time video sharing" + "SPOUT READER (JOV_SPOUT)": "Capture frames from Spout streams", + "SPOUT WRITER (JOV_SPOUT)": "Sends frame(s) to a specified Spout receiver application for real-time video sharing" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 0332476..a3f3360 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "jovi_spout" description = "ComfyUI Nodes for using Spout streams " -version = "1.0.2" +version = "1.0.3" license = { file = "LICENSE" } dependencies = [ "aenum",