diff --git a/README.md b/README.md index fbc683d..df4678f 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ Tips: base64格式字符串比较长,会导致界面卡顿,接口请求带 | × | TryFreeMemory | 回收内(显)存 | | × | IfElseForEmptyObject | 可以对list类型进行判断 | | × | ImageSizeGetter | 获取图片尺寸(宽、高、最大边、最小边、批次) | +| × | FilterSortDependSubGraphs | 使前置依赖子图按指定顺序执行(且只执行配置的前置依赖),如:配置filter_sort为1,4,3 表示按 depend_1 => depend_4 => depend_3 依次执行, 而depend_2不会被执行。 | +| × | SortDependSubGraphs | 使前置依赖子图按指定顺序执行(未配置的依赖输入项在指定项后按默认顺序执行),如:配置sort为1,4 表示先按 depend_1 => depend_4 依次执行, 然后depend_3和depend_2按默认顺序执行。 | ### 示例 ![save api extended](docs/example_note.png) @@ -101,6 +103,10 @@ Tips: base64格式字符串比较长,会导致界面卡顿,接口请求带 ![批量裁剪打标](example/example_image_crop_tag.png) ## 更新记录 +### 2025-03-28 (v1.1.2) +- 新增节点 ImageSizeGetter、FilterSortDependSubGraphs、SortDependSubGraphs + ![依赖项拓扑顺序](docs/sort_subgraphs.gif) + ![依赖项拓扑过滤和顺序](docs/filter_sort_subgraphs.gif) ### 2025-02-19 (v1.1.1) - 新增节点:IsNoneOrEmptyOptional ### 2024-12-01 (v1.1.0) diff --git a/docs/filter_sort_subgraphs.gif b/docs/filter_sort_subgraphs.gif new file mode 100644 index 0000000..c80bfa6 Binary files /dev/null and b/docs/filter_sort_subgraphs.gif differ diff --git a/docs/sort_subgraphs.gif b/docs/sort_subgraphs.gif new file mode 100644 index 0000000..a25db8d Binary files /dev/null and b/docs/sort_subgraphs.gif differ diff --git a/easyapi/ForEachNode.py b/easyapi/ForEachNode.py index 5111298..46ea8ae 100644 --- a/easyapi/ForEachNode.py +++ b/easyapi/ForEachNode.py @@ -1,5 +1,5 @@ from comfy_execution.graph_utils import GraphBuilder, is_link -from .util import any_type +from .util import any_type, find_max_suffix_number # 支持的最大参数个数 NUM_FLOW_SOCKETS = 20 @@ -165,19 +165,7 @@ class InnerLoopClose: def find_max_initial_value_number(kwargs, substring): - # 提取所有键 - keys = list(kwargs.keys()) - - # 筛选出形如 'initial_valueX' 的键 - matching_keys = [key for key in keys if key.startswith('initial_value')] - - # 从匹配的键中提取数字部分 - numbers = [int(key[len('initial_value'):]) for key in matching_keys] - - # 找到最大数字 - max_number = max(numbers) if numbers else 1 - - return max_number + return find_max_suffix_number(kwargs, substring) class ForEachOpen: diff --git a/easyapi/UtilNode.py b/easyapi/UtilNode.py index e819d45..f76396d 100644 --- a/easyapi/UtilNode.py +++ b/easyapi/UtilNode.py @@ -12,7 +12,7 @@ from comfy.model_patcher import ModelPatcher import comfy.model_base import comfy.model_management as mm from server import PromptServer -from .util import tensor_to_pil, hex_to_rgba, any_type +from .util import hex_to_rgba, any_type, find_max_suffix_number class GetImageBatchSize: @@ -178,10 +178,8 @@ class ListMerge: @classmethod def INPUT_TYPES(self): return { - "required": { - "list_a": ('LIST', {"forceInput": True}), - }, "optional": { + "list_a": ('LIST', {"forceInput": True}), "list_b": ('LIST', {"forceInput": True}), } } @@ -193,9 +191,11 @@ class ListMerge: OUTPUT_NODE = False CATEGORY = "EasyApi/List" - DESCRIPTION = "合并两个列表。如 [1,2] 和 [3,4] => [1,2,3,4]" + DESCRIPTION = "合并两个列表。如 [1,2] 和 [3,4] => [1,2,3,4], 都不存在时返回空列表" - def convert(self, list_a, list_b=None): + def convert(self, list_a=None, list_b=None): + if list_a is None: + list_a = [] list = [] + list_a if list_b: list = list + list_b @@ -416,10 +416,9 @@ class SDBaseVerNumber: class ListWrapper: @classmethod def INPUT_TYPES(self): - return {"required": { - "any_1": (any_type, {"forceInput": True}), - }, + return { "optional": { + "any_1": (any_type, {"forceInput": True}), "any_2": (any_type, {"forceInput": True}), }, } @@ -434,14 +433,16 @@ class ListWrapper: INPUT_IS_LIST = False OUTPUT_IS_LIST = (False, ) - DESCRIPTION = "把输入放到一个列表中,如 [a,b]和[c] => [[a,b],[c]]" + DESCRIPTION = "把输入放到一个列表中,如 [a,b]和[c] => [[a,b],[c]], 都不存在时返回None" - def wrapper(self, any_1, any_2=None): - if any_1 is None: + def wrapper(self, any_1=None, any_2=None): + if any_1 is None and any_2 is None: return None, else: if any_2 is None: return ([any_1,],) + elif any_1 is None: + return ([any_2,],) else: return ([any_1, any_2],) @@ -598,18 +599,19 @@ class LoadJsonStrToList: } } - RETURN_TYPES = ("LIST",) + RETURN_TYPES = ("LIST", "INT", ) + RETURN_NAMES = ("LIST", "length", ) CATEGORY = "EasyApi/Utils" FUNCTION = "load_json" def load_json(self, json_str: str): if len(json_str.strip()) == 0: - return ([],) + return [], 0, json = simplejson.loads(json_str) if isinstance(json, (list, tuple)): - return (json,) + return json, len(json) else: - return ([json],) + return [json], 1 class ConvertToJsonStr: @@ -1084,6 +1086,103 @@ class TryFreeMemory: return (a,) +NUM_FLOW_SOCKETS = 20 +class FilterSortDependSubGraphs: + def __init__(self): + self.count = 0 + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "filter_sort": ("STRING", {"default": "1", "tooltip": "按顺序执行指定的前置依赖子图,为空时都不执行,返回结果都为None" + "\n如:1,4,3 表示按 depend_1 => depend_4 => depend_3 依次执行, 而depend_2不会被执行"}), + }, + "optional": { + "depend_%d" % i: (any_type, {"lazy": True}) for i in range(1, NUM_FLOW_SOCKETS) + }, + } + + RETURN_TYPES = tuple([any_type] * (NUM_FLOW_SOCKETS - 1)) + RETURN_NAMES = tuple(["value%d" % i for i in range(1, NUM_FLOW_SOCKETS)]) + FUNCTION = "sort_graph" + + CATEGORY = "EasyApi/Logic" + DESCRIPTION = ("使前置依赖子图按指定顺序执行(且只执行配置的前置依赖)\n" + "如:配置filter_sort为1,4,3 表示按 depend_1 => depend_4 => depend_3 依次执行, 而depend_2不会被执行。") + + def sort_graph(self, filter_sort, **kwargs): + self.count = 0 + max_num = find_max_suffix_number(kwargs, "depend_") + indexes = [int(item.strip()) for item in filter_sort.split(",") if len(item.strip()) > 0] + + outputs = [kwargs.get("depend_%d" % i, None) if i in indexes else None for i in range(1, max_num + 1)] + return outputs + + def check_lazy_status(self, filter_sort, **kwargs): + if filter_sort and len(filter_sort.strip()) > 0: + indexes = [int(item.strip()) for item in filter_sort.split(",") if "depend_%d" % int(item.strip()) in kwargs.keys()] + for i in indexes: + if self.count < len(indexes): + index = indexes[self.count] + input_name = "depend_%d" % index + self.count += 1 + if kwargs.get(input_name, None) is None: + # 找到没有缓存的输入项,解决靠后的依赖节点变化后没有被执行的问题 + return [input_name] + else: + break + + +class SortDependSubGraphs: + def __init__(self): + self.count = 0 + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "sort": ("STRING", {"default": "", "tooltip": "按顺序执行指定的前置依赖子图,为空时按默认顺序执行" + "\n如:1,4 表示先按 depend_1 => depend_4 依次执行, 而depend_3和depend_2按默认顺序执行"}), + }, + "optional": { + "depend_%d" % i: (any_type, {"lazy": True}) for i in range(1, NUM_FLOW_SOCKETS) + }, + } + + RETURN_TYPES = tuple([any_type] * (NUM_FLOW_SOCKETS - 1)) + RETURN_NAMES = tuple(["value%d" % i for i in range(1, NUM_FLOW_SOCKETS)]) + FUNCTION = "sort_graph" + + CATEGORY = "EasyApi/Logic" + DESCRIPTION = ("使前置依赖子图按指定顺序执行(未配置的依赖输入项在指定项后按默认顺序执行)" + "\n如:配置sort为1,4 表示先按 depend_1 => depend_4 依次执行, 而depend_3和depend_2按默认顺序执行。") + + def sort_graph(self, sort, **kwargs): + self.count = 0 + max_num = find_max_suffix_number(kwargs, "depend_") + + outputs = [kwargs.get("depend_%d" % i, None) for i in range(1, max_num + 1)] + return outputs + + def check_lazy_status(self, sort, **kwargs): + if sort and len(sort.strip()) > 0: + indexes = [int(item.strip()) for item in sort.split(",") if "depend_%d" % int(item.strip()) in kwargs.keys()] + for i in indexes: + if self.count < len(indexes): + index = indexes[self.count] + input_name = "depend_%d" % index + self.count += 1 + if kwargs.get(input_name, None) is None: + # 找到没有缓存的输入项,解决靠后的依赖节点变化后没有被执行的问题 + return [input_name] + + max_num = find_max_suffix_number(kwargs, "depend_") + return ["depend_%d" % j for j in range(1, max_num + 1) if j not in indexes and "depend_%d" % j in kwargs.keys() and kwargs.get("depend_%d" % j, None) is None] + else: + return [i for i in kwargs.keys() if kwargs.get(i, None) is None] + + NODE_CLASS_MAPPINGS = { "GetImageBatchSize": GetImageBatchSize, "JoinList": JoinList, @@ -1121,6 +1220,8 @@ NODE_CLASS_MAPPINGS = { "ReadTextFromLocalFile": ReadTextFromLocalFile, "TryFreeMemory": TryFreeMemory, "IfElseForEmptyObject": IfElseForEmptyObject, + "FilterSortDependSubGraphs": FilterSortDependSubGraphs, + "SortDependSubGraphs": SortDependSubGraphs, } # A dictionary that contains the friendly/humanly readable titles for the nodes @@ -1161,4 +1262,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ReadTextFromLocalFile": "ReadTextFromLocalFile", "TryFreeMemory": "TryFreeMemory", "IfElseForEmptyObject": "If Else For Empty Object", + "FilterSortDependSubGraphs": "Filter And Sort Depend SubGraphs", + "SortDependSubGraphs": "Sort Depend SubGraphs", } diff --git a/easyapi/util.py b/easyapi/util.py index 2da1ec9..00a80ac 100644 --- a/easyapi/util.py +++ b/easyapi/util.py @@ -86,6 +86,22 @@ def hex_to_rgba(hex_color): return r, g, b, a +def find_max_suffix_number(kwargs, substring): + # 提取所有键 + keys = list(kwargs.keys()) + + # 筛选出形如 'initial_valueX' 的键 + matching_keys = [key for key in keys if key.startswith(substring)] + + # 从匹配的键中提取数字部分 + numbers = [int(key[len(substring):]) for key in matching_keys] + + # 找到最大数字 + max_number = max(numbers) if numbers else 1 + + return max_number + + class AnyType(str): """A special class that is always equal in not equal comparisons. Credit to pythongosssss""" diff --git a/pyproject.toml b/pyproject.toml index 22a1188..d820bce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-easyapi-nodes" description = "Provides some features and nodes related to API calls. 开发独立应用调用ComfyUI服务的一些补充节点。" -version = "1.1.1" +version = "1.1.2" license = { file = "LICENSE" } dependencies = ["segment_anything", "simple_lama_inpainting", "insightface", "simplejson"] diff --git a/static/js/easyapi.js b/static/js/easyapi.js index 35327c6..59a74c5 100644 --- a/static/js/easyapi.js +++ b/static/js/easyapi.js @@ -591,19 +591,20 @@ function removeOutSoltAndLink(node, out_slot_i) { } -let filter_node_type = ['ForEachOpen', 'ForEachClose'] -let output_fixed_num_for_filter_node_type = [3, 0] +let filter_node_type = ['ForEachOpen', 'ForEachClose', 'SortDependSubGraphs', 'FilterSortDependSubGraphs'] +let output_fixed_num_for_filter_node_type = [3, 0, 0, 0] +let filter_node_type_input_prefix = ['initial_value', 'initial_value', 'depend_', 'depend_'] app.registerExtension({ name: "Comfy.EasyApi.ForNode", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (filter_node_type.indexOf(nodeData.name) > -1) { - let input_name = "initial_value"; + let input_name = filter_node_type_input_prefix[filter_node_type.indexOf(nodeData.name)]; let output_name = "value"; let fixed_head_input_names = ["flow_control"]; - let fixed_tail_input_names = ["total"]; - // 比py代码中定义少1,因为不含initial_value0 + let fixed_tail_input_names = ["total", "filter_sort", 'sort']; + // 不含initial_value0 let max_number_of_inputs = 19; let out_fixed_num = output_fixed_num_for_filter_node_type[filter_node_type.indexOf(nodeData.name)]; @@ -643,15 +644,15 @@ app.registerExtension({ if (fixed_tail_input_names.indexOf(this.inputs[index].name) > -1 || fixed_head_input_names.indexOf(this.inputs[index].name) > -1) return; - if (this.inputs[0].type == '*') { - const node = app.graph.getNodeById(link_info.origin_id); - let origin_type = node.outputs[link_info.origin_slot].type; - - if (origin_type == '*') { - this.disconnectInput(link_info.target_slot); - return; - } - } + // if (this.inputs[0].type == '*') { + // const node = app.graph.getNodeById(link_info.origin_id); + // let origin_type = node.outputs[link_info.origin_slot].type; + // + // if (origin_type == '*') { + // this.disconnectInput(link_info.target_slot); + // return; + // } + // } let fixed_head_solts = this.inputs.filter(x => fixed_head_input_names.indexOf(x.name) > -1); let fixed_head_solt_count = fixed_head_solts ? fixed_head_solts.length : 0; @@ -731,10 +732,10 @@ app.registerExtension({ async nodeCreated(node, app) { // Fires every time a node is constructed // You can modify widgets/add handlers/etc here - if (filter_node_type.indexOf(node.title) > -1) { - let out_fixed_num = output_fixed_num_for_filter_node_type[filter_node_type.indexOf(node.title)]; + if (filter_node_type.indexOf(node.comfyClass) > -1) { + let out_fixed_num = output_fixed_num_for_filter_node_type[filter_node_type.indexOf(node.comfyClass)]; if (node.id == -1) { - let input_name = "initial_value"; + let input_name = filter_node_type_input_prefix[filter_node_type.indexOf(node.comfyClass)]; for (let i = node.inputs.length - 1; i >= 0; i--) { let index = node.inputs[i].name.indexOf(input_name); if (index == 0) {