Skip to content

Commit 6cda4ca

Browse files
Merge pull request #8 from ryanontheinside/refactor/initial-refactor
Refactor/initial refactor
2 parents d63bb59 + 0b7659c commit 6cda4ca

25 files changed

+233
-228
lines changed

__init__.py

Lines changed: 74 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,82 @@
1-
from .controls.value_controls import FloatControl, IntControl, StringControl
2-
from .controls.sequence_controls import FloatSequence, IntSequence, StringSequence
3-
from .controls.utility_controls import FPSMonitor, SimilarityFilter, LazyCondition, LogicOperator, IfThenElse
4-
from .controls.state_management_controls import StateResetNode, StateTestNode
5-
from .controls.motion_controls import MotionController, ROINode, IntegerMotionController
6-
from .misc_nodes import(
7-
DTypeConverter,
8-
FastWebcamCapture,
9-
YOLOSimilarityCompare,
10-
TextRenderer,
11-
QuickShapeMask,
12-
MultilineText,
13-
LoadImageFromPath_
14-
)
15-
from .media_pipe_nodes import HandTrackingNode, HandMaskNode
16-
from .controls.mask_controls import RepulsiveMaskNode, ResizeMaskNode
17-
1+
import os
2+
import importlib
3+
import inspect
184
import re
5+
import traceback
196

7+
# Directory containing node wrapper modules relative to this __init__.py
8+
WRAPPER_DIR = "node_wrappers.realtimenodes"
9+
NODE_CLASS_MAPPINGS = {}
10+
NODE_DISPLAY_NAME_MAPPINGS = {}
2011

21-
NODE_CLASS_MAPPINGS = {
22-
"FloatControl": FloatControl,
23-
"IntControl": IntControl,
24-
"StringControl": StringControl,
25-
"FloatSequence": FloatSequence,
26-
"IntSequence": IntSequence,
27-
"StringSequence": StringSequence,
28-
"FPSMonitor": FPSMonitor,
29-
"SimilarityFilter": SimilarityFilter,
30-
"LazyCondition": LazyCondition,
31-
"LogicOperator": LogicOperator,
32-
"IfThenElse": IfThenElse,
33-
"MotionController": MotionController,
34-
"IntegerMotionController": IntegerMotionController,
35-
"YOLOSimilarityCompare": YOLOSimilarityCompare,
36-
"TextRenderer": TextRenderer,
37-
"ROINode": ROINode,
38-
"QuickShapeMask": QuickShapeMask,
39-
"DTypeConverter": DTypeConverter,
40-
"FastWebcamCapture": FastWebcamCapture,
41-
"MultilineText": MultilineText,
42-
"LoadImageFromPath_": LoadImageFromPath_,
43-
"HandTrackingNode": HandTrackingNode,
44-
"HandMaskNode": HandMaskNode,
45-
"RepulsiveMaskNode": RepulsiveMaskNode,
46-
"ResizeMaskNode": ResizeMaskNode,
47-
"StateResetNode": StateResetNode,
48-
"StateTestNode": StateTestNode,
49-
}
12+
print("\033[93m" + "[ComfyUI_RealtimeNodes] Loading nodes...")
5013

14+
def load_nodes_from_directory(package_path):
15+
"""Dynamically loads nodes from Python files in a directory."""
16+
loaded_nodes = {}
17+
loaded_display_names = {}
18+
19+
# Calculate absolute path to the package directory
20+
package_abs_path = os.path.join(os.path.dirname(__file__), *package_path.split('.'))
21+
22+
if not os.path.isdir(package_abs_path):
23+
print(f"\033[91m" + f"[ComfyUI_RealtimeNodes] Warning: Directory not found: {package_abs_path}")
24+
return loaded_nodes, loaded_display_names
5125

26+
print(f"\033[92m" + f"[ComfyUI_RealtimeNodes] Searching for nodes in: {package_abs_path}")
5227

