add ForEachOpen and ForEachClose

This commit is contained in:
刘雪峰
2024-09-25 15:41:24 +08:00
parent edaf8b156f
commit 55de32fd50
5 changed files with 448 additions and 3 deletions
+7 -2
View File
@@ -58,6 +58,8 @@
| IndexOfList | 从列表中获取指定位置的元素 | |
| IndexesOfList | 从列表中筛选出指定位置的元素列表 | |
| StringArea | 字符串文本框(多行输入区域) | |
| ForEachOpen | 循环开始节点 | |
| ForEachClose | 循环结束节点 | |
Tips: base64格式字符串比较长,会导致界面卡顿,接口请求带宽可能也会有瓶颈,条件允许可以把图片上传到OSS服务器得到URL,然后用LoadImageFromUrl加载,由于无相关OSS账号,上传OSS节点需自行编写,暂不支持。
@@ -70,8 +72,11 @@ Tips: base64格式字符串比较长,会导致界面卡顿,接口请求带
![save api extended](example/example_3.png)
## 更新记录
### 2024-09-20
- 添加节点:SplitStringToList、 IndexOfList、 IndexesOfList、 StringArea。[简单示例](example/example_4.png)
### 2024-09-25 [示例](example/example_4.png)
- 新增节点:ForEachOpen、 ForEachClose
### 2024-09-20 [示例](example/example_4.png)
- 添加节点:SplitStringToList、 IndexOfList、 IndexesOfList、 StringArea
### 2024-09-19
- 添加ListUnWrapper节点
+280
View File
@@ -0,0 +1,280 @@
from comfy_execution.graph_utils import GraphBuilder, is_link
from .util import any_type
# 支持的最大参数个数
NUM_FLOW_SOCKETS = 20
class InnerIntMathOperation:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
"b": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 1}),
"operation": (["add", "subtract", "multiply", "divide", "modulo", "power"],),
},
}
RETURN_TYPES = ("INT",)
FUNCTION = "calc"
CATEGORY = "EasyApi/Logic"
def calc(self, a, b, operation):
if operation == "add":
return (a + b,)
elif operation == "subtract":
return (a - b,)
elif operation == "multiply":
return (a * b,)
elif operation == "divide":
return (a // b,)
elif operation == "modulo":
return (a % b,)
elif operation == "power":
return (a ** b,)
COMPARE_FUNCTIONS = {
"a == b": lambda a, b: a == b,
"a != b": lambda a, b: a != b,
"a < b": lambda a, b: a < b,
"a > b": lambda a, b: a > b,
"a <= b": lambda a, b: a <= b,
"a >= b": lambda a, b: a >= b,
}
class InnerIntCompare:
@classmethod
def INPUT_TYPES(s):
compare_functions = list(COMPARE_FUNCTIONS.keys())
return {
"required": {
"a": ("INT", {"default": 0}),
"b": ("INT", {"default": 0}),
"comparison": (compare_functions, {"default": "a == b"}),
},
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("boolean",)
FUNCTION = "compare"
CATEGORY = "EasyApi/Logic"
def compare(self, a, b, comparison):
return (COMPARE_FUNCTIONS[comparison](a, b),)
class InnerLoopClose:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
inputs = {
"required": {
"flow_control": ("FLOW_CONTROL", {"rawLink": True}),
"condition": ("BOOLEAN", {"forceInput": True}),
},
"optional": {
},
"hidden": {
"dynprompt": "DYNPROMPT",
"unique_id": "UNIQUE_ID",
}
}
for i in range(NUM_FLOW_SOCKETS):
inputs["optional"]["initial_value%d" % i] = ("*",)
return inputs
RETURN_TYPES = tuple(["*"] * NUM_FLOW_SOCKETS)
RETURN_NAMES = tuple(["value%d" % i for i in range(NUM_FLOW_SOCKETS)])
FUNCTION = "while_loop_close"
CATEGORY = "EasyApi/Logic"
def explore_dependencies(self, node_id, dynprompt, upstream):
node_info = dynprompt.get_node(node_id)
if "inputs" not in node_info:
return
for k, v in node_info["inputs"].items():
if is_link(v):
parent_id = v[0]
if parent_id not in upstream:
upstream[parent_id] = []
self.explore_dependencies(parent_id, dynprompt, upstream)
upstream[parent_id].append(node_id)
def collect_contained(self, node_id, upstream, contained):
if node_id not in upstream:
return
for child_id in upstream[node_id]:
if child_id not in contained:
contained[child_id] = True
self.collect_contained(child_id, upstream, contained)
def while_loop_close(self, flow_control, condition, dynprompt=None, unique_id=None, **kwargs):
if not condition:
# We're done with the loop
values = []
for i in range(NUM_FLOW_SOCKETS):
values.append(kwargs.get("initial_value%d" % i, None))
return tuple(values)
# We want to loop
this_node = dynprompt.get_node(unique_id)
upstream = {}
# Get the list of all nodes between the open and close nodes
self.explore_dependencies(unique_id, dynprompt, upstream)
contained = {}
open_node = flow_control[0]
self.collect_contained(open_node, upstream, contained)
contained[unique_id] = True
contained[open_node] = True
# We'll use the default prefix, but to avoid having node names grow exponentially in size,
# we'll use "Recurse" for the name of the recursively-generated copy of this node.
graph = GraphBuilder()
for node_id in contained:
original_node = dynprompt.get_node(node_id)
node = graph.node(original_node["class_type"], "Recurse" if node_id == unique_id else node_id)
node.set_override_display_id(node_id)
for node_id in contained:
original_node = dynprompt.get_node(node_id)
node = graph.lookup_node("Recurse" if node_id == unique_id else node_id)
for k, v in original_node["inputs"].items():
if is_link(v) and v[0] in contained:
parent = graph.lookup_node(v[0])
node.set_input(k, parent.out(v[1]))
else:
node.set_input(k, v)
new_open = graph.lookup_node(open_node)
for i in range(NUM_FLOW_SOCKETS):
key = "initial_value%d" % i
new_open.set_input(key, kwargs.get(key, None))
my_clone = graph.lookup_node("Recurse" )
result = map(lambda x: my_clone.out(x), range(NUM_FLOW_SOCKETS))
return {
"result": tuple(result),
"expand": graph.finalize(),
}
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
class ForEachOpen:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"total": ("INT", {"default": 1, "min": 1, "max": 1000, "step": 1, "tooltip": "总循环次数"}),
},
"optional": {
"initial_value1": (any_type,)
},
"hidden": {
"initial_value0": (any_type,)
}
}
RETURN_TYPES = tuple(["FLOW_CONTROL", "INT", "INT"] + [any_type] * (NUM_FLOW_SOCKETS - 1))
RETURN_NAMES = tuple(["flow_control", "index", "total"] + ["value%d" % i for i in range(1, NUM_FLOW_SOCKETS)])
OUTPUT_TOOLTIPS = ("开始节点元信息", "循环索引值", "总循环次数,不宜太大,会影响到消息长度",)
FUNCTION = "for_loop_open"
CATEGORY = "EasyApi/Logic"
def for_loop_open(self, total, **kwargs):
graph = GraphBuilder()
if "initial_value0" in kwargs:
index = kwargs["initial_value0"]
else:
index = 0
initial_value_num = find_max_initial_value_number(kwargs, "initial_value")
# 好像没啥用
# while_open = graph.node("WhileLoopOpen", condition=remaining, initial_value0=total, **{("initial_value%d" % i): kwargs.get("initial_value%d" % i, None) for i in range(1, NUM_FLOW_SOCKETS)})
outputs = [kwargs.get("initial_value%d" % i, None) for i in range(1, initial_value_num + 1)]
return {
"result": tuple(["stub", index, total] + outputs),
"expand": graph.finalize(),
}
class ForEachClose:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"flow_control": ("FLOW_CONTROL", {"rawLink": True}),
},
"optional": {
"initial_value1": (any_type, {"rawLink": True})
},
}
RETURN_TYPES = tuple([any_type] * (NUM_FLOW_SOCKETS-1))
RETURN_NAMES = tuple(["value%d" % i for i in range(1, NUM_FLOW_SOCKETS)])
FUNCTION = "for_loop_close"
CATEGORY = "EasyApi/Logic"
def for_loop_close(self, flow_control, **kwargs):
graph = GraphBuilder()
# ForEachOpen node id
openNodeId = flow_control[0]
# 计算索引, a传open节点的第3个输出参数,即index参数
sub = graph.node(InnerIntMathOperation.__name__, operation="add", a=[openNodeId, 1], b=1)
# 边界条件约束, b传open节点的第3个输出参数,即total参数
cond = graph.node(InnerIntCompare.__name__, a=sub.out(0), b=[openNodeId, 2], comparison='a < b')
# 构建循环传递参数
initial_value_num = find_max_initial_value_number(kwargs, "initial_value")
input_values = {("initial_value%d" % i): kwargs.get("initial_value%d" % i, None) for i in range(1, initial_value_num + 1)}
while_close = graph.node(InnerLoopClose.__name__,
flow_control=flow_control,
condition=cond.out(0),
initial_value0=sub.out(0),
**input_values)
return {
"result": tuple([while_close.out(i) for i in range(1, initial_value_num + 1)]),
"expand": graph.finalize(),
}
NODE_CLASS_MAPPINGS = {
"InnerIntMathOperation": InnerIntMathOperation,
"InnerIntCompare": InnerIntCompare,
"InnerLoopClose": InnerLoopClose,
"ForEachOpen": ForEachOpen,
"ForEachClose": ForEachClose,
}
# A dictionary that contains the friendly/humanly readable titles for the nodes
NODE_DISPLAY_NAME_MAPPINGS = {
"InnerIntMathOperation": "InnerIntMathOperation",
"InnerIntCompare": "InnerIntCompare",
"InnerLoopClose": "InnerLoopClose",
"ForEachOpen": "ForEachOpen",
"ForEachClose": "ForEachClose",
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-easyapi-nodes"
description = "Provides some features and nodes related to API calls."
version = "1.0.3"
version = "1.0.4"
license = { file = "LICENSE" }
dependencies = ["segment_anything", "simple_lama_inpainting", "insightface"]
+160
View File
@@ -578,4 +578,164 @@ app.registerExtension({
return dialog;
}
}
})
function removeOutSoltAndLink(node, out_slot_i) {
let outNodes = node.getOutputNodes(out_slot_i);
if (!!node.getOutputNodes(out_slot_i)) {
outNodes.forEach(outNode => {
node.disconnectOutput(out_slot_i, outNode.id);
})
}
node.removeOutput(out_slot_i);
}
let filter_node_type = ['ForEachOpen', 'ForEachClose']
let output_fixed_num_for_filter_node_type = [3, 0]
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 output_name = "value";
let fixed_head_input_names = ["flow_control"];
let fixed_tail_input_names = ["total"];
// 与py代码中定义保持一致
let max_number_of_inputs = 20;
let out_fixed_num = output_fixed_num_for_filter_node_type[filter_node_type.indexOf(nodeData.name)];
nodeType.prototype.onConnectionsChange = function (type, index, connected, link_info) {
if(!link_info)
return;
if(type == 2) {
// connect output
if (link_info.origin_slot >= out_fixed_num){
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;
// 设置输入类型
let input_slot = link_info.origin_slot - out_fixed_num + fixed_head_solt_count
if (connected) {
let output_type = app.graph._nodes_by_id[link_info.target_id].inputs[link_info.target_slot].type
if(!this.inputs[input_slot]?.link && output_type !== "*") {
// 输入节点无连接
this.setOutputDataType(link_info.origin_slot, output_type)
this.inputs[input_slot].type = output_type
}
} else {
if(!this.inputs[input_slot]?.link && this.inputs[input_slot]?.type !== "*") {
this.setOutputDataType(link_info.origin_slot, "*")
if (input_slot < this.inputs.length) {
this.inputs[input_slot].type = "*";
}
}
}
}
}
else {
if (filter_node_type.indexOf(nodeData.name) > -1 && app.graph._nodes_by_id[link_info.origin_id].type == 'Reroute')
this.disconnectInput(link_info.target_slot);
// connect input
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;
}
}
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;
// let fixed_tail_solts = this.inputs.filter(x => fixed_tail_input_names.indexOf(x.name) > -1);
// let fixed_tail_solt_count = fixed_tail_solts ? fixed_tail_solts.length : 0;
// let converted_count = fixed_head_solt_count + fixed_tail_solt_count;
// 设置类型
if (connected) {
let input_type = app.graph._nodes_by_id[link_info.origin_id].outputs[link_info.origin_slot].type
this.inputs[link_info.target_slot].type = input_type
this.setOutputDataType(link_info.target_slot - fixed_head_solt_count + out_fixed_num, input_type)
} else {
let out_slot = link_info.target_slot - fixed_head_solt_count + out_fixed_num
if ((!this.outputs[out_slot]?.links || this.outputs[out_slot]?.links?.length == 0) && this.inputs[link_info.target_slot]?.type !== "*") {
this.inputs[link_info.target_slot].type = "*"
this.setOutputDataType(out_slot, "*")
}
}
// 给所有动态输入编号
let slot_i = 0;
for (let i = 0; i < this.inputs.length; i++) {
let input_i = this.inputs[i];
if (fixed_tail_input_names.indexOf(input_i.name) < 0 && fixed_head_input_names.indexOf(input_i.name) < 0) {
input_i.name = `${input_name}${slot_i + 1}`;
slot_i++;
}
}
if (connected && index == (fixed_head_solt_count + slot_i - 1)) {
if (max_number_of_inputs == slot_i) {
return;
}
slot_i++;
this.addInput(`${input_name}${slot_i}`, "*");
this.addOutput(`${output_name}${slot_i}`, "*");
}
// 最后面未连接的只保留一个
for (let i = fixed_head_solt_count + slot_i - 1; i >= 1; i--) {
let input_i_1 = this.inputs[i - 1];
let input_i = this.inputs[i];
if (fixed_head_input_names.indexOf(input_i_1.name) < 0 && !input_i_1.link && !input_i.link) {
// 删除最后一个输入插槽
this.removeInput(i);
// 对应输出有连线,断开连线
let out_slot_i = out_fixed_num + i - fixed_head_solt_count;
removeOutSoltAndLink(this, out_slot_i);
} else {
break;
}
}
let that = this;
// 找到所有 name 属性为 fixed_tail_input_name 的元素的索引
const indicesToMove = this.inputs.reduce((acc, item, index) => {
if (fixed_tail_input_names.indexOf(item.name) > -1) {
acc.push(index);
}
return acc;
}, []);
// 从原数组中移除这些元素
const elementsToMove = indicesToMove.map(index => that.inputs.splice(index, 1)).flat();
// 将这些元素添加到数组末尾
elementsToMove.forEach(element => that.inputs.push(element));
}
}
}
},
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 (node.id == -1) {
for (let i = node.outputs.length - 1; i > out_fixed_num; i--) {
removeOutSoltAndLink(node, i);
}
}
}
}
})