1186 lines
41 KiB
Python
1186 lines
41 KiB
Python
"""
|
|
@Author: VykosX
|
|
@Description: Custom nodes for ComfyUI to enable flow control with advanced loops, conditional branching, logic operations and several other nifty utilities to enhance your ComfyUI workflows
|
|
@Title: ControlFlowUtils
|
|
@Nickname: ControlFlowUtils
|
|
@Version: 0.1.0 ALPHA
|
|
@URL: https://github.com/VykosX/ControlFlowUtils
|
|
"""
|
|
# UNSTABLE ALPHA RELEASE - EXPECT BUGS TO BITE
|
|
# PLEASE DO NOT SHARE OUTSIDE OF LINKING TO THE GITHUB
|
|
|
|
import json
|
|
import math
|
|
import folder_paths
|
|
import os
|
|
import torch
|
|
import gc
|
|
import time
|
|
import nodes
|
|
import comfy.model_management as mem_manager
|
|
from server import PromptServer
|
|
|
|
from . import Types
|
|
any_type = Types.AnyType("*")
|
|
|
|
MAIN_CATEGORY = "🐺 VykosX-ControlFlowUtils"
|
|
|
|
DEBUG_MODE = False #Enable this flag to get all sorts of useful debug information in the console from most of the nodes in this pack.
|
|
|
|
'''
|
|
FUNCTION NAME: cbool
|
|
PURPOSE: Converts values to Boolean
|
|
PARAMETERS:
|
|
- Value (Any): The value to convert
|
|
RETURNS: True or False based on whether Value can be interpreted as a Boolean
|
|
'''
|
|
def cbool(Value):
|
|
if str(Value).lower() in ("yes", "y", "true", "t", "1"):
|
|
return True
|
|
if str(Value).lower() in ("no", "n", "false", "f", "0", "0.0", "", "none", "[]", "{}"):
|
|
return False
|
|
raise Exception('Invalid value for boolean conversion:', Value)
|
|
|
|
'''
|
|
FUNCTION NAME: cint
|
|
PURPOSE: Converts values to Integer
|
|
PARAMETERS:
|
|
- Value (Any): The value to convert
|
|
RETURNS: An integer rounded to the nearest even number
|
|
'''
|
|
def cint(Value):
|
|
|
|
d = 0 #How many decimals to round to. For integers this is always 0
|
|
try:
|
|
Value=float(Value)
|
|
except:
|
|
try:
|
|
Value = len(Value)
|
|
if l == 0:
|
|
return 0
|
|
except:
|
|
raise Exception('Invalid value for integer conversion:',x)
|
|
|
|
p = 10 ** d
|
|
|
|
if Value > 0:
|
|
z = float(math.floor((Value * p) + 0.5))/p
|
|
else:
|
|
z = float(math.ceil((Value * p) - 0.5))/p
|
|
|
|
return int(z)
|
|
|
|
'''
|
|
CLASS NAME: Cycle
|
|
PURPOSE: Allows transmitting cached data to be processed iteratively between multiple queues
|
|
INPUTS:
|
|
- start (Integer): The initial value for the loop counters
|
|
- step (Integer): A positive or negative offset to apply to the loop counter on each iteration
|
|
- end (Integer): The value in which the cycle will be considered to have completed and the Finish flag will be set
|
|
- manual_reset (Boolean): Toggle this property to have the next cycle be a dry run and re-initialize all parameters.
|
|
Index, Data and Aux_Data will be set to None for the initial execution
|
|
- auto-reset (Boolean): Specifies whether the Cycle should reset to the original start value once the cycle is complete
|
|
or keep cycling indefinitely
|
|
INTERNAL:
|
|
- state (Dictionary): Internal variable which gets shared across the cycle and keeps track of its parameters
|
|
- index (Integer): The current loop index
|
|
- finish (Boolean): A flag which specifies whether the index has reached the end value
|
|
'''
|
|
class Cycle:
|
|
def __init__(self):
|
|
self.state = {'index': 0,'step': 1,'end': 0, 'start': 0, 'finish': False, 'auto_reset': True}
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"start": ("INT", {"default": 0,"forceInput": False}),
|
|
"step": ("INT", {"default": 1,"forceInput": False}),
|
|
"end": ("INT", {"default": 0,"forceInput": False})
|
|
},"optional": {
|
|
"manual_reset": ("BOOLEAN", {"forceInput": False}),
|
|
"auto_reset": ("BOOLEAN", {"forceInput": False, "default": True, "label_on": "Auto Reset on Cycle End", "label_off": "Keep Cycling until Manual Stop" })
|
|
},"hidden": { "node_id": "UNIQUE_ID" },
|
|
}
|
|
|
|
RETURN_TYPES = ("CYCLE","BOOLEAN")
|
|
RETURN_NAMES = ("CYCLE","RESET?")
|
|
FUNCTION = "run"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def run(self,start,step,end,manual_reset,auto_reset,node_id):
|
|
|
|
if DEBUG_MODE:
|
|
print (">> CYCLE INIT!")
|
|
|
|
#PromptServer.instance.send_sync("VykosX.StartCycle", {"node": node_id})
|
|
|
|
if manual_reset or self.state['finish']:
|
|
if manual_reset or auto_reset:
|
|
|
|
print (">> CYCLE RESET!")
|
|
|
|
self.state = {'index': None,'step':step,'end': end, 'start': start, 'finish': False, 'auto_reset': auto_reset}
|
|
|
|
return (self.state,True)
|
|
|
|
return (self.state,False)
|
|
|
|
@classmethod
|
|
def IS_CHANGED(self, start ,step, end,manual_reset,auto_reset,node_id):
|
|
if reset:
|
|
self.state = {'index': start,'step':step,'end': end, 'start': start, 'finish': False, 'auto_reset': auto_reset}
|
|
return True
|
|
return False
|
|
|
|
class CycleStart:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {"data": any_type, "CYCLE": ("CYCLE",)},
|
|
"optional": {"aux_data": any_type, "index_override": ("INT", {"forceInput": True},) }
|
|
}
|
|
|
|
RETURN_TYPES = (any_type,any_type,"INT",)
|
|
RETURN_NAMES = ("*","*","index",)
|
|
FUNCTION = "run"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def run(self, data, CYCLE, aux_data=None,index_override=None):
|
|
|
|
if CYCLE['index'] is None:
|
|
|
|
if DEBUG_MODE:
|
|
print (">> CYCLE START - DRY RUN")
|
|
|
|
return (None,None,None,)
|
|
|
|
else:
|
|
|
|
if index_override is not None:
|
|
CYCLE['index'] = index_override
|
|
|
|
if 'next' in CYCLE:
|
|
return (CYCLE['next'],CYCLE['aux'],CYCLE['index'])
|
|
return (data,aux_data,CYCLE['index'])
|
|
|
|
@classmethod
|
|
def IS_CHANGED(s, data, CYCLE, aux_data=None):
|
|
if 'next' in CYCLE:
|
|
return id(CYCLE['next'])
|
|
return float("NaN")
|
|
|
|
class CycleEnd:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": { "data": (any_type,), "CYCLE": ("CYCLE",) },
|
|
"optional": { "aux_data": (any_type,)}
|
|
}
|
|
|
|
RETURN_TYPES = ("INT","BOOLEAN",)
|
|
RETURN_NAMES = ("index","finished",)
|
|
FUNCTION = "run"
|
|
CATEGORY = MAIN_CATEGORY
|
|
OUTPUT_NODE = True
|
|
|
|
def run(self, data, CYCLE, aux_data=None):
|
|
|
|
if CYCLE['index'] is None:
|
|
CYCLE['index'] = CYCLE['start']
|
|
if DEBUG_MODE:
|
|
print (">> CYCLE END - DRY RUN")
|
|
return (None,None,None)
|
|
|
|
if DEBUG_MODE:
|
|
print (">> CYCLE ITERATION '", CYCLE['index'],"'!")
|
|
|
|
CYCLE['next'] = data
|
|
CYCLE['aux'] = aux_data
|
|
CYCLE['index'] += CYCLE['step']
|
|
|
|
if CYCLE['step'] >= 0:
|
|
CYCLE['finished'] = ( CYCLE['end']+1 - CYCLE['index'] <= 0 )
|
|
else:
|
|
CYCLE['finished'] = ( CYCLE['end']+1 - CYCLE['index'] >= 0)
|
|
|
|
if CYCLE['finished'] and CYCLE['auto_reset']:
|
|
|
|
if DEBUG_MODE:
|
|
print (">> CYCLE AUTO-RESET!")
|
|
|
|
CYCLE['index'] = CYCLE['start']
|
|
|
|
return (CYCLE['index'],CYCLE['finished'])
|
|
|
|
@classmethod
|
|
def IS_CHANGED(s, data, CYCLE, aux_data=None):
|
|
ret = (not CYCLE['finished'])
|
|
if DEBUG_MODE:
|
|
print ("CYCLE CHANGE:",ret)
|
|
return ret
|
|
|
|
|
|
class UniversalSwitch:
|
|
|
|
#TODO: Make input and output amounts dynamic.
|
|
# Have a working setting for type validation.
|
|
|
|
max_slots = 6
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
|
|
dynamic_inputs = {"input2": (any_type, {"lazy": True}),}
|
|
for x in range(2, s.max_slots+1):
|
|
dynamic_inputs[f"input{x}"] = (any_type, {"lazy": True})
|
|
return {
|
|
"required": { "input1": (any_type, {"lazy": True}),"selection": ("INT", {"default": 1, "min": 0, "max": s.max_slots, "step": 1, "forceInput": False}),"mode": (["SWITCH > ONE","SWITCH > ALL","INV. SWITCH > ONE","INV. SWITCH > ALL","PASSTHROUGH","SORT","REVERSE"],),"validate_typing": ("BOOLEAN", {"forceInput": False}),
|
|
}, "optional": dynamic_inputs, "hidden": {"prompt": "PROMPT", "unique_id": "UNIQUE_ID","extra_pnginfo": "EXTRA_PNGINFO"},
|
|
}
|
|
|
|
def pack_tuple(s,prefix_type,general_type,count):
|
|
|
|
return tuple([prefix_type] + [general_type for x in range(1,count+1)])
|
|
|
|
RETURN_TYPES = pack_tuple(None,"INT",any_type,max_slots) #tuple(["INT"] + [any_type for x in range (1,max_slots+1)])
|
|
RETURN_NAMES = tuple([x for x in ["Index"] + list( "*" * max_slots) ]) #tuple('*' * max_slots) #("Index","*","*","*","*","*",)
|
|
FUNCTION = "switch"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def check_lazy_status(s, *args, **kwargs):
|
|
|
|
selection = int(kwargs['selection'])
|
|
|
|
if selection == 0:
|
|
inputs = ["input1"]
|
|
for x in range(2,s.max_slots+1):
|
|
inputs.append("input"+str(x))
|
|
else:
|
|
inputs = [f"input{selection}"]
|
|
|
|
if DEBUG_MODE:
|
|
print ("SWITCH [",kwargs['unique_id'],"] LAZY CHECK: ", inputs)
|
|
|
|
return inputs
|
|
|
|
def switch(s,input1,mode,selection,validate_typing,**kwargs):
|
|
|
|
if selection is None:
|
|
return (None*s.max_slots)
|
|
|
|
def find_output_count(kwargs):
|
|
|
|
if DEBUG_MODE:
|
|
workflow_info,unique_id,prompt = kwargs.pop('extra_pnginfo'),kwargs.pop('unique_id'),kwargs.pop('prompt')
|
|
#print ("WORKFLOW:",workflow_info,"\n\nUNIQUE_ID:",unique_id,"\n\nPROMPT:",prompt)
|
|
|
|
print ("\n>> SWITCH [",unique_id,"]")
|
|
|
|
return 0 #Implement mechanism for returning the index of the first output node that is actually connected to another node
|
|
#use extra_pinginfo and unique_id
|
|
|
|
selected_slot = None
|
|
outputs = find_output_count(kwargs)
|
|
|
|
options = [input1]
|
|
|
|
for key,value in kwargs.items():
|
|
options.append(value)
|
|
|
|
if DEBUG_MODE:
|
|
print (">> OPTIONS:",options)
|
|
|
|
|
|
if "INV. SWITCH" not in mode:
|
|
|
|
if selection == 0:
|
|
i=0
|
|
for input in options:
|
|
i+=1
|
|
if input is not None:
|
|
selected_slot = input
|
|
if DEBUG_MODE:
|
|
print (">> SELECTED SLOT (FIRST VALID):",i)
|
|
break
|
|
|
|
elif selection > 0:
|
|
if selection >= len(options):
|
|
selected_slot = options[len(options)-1]
|
|
else:
|
|
selected_slot = options[selection-1]
|
|
|
|
match mode:
|
|
|
|
case "SWITCH > ONE":
|
|
|
|
ret = [selection] + [selected_slot] + [None] * (s.max_slots-1)
|
|
|
|
case "SWITCH > ALL":
|
|
ret = s.pack_tuple(selection,selected_slot, s.max_slots)
|
|
|
|
case "INV. SWITCH > ONE":
|
|
|
|
ret = [selection] + [None] * (s.max_slots)
|
|
ret[selection] = input1
|
|
|
|
case "INV. SWITCH > ALL":
|
|
|
|
ret = s.pack_tuple(selection,input1, s.max_slots)
|
|
|
|
#The following operation modes are still untested
|
|
|
|
case "PASSTHROUGH":
|
|
|
|
ret = [selection] + options
|
|
|
|
case "SORT":
|
|
|
|
ret = [selection] + sorted(options)
|
|
|
|
case "REVERSE":
|
|
|
|
ret = [selection] + options[::-1]
|
|
|
|
print("\n>> RETURN: ", ret)
|
|
|
|
return ret
|
|
|
|
class IfConditionSelector:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"condition": (["A is TRUE", "B is TRUE", "A is NONE","B is NONE", "A == B", "A != B", "A > B", "A >= B", "A < B", "A <= B","A is B","A is not B","A in B", "B in A", "A & B", "A | B", "A ^ B"],), "comparison_type": (["Values","Length(A)|Value(B)","Length (Both)","Address(A)|Value (B)","Address (Both)","Custom Expression"],),
|
|
"NOT": ("BOOLEAN",),
|
|
"A": (any_type, {"forceInput": True, "lazy": True}),
|
|
"B": (any_type, {"forceInput": True, "lazy": True}),
|
|
}, "optional": {
|
|
"TRUE_IN": (any_type, {"forceInput": True, "lazy": True}),
|
|
"FALSE_IN": (any_type, {"forceInput": True, "lazy": True}),
|
|
}, "hidden": { "unique_id": "UNIQUE_ID" }
|
|
|
|
}
|
|
|
|
RETURN_TYPES = (any_type,)
|
|
RETURN_NAMES = ("*",)
|
|
FUNCTION = "run_comparison"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def compare(self,A,B,condition,comparison_type,NOT):
|
|
|
|
ret = False
|
|
|
|
match comparison_type:
|
|
|
|
case "Length(A)|Value(B)":
|
|
A = len(A)
|
|
case "Length (Both)":
|
|
A,B = len(A),len(B)
|
|
case "Address(A)|Value (B)":
|
|
A = id(A)
|
|
case "Address (Both)":
|
|
A,B, = id(A),id(B)
|
|
case "Custom Expression": #UNTESTED! Expected format: (A={Keys:Values}, B=Expression (%Key% to resolve)
|
|
if type(A) is dict:
|
|
for key, value in my_dict.items():
|
|
B = B.replace('%'+key+'%',value)
|
|
return cbool( eval(B) )
|
|
|
|
match condition:
|
|
|
|
case "A is TRUE":
|
|
ret = (A == True)
|
|
case "B is TRUE":
|
|
ret = (B == True)
|
|
case "A is NONE":
|
|
ret = (A is None)
|
|
case "B is NONE":
|
|
ret = (B is None)
|
|
case "A is B":
|
|
ret = (A is B)
|
|
case "A is not B":
|
|
ret = (A is not B)
|
|
case "A in B":
|
|
ret = (A in B)
|
|
case "B in A":
|
|
ret = (B in A)
|
|
case "A == B":
|
|
ret = (A == B)
|
|
case "A != B":
|
|
ret = (A != B)
|
|
case "A > B":
|
|
ret = (A > B)
|
|
case "A >= B":
|
|
ret = (A <= B)
|
|
case "A < B":
|
|
ret = (A < B)
|
|
case "A <= B":
|
|
ret = (A < B)
|
|
case "A & B":
|
|
ret = (A & B)
|
|
case "A | B":
|
|
ret = (A | B)
|
|
case "A ^ B":
|
|
ret = (A ^ B)
|
|
|
|
if NOT:
|
|
ret = ~ret
|
|
|
|
return ret
|
|
|
|
def run_comparison(s,condition,comparison_type,NOT,A,B,TRUE_IN=None,FALSE_IN=None,unique_id=0):
|
|
|
|
print ("\n>> IF CONDITION [",unique_id,"]")
|
|
|
|
print (">> COMPARE! A:",A,"B:",B,"cond:",condition,"comp:",comparison_type,"NOT:",NOT,"TRUE:",TRUE_IN,"FALSE:",FALSE_IN)
|
|
|
|
ret = s.compare(A,B,condition,comparison_type,NOT)
|
|
|
|
print (">> COMPARE RESULT:",ret)
|
|
|
|
return (TRUE_IN,) if ret else (FALSE_IN,)
|
|
|
|
def check_lazy_status(s,condition,comparison_type,NOT,A,B,TRUE_IN=None,FALSE_IN=None,unique_id=0):
|
|
|
|
print (">> IF LAZY CHECK! A:",A,"B:",B,"cond:",condition,"comp:",comparison_type,"NOT:",NOT,"TRUE:",TRUE_IN,"FALSE:",FALSE_IN)
|
|
|
|
ret = s.compare(A,B,condition,comparison_type,NOT)
|
|
|
|
lazy = ["A","B"] #Decided to make both A and B required. Pass NULL Output to whichever one is not in use if needed.
|
|
lazy += ["TRUE_IN"] if ret else ["FALSE_IN"]
|
|
|
|
print (">> LAZY RESULT:",lazy)
|
|
return lazy
|
|
|
|
class HaltExecution:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"disable": ("BOOLEAN", {"default": False, "forceInput": False}),
|
|
"clear_queue": ("BOOLEAN", {"default": False, "forceInput": False}),
|
|
"input": (any_type,),
|
|
}, "optional": { "aux": (any_type,), }
|
|
}
|
|
|
|
RETURN_TYPES = (any_type,)
|
|
RETURN_NAMES = ("*",)
|
|
CATEGORY = MAIN_CATEGORY
|
|
FUNCTION = "halt"
|
|
|
|
def halt(s,disable,clear_queue,input, aux=None):
|
|
|
|
print (" >> ATTEMPTING HALT!",disable,input, )
|
|
|
|
if not disable and input is not None:
|
|
|
|
if DEBUG_MODE:
|
|
print ("\n"+"-"*36+"\n # HALTING EXECUTION #"+"\n"+"-"*36)
|
|
|
|
if clear_queue:
|
|
PromptServer.instance.send_sync("VykosX.ClearQueue", {}) #Not working currently
|
|
|
|
nodes.interrupt_processing(True)
|
|
|
|
return (None,)
|
|
|
|
return (input,)
|
|
|
|
class DataMonitor:
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"text":("STRING", {"default": '',"multiline": True,"forceInput": False,"print_to_screen": True}),
|
|
"output_type": (["ANY","STRING","INT","FLOAT","BOOLEAN","LIST","TUPLE","JSON","FORMULA"],),
|
|
},
|
|
"optional": {
|
|
"passthrough":(any_type, {"default": "","multiline": True,"forceInput": True}),
|
|
"aux":(any_type, {"default": "","forceInput": True}),
|
|
}, "hidden": { "unique_id": "UNIQUE_ID" }
|
|
}
|
|
|
|
RETURN_TYPES = (any_type,)
|
|
RETURN_NAMES = ("output",)
|
|
FUNCTION = "data_monitor"
|
|
OUTPUT_NODE = True
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def data_monitor(self,text="",output_type="ANY",passthrough=None,aux=None,unique_id=0):
|
|
|
|
#TODO:
|
|
#Add %prev% for the previous value set, %clear% to empty the text via aux, $mem$ to resolve to the value of memory storage nodes and and %var% to resolve to the value of mape variables/KJNodes get/set/ and anything everywhere nodes
|
|
|
|
def replace_aux(text_value,aux_value):
|
|
|
|
print ("text:", text_value,"aux:",aux_value)
|
|
|
|
if text_value is not None:
|
|
|
|
if aux_value is None:
|
|
aux_value = ""
|
|
print ("text:", text_value,"aux:",aux_value)
|
|
|
|
try:
|
|
s = str(text_value)
|
|
try:
|
|
x = str(aux_value)
|
|
|
|
print ("str_text:", s,"str_aux",x)
|
|
|
|
if s!="" and "%aux%" in s:
|
|
if DEBUG_MODE:
|
|
print ('REPLACING %AUX% in "'+s+'" with "'+x+'"!')
|
|
|
|
return s.replace("%aux%",x)
|
|
except:
|
|
pass
|
|
except:
|
|
pass
|
|
|
|
return text_value
|
|
|
|
#CHECK HOW DISPLAY ANY RENDERS LATENT IMAGE (if type=dict). Currently crashes.
|
|
|
|
if DEBUG_MODE:
|
|
|
|
print ("\n>> DATA MONITOR [",unique_id,"]")
|
|
print( "PASSTHROUGH: ", type(passthrough), repr(passthrough) )
|
|
print( "AUX: ", type(aux), repr(aux), "[",aux,"]" )
|
|
print( "OUTPUT TYPE: ", output_type)
|
|
|
|
if (passthrough is not None):
|
|
|
|
encapsulate = False
|
|
|
|
try:
|
|
iterator = iter(passthrough)
|
|
except TypeError:
|
|
encapsulate = True
|
|
if DEBUG_MODE:
|
|
print ("[!] PASSTHROUGH NOT ITERABLE!")
|
|
else:
|
|
if DEBUG_MODE:
|
|
print ("[!] PASSTHROUGH ITERABLE!")
|
|
try:
|
|
float(passthrough)
|
|
except:
|
|
if DEBUG_MODE:
|
|
print ("[!] PASSTHROUGH NOT NUMERIC!")
|
|
else:
|
|
if DEBUG_MODE:
|
|
print ("[!] PASSTHROUGH NUMERIC!")
|
|
encapsulate = True
|
|
|
|
if DEBUG_MODE:
|
|
print("OUTPUT TYPE: ", output_type)
|
|
|
|
ret = replace_aux(passthrough,aux)
|
|
|
|
if ret == "":
|
|
return text,
|
|
else:
|
|
text = ret
|
|
|
|
if DEBUG_MODE:
|
|
print ( "TEXT:", text )
|
|
|
|
match output_type:
|
|
case "INT":
|
|
text = cint(text)
|
|
case "FLOAT":
|
|
text = float(text)
|
|
case "BOOLEAN":
|
|
text = cbool(text)
|
|
encapsulate=True
|
|
case "STRING":
|
|
text = str(text)
|
|
case "LIST":
|
|
text = list(text)
|
|
case "TUPLE":
|
|
text = tuple(text)
|
|
case "JSON":
|
|
text = json.loads(text),
|
|
case "FORMULA":
|
|
if text != "":
|
|
text = eval(str(text))
|
|
try:
|
|
float(text)
|
|
except ValueError:
|
|
if DEBUG_MODE:
|
|
print ("PASSTHROUGH FORMULA NOT NUMERIC!")
|
|
else:
|
|
if DEBUG_MODE:
|
|
print ("PASSTHROUGH FORMULA NUMERIC!")
|
|
encapsulate = True
|
|
case _:
|
|
text = passthrough
|
|
|
|
if DEBUG_MODE:
|
|
print ("RETURN [PASSTHROUGH]:",type(text), repr(text))
|
|
|
|
if encapsulate:
|
|
return {"ui": {"text": tuple([text]) },"result": tuple([text]) }
|
|
else:
|
|
return {"ui": {"text": text},"result": (text,)}
|
|
else:
|
|
|
|
if DEBUG_MODE:
|
|
print("TEXT: ", type(text), repr(text))
|
|
|
|
match output_type:
|
|
case "INT":
|
|
text = cint(text)
|
|
case "FLOAT":
|
|
text = float(text)
|
|
case "BOOLEAN":
|
|
text = cbool(text)
|
|
case "STRING":
|
|
text = str(text)
|
|
case "LIST":
|
|
text = list(text)
|
|
case "TUPLE":
|
|
text = tuple(text)
|
|
case "JSON":
|
|
text = json.loads(text),
|
|
case "FORMULA":
|
|
if text != "":
|
|
text = eval(str(text))
|
|
|
|
if DEBUG_MODE:
|
|
print ("RETURN [TEXT]:",text)
|
|
|
|
return text,
|
|
|
|
class CheckpointSelector:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
CHECKPOINT_LIST = sorted(folder_paths.get_filename_list("checkpoints"), key=str.lower)
|
|
return {
|
|
"required": {
|
|
"checkpoints": (CHECKPOINT_LIST, ),
|
|
}, "optional": {
|
|
"aux":(any_type, {"default": "","forceInput": True})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (any_type,)
|
|
RETURN_NAMES = ("Checkpoint",)
|
|
FUNCTION = "load_checkpoints"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def load_checkpoints(self, checkpoints,aux=None) -> any_type:
|
|
return (checkpoints,)
|
|
|
|
class LoraSelector:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
LORA_LIST = sorted(folder_paths.get_filename_list("loras"), key=str.lower)
|
|
return {
|
|
"required": {
|
|
"lora_name": (LORA_LIST, ),
|
|
}, "optional": {
|
|
"aux":(any_type, {"default": "","forceInput": True})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (any_type,)
|
|
RETURN_NAMES = ("LoRA",)
|
|
FUNCTION = "load_lora"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def load_lora(self, lora_name,aux=None) -> any_type:
|
|
return (lora_name,)
|
|
|
|
class VAESelector:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
VAE_LIST = sorted(folder_paths.get_filename_list("vae"), key=str.lower) + ["taesd","taesdxl"]
|
|
return {
|
|
"required": {
|
|
"vae": (VAE_LIST, ),
|
|
}, "optional": {
|
|
"aux":(any_type, {"default": "","forceInput": True})
|
|
}
|
|
}
|
|
RETURN_TYPES = (any_type,)
|
|
RETURN_NAMES = ("VAE Name",)
|
|
FUNCTION = "load_vaes"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def load_vaes(self, vae, aux=None) -> any_type:
|
|
return (vae,)
|
|
|
|
class NullInput:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "NULL": (any_type, ), }}
|
|
|
|
@classmethod
|
|
def IS_CHANGED(s):
|
|
return True
|
|
|
|
RETURN_TYPES = ()
|
|
CATEGORY = MAIN_CATEGORY
|
|
FUNCTION = "return_null"
|
|
OUTPUT_NODE = True
|
|
|
|
def return_null(self,NULL):
|
|
return (None,)
|
|
|
|
class NullOutput:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {},
|
|
}
|
|
|
|
@classmethod
|
|
def VALIDATE_INPUTS(s, **kwargs):
|
|
return True
|
|
|
|
|
|
RETURN_TYPES = (any_type,)
|
|
RETURN_NAMES = ('NULL',)
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "return_null"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def return_null(self):
|
|
return (None,)
|
|
|
|
class ImageResolutionAdjust:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
|
|
MAX_RESOLUTION = 8192
|
|
|
|
return {"required": { "source_width": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
|
|
"source_height": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
|
|
"scaling_factor": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10, "step": 0.1}),
|
|
"target_width": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
|
|
"target_height": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
|
|
"tolerance": ("INT", {"default": 32, "min": 16, "max": 1024})
|
|
}
|
|
}
|
|
RETURN_TYPES = ("INT","INT","STRING")
|
|
RETURN_NAMES = ("Adj. Width","Adj. Height","Result")
|
|
FUNCTION = "adjust_res"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def adjust_res (self, source_width: int, source_height: int,scaling_factor: float, target_width: int, target_height: int,tolerance:int):
|
|
|
|
if DEBUG_MODE:
|
|
print("W: ",source_width,"H: ",source_height,"DW: ",target_width,"DH: ",target_height,"T: ",tolerance)
|
|
|
|
new_res = self.calc_imageres(source_width,source_height,scaling_factor,target_width,target_height,tolerance)
|
|
|
|
match new_res[2]:
|
|
case 0:
|
|
adjust_ret = "TARGET RESOLUTION MAINTAINED."
|
|
case 1:
|
|
adjust_ret = "RESOLUTION ADJUSTED TO FIT ASPECT RATIO."
|
|
case 2:
|
|
adjust_ret = "RESOLUTION ADJUSTED WITH SCALING FACTOR: " + f'{scaling_factor:.2f}' + "."
|
|
|
|
return ( new_res[0],new_res[1],"Original Resolution: " + str(source_width) + " x " + str(source_height) + " = " + f'{source_width*source_height:,}' + ' px. \n' + "Adjusted Resolution: " + str(new_res[0]) + " x " + str(new_res[1]) + " = " + f'{new_res[0] * new_res[1]:,}' + ' px. \n' + adjust_ret ,)
|
|
|
|
def calc_imageres(self, width, height, scaling_factor,target_width,target_height,tolerance=32):
|
|
|
|
if (target_width == target_height) or (width != target_width and height != target_height):
|
|
if scaling_factor == 1.0:
|
|
if DEBUG_MODE:
|
|
print ("Maintaining Target Resolution")
|
|
return (target_width,target_height,0)
|
|
else:
|
|
if DEBUG_MODE:
|
|
print("Resolution Override - Using Scaling Factor")
|
|
|
|
w_diff,h_diff = 1,1
|
|
|
|
if DEBUG_MODE:
|
|
print("Src W: ", width,"Src H:",height,"Target W:",target_width,"Target H:",target_height)
|
|
print("Src S: ", width*height,"Target S:",target_width*target_height)
|
|
print("W diff: ", w_diff,"H diff: ",h_diff)
|
|
|
|
if scaling_factor == 1:
|
|
w_diff = width / target_width
|
|
h_diff = target_height / height
|
|
else:
|
|
w_diff,h_diff = 1/scaling_factor,1/scaling_factor
|
|
|
|
if DEBUG_MODE:
|
|
print("W diff: ", w_diff,"H diff: ",h_diff)
|
|
|
|
if h_diff != 1:
|
|
if DEBUG_MODE:
|
|
print(">> Adjusted Width")
|
|
target_width = self.round_to_multiple (target_width/h_diff,tolerance)
|
|
|
|
if w_diff != 1:
|
|
if DEBUG_MODE:
|
|
print(">> Adjusted Height")
|
|
target_height = self.round_to_multiple (target_height*w_diff,tolerance)
|
|
|
|
if DEBUG_MODE:
|
|
print("New W:",target_width,"New H:",target_height,"New S:",target_width*target_height)
|
|
|
|
return (target_width,target_height, 1 if scaling_factor==1 else 2)
|
|
|
|
def round_to_multiple(self, number, multiple, direction='nearest'):
|
|
if direction == 'nearest':
|
|
return multiple * round(number / multiple)
|
|
elif direction == 'up':
|
|
return multiple * ceil(number / multiple)
|
|
elif direction == 'down':
|
|
return multiple * floor(number / multiple)
|
|
else:
|
|
return multiple * round(number / multiple)
|
|
|
|
class ReadTextFile():
|
|
|
|
@classmethod
|
|
def IS_CHANGED(self, **kwargs):
|
|
return os.path.getmtime(kwargs['file'])
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"file": ("STRING", {"default": "file.txt"})
|
|
}
|
|
}
|
|
|
|
FUNCTION = "load_text"
|
|
RETURN_TYPES = ("STRING",)
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def load_text(self, file):
|
|
|
|
if file is not None:
|
|
if os.path.isfile(file):
|
|
with open(file, "r") as f:
|
|
return (f.read(), )
|
|
|
|
return ("",)
|
|
|
|
class SaveTextFile():
|
|
|
|
@classmethod
|
|
def IS_CHANGED(self, **kwargs):
|
|
return float("nan")
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"file": ("STRING", {"default": "file.txt"}),
|
|
"append": (["append", "overwrite", "new only"], {}),
|
|
"insert": ("BOOLEAN", {
|
|
"default": True, "label_on": "new line", "label_off": "none",
|
|
"vykosx.binding": [{
|
|
"source": "append",
|
|
"callback": [{
|
|
"type": "if",
|
|
"condition": [{
|
|
"left": "$source.value",
|
|
"op": "eq",
|
|
"right": '"append"'
|
|
}],
|
|
"true": [{
|
|
"type": "set",
|
|
"target": "$this.disabled",
|
|
"value": False
|
|
}],
|
|
"false": [{
|
|
"type": "set",
|
|
"target": "$this.disabled",
|
|
"value": True
|
|
}],
|
|
}]
|
|
}]
|
|
}),
|
|
"text": ("STRING", {"multiline": True})
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
CATEGORY = MAIN_CATEGORY
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "write_text"
|
|
|
|
def write_text(self, file, append, insert, text):
|
|
|
|
if append == "new only" and os.path.exists(file):
|
|
raise Exception("File already exists and 'new only' is selected.")
|
|
Return (False,)
|
|
|
|
with open(file, "a+" if append == "append" else "w") as f:
|
|
|
|
is_append = f.tell() != 0
|
|
|
|
if is_append and insert:
|
|
f.write("\n")
|
|
|
|
f.write(text)
|
|
|
|
return (True,)
|
|
|
|
return (False,)
|
|
|
|
|
|
class MemoryStorage:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"Input": (any_type,),
|
|
"Name": ("STRING", { "forceInput": False } ),
|
|
}, "optional": {
|
|
"Reset": ("BOOLEAN", { "forceInput": False } ),
|
|
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (any_type,)
|
|
RETURN_NAMES = ("Output",)
|
|
FUNCTION = "store"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
Data = None
|
|
|
|
def store(s, Input, Name, Reset):
|
|
|
|
if DEBUG_MODE:
|
|
print (">> MEMORY STORAGE [",Name,"]: CURRENT = ",s.Data,"NEW = ",Input,"RESET = ",Reset)
|
|
|
|
if Reset:
|
|
s.Data = None
|
|
|
|
elif Input is not None:
|
|
s.Data = Input
|
|
|
|
return (s.Data,)
|
|
|
|
class DelayExecution:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"Delay": ('FLOAT', {"default": 5.0, "forceInput": False } ),
|
|
},
|
|
"optional": {
|
|
"Input": (any_type,),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (any_type,)
|
|
RETURN_NAMES = ("Output",)
|
|
FUNCTION = "wait"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def wait(self, Delay, Input=[""]):
|
|
|
|
if DEBUG_MODE:
|
|
print ("Sleeping for ", Delay, " seconds!")
|
|
time.sleep(Delay)
|
|
|
|
if DEBUG_MODE:
|
|
print ("Zzzz... Ah there we go, much better!")
|
|
|
|
return (Input,)
|
|
|
|
class UnloadModels:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"Passthrough": (any_type,),
|
|
"ForceUnload": ("BOOLEAN", { "default": False, "forceInput": False, "label_on": "Unload Models Immediately", "label_off": "Request Model Unloading" } ),
|
|
"PurgeTensors": ("BOOLEAN", { "default": False, "forceInput": False, "label_on": "Force Purging Tensors (May Cause Issues)", "label_off": "Disabled"} )
|
|
},
|
|
"hidden": {
|
|
"node_id": "UNIQUE_ID"
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = (any_type,)
|
|
RETURN_NAMES = ("Output",)
|
|
FUNCTION = "unload"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def unload(self, Passthrough, ForceUnload, PurgeTensors, node_id):
|
|
|
|
if ForceUnload:
|
|
|
|
if DEBUG_MODE:
|
|
print (">> FORCE UNLOADING MODELS...")
|
|
|
|
mem_manager.unload_all_models()
|
|
mem_manager.soft_empty_cache()
|
|
|
|
else:
|
|
|
|
if DEBUG_MODE:
|
|
print (">> REQUESTING MODEL UNLOADING...")
|
|
|
|
PromptServer.instance.send_sync("VykosX.UnloadModels", {"node": node_id})
|
|
|
|
if PurgeTensors:
|
|
|
|
if DEBUG_MODE:
|
|
print (">> ATTEMPTING TO PURGE ALL TENSOR OBJECTS FROM VRAM. BRACE YOURSELF AND HOPE FOR THE BEST.")
|
|
|
|
self.clear_tensors()
|
|
clear = torch.empty((0,1))
|
|
torch.Tensor.set_(clear)
|
|
|
|
return (Passthrough,)
|
|
|
|
#This attempts to find all tensor objects in memory and clear them. Very unsafe, mostly here for testing reasons.
|
|
def clear_tensors(self, only_cuda=False, omit_objs=[]):
|
|
|
|
add_all_tensors = False if only_cuda is True else True
|
|
# To avoid counting the same tensor twice, create a dictionary of tensors,
|
|
# each one identified by its id (the in memory address).
|
|
tensors = {}
|
|
i = 0
|
|
|
|
# omit_obj_ids = [id(obj) for obj in omit_objs]
|
|
|
|
def add_tensor(obj):
|
|
if torch.is_tensor(obj):
|
|
tensor = obj
|
|
elif hasattr(obj, 'data') and torch.is_tensor(obj.data):
|
|
tensor = obj.data
|
|
else:
|
|
return
|
|
|
|
if (only_cuda and tensor.is_cuda) or add_all_tensors:
|
|
tensors[id(tensor)] = tensor
|
|
|
|
for obj in gc.get_objects():
|
|
try:
|
|
# Add the obj if it is a tensor.
|
|
add_tensor(obj)
|
|
# Some tensors are "saved & hidden" for the backward pass.
|
|
if hasattr(obj, 'saved_tensors') and (id(obj) not in omit_objs):
|
|
for tensor_obj in obj.saved_tensors:
|
|
add_tensor(tensor_obj)
|
|
i+=1
|
|
if DEBUG_MODE:
|
|
print (">> TENSORS FOUND: ",i)
|
|
|
|
except Exception as ex:
|
|
pass
|
|
#print(">> EXCEPTION: ", ex)
|
|
# logger.debug(f"Exception: {str(ex)}")
|
|
|
|
if DEBUG_MODE:
|
|
print (">> [" , i , "] TENSOR OBJECTS FOUND.")
|
|
|
|
i = 0
|
|
|
|
for x in tensors.values():
|
|
|
|
i+=1
|
|
|
|
if DEBUG_MODE:
|
|
print (">> Tensor '", i, "': ", type(x))
|
|
try:
|
|
print (x.type(), x.nbytes,x.itemsize)
|
|
except:
|
|
print (x.nbytes,x.itemsize)
|
|
pass
|
|
|
|
if x.is_cuda:
|
|
|
|
if DEBUG_MODE:
|
|
print (">> Dettaching Tensor '",i, "'.")
|
|
|
|
x.detach()
|
|
x.grad = None
|
|
x.resize_(0)
|
|
|
|
return #tensors.values() # return a list of detected tensor
|
|
|
|
class GarbageCollector:
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"Input": (any_type,),
|
|
"RAM": ("BOOLEAN", { "default": True, "label_on": "Free Unused Memory (Windows only)", "label_off": "Free Unused Memory (Windows only)"} ),
|
|
"Mode": ("BOOLEAN", { "default": False, "label_on": "Run Async. (Requires Admin)", "label_off": "Wait until Complete"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (any_type,)
|
|
RETURN_NAMES = ("Output",)
|
|
FUNCTION = "garbage_collect"
|
|
CATEGORY = MAIN_CATEGORY
|
|
|
|
def garbage_collect(self, Input, RAM, Mode):
|
|
|
|
if DEBUG_MODE:
|
|
print(">> TRIGGERING GARBAGE COLLECTION.")
|
|
|
|
gc.collect()
|
|
#mem_manager.soft_empty_cache()
|
|
torch.cuda.empty_cache()
|
|
torch.cuda.ipc_collect()
|
|
|
|
if RAM:
|
|
|
|
process = "WinMemoryCleaner.exe"
|
|
cmd_line = "/ModifiedPageList /ProcessesWorkingSet /StandbyList /SystemWorkingSet"
|
|
|
|
if os.path.isfile(process):
|
|
|
|
if Mode:
|
|
|
|
print (">> ATTEMPTING TO CLEAN THE MEMORY WITHOUT INTERRUPTING WORKFLOW EXECUTION...")
|
|
|
|
try:
|
|
pid = os.spawnl(os.P_NOWAIT , process, cmd_line)
|
|
if pid != 0:
|
|
print (">> SYSTEM MEMORY CLEANING REQUEST SUCCESSFUL!")
|
|
return (Input,)
|
|
except:
|
|
pass
|
|
print (">> UNABLE TO FREE MEMORY ASYNCHRONOUSLY (MAKE SURE TO RUN WITH ADMIN PRIVILEGES)")
|
|
|
|
print (">> ATTEMPTING TO CLEAN THE MEMORY IN BLOCKING MODE. EXECUTION WILL RESUME ONCE RAM HAS BEEN FREED.")
|
|
|
|
try:
|
|
os.system(process + " " + cmd_line)
|
|
print (">> SYSTEM MEMORY HAS BEEN CLEANED SUCCESSFULLY!")
|
|
return (Input,)
|
|
except:
|
|
pass
|
|
|
|
print (">> MEMORY CLEANER PROCESS NOT FOUND OR CANNOT BE EXECUTED. ENSURE 'WinMemoryCleaner.exe' IS PRESENT IN COMFYUI ROOT FOLDER.")
|
|
|
|
return (Input,) |