28+
for filename in os.listdir(package_abs_path):
29+
if filename.endswith(".py") and not filename.startswith('__'):
30+
module_name = filename[:-3]
31+
full_module_path = f".{package_path}.{module_name}" # Relative import path
32+
33+
try:
34+
module = importlib.import_module(full_module_path, package=__name__)
35+
36+
# Find classes defined in the module
37+
for name, obj in inspect.getmembers(module):
38+
if inspect.isclass(obj):
39+
# Check if it's a ComfyUI node (heuristic: has INPUT_TYPES)
40+
if hasattr(obj, 'INPUT_TYPES') and callable(obj.INPUT_TYPES):
41+
# Exclude base classes/mixins if they are explicitly named or lack a CATEGORY
42+
if not name.endswith("Base") and not name.endswith("Mixin") and hasattr(obj, 'CATEGORY'):
43+
loaded_nodes[name] = obj
44+
45+
# Generate display name (similar to original logic)
46+
display_name = ' '.join(word.capitalize() for word in re.findall(r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\d|\W|$)|\d+', name))
47+
suffix = " 🕒🅡🅣🅝"
48+
if not display_name.endswith(suffix):
49+
display_name += suffix
50+
loaded_display_names[name] = display_name
51+
print(f"\033[94m" + f"[ComfyUI_RealtimeNodes] - Loaded node: {name} -> {display_name}")
52+
53+
except ImportError as e:
54+
print(f"\033[91m" + f"[ComfyUI_RealtimeNodes] Error importing module {full_module_path}: {e}")
55+
traceback.print_exc()
56+
except Exception as e:
57+
print(f"\033[91m" + f"[ComfyUI_RealtimeNodes] Error processing module {full_module_path}: {e}")
58+
traceback.print_exc()
59+
60+
return loaded_nodes, loaded_display_names
5361

62+
# --- Main Loading Logic ---
5463

64+
# Define the subdirectories within WRAPPER_DIR to scan
65+
# Order might matter if there are dependencies between modules, though ideally there shouldn't be
66+
subdirs_to_scan = [
67+
"controls",
68+
"media",
69+
"utils",
70+
"specialized" # Add other subdirs like 'specialized' if needed
71+
]
72+
73+
# Iterate through specified subdirectories and load nodes
74+
for subdir in subdirs_to_scan:
75+
dir_path = f"{WRAPPER_DIR}.{subdir}"
76+
nodes, display_names = load_nodes_from_directory(dir_path)
77+
NODE_CLASS_MAPPINGS.update(nodes)
78+
NODE_DISPLAY_NAME_MAPPINGS.update(display_names)
5579

56-
NODE_DISPLAY_NAME_MAPPINGS = {}
5780

5881
suffix = " 🕒🅡🅣🅝"
5982

@@ -70,8 +93,9 @@
7093

7194
# Assign the final display name to the mappings
7295
NODE_DISPLAY_NAME_MAPPINGS[node_name] = display_name
96+
# --- Original Web Directory and Export ---
97+
WEB_DIRECTORY = "./web/js" # Adjusted path if needed
7398

99+
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
74100

75-
WEB_DIRECTORY = "./web/js"
76-
77-
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
101+
print("\033[92m" + f"[ComfyUI_RealtimeNodes] Loaded {len(NODE_CLASS_MAPPINGS)} nodes.")

node_wrappers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This file intentionally left blank
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This file intentionally left blank
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This file intentionally left blank

controls/mask_controls.py renamed to node_wrappers/realtimenodes/controls/mask_controls.py

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,8 @@
11
import torch
22
import numpy as np
33
import cv2
4-
from ..base.control_base import ControlNodeBase
5-
6-
class MaskControlMixin:
7-
"""Mixin providing common mask functionality"""
8-
def create_circle_mask(self, height, width, center_y, center_x, size):
9-
"""Create a circular mask with anti-aliased edges"""
10-
y, x = np.ogrid[:height, :width]
11-
radius = size * min(height, width) / 2
12-
dist = np.sqrt((x - center_x)**2 + (y - center_y)**2)
13-
mask = np.clip(radius + 1 - dist, 0, 1).astype(np.float32)
14-
return mask
15-
16-
def get_initial_state(self, x_pos, y_pos, size, min_size=None, max_size=None):
17-
"""Get initial state dictionary"""
18-
state = {
19-
"x": x_pos,
20-
"y": y_pos,
21-
"size": size,
22-
"initialized": True
23-
}
24-
if min_size is not None:
25-
state["min_size"] = min_size
26-
if max_size is not None:
27-
state["max_size"] = max_size
28-
return state
4+
from ....src.realtimenodes.control_base import ControlNodeBase
5+
from ....src.realtimenodes.mask_controls import MaskControlMixin
296

307
class RepulsiveMaskNode(ControlNodeBase, MaskControlMixin):
318
"""Node that maintains a mask that repulses or attracts to input masks"""

controls/motion_controls.py renamed to node_wrappers/realtimenodes/controls/motion_controls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from ..base.control_base import ControlNodeBase
1+
from ....src.realtimenodes.control_base import ControlNodeBase
22
from enum import Enum
33
import numpy as np
44
import torch

controls/sequence_controls.py renamed to node_wrappers/realtimenodes/controls/sequence_controls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from ..base.control_base import ControlNodeBase
1+
from ....src.realtimenodes.control_base import ControlNodeBase
22

33
class SequenceControlBase(ControlNodeBase):
44
"""Base class for sequence-based controls"""

controls/state_management_controls.py renamed to node_wrappers/realtimenodes/controls/state_management_controls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from ..base.control_base import ControlNodeBase
1+
from ....src.realtimenodes.control_base import ControlNodeBase
22

33
class StateResetNode(ControlNodeBase):
44
"""Node that resets all control node states when triggered"""

controls/value_controls.py renamed to node_wrappers/realtimenodes/controls/value_controls.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from ..base.control_base import ControlNodeBase
2-
from ..patterns.movement_patterns import MOVEMENT_PATTERNS
1+
from ....src.realtimenodes.control_base import ControlNodeBase
2+
from ....src.realtimenodes.patterns.movement_patterns import MOVEMENT_PATTERNS
33

44
class ValueControlBase(ControlNodeBase):
55
"""Base class for float and integer control nodes"""
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from ....src.utils.general_utils import AlwaysEqualProxy
2+
from ....src.realtimenodes.control_base import ControlNodeBase
3+
4+
class LazyCondition(ControlNodeBase):
5+
DESCRIPTION = "Uses lazy evaluation to truly skip execution of unused paths. Maintains state of the last value to circumvent feedback loops."
6+
7+
@classmethod
8+
def INPUT_TYPES(s):
9+
return {
10+
"required": {
11+
"condition": (AlwaysEqualProxy("*"), {
12+
"tooltip": "When truthy (non-zero, non-empty, non-None), evaluates and returns if_true path. When falsy, returns either fallback or previous state of if_true.",
13+
"forceInput": True,
14+
}),
15+
"if_true": (AlwaysEqualProxy("*"), {
16+
"lazy": True,
17+
"tooltip": "The path that should only be evaluated when condition is truthy"
18+
}),
19+
"fallback": (AlwaysEqualProxy("*"), {
20+
"tooltip": "Alternative value to use when condition is falsy or no previous state of if_true"
21+
}),
22+
"use_fallback": ("BOOLEAN", {
23+
"default": False,
24+
"tooltip": "When False, uses last successful if_true result (if one exists). When True, uses fallback value"
25+
}),
26+
}
27+
}
28+
29+
RETURN_TYPES = (AlwaysEqualProxy("*"),)
30+
FUNCTION = "update"
31+
CATEGORY = "real-time/control/utility"
32+
33+
def check_lazy_status(self, condition, if_true, fallback, use_fallback):
34+
"""Only evaluate the if_true path if condition is truthy."""
35+
needed = ["fallback"] # Always need the fallback value
36+
if condition:
37+
needed.append("if_true")
38+
return needed
39+
40+
def update(self, condition, if_true, fallback, use_fallback):
41+
"""Route to either if_true output or fallback value, maintaining state of last if_true."""
42+
state = self.get_state({
43+
"prev_output": None
44+
})
45+
46+
if condition: # Let Python handle truthiness
47+
# Update last state when we run if_true path
48+
state["prev_output"] = if_true if if_true is not None else fallback
49+
if hasattr(if_true, 'detach'):
50+
state["prev_output"] = if_true.detach().clone()
51+
self.set_state(state)
52+
return (if_true,)
53+
else:
54+
if use_fallback or state["prev_output"] is None:
55+
return (fallback,)
56+
else:
57+
return (state["prev_output"],)
58+
59+
class LogicOperator(ControlNodeBase):
60+
DESCRIPTION = "Performs logical operations (AND, OR, NOT, XOR) on inputs based on their truthiness"
61+
62+
@classmethod
63+
def INPUT_TYPES(s):
64+
return {
65+
"required": {
66+
"operation": (["AND", "OR", "NOT", "XOR"], {
67+
"default": "AND",
68+
"tooltip": "Logical operation to perform"
69+
}),
70+
"input_a": (AlwaysEqualProxy("*"), {
71+
"tooltip": "First input to evaluate for truthiness",
72+
"forceInput": True,
73+
}),
74+
"always_execute": ("BOOLEAN", {
75+
"default": True,
76+
}),
77+
},
78+
"optional": {
79+
"input_b": (AlwaysEqualProxy("*"), {
80+
"tooltip": "Second input to evaluate for truthiness (not used for NOT operation)",
81+
}),
82+
}
83+
}
84+
85+
RETURN_TYPES = ("BOOLEAN",)
86+
FUNCTION = "update"
87+
CATEGORY = "real-time/control/logic"
88+
89+
def update(self, operation, input_a, always_execute=True, input_b=None):
90+
"""Perform the selected logical operation on inputs based on their truthiness."""
91+
a = bool(input_a)
92+
93+
if operation == "NOT":
94+
return (not a,)
95+
96+
# For all other operations, we need input_b
97+
b = bool(input_b)
98+
99+
if operation == "AND":
100+
return (a and b,)
101+
elif operation == "OR":
102+
return (a or b,)
103+
elif operation == "XOR":
104+
return (a != b,)
105+
106+
# Should never get here, but just in case
107+
return (False,)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This file intentionally left blank
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This file intentionally left blank
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# This file intentionally left blank

0 commit comments

Comments
 (0)