├── .gitignore ├── workflow.png ├── pyproject.toml ├── floodgate.py ├── .github └── workflows │ └── publish.yml ├── LICENSE ├── README.md └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haoming02/comfyui-floodgate/HEAD/workflow.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-floodgate" 3 | description = "A node that allows you to switch between execution paths" 4 | version = "1.2.0" 5 | license = { text = "MIT License" } 6 | dependencies = [] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/Haoming02/comfyui-floodgate" 10 | 11 | [tool.comfy] 12 | PublisherId = "haoming02" 13 | DisplayName = "Floodgate" 14 | Icon = "" 15 | -------------------------------------------------------------------------------- /floodgate.py: -------------------------------------------------------------------------------- 1 | from comfy.comfy_types.node_typing import IO, ComfyNodeABC, InputTypeDict 2 | 3 | 4 | class FloodGate(ComfyNodeABC): 5 | @classmethod 6 | def INPUT_TYPES(s) -> InputTypeDict: 7 | return { 8 | "required": { 9 | "source": (IO.ANY,), 10 | "gate_open": (IO.BOOLEAN, {"default": False}), 11 | } 12 | } 13 | 14 | RETURN_TYPES = (IO.ANY, IO.ANY) 15 | RETURN_NAMES = ("CLOSE", "OPEN") 16 | 17 | FUNCTION = "gate" 18 | CATEGORY = "utils" 19 | 20 | def gate(self, source, gate_open): 21 | return (source, source) 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.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 == 'Haoming02' }} 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Haoming 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI Floodgate 2 | This is an Extension for [ComfyUI](https://github.com/comfyanonymous/ComfyUI), which allows you to easily control the logic flow! 3 | 4 |

5 | 6 |

Motivation

7 | 8 | While **Hires. Fix** *(or similar workflows)* can significantly improve the output, it also takes a longer time to process. Thus, many will simply run the base resolution until a good seed is found before enabling it. However, for `ComfyUI` this means connecting and disconnecting multiple nodes every single time... 9 | 10 |

Solution

11 | 12 | Introducing, **Floodgate**!
13 | Simply toggle between the logic flows. No more reconnecting multiple nodes! 14 | 15 |

How to Use

16 | 17 | Connect the output of a node *(**eg.** `LATENT`)* to the `source` of the Floodgate node, then connect each path to the desired remaining workflow. Simply click on `gate_open` to toggle between the paths to take. The path not chosen will not be executed. Furthermore, since `ComfyUI` caches the intermediate results, opening the Floodgate will not require the precedent nodes to be processed again! 18 | 19 |

Features

20 | 21 | 1. Connect any arbitrary types 22 | - *(input/output types still have to match)* 23 | 2. Multiple Floodgates in one workflow 24 | 3. Control each Floodgate individually 25 | 26 |
27 | 28 | **Note:** The logic flow is parsed during the queuing stage, **not** the execution stage. As a result, the boolean value has to be already determined when you press `Queue Prompt`, such as from the toggle or a primitive node. If you use a node that outputs a boolean during execution, this will not work. 29 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import execution 2 | import nodes 3 | 4 | from .floodgate import FloodGate 5 | 6 | NODE_CLASS_MAPPINGS = {"FloodGate": FloodGate} 7 | NODE_DISPLAY_NAME_MAPPINGS = {"FloodGate": "Flood Gate"} 8 | 9 | 10 | def find_gate(prompt: dict) -> list: 11 | """Find the Unique ID of the Floodgate Node""" 12 | gate_IDs = [] 13 | 14 | for k, v in prompt.items(): 15 | if v["class_type"] == "FloodGate": 16 | gate_IDs.append(k) 17 | 18 | # if len(gate_IDs) > 1: 19 | # print('[Warning] Multiple Floodgates Detected is still experimental!') 20 | 21 | return gate_IDs 22 | 23 | 24 | def block_gate(prompt: dict, gate_ID: str, floodgate_open: bool) -> dict: 25 | """ "Bypass" the Nodes that should be Blocked""" 26 | nodes_affected = [] 27 | 28 | try: 29 | sauce_id, out_index = prompt[gate_ID]["inputs"]["source"] 30 | except KeyError: 31 | # Floodgate is not connected; let ComfyUI raise the error 32 | return prompt 33 | 34 | sauce_class = nodes.NODE_CLASS_MAPPINGS[prompt[sauce_id]["class_type"]] 35 | sauce_type = str(sauce_class.RETURN_TYPES[out_index]).lower().strip() 36 | 37 | for node, data in prompt.items(): 38 | for k, v in data["inputs"].items(): 39 | if not isinstance(v, list): 40 | continue 41 | 42 | if gate_ID in v: 43 | 44 | target_class = nodes.NODE_CLASS_MAPPINGS[data["class_type"]] 45 | target_type = ( 46 | (target_class.INPUT_TYPES()["required"][k][0]).lower().strip() 47 | ) 48 | 49 | if sauce_type != target_type: 50 | raise IOError() 51 | 52 | if (not floodgate_open) and (v[1] == 1): 53 | nodes_affected.append(node) 54 | break 55 | 56 | if (floodgate_open) and (v[1] == 0): 57 | nodes_affected.append(node) 58 | break 59 | 60 | for key in nodes_affected: 61 | del prompt[key] 62 | 63 | if len(nodes_affected) > 0: 64 | return recursive_block_gate(prompt, nodes_affected) 65 | else: 66 | return prompt 67 | 68 | 69 | def recursive_block_gate(prompt: dict, node_IDs: list) -> dict: 70 | """Block the subsequent nodes of which source has been blocked""" 71 | to_delete = [] 72 | 73 | for node, data in prompt.items(): 74 | for k, v in data["inputs"].items(): 75 | # Connection is always a List 76 | if not isinstance(v, list): 77 | continue 78 | 79 | if any(ID in v for ID in node_IDs): 80 | to_delete.append(node) 81 | break 82 | 83 | for key in to_delete: 84 | del prompt[key] 85 | 86 | if len(to_delete) > 0: 87 | return recursive_block_gate(prompt, to_delete) 88 | else: 89 | return prompt 90 | 91 | 92 | original_validate = execution.validate_prompt 93 | 94 | 95 | async def hijack_validate(*args): 96 | for arg in args: 97 | if isinstance(arg, dict): 98 | prompt = arg 99 | break 100 | 101 | gate_IDs: list = find_gate(prompt) 102 | 103 | if len(gate_IDs) == 0: 104 | return await original_validate(*args) 105 | 106 | for ID in gate_IDs: 107 | if ID not in prompt.keys(): 108 | continue 109 | 110 | try: 111 | gate_open = prompt[ID]["inputs"]["gate_open"] 112 | 113 | if isinstance(gate_open, (tuple, list)) and len(gate_open) == 0: 114 | gate_open = bool(gate_open[0]) 115 | if type(gate_open) is bool: 116 | prompt = block_gate(prompt, ID, gate_open) 117 | elif type(gate_open) is list: 118 | sauce_id, conn_id = gate_open 119 | 120 | gate_open = list(prompt[sauce_id]["inputs"].values())[conn_id] 121 | if type(gate_open) is bool: 122 | prompt = block_gate(prompt, ID, gate_open) 123 | else: 124 | raise ValueError 125 | else: 126 | raise ValueError 127 | 128 | except IOError: 129 | return ( 130 | False, 131 | { 132 | "type": "floodgate_io_mismatch", 133 | "message": "Floodgate IO Type Mismatch", 134 | "details": "source cannot be connected to outputs", 135 | "extra_info": {}, 136 | }, 137 | [], 138 | [], 139 | ) 140 | 141 | except ValueError: 142 | return ( 143 | False, 144 | { 145 | "type": "floodgate_invalid_boolean", 146 | "message": "Floodgate Unable to Determine Boolean", 147 | "details": "please use a primitive boolean node", 148 | "extra_info": {}, 149 | }, 150 | [], 151 | [], 152 | ) 153 | 154 | return await original_validate(*args) 155 | 156 | 157 | execution.validate_prompt = hijack_validate 158 | --------------------------------------------------------------------------------