Seven production pipeline utility nodes for ComfyUI: - PD Project Context: top-level metadata/token source - PD Shot Token: lightweight template string resolver - PD Output Namer: structured output filename builder - PD Network Output: image writer with UNC path support - PD Switch: multi-channel wildcard input router (up to 8×8) - PD Wedge: iteration node with optional auto-queue injection - PD Rasterizer: dry-run wedge cartesian product previewer Core infrastructure: - core/compat.py: legacy + Nodes 2.0 (V3) API compatibility shim - core/tokens.py: standalone token resolution engine - core/utils.py: path normalization and directory helpers - web/pipedream_rasterizer.js: minimal JS for rasterizer preview https://claude.ai/code/session_015UV4rjSvFatMiNtKhd1sT3
187 lines
6.2 KiB
Python
187 lines
6.2 KiB
Python
"""
|
|
ComfyUI API compatibility shim.
|
|
|
|
Detects whether the V3 (Nodes 2.0) API is available and provides helpers
|
|
so that individual node files can be written once using the legacy class
|
|
pattern and automatically registered for whichever API is present.
|
|
|
|
Node authors write standard legacy-style classes (INPUT_TYPES classmethod,
|
|
RETURN_TYPES tuple, instance ``execute`` method, etc.) and add a
|
|
``DISPLAY_NAME`` class attribute. The helpers here take care of the rest.
|
|
"""
|
|
|
|
import inspect
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# V3 API detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
try:
|
|
from comfy_api.latest import io # type: ignore[import-not-found]
|
|
|
|
V3_AVAILABLE = True
|
|
except Exception:
|
|
V3_AVAILABLE = False
|
|
io = None # type: ignore[assignment]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Type mapping (legacy type string -> V3 io type)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_LEGACY_TO_V3: dict[str, object] = {}
|
|
|
|
if V3_AVAILABLE:
|
|
_LEGACY_TO_V3 = {
|
|
"IMAGE": io.Image,
|
|
"LATENT": io.Latent,
|
|
"STRING": io.String,
|
|
"INT": io.Int,
|
|
"FLOAT": io.Float,
|
|
"BOOLEAN": io.Boolean,
|
|
}
|
|
|
|
|
|
def _v3_type_for(legacy_type: str):
|
|
"""Return the V3 ``io`` type object for a legacy type string."""
|
|
if legacy_type == "*":
|
|
return io.AnyType if hasattr(io, "AnyType") else io.Custom("*")
|
|
if legacy_type in _LEGACY_TO_V3:
|
|
return _LEGACY_TO_V3[legacy_type]
|
|
return io.Custom(legacy_type)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# V3 wrapper factory
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _build_v3_input(name: str, type_str: str, config: dict | None = None):
|
|
"""Create a V3 input descriptor from legacy INPUT_TYPES info."""
|
|
v3_type = _v3_type_for(type_str)
|
|
|
|
kwargs: dict = {}
|
|
if config:
|
|
for key in ("default", "min", "max", "step"):
|
|
if key in config:
|
|
kwargs[key] = config[key]
|
|
if config.get("multiline"):
|
|
kwargs["multiline"] = True
|
|
|
|
return v3_type.Input(name, **kwargs)
|
|
|
|
|
|
def _build_v3_output(type_str: str, display_name: str | None = None):
|
|
"""Create a V3 output descriptor from a legacy RETURN_TYPES entry."""
|
|
v3_type = _v3_type_for(type_str)
|
|
kwargs = {}
|
|
if display_name:
|
|
kwargs["display_name"] = display_name
|
|
return v3_type.Output(**kwargs)
|
|
|
|
|
|
def wrap_as_v3(legacy_cls):
|
|
"""
|
|
Wrap a legacy-style node class as a V3 ``io.ComfyNode`` subclass.
|
|
|
|
Returns a new class that delegates execution to an instance of
|
|
*legacy_cls* while exposing the V3 schema interface.
|
|
"""
|
|
if not V3_AVAILABLE:
|
|
return legacy_cls
|
|
|
|
input_spec = legacy_cls.INPUT_TYPES()
|
|
func_name = getattr(legacy_cls, "FUNCTION", "execute")
|
|
exec_method = getattr(legacy_cls, func_name)
|
|
param_names = [
|
|
p
|
|
for p in inspect.signature(exec_method).parameters
|
|
if p != "self"
|
|
]
|
|
|
|
# Build V3 inputs
|
|
v3_inputs = []
|
|
for section in ("required", "optional"):
|
|
for name, spec in input_spec.get(section, {}).items():
|
|
if isinstance(spec, tuple) and len(spec) >= 1:
|
|
type_str = spec[0]
|
|
config = spec[1] if len(spec) > 1 and isinstance(spec[1], dict) else None
|
|
# COMBO type — spec[0] is a list of strings
|
|
if isinstance(type_str, (list, tuple)):
|
|
inp = io.Combo.Input(name, options=list(type_str))
|
|
else:
|
|
inp = _build_v3_input(name, type_str, config)
|
|
if section == "optional":
|
|
inp = inp.make_optional()
|
|
v3_inputs.append(inp)
|
|
|
|
# Build V3 outputs
|
|
return_types = getattr(legacy_cls, "RETURN_TYPES", ())
|
|
return_names = getattr(legacy_cls, "RETURN_NAMES", (None,) * len(return_types))
|
|
v3_outputs = [
|
|
_build_v3_output(rt, rn) for rt, rn in zip(return_types, return_names)
|
|
]
|
|
|
|
# Capture in closure
|
|
_legacy_cls = legacy_cls
|
|
_param_names = param_names
|
|
_func_name = func_name
|
|
|
|
class V3Wrapper(io.ComfyNode):
|
|
@classmethod
|
|
def define_schema(cls):
|
|
return io.Schema(
|
|
node_id=_legacy_cls.__name__,
|
|
display_name=getattr(_legacy_cls, "DISPLAY_NAME", _legacy_cls.__name__),
|
|
category=getattr(_legacy_cls, "CATEGORY", "Pipedream"),
|
|
description=getattr(_legacy_cls, "DESCRIPTION", ""),
|
|
inputs=v3_inputs,
|
|
outputs=v3_outputs,
|
|
is_output_node=getattr(_legacy_cls, "OUTPUT_NODE", False),
|
|
)
|
|
|
|
@classmethod
|
|
def execute(cls, **kwargs):
|
|
instance = _legacy_cls()
|
|
ordered = [kwargs.get(p) for p in _param_names]
|
|
result = getattr(instance, _func_name)(*ordered)
|
|
if isinstance(result, tuple):
|
|
return io.NodeOutput(*result)
|
|
return io.NodeOutput(result)
|
|
|
|
V3Wrapper.__name__ = _legacy_cls.__name__ + "_V3"
|
|
V3Wrapper.__qualname__ = V3Wrapper.__name__
|
|
return V3Wrapper
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registration helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def get_node_class_mappings(node_classes: list) -> dict:
|
|
"""Build ``NODE_CLASS_MAPPINGS`` dict from a list of legacy node classes."""
|
|
return {cls.__name__: cls for cls in node_classes}
|
|
|
|
|
|
def get_display_name_mappings(node_classes: list) -> dict:
|
|
"""Build ``NODE_DISPLAY_NAME_MAPPINGS`` dict from a list of legacy node classes."""
|
|
return {
|
|
cls.__name__: getattr(cls, "DISPLAY_NAME", cls.__name__)
|
|
for cls in node_classes
|
|
}
|
|
|
|
|
|
def get_v3_nodes(node_classes: list) -> list:
|
|
"""
|
|
Return a list of V3-wrapped node classes.
|
|
|
|
Returns an empty list when the V3 API is not available.
|
|
"""
|
|
if not V3_AVAILABLE:
|
|
return []
|
|
wrapped = []
|
|
for cls in node_classes:
|
|
try:
|
|
wrapped.append(wrap_as_v3(cls))
|
|
except Exception as exc:
|
|
print(f"[Pipedream] Warning: failed to wrap {cls.__name__} for V3: {exc}")
|
|
return wrapped
|