Modifier now uses internal references, not separate instance of loaded graph. Deprecates the Modifier (from URL) node.
This commit is contained in:
+144
-29
@@ -3,6 +3,7 @@ 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
|
||||
import weakref
|
||||
|
||||
from comfy_api.latest import io
|
||||
from .debug_utils import configure_logger
|
||||
@@ -93,6 +94,8 @@ class BaseSubworkflow(io.ComfyNode):
|
||||
return float("NaN")
|
||||
|
||||
_loaded_workflows: dict[str, dict] = {}
|
||||
_loaded_workflow_instances: dict[str, tuple[str, dict]] = {}
|
||||
_prompt_workflows = weakref.WeakKeyDictionary()
|
||||
|
||||
@classmethod
|
||||
def _source_label(cls, **kwargs) -> str:
|
||||
@@ -107,16 +110,72 @@ class BaseSubworkflow(io.ComfyNode):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def _get_workflow_data(cls, reload_each_execution: bool, **kwargs) -> dict:
|
||||
def _get_workflow_data(
|
||||
cls,
|
||||
reload_each_execution: bool,
|
||||
*,
|
||||
instance_id: str | None = None,
|
||||
dynprompt=None,
|
||||
**kwargs,
|
||||
) -> dict:
|
||||
cache_key = cls._source_cache_key(**kwargs)
|
||||
label = cls._source_label(**kwargs)
|
||||
if reload_each_execution or cache_key not in cls._loaded_workflows:
|
||||
|
||||
prompt_cache = None
|
||||
prompt_cache_key = None
|
||||
if dynprompt is not None and instance_id is not None:
|
||||
try:
|
||||
prompt_cache = cls._prompt_workflows.setdefault(dynprompt, {})
|
||||
prompt_cache_key = (str(instance_id), cache_key)
|
||||
if prompt_cache_key in prompt_cache:
|
||||
log.debug(
|
||||
"[Subworkflow] reusing prompt-scoped inner workflow %r for node %s",
|
||||
label,
|
||||
instance_id,
|
||||
)
|
||||
return prompt_cache[prompt_cache_key]
|
||||
except TypeError:
|
||||
# Some test and mock prompt objects may not support weak references.
|
||||
prompt_cache = None
|
||||
prompt_cache_key = None
|
||||
|
||||
instance_key = str(instance_id) if instance_id is not None else None
|
||||
instance_cached = (
|
||||
cls._loaded_workflow_instances.get(instance_key)
|
||||
if instance_key is not None
|
||||
else None
|
||||
)
|
||||
instance_cache_hit = (
|
||||
instance_cached is not None and instance_cached[0] == cache_key
|
||||
)
|
||||
|
||||
if reload_each_execution or (
|
||||
instance_key is not None and not instance_cache_hit
|
||||
) or (
|
||||
instance_key is None and cache_key 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)", label, reason)
|
||||
cls._loaded_workflows[cache_key] = cls._load_source(**kwargs)
|
||||
log.info(
|
||||
"[Subworkflow] loading inner workflow %r for node %s (%s)",
|
||||
label,
|
||||
instance_id or "shared source",
|
||||
reason,
|
||||
)
|
||||
data = cls._load_source(**kwargs)
|
||||
if instance_key is not None:
|
||||
if not reload_each_execution:
|
||||
cls._loaded_workflow_instances[instance_key] = (cache_key, data)
|
||||
else:
|
||||
cls._loaded_workflows[cache_key] = data
|
||||
elif instance_key is not None:
|
||||
data = instance_cached[1]
|
||||
else:
|
||||
log.debug("[Subworkflow] reusing cached inner workflow %r", label)
|
||||
return cls._loaded_workflows[cache_key]
|
||||
data = cls._loaded_workflows[cache_key]
|
||||
|
||||
if prompt_cache is not None and prompt_cache_key is not None:
|
||||
prompt_cache[prompt_cache_key] = data
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def _execute_source(cls, reload_each_execution=True, **kwargs):
|
||||
@@ -127,7 +186,12 @@ class BaseSubworkflow(io.ComfyNode):
|
||||
reload_each_execution,
|
||||
sorted(k for k in kwargs if k.startswith("swf_in_")),
|
||||
)
|
||||
data = cls._get_workflow_data(reload_each_execution, **kwargs)
|
||||
data = cls._get_workflow_data(
|
||||
reload_each_execution,
|
||||
instance_id=cls.hidden.unique_id,
|
||||
dynprompt=cls.hidden.dynprompt,
|
||||
**kwargs,
|
||||
)
|
||||
validate_workflow_nodes_installed(data)
|
||||
|
||||
effective_inputs = _apply_primitive_overrides(data, kwargs)
|
||||
@@ -176,6 +240,7 @@ class Subworkflow(BaseSubworkflow):
|
||||
io.Combo.Input("workflow", options=list_workflow_files()),
|
||||
_reload_input(),
|
||||
],
|
||||
hidden=[io.Hidden.unique_id, io.Hidden.dynprompt],
|
||||
outputs=_subworkflow_outputs(),
|
||||
enable_expand=True,
|
||||
accept_all_inputs=True,
|
||||
@@ -236,6 +301,7 @@ class SubworkflowFromURL(BaseSubworkflow):
|
||||
),
|
||||
_reload_input(),
|
||||
],
|
||||
hidden=[io.Hidden.unique_id, io.Hidden.dynprompt],
|
||||
outputs=_subworkflow_outputs(),
|
||||
enable_expand=True,
|
||||
accept_all_inputs=True,
|
||||
@@ -300,42 +366,90 @@ class SubworkflowModifierSource(BaseSubworkflow):
|
||||
display_name="Subworkflow Modifier Source",
|
||||
category="Subworkflow",
|
||||
description=(
|
||||
"Exposes all modifier source slots from a selected inner workflow. "
|
||||
"Use this together with the main Subworkflow node to break outer "
|
||||
"modifier dependency loops."
|
||||
"Exposes modifier source slots from another Subworkflow node in "
|
||||
"this workflow. Use this together with that Subworkflow node to "
|
||||
"break outer modifier dependency loops."
|
||||
),
|
||||
inputs=[
|
||||
io.Combo.Input("workflow", options=list_workflow_files()),
|
||||
io.Combo.Input(
|
||||
"target_subworkflow",
|
||||
display_name="Subworkflow node",
|
||||
options=[""],
|
||||
),
|
||||
],
|
||||
hidden=[io.Hidden.unique_id, io.Hidden.dynprompt],
|
||||
outputs=_subworkflow_outputs(),
|
||||
enable_expand=True,
|
||||
accept_all_inputs=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _source_label(cls, workflow: str, **kwargs) -> str:
|
||||
return workflow
|
||||
def _target_node_id(cls, target_subworkflow: str) -> str:
|
||||
return str(target_subworkflow or "").split(" | ", 1)[0].strip()
|
||||
|
||||
@classmethod
|
||||
def _source_cache_key(cls, workflow: str, **kwargs) -> str:
|
||||
return f"file:{workflow}"
|
||||
def _resolve_target(cls, target_subworkflow: str):
|
||||
target_id = cls._target_node_id(target_subworkflow)
|
||||
if not target_id:
|
||||
raise ValueError("No Subworkflow node selected.")
|
||||
|
||||
dynprompt = cls.hidden.dynprompt
|
||||
if dynprompt is None or not dynprompt.has_node(target_id):
|
||||
raise ValueError(
|
||||
f"Selected Subworkflow node {target_id!r} is not present in the queued workflow."
|
||||
)
|
||||
|
||||
target = dynprompt.get_node(target_id)
|
||||
inputs = target.get("inputs") or {}
|
||||
class_type = target.get("class_type")
|
||||
if class_type == "SWF_Subworkflow":
|
||||
workflow = inputs.get("workflow")
|
||||
if not isinstance(workflow, str) or not workflow:
|
||||
raise ValueError(
|
||||
f"Selected Subworkflow node {target_id!r} has no workflow selected."
|
||||
)
|
||||
source_class = Subworkflow
|
||||
source_kwargs = {"workflow": workflow}
|
||||
elif class_type == "SWF_SubworkflowFromURL":
|
||||
url = inputs.get("url")
|
||||
verify_ssl = inputs.get("verify_ssl", True)
|
||||
if not isinstance(url, str) or not url:
|
||||
raise ValueError(
|
||||
f"Selected Subworkflow node {target_id!r} has no workflow URL."
|
||||
)
|
||||
if not isinstance(verify_ssl, bool):
|
||||
raise ValueError(
|
||||
f"Selected Subworkflow node {target_id!r} has an invalid verify-SSL setting."
|
||||
)
|
||||
source_class = SubworkflowFromURL
|
||||
source_kwargs = {"url": url, "verify_ssl": verify_ssl}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Selected node {target_id!r} is not a Subworkflow node."
|
||||
)
|
||||
|
||||
reload_each_execution = inputs.get("reload_each_execution", True)
|
||||
if not isinstance(reload_each_execution, bool):
|
||||
raise ValueError(
|
||||
f"Selected Subworkflow node {target_id!r} has an invalid at-execution setting."
|
||||
)
|
||||
return target_id, source_class, source_kwargs, reload_each_execution
|
||||
|
||||
@classmethod
|
||||
def _load_source(cls, workflow: str, **kwargs) -> dict:
|
||||
if not workflow:
|
||||
raise ValueError("No workflow selected. Choose a workflow file from the dropdown.")
|
||||
try:
|
||||
return load_workflow_file(workflow)
|
||||
except FileNotFoundError:
|
||||
raise ValueError(f"Workflow file not found: {workflow!r}") from None
|
||||
|
||||
@classmethod
|
||||
def execute(cls, workflow: str, **kwargs):
|
||||
reload_each_execution = True
|
||||
data = cls._get_workflow_data(reload_each_execution, workflow=workflow, **kwargs)
|
||||
def execute(cls, target_subworkflow: str, **kwargs):
|
||||
target_id, source_class, source_kwargs, reload_each_execution = (
|
||||
cls._resolve_target(target_subworkflow)
|
||||
)
|
||||
data = source_class._get_workflow_data(
|
||||
reload_each_execution,
|
||||
instance_id=target_id,
|
||||
dynprompt=cls.hidden.dynprompt,
|
||||
**source_kwargs,
|
||||
)
|
||||
validate_workflow_nodes_installed(data)
|
||||
output_refs, graph = build_modifier_source_expansion(data, kwargs)
|
||||
return cls._finalize_execution(output_refs, graph, data, reload_each_execution)
|
||||
# The selected Subworkflow owns control-after-generate mutations.
|
||||
return io.NodeOutput(*output_refs, expand=graph.finalize())
|
||||
|
||||
|
||||
class SubworkflowModifierSourceFromURL(BaseSubworkflow):
|
||||
@@ -345,9 +459,10 @@ class SubworkflowModifierSourceFromURL(BaseSubworkflow):
|
||||
node_id="SWF_SubworkflowModifierSourceFromURL",
|
||||
display_name="Subworkflow Modifier Source (from URL)",
|
||||
category="Subworkflow",
|
||||
is_deprecated=True,
|
||||
description=(
|
||||
"Exposes all modifier source slots from a workflow loaded from an "
|
||||
"HTTP(S) URL."
|
||||
"Deprecated compatibility node. Use Subworkflow Modifier Source "
|
||||
"and select a Subworkflow (from URL) node instead."
|
||||
),
|
||||
inputs=[
|
||||
io.String.Input("url", multiline=False, default=""),
|
||||
|
||||
Reference in New Issue
Block a user