101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
"""
|
|
Subworkflow: loads an inner workflow (UI or API format), infers its I/O from
|
|
Subworkflow Input / Subworkflow Output nodes, and executes it as a subgraph expansion.
|
|
"""
|
|
import logging
|
|
|
|
from .workflow_utils import (
|
|
list_workflow_files,
|
|
load_workflow,
|
|
get_workflow_io,
|
|
build_expansion,
|
|
apply_control_after_generate,
|
|
MAX_SLOTS,
|
|
PLACEHOLDER,
|
|
)
|
|
|
|
log = logging.getLogger("ComfyUI-Subworkflow")
|
|
|
|
# A string subclass whose __ne__ always returns False so that ComfyUI's
|
|
# type-matching considers it equal to any other type string.
|
|
class _AnyType(str):
|
|
def __eq__(self, _):
|
|
return True
|
|
def __ne__(self, _):
|
|
return False
|
|
|
|
_ANY = _AnyType("*")
|
|
|
|
|
|
class Subworkflow:
|
|
"""
|
|
Selects a saved API-format workflow, exposes its Subworkflow Input nodes as
|
|
inputs and its Subworkflow Output nodes as outputs, then executes the inner
|
|
workflow as a transparent subgraph when the outer workflow runs.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
workflows = list_workflow_files()
|
|
return {
|
|
"required": {
|
|
"workflow": (workflows, {}),
|
|
"reload_each_execution": (
|
|
"BOOLEAN",
|
|
{"default": True, "label_on": "reload", "label_off": "keep loaded"},
|
|
),
|
|
},
|
|
}
|
|
|
|
# Fixed upper bound; the JS extension hides the unused tail slots.
|
|
RETURN_TYPES = (_ANY,) * MAX_SLOTS
|
|
RETURN_NAMES = tuple(f"out_{i}" for i in range(MAX_SLOTS))
|
|
FUNCTION = "execute"
|
|
CATEGORY = "subworkflow"
|
|
|
|
@classmethod
|
|
def VALIDATE_INPUTS(cls, input_types=None, **kwargs):
|
|
# Accept any incoming types for dynamic swf_in_* inputs.
|
|
return True
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, workflow: str, reload_each_execution=True, **kwargs):
|
|
# The inner workflow can change between runs by file reload or by cached
|
|
# control-after-generate mutations, even when outer inputs are unchanged.
|
|
return float("NaN")
|
|
|
|
_loaded_workflows: dict[str, dict] = {}
|
|
|
|
@classmethod
|
|
def _get_workflow_data(cls, workflow: str, reload_each_execution: bool) -> dict:
|
|
if reload_each_execution or workflow not in cls._loaded_workflows:
|
|
reason = "reload_each_execution enabled" if reload_each_execution else "cache miss"
|
|
log.info("Subworkflow: loading inner workflow %r (%s)", workflow, reason)
|
|
cls._loaded_workflows[workflow] = load_workflow(workflow)
|
|
else:
|
|
log.info("Subworkflow: reusing cached inner workflow %r", workflow)
|
|
return cls._loaded_workflows[workflow]
|
|
|
|
@classmethod
|
|
def execute(cls, workflow: str, reload_each_execution=True, **kwargs):
|
|
if not workflow or workflow.startswith("["):
|
|
raise ValueError("No workflow selected. Choose a workflow file from the dropdown.")
|
|
log.info(
|
|
"Subworkflow: executing inner workflow %r (reload_each_execution=%s)",
|
|
workflow,
|
|
reload_each_execution,
|
|
)
|
|
try:
|
|
data = cls._get_workflow_data(workflow, reload_each_execution)
|
|
except FileNotFoundError:
|
|
raise ValueError(f"Workflow file not found: {workflow!r}")
|
|
|
|
output_refs, graph = build_expansion(data, kwargs)
|
|
if not reload_each_execution:
|
|
apply_control_after_generate(data)
|
|
|
|
while len(output_refs) < MAX_SLOTS:
|
|
output_refs.append(None)
|
|
|
|
return {"result": tuple(output_refs), "expand": graph.finalize()}
|