From 0eeedf654552091a60c8dae04ac1e34f9edcda98 Mon Sep 17 00:00:00 2001 From: Teddy Xiong Date: Fri, 27 Mar 2026 02:38:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20re-design=20catalog=20|?= =?UTF-8?q?=20BatchMode=20Cross=20Task=20Result=20Compose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AsyncOutput.py | 7 ++- BatchIterator.py | 158 +++++++++++++++++++++++++++++++++++++++++++++++ Utils.py | 70 +++++++++++++++++++++ __init__.py | 11 ++++ 4 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 Utils.py diff --git a/AsyncOutput.py b/AsyncOutput.py index 32f0d32..d650c4b 100644 --- a/AsyncOutput.py +++ b/AsyncOutput.py @@ -4,6 +4,11 @@ @Nickname: AsyncOutput @Version: 0.0.1 ALPHA @URL: https://github.com/teddy1565/ComfyUI-AsyncOutput + + +NOT RECOMMENDED USE, Because ComfyUI not real support change status in upstream on a workflow. + +So Many Design not real match this scheduler(scope), it't not a real Turing complete. """ import os @@ -21,7 +26,7 @@ from server import PromptServer -MAIN_CATEGORY = "AsyncOutput" +MAIN_CATEGORY = "AsyncOutput/Deprecated(ComfyUI NOT Turing complete)" ASYNC_OUTPUT_GLOBAL_AUTO_RESET = True ASYNC_OUTPUT_STORAGE_DATA = {} diff --git a/BatchIterator.py b/BatchIterator.py index 0b29e08..6a8ee3b 100644 --- a/BatchIterator.py +++ b/BatchIterator.py @@ -23,6 +23,8 @@ MAIN_CATEGORY = "AsyncOutput/BatchIterator" ASYNC_OUTPUT_BATCH_ITERATOR_GLOBAL_AUTO_RESET = True ASYNC_OUTPUT_BATCH_ITERATOR_MULTILINE_TEXT_ITERATOR_DICT = {} +ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_DICT = {} +ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_COUNTER_DICT = {} class BatchIteratorMultiLineNode: def __init__(self): @@ -146,4 +148,160 @@ class BatchIteratorGlobalCacheClearNode: @classmethod def IS_CHANGED(self, force_clear=True, unique_id=0): + return float('nan') + +class BatchIteratorStringCollectionNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "incoming_input": ("STRING", { "forceInput": True }), + "key_id": ("STRING", {}) + }, + "hidden": { + "unique_id": "UNIQUE_ID" + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("passthough_output",) + CATEGORY = f'{MAIN_CATEGORY}/String' + FUNCTION = "collect" + + def collect(self, incoming_input, key_id, unique_id): + global ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_DICT + + if key_id == "": + raise Exception("ERROR: key_id property not set.") + + if key_id not in ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_DICT: + ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_DICT[key_id] = [] + + ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_DICT[key_id].append(incoming_input) + + return (incoming_input,) + + @classmethod + def IS_CHANGED(s, incoming_input, key_id, unique_id): + return float('nan') + +class BatchIteratorStringEmitterNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "touch": ("BOOLEAN", { "forceInput": True }), + + "mode": (["==", ">="], {"default": "=="}), + "key_id": ("STRING", {}), + "step": ("INT", { "default": 1, "min": 1 }), + "conditions": ("INT", { "min": 0, "default": 1 }), + }, + "optional": { + "reset": ("BOOLEAN", { "defaultInput": False }) + }, + "hidden": { + "unique_id": "UNIQUE_ID" + } + } + + RETURN_TYPES = ("INT", "BOOLEAN",) + RETURN_NAMES = ("count", "emit_result",) + OUTPUT_TOOLTIPS = ("Current Count Value", "if count == conditions, return true. then remove this record") + CATEGORY = f'{MAIN_CATEGORY}/String' + FUNCTION = "emit" + + def emit(self, touch, mode, key_id, step=1, conditions=1, reset=False, unique_id=0): + global ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_COUNTER_DICT + + if isinstance(step, int) == False: + step = int(step) + if isinstance(conditions, int) == False: + conditions = int(conditions) + + if math.isnan(step) or math.isnan(conditions): + raise Exception(f'ERROR: step or conditions data type not int. step: {type(step)}, conditions: {type(conditions)}') + + if key_id not in ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_COUNTER_DICT: + ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_COUNTER_DICT[key_id] = 0 + k = ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_COUNTER_DICT[key_id] + + if reset == True: + ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_COUNTER_DICT[key_id] = 0 + return (k, False) + + if touch == False: + return (k, False) + else: + if key_id not in ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_COUNTER_DICT: + ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_COUNTER_DICT[key_id] = 0 + ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_COUNTER_DICT[key_id] += step + + + emit_result = False + + if mode == "==" and ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_COUNTER_DICT[key_id] == conditions: + emit_result = True + elif mode == ">=" and ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_COUNTER_DICT[key_id] >= conditions: + emit_result = True + + return (k, emit_result) + + @classmethod + def IS_CHANGED(s, touch, step, conditions, mode, key_id, reset=False, unique_id=0): + return float('nan') + +class BatchIteratorStringCallbackNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "emitter_result": ("BOOLEAN", { "forceInput": True }), + "key_id": ("STRING", { }) + }, + "optional": { + "reset": ("BOOLEAN", { "defaultInput": False }) + }, + "hidden": { + "unique_id": "UNIQUE_ID" + } + } + + RETURN_TYPES = ("LIST", ) + RETURN_NAMES = ("output_list", ) + + CATEGORY = f'{MAIN_CATEGORY}/String' + FUNCTION = "get_collection" + + def get_collection(self, emitter_result, key_id, reset=False, unique_id=0): + global ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_DICT + + if reset == True: + if key_id in ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_DICT: + del ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_DICT[key_id] + return (comfy_execution.graph.ExecutionBlocker(None), ) + + if emitter_result == False: + return (comfy_execution.graph.ExecutionBlocker(None), ) + + if emitter_result == True: + if key_id not in ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_DICT: + return (comfy_execution.graph.ExecutionBlocker(None), ) + + data = ASYNC_OUTPUT_BATCH_ITERATOR_STORAGE_DATA_DICT[key_id] + return (data, ) + + return (comfy_execution.graph.ExecutionBlocker(None), ) + + @classmethod + def IS_CHANGED(s, emitter_result, key_id, reset=False, unique_id=0): return float('nan') \ No newline at end of file diff --git a/Utils.py b/Utils.py new file mode 100644 index 0000000..7538fb6 --- /dev/null +++ b/Utils.py @@ -0,0 +1,70 @@ +import os +import time +import json +import math +import random + +import nodes +import folder_paths +import comfy.utils +import comfy_execution +from server import PromptServer + +MAIN_CATEGORY = "AsyncOutput/Utils" + +class AsyncOutputUtilsConvertStringListToStringNode: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input_string_list": ("LIST", { "forceInput": True }), + "join_word": ("STRING", { "default": "" }), + "is_escape_char": ("BOOLEAN", { "default": False, "tooltip": "if use ascii escape char, must enable" }) + } + } + + RETURN_TYPES = ("STRING", ) + RETURN_NAMES = ("strings", ) + + CATEGORY = f'{MAIN_CATEGORY}/String' + FUNCTION = "convert_string_list_to_string" + DESCRIPTION = \ + """ + If use `\n` or others escape_char, must enable `is_escape_char`, or not, module will receive `\\n`, that not expected + + support list: + - `\n` + - `\t` + - `\r` + - `\'` + - `\"` + - `\\` + """ + + def convert_string_list_to_string(self, input_string_list=None, join_word="", is_escape_char=False): + + if isinstance(input_string_list, list) == False: + raise Exception("ERROR: input_string_list not a list.") + + result = join_word + + if is_escape_char == True: + escape_map = { + "\\n": "\n", + "\\t": "\t", + "\\r": "\r", + "\\\"": "\"", + "\\\'": "\'", + "\\\\": "\\" + } + + for escaped, real in escape_map.items(): + result = result.replace(escaped, real) + + + result = result.join(input_string_list) + + return (result, ) \ No newline at end of file diff --git a/__init__.py b/__init__.py index 4b23830..a351a62 100644 --- a/__init__.py +++ b/__init__.py @@ -7,6 +7,7 @@ from . import AsyncOutput from . import BatchIterator +from . import Utils NODE_CLASS_MAPPINGS = { "AsyncOutputCollect": AsyncOutput.AsyncOutputCollectionNode, @@ -22,6 +23,11 @@ NODE_CLASS_MAPPINGS = { "BatchIteratorMultiLine": BatchIterator.BatchIteratorMultiLineNode, "BatchIteratorGlobalCacheClear": BatchIterator.BatchIteratorGlobalCacheClearNode, + "BatchIteratorStringCollection": BatchIterator.BatchIteratorStringCollectionNode, + "BatchIteratorStringEmitter": BatchIterator.BatchIteratorStringEmitterNode, + "BatchIteratorStringCallback": BatchIterator.BatchIteratorStringCallbackNode, + + "UtilsConvertStringListToStringNode": Utils.AsyncOutputUtilsConvertStringListToStringNode, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -38,6 +44,11 @@ NODE_DISPLAY_NAME_MAPPINGS = { "BatchIteratorMultiLine": "Multiline Text Iterator", "BatchIteratorGlobalCacheClear": "BatchIterator GlobalCache Clear", + "BatchIteratorStringCollection": "BatchIterator String Collection", + "BatchIteratorStringEmitter": "BatchIterator String Emitter", + "BatchIteratorStringCallback": "BatchIterator String Callback", + + "UtilsConvertStringListToStringNode": "StringList To String", } __all__ = [