├── util ├── __init__.py ├── util.py └── categories.py ├── img ├── Flow 1.jpg ├── Int switch.jpg ├── Counter example.jpg └── Batch List Convert.jpg ├── .idea ├── vcs.xml ├── inspectionProfiles │ ├── profiles_settings.xml │ └── Project_Default.xml ├── modules.xml ├── FlowNodes.iml ├── misc.xml └── workspace.xml ├── __init__.py ├── nodes ├── anytype.py ├── basenode.py ├── __init__.py ├── logic.py ├── comp.py ├── math.py ├── cond.py ├── convert.py └── function.py ├── pyproject.toml ├── .github └── workflows │ └── publish_action.yml ├── js ├── warnUnsafe.js ├── noCache.js ├── coloredConnections.js └── dynamicAvailable.js ├── LICENSE └── readme.md /util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img/Flow 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmylo/FlowNodes/HEAD/img/Flow 1.jpg -------------------------------------------------------------------------------- /img/Int switch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmylo/FlowNodes/HEAD/img/Int switch.jpg -------------------------------------------------------------------------------- /img/Counter example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmylo/FlowNodes/HEAD/img/Counter example.jpg -------------------------------------------------------------------------------- /img/Batch List Convert.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmylo/FlowNodes/HEAD/img/Batch List Convert.jpg -------------------------------------------------------------------------------- /util/util.py: -------------------------------------------------------------------------------- 1 | def format_name(name: str, emoji: str = "🔂") -> str: 2 | return emoji + " " + name 3 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # Node imports go here 2 | from .nodes import nodes 3 | 4 | NODE_CLASS_MAPPINGS = {node.name: node for node in nodes} 5 | NODE_DISPLAY_NAME_MAPPINGS = {node.name: node.display_name for node in nodes} 6 | 7 | WEB_DIRECTORY = "./js" 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/FlowNodes.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /nodes/anytype.py: -------------------------------------------------------------------------------- 1 | # Credits: https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/3f2c021e50be2fed3c9d1552ee8dcaae06ad1fe5/py/repeater.py 2 | # Hack: string type that is always equal in not equal comparisons 3 | class AnyType(str): 4 | def __ne__(self, __value: object) -> bool: 5 | return False 6 | 7 | 8 | any = AnyType("*") 9 | -------------------------------------------------------------------------------- /util/categories.py: -------------------------------------------------------------------------------- 1 | from .util import format_name 2 | 3 | 4 | def category(name: str) -> str: 5 | return format_name("FlowNodes/" + name) 6 | 7 | 8 | categories = { 9 | "cond": category("conditions"), 10 | "comp": category("comparisons"), 11 | "logic": category("logic"), 12 | "math": category("math"), 13 | "func": category("function"), 14 | "conv": category("convert") 15 | } 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "flownodes" 3 | description = "A ComfyUI node pack containing nodes for basic programming logic." 4 | version = "1.0.0" 5 | license = {file = "LICENSE"} 6 | 7 | [project.urls] 8 | Repository = "https://github.com/gitmylo/FlowNodes" 9 | # Used by Comfy Registry https://comfyregistry.org 10 | 11 | [tool.comfy] 12 | PublisherId = "mylo" 13 | DisplayName = "FlowNodes" 14 | Icon = "" 15 | -------------------------------------------------------------------------------- /nodes/basenode.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | from ..util.categories import category 4 | 5 | 6 | class BaseNode: 7 | name = "Node" 8 | display_name = name 9 | 10 | @classmethod 11 | @abstractmethod 12 | def INPUT_TYPES(s): 13 | pass # required, optional, hidden 14 | 15 | RETURN_TYPES = () 16 | RETURN_NAMES = () 17 | FUNCTION = "" 18 | CATEGORY = category("uncategorized") 19 | -------------------------------------------------------------------------------- /.github/workflows/publish_action.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "pyproject.toml" 9 | 10 | permissions: 11 | issues: write 12 | 13 | jobs: 14 | publish-node: 15 | name: Publish Custom Node to registry 16 | runs-on: ubuntu-latest 17 | if: ${{ github.repository_owner == 'gitmylo' }} 18 | steps: 19 | - name: Check out code 20 | uses: actions/checkout@v4 21 | - name: Publish Custom Node 22 | uses: Comfy-Org/publish-node-action@v1 23 | with: 24 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} 25 | -------------------------------------------------------------------------------- /js/warnUnsafe.js: -------------------------------------------------------------------------------- 1 | import { app } from '../../scripts/app.js' 2 | 3 | const importCheck = /^[ \t]*import\s*/m; 4 | 5 | app.registerExtension({ 6 | name: "FlowNodes.warnUnsafe", 7 | loadedGraphNode(node, app) { 8 | if (node.type === "Execute Python") { 9 | const textBoxWidget = node?.widgets?.[0] 10 | const textBoxEl = textBoxWidget?.inputEl 11 | if (textBoxEl == null) return 12 | 13 | const fun = () => { 14 | const flag = importCheck.test(textBoxEl.value) 15 | if (flag) { 16 | node.color = "#D10101" 17 | node.bgcolor = "#830000" 18 | } 19 | } 20 | 21 | textBoxEl.addEventListener("change", fun) 22 | fun() 23 | } 24 | } 25 | }) -------------------------------------------------------------------------------- /js/noCache.js: -------------------------------------------------------------------------------- 1 | import { app } from '../../scripts/app.js' 2 | 3 | app.registerExtension({ 4 | name: "FlowNodes.noCache", 5 | getCustomWidgets() { 6 | return { 7 | // Dummy widget, doesn't render, but returns a random value when the value is requested, this makes every forward-connected node re-run 8 | NO_CACHE (node, inputName, inputData, app) { 9 | return { widget: node.addCustomWidget({ 10 | type: "NO_CACHE", 11 | name: inputName, 12 | computeSize (...args) { 13 | return [0, 0] 14 | }, 15 | async serializeValue (nodeId, widgetIndex) { 16 | return Math.random() 17 | } 18 | }) 19 | } 20 | } 21 | } 22 | } 23 | }) -------------------------------------------------------------------------------- /nodes/__init__.py: -------------------------------------------------------------------------------- 1 | from .cond import * 2 | conds = [ 3 | IfCond, 4 | BoolSwitchExpr, 5 | SwitchExpr, 6 | FlowStartNode, 7 | FlowMergeNode 8 | ] 9 | 10 | from .comp import * 11 | comps = [ 12 | IntCompare, 13 | FloatCompare 14 | ] 15 | 16 | from .logic import * 17 | logics = [ 18 | LogicNode, 19 | NotNode 20 | ] 21 | 22 | from .math import * 23 | maths = [ 24 | IntExpression, 25 | FloatExpression 26 | ] 27 | 28 | from .function import * 29 | functions = [ 30 | RegexMatch, 31 | ConsolePrint, 32 | CustomOperation, 33 | CustomOutputOperation, 34 | ExecuteScript, 35 | StackParams, 36 | GetGlobalObject 37 | ] 38 | 39 | from .convert import * 40 | converts = [ 41 | ConvertToType, 42 | CreateEmpty, 43 | MergeList, 44 | UnMergeList, 45 | CreateList 46 | ] 47 | 48 | nodes = conds + comps + logics + maths + functions + converts 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 gitmylo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nodes/logic.py: -------------------------------------------------------------------------------- 1 | from .basenode import BaseNode 2 | from ..util.categories import categories 3 | 4 | category = categories["logic"] 5 | 6 | actions = ["and", "or", "equal", "not equal"] 7 | 8 | 9 | def operation(a, b, action): 10 | match action: 11 | case "and": 12 | return a and b 13 | case "or": 14 | return a or b 15 | case "equal": 16 | return a == b 17 | case "not equal": 18 | return a != b 19 | return False 20 | 21 | 22 | class LogicNode(BaseNode): 23 | name = "2 boolean operation" 24 | display_name = "🆎 2 Boolean operation" 25 | 26 | @classmethod 27 | def INPUT_TYPES(s): 28 | return {"required": { 29 | "Operation": (actions,), 30 | "A": ("BOOLEAN",), 31 | "B": ("BOOLEAN",) 32 | }} 33 | 34 | CATEGORY = category 35 | RETURN_TYPES = ("BOOLEAN",) 36 | RETURN_NAMES = ("Result",) 37 | FUNCTION = "operation" 38 | 39 | def operation(self, Operation, A: bool, B: bool, **kwargs): 40 | return (operation(A, B, Operation),) 41 | 42 | 43 | class NotNode(BaseNode): 44 | name = "Boolean Not" 45 | display_name = "❗ Boolean Not" 46 | 47 | @classmethod 48 | def INPUT_TYPES(s): 49 | return {"required": { 50 | "In": ("BOOLEAN",) 51 | }} 52 | 53 | CATEGORY = category 54 | RETURN_TYPES = ("BOOLEAN",) 55 | RETURN_NAMES = ("Result",) 56 | FUNCTION = "operation" 57 | 58 | def operation(self, In, **kwargs): 59 | return (not In,) 60 | -------------------------------------------------------------------------------- /nodes/comp.py: -------------------------------------------------------------------------------- 1 | from .basenode import BaseNode 2 | from ..util.categories import categories 3 | 4 | category = categories["comp"] 5 | 6 | actions = ["==", "!=", "<", ">", "<=", ">="] 7 | 8 | 9 | def operation(a, b, action): 10 | match action: 11 | case "==": 12 | return a == b 13 | case "!=": 14 | return a != b 15 | case "<": 16 | return a < b 17 | case ">": 18 | return a > b 19 | case "<=": 20 | return a <= b 21 | case ">=": 22 | return a >= b 23 | return False 24 | 25 | 26 | class IntCompare(BaseNode): 27 | name = "Int Compare" 28 | display_name = "❓ Compare (Int)" 29 | 30 | @classmethod 31 | def INPUT_TYPES(s): 32 | return { 33 | "required": { 34 | "Action": (actions,), 35 | "A": ("INT", {}), 36 | "B": ("INT", {}) 37 | } 38 | } 39 | 40 | RETURN_TYPES = ("BOOLEAN",) 41 | RETURN_NAMES = ("Result",) 42 | FUNCTION = "compare" 43 | CATEGORY = category 44 | 45 | def compare(self, Action, A, B, **kwargs): 46 | return (operation(A, B, Action),) 47 | 48 | 49 | class FloatCompare(BaseNode): 50 | name = "Float Compare" 51 | display_name = "❓ Compare (Float)" 52 | 53 | @classmethod 54 | def INPUT_TYPES(s): 55 | return { 56 | "required": { 57 | "Action": (actions,), 58 | "A": ("FLOAT", {}), 59 | "B": ("FLOAT", {}) 60 | } 61 | } 62 | 63 | RETURN_TYPES = ("BOOLEAN",) 64 | RETURN_NAMES = ("Result",) 65 | FUNCTION = "compare" 66 | CATEGORY = category 67 | 68 | def compare(self, Action, A, B, **kwargs): 69 | return (operation(A, B, Action),) 70 | -------------------------------------------------------------------------------- /nodes/math.py: -------------------------------------------------------------------------------- 1 | from .basenode import BaseNode 2 | from ..util.categories import categories 3 | 4 | category = categories["math"] 5 | 6 | actions = ["+", "-", "*", "/", "^ (**)", "%"] 7 | 8 | 9 | def operation(a, b, action, c_type=float): 10 | match action: 11 | case "+": 12 | return a + b 13 | case "-": 14 | return a - b 15 | case "*": 16 | return a * b 17 | case "/": 18 | return a / b 19 | case "^ (**)": 20 | return a ** b 21 | case "%": 22 | return a % b 23 | return c_type(0) 24 | 25 | 26 | class IntExpression(BaseNode): 27 | name = "Int Expression" 28 | display_name = "❓ Math expression (Int)" 29 | 30 | @classmethod 31 | def INPUT_TYPES(s): 32 | return { 33 | "required": { 34 | "Action": (actions,), 35 | "A": ("INT", {}), 36 | "B": ("INT", {}) 37 | } 38 | } 39 | 40 | RETURN_TYPES = ("INT",) 41 | RETURN_NAMES = ("Result",) 42 | FUNCTION = "expr" 43 | CATEGORY = category 44 | 45 | def expr(self, Action, A, B, **kwargs): 46 | return (operation(A, B, Action, int),) 47 | 48 | 49 | class FloatExpression(BaseNode): 50 | name = "Float Expression" 51 | display_name = "❓ Math expression (Float)" 52 | 53 | @classmethod 54 | def INPUT_TYPES(s): 55 | return { 56 | "required": { 57 | "Action": (actions,), 58 | "A": ("FLOAT", {}), 59 | "B": ("FLOAT", {}) 60 | } 61 | } 62 | 63 | RETURN_TYPES = ("FLOAT",) 64 | RETURN_NAMES = ("Result",) 65 | FUNCTION = "expr" 66 | CATEGORY = category 67 | 68 | def expr(self, Action, A, B, **kwargs): 69 | return (operation(A, B, Action, float),) 70 | -------------------------------------------------------------------------------- /js/coloredConnections.js: -------------------------------------------------------------------------------- 1 | import { app } from '../../scripts/app.js' 2 | 3 | const colors = { 4 | "FLOW": ["#dddddd", "#ffffff", LiteGraph.ARROW_SHAPE], 5 | "EXEC_PARAMS": ["#5895fd", "#0054ff", LiteGraph.ROUND_SHAPE], 6 | } 7 | 8 | function updateNodeRender(node) { 9 | for (const input of node?.inputs ?? []) { 10 | const type = input?.type 11 | if (colors[type]) { 12 | const vals = colors[type] 13 | input.color_off = vals[0] 14 | input.color_on = vals[1] 15 | input.shape = vals[2] 16 | } 17 | } 18 | for (const output of node?.outputs ?? []) { 19 | const type = output?.type 20 | if (colors[type]) { 21 | const vals = colors[type] 22 | output.color_off = vals[0] 23 | output.color_on = vals[1] 24 | output.shape = vals[2] 25 | } 26 | } 27 | for (let type in colors) { 28 | LGraphCanvas.link_type_colors[type] = colors[type][1] 29 | } 30 | app.graph.setDirtyCanvas(true, true) 31 | } 32 | 33 | app.registerExtension({ 34 | name: "FlowNodes.coloredConnections", 35 | init() { 36 | for (let type in colors) { 37 | // This doesn't always appear to work, which is why there's another variant for when nodes connections are updated. 38 | LGraphCanvas.link_type_colors[type] = colors[type][1] 39 | } 40 | const add = app.graph.add 41 | app.graph.add = function (node, skip_compute_order) { 42 | const result = add?.apply(this, arguments) 43 | updateNodeRender(node) 44 | return result 45 | } 46 | }, 47 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 48 | let flag = false 49 | 50 | for (const output of nodeData?.output ?? []) { 51 | if (output in colors) { 52 | flag = true 53 | break 54 | } 55 | } 56 | 57 | if (!flag) 58 | for (const incat of nodeData?.input?.values?.() ?? []) { // Required, optional, hidden 59 | for (const inputVals of incat?.values?.() ?? []) { // Actual inputs 60 | if (inputVals[0] in colors) { 61 | flag = true 62 | break 63 | } 64 | } 65 | } 66 | 67 | if (!flag) { 68 | return 69 | } 70 | 71 | const onConnectionsChange = nodeType.prototype.onConnectionsChange 72 | nodeType.prototype.onConnectionsChange = function (slotType, slot, event, link_info, data) { 73 | const result = onConnectionsChange?.apply(this, arguments) 74 | updateNodeRender(this) 75 | return result 76 | } 77 | }, 78 | loadedGraphNode(node, app) { 79 | updateNodeRender(node) 80 | } 81 | }) -------------------------------------------------------------------------------- /nodes/cond.py: -------------------------------------------------------------------------------- 1 | from .basenode import BaseNode 2 | from ..util.categories import categories 3 | from .anytype import any 4 | 5 | 6 | category = categories["cond"] 7 | 8 | 9 | class IfCond(BaseNode): 10 | name = "If condition" 11 | display_name = "🔀 If condition" 12 | 13 | @classmethod 14 | def INPUT_TYPES(s): 15 | return {"required": { 16 | "Bool": ("BOOLEAN",), 17 | "Value": (any,) 18 | }, "optional": { 19 | "Flow": ("FLOW",), 20 | }} 21 | 22 | CATEGORY = category 23 | RETURN_TYPES = (any, any) 24 | RETURN_NAMES = ("True", "False") 25 | FUNCTION = "node" 26 | 27 | def node(self, Bool: bool, Value, **kwargs): 28 | return (Value, None) if Bool else (None, Value) 29 | 30 | 31 | class BoolSwitchExpr(BaseNode): 32 | name = "Boolean Switch expression" 33 | display_name = "🔀 Boolean Switch expression" 34 | 35 | @classmethod 36 | def INPUT_TYPES(s): 37 | return {"required": { 38 | "Value": ("BOOLEAN",) 39 | }, "optional": { 40 | "Flow": ("FLOW",), 41 | "True": (any,), 42 | "False": (any,), 43 | }} 44 | 45 | CATEGORY = category 46 | RETURN_TYPES = (any,) 47 | RETURN_NAMES = ("Result",) 48 | FUNCTION = "node" 49 | 50 | def node(self, Value, **kwargs): 51 | return (kwargs["True"] if Value else kwargs["False"],) 52 | 53 | 54 | class SwitchExpr(BaseNode): 55 | name = "Int Switch expression" 56 | display_name = "🔀 Int Switch expression" 57 | 58 | @classmethod 59 | def INPUT_TYPES(s): 60 | return {"required": { 61 | "Value": ("INT", {"default": 0, "min": 0, "max": 19, "step": 1}) 62 | }, "optional": 63 | {"Case " + str(v): (any, {"dynamicAvailable": "Case"}) for v in range(20)} | {"Flow": ("FLOW",)} 64 | } 65 | 66 | CATEGORY = category 67 | RETURN_TYPES = (any,) 68 | RETURN_NAMES = ("Result",) 69 | FUNCTION = "node" 70 | 71 | def node(self, Value, **kwargs): 72 | key = "Case " + str(Value) 73 | return (kwargs[key] if key in kwargs else None,) 74 | 75 | 76 | class FlowStartNode(BaseNode): 77 | name = "Flow start" 78 | display_name = "🌊 Activate flow from any" 79 | 80 | @classmethod 81 | def INPUT_TYPES(s): 82 | return {"required": {}, 83 | "optional": { 84 | "Trigger": (any,) 85 | }} 86 | 87 | CATEGORY = category 88 | RETURN_TYPES = ("FLOW",) 89 | RETURN_NAMES = ("Flow",) 90 | FUNCTION = "node" 91 | 92 | def node(self, **kwargs): 93 | return (None,) # Flow is just a hack to get the executions in the wanted order. 94 | 95 | 96 | class FlowMergeNode(BaseNode): 97 | name = "Flow merge" 98 | display_name = "🌊 Merge flow (bottleneck)" 99 | 100 | @classmethod 101 | def INPUT_TYPES(s): 102 | return {"required": { 103 | "Input": (any,) 104 | }, "optional": { 105 | "Flow": ("FLOW",) 106 | }} 107 | 108 | CATEGORY = category 109 | RETURN_TYPES = (any, "FLOW") 110 | RETURN_NAMES = ("Output", "Flow") 111 | FUNCTION = "node" 112 | 113 | def node(self, Input, Flow, **kwargs): 114 | return Input, Flow 115 | -------------------------------------------------------------------------------- /nodes/convert.py: -------------------------------------------------------------------------------- 1 | from .anytype import any 2 | from .basenode import BaseNode 3 | from ..util.categories import categories 4 | 5 | category = categories["conv"] 6 | 7 | original_bool = bool 8 | 9 | 10 | def bool(val): 11 | match val: 12 | case False, 0, "0", "false", "False", "FALSE": 13 | return False 14 | case True, 1, "1", "true", "True", "TRUE": 15 | return True 16 | if val is float or val is int: 17 | return val != 0 18 | return original_bool(val) 19 | 20 | 21 | types = {type.__name__: type for type in [int, float, str, bool]} 22 | 23 | 24 | class ConvertToType(BaseNode): 25 | name = "Convert to type" 26 | display_name = "Convert" 27 | 28 | @classmethod 29 | def INPUT_TYPES(s): 30 | return {"required": { 31 | "Type": (list(types.keys()),), 32 | "Input": (any,) 33 | }} 34 | 35 | CATEGORY = category 36 | RETURN_TYPES = (any,) 37 | RETURN_NAMES = ("Result",) 38 | FUNCTION = "convert" 39 | 40 | def convert(self, Type, Input, **kwargs): 41 | return (types[Type](Input),) 42 | 43 | 44 | types_create = {"list": lambda: [], "dict": lambda: {}} 45 | 46 | 47 | class CreateEmpty(BaseNode): 48 | name = "Create empty object" 49 | display_name = "➕ Create empty object" 50 | 51 | @classmethod 52 | def INPUT_TYPES(s): 53 | return {"required": { 54 | "Type": (list(types_create.keys()),) 55 | }} 56 | 57 | CATEGORY = category 58 | RETURN_TYPES = (any,) 59 | RETURN_NAMES = ("Object",) 60 | FUNCTION = "create" 61 | 62 | def create(self, Type, **kwargs): 63 | return (types_create[Type](),) 64 | 65 | 66 | class MergeList(BaseNode): 67 | name = "Merge batch (any)" 68 | display_name = "📃 List to Batch" 69 | 70 | @classmethod 71 | def INPUT_TYPES(s): 72 | return {"required": { 73 | "List": (any,) 74 | }} 75 | 76 | INPUT_IS_LIST = (True,) 77 | 78 | CATEGORY = category 79 | RETURN_TYPES = (any,) 80 | RETURN_NAMES = ("Batch",) 81 | FUNCTION = "merge_list" 82 | 83 | def merge_list(self, List, **kwargs): 84 | return (List,) 85 | 86 | 87 | class UnMergeList(BaseNode): 88 | name = "Unmerge batch (any)" 89 | display_name = "📃 Batch to List" 90 | 91 | @classmethod 92 | def INPUT_TYPES(s): 93 | return {"required": { 94 | "Batch": (any,) 95 | }} 96 | 97 | OUTPUT_IS_LIST = (True,) 98 | 99 | CATEGORY = category 100 | RETURN_TYPES = (any,) 101 | RETURN_NAMES = ("List",) 102 | FUNCTION = "unmerge_list" 103 | 104 | def unmerge_list(self, Batch, **kwargs): 105 | return (Batch,) 106 | 107 | 108 | class CreateList(BaseNode): 109 | name = "Create batch" 110 | display_name = "📃 Create batch" 111 | 112 | @classmethod 113 | def INPUT_TYPES(s): 114 | return { 115 | "required": {"no_cache": ("NO_CACHE",)}, 116 | "optional": {"input" + str(i): (any, {"dynamicAvailable": "input"}) for i in range(20)} 117 | } 118 | 119 | 120 | CATEGORY = category 121 | RETURN_TYPES = (any,) 122 | RETURN_NAMES = ("Batch",) 123 | FUNCTION = "create_list" 124 | 125 | def create_list(self, **kwargs): 126 | return (list({k: kwargs[k] for k in kwargs if k != "no_cache"}.values()),) 127 | -------------------------------------------------------------------------------- /js/dynamicAvailable.js: -------------------------------------------------------------------------------- 1 | import { app } from '../../scripts/app.js' 2 | 3 | let nodeGroups = {} 4 | 5 | function updateNodeEnabledConnections(node) { 6 | if (!(node.type in nodeGroups)) return 7 | 8 | const group = nodeGroups[node.type] 9 | const hiddenInputs = [] 10 | for (const inputGroupKey in group["groups"]) { 11 | let flag = false 12 | const inputGroup = group["groups"][inputGroupKey] 13 | for (const inputKey in inputGroup) { 14 | const input = node.inputs.filter(item => item?.name === inputKey)?.[0] 15 | 16 | if (flag && input?.link == null) hiddenInputs.push(inputKey) 17 | if (input?.link == null && !flag) { 18 | flag = true 19 | } 20 | } 21 | } 22 | 23 | // Filter groups 24 | const outGroup = [] 25 | for (const inputKey in group["original"]) { 26 | if (!(hiddenInputs.includes(inputKey))) { 27 | const input = node.inputs.filter(item => item?.name === inputKey)?.[0] 28 | if (input == null) { 29 | outGroup.push({ 30 | name: inputKey, 31 | type: group["original"][inputKey]?.[0], 32 | link: null 33 | }) 34 | } 35 | else outGroup.push(input) 36 | } 37 | } 38 | 39 | // Get rid of widgets, as converting between them can be buggy. Setting to [] causes less bugs than setting to undefined. 40 | node.widgets = [] 41 | node.widgets_values = [] 42 | 43 | node.inputs = outGroup 44 | 45 | // TODO: Make this work with multiline textboxes (They don't work properly) 46 | const nSize = node.computeSize() 47 | node.size[1] = nSize[1] 48 | if (node.onResize) 49 | node.onResize(node.size); 50 | 51 | app.graph.setDirtyCanvas(true, true) 52 | } 53 | 54 | app.registerExtension({ 55 | name: "FlowNodes.dynamicAvailable", 56 | init() { 57 | nodeGroups = {} 58 | 59 | const add = app.graph.add 60 | app.graph.add = function (node, skip_compute_order) { 61 | const result = add?.apply(this, arguments) 62 | updateNodeEnabledConnections(node) 63 | return result 64 | } 65 | }, 66 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 67 | let flag = false 68 | const groups = {} 69 | 70 | const req = nodeData?.input?.["required"] ?? {} 71 | const opt = nodeData?.input?.["optional"] ?? {} 72 | 73 | const all = Object.assign({}, req, opt) 74 | 75 | for (const inputValKey in all) { // All optional inputs 76 | const inputVal = all[inputValKey] 77 | const flags = inputVal?.[1] 78 | const dya = flags?.dynamicAvailable 79 | if (flags && flags?.dynamicAvailable) { 80 | flag = true 81 | if (!groups?.[dya]) groups[dya] = {} 82 | // groups[dya].push({inputValKey: inputVal}) 83 | groups[dya][inputValKey] = inputVal 84 | } 85 | } 86 | 87 | if (!flag) return 88 | 89 | // Register this group list 90 | nodeGroups[nodeData.name] = { 91 | "groups": groups, 92 | "original": all 93 | } 94 | 95 | const onConnectionsChange = nodeType.prototype.onConnectionsChange 96 | nodeType.prototype.onConnectionsChange = function (slotType, slot, event, link_info, data) { 97 | const result = onConnectionsChange?.apply(this, arguments) 98 | updateNodeEnabledConnections(this) 99 | return result 100 | } 101 | }, 102 | loadedGraphNode(node, app) { 103 | updateNodeEnabledConnections(node) 104 | } 105 | }) -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ComfyUI FlowNodes 2 | A [ComfyUI](https://github.com/comfyanonymous/comfyui) node pack containing nodes for basic programming logic. 3 | 4 | ## NOTICE: 5 | FlowNodes is very WIP and early in development. If you encounter a problem, please create an issue. 6 | 7 | # Table of contents 8 | 9 | * [ComfyUI FlowNodes](#comfyui-flownodes) 10 | * [NOTICE:](#notice) 11 | * [Table of contents](#table-of-contents) 12 | * [Examples](#examples) 13 | * [Counter](#counter) 14 | * [Flow](#flow) 15 | * [What is "Flow"?](#what-is-flow) 16 | * [Nodes](#nodes) 17 | * [Conditions](#conditions) 18 | * [If](#if) 19 | * [Switch (Bool)](#switch-bool) 20 | * [Switch (Int)](#switch-int) 21 | * [Comparisons](#comparisons) 22 | * [Compare (Both Int/Float)](#compare-both-intfloat) 23 | * [Logic](#logic) 24 | * [Operation](#operation) 25 | * [Math](#math) 26 | * [Expression (Both Int/Float)](#expression-both-intfloat) 27 | * [Function](#function) 28 | * [Regex match](#regex-match) 29 | * [Operations](#operations) 30 | * [Print to console](#print-to-console) 31 | * [Execute script (UNSAFE) / Stack params](#execute-script-unsafe--stack-params) 32 | * [Get persistent dict](#get-persistent-dict) 33 | * [Convert](#convert) 34 | * [Convert to type](#convert-to-type) 35 | * [Create empty object](#create-empty-object) 36 | * [Convert List to Batch/Batch to List](#convert-list-to-batchbatch-to-list) 37 | * [Create list](#create-list) 38 | 39 | 40 | # Examples 41 | ## Counter 42 | The counter counts up one number on each run, each run will re-execute all the required nodes. 43 | **Additional nodes used in the example:** 44 | * String and Int from [Various ComfyUI Nodes by Type](https://github.com/jamesWalker55/comfyui-various) 45 | * Show text from [ComfyUI custom scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) 46 | 47 | ![Counter example](https://github.com/gitmylo/FlowNodes/blob/master/img/Counter%20example.jpg?raw=true) 48 | 49 | # Flow 50 | ## What is "Flow"? 51 | Flow is a connection, which can be used to force comfyUI to perform function operations in a certain order. Flow is a single ton `None` value which can be passed through as optional input to enforce an order of operations. 52 | A flow is started from one of the following: 53 | 1. a node with flow output 54 | 2. an `activate flow from any` node. (The input here is optional.) 55 | 56 | Flows can be used in two ways 57 | 1. As input, for a node which takes a flow, like some of the function nodes. 58 | 2. As input for the `Merge flow (bottleneck)` node, this node makes sure the order is fixed, and can be used to repeat if the flow is repeated using a `repeater` node. (For example, from [Comfyui-custom-scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts)) 59 | 60 | ![Flow](https://github.com/gitmylo/FlowNodes/blob/master/img/Flow%201.jpg?raw=true) 61 | *(Screenshot was taken before flow nodes got styled)* 62 | 63 | # Nodes 64 | If any nodes are missing from this list, please inform me by making an issue. 65 | 66 | ## Conditions 67 | Condition nodes will behave differently depending on the condition's value. 68 | ### If 69 | Takes a boolean (condition) and a value. 70 | Two outputs: True, and False. 71 | * True: if `true`, contains the input value, if `false`, contains `None` 72 | * False: if `false`, contains the input value, if `true`, contains `None` 73 | 74 | This node should be connected on the location of an `optional` input, as otherwise, returning `None` will cause errors. 75 | 76 | ### Switch (Bool) 77 | Takes a boolean, and two values. If the boolean is `True`, the first value will be outputted, if it's `False`, the second value will be outputted. 78 | 79 | ### Switch (Int) 80 | Same as `Switch (Bool)`, but instead of `True`/`False`, it's the numbers `0-9` 81 | ![Switch (Int)](https://github.com/gitmylo/FlowNodes/blob/master/img/Int%20switch.jpg?raw=true) 82 | 83 | ## Comparisons 84 | Comparison nodes take multiple inputs, and perform a comparison between them, and output the result. 85 | ### Compare (Both Int/Float) 86 | * Takes 2 inputs, and an operation. 87 | * Returns the result of the operation on the two inputs. 88 | 89 | ## Logic 90 | Logic nodes take 1/2 boolean inputs, and return a boolean. 91 | ### Operation 92 | * Takes 2 booleans, and an operation 93 | * Returns the result of the operation on the two inputs. 94 | 95 | ## Math 96 | Math nodes take a few numerical inputs, and returns the result of the selected operation. 97 | ### Expression (Both Int/Float) 98 | * Takes 2 inputs, and an operation. 99 | * Returns the result of the operation on the two inputs. 100 | 101 | ## Function 102 | Function nodes perform an operation on the inputs, and output the result. (Most similar to most other comfy nodes) 103 | ### Regex match 104 | Takes a pattern and a string. Uses python regex. Returns an array containing the regex's matches. 105 | ### Operations 106 | Simple operations like +, -, *, and some functions. Except they are executed on any object. 107 | ### Print to console 108 | Prints its input to console. This is mainly for debugging. 109 | ### Execute script (UNSAFE) / Stack params 110 | **this node can be very unsafe because it executes user specified code. To check the code, look for imports. Nodes will automatically become red if they contain import statements.** 111 | Execute a python script, takes one input variable currently. Referred to in the code as `input0`. 112 | If a parameter stacker node is connected, the node can have multiple inputs, referred to as inputN where N is the number of the input. 113 | **example**: 114 | ```python 115 | out = input0 # Output the input, this node will do nothing, just pass through. 116 | ``` 117 | ### Get persistent dict 118 | This node lets you access the persistent dict. This is a dictionary which is available between runs. Note that it resets when the server is restarted. And it's not stored in the workflow. It's specifically for changing things between runs. 119 | 120 | ## Convert 121 | Convert nodes specifically convert from one data type to another. 122 | ### Convert to type 123 | Converts from one type to the selected type. 124 | ### Create empty object 125 | Create an empty object. Currently supports dicts and lists. 126 | ### Convert List to Batch/Batch to List 127 | Convert a list (Items are executed separately when put into another node) to a batch (Items are all given at once, but the node needs to support it.) 128 | This is most useful for reading/writing individual batch items using operation nodes. Or to give it as an input for a script node. 129 | ![Batch and list convert](https://github.com/gitmylo/FlowNodes/blob/master/img/Batch%20List%20Convert.jpg?raw=true) 130 | ### Create list 131 | Uses no-cache, so it won't lead to unexpected results with caching. 132 | Creates a python list containing the items. (Can be modified with write operation nodes, using flow is recommended in this case, merge flow after the last operation.) -------------------------------------------------------------------------------- /nodes/function.py: -------------------------------------------------------------------------------- 1 | from .basenode import BaseNode 2 | import re 3 | from ..util.categories import categories 4 | from .anytype import any 5 | 6 | category = categories["func"] 7 | 8 | 9 | class RegexMatch(BaseNode): 10 | name = "Regex match" 11 | display_name = "🔍 Regex match" 12 | 13 | @classmethod 14 | def INPUT_TYPES(s): 15 | return {"required": { 16 | "Pattern": ("STRING", {"default": "(.+)", "multiline": False}), 17 | "String": ("STRING", {"default": "", "multiline": True}) 18 | }} 19 | 20 | RETURN_TYPES = ("REGEX_MATCHES", "INT") 21 | RETURN_NAMES = ("matches", "amount") 22 | FUNCTION = "regex_match" 23 | CATEGORY = category 24 | 25 | def regex_match(self, Pattern, String, **kwargs): 26 | matches = re.findall(Pattern, String, re.MULTILINE) 27 | return matches, len(matches) 28 | 29 | 30 | custom_op_actions = ["f + s", "f - s", "f * s", "f / s", "f[s]", "getattr(f, s)"] 31 | output_op_actions = ["f[s] = t", "setattr(f, s, t)", "f.append(s)"] 32 | 33 | 34 | def custom_op_action(first, second, third, action): 35 | match action: 36 | case "f + s": 37 | return first + second 38 | case "f - s": 39 | return first - second 40 | case "f * s": 41 | return first * second 42 | case "f / s": 43 | return first / second 44 | case "f[s]": 45 | if isinstance(first, list): 46 | if len(first) > second >= 0: 47 | return first[second] 48 | elif isinstance(first, dict): 49 | if second in first: 50 | return first[second] 51 | return third 52 | case "getattr(f, s)": 53 | if hasattr(first, second): 54 | return getattr(first, second) 55 | return third 56 | 57 | case "f[s] = t": 58 | first[second] = third 59 | return first 60 | case "setattr(f, s, t)": 61 | setattr(first, second, third) 62 | return first 63 | case "f.append(s)": 64 | first.append(second) 65 | return first 66 | return None 67 | 68 | 69 | class CustomOperation(BaseNode): 70 | name = "Generic operation" 71 | display_name = "📖 Generic operation" 72 | 73 | @classmethod 74 | def INPUT_TYPES(s): 75 | return {"required": { 76 | "Action": (custom_op_actions,), 77 | "First": (any,), 78 | "Second": (any,), 79 | 80 | "no_cache": ("NO_CACHE",) 81 | }, "optional": { 82 | "Default": (any,), 83 | "Flow": ("FLOW",) 84 | }} 85 | 86 | RETURN_TYPES = (any, "FLOW") 87 | RETURN_NAMES = ("Result", "Flow") 88 | FUNCTION = "custom_operation" 89 | CATEGORY = category 90 | 91 | def custom_operation(self, Action, First, Second, **kwargs): 92 | return custom_op_action(First, Second, kwargs["Default"] if "Default" in kwargs else None, Action), None 93 | 94 | 95 | class CustomOutputOperation(CustomOperation): 96 | name = "Generic operation (write)" 97 | display_name = "📖 Generic operation (write)" 98 | 99 | @classmethod 100 | def INPUT_TYPES(s): 101 | return {"required": { 102 | "Action": (output_op_actions,), 103 | "First": (any,), 104 | "Second": (any,), 105 | 106 | "no_cache": ("NO_CACHE",) 107 | }, 108 | "optional": { 109 | "Third": (any), 110 | "Flow": ("FLOW",) 111 | }} 112 | 113 | RETURN_TYPES = (any, "FLOW") 114 | RETURN_NAMES = ("First", "Flow",) 115 | 116 | def custom_operation(self, Action, First, Second, **kwargs): 117 | return (custom_op_action(First, Second, kwargs["Third"] if "Third" in kwargs else None, Action), None) 118 | 119 | 120 | class ConsolePrint(BaseNode): 121 | name = "Console print" 122 | display_name = "💻 Print to console" 123 | 124 | @classmethod 125 | def INPUT_TYPES(s): 126 | return {"required": { 127 | "Value": (any,), 128 | "no_cache": ("NO_CACHE",) 129 | }} 130 | 131 | RETURN_TYPES = ("FLOW",) 132 | RETURN_NAMES = ("Flow",) 133 | FUNCTION = "console_print" 134 | CATEGORY = category 135 | OUTPUT_NODE = True 136 | 137 | def console_print(self, Value, **kwargs): 138 | print(Value) 139 | return (None,) 140 | 141 | 142 | class ExecuteScript(BaseNode): 143 | name = "Execute Python" 144 | display_name = "🐍 Execute python (UNSAFE)" 145 | 146 | @classmethod 147 | def INPUT_TYPES(s): 148 | return {"required": { 149 | "Value": ("STRING", {"default": "out = input0", "multiline": True}), 150 | "no_cache": ("NO_CACHE",) 151 | }, "optional": { 152 | "Input": (any,), 153 | "Flow": ("FLOW",) 154 | }} 155 | 156 | RETURN_TYPES = (any, "FLOW") 157 | RETURN_NAMES = ("Output", "Flow") 158 | FUNCTION = "exec_script" 159 | CATEGORY = category 160 | 161 | def exec_script(self, Value, **kwargs): 162 | # inp = kwargs["Input"] if "Input" in kwargs.keys() else None 163 | glob = {} 164 | loc = {"out": None} 165 | if "Input" in kwargs: 166 | if isinstance(kwargs["Input"], dict): 167 | loc = loc | (kwargs["Input"]) 168 | else: 169 | loc["input0"] = kwargs["Input"] 170 | 171 | exec(Value, glob, loc) # defined function will be stored in locals 172 | return loc['out'], None 173 | 174 | 175 | class StackParams(BaseNode): 176 | name = "Stack parameters" 177 | display_name = "🐍 Stack python exec parameters" 178 | 179 | @classmethod 180 | def INPUT_TYPES(s): 181 | return { 182 | "required": {}, "optional": 183 | {"input" + str(i): (any, {"dynamicAvailable": "input"}) for i in range(20)} | {"Flow": ("FLOW",)} 184 | } 185 | 186 | RETURN_TYPES = ("EXEC_PARAMS", "FLOW") 187 | RETURN_NAMES = ("Stacked params", "Flow") 188 | FUNCTION = "stack_params" 189 | CATEGORY = category 190 | 191 | def stack_params(self, **kwargs): 192 | return kwargs, None 193 | 194 | 195 | persistent_object = {} 196 | 197 | 198 | class GetGlobalObject(BaseNode): 199 | name = "Get persistent dict" 200 | display_name = "🔺 Get persistent dict" 201 | 202 | @classmethod 203 | def INPUT_TYPES(s): 204 | # return {"required": {}, "hidden": {"no_cache": ("_", {"NoCache": True})}} 205 | return {"required": { 206 | "no_cache": ("NO_CACHE",) 207 | }} 208 | 209 | RETURN_TYPES = (any,) 210 | RETURN_NAMES = ("Persistent dict",) 211 | FUNCTION = "get_persistent" 212 | CATEGORY = category 213 | 214 | def get_persistent(self, **kwargs): 215 | global persistent_object 216 | return (persistent_object,) 217 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 24 | 25 | 26 | 28 | { 29 | "lastFilter": { 30 | "state": "OPEN", 31 | "assignee": "gitmylo" 32 | } 33 | } 34 | { 35 | "selectedUrlAndAccountId": { 36 | "url": "https://github.com/gitmylo/FlowNodes.git", 37 | "accountId": "b609a173-3d46-4b22-a381-aa5cb07aaee3" 38 | } 39 | } 40 | { 41 | "associatedIndex": 1 42 | } 43 | 44 | 45 | 46 | 47 | 48 | 51 | { 52 | "keyToString": { 53 | "ASKED_ADD_EXTERNAL_FILES": "true", 54 | "Python.main.executor": "Run", 55 | "RunOnceActivity.ShowReadmeOnStart": "true", 56 | "git-widget-placeholder": "master", 57 | "last_opened_file_path": "C:/Users/milan/Desktop/AIAIA/ComfyUI", 58 | "node.js.detected.package.eslint": "true", 59 | "node.js.detected.package.tslint": "true", 60 | "node.js.selected.package.eslint": "(autodetect)", 61 | "node.js.selected.package.tslint": "(autodetect)", 62 | "nodejs_package_manager_path": "npm", 63 | "settings.editor.selected.configurable": "preferences.pluginManager", 64 | "vue.rearranger.settings.migration": "true" 65 | } 66 | } 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 97 | 98 | 99 | 100 | 101 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 1714483854113 111 | 123 | 124 | 131 | 132 | 139 | 140 | 147 | 148 | 155 | 156 | 163 | 164 | 171 | 172 | 179 | 180 | 187 | 188 | 195 | 196 | 203 | 204 | 211 | 212 | 219 | 220 | 227 | 228 | 235 | 236 | 243 | 244 | 251 | 254 | 255 | 257 | 258 | 277 | 278 | 279 | 280 | --------------------------------------------------------------------------------