Repository navigation
Expand file tree
/
Copy pathControlFlowUtils.py
More file actions
2870 lines (2078 loc) · 119 KB
/
Copy pathControlFlowUtils.py
File metadata and controls
2870 lines (2078 loc) · 119 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
@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.6.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
#Default Python StdLib Imports
import json
import os
import gc
import time
import re
from datetime import datetime
from random import randrange as rnd
#ComfyUI Related Imports
import torch
import nodes
import folder_paths
import comfy.utils
import comfy.model_management as mem_manager
from server import PromptServer
try:
from comfy_execution.graph_utils import GraphBuilder, is_link
from comfy_execution.graph import ExecutionBlocker
except:
ExecutionBlocker = None
GraphBuilder = None
#from nodes import PreviewImage
#Custom Imports
from .Types import *
#from . import Types
from .Helpers import *
any_type = AnyType("*") #Types.AnyType("*")
reverse = slice(None,None,-1)
MAX_SLOTS = 6 #Adjust this value to change how many inputs/outputs the dynamic nodes in this pack expose.
MAIN_CATEGORY = "🐺 VykosX-ControlFlowUtils"
VYKOSX_STORAGE_DATA = {}
#CUSTOM NODES
#*************
'''
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,"defaultInput": False,"tooltip": "The initial value of the loop counter"}),
"step": ("INT", {"default": 1,"defaultInput": False,"tooltip": "How much the loop counter gets increased or decreased by on each iteration"}),
"end": ("INT", {"default": 10,"defaultInput": False,"tooltip": "The value at which the loop is considered to be finished"})
},"optional": {
"manual_reset": ("BOOLEAN", {"defaultInput": False,"tooltip": "Enable to force a 'DRY RUN' and reset the Cycle to its first iteration"}),
"auto_reset": ("BOOLEAN", {"defaultInput": False, "default": True, "label_on": "Auto Reset on Cycle End", "label_off": "Keep Cycling until Manual Stop", "tooltip": "Enable to have the loop counter automatically reset to Start once it reaches End"})
},"hidden": { "unique_id": "UNIQUE_ID" },
}
RETURN_TYPES = ("CYCLE","BOOLEAN",)
RETURN_NAMES = ("CYCLE","RESET?",)
OUTPUT_TOOLTIPS = ("Connect to the CYCLE input of a [CycleContinue] node to establish a loop","Specifies whether the Cycle is executing its 'DRY RUN' iteration",)
FUNCTION = "run"
CATEGORY = MAIN_CATEGORY
DESCRIPTION = \
"""Begins a Cycle, a kind of Loop that gets iterated each time a prompt is executed. You can use Cycles to send information forwards into subsequent prompts, allowing for workflows that process data in an iterative manner.
Connect the CYCLE Output of this node to a [Cycle Continue](#Cycle_Continue)'s CYCLE Input to begin the loop.
The first iteration of the loop is called a 'DRY RUN', Index, Data and all Aux_Data Inputs will be set to None. Additionally the "Reset?" output will
return True so you can test this with an [IF Selector](#IF_Condition_Selector) node to determine whether you are in the first execution of the loop. You can also trigger a manual reset to the fist 'DRY RUN' iteration by toggling the Manual_Reset Input. Use the 'DRY RUN' to reset any variables or the state of your workflow whenever a fresh Cycle has started.
Set your Start, End and Step parameters to the values you would like to iterate from, to and by, respectively. If you'd like the loop to return automatically to the original Start value, enable the Auto_Reset Input, otherwise the cycle will continue to apply Step to Index on each iteration.
HOVER OVER THE INPUTS AND OUTPUTS FOR MORE INFO.
"""
def run(self,start,step,end,manual_reset,auto_reset,unique_id):
debug_print ("\n>> CYCLE [",unique_id,"] INIT!")
if manual_reset or self.state['finish']:
if manual_reset or auto_reset:
debug_print (">> CYCLE [",unique_id,"] 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, unique_id):
if manual_reset:
self.state = {'index': start,'step':step,'end': end, 'start': start, 'finish': False, 'auto_reset': auto_reset}
return float("NaN")
return False
class CycleContinue:
max_slots = MAX_SLOTS
@classmethod
def INPUT_TYPES(s):
inputs = {
"required": {
"CYCLE": ("CYCLE", {"tooltip": "Connect to the CYCLE output of a [Cycle] node to establish a loop"}),
"data": (any_type, {"tooltip": "The initial value of any data you wish to iterate on and propagate to subsequent prompts"}),
}, "optional": {
"index_override": ("INT", {"defaultInput": True, "tooltip": "Manually override the current index of the Cycle.\nYou can use this input to have complex Step calculations or to reset the loop manually"}),
"aux": (any_type, {"tooltip": "Any optional Auxiliary data you wish to iterate on and propagate to subsequent prompts"}),
}, "hidden": { "unique_id": "UNIQUE_ID" }
}
for i in range(2,s.max_slots): inputs["optional"]["aux%d" % i] = (any_type,{"tooltip": "The initial value of any Auxiliary data you wish to iterate on and propagate to subsequent prompts"})
return inputs
RETURN_TYPES = tuple (["INT"] + [any_type for x in range(max_slots)] )
RETURN_NAMES = tuple(["index","data","aux"]+ ["aux"+str(x) for x in range(2,max_slots)])
OUTPUT_TOOLTIPS = tuple(["The Index of the loop counter before being incremented or decremented by Step","Connect to the Data input of a [CycleEnd] node after processing"]+["Connect to the respective Aux input of a [CycleEnd] node after processing"] * (max_slots-1))
FUNCTION = "run"
CATEGORY = MAIN_CATEGORY
DESCRIPTION = \
"""Continues a Cycle, a kind of Loop that gets iterated each time a prompt
is executed. You can use Cycles to send information forwards into subsequent
prompts, allowing for workflows that process data in an iterative manner.
Connect the Data and Aux outputs of this node to a [Cycle End]'s Data
and Aux inputs to continue the loop, ideally after processing them.
The Index output specifies the current value of the loop counter before
Step is applied.
You can connect any outputs to this node's Data or Aux_Data inputs to
propagate that data into subsequent prompt executions. This allows you
to process data iteratively by queuing multiple prompts at a time or even
setting the execution to Instant which will queue up prompts automatically.
Execution can then be interrupted via the [HaltExecution] node on demand
once your Cycle has finished or for any other reason you may have.
HOVER OVER THE INPUTS AND OUTPUTS FOR MORE INFO.
"""
def run(self, CYCLE, index_override=None, **kwargs):
if index_override is not None:
CYCLE['index'] = index_override
if 'index' not in CYCLE:
raise Exception("\nCYCLE [",kwargs['unique_id'],"] NOT PROPERLY CLOSED. PLEASE CONNECT THE CYCLE NODES CORRECTLY!")
return tuple([None] * (self.max_slots+1))
if CYCLE['index'] is None:
debug_print ("\n>> CYCLE [",kwargs['unique_id'],"] START - DRY RUN")
return tuple([None] * (self.max_slots+1))
else:
debug_print ("\n>> CYCLE [",kwargs['unique_id'],"] CYCLE=",CYCLE)
if 'next' in CYCLE:
ret = [CYCLE.get('index',None), CYCLE.get('next',None),CYCLE.get('aux',None),]
for x in range(2, self.max_slots): ret.append(CYCLE.get('aux'+str(x),None))
#debug_print ("CYCLE NEXT=",ret)
else:
ret = [CYCLE.get('index',None), kwargs.get('data',None), kwargs.get('aux',None),]
for x in range(2, self.max_slots): ret.append(kwargs.get('aux'+str(x),None))
#debug_print ("CYCLE FIRST=",ret)
return tuple(ret)
@classmethod
def IS_CHANGED(s, CYCLE, **kwargs):
#if 'next' in CYCLE:
#return id(CYCLE['next'])
return float("NaN")
class CycleEnd:
max_slots = MAX_SLOTS
@classmethod
def INPUT_TYPES(s):
inputs = {
"required": {
"CYCLE": ("CYCLE", {"tooltip": "Connect to the CYCLE output of a [Cycle] node to establish a loop"}),
"data": (any_type, {"tooltip": "Any data you wish to iterate on and propagate to subsequent prompts"}),
}, "optional": {
"aux": (any_type,{"tooltip": "Any optional Auxiliary data you wish to iterate on and propagate to subsequent prompts"}),
}, "hidden": { "unique_id": "UNIQUE_ID" }
}
for i in range(2,s.max_slots): inputs["optional"]["aux%d" % i] = (any_type,{"tooltip": "Any optional Auxiliary data you wish to iterate on and propagate to subsequent prompts"})
return inputs
RETURN_TYPES = ("INT","BOOLEAN",)
RETURN_NAMES = ("index","FINISHED?",)
OUTPUT_TOOLTIPS = ("The Index of the loop counter after being incremented or decremented by Step","Boolean value that specifies whether the cycle has terminated",)
FUNCTION = "run"
CATEGORY = MAIN_CATEGORY
OUTPUT_NODE = True
DESCRIPTION = \
"""Ends a Cycle, a kind of Loop that gets iterated each time a prompt is
executed. You can use Cycles to send information forwards into subsequent
prompts, allowing for workflows that process data in an iterative manner.
Connect the CYCLE input of this node to a [Cycle]'s CYCLE output node to
establish a loop. Information from Data and Aux Inputs will flow through the
CYCLE line and be cached to be retrieved upon the next loop iteration.
The Index output specifies the current value of the loop counter after Step
is applied. The Finished output returns True if the loop counter has reached
or surpassed the value specified by End.
HOVER OVER THE INPUTS AND OUTPUTS FOR MORE INFO.
"""
def run(self, CYCLE, **kwargs):
#debug_print ("CYCLE END, CYCLE=",CYCLE,"KWARGS=",kwargs)
if CYCLE['index'] is None:
CYCLE['index'] = CYCLE['start']
debug_print (">> CYCLE [",kwargs['unique_id'],"] END - DRY RUN")
return (None,None,)
debug_print (">> CYCLE [",kwargs['unique_id'],"] ITERATION '", CYCLE['index'],"'!")
CYCLE['next'] = kwargs.get('data',None)
CYCLE['aux'] = kwargs.get('aux',None)
for x in range(2,self.max_slots): CYCLE['aux'+str(x)] = kwargs.get('aux'+str(x),None)
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']:
debug_print (">> CYCLE [",kwargs['unique_id'],"] AUTO-RESET!")
CYCLE['index'] = CYCLE['start']
#debug_print ("CYCLE END FINISH, CYCLE=",CYCLE)
return (CYCLE['index'],CYCLE['finished'])
@classmethod
def IS_CHANGED(s, CYCLE, **kwargs):
ret = (not CYCLE['finished'])
debug_print ("CYCLE [",kwargs['unique_id'],"] CHANGE:",ret)
return ret
class LoopOpen:
max_slots = MAX_SLOTS
@classmethod
def INPUT_TYPES(s):
inputs = {
"required": {
"start": ("INT", {"default": 1,"defaultInput": False,"tooltip": "The initial value of the loop counter"}),
"step": ("INT", {"default": 1,"defaultInput": False,"tooltip": "How much the loop counter gets increased or decreased by on each iteration"}),
"end": ("INT", {"default": 10,"defaultInput": False, "tooltip": "The value at which the loop should stop"}),
"condition": ("STRING", {"default" : "True", "defaultInput": False, "tooltip": "The loop will be executed only if the specified condition evaluates to True"}),
},
"optional": {
"index_override": (any_type, {"forceInput": True, "tooltip": "Manually override the current index of the Cycle.\nYou can use this input to have complex Step calculations or to reset the loop manually"}),
"data": (any_type, {"forceInput": True, "tooltip": "Any data you wish to iterate on in the loop"}),
"aux": (any_type, {"forceInput": True, "tooltip": "Any auxilliary data you wish to iterate on in the loop"}),
},"hidden": {
"unique_id": "UNIQUE_ID",
"original_id": "INT",
}
}
for i in range(2,s.max_slots): inputs["optional"]["aux%d" % i] = (any_type, {"forceInput": True, "tooltip": "Any auxilliary data you wish to iterate on in the loop"})
return inputs
FUNCTION = "loop"
CATEGORY = MAIN_CATEGORY
RETURN_TYPES = tuple ( any_type * (max_slots+1) )
RETURN_NAMES = tuple ( ["LOOP","data","aux"] + ["aux%d" % x for x in range(2, max_slots)] )
OUTPUT_TOOLTIPS = tuple ( [ "Dictionary with information about the Loop. Connect to the LOOP input of a [Loop Close] node"] + ["Data to iterate on in the loop. Connect to the relevant Input of a [Loop Close] node to preserve any changes between iterations"] * max_slots)
DESCRIPTION = \
"""Loops allow you to automate entire sections of your workflow by repeating the nodes connected to the Loop a specific number of times or until a specific condition no longer applies.
To use this node, you must link the Loop Output of the [Loop Open] node to the Loop Input of a [Loop Close] node. If you want to iterate a predefined amount of times (such as when using a For Loop) then set your Start, Step and End parameters accordingly. If you would like the Loop to behave as a While Loop instead, then you may set a Condition which must evaluate to True in order for the loop to execute, and it will continue executing indefinitely until the condition no longer evaluates to True. In order to use the Loop as a pure While Loop, you should set Step to 0. You can also have a combined Loop of both For and While types by having both a Condition and a Step set such that the Loop will continue to execute until the Codition is no longer true OR it fully iterates from Start to End, whichever happens first.
You can set the Data and Aux Data Inputs to any Data you wish to iterate on for each execution of the Loop. The data will be preserved so long as you link the Outputs from [Loop Open] node to the relevant Inputs of the [Loop Close] nodes. The LOOP Input is a dictionary containing information about the Loop. It may be intercepted and accessed via [Data Monitor] nodes to extract information about the current Loop, such as the Index, Start, Step, End, Finished status, Condition, and other useful information.
You can use the Index_Override Input to manually force the loop to skip to a specific index prematurely. If Condition does not evaluate to True for the [Loop Open] node, Execution will be blocked and any nodes following [Loop Open] will not execute.
This node uses the new Execution Inversion mechanics and requires an updated version of ComfyUI. It works by dynamically cloning all the nodes that are connected as children of the [Loop Open] and [Loop Close] nodes, while forwarding the data to the copies on each iteration. This is still a bit of proof of concept, so issues may occur. Please report any bugs on the Issues page of this node pack's Github repo!
HOVER OVER THE INPUTS AND OUTPUTS FOR MORE INFO.
"""
def loop(self, start, step, end, condition, **kwargs):
if condition=="": condition = True
index = kwargs.get("index_override", start)
if index is None: index = start
original_id = kwargs.get("original_id", None)
if original_id is None:
original_id = kwargs['unique_id']
debug_print ("\n>> LOOP [",original_id,"] START!")
else:
debug_print ("\n>> LOOP [",original_id,"] ITERATION:",index)
finished = (( end - index ) <= 0) if step >= 0 else (( end - index ) >= 0)
loop_status = {"id":original_id,"start":start,"end":end,"step":step,"index":index,"finished":finished,"last_id":kwargs['unique_id']}
Vars = loop_status
for key, value in VYKOSX_STORAGE_DATA.items(): Vars[key] = value
for key, value in kwargs.items():
if key!="unique_id" or key!="original_id": Vars[key] = value
debug_print ("VARS=",Vars)
condition = safe_eval(condition,Vars)
if condition:
loop_status['condition_open'] = condition
return tuple( [loop_status] + [kwargs.get('data',None), kwargs.get('aux',None)] + [kwargs.get("aux%d" % x, None) for x in range(2,self.max_slots+1) ],) #debug_print ("LOOP RET=",ret)
else:
debug_print ("LOOP [",original_id,"] CONDITION FAILED -- BLOCKING EXECUTION!")
loop_status['finished'] = True
if ExecutionBlocker is None:
raise Exception("Unable to block execution, please update your ComfyUI to enable this functionality!")
else:
return tuple( [ExecutionBlocker(None) for x in range(0,self.max_slots+1)] )
class LoopClose:
def __init__(self):
pass
max_slots = MAX_SLOTS+1
@classmethod
def INPUT_TYPES(s):
inputs = {
"required": {
"LOOP": (any_type, {"forceInput": True,"tooltip": "Connect to the LOOP output of a [Loop Open] node to establish a Loop"}),
"condition": ("STRING", {"default" : "True", "defaultInput": False, "tooltip": "The loop will be executed only if the specified condition evaluates to True"}),
},
"optional": {
"data": (any_type, {"forceInput": True, "tooltip": "Any data you wish to iterate on in the loop"}),
"aux": (any_type, {"forceInput": True, "tooltip": "Any auxilliary data you wish to iterate on in the loop"}),
},
"hidden": {
"dynprompt": "DYNPROMPT",
"unique_id": "UNIQUE_ID",
}
}
for i in range(2,s.max_slots-1): inputs["optional"]["aux%d" % i] = (any_type, {"forceInput": True, "tooltip": "Any auxilliary data you wish to iterate on in the loop"})
return inputs
CATEGORY = MAIN_CATEGORY
FUNCTION = "loop"
RETURN_TYPES = tuple( ["BOOLEAN"]+[any_type for x in range(max_slots-1) ] )
RETURN_NAMES = tuple ( ["FINISHED?","data","aux"] + ["aux%d" % x for x in range(2, max_slots)] )
OUTPUT_TOOLTIPS = tuple ( [ "This output will return True when the Loop is done executing"] + ["Final values for any Data that has been processed by the Loop"] * max_slots)
DESCRIPTION = \
"""Loops allow you to automate entire sections of your workflow by repeating the nodes connected to the Loop a specific number of times or until a specific condition no longer applies.
To use this node, you must link the Loop Output of the [Loop Open] node to the Loop Input of a [Loop Close] node. If you wish to iterate a predefined amount of times (such as when using a For Loop) then set your Start, Step and End parameters accordingly. If you would like the Loop to behave as a While Loop instead, then you may set a Condition which must evaluate to True in order for the loop to execute, and it will continue executing indefinitely until the condition no longer evaluates to True. In order to use the Loop as a pure While Loop, you should set Step to 0. You can also have a combined Loop of both For and While types by having both a Condition and a Step set such that the Loop will continue to execute until the Codition is no longer true OR it fully iterates from Start to End, whichever happens first.
You can set the Data and Aux Data Inputs to any Data you wish to iterate on for each execution of the Loop. The data will be preserved so long as you link the Outputs from [Loop Open] node to the relevant Inputs of the [Loop Close] nodes. The LOOP Input is a dictionary containing information about the Loop. It may be intercepted and accessed via [Data Monitor] nodes to extract information about the current Loop, such as the Index, Start, Step, End, Finished status, Condition, and other useful information.
Once the Loop is done executing, the FINISHED? Output will return True. If Condition does not evaluate to True for the [Loop Close] node, the loop will terminate and no further iterations will be processed. The final values of any data that has been processed by the Loop can be accessed via the Data and Aux Data Outputs of this node, provided they have been passed to the relevant Inputs.
This node uses the new Execution Inversion mechanics and requires an updated version of ComfyUI. It works by dynamically cloning all the nodes that are connected as children of the [Loop Open] and [Loop Close] nodes, while forwarding the data to the copies on each iteration. This is still a bit of proof of concept, so issues may occur. Please report any bugs on the Issues page of this node pack's Github repo!
HOVER OVER THE INPUTS AND OUTPUTS FOR MORE INFO.
"""
def find_subnodes(self, node_id, dynprompt, node_data):
node_info = dynprompt.get_node(node_id)
if "inputs" not in node_info:
return
for key, value in node_info["inputs"].items():
if is_link(value):
parent_id = value[0]
if parent_id not in node_data:
node_data[parent_id] = []
self.find_subnodes(parent_id, dynprompt, node_data)
node_data[parent_id].append(node_id)
def get_subnodes(self, node_id, node_data, subnodes):
if node_id not in node_data:
return
for child_id in node_data[node_id]:
if child_id not in subnodes:
subnodes[child_id] = True
self.get_subnodes(child_id, node_data, subnodes)
def loop(self, LOOP, condition, dynprompt=None, unique_id=None, **kwargs):
if condition=="": condition = True
debug_print ("\n>> LOOP [",LOOP.get('id',0),"] CLOSE")
Vars = LOOP; Vars['condition_close'] = condition
for key, value in VYKOSX_STORAGE_DATA.items(): Vars[key] = value
for key, value in kwargs.items(): Vars[key] = value
if LOOP['finished'] or not safe_eval(condition,Vars):
values = [True, kwargs.get('data',None) , kwargs.get('aux',None)]
for i in range(2, self.max_slots):
values.append(kwargs.get("aux%d" % i, None))
debug_print ("\nLOOP [",LOOP.get('id',0),"] FINISHED!")
return tuple(values)
else:
this_node = dynprompt.get_node(unique_id)
self.find_subnodes(unique_id, dynprompt, node_data:={}) # Get list of all nodes connected to the loop
self.get_subnodes(open_node:=LOOP['last_id'], node_data, subnodes:={}) #Find only the nodes that are within both Open and Close Loop
subnodes[unique_id] = True; subnodes[open_node] = True
graph = GraphBuilder()
if graph is None:
raise Exception("Unable to create Loop system, this ComfyUI version is too old to support it.\nPlease update your ComfyUI to be able to utilize Loops!")
for node_id in subnodes:
original_node = dynprompt.get_node(node_id)
node = graph.node(original_node["class_type"], "R" if node_id == unique_id else node_id)
node.set_override_display_id(node_id)
for node_id in subnodes: #Iterate over each of our subnodes
original_node = dynprompt.get_node(node_id)
node = graph.lookup_node("R" if node_id == unique_id else node_id)
for key, value in original_node["inputs"].items(): #Recreate the inputs for all the subnodes
if is_link(value) and value[0] in subnodes:
parent = graph.lookup_node(value[0])
node.set_input(key, parent.out(value[1]))
else:
node.set_input(key, value)
#Recreate the inputs of our new Loop Open node
new_open = graph.lookup_node(open_node)
new_open.set_input("data", kwargs.get("data", None))
new_open.set_input("aux", kwargs.get("aux", None))
new_open.set_input("index_override", LOOP['index']+LOOP['step'] )
new_open.set_input("original_id", LOOP['id'] )
for i in range(2, self.max_slots-1): new_open.set_input(key:="aux%d" % i, kwargs.get(key, None))
new_subnode_graph = graph.lookup_node("R")
result = [ new_subnode_graph.out(x) for x in range(self.max_slots) ] #result = map(lambda x: new_subnode_graph.out(x), range(self.max_slots))
return { "result": tuple(result), "expand": graph.finalize(), }
class UniversalSwitch:
#TODO: Make input and output amounts dynamic and have a working setting for type validation.
max_slots = MAX_SLOTS
default_to_first = True #Constant which determines whether to output on the first slot even if no valid connections are found.
@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,"tooltip":"Data to forward through the Universal Switch"})
return {
"required": { "input1": (any_type, {"lazy": True,"tooltip": "Data to forward through the Universal Switch"}),
"mode": (["SWITCH","PASSTHROUGH","CYCLE","SORT","REVERSE"],{"tooltip": "Mode that will be used to select Inputs and Outputs"}),
"selection_in": ("INT", {"default": 1, "min": -1, "max": s.max_slots, "step": 1, "defaultInput": False, "tooltip":"Which Input slot to select from (0=Auto,-1=All)"}),
"selection_out": ("INT", {"default": 1, "min": -1, "max": s.max_slots, "step": 1, "defaultInput": False, "tooltip":"Which Output slot to transmit to (0=Auto,-1=All)"}),
"validate_typing": ("BOOLEAN", {"defaultInput": False,"tooltip":"[UNIMPLEMENTED] Match Input types to their relative Outputs"}),
}, "optional": dynamic_inputs, "hidden": {"prompt": "PROMPT", "unique_id": "UNIQUE_ID",}, #"extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = tuple([any_type for x in range(1,max_slots+1)])
RETURN_NAMES = tuple(list("*" * max_slots))
OUTPUT_TOOLTIPS = tuple(["Output "+ str(x) for x in range(1,max_slots+1)],)
FUNCTION = "switch"
CATEGORY = MAIN_CATEGORY
DESCRIPTION = \
"""Allows you to pick a specific Input or Output to control the flow of execution
and transmit data variably based on your selections.
Selection_In determines what Input slot will be transmitted and Selection_Out
specifies what slot that data will be output to. All other Outputs from this
node that are not used will return None. This node supports Lazy evaluation.
Only the Inputs that are required based on the selections will be executed.
Depending on the mode this node is set to, different actions will occur:
• SWITCH: Transmits the Input chosen by Selection_In on the Output of Selection_Out.
• CYCLE: Similar to SWITCH but all subsequent Outputs will be matched linearly
after the chosen Input, wrapping around until all Inputs are matched.
• PASSTHROUGH: All Inputs will be directly matched to all Outputs.
(Selection_in and Selection_Out are ignored).
• SORT: All Inputs will be sorted and matched to all Outputs.
(Selection_in and Selection_Out are ignored).
• REVERSE: All Inputs will be inversely matched to all Outputs.
(Selection_in and Selection_Out are ignored).
If Selection_In is set to 0 (for Modes that support it), the first Input that isn't
None will be selected. Similarly, Selection_Out will pick the first Output slot
that has a valid connection.
With Selection_In set to -1, all Inputs will be packed into a List and output
through the slot specified by Selection_Out. With Selection_Out set to -1,
the Input chosen by Selection_In will be sent to ALL Outputs simultaneously.
A Selection_In of -1 and a Selection_Out of -1 will match ALL Input Nodes and
ALL Output nodes respectively. With both values set to -1, this is equivalent
to PASSTHROUGH mode.
If Validate_Typing is enabled, Outputs will match the type of their respective Inputs. (Currently this functionality is unimplemented).
HOVER OVER THE INPUTS AND OUTPUTS FOR MORE INFO.
"""
def check_lazy_status(s, *args, **kwargs):
selection = int(kwargs['selection_in'])
inputs = []
if selection > 0 and kwargs['mode'] == "SWITCH":
inputs = [f"input{selection}"]
else:
debug_print ("SWITCH [",kwargs['unique_id'],"] UNABLE TO LAZY CHECK, REQUESTING ALL INPUTS.")
for x in range(1,s.max_slots+1):
inputs.append("input"+str(x))
debug_print ("SWITCH [",kwargs['unique_id'],"] LAZY CHECK REQUESTED: ", inputs)
return inputs
def switch(s,input1,mode,selection_in,selection_out,validate_typing,**kwargs):
def find_valid_output(kwargs):
unique_id,prompt = kwargs.pop('unique_id'),kwargs.pop('prompt') #workflow_info = kwargs.pop('extra_pnginfo')
val_out = None
debug_print ("\n>> SWITCH [",unique_id,"]: MODE =",mode)
#debug_print ("\nPROMPT:\n",prompt,"\n") #,"\n\nEXTRA INFO:\n",workflow_info)
for x in range(0,s.max_slots):
if "'source': ['"+str(unique_id)+"', " + str(x) + "]" in str(prompt):
val_out = x+1; break
debug_print (">> FIRST VALID OUTPUT SLOT:",val_out)
return val_out
selected_slot = None
valid_out = find_valid_output(kwargs)
if selection_out == 0: selection_out = valid_out
options = [input1]
for key,value in kwargs.items(): options.append(value)
debug_print (">> OPTIONS:",options)
if selection_in == 0:
i=0
for input in options:
i+=1
if input is not None:
selected_slot = input; selection_in = i
debug_print (">> SELECTED INPUT SLOT (FIRST VALID):",i)
break
elif selection_in > 0:
if selection_in >= len(options):
selected_slot = options[len(options)-1]
else:
selected_slot = options[selection_in-1]
elif selection_in == -1:
if selection_out == -1:
mode = "PASSTHROUGH"
else:
selected_slot = options
debug_print (">> SELECTED SLOTS -> INPUT:",selection_in," OUTPUT:",selection_out)
match mode:
case "SWITCH":
if selection_out is None:
debug_print (">> NO OUTPUTS CONNECTED!")
ret = [None] * (s.max_slots)
if (s.default_to_first):
debug_print (">> DEFAULTING TO FIRST OUTPUT SLOT!")
ret[0] = selected_slot
elif selection_out == -1:
ret = [selected_slot for x in range(0,s.max_slots)]
else:
ret = [None] * (s.max_slots)
ret[selection_out-1] = selected_slot
case "CYCLE":
ret = [None] * (s.max_slots)
for x in range(0,s.max_slots):
i = x+selection_in-1
debug_print ("I=",i)
if i > s.max_slots-1:
i-=s.max_slots
debug_print ("new I=",i)
j = x+selection_out-1
debug_print ("J=",j)
if j > s.max_slots-1:
j-=s.max_slots
debug_print ("new J=",j)
ret[j] = options[i]
case "PASSTHROUGH":
ret = options
case "SORT":
ret = sorted(options)
case "REVERSE":
ret = options[reverse]
#debug_print("\n>> RETURN: ", ret)
return tuple(ret)
@classmethod
def IS_CHANGED(s,**kwargs):
return float("nan")
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", "CUSTOM"], {"tooltip": "Condition to evaluate. The output will be set to the result."}),
"require_inputs": ("BOOLEAN", {"default": True,"label_on":"Return Inputs","label_off":"Return Boolean","tooltip": "Enable to forward TRUE_IN/FALSE_IN Inputs of this node, disable to return Boolean results"}),
"NOT": ("BOOLEAN",{"tooltip":"Invert the result of the condition post-evaluation"}),
"custom_expression": ("STRING", {"default": "2*a == 5*b+2","multiline": True, "tooltip": "Custom expression used with the CUSTOM Condition",
"vykosx.binding": [{
"source": "condition",
"callback": [{
"type": "if",
"condition": [{
"left": "$source.value",
"op": "eq",
"right": '"CUSTOM"'
}],
"true": [{
"type": "set",
"target": "$this.disabled",
"value": False,
}],
"false": [{
"type": "set",
"target": "$this.disabled",
"value": True,
}],
}]
}]
}),
}, "optional": {
"A": (any_type, {"forceInput": True, "lazy": True,"tooltip": "First Input to Evaluate"}),
"B": (any_type, {"forceInput": True, "lazy": True,"tooltip": "Second Input to Evaluate"}),
"TRUE_IN": (any_type, {"forceInput": True, "lazy": True, "tooltip": "Input to forward if the expression evaluates to True"}),
"FALSE_IN": (any_type, {"forceInput": True, "lazy": True, "tooltip": "Input to forward if the expression evaluates to False"}),
}, "hidden": { "unique_id": "UNIQUE_ID" } #"comparison_type": (["Values","Length(A)|Value(B)","Length (Both)","Address(A)|Value (B)","Address (Both)"],),
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("*",)
OUTPUT_TOOLTIPS = ("Result of the comparison. Either a forwarded input or a boolean value depending on whether Required_Input is set",)
FUNCTION = "run_comparison"
CATEGORY = MAIN_CATEGORY
DESCRIPTION = \
"""Allows you to test for a condition and change the flow of execution on your prompt based on whether the condition evaluates to TRUE or FALSE.
This node supports Lazy evaluation. Only the Inputs that are required based on the specified condition will be executed.
A and B are the parameters that will be tested against each other and Condition specifies what kind of comparison will be made between them.
If NOT is enabled then the condition will be reversed after evaluation.
If Require_Inputs is enabled, then you must specify the TRUE_IN and FALSE_IN inputs as their values will be forwarded to the Output based on the results of the evaluation i.e. TRUE_IN will be forwarded if the condition evaluates to TRUE and FALSE_IN will be forwarded if the condition evaluates to FALSE. If Require_Inputs is disabled then TRUE_IN and FALSE_IN can be omitted and the raw boolean values True and False will be returned based on the result of the comparison.
If Condition is set to CUSTOM, you may further specify a custom expression which will be evaluated. Within this expression you may use the variables A and B to refer to those respective inputs as well as any named global variables from [Memory Storage] nodes. Most valid Python expressions, types and built-in functions are supported in the custom expression. For a full list of supported operations please consult the file 'Helper.py' included in this node pack.
HOVER OVER THE INPUTS AND OUTPUTS FOR MORE INFO.
When the condition is set to 'CUSTOM', the parameters Input A and Input B used in the custom_expression must not be None.
"""
def compare(self,A,B,condition,NOT,custom_expression=""):
global VYKOSX_STORAGE_DATA
ret = False
try:
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":
if isinstance(A, torch.Tensor) and isinstance(B, torch.Tensor):
ret = torch.equal(A, B)
else:
ret = (A == B)
case "A != B":
if isinstance(A, torch.Tensor) and isinstance(B, torch.Tensor):
ret = not torch.equal(A, B)
else:
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)
except Exception as x:
debug_print ("[!] ERROR PROCESSING CONDITION:",x,". DEFAULTING TO FALSE.")
pass
if condition == "CUSTOM":
if custom_expression!="":
Vars = {"A":A,"a":A,"B":B,"b":B}
for key, value in VYKOSX_STORAGE_DATA.items():
Vars[key] = value
ret = safe_eval(custom_expression,Vars) #return cbool( ast.literal_eval(B) )
if NOT:
if type(ret) is bool:
ret = not ret
else:
ret = ~ret
return ret
def check_lazy_status(s,condition,require_inputs,NOT,custom_expression,A=None,B=None,TRUE_IN=None,FALSE_IN=None,unique_id=0):
debug_print (">> IF LAZY CHECK! A:",A,"B:",B,"cond:",condition,"NOT:",NOT,"TRUE:",TRUE_IN,"FALSE:",FALSE_IN)
lazy = []
if condition == "CUSTOM":
if "a" in custom_expression.casefold(): lazy+=["A"]
if "b" in custom_expression.casefold(): lazy+=["B"]
else:
if "A" in condition: lazy+=["A"]
if "B" in condition: lazy+=["B"]
skipped_computation = False
if condition == "CUSTOM":
if "a" in custom_expression.casefold() and A is None:
skipped_computation = True
if "b" in custom_expression.casefold() and B is None:
skipped_computation = True
if require_inputs and not skipped_computation:
ret = s.compare(A,B,condition,NOT,custom_expression)
lazy += ["TRUE_IN"] if ret else ["FALSE_IN"]
debug_print (">> LAZY RESULT:",lazy)
return lazy
def run_comparison(s,condition,require_inputs,NOT,custom_expression,A=None,B=None,TRUE_IN=None,FALSE_IN=None,unique_id=0):
debug_print ("\n>> IF CONDITION [",unique_id,"]\n>> COMPARE! A:",A,"B:",B,"cond:",condition,"CUSTOM:",custom_expression,"NOT:",NOT,"TRUE:",TRUE_IN,"FALSE:",FALSE_IN)
ret = s.compare(A,B,condition,NOT,custom_expression)
debug_print (">> COMPARE RESULT:",ret)
if require_inputs:
debug_print (">> RETURN:", str(repr(TRUE_IN)).strip("'") if ret else str(repr(FALSE_IN)).strip("'") )
return (TRUE_IN,) if ret else (FALSE_IN,)
else:
debug_print (">> RETURN:",ret)
return (ret,)
@classmethod
def IS_CHANGED(s,**kwargs):
return float("nan")
class HaltExecution:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"disable": ("BOOLEAN", {"default": False, "defaultInput": False, "tooltip": "Will prevent execution from being halted if set to True"}),
"method": ("BOOLEAN",{"default": False, "label_off":"Cancel Prompt","label_on":"Block Execution","tooltip":"Method for how to halt the prompt. Cancel Prompt will interrupt the prompt altogether whereas Block Execution will prevent any nodes that follow this one from running."}),
"clear_queue": ("BOOLEAN", {"default": False, "defaultInput": False, "tooltip": "Clear any remaining items in the queue and disable Auto-Queue mode"}),
"alert_on_trigger": ("BOOLEAN", {"default": False, "defaultInput": False, "tooltip": "Raise an alert message when this node interrupts execution"}),
"input": (any_type,{"tooltip": "Any data you wish to forward through this node"}),
}, "optional": { "run_after": (any_type, {"tooltip": "Optional input used only to ensure this node will run after any node you connect to this Input"}),
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("*",)
OUTPUT_TOOLTIPS = ("Will return the same data sent through Input",)
CATEGORY = MAIN_CATEGORY
FUNCTION = "halt"
DESCRIPTION = \
"""Allows you to end the current prompt execution on demand or depending on a given condition.
In order for this node to execute, it must be placed in the middle of an operation, so use the Input and Output channels as a Passthrough for any data you may wish to interrupt from being forwarded. To ensure this node runs at a specific and more predictable order, you may also connect the Output of any node of your choice to the Run_After Input of this node. This node should execute only after the previous node is done executing (lazy execution notwithstanding).
You may convert this node's Disable widget to an Input to make it a Condition that you can trigger with an [IF Selector] Node. If Disable is set to False, the current prompt will be halted prematurely when this node is executed.
Method will determine how the execution will be affected. Cancel Prompt will immediately interrupt execution everywhere when this node runs, whereas Block Execution will allow the execution to continue but will prevent any nodes connected to this one from running subsequently.
The Clear_Queue option will additionally clear your Execution Queue and set your Auto-Queue execution mode to Disabled, allowing you to cancel any pending processes.
The Alert_On_Trigger toggle will further raise an exception when the node interrupts the flow of execution, ensuring no other cached nodes continue executing after this one, giving you ample time to review your workflow's state after it has been halted.
Run_After is an unused optional parameter made available only to ensure this node executes after the node it is connected to.
HOVER OVER THE INPUTS AND OUTPUTS FOR MORE INFO.
"""
def halt(s,disable,method,clear_queue,alert_on_trigger,input,run_after=None):
debug_print (" >> HALT EXECUTION REQUESTED. CANCEL = ", disable)
if not disable and input is not None:
debug_print ("\n"+"-"*36+"\n # HALTING PROMPT EXECUTION #"+"\n"+"-"*36)
if clear_queue:
PromptServer.instance.send_sync("VykosX.ClearQueue", {"interrupt" : not method})
if method:
if ExecutionBlocker is None:
raise Exception("Unable to block execution, please update your ComfyUI to enable this functionality!")
else:
text = "❌ EXECUTION BLOCKED!"
return {"ui": {"text": text},"result": (ExecutionBlocker(text if alert_on_trigger else None)),}
else:
nodes.interrupt_processing(True)
text = "🛑 EXECUTION HALTED!"
if alert_on_trigger: raise Exception(text)
return {"ui": {"text": text},"result": (None,)} #return (None,)
text = "➡️ EXECUTION CONTINUING! Time: " + str(datetime.now().time())
return {"ui": {"text": text},"result": (input,)} #return (input,)
@classmethod
def IS_CHANGED(s,**kwargs):
return float("nan")
class FallbackAnyBatch():
max_slots = MAX_SLOTS
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"method_for_images": (["nearest-exact", "bilinear", "area", "bicubic", "lanczos"], { "default": "lanczos", "tooltip": "Image scaling algorithm to use when creating image batches" }),
},"optional": {
"input%d" % x: (any_type, {"tooltip": "Data to join into a batch.\nMay be a tensor, list, tuple or primitive"}) for x in range(1,s.max_slots+1) #{"lazy": True}
},
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("batch",)
OUTPUT_TOOLTIPS = ("Batch created by joining all the Inputs",)
FUNCTION = "fallback_batch"
CATEGORY = MAIN_CATEGORY
DESCRIPTION = \
"""This node allows you to easily create batches from any type of Data. This is invaluable when working with Loops as often you will have different amounts of image generations or data to process depending on the iteration. This node silently ignores any missing inputs allowing you to be conditional with your batches.
Supported Data types are Tensors (images, latents, models, etc), Lists, Tuples and Primitive data types (strings, integers, floats, etc).
When batching images, the final output size will match the dimensions of the very first input image. We recommend creating batches of a single type per node, mixing and matching different types of data into the same batch may have unexpected behavior.
HOVER OVER THE INPUTS AND OUTPUTS FOR MORE INFO.
"""
def fallback_batch(self, method_for_images, **kwargs):
batch = None
#print ("kwargs=",kwargs)