diff --git a/README.md b/README.md index f5d036e..1570fef 100644 --- a/README.md +++ b/README.md @@ -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节点 diff --git a/easyapi/ForEachNode.py b/easyapi/ForEachNode.py new file mode 100644 index 0000000..b544976 --- /dev/null +++ b/easyapi/ForEachNode.py @@ -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", +} diff --git a/example/example_4.png b/example/example_4.png index 882e62e..d4f0c7e 100644 Binary files a/example/example_4.png and b/example/example_4.png differ diff --git a/pyproject.toml b/pyproject.toml index 680d885..d564fdb 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." -version = "1.0.3" +version = "1.0.4" license = { file = "LICENSE" } dependencies = ["segment_anything", "simple_lama_inpainting", "insightface"] diff --git a/static/js/easyapi.js b/static/js/easyapi.js index 6f909f1..56bbeba 100644 --- a/static/js/easyapi.js +++ b/static/js/easyapi.js @@ -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); + } + } + } + } }) \ No newline at end of file