add ForEachOpen and ForEachClose
This commit is contained in:
@@ -58,6 +58,8 @@
|
||||
| IndexOfList | 从列表中获取指定位置的元素 | |
|
||||
| IndexesOfList | 从列表中筛选出指定位置的元素列表 | |
|
||||
| StringArea | 字符串文本框(多行输入区域) | |
|
||||
| ForEachOpen | 循环开始节点 | |
|
||||
| ForEachClose | 循环结束节点 | |
|
||||
|
||||
Tips: base64格式字符串比较长,会导致界面卡顿,接口请求带宽可能也会有瓶颈,条件允许可以把图片上传到OSS服务器得到URL,然后用LoadImageFromUrl加载,由于无相关OSS账号,上传OSS节点需自行编写,暂不支持。
|
||||
|
||||
@@ -70,8 +72,11 @@ Tips: base64格式字符串比较长,会导致界面卡顿,接口请求带
|
||||

|
||||
|
||||
## 更新记录
|
||||
### 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节点
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user