├── 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 |
4 |
5 |
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 |
4 |
5 |
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 |
5 |
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 | 
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 | 
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 | 
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 | 
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
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 |
49 |
50 |
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 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | 1714483854113
111 |
112 |
113 | 1714483854113
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | 1714490192963
126 |
127 |
128 |
129 | 1714490192963
130 |
131 |
132 |
133 | 1714495681281
134 |
135 |
136 |
137 | 1714495681281
138 |
139 |
140 |
141 | 1714501682671
142 |
143 |
144 |
145 | 1714501682671
146 |
147 |
148 |
149 | 1714569398317
150 |
151 |
152 |
153 | 1714569398317
154 |
155 |
156 |
157 | 1714569757707
158 |
159 |
160 |
161 | 1714569757707
162 |
163 |
164 |
165 | 1714569902600
166 |
167 |
168 |
169 | 1714569902600
170 |
171 |
172 |
173 | 1714570005044
174 |
175 |
176 |
177 | 1714570005044
178 |
179 |
180 |
181 | 1714582664621
182 |
183 |
184 |
185 | 1714582664621
186 |
187 |
188 |
189 | 1714641301850
190 |
191 |
192 |
193 | 1714641301850
194 |
195 |
196 |
197 | 1714650862157
198 |
199 |
200 |
201 | 1714650862157
202 |
203 |
204 |
205 | 1714670883330
206 |
207 |
208 |
209 | 1714670883330
210 |
211 |
212 |
213 | 1714671379848
214 |
215 |
216 |
217 | 1714671379848
218 |
219 |
220 |
221 | 1714675770761
222 |
223 |
224 |
225 | 1714675770761
226 |
227 |
228 |
229 | 1714677008815
230 |
231 |
232 |
233 | 1714677008815
234 |
235 |
236 |
237 | 1714725564100
238 |
239 |
240 |
241 | 1714725564100
242 |
243 |
244 |
245 | 1714727204483
246 |
247 |
248 |
249 | 1714727204483
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
--------------------------------------------------------------------------------