Files

249 lines
7.6 KiB
Python

import os
import json
from comfy_execution.graph_utils import ExecutionBlocker
from .utils import AnyType, ContainsDynamicDict, Closure, create_graph_from_closure
RECURISON_LIMIT = int(os.getenv("COMFYUI_FUNCTIONAL_RECURSION_LIMIT", "100"))
COROUTINE_LIMIT = int(os.getenv("COMFYUI_FUNCTIONAL_COROUTINE_LIMIT", "100"))
class FunctionParam:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"index": ("INT", {"default": 0, "min": 0, "max": 1000, "step": 1}),
},
}
RETURN_TYPES = (AnyType("*"),)
FUNCTION = "run"
CATEGORY = "duanyll/functional"
def run(self, index):
return (
ExecutionBlocker(
"All outputs of nodes that directly or indirectly depend on the Function "
"Parameter node must ultimately connect to a Function End node or an Inspect "
"node. An output node is incorrectly connected, as it depends on the Function "
"Parameter node without passing through a Function End or Inspect node. To "
"view temporary results within the function, please use an Inspect node before "
"the output node. Check your node connections and try again."
),
)
class FunctionEnd:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"return_value": (AnyType("*"),),
"capture": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("CLOSURE",)
FUNCTION = "run"
CATEGORY = "duanyll/functional"
def run(self, return_value, capture):
raise NotImplementedError(
"The Function End node is a marker and should never be executed directly. If you see this error, please file a bug report."
)
class CreateClosure:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"body": ("STRING", {"multiline": True}),
"output": ("STRING",),
"side_effects": ("FLOAT",),
},
"optional": ContainsDynamicDict(
{
"capture_0": (AnyType("*"), {"_dynamic": "number"}),
}
),
}
RETURN_TYPES = ("CLOSURE",)
FUNCTION = "run"
CATEGORY = "duanyll/functional/internal"
def run(self, body, output, side_effects, **kwargs):
# kwargs: capture_0, capture_1, ...
captures = []
for i in range(len(kwargs)):
captures.append(kwargs[f"capture_{i}"])
body = json.loads(body)
output = json.loads(output)
return (
Closure(
body=body,
output=output,
captures=captures,
),
)
class CallClosure:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"closure": ("CLOSURE",),
},
"optional": ContainsDynamicDict(
{
"param_0": (AnyType("*"), {"_dynamic": "number"}),
}
),
"hidden": {"unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = (AnyType("*"),)
FUNCTION = "run"
CATEGORY = "duanyll/functional"
def run(self, closure, unique_id, **kwargs):
if len(unique_id) > RECURISON_LIMIT:
raise RecursionError(
"Function call recursion limit exceeded. Possible infinite recursion "
"detected. If you are intentionally using deep recursion, you can increase "
"the limit by setting the COMFYUI_FUNCTIONAL_RECURSION_LIMIT environment variable."
)
# kwargs: param_0, param_1, ...
params = []
for i in range(len(kwargs)):
params.append(kwargs[f"param_{i}"])
graph, output = create_graph_from_closure(closure, params, caller_unique_id=unique_id)
if len(graph) == 0:
return (output,)
return {"result": (output,), "expand": graph}
class IntermidiateCoroutine:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"return_value": (AnyType("*"),),
"coroutine": (AnyType("*"),),
},
"hidden": {"unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = (AnyType("*"),)
FUNCTION = "run"
CATEGORY = "duanyll/functional/internal"
def run(self, return_value, coroutine, unique_id):
(base_id, index) = unique_id.rsplit("_", 1)
graph = {}
while len(graph) == 0:
try:
(closure, params) = coroutine.send(return_value)
except StopIteration as e:
return (e.value,)
index = int(index) + 1
next_node_id = f"{base_id}_{index}"
if index > COROUTINE_LIMIT:
raise RecursionError(
"Coroutine execution limit exceeded. Possible infinite loop "
"detected. If you are intentionally using long-running coroutines, "
"you can increase the limit by setting the COMFYUI_FUNCTIONAL_COROUTINE_LIMIT environment variable."
)
graph, output = create_graph_from_closure(closure, params, caller_unique_id=unique_id)
if len(graph) == 0:
return_value = output
return {
"result": ([next_node_id, 0],),
"expand": {
**graph,
next_node_id: {
"inputs": {
"return_value": output,
"coroutine": coroutine,
},
"class_type": "__IntermidiateCoroutine__",
},
},
}
class CoroutineNodeBase:
@classmethod
def INPUT_TYPES(cls):
raise NotImplementedError("This is an abstract base class and should not be instantiated directly.")
RETURN_TYPES = (AnyType("*"),)
FUNCTION = "run"
def run_coroutine(self, **kwargs):
raise NotImplementedError("Subclasses must implement the run_coroutine method as a generator.")
yield # This is just to make this function a generator
def run(self, unique_id, **kwargs):
if isinstance(unique_id, list):
unique_id = unique_id[0]
coroutine = self.run_coroutine(**kwargs)
return {
"result": ([f"{unique_id}_0", 0],),
"expand": {
f"{unique_id}_0": {
"inputs": {
"return_value": None,
"coroutine": coroutine,
},
"class_type": "__IntermidiateCoroutine__",
}
},
}
class RecoverList:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"values": (AnyType("*"),),
},
}
RETURN_TYPES = (AnyType("*"),)
FUNCTION = "run"
CATEGORY = "duanyll/functional/internal"
def run(self, values):
return values
NODE_CLASS_MAPPINGS = {
"__FunctionParam__": FunctionParam,
"__FunctionEnd__": FunctionEnd,
"__CreateClosure__": CreateClosure,
"__IntermidiateCoroutine__": IntermidiateCoroutine,
"__RecoverList__": RecoverList,
"CallClosure": CallClosure,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"__FunctionParam__": "Function Parameter",
"__FunctionEnd__": "Function End",
"__CreateClosure__": "[Internal] Create Closure",
"__IntermidiateCoroutine__": "[Internal] Intermidiate Coroutine",
"__RecoverList__": "[Internal] Recover List",
"CallClosure": "Call Function",
}