commit 25399acf4965d1d7c8ba68355d25bbf22d36d8c2 Author: younger15 Date: Wed Apr 22 22:56:25 2026 +0800 first upload text switch(with add) clip switch condition switch image switch model switch diff --git a/Iter_CondClipSwitch.py b/Iter_CondClipSwitch.py new file mode 100644 index 0000000..a7f56fa --- /dev/null +++ b/Iter_CondClipSwitch.py @@ -0,0 +1,57 @@ +import torch +import gc + +class Iter_ConditioningSwitch: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "output_index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "input_count": ("INT", {"default": 2, "min": 1, "max": 64}), + "use_last_input": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("CONDITIONING",) + FUNCTION = "switch_conditioning" + CATEGORY = "CustomNodes/Iterators" + + def switch_conditioning(self, output_index, input_count, use_last_input, **kwargs): + target_idx = output_index % input_count + res = None + for i in range(target_idx, -1, -1): + val = kwargs.get(f"cond_{i}") + if val is not None or not use_last_input: + res = val + break + #gc.collect() + #torch.cuda.empty_cache() + return (res,) + +class Iter_CLIPSwitch: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "output_index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "input_count": ("INT", {"default": 2, "min": 1, "max": 64}), + "use_last_input": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("CLIP",) + FUNCTION = "switch_clip" + CATEGORY = "CustomNodes/Iterators" + + def switch_clip(self, output_index, input_count, use_last_input, **kwargs): + target_idx = output_index % input_count + res = None + for i in range(target_idx, -1, -1): + val = kwargs.get(f"clip_{i}") + if val is not None or not use_last_input: + res = val + break + #gc.collect() + #torch.cuda.empty_cache() + return (res,) + \ No newline at end of file diff --git a/Iter_FloatSwitch.py b/Iter_FloatSwitch.py new file mode 100644 index 0000000..d8f3a68 --- /dev/null +++ b/Iter_FloatSwitch.py @@ -0,0 +1,29 @@ +class Iter_FloatSwitch: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "output_index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "input_count": ("INT", {"default": 2, "min": 1, "max": 128, "step": 1}), + "use_last_input": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("FLOAT",) + FUNCTION = "switch_float" + CATEGORY = "CustomNodes/Iterators" + + def switch_float(self, output_index, input_count, use_last_input, **kwargs): + # 1. 計算目標索引 + target_idx = output_index % input_count + + # 2. 搜尋有效浮點數 + res_float = None + for i in range(target_idx, -1, -1): + val = kwargs.get(f"float_{i}") + # 注意:在 Python 中 0.0 是 False,所以要檢查是否為 None + if val is not None or not use_last_input: + res_float = val + break + + return (float(res_float),) \ No newline at end of file diff --git a/Iter_ImageSwitch.py b/Iter_ImageSwitch.py new file mode 100644 index 0000000..76210fb --- /dev/null +++ b/Iter_ImageSwitch.py @@ -0,0 +1,35 @@ +import torch +import gc + + +class Iter_ImageSwitch: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "output_index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "input_count": ("INT", {"default": 2, "min": 1, "max": 64, "step": 1}), + "use_last_input": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "switch_image" + CATEGORY = "CustomNodes/Iterators" + + def switch_image(self, output_index, input_count, use_last_input, **kwargs): + # 1. 取得目標索引(對 input_count 取模,確保不溢出) + # 這樣 index 0~n 就會嚴格對應到 image_0 ~ image_n + target_idx = output_index % input_count + + # 2. 從目標索引開始「往回」搜尋有效接點 + res_image = None + for i in range(target_idx, -1, -1): + val = kwargs.get(f"image_{i}") + if val is not None or not use_last_input: + res_image = val + break + + gc.collect() + torch.cuda.empty_cache() + return (res_image,) diff --git a/Iter_ModelSwitch.py b/Iter_ModelSwitch.py new file mode 100644 index 0000000..a32e1e3 --- /dev/null +++ b/Iter_ModelSwitch.py @@ -0,0 +1,40 @@ +import torch +import gc + +class Iter_ModelSwitch: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "output_index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "input_count": ("INT", {"default": 2, "min": 1, "max": 64, "step": 1}), + "use_last_input": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "switch_model" + CATEGORY = "CustomNodes/Iterators" + + def switch_model(self, output_index, input_count, use_last_input, **kwargs): + # 1. 確定目標索引 (嚴格對應編號) + target_idx = output_index % input_count + + # 2. 往回搜尋有效模型 + res_model = None + for i in range(target_idx, -1, -1): + val = kwargs.get(f"model_{i}") + if val is not None or not use_last_input: + res_model = val + break + + + # 4. VRAM 管理 + #if force_gc == "enable": + #gc.collect() + #torch.cuda.empty_cache() + + return (res_model,) diff --git a/Iter_TextSwitch.py b/Iter_TextSwitch.py new file mode 100644 index 0000000..ae086f3 --- /dev/null +++ b/Iter_TextSwitch.py @@ -0,0 +1,68 @@ +import torch +import gc + +class Iter_TextSwitch: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + # 預設為空字串,允許不接線 + "add_text": ("STRING", {"forceInput": True, "default": ""}), + "output_index": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "input_count": ("INT", {"default": 2, "min": 1, "max": 128, "step": 1}), + "use_last_input": ("BOOLEAN", {"default": False}), + }, + "optional": { + # 這裡不需要寫死 text_0,因為 JS 會動態產生所有輸入 + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "switch_and_combine" + CATEGORY = "CustomNodes/Logic" + + def switch_and_combine(self, add_text="", output_index=0, input_count=1, use_last_input=False, **kwargs): + """ + # 1. 蒐集所有動態傳入的字串 + texts = [] + for i in range(input_count): + val = kwargs.get(f"text_{i}") + # 只有當該接點有連線且有值時才加入(避免計算空的接點) + if val is not None: + texts.append(val) + + # 2. 安全性檢查:如果完全沒有輸入接點有連線 + if not texts: + # 如果連 add_text 都沒接,就回傳純空字串 + return (add_text if add_text is not None else "",) + + # 3. 根據索引選取文本(防止除以零) + target_index = output_index % len(texts) + selected_text = texts[target_index] + + # 4. 串接並回傳 + # 確保兩者皆為字串,避免 NoneType 相加報錯 + final_header = add_text if add_text is not None else "" + final_selected = selected_text if selected_text is not None else "" + gc.collect() + torch.cuda.empty_cache() + return (final_header + final_selected,) + """ + # 1. 取得目標索引(對 input_count 取模,確保不溢出) + # 這樣 index 0~n 就會嚴格對應到 image_0 ~ image_n + target_idx = output_index % input_count + + # 2. 從目標索引開始「往回」搜尋有效接點 + res_txt = None + for i in range(target_idx, -1, -1): + val = kwargs.get(f"text_{i}") + if val is not None or not use_last_input: + res_txt = val + break + + #gc.collect() + #torch.cuda.empty_cache() + return (add_text + res_txt,) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8edeef6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 ChengJhong (younger15) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7647e84 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# ComfyUI-IterSwitch +A set of dynamic switch nodes for ComfyUI, designed to simplify complex workflows by allowing flexible, iterative input selection. These nodes provide dynamic connection management and intelligent fallback logic. + +## Key Features +Dynamic Inputs: Automatically resizes the number of input connections based on the input_count widget. + +## Smart Fallback: +With the use_last_input option enabled, the node can search backwards from the current index to find the nearest valid (connected) input, ensuring a result is always returned. + +## Type Support: +Includes switches for Text, Conditioning, CLIP, Image, Model, and Float types. + +### Example Usage: Iter_TextSwitch +The Iter_TextSwitch node allows you to combine a base string with one of many dynamically connected inputs. + +1. Basic Connection +The node takes an add_text input (base string) and allows you to dynamically define multiple text inputs. You can select the specific input using output_index. +![text switch sample](assets/example_iterTextSwitch01.png) +*Example using [waiIllustrious v16](https://civitai.com/models/827184?modelVersionId=2514310)* +2. Fallback Logic +When use_last_input is enabled, the node becomes more robust. If your current output_index points to a disconnected or empty input, the node will automatically search backwards through your inputs until it finds the most recent valid (connected) string. +![will use last valid input if use_last_input checked](assets/example_iterTextSwitch02.png) +*Example using [waiIllustrious v16](https://civitai.com/models/827184?modelVersionId=2514310)* +3. Iterative Workflow +By using the output_index, you can easily integrate this node into iterative loops, ensuring that your text prompts are dynamically selected or concatenated based on your workflow state. +![text switch sample2](assets/example_iterTextSwitch03.png) +*Example using [waiIllustrious v16](https://civitai.com/models/827184?modelVersionId=2514310)* + + + + +## Installation +Navigate to your ComfyUI custom_nodes directory. + +Run: + +Bash +`git clone https://github.com/YOUR_USERNAME/ComfyUI-SwitchForIteration.git` +Restart ComfyUI. + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..bbf9b26 --- /dev/null +++ b/__init__.py @@ -0,0 +1,28 @@ +from .Iter_TextSwitch import Iter_TextSwitch +from .Iter_CondClipSwitch import Iter_ConditioningSwitch, Iter_CLIPSwitch +from .Iter_ImageSwitch import Iter_ImageSwitch +from .Iter_ModelSwitch import Iter_ModelSwitch +from .Iter_FloatSwitch import Iter_FloatSwitch + +NODE_CLASS_MAPPINGS = { + "Iter_TextSwitch": Iter_TextSwitch, + "Iter_ConditioningSwitch": Iter_ConditioningSwitch, + "Iter_CLIPSwitch": Iter_CLIPSwitch, + "Iter_ImageSwitch": Iter_ImageSwitch, + "Iter_ModelSwitch": Iter_ModelSwitch, + "Iter_FloatSwitch": Iter_FloatSwitch +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "Iter_TextSwitch": "Iter_TextSwitch", + "Iter_ConditioningSwitch": "Iter_ConditioningSwitch", + "Iter_CLIPSwitch": "Iter_CLIPSwitch", + "Iter_ImageSwitch": "Iter_ImageSwitch", + "Iter_ModelSwitch": "Iter_ModelSwitch", + "Iter_FloatSwitch": "Iter_FloatSwitch" +} + +# 確保 JS 資料夾被載入 +WEB_DIRECTORY = "./web" + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] \ No newline at end of file diff --git a/assets/example_iterTextSwitch01.png b/assets/example_iterTextSwitch01.png new file mode 100644 index 0000000..4fc34d5 Binary files /dev/null and b/assets/example_iterTextSwitch01.png differ diff --git a/assets/example_iterTextSwitch02.png b/assets/example_iterTextSwitch02.png new file mode 100644 index 0000000..aa26b0b Binary files /dev/null and b/assets/example_iterTextSwitch02.png differ diff --git a/assets/example_iterTextSwitch03.png b/assets/example_iterTextSwitch03.png new file mode 100644 index 0000000..5afaa39 Binary files /dev/null and b/assets/example_iterTextSwitch03.png differ diff --git a/web/js/Iter_Switch.js b/web/js/Iter_Switch.js new file mode 100644 index 0000000..453db49 --- /dev/null +++ b/web/js/Iter_Switch.js @@ -0,0 +1,62 @@ +import { app } from "../../../scripts/app.js"; + +// 通用的動態接點處理函數 +const setupSmartDynamicInputs = (nodeType, prefix, type) => { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated?.apply(this, arguments); + const inputCountWidget = this.widgets.find(w => w.name === "input_count"); + + inputCountWidget.callback = () => { + const targetCount = inputCountWidget.value; + + // 1. 找出目前所有屬於該 prefix 的輸入點 + const currentInputs = this.inputs.filter(i => i.name.startsWith(`${prefix}_`)); + const currentCount = currentInputs.length; + + if (targetCount > currentCount) { + // 2. 如果目標數量較多:只新增缺少的接點,原有的接點不受影響 + for (let i = currentCount; i < targetCount; i++) { + this.addInput(`${prefix}_${i}`, type); + } + } else if (targetCount < currentCount) { + // 3. 如果目標數量較少:從最後面開始移除,保留前面的連線 + for (let i = currentCount - 1; i >= targetCount; i--) { + // 找到該名稱對應的索引並移除 + const inputIdx = this.inputs.findIndex(input => input.name === `${prefix}_${i}`); + if (inputIdx !== -1) { + this.removeInput(inputIdx); + } + } + } + + // 重新整理 UI 配置 + this.setSize(this.computeSize()); + }; + + // 延遲初始化,確保節點建立時 UI 正確 + setTimeout(() => { + if (inputCountWidget.callback) inputCountWidget.callback(); + }, 50); + }; +}; + +app.registerExtension({ + name: "CustomNodes.IterSwitches", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // 根據節點名稱套用不同的前綴與類型 + if (nodeData.name === "Iter_TextSwitch") { + setupSmartDynamicInputs(nodeType, "text", "STRING"); + } else if (nodeData.name === "Iter_ConditioningSwitch") { + setupSmartDynamicInputs(nodeType, "cond", "CONDITIONING"); + } else if (nodeData.name === "Iter_CLIPSwitch") { + setupSmartDynamicInputs(nodeType, "clip", "CLIP"); + } else if (nodeData.name === "Iter_ImageSwitch") { + setupSmartDynamicInputs(nodeType, "image", "IMAGE"); + } else if (nodeData.name === "Iter_ModelSwitch") { + setupSmartDynamicInputs(nodeType, "model", "MODEL"); + } else if (nodeData.name === "Iter_FloatSwitch") { + setupSmartDynamicInputs(nodeType, "float", "FLOAT"); + } + } +}); \ No newline at end of file