add FilterSortDependSubGraphs and SortDependSubGraphs

This commit is contained in:
刘雪峰
2025-03-28 14:32:45 +08:00
parent af1d00a49c
commit 38bb1979d3
8 changed files with 162 additions and 48 deletions
+6
View File
@@ -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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

+2 -14
View File
@@ -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:
+119 -16
View File
@@ -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",
}
+16
View File
@@ -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"""
+1 -1
View File
@@ -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"]
+18 -17
View File
@@ -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) {