├── requirements.txt ├── .gitattributes ├── .gitignore ├── .github └── workflows │ └── publish_action.yml ├── LICENSE ├── pyproject.toml ├── README.md ├── __init__.py └── web └── node └── dynamicnode.js /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | _old/* 3 | *.py[cod] 4 | *$py.class 5 | *.code-workspace 6 | .vscode 7 | .env 8 | .DS_Store 9 | *.egg-info 10 | *.bak -------------------------------------------------------------------------------- /.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.event.repository.fork == false 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 }} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Alexander G. Morano 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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "cozy_ex_dynamic" 3 | description = "Dynamic Node examples for ComfyUI" 4 | version = "1.0.4" 5 | license = { file = "LICENSE" } 6 | readme = "README.md" 7 | authors = [{ name = "Alexander G. Morano", email = "amorano@gmail.com" }] 8 | classifiers = [ 9 | "License :: OSI Approved :: MIT License", 10 | "Operating System :: OS Independent", 11 | "Programming Language :: Python", 12 | "Programming Language :: Python :: 3", 13 | "Programming Language :: Python :: 3.11", 14 | "Programming Language :: Python :: 3.12", 15 | "Intended Audience :: Developers", 16 | ] 17 | requires-python = ">=3.11" 18 | dependencies = [] 19 | 20 | [project.urls] 21 | Homepage = "https://github.com/cozy-comfyui/cozy_ex_dynamic" 22 | Documentation = "https://github.com/cozy-comfyui/cozy_ex_dynamic/wiki" 23 | Repository = "https://github.com/cozy-comfyui/cozy_ex_dynamic" 24 | Issues = "https://github.com/cozy-comfyui/cozy_ex_dynamic/issues" 25 | 26 | [tool.comfy] 27 | PublisherId = "amorano" 28 | DisplayName = "Cozy Dynamic" 29 | Icon = "https://raw.githubusercontent.com/Amorano/Jovimetrix-examples/refs/heads/master/res/logo-cozy-comfyui.png" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example of Dynamic Inputs for ComfyUI 2 | 3 | ![image](https://github.com/user-attachments/assets/6f5ac899-66ed-459b-8a61-5825f87853db) 4 | 5 | An example for how to do the specific mechanism of adding dynamic inputs to a node. 6 | 7 | ## Why is this a thing? 8 | 9 | Because a lot of people ask the same questions over and over and the examples are always in some type of compound setup which requires unwinding a lot of extra code or logic that is not required to answer the main question. 10 | 11 | ## How is this different than having to pull apart all those other repositories? 12 | 13 | The example is kept to (at most) two files: 14 | * The python entry point 15 | * The supporting js 16 | This keeps the focus on the actual problem being solved. 17 | 18 | The file names for the nodes will match in name to the node example they represent. 19 | 20 | ## Installation: 21 | 22 | Clone this repository to 'ComfyUI/custom_nodes` folder. 23 | 24 | There are no extra requirements. 25 | 26 | # Node List 27 | 28 | ## Dynamic Node (cozy) 29 | 30 | Multiple Inputs. 31 | 32 | ## CONTRIBUTORS TO THIS CODE CONCEPT 33 | 34 | This example uses concepts and code constructs from various repositories. The contributions by these developers have made such exemplars possible. Please take a moment to look into their work or send them a thank you. 35 | 36 | ## [Kijai](https://github.com/Kijai) 37 | 38 | ## [pythongosssss](https://github.com/pythongosssss) 39 | 40 | ## [melmass](https://github.com/melMass) 41 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | @title: cozy_ex_dynamic 3 | @author: amorano 4 | @category: Example 5 | @reference: https://github.com/cozy-comfyui/cozy_ex_dynamic 6 | @tags: dynamic, example, developer, script, mechanism, exemplar 7 | @description: Example of a Node with Dynamic Inputs 8 | @node list: 9 | CozyDynamicNode 10 | @version: 1.0.4 11 | """ 12 | 13 | from typing import Any, Dict 14 | 15 | # ============================================================================= 16 | # === GLOBAL === 17 | # ============================================================================= 18 | 19 | NODE_CLASS_MAPPINGS = {} 20 | NODE_DISPLAY_NAME_MAPPINGS = {} 21 | WEB_DIRECTORY = "./web" 22 | __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] 23 | 24 | # ============================================================================= 25 | # === NODE === 26 | # ============================================================================= 27 | 28 | class DynamicNodeCozy(): 29 | """ 30 | A class to represent a dynamic node in ComfyUI. 31 | """ 32 | RETURN_TYPES = ("IMAGE",) 33 | RETURN_NAMES = ("IMAGE",) 34 | FUNCTION = "run" 35 | CATEGORY = "_EXAMPLES" 36 | 37 | @classmethod 38 | def INPUT_TYPES(s) -> Dict[str, dict]: 39 | return { 40 | "required": {}, 41 | "optional": {} 42 | } 43 | 44 | def run(self, **kw) -> tuple[Any | None]: 45 | # the dynamically created input data will be in the dictionary kwargs 46 | #for k, v in kw.items(): 47 | # print(f'{k} => {v}') 48 | 49 | # return the first value found or `None` if no inputs are defined 50 | value = None 51 | if len(values := kw.values()) > 0: 52 | value = next(iter(values)) 53 | return (value,) 54 | 55 | # ============================================================================= 56 | # === REGISTRATION === 57 | # ============================================================================= 58 | 59 | NODE_CLASS_MAPPINGS = { 60 | "DynamicNodeCozy": DynamicNodeCozy, 61 | } 62 | 63 | NODE_DISPLAY_NAME_MAPPINGS = { 64 | "DynamicNodeCozy": "Dynamic Node (cozy)", 65 | } 66 | -------------------------------------------------------------------------------- /web/node/dynamicnode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * File: dynamicnode.js 3 | * Project: cozy_ex_dynamic 4 | * 5 | */ 6 | 7 | import { app } from "../../../scripts/app.js" 8 | 9 | const TypeSlot = { 10 | Input: 1, 11 | Output: 2, 12 | }; 13 | 14 | const TypeSlotEvent = { 15 | Connect: true, 16 | Disconnect: false, 17 | }; 18 | 19 | const _ID = "DynamicNodeCozy"; 20 | const _PREFIX = "image"; 21 | const _TYPE = "IMAGE"; 22 | 23 | app.registerExtension({ 24 | name: 'cozy_ex.' + _ID, 25 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 26 | // skip the node if it is not the one we want 27 | if (nodeData.name !== _ID) { 28 | return 29 | } 30 | 31 | const onNodeCreated = nodeType.prototype.onNodeCreated; 32 | nodeType.prototype.onNodeCreated = async function () { 33 | const me = onNodeCreated?.apply(this); 34 | // start with a new dynamic input 35 | this.addInput(_PREFIX, _TYPE); 36 | // Ensure the new slot has proper appearance 37 | const slot = this.inputs[this.inputs.length - 1]; 38 | if (slot) { 39 | slot.color_off = "#666"; 40 | } 41 | return me; 42 | } 43 | 44 | const onConnectionsChange = nodeType.prototype.onConnectionsChange 45 | nodeType.prototype.onConnectionsChange = function (slotType, slot_idx, event, link_info, node_slot) { 46 | const me = onConnectionsChange?.apply(this, arguments); 47 | 48 | if (slotType === TypeSlot.Input) { 49 | if (link_info && event === TypeSlotEvent.Connect) { 50 | // get the parent (left side node) from the link 51 | const fromNode = this.graph._nodes.find( 52 | (otherNode) => otherNode.id == link_info.origin_id 53 | ) 54 | 55 | if (fromNode) { 56 | // make sure there is a parent for the link 57 | const parent_link = fromNode.outputs[link_info.origin_slot]; 58 | if (parent_link) { 59 | node_slot.type = parent_link.type; 60 | node_slot.name = `${_PREFIX}_`; 61 | } 62 | } 63 | } else if (event === TypeSlotEvent.Disconnect) { 64 | this.removeInput(slot_idx); 65 | } 66 | 67 | // Track each slot name so we can index the uniques 68 | let idx = 0; 69 | let slot_tracker = {}; 70 | for(const slot of this.inputs) { 71 | if (slot.link === null) { 72 | try { 73 | this.removeInput(idx); 74 | } catch { 75 | 76 | } 77 | continue; 78 | } 79 | idx += 1; 80 | const name = slot.name.split('_')[0]; 81 | 82 | // Correctly increment the count in slot_tracker 83 | let count = (slot_tracker[name] || 0) + 1; 84 | slot_tracker[name] = count; 85 | 86 | // Update the slot name with the count if greater than 1 87 | slot.name = `${name}_${count}`; 88 | } 89 | 90 | // check that the last slot is a dynamic entry.... 91 | let last = this.inputs[this.inputs.length - 1]; 92 | if (last === undefined || (last.name != _PREFIX || last.type != _TYPE)) { 93 | this.addInput(_PREFIX, _TYPE); 94 | // Set the unconnected slot to appear gray 95 | last = this.inputs[this.inputs.length - 1]; 96 | if (last) { 97 | last.color_off = "#666"; 98 | } 99 | } 100 | 101 | // force the node to resize itself for the new/deleted connections 102 | this?.graph?.setDirtyCanvas(true); 103 | return me; 104 | } 105 | } 106 | return nodeType; 107 | }, 108 | 109 | }) 110 | --------------------------------------------------------------------------------