mhnakif commited on
Commit
1028322
·
verified ·
1 Parent(s): dacf79e

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +10 -0
  2. custom_nodes/rgthree-comfy/.github/workflows/publish_comfy_registry_action.yml +20 -0
  3. custom_nodes/rgthree-comfy/.gitignore +13 -0
  4. custom_nodes/rgthree-comfy/.prettierrc.json +5 -0
  5. custom_nodes/rgthree-comfy/.pylintrc +633 -0
  6. custom_nodes/rgthree-comfy/.style.yapf +11 -0
  7. custom_nodes/rgthree-comfy/.tracking +257 -0
  8. custom_nodes/rgthree-comfy/LICENSE +21 -0
  9. custom_nodes/rgthree-comfy/README.md +434 -0
  10. custom_nodes/rgthree-comfy/__build__.py +151 -0
  11. custom_nodes/rgthree-comfy/__commit__.py +55 -0
  12. custom_nodes/rgthree-comfy/__init__.py +126 -0
  13. custom_nodes/rgthree-comfy/__update_comfy__.py +57 -0
  14. custom_nodes/rgthree-comfy/docs/rgthree_advanced.png +3 -0
  15. custom_nodes/rgthree-comfy/docs/rgthree_advanced_metadata.png +3 -0
  16. custom_nodes/rgthree-comfy/docs/rgthree_context.png +3 -0
  17. custom_nodes/rgthree-comfy/docs/rgthree_context_metadata.png +3 -0
  18. custom_nodes/rgthree-comfy/docs/rgthree_router.png +0 -0
  19. custom_nodes/rgthree-comfy/docs/rgthree_seed.png +0 -0
  20. custom_nodes/rgthree-comfy/package-lock.json +568 -0
  21. custom_nodes/rgthree-comfy/package.json +12 -0
  22. custom_nodes/rgthree-comfy/pnpm-lock.yaml +507 -0
  23. custom_nodes/rgthree-comfy/prestartup_script.py +5 -0
  24. custom_nodes/rgthree-comfy/py/__init__.py +0 -0
  25. custom_nodes/rgthree-comfy/py/any_switch.py +38 -0
  26. custom_nodes/rgthree-comfy/py/config.py +111 -0
  27. custom_nodes/rgthree-comfy/py/constants.py +11 -0
  28. custom_nodes/rgthree-comfy/py/context.py +33 -0
  29. custom_nodes/rgthree-comfy/py/context_big.py +31 -0
  30. custom_nodes/rgthree-comfy/py/context_merge.py +37 -0
  31. custom_nodes/rgthree-comfy/py/context_merge_big.py +16 -0
  32. custom_nodes/rgthree-comfy/py/context_switch.py +36 -0
  33. custom_nodes/rgthree-comfy/py/context_switch_big.py +16 -0
  34. custom_nodes/rgthree-comfy/py/context_utils.py +118 -0
  35. custom_nodes/rgthree-comfy/py/display_any.py +77 -0
  36. custom_nodes/rgthree-comfy/py/dynamic_context.py +56 -0
  37. custom_nodes/rgthree-comfy/py/dynamic_context_switch.py +39 -0
  38. custom_nodes/rgthree-comfy/py/image_comparer.py +42 -0
  39. custom_nodes/rgthree-comfy/py/image_inset_crop.py +93 -0
  40. custom_nodes/rgthree-comfy/py/image_or_latent_size.py +31 -0
  41. custom_nodes/rgthree-comfy/py/image_resize.py +117 -0
  42. custom_nodes/rgthree-comfy/py/ksampler_config.py +56 -0
  43. custom_nodes/rgthree-comfy/py/log.py +100 -0
  44. custom_nodes/rgthree-comfy/py/lora_stack.py +46 -0
  45. custom_nodes/rgthree-comfy/py/power_lora_loader.py +101 -0
  46. custom_nodes/rgthree-comfy/py/power_primitive.py +83 -0
  47. custom_nodes/rgthree-comfy/py/power_prompt.py +95 -0
  48. custom_nodes/rgthree-comfy/py/power_prompt_simple.py +42 -0
  49. custom_nodes/rgthree-comfy/py/power_prompt_utils.py +104 -0
  50. custom_nodes/rgthree-comfy/py/power_puter.py +842 -0
.gitattributes CHANGED
@@ -15,9 +15,19 @@ models/unet/wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors filter=lfs diff=lfs
15
  models/vae/wan_2.1_vae.safetensors filter=lfs diff=lfs merge=lfs -text
16
  output/video/ComfyUI_00001_.mp4 filter=lfs diff=lfs merge=lfs -text
17
  user/comfyui.db filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
18
  models/loras/DR34ML4Y_I2V_14B_HIGH_V2.safetensors filter=lfs diff=lfs merge=lfs -text
19
  models/loras/DR34ML4Y_I2V_14B_LOW_V2.safetensors filter=lfs diff=lfs merge=lfs -text
20
  models/loras/NSFW-22-H-e8.safetensors filter=lfs diff=lfs merge=lfs -text
21
  models/loras/NSFW-22-L-e8.safetensors filter=lfs diff=lfs merge=lfs -text
 
 
22
  models/loras/wan22-m4crom4sti4-i2v-20epoc-high-k3nk.safetensors filter=lfs diff=lfs merge=lfs -text
23
  models/loras/wan22-m4crom4sti4-i2v-20epoc-low-k3nk.safetensors filter=lfs diff=lfs merge=lfs -text
 
15
  models/vae/wan_2.1_vae.safetensors filter=lfs diff=lfs merge=lfs -text
16
  output/video/ComfyUI_00001_.mp4 filter=lfs diff=lfs merge=lfs -text
17
  user/comfyui.db filter=lfs diff=lfs merge=lfs -text
18
+ custom_nodes/rgthree-comfy/docs/rgthree_advanced.png filter=lfs diff=lfs merge=lfs -text
19
+ custom_nodes/rgthree-comfy/docs/rgthree_advanced_metadata.png filter=lfs diff=lfs merge=lfs -text
20
+ custom_nodes/rgthree-comfy/docs/rgthree_context.png filter=lfs diff=lfs merge=lfs -text
21
+ custom_nodes/rgthree-comfy/docs/rgthree_context_metadata.png filter=lfs diff=lfs merge=lfs -text
22
+ custom_nodes/rgthree-comfy/src_web/lib/tree-sitter-python.wasm filter=lfs diff=lfs merge=lfs -text
23
+ custom_nodes/rgthree-comfy/src_web/lib/tree-sitter.wasm filter=lfs diff=lfs merge=lfs -text
24
+ custom_nodes/rgthree-comfy/web/lib/tree-sitter-python.wasm filter=lfs diff=lfs merge=lfs -text
25
+ custom_nodes/rgthree-comfy/web/lib/tree-sitter.wasm filter=lfs diff=lfs merge=lfs -text
26
  models/loras/DR34ML4Y_I2V_14B_HIGH_V2.safetensors filter=lfs diff=lfs merge=lfs -text
27
  models/loras/DR34ML4Y_I2V_14B_LOW_V2.safetensors filter=lfs diff=lfs merge=lfs -text
28
  models/loras/NSFW-22-H-e8.safetensors filter=lfs diff=lfs merge=lfs -text
29
  models/loras/NSFW-22-L-e8.safetensors filter=lfs diff=lfs merge=lfs -text
30
+ models/loras/wan2.2-i2v-high-pov-insertion-v1.0.safetensors filter=lfs diff=lfs merge=lfs -text
31
+ models/loras/wan2.2-i2v-low-pov-insertion-v1.0.safetensors filter=lfs diff=lfs merge=lfs -text
32
  models/loras/wan22-m4crom4sti4-i2v-20epoc-high-k3nk.safetensors filter=lfs diff=lfs merge=lfs -text
33
  models/loras/wan22-m4crom4sti4-i2v-20epoc-low-k3nk.safetensors filter=lfs diff=lfs merge=lfs -text
custom_nodes/rgthree-comfy/.github/workflows/publish_comfy_registry_action.yml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Publish to Comfy registry
2
+ on:
3
+ workflow_dispatch:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - "pyproject.toml"
9
+
10
+ jobs:
11
+ publish-node:
12
+ name: Publish Custom Node to registry
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Check out code
16
+ uses: actions/checkout@v4
17
+ - name: Publish Custom Node
18
+ uses: Comfy-Org/publish-node-action@main
19
+ with:
20
+ personal_access_token: ${{ secrets.COMFY_REGISTRY_ACCESS_TOKEN }}
custom_nodes/rgthree-comfy/.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ *.ini
3
+ wildcards/**
4
+ .vscode/
5
+ .idea/
6
+ node_modules/
7
+ rgthree_config.json
8
+ web/rgthree_config.js
9
+ web/comfyui/rgthree_config.js
10
+ userdata/
11
+ userdata/**
12
+ web/comfyui/testing/
13
+ web/comfyui/tests/
custom_nodes/rgthree-comfy/.prettierrc.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "printWidth": 100,
3
+ "bracketSpacing": false,
4
+ "bracketSameLine": true
5
+ }
custom_nodes/rgthree-comfy/.pylintrc ADDED
@@ -0,0 +1,633 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [MAIN]
2
+
3
+ # Analyse import fallback blocks. This can be used to support both Python 2 and
4
+ # 3 compatible code, which means that the block might have code that exists
5
+ # only in one or another interpreter, leading to false positives when analysed.
6
+ analyse-fallback-blocks=no
7
+
8
+ # Clear in-memory caches upon conclusion of linting. Useful if running pylint
9
+ # in a server-like mode.
10
+ clear-cache-post-run=no
11
+
12
+ # Load and enable all available extensions. Use --list-extensions to see a list
13
+ # all available extensions.
14
+ #enable-all-extensions=
15
+
16
+ # In error mode, messages with a category besides ERROR or FATAL are
17
+ # suppressed, and no reports are done by default. Error mode is compatible with
18
+ # disabling specific errors.
19
+ #errors-only=
20
+
21
+ # Always return a 0 (non-error) status code, even if lint errors are found.
22
+ # This is primarily useful in continuous integration scripts.
23
+ #exit-zero=
24
+
25
+ # A comma-separated list of package or module names from where C extensions may
26
+ # be loaded. Extensions are loading into the active Python interpreter and may
27
+ # run arbitrary code.
28
+ extension-pkg-allow-list=
29
+
30
+ # A comma-separated list of package or module names from where C extensions may
31
+ # be loaded. Extensions are loading into the active Python interpreter and may
32
+ # run arbitrary code. (This is an alternative name to extension-pkg-allow-list
33
+ # for backward compatibility.)
34
+ extension-pkg-whitelist=
35
+
36
+ # Return non-zero exit code if any of these messages/categories are detected,
37
+ # even if score is above --fail-under value. Syntax same as enable. Messages
38
+ # specified are enabled, while categories only check already-enabled messages.
39
+ fail-on=
40
+
41
+ # Specify a score threshold under which the program will exit with error.
42
+ fail-under=10
43
+
44
+ # Interpret the stdin as a python script, whose filename needs to be passed as
45
+ # the module_or_package argument.
46
+ #from-stdin=
47
+
48
+ # Files or directories to be skipped. They should be base names, not paths.
49
+ ignore=CVS
50
+
51
+ # Add files or directories matching the regular expressions patterns to the
52
+ # ignore-list. The regex matches against paths and can be in Posix or Windows
53
+ # format. Because '\\' represents the directory delimiter on Windows systems,
54
+ # it can't be used as an escape character.
55
+ ignore-paths=
56
+
57
+ # Files or directories matching the regular expression patterns are skipped.
58
+ # The regex matches against base names, not paths. The default value ignores
59
+ # Emacs file locks
60
+ ignore-patterns=^\.#
61
+
62
+ # List of module names for which member attributes should not be checked
63
+ # (useful for modules/projects where namespaces are manipulated during runtime
64
+ # and thus existing member attributes cannot be deduced by static analysis). It
65
+ # supports qualified module names, as well as Unix pattern matching.
66
+ ignored-modules=
67
+
68
+ # Python code to execute, usually for sys.path manipulation such as
69
+ # pygtk.require().
70
+ #init-hook=
71
+
72
+ # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
73
+ # number of processors available to use, and will cap the count on Windows to
74
+ # avoid hangs.
75
+ jobs=1
76
+
77
+ # Control the amount of potential inferred values when inferring a single
78
+ # object. This can help the performance when dealing with large functions or
79
+ # complex, nested conditions.
80
+ limit-inference-results=100
81
+
82
+ # List of plugins (as comma separated values of python module names) to load,
83
+ # usually to register additional checkers.
84
+ load-plugins=
85
+
86
+ # Pickle collected data for later comparisons.
87
+ persistent=yes
88
+
89
+ # Minimum Python version to use for version dependent checks. Will default to
90
+ # the version used to run pylint.
91
+ py-version=3.10
92
+
93
+ # Discover python modules and packages in the file system subtree.
94
+ recursive=no
95
+
96
+ # Add paths to the list of the source roots. Supports globbing patterns. The
97
+ # source root is an absolute path or a path relative to the current working
98
+ # directory used to determine a package namespace for modules located under the
99
+ # source root.
100
+ source-roots=
101
+
102
+ # When enabled, pylint would attempt to guess common misconfiguration and emit
103
+ # user-friendly hints instead of false-positive error messages.
104
+ suggestion-mode=yes
105
+
106
+ # Allow loading of arbitrary C extensions. Extensions are imported into the
107
+ # active Python interpreter and may run arbitrary code.
108
+ unsafe-load-any-extension=no
109
+
110
+ # In verbose mode, extra non-checker-related info will be displayed.
111
+ #verbose=
112
+
113
+
114
+ [BASIC]
115
+
116
+ # Naming style matching correct argument names.
117
+ argument-naming-style=snake_case
118
+
119
+ # Regular expression matching correct argument names. Overrides argument-
120
+ # naming-style. If left empty, argument names will be checked with the set
121
+ # naming style.
122
+ #argument-rgx=
123
+
124
+ # Naming style matching correct attribute names.
125
+ attr-naming-style=snake_case
126
+
127
+ # Regular expression matching correct attribute names. Overrides attr-naming-
128
+ # style. If left empty, attribute names will be checked with the set naming
129
+ # style.
130
+ #attr-rgx=
131
+
132
+ # Bad variable names which should always be refused, separated by a comma.
133
+ bad-names=foo,
134
+ bar,
135
+ baz,
136
+ toto,
137
+ tutu,
138
+ tata
139
+
140
+ # Bad variable names regexes, separated by a comma. If names match any regex,
141
+ # they will always be refused
142
+ bad-names-rgxs=
143
+
144
+ # Naming style matching correct class attribute names.
145
+ class-attribute-naming-style=any
146
+
147
+ # Regular expression matching correct class attribute names. Overrides class-
148
+ # attribute-naming-style. If left empty, class attribute names will be checked
149
+ # with the set naming style.
150
+ #class-attribute-rgx=
151
+
152
+ # Naming style matching correct class constant names.
153
+ class-const-naming-style=UPPER_CASE
154
+
155
+ # Regular expression matching correct class constant names. Overrides class-
156
+ # const-naming-style. If left empty, class constant names will be checked with
157
+ # the set naming style.
158
+ #class-const-rgx=
159
+
160
+ # Naming style matching correct class names.
161
+ class-naming-style=PascalCase
162
+
163
+ # Regular expression matching correct class names. Overrides class-naming-
164
+ # style. If left empty, class names will be checked with the set naming style.
165
+ #class-rgx=
166
+
167
+ # Naming style matching correct constant names.
168
+ const-naming-style=UPPER_CASE
169
+
170
+ # Regular expression matching correct constant names. Overrides const-naming-
171
+ # style. If left empty, constant names will be checked with the set naming
172
+ # style.
173
+ #const-rgx=
174
+
175
+ # Minimum line length for functions/classes that require docstrings, shorter
176
+ # ones are exempt.
177
+ docstring-min-length=-1
178
+
179
+ # Naming style matching correct function names.
180
+ function-naming-style=snake_case
181
+
182
+ # Regular expression matching correct function names. Overrides function-
183
+ # naming-style. If left empty, function names will be checked with the set
184
+ # naming style.
185
+ #function-rgx=
186
+
187
+ # Good variable names which should always be accepted, separated by a comma.
188
+ good-names=i,
189
+ j,
190
+ k,
191
+ ex,
192
+ Run,
193
+ _
194
+
195
+ # Good variable names regexes, separated by a comma. If names match any regex,
196
+ # they will always be accepted
197
+ good-names-rgxs=
198
+
199
+ # Include a hint for the correct naming format with invalid-name.
200
+ include-naming-hint=no
201
+
202
+ # Naming style matching correct inline iteration names.
203
+ inlinevar-naming-style=any
204
+
205
+ # Regular expression matching correct inline iteration names. Overrides
206
+ # inlinevar-naming-style. If left empty, inline iteration names will be checked
207
+ # with the set naming style.
208
+ #inlinevar-rgx=
209
+
210
+ # Naming style matching correct method names.
211
+ method-naming-style=snake_case
212
+
213
+ # Regular expression matching correct method names. Overrides method-naming-
214
+ # style. If left empty, method names will be checked with the set naming style.
215
+ #method-rgx=
216
+
217
+ # Naming style matching correct module names.
218
+ module-naming-style=snake_case
219
+
220
+ # Regular expression matching correct module names. Overrides module-naming-
221
+ # style. If left empty, module names will be checked with the set naming style.
222
+ #module-rgx=
223
+
224
+ # Colon-delimited sets of names that determine each other's naming style when
225
+ # the name regexes allow several styles.
226
+ name-group=
227
+
228
+ # Regular expression which should only match function or class names that do
229
+ # not require a docstring.
230
+ no-docstring-rgx=^_
231
+
232
+ # List of decorators that produce properties, such as abc.abstractproperty. Add
233
+ # to this list to register other decorators that produce valid properties.
234
+ # These decorators are taken in consideration only for invalid-name.
235
+ property-classes=abc.abstractproperty
236
+
237
+ # Regular expression matching correct type alias names. If left empty, type
238
+ # alias names will be checked with the set naming style.
239
+ #typealias-rgx=
240
+
241
+ # Regular expression matching correct type variable names. If left empty, type
242
+ # variable names will be checked with the set naming style.
243
+ #typevar-rgx=
244
+
245
+ # Naming style matching correct variable names.
246
+ variable-naming-style=snake_case
247
+
248
+ # Regular expression matching correct variable names. Overrides variable-
249
+ # naming-style. If left empty, variable names will be checked with the set
250
+ # naming style.
251
+ #variable-rgx=
252
+
253
+
254
+ [CLASSES]
255
+
256
+ # Warn about protected attribute access inside special methods
257
+ check-protected-access-in-special-methods=no
258
+
259
+ # List of method names used to declare (i.e. assign) instance attributes.
260
+ defining-attr-methods=__init__,
261
+ __new__,
262
+ setUp,
263
+ asyncSetUp,
264
+ __post_init__
265
+
266
+ # List of member names, which should be excluded from the protected access
267
+ # warning.
268
+ exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
269
+
270
+ # List of valid names for the first argument in a class method.
271
+ valid-classmethod-first-arg=cls
272
+
273
+ # List of valid names for the first argument in a metaclass class method.
274
+ valid-metaclass-classmethod-first-arg=mcs
275
+
276
+
277
+ [DESIGN]
278
+
279
+ # List of regular expressions of class ancestor names to ignore when counting
280
+ # public methods (see R0903)
281
+ exclude-too-few-public-methods=
282
+
283
+ # List of qualified class names to ignore when counting class parents (see
284
+ # R0901)
285
+ ignored-parents=
286
+
287
+ # Maximum number of arguments for function / method.
288
+ max-args=5
289
+
290
+ # Maximum number of attributes for a class (see R0902).
291
+ max-attributes=7
292
+
293
+ # Maximum number of boolean expressions in an if statement (see R0916).
294
+ max-bool-expr=5
295
+
296
+ # Maximum number of branch for function / method body.
297
+ max-branches=12
298
+
299
+ # Maximum number of locals for function / method body.
300
+ max-locals=15
301
+
302
+ # Maximum number of parents for a class (see R0901).
303
+ max-parents=7
304
+
305
+ # Maximum number of public methods for a class (see R0904).
306
+ max-public-methods=20
307
+
308
+ # Maximum number of return / yield for function / method body.
309
+ max-returns=6
310
+
311
+ # Maximum number of statements in function / method body.
312
+ max-statements=50
313
+
314
+ # Minimum number of public methods for a class (see R0903).
315
+ min-public-methods=2
316
+
317
+
318
+ [EXCEPTIONS]
319
+
320
+ # Exceptions that will emit a warning when caught.
321
+ overgeneral-exceptions=builtins.BaseException,builtins.Exception
322
+
323
+
324
+ [FORMAT]
325
+
326
+ # Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
327
+ expected-line-ending-format=
328
+
329
+ # Regexp for a line that is allowed to be longer than the limit.
330
+ ignore-long-lines=^\s*(# )?<?https?://\S+>?$
331
+
332
+ # Number of spaces of indent required inside a hanging or continued line.
333
+ indent-after-paren=4
334
+
335
+ # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
336
+ # tab).
337
+ indent-string=' '
338
+
339
+ # Maximum number of characters on a single line.
340
+ max-line-length=100
341
+
342
+ # Maximum number of lines in a module.
343
+ max-module-lines=1000
344
+
345
+ # Allow the body of a class to be on the same line as the declaration if body
346
+ # contains single statement.
347
+ single-line-class-stmt=no
348
+
349
+ # Allow the body of an if to be on the same line as the test if there is no
350
+ # else.
351
+ single-line-if-stmt=no
352
+
353
+
354
+ [IMPORTS]
355
+
356
+ # List of modules that can be imported at any level, not just the top level
357
+ # one.
358
+ allow-any-import-level=
359
+
360
+ # Allow explicit reexports by alias from a package __init__.
361
+ allow-reexport-from-package=no
362
+
363
+ # Allow wildcard imports from modules that define __all__.
364
+ allow-wildcard-with-all=no
365
+
366
+ # Deprecated modules which should not be used, separated by a comma.
367
+ deprecated-modules=
368
+
369
+ # Output a graph (.gv or any supported image format) of external dependencies
370
+ # to the given file (report RP0402 must not be disabled).
371
+ ext-import-graph=
372
+
373
+ # Output a graph (.gv or any supported image format) of all (i.e. internal and
374
+ # external) dependencies to the given file (report RP0402 must not be
375
+ # disabled).
376
+ import-graph=
377
+
378
+ # Output a graph (.gv or any supported image format) of internal dependencies
379
+ # to the given file (report RP0402 must not be disabled).
380
+ int-import-graph=
381
+
382
+ # Force import order to recognize a module as part of the standard
383
+ # compatibility libraries.
384
+ known-standard-library=
385
+
386
+ # Force import order to recognize a module as part of a third party library.
387
+ known-third-party=enchant
388
+
389
+ # Couples of modules and preferred modules, separated by a comma.
390
+ preferred-modules=
391
+
392
+
393
+ [LOGGING]
394
+
395
+ # The type of string formatting that logging methods do. `old` means using %
396
+ # formatting, `new` is for `{}` formatting.
397
+ logging-format-style=old
398
+
399
+ # Logging modules to check that the string format arguments are in logging
400
+ # function parameter format.
401
+ logging-modules=logging
402
+
403
+
404
+ [MESSAGES CONTROL]
405
+
406
+ # Only show warnings with the listed confidence levels. Leave empty to show
407
+ # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
408
+ # UNDEFINED.
409
+ confidence=HIGH,
410
+ CONTROL_FLOW,
411
+ INFERENCE,
412
+ INFERENCE_FAILURE,
413
+ UNDEFINED
414
+
415
+ # Disable the message, report, category or checker with the given id(s). You
416
+ # can either give multiple identifiers separated by comma (,) or put this
417
+ # option multiple times (only on the command line, not in the configuration
418
+ # file where it should appear only once). You can also use "--disable=all" to
419
+ # disable everything first and then re-enable specific checks. For example, if
420
+ # you want to run only the similarities checker, you can use "--disable=all
421
+ # --enable=similarities". If you want to run only the classes checker, but have
422
+ # no Warning level messages displayed, use "--disable=all --enable=classes
423
+ # --disable=W".
424
+ disable=raw-checker-failed,
425
+ bad-inline-option,
426
+ locally-disabled,
427
+ file-ignored,
428
+ suppressed-message,
429
+ useless-suppression,
430
+ deprecated-pragma,
431
+ use-symbolic-message-instead,
432
+ C0103, # invalid-name :: constant case
433
+ W0603 # global-statements
434
+
435
+ # Enable the message, report, category or checker with the given id(s). You can
436
+ # either give multiple identifier separated by comma (,) or put this option
437
+ # multiple time (only on the command line, not in the configuration file where
438
+ # it should appear only once). See also the "--disable" option for examples.
439
+ enable=c-extension-no-member
440
+
441
+
442
+ [METHOD_ARGS]
443
+
444
+ # List of qualified names (i.e., library.method) which require a timeout
445
+ # parameter e.g. 'requests.api.get,requests.api.post'
446
+ timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
447
+
448
+
449
+ [MISCELLANEOUS]
450
+
451
+ # List of note tags to take in consideration, separated by a comma.
452
+ notes=FIXME,
453
+ XXX,
454
+ TODO
455
+
456
+ # Regular expression of note tags to take in consideration.
457
+ notes-rgx=
458
+
459
+
460
+ [REFACTORING]
461
+
462
+ # Maximum number of nested blocks for function / method body
463
+ max-nested-blocks=5
464
+
465
+ # Complete name of functions that never returns. When checking for
466
+ # inconsistent-return-statements if a never returning function is called then
467
+ # it will be considered as an explicit return statement and no message will be
468
+ # printed.
469
+ never-returning-functions=sys.exit,argparse.parse_error
470
+
471
+
472
+ [REPORTS]
473
+
474
+ # Python expression which should return a score less than or equal to 10. You
475
+ # have access to the variables 'fatal', 'error', 'warning', 'refactor',
476
+ # 'convention', and 'info' which contain the number of messages in each
477
+ # category, as well as 'statement' which is the total number of statements
478
+ # analyzed. This score is used by the global evaluation report (RP0004).
479
+ evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
480
+
481
+ # Template used to display messages. This is a python new-style format string
482
+ # used to format the message information. See doc for all details.
483
+ msg-template=
484
+
485
+ # Set the output format. Available formats are text, parseable, colorized, json
486
+ # and msvs (visual studio). You can also give a reporter class, e.g.
487
+ # mypackage.mymodule.MyReporterClass.
488
+ #output-format=
489
+
490
+ # Tells whether to display a full report or only the messages.
491
+ reports=no
492
+
493
+ # Activate the evaluation score.
494
+ score=yes
495
+
496
+
497
+ [SIMILARITIES]
498
+
499
+ # Comments are removed from the similarity computation
500
+ ignore-comments=yes
501
+
502
+ # Docstrings are removed from the similarity computation
503
+ ignore-docstrings=yes
504
+
505
+ # Imports are removed from the similarity computation
506
+ ignore-imports=yes
507
+
508
+ # Signatures are removed from the similarity computation
509
+ ignore-signatures=yes
510
+
511
+ # Minimum lines number of a similarity.
512
+ min-similarity-lines=4
513
+
514
+
515
+ [SPELLING]
516
+
517
+ # Limits count of emitted suggestions for spelling mistakes.
518
+ max-spelling-suggestions=4
519
+
520
+ # Spelling dictionary name. No available dictionaries : You need to install
521
+ # both the python package and the system dependency for enchant to work..
522
+ spelling-dict=
523
+
524
+ # List of comma separated words that should be considered directives if they
525
+ # appear at the beginning of a comment and should not be checked.
526
+ spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
527
+
528
+ # List of comma separated words that should not be checked.
529
+ spelling-ignore-words=
530
+
531
+ # A path to a file that contains the private dictionary; one word per line.
532
+ spelling-private-dict-file=
533
+
534
+ # Tells whether to store unknown words to the private dictionary (see the
535
+ # --spelling-private-dict-file option) instead of raising a message.
536
+ spelling-store-unknown-words=no
537
+
538
+
539
+ [STRING]
540
+
541
+ # This flag controls whether inconsistent-quotes generates a warning when the
542
+ # character used as a quote delimiter is used inconsistently within a module.
543
+ check-quote-consistency=no
544
+
545
+ # This flag controls whether the implicit-str-concat should generate a warning
546
+ # on implicit string concatenation in sequences defined over several lines.
547
+ check-str-concat-over-line-jumps=no
548
+
549
+
550
+ [TYPECHECK]
551
+
552
+ # List of decorators that produce context managers, such as
553
+ # contextlib.contextmanager. Add to this list to register other decorators that
554
+ # produce valid context managers.
555
+ contextmanager-decorators=contextlib.contextmanager
556
+
557
+ # List of members which are set dynamically and missed by pylint inference
558
+ # system, and so shouldn't trigger E1101 when accessed. Python regular
559
+ # expressions are accepted.
560
+ generated-members=
561
+
562
+ # Tells whether to warn about missing members when the owner of the attribute
563
+ # is inferred to be None.
564
+ ignore-none=yes
565
+
566
+ # This flag controls whether pylint should warn about no-member and similar
567
+ # checks whenever an opaque object is returned when inferring. The inference
568
+ # can return multiple potential results while evaluating a Python object, but
569
+ # some branches might not be evaluated, which results in partial inference. In
570
+ # that case, it might be useful to still emit no-member and other checks for
571
+ # the rest of the inferred objects.
572
+ ignore-on-opaque-inference=yes
573
+
574
+ # List of symbolic message names to ignore for Mixin members.
575
+ ignored-checks-for-mixins=no-member,
576
+ not-async-context-manager,
577
+ not-context-manager,
578
+ attribute-defined-outside-init
579
+
580
+ # List of class names for which member attributes should not be checked (useful
581
+ # for classes with dynamically set attributes). This supports the use of
582
+ # qualified names.
583
+ ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
584
+
585
+ # Show a hint with possible names when a member name was not found. The aspect
586
+ # of finding the hint is based on edit distance.
587
+ missing-member-hint=yes
588
+
589
+ # The minimum edit distance a name should have in order to be considered a
590
+ # similar match for a missing member name.
591
+ missing-member-hint-distance=1
592
+
593
+ # The total number of similar names that should be taken in consideration when
594
+ # showing a hint for a missing member.
595
+ missing-member-max-choices=1
596
+
597
+ # Regex pattern to define which classes are considered mixins.
598
+ mixin-class-rgx=.*[Mm]ixin
599
+
600
+ # List of decorators that change the signature of a decorated function.
601
+ signature-mutators=
602
+
603
+
604
+ [VARIABLES]
605
+
606
+ # List of additional names supposed to be defined in builtins. Remember that
607
+ # you should avoid defining new builtins when possible.
608
+ additional-builtins=
609
+
610
+ # Tells whether unused global variables should be treated as a violation.
611
+ allow-global-unused-variables=yes
612
+
613
+ # List of names allowed to shadow builtins
614
+ allowed-redefined-builtins=
615
+
616
+ # List of strings which can identify a callback function by name. A callback
617
+ # name must start or end with one of those strings.
618
+ callbacks=cb_,
619
+ _cb
620
+
621
+ # A regular expression matching the name of dummy variables (i.e. expected to
622
+ # not be used).
623
+ dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
624
+
625
+ # Argument names that match this expression will be ignored.
626
+ ignored-argument-names=_.*|^ignored_|^unused_
627
+
628
+ # Tells whether we should check for unused import in __init__ files.
629
+ init-import=no
630
+
631
+ # List of qualified module names which can have objects that can redefine
632
+ # builtins.
633
+ redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
custom_nodes/rgthree-comfy/.style.yapf ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # https://github.com/google/yapf
2
+ [style]
3
+ based_on_style = google
4
+ INDENT_WIDTH = 2
5
+ CONTINUATION_INDENT_WIDTH = 2
6
+ COLUMN_LIMIT = 100
7
+ DISABLE_ENDING_COMMA_HEURISTIC = true
8
+ DEDENT_CLOSING_BRACKETS = true
9
+ FORCE_MULTILINE_DICT = false
10
+ coalesce_brackets=True
11
+ ALLOW_SPLIT_BEFORE_DICT_VALUE = false
custom_nodes/rgthree-comfy/.tracking ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .github/workflows/publish_comfy_registry_action.yml
2
+ .gitignore
3
+ .prettierrc.json
4
+ .pylintrc
5
+ .style.yapf
6
+ LICENSE
7
+ README.md
8
+ __build__.py
9
+ __commit__.py
10
+ __init__.py
11
+ __update_comfy__.py
12
+ docs/rgthree_advanced.png
13
+ docs/rgthree_advanced_metadata.png
14
+ docs/rgthree_context.png
15
+ docs/rgthree_context_metadata.png
16
+ docs/rgthree_router.png
17
+ docs/rgthree_seed.png
18
+ package-lock.json
19
+ package.json
20
+ pnpm-lock.yaml
21
+ prestartup_script.py
22
+ py/__init__.py
23
+ py/any_switch.py
24
+ py/config.py
25
+ py/constants.py
26
+ py/context.py
27
+ py/context_big.py
28
+ py/context_merge.py
29
+ py/context_merge_big.py
30
+ py/context_switch.py
31
+ py/context_switch_big.py
32
+ py/context_utils.py
33
+ py/display_any.py
34
+ py/dynamic_context.py
35
+ py/dynamic_context_switch.py
36
+ py/image_comparer.py
37
+ py/image_inset_crop.py
38
+ py/image_or_latent_size.py
39
+ py/image_resize.py
40
+ py/ksampler_config.py
41
+ py/log.py
42
+ py/lora_stack.py
43
+ py/power_lora_loader.py
44
+ py/power_primitive.py
45
+ py/power_prompt.py
46
+ py/power_prompt_simple.py
47
+ py/power_prompt_utils.py
48
+ py/power_puter.py
49
+ py/pyproject.py
50
+ py/sdxl_empty_latent_image.py
51
+ py/sdxl_power_prompt_postive.py
52
+ py/sdxl_power_prompt_simple.py
53
+ py/seed.py
54
+ py/server/rgthree_server.py
55
+ py/server/routes_config.py
56
+ py/server/routes_model_info.py
57
+ py/server/utils_info.py
58
+ py/server/utils_server.py
59
+ py/utils.py
60
+ py/utils_userdata.py
61
+ pyproject.toml
62
+ requirements.txt
63
+ rgthree_config.json.default
64
+ src_web/comfyui/any_switch.ts
65
+ src_web/comfyui/base_any_input_connected_node.ts
66
+ src_web/comfyui/base_node.ts
67
+ src_web/comfyui/base_node_collector.ts
68
+ src_web/comfyui/base_node_mode_changer.ts
69
+ src_web/comfyui/base_power_prompt.ts
70
+ src_web/comfyui/bookmark.ts
71
+ src_web/comfyui/bypasser.ts
72
+ src_web/comfyui/comfy_ui_bar.ts
73
+ src_web/comfyui/config.ts
74
+ src_web/comfyui/constants.ts
75
+ src_web/comfyui/context.ts
76
+ src_web/comfyui/dialog_info.ts
77
+ src_web/comfyui/display_any.ts
78
+ src_web/comfyui/dynamic_context.ts
79
+ src_web/comfyui/dynamic_context_base.ts
80
+ src_web/comfyui/dynamic_context_switch.ts
81
+ src_web/comfyui/fast_actions_button.ts
82
+ src_web/comfyui/fast_groups_bypasser.ts
83
+ src_web/comfyui/fast_groups_muter.ts
84
+ src_web/comfyui/feature_group_fast_toggle.ts
85
+ src_web/comfyui/feature_import_individual_nodes.ts
86
+ src_web/comfyui/image_comparer.ts
87
+ src_web/comfyui/image_inset_crop.ts
88
+ src_web/comfyui/image_or_latent_size.ts
89
+ src_web/comfyui/label.ts
90
+ src_web/comfyui/menu_auto_nest.ts
91
+ src_web/comfyui/menu_copy_image.ts
92
+ src_web/comfyui/menu_queue_node.ts
93
+ src_web/comfyui/muter.ts
94
+ src_web/comfyui/node_collector.ts
95
+ src_web/comfyui/node_mode_relay.ts
96
+ src_web/comfyui/node_mode_repeater.ts
97
+ src_web/comfyui/power_conductor.ts
98
+ src_web/comfyui/power_lora_loader.ts
99
+ src_web/comfyui/power_primitive.ts
100
+ src_web/comfyui/power_prompt.ts
101
+ src_web/comfyui/power_puter.ts
102
+ src_web/comfyui/random_unmuter.ts
103
+ src_web/comfyui/reroute.ts
104
+ src_web/comfyui/rgthree.scss
105
+ src_web/comfyui/rgthree.ts
106
+ src_web/comfyui/seed.ts
107
+ src_web/comfyui/services/bookmarks_services.ts
108
+ src_web/comfyui/services/config_service.ts
109
+ src_web/comfyui/services/context_service.ts
110
+ src_web/comfyui/services/fast_groups_service.ts
111
+ src_web/comfyui/services/key_events_services.ts
112
+ src_web/comfyui/testing/comfyui_env.ts
113
+ src_web/comfyui/testing/runner.ts
114
+ src_web/comfyui/testing/utils_test.ts
115
+ src_web/comfyui/tests/context_dynamic_tests.ts
116
+ src_web/comfyui/tests/image_or_latent_size_tests.ts
117
+ src_web/comfyui/tests/power_puter.ts
118
+ src_web/comfyui/utils.ts
119
+ src_web/comfyui/utils_canvas.ts
120
+ src_web/comfyui/utils_deprecated_comfyui.ts
121
+ src_web/comfyui/utils_inputs_outputs.ts
122
+ src_web/comfyui/utils_menu.ts
123
+ src_web/comfyui/utils_widgets.ts
124
+ src_web/common/comfyui_shim.ts
125
+ src_web/common/comfyui_shim_pnginfo.ts
126
+ src_web/common/components/base_custom_element.ts
127
+ src_web/common/css/buttons.scss
128
+ src_web/common/css/dialog.scss
129
+ src_web/common/css/dialog_lora_chooser.scss
130
+ src_web/common/css/dialog_model_info.scss
131
+ src_web/common/css/menu.scss
132
+ src_web/common/css/pages_base.scss
133
+ src_web/common/dialog.ts
134
+ src_web/common/link_fixer.ts
135
+ src_web/common/media/rgthree.svg
136
+ src_web/common/media/svgs.ts
137
+ src_web/common/menu.ts
138
+ src_web/common/model_info_service.ts
139
+ src_web/common/progress_bar.ts
140
+ src_web/common/prompt_service.ts
141
+ src_web/common/py_parser.ts
142
+ src_web/common/rgthree_api.ts
143
+ src_web/common/shared_utils.ts
144
+ src_web/common/utils_dom.ts
145
+ src_web/common/utils_templates.ts
146
+ src_web/common/utils_workflow.ts
147
+ src_web/lib/tree-sitter-python.wasm
148
+ src_web/lib/tree-sitter.js
149
+ src_web/lib/tree-sitter.wasm
150
+ src_web/link_fixer/icon_file_json.png
151
+ src_web/link_fixer/index.html
152
+ src_web/link_fixer/link_page.ts
153
+ src_web/models/components/model-info-card.html
154
+ src_web/models/components/model-info-card.scss
155
+ src_web/models/components/model-info-card.ts
156
+ src_web/models/index.html
157
+ src_web/models/models.scss
158
+ src_web/models/models_info_page.ts
159
+ src_web/scripts_comfy/README.md
160
+ src_web/scripts_comfy/api.ts
161
+ src_web/scripts_comfy/app.ts
162
+ src_web/scripts_comfy/widgets.ts
163
+ src_web/typings/comfy.d.ts
164
+ src_web/typings/index.d.ts
165
+ src_web/typings/litegraph.d.ts
166
+ src_web/typings/rgthree.d.ts
167
+ src_web/typings/web-tree-sitter.d.ts
168
+ tsconfig.json
169
+ web/comfyui/any_switch.js
170
+ web/comfyui/base_any_input_connected_node.js
171
+ web/comfyui/base_node.js
172
+ web/comfyui/base_node_collector.js
173
+ web/comfyui/base_node_mode_changer.js
174
+ web/comfyui/base_power_prompt.js
175
+ web/comfyui/bookmark.js
176
+ web/comfyui/bypasser.js
177
+ web/comfyui/comfy_ui_bar.js
178
+ web/comfyui/config.js
179
+ web/comfyui/constants.js
180
+ web/comfyui/context.js
181
+ web/comfyui/dialog_info.js
182
+ web/comfyui/display_any.js
183
+ web/comfyui/dynamic_context.js
184
+ web/comfyui/dynamic_context_base.js
185
+ web/comfyui/dynamic_context_switch.js
186
+ web/comfyui/fast_actions_button.js
187
+ web/comfyui/fast_groups_bypasser.js
188
+ web/comfyui/fast_groups_muter.js
189
+ web/comfyui/feature_group_fast_toggle.js
190
+ web/comfyui/feature_import_individual_nodes.js
191
+ web/comfyui/image_comparer.js
192
+ web/comfyui/image_inset_crop.js
193
+ web/comfyui/image_or_latent_size.js
194
+ web/comfyui/label.js
195
+ web/comfyui/menu_auto_nest.js
196
+ web/comfyui/menu_copy_image.js
197
+ web/comfyui/menu_queue_node.js
198
+ web/comfyui/muter.js
199
+ web/comfyui/node_collector.js
200
+ web/comfyui/node_mode_relay.js
201
+ web/comfyui/node_mode_repeater.js
202
+ web/comfyui/power_conductor.js
203
+ web/comfyui/power_lora_loader.js
204
+ web/comfyui/power_primitive.js
205
+ web/comfyui/power_prompt.js
206
+ web/comfyui/power_puter.js
207
+ web/comfyui/random_unmuter.js
208
+ web/comfyui/reroute.js
209
+ web/comfyui/rgthree.css
210
+ web/comfyui/rgthree.js
211
+ web/comfyui/seed.js
212
+ web/comfyui/services/bookmarks_services.js
213
+ web/comfyui/services/config_service.js
214
+ web/comfyui/services/context_service.js
215
+ web/comfyui/services/fast_groups_service.js
216
+ web/comfyui/services/key_events_services.js
217
+ web/comfyui/utils.js
218
+ web/comfyui/utils_canvas.js
219
+ web/comfyui/utils_deprecated_comfyui.js
220
+ web/comfyui/utils_inputs_outputs.js
221
+ web/comfyui/utils_menu.js
222
+ web/comfyui/utils_widgets.js
223
+ web/common/comfyui_shim.js
224
+ web/common/comfyui_shim_pnginfo.js
225
+ web/common/components/base_custom_element.js
226
+ web/common/css/buttons.css
227
+ web/common/css/dialog.css
228
+ web/common/css/dialog_lora_chooser.css
229
+ web/common/css/dialog_model_info.css
230
+ web/common/css/menu.css
231
+ web/common/css/pages_base.css
232
+ web/common/dialog.js
233
+ web/common/link_fixer.js
234
+ web/common/media/rgthree.svg
235
+ web/common/media/svgs.js
236
+ web/common/menu.js
237
+ web/common/model_info_service.js
238
+ web/common/progress_bar.js
239
+ web/common/prompt_service.js
240
+ web/common/py_parser.js
241
+ web/common/rgthree_api.js
242
+ web/common/shared_utils.js
243
+ web/common/utils_dom.js
244
+ web/common/utils_templates.js
245
+ web/common/utils_workflow.js
246
+ web/lib/tree-sitter-python.wasm
247
+ web/lib/tree-sitter.js
248
+ web/lib/tree-sitter.wasm
249
+ web/link_fixer/icon_file_json.png
250
+ web/link_fixer/index.html
251
+ web/link_fixer/link_page.js
252
+ web/models/components/model-info-card.css
253
+ web/models/components/model-info-card.html
254
+ web/models/components/model-info-card.js
255
+ web/models/index.html
256
+ web/models/models.css
257
+ web/models/models_info_page.js
custom_nodes/rgthree-comfy/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Regis Gaughan, III (rgthree)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
custom_nodes/rgthree-comfy/README.md ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <h1 align="center">
2
+ rgthree-comfy
3
+ <br>
4
+ <sub><sup><i>Making ComfyUI more comfortable!</i></sup></sub>
5
+ <br>
6
+ </h1>
7
+ <p align="center">
8
+ <a href="#️-the-nodes">The Nodes</a> &nbsp; | &nbsp; <a href="#-improvements--features">Improvements & Features</a> &nbsp; | &nbsp; <a href="#-link-fixer">Link Fixer</a>
9
+ </p>
10
+ <hr>
11
+
12
+ A collection of nodes and improvements created while messing around with ComfyUI. I made them for myself to make my workflow cleaner, easier, and faster. You're welcome to try them out. But remember, I made them for my own use cases :)
13
+
14
+ ![Context Node](./docs/rgthree_advanced.png)
15
+
16
+ # Get Started
17
+
18
+ ## Install
19
+
20
+ 1. Install the great [ComfyUi](https://github.com/comfyanonymous/ComfyUI).
21
+ 2. Clone this repo into `custom_modules`:
22
+ ```
23
+ cd ComfyUI/custom_nodes
24
+ git clone https://github.com/rgthree/rgthree-comfy.git
25
+ ```
26
+ 3. Start up ComfyUI.
27
+
28
+ ## Settings
29
+
30
+ You can configure certain aspect of rgthree-comfy. For instance, perhaps a future ComfyUI change breaks rgthree-comfy, or you already have another extension that does something similar and you want to turn it off for rgthree-comfy.
31
+
32
+ You can get to rgthree-settings by right-clicking on the empty part of the graph, and selecting `rgthree-comfy > Settings (rgthree-comfy)` or by clicking the `rgthree-comfy settings` in the ComfyUI settings dialog.
33
+
34
+ _(Note, settings are stored in an `rgthree_config.json` in the `rgthree-comfy` directory. There are other advanced settings that can only be configured there; You can copy default settings from `rgthree_config.json.default` before `rgthree_config.json` before modifying)_.
35
+
36
+ <br>
37
+
38
+ # ✴️ The Nodes
39
+
40
+ Note, you can right-click on a bunch of the rgthree-comfy nodes and select `🛟 Node Help` menu item for in-app help when available.
41
+
42
+ ## Seed
43
+ > An intuitive seed control node for ComfyUI that works very much like Automatic1111's seed control.
44
+ > <details>
45
+ > <summary>ℹ️ <i>See More Information</i></summary>
46
+ >
47
+ > - Set the seed value to "-1" to use a random seed every time
48
+ > - Set any other number in there to use as a static/fixed seed
49
+ > - Quick actions to randomize, or (re-)use the last queued seed.
50
+ > - Images metadata will store the seed value _(so dragging an image in, will have the seed field already fixed to its seed)_.
51
+ > - _Secret Features_: You can manually set the seed value to "-2" or "-3" to increment or decrement the last seed value. If there was not last seed value, it will randomly use on first.
52
+ >
53
+ > ![Router Node](./docs/rgthree_seed.png)
54
+ > </details>
55
+
56
+
57
+ ## Reroute
58
+ > Keep your workflow neat with this much improved Reroute node with, like, actual rerouting with multiple directions and sizes.
59
+ > <details>
60
+ > <summary>ℹ️ <i>More Information</i></summary>
61
+ >
62
+ > - Use the right-click context menu to change the width, height and connection layout
63
+ > - Also toggle resizability (min size is 40x43 if resizing though), and title/type display.
64
+ >
65
+ > ![Router Node](./docs/rgthree_router.png)
66
+ > </details>
67
+
68
+ ## Bookmark (🔖)
69
+ > Place the bookmark node anywhere on screen to quickly navigate to that with a shortcut key.
70
+ > <details>
71
+ > <summary>ℹ️ <i>See More Information</i></summary>
72
+ >
73
+ > - Define the `shortcut_key` to press to go right to that bookmark node, anchored in the top left.
74
+ > - You can also define the zoom level as well!
75
+ > - Pro tip: `shortcut_key` can be multiple keys. For instance "alt + shift + !" would require
76
+ > pressing the alt key, the shift key, and the "!" (as in the "1" key, but with shift pressed)
77
+ > in order to trigger.
78
+ > </details>
79
+
80
+
81
+ ## Context / Context Big
82
+ > Pass along in general flow properties, and merge in new data. Similar to some other node suites "pipes" but easier merging, is more easily interoperable with standard nodes by both combining and exploding all in a single node.
83
+ > <details>
84
+ > <summary>ℹ️ <i>More Information</i></summary>
85
+ >
86
+ > - Context and Context Big are backwards compatible with each other. That is, an input connected to a Context Big will be passed through the CONTEXT outputs through normal Context nodes and available as an output on either (or, Context Big if the output is only on that node, like "steps").
87
+ > - Pro Tip: When dragging a Context output over a nother node, hold down "ctrl" and release to automatically connect the other Context outputs to the hovered node.
88
+ > - Pro Tip: You can change between Context and Context Big nodes from the menu.
89
+ >
90
+ > ![Context Node](./docs/rgthree_context.png)
91
+ > </details>
92
+
93
+ ## Image Comparer
94
+ > The Image Comparer node compares two images on top of each other.
95
+ > <details>
96
+ > <summary>ℹ️ <i>More Information</i></summary>
97
+ >
98
+ > - **Note:** The right-click menu may show image options (Open Image, Save Image, etc.) which will correspond to the first image (image_a) if clicked on the left-half of the node, or the second image if on the right half of the node.
99
+ > - **Inputs:**
100
+ > - `image_a` _Required._ The first image to use to compare. If image_b is not supplied and image_a is a batch, the comparer will use the first two images of image_a.
101
+ > - `image_b` _Optional._ The second image to use to compare. Optional only if image_a is a batch with two images.
102
+ > - **Properties:** You can change the following properties (by right-clicking on the node, and select "Properties" or "Properties Panel" from the menu):
103
+ > - `comparer_mode` - Choose between "Slide" and "Click". Defaults to "Slide".
104
+
105
+
106
+ ## Image Inset Crop
107
+ > The node that lets you crop an input image by either pixel value, or percentage value.
108
+
109
+
110
+ ## Display Any
111
+ > Displays most any piece of text data from the backend _after execution_.
112
+
113
+ ## Power Lora Loader
114
+ > A super-simply Lora Loader node that can load multiple Loras at once, and quick toggle each, all in an ultra-condensed node.
115
+ > <details>
116
+ > <summary>ℹ️ <i>More Information</i></summary>
117
+ >
118
+ > - Add as many Lora's as you would like by clicking the "+ Add Lora" button. There's no real limit!
119
+ > - Right-click on a Lora widget for special options to move the lora up or down
120
+ > _(no affect on image, just presentation)_, toggle it on/off, or delete the row all together.
121
+ > - from the properties, change the `Show Strengths` to choose between showing a single, simple
122
+ > strength value (which will be used for both model and clip), or a more advanced view with
123
+ > both model and clip strengths being modifiable.
124
+ > </details>
125
+
126
+
127
+ ## ~~Lora Loader Stack~~
128
+ > _**Deprecated.** Used the `Power Lora Loader` instead._
129
+ >
130
+ > A simplified Lora Loader stack. Much like other suites, but more interoperable with standard inputs/outputs.
131
+
132
+
133
+ ## Power Prompt
134
+ > Power up your prompt and get drop downs for adding your embeddings, loras, and even have saved prompt snippets.
135
+ > <details>
136
+ > <summary>ℹ️ <i>More Information</i></summary>
137
+ >
138
+ > - At the core, you can use Power Prompt almost as a String Primitive node with additional features of dropdowns for choosing your embeddings, and even loras, with no further processing. This will output just the raw `TEXT` to another node for any lora processing, CLIP Encoding, etc.
139
+ > - Connect a `CLIP` to the input to encode the text, with both the `CLIP` and `CONDITIONING` output right from the node.
140
+ > - Connect a `MODEL` to the input to parse and load any `<lora:...>` tags in the text automatically, without
141
+ > needing a separate Lora Loaders
142
+ > </details>
143
+
144
+ ## Power Prompt - Simple
145
+ > Same as Power Prompt above, but without LORA support; made for a slightly cleaner negative prompt _(since negative prompts do not support loras)_.
146
+
147
+ ## SDXL Power Prompt - Positive
148
+ > The SDXL sibling to the Power Prompt above. It contains the text_g and text_l as separate text inputs, as well a couple more input slots necessary to ensure proper clipe encoding. Combine with
149
+
150
+ ## SDXL Power Prompt - Simple
151
+ > Like the non-SDXL `Power Prompt - Simple` node, this one is essentially the same as the SDXL Power Prompt but without lora support for either non-lora positive prompts or SDXL negative prompts _(since negative prompts do not support loras)_.
152
+
153
+ ## SDXL Config
154
+ > Just some configuration fields for SDXL prompting. Honestly, could be used for non SDXL too.
155
+
156
+ ## Context Switch / Context Switch Big
157
+ > A powerful node to branch your workflow. Works by choosing the first Context input that is not null/empty.
158
+ > <details>
159
+ > <summary>ℹ️ <i>More Information</i></summary>
160
+ >
161
+ > - Pass in several context nodes and the Context Switch will automatically choose the first non-null context to continue onward with.
162
+ > - Wondering how to toggle contexts to null? Use in conjuction with the **Fast Muter** or **Fast Groups Muter**
163
+ >
164
+ > </details>
165
+
166
+ ## Any Switch
167
+ > A powerful node to similar to the Context Switch above, that chooses the first input that is not null/empty.
168
+ > <details>
169
+ > <summary>ℹ️ <i>More Information</i></summary>
170
+ >
171
+ > - Pass in several inmputs of the same type and the Any Switch will automatically choose the first non-null value to continue onward with.
172
+ > - Wondering how to toggle contexts to null? Use in conjuction with the **Fast Muter** or **Fast Groups Muter**
173
+ >
174
+ > </details>
175
+
176
+
177
+ ## Power Primitive
178
+ > A single node that can output primitives (STRING, INT, FLOAT, BOOLEAN). If connecting an input, it will cast/convert the primitive input to the desired output.
179
+ > <details>
180
+ > <summary>ℹ️ <i>More Information</i></summary>
181
+ >
182
+ > - You can hide the type selector input from the right-click menu or properties.
183
+ > - You can also fast-switch the output type from the right-click menu as well.
184
+ >
185
+ > </details>
186
+
187
+
188
+ ## Power Puter
189
+ > A powerful and versatile node that opens the door for a wide range of utility by offering mult-line code parsing for output. This node can be used for simple string concatenation, or math operations; to an image dimension or a node's widgets with advanced list comprehension. If you want to output something in your workflow, this is the node to do it.
190
+ >
191
+ > Additional documentation available in the [wiki](https://github.com/rgthree/rgthree-comfy/wiki/Node:-Power-Puter)
192
+ > <details>
193
+ > <summary>ℹ️ <i>More Information</i></summary>
194
+ >
195
+ > - Evaluate almost any kind of input and more, and choose your output from INT, FLOAT, STRING, or BOOLEAN.
196
+ > - Connect some nodes and do simply math operations like `a + b` or `ceil(1 / 2)`.
197
+ > - Or do more advanced things, like input an image, and get the width like `a.shape[2]`.
198
+ > - Even more powerful, you can target nodes in the prompt that's sent to the backend. For instance; if you have a Power Lora Loader node at id #5, and want to get a comma-delimited list of the enabled loras, you could enter:
199
+ >
200
+ > ```
201
+ > loras = [v.lora for v in node(5).inputs.values() if 'lora' in v and v.on]
202
+ > ', '.join(loras)
203
+ > ```
204
+ >
205
+ > </details>
206
+
207
+ ## Fast Groups Muter
208
+ > The Fast Groups Muter is an input-less node that automatically collects all groups in your current workflow and allows you to quickly mute and unmute all nodes within the group.
209
+ > <details>
210
+ > <summary>ℹ️ <i>More Information</i></summary>
211
+ >
212
+ > - Groups will automatically be shown, though you can filter, sort and more from the **node Properties** _(by right-clicking on the node, and select "Properties" or "Properties Panel" from the menu)_. Properties include:
213
+ > - `matchColors` - Only add groups that match the provided colors. Can be ComfyUI colors (red, pale_blue) or hex codes (#a4d399). Multiple can be added, comma delimited.
214
+ > - `matchTitle` - Filter the list of toggles by title match (string match, or regular expression).
215
+ > - `showNav` - Add / remove a quick navigation arrow to take you to the group. (default: true)
216
+ > - `showAllGraphs` - Show groups from all [sub]graphs in the workflow. (default: true)
217
+ > - `sort` - Sort the toggles' order by "alphanumeric", graph "position", or "custom alphabet". (default: "position")
218
+ > - `customSortAlphabet` - When the sort property is "custom alphabet" you can define the alphabet to use here, which will match the beginning of each group name and sort against it. If group titles do not match any custom alphabet entry, then they will be put after groups that do, ordered alphanumerically.
219
+ >
220
+ > This can be a list of single characters, like "zyxw..." or comma delimited strings for more control, like "sdxl,pro,sd,n,p".
221
+ >
222
+ > Note, when two group title match the same custom alphabet entry, the normal alphanumeric alphabet breaks the tie. For instance, a custom alphabet of "e,s,d" will order groups names like "SDXL, SEGS, Detailer" eventhough the custom alphabet has an "e" before "d" (where one may expect "SE" to be before "SD").
223
+ >
224
+ > To have "SEGS" appear before "SDXL" you can use longer strings. For instance, the custom alphabet value of "se,s,f" would work here.
225
+ > - `toggleRestriction` - Optionally, attempt to restrict the number of widgets that can be enabled to a maximum of one, or always one.
226
+ >
227
+ > _Note: If using "max one" or "always one" then this is only enforced when clicking a toggle on this node; if nodes within groups are changed outside of the initial toggle click, then these restriction will not be enforced, and could result in a state where more than one toggle is enabled. This could also happen if nodes are overlapped with multiple groups._
228
+ > </details>
229
+
230
+ ## Fast Groups Bypasser
231
+ > _Same as **Fast Groups Muter** above, but sets the connected nodes to "Bypass" instead of "Mute"_
232
+
233
+
234
+ ## Fast Muter
235
+ > A powerful 'control panel' node to quickly toggle connected nodes allowing them to quickly be muted or enabled
236
+ > <details>
237
+ > <summary>ℹ️ <i>More Information</i></summary>
238
+ >
239
+ > - Add a collection of all connected nodes allowing a single-spot as a "dashboard" to quickly enable and disable nodes. Two distinct nodes; one for "Muting" connected nodes, and one for "Bypassing" connected nodes.
240
+ > </details>
241
+
242
+
243
+ ## Fast Bypasser
244
+ > Same as Fast Muter but sets the connected nodes to "Bypass"
245
+
246
+ ## Fast Actions Button
247
+ > Oh boy, this node allows you to semi-automate connected nodes and/or ConfyUI.
248
+ > <details>
249
+ > <summary>ℹ️ <i>More Information</i></summary>
250
+ >
251
+ > - Connect nodes and, at the least, mute, bypass or enable them when the button is pressed.
252
+ > - Certain nodes expose additional actions. For instance, the `Seed` node you can set `Randomize Each Time` or `Use Last Queued Seed` when the button is pressed.
253
+ > - Also, from the node properties, set a shortcut key to toggle the button actions, without needing a click!
254
+ > </details>
255
+
256
+
257
+ ## Node Collector
258
+ > Used to cleanup noodles, this will accept any number of input nodes and passes it along to another node.
259
+ >
260
+ > ⚠️ *Currently, this should really only be connected to **Fast Muter**, **Fast Bypasser**, or **Mute / Bypass Relay**.*
261
+
262
+
263
+ ## Mute / Bypass Repeater
264
+ > A powerful node that will dispatch its Mute/Bypass/Active mode to all connected input nodes or, if in a group w/o any connected inputs, will dispatch its Mute/Bypass/Active mode to all nodes in that group.
265
+ > <details>
266
+ > <summary>ℹ️ <i>More Information</i></summary>
267
+ >
268
+ > - 💡 Pro Tip #1: Connect this node's output to a **Fast Muter** or **Fast Bypasser** to have a single toggle there that can mute/bypass/enable many nodes with one click.
269
+ >
270
+ > - 💡 Pro Tip #2: Connect a **Mute / Bypass Relay** node to this node's inputs to have the relay automatically dispatch a mute/bypass/enable change to the repeater.
271
+ > </details>
272
+
273
+
274
+ ## Mute / Bypass Relay
275
+ > An advanced node that, when working with a **Mute / Bypass Repeater**, will relay its input nodes'
276
+ > modes (Mute, Bypass, or Active) to a connected repeater (which would then repeat that mode change
277
+ > to all of its inputs).
278
+ > <details>
279
+ > <summary>ℹ️ <i>More Information</i></summary>
280
+ >
281
+ > - When all connected input nodes are muted, the relay will set a connected repeater to mute (by
282
+ > default).
283
+ > - When all connected input nodes are bypassed, the relay will set a connected repeater to
284
+ > bypass (by default).
285
+ > - When _any_ connected input nodes are active, the relay will set a connected repeater to
286
+ > active (by default).
287
+ > - **Note:** If no inputs are connected, the relay will set a connected repeater to its mode
288
+ > _when its own mode is changed_. **Note**, if any inputs are connected, then the above bullets
289
+ > will occur and the Relay's mode does not matter.
290
+ > - **Pro Tip:** You can change which signals get sent on the above in the `Properties`.
291
+ > For instance, you could configure an inverse relay which will send a MUTE when any of its
292
+ > inputs are active (instead of sending an ACTIVE signal), and send an ACTIVE signal when all
293
+ > of its inputs are muted (instead of sending a MUTE signal), etc.
294
+ > </details>
295
+
296
+
297
+ ## Random Unmuter
298
+ > An advanced node used to unmute one of its inputs randomly when the graph is queued (and, immediately mute it back).
299
+ > <details>
300
+ > <summary>ℹ️ <i>More Information</i></summary>
301
+ >
302
+ > - **Note:** All input nodes MUST be muted to start; if not this node will not randomly unmute another. (This is powerful, as the generated image can be dragged in and the chosen input will already by unmuted and work w/o any further action.)
303
+ > - **Tip:** Connect a Repeater's output to this nodes input and place that Repeater on a group without any other inputs, and it will mute/unmute the entire group.
304
+ > </details>
305
+
306
+
307
+ ## Label
308
+ > A purely visual node, this allows you to add a floating label to your workflow.
309
+ > <details>
310
+ > <summary>ℹ️ <i>More Information</i></summary>
311
+ >
312
+ > - The text shown is the "Title" of the node and you can adjust the the font size, font family,
313
+ > font color, text alignment as well as a background color, padding, background border
314
+ > radius, and angle (in degrees) from the node's properties. You can double-click the node to
315
+ > open the properties panel.
316
+ > - The Title also supports the literal sequence "\\n" to insert a newline when drawing the label.
317
+ > - ~**Pro Tip #1:** You can add multiline text from the properties panel _(because ComfyUI let's
318
+ > you shift + enter there, only)._~
319
+ > - **Pro Tip #2:** You can use ComfyUI's native "pin" option in the right-click menu to make the
320
+ > label stick to the workflow and clicks to "go through". You can right-click at any time to
321
+ > unpin.
322
+ > - **Pro Tip #3:** Color values are hexidecimal strings, like "#FFFFFF" for white, or "#660000"
323
+ > for dark red. You can supply a 7th & 8th value (or 5th if using shorthand) to create a
324
+ > transluscent color. For instance, "#FFFFFF88" is semi-transparent white.
325
+ > </details>
326
+
327
+
328
+ # Advanced Techniques
329
+
330
+ ## First, a word on muting
331
+
332
+ A lot of the power of these nodes comes from *Muting*. Muting is the basis of correctly implementing multiple paths for a workflow utlizing the Context Switch node.
333
+
334
+ While other extensions may provide switches, they often get it wrong causing your workflow to do more work than is needed. While other switches may have a selector to choose which input to pass along, they don't stop the execution of the other inputs, which will result in wasted work. Instead, Context Switch works by choosing the first non-empty context to pass along and correctly Muting is one way to make a previous node empty, and causes no extra work to be done when set up correctly.
335
+
336
+ ### To understand muting, is to understand the graph flow
337
+
338
+ Muting, and therefore using Switches, can often confuse people at first because it _feels_ like muting a node, or using a switch, should be able to stop or direct the _forward_ flow of the graph. However, this is not the case and, in fact, the graph actually starts working backwards.
339
+
340
+ If you have a workflow that has a path like `... > Context > KSampler > VAE Decode > Save Image` it may initially _feel_ like you should be able to mute that first Context node and the graph would stop there when moving forward and skip the rest of that workflow.
341
+
342
+ But you'll quickly find that will cause an error, becase the graph doesn't actually move forward. When a workflow is processed, it _first moves backwards_ starting at each "Output Node" (Preview Image, Save Image, even "Display String" etc.) and then walking backwards to all possible paths to get there.
343
+
344
+ So, with that `... > Context > KSampler > VAE Decode > Save Image` example from above, we actually want to mute the `Save Image` node to stop this path. Once we do, since the output node is gone, none of these nodes will be run.
345
+
346
+ Let's take a look at an example.
347
+
348
+ ### A powerful combination: Using Context, Context Switch, & Fast Muter
349
+
350
+ ![Context Node](./docs/rgthree_advanced.png)
351
+
352
+ 1. Using the **Context Switch** (aqua colored in screenshot) feed context inputs in order of preference. In the workflow above, the `Upscale Out` context is first so, if that one is enabled, it will be chosen for the output. If not, the second input slot which comes from the context rerouted from above (before the Upscaler booth) will be chosen.
353
+
354
+ - Notice the `Upscale Preview` is _after_ the `Upscale Out` context node, using the image from it instead of the image from the upscale `VAE Decoder`. This is on purpose so, when we disable the `Upscale Out` context, none of the Upscaler nodes will run, saving precious GPU cycles. If we had the preview hooked up directly to the `VAE Decoder` the upscaler would always run to generate the preview, even if we had the `Upscale Out` context node disabled.
355
+
356
+ 2. We can now disable the `Upscale Out` context node by _muting_ it. Highlighting it and pressing `ctrl + m` will work. By doing so, it's output will be None, and it will not pass anthing onto the further nodes. In the diagram you can see the `Upscale Preview` is red, but that's OK; there are no actual errors to stop execution.
357
+
358
+ 3. Now, let's hook it up to the `Fast Muter` node. `The Fast Muter` node works as dashboard by adding quick toggles for any connected node (ignoring reroutes). In the diagram, we have both the `Upscaler Out` context node, and the `Save File` context node hooked up. So, we can quickly enable and disable those.
359
+
360
+ - The workflow seen here would be a common one where we can generate a handful of base previews cheaply with a random seed, and then choose one to upscale and save to disk.
361
+
362
+ 4. Lastly, and optionally, you can see the `Node Collector`. Use it to clean up noodles if you want and connect it to the muter. You can connect anything to it, but doing so may break your workflow's execution.
363
+
364
+ <br>
365
+
366
+ # ⚡ Improvements & Features
367
+
368
+ rgthree-comfy adds several improvements, features, and optimizations to ComfyUI that are not directly tied to nodes.
369
+
370
+ ## Progress Bar
371
+ > A minimal progress bar that run alongs the top of the app window that shows the queue size, the current progress of the a prompt execution (within the same window), and the progress of multi-step nodes as well.
372
+ >
373
+ > <i>You can remove/enable from rgthree-comfy settings, as well as configure the height/size.</i>
374
+
375
+
376
+ ## ~~ComfyUI Recursive Optimization~~
377
+ > 🎉 The newest version of ComfyUI no longer suffers from poor execution recursion! This feature
378
+ > has been removed from rgthree-comfy.
379
+
380
+
381
+ ## "Queue Selected Output Nodes" in right-click menu
382
+ > Sometimes you want to just queue one or two paths to specific output node(s) without executing the entire workflow. Well, now you can do just that by right-clicking on an output node and selecting `Queue Selected Output Nodes (rgthree)`.
383
+ >
384
+ > <details>
385
+ > <summary>ℹ️ <i>More Information</i></summary>
386
+ >
387
+ > - Select the _output_ nodes you want to execute.
388
+ >
389
+ > - Note: Only output nodes are captured and traversed, not all selected nodes. So if you select an output AND a node from a different path, only the path connected to the output will be executed and not non-output nodes, even if they were selected.
390
+ >
391
+ > - Note: The whole workflow is serialized, and then we trim what we don't want for the backend. So things like all seed random/increment/decrement will run even if that node isn't being sent in the end, etc.
392
+ >
393
+ > </details>
394
+
395
+
396
+ ## Auto-Nest Subdirectories in long Combos
397
+ > _(Off by default while experimenting, turn on in rgthree-comfy settings)_.
398
+ >
399
+ > Automatically detect top-level subdirectories in long combo lists (like, Load Checkpoint) and break out into sub directories.
400
+
401
+
402
+ ## Quick Mute/Bypass Toggles in Group Headers
403
+ > _(Off by default while experimenting, turn on in rgthree-comfy settings)_.
404
+ >
405
+ > Adds a mute and/or bypass toggle icons in the top-right of Group Headers for one-click toggling of groups you may be currently looking at.
406
+
407
+
408
+ ## Import Individual Node Widgets (Drag & Drop)
409
+ > _(Off by default while experimenting, turn on in rgthree-comfy settings)_.
410
+ >
411
+ > Allows dragging and dropping an image/JSON workflow from a previous generation and overriding the same node's widgets
412
+ > (that match with the same id & type). This is useful if you have several generations using the same general workflow
413
+ > and would like to import just some data, like a previous generation's seed, or prompt, etc.
414
+
415
+
416
+
417
+ ## "Copy Image" in right-click menu
418
+ > Right clicking on a node that has an image should have a context-menu item of "Copy Image" will allow you to copy the image right to your clipboard
419
+ >
420
+ > <i>🎓 I believe this has graduated, with ComfyUI recently adding this setting too. You won't get two menu items; my code checks that there isn't already a "Copy Image" item there before adding it.</i>
421
+
422
+
423
+ ## Other/Smaller Fixes
424
+ - Fixed the width of ultra-wide node chooser on double click.
425
+ - Fixed z-indexes for textareas that would overlap above other elements, like Properties Panel, or @pythongosssss's image viewer.
426
+ - Check for bad links when loading a workflow and log to console, by default. _(See Link Fixer below)._
427
+
428
+ <br>
429
+
430
+ # 📄 Link Fixer
431
+
432
+ If your workflows sometimes have missing connections, or even errors on load, start up ComfyUI and go to http://127.0.0.1:8188/rgthree/link_fixer which will allow you to drop in an image or workflow json file and check for and fix any bad links.
433
+
434
+ You can also enable a link fixer check in the rgthree-comfy settings to give you an alert if you load a workflow with bad linking data to start.
custom_nodes/rgthree-comfy/__build__.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import subprocess
4
+ import os
5
+ from shutil import rmtree, copytree, ignore_patterns
6
+ from glob import glob
7
+ import time
8
+ import re
9
+ import argparse
10
+
11
+ from py.log import COLORS
12
+ from py.config import RGTHREE_CONFIG
13
+
14
+ step_msg = ''
15
+ step_start = 0
16
+ step_infos = []
17
+
18
+ def log_step(msg=None, status=None):
19
+ """ Logs a step keeping track of timing and initial msg. """
20
+ global step_msg # pylint: disable=W0601
21
+ global step_start # pylint: disable=W0601
22
+ global step_infos # pylint: disable=W0601
23
+ if msg:
24
+ tag = f'{COLORS["YELLOW"]}[ Notice ]' if status == 'Notice' else f'{COLORS["RESET"]}[Starting]'
25
+ step_msg = f'▻ {tag}{COLORS["RESET"]} {msg}...'
26
+ step_start = time.time()
27
+ step_infos = []
28
+ print(step_msg, end="\r")
29
+ elif status:
30
+ if status != 'Error':
31
+ warnings = [w for w in step_infos if w["type"] == 'warn']
32
+ status = "Warn" if warnings else status
33
+ step_time = round(time.time() - step_start, 3)
34
+ if status == 'Error':
35
+ status_msg = f'{COLORS["RED"]}⤫ {status}{COLORS["RESET"]}'
36
+ elif status == 'Warn':
37
+ status_msg = f'{COLORS["YELLOW"]}! {status}{COLORS["RESET"]}'
38
+ else:
39
+ status_msg = f'{COLORS["BRIGHT_GREEN"]}🗸 {status}{COLORS["RESET"]}'
40
+ print(f'{step_msg.ljust(64, ".")} {status_msg} ({step_time}s)')
41
+ for info in step_infos:
42
+ print(info["msg"])
43
+
44
+ def log_step_info(msg:str, status='info'):
45
+ global step_infos # pylint: disable=W0601
46
+ step_infos.append({"msg": f' - {msg}', "type": status})
47
+
48
+
49
+ def build(without_tests = True, fix = False):
50
+
51
+ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
52
+ DIR_SRC_WEB = os.path.abspath(f'{THIS_DIR}/src_web/')
53
+ DIR_WEB = os.path.abspath(f'{THIS_DIR}/web/')
54
+ DIR_WEB_COMFYUI = os.path.abspath(f'{DIR_WEB}/comfyui/')
55
+
56
+ if fix:
57
+ tss = glob(os.path.join(DIR_SRC_WEB, "**", "*.ts"), recursive=True)
58
+ log_step(msg=f'Fixing {len(tss)} ts files')
59
+ for ts in tss:
60
+ with open(ts, 'r', encoding="utf-8") as f:
61
+ content = f.read()
62
+ # (\s*from\s*['"](?!.*[.]js['"]).*?)(['"];) in vscode.
63
+ content, n = re.subn(r'(\s*from [\'"](?!.*[.]js[\'"]).*?)([\'"];)', '\\1.js\\2', content)
64
+ if n > 0:
65
+ filename = os.path.basename(ts)
66
+ log_step_info(
67
+ f'{filename} has {n} import{"s" if n > 1 else ""} that do not end in ".js"', 'warn')
68
+ with open(ts, 'w', encoding="utf-8") as f:
69
+ f.write(content)
70
+ log_step(status="Done")
71
+
72
+ log_step(msg='Copying web directory')
73
+ rmtree(DIR_WEB)
74
+ copytree(DIR_SRC_WEB, DIR_WEB, ignore=ignore_patterns("typings*", "*.ts", "*.scss"))
75
+ log_step(status="Done")
76
+
77
+ ts_version_result = subprocess.run(["node", "./node_modules/typescript/bin/tsc", "-v"],
78
+ capture_output=True,
79
+ text=True,
80
+ check=True)
81
+ ts_version = re.sub(r'^.*Version\s*([\d\.]+).*', 'v\\1', ts_version_result.stdout, flags=re.DOTALL)
82
+
83
+ log_step(msg=f'TypeScript ({ts_version})')
84
+ checked = subprocess.run(["node", "./node_modules/typescript/bin/tsc"], check=True)
85
+ log_step(status="Done")
86
+
87
+ if not without_tests:
88
+ log_step(msg='Removing directories (KEEPING TESTING)', status="Notice")
89
+ else:
90
+ log_step(msg='Removing unneeded directories')
91
+ test_path = os.path.join(DIR_WEB, 'comfyui', 'tests')
92
+ if os.path.exists(test_path):
93
+ rmtree(test_path)
94
+ rmtree(os.path.join(DIR_WEB, 'comfyui', 'testing'))
95
+ # Always remove the dummy scripts_comfy directory
96
+ rmtree(os.path.join(DIR_WEB, 'scripts_comfy'))
97
+ log_step(status="Done")
98
+
99
+ scsss = glob(os.path.join(DIR_SRC_WEB, "**", "*.scss"), recursive=True)
100
+ log_step(msg=f'SASS for {len(scsss)} files')
101
+ scsss = [i.replace(THIS_DIR, '.') for i in scsss]
102
+ cmds = ["node", "./node_modules/sass/sass"]
103
+ for scss in scsss:
104
+ out = scss.replace('src_web', 'web').replace('.scss', '.css')
105
+ cmds.append(f'{scss}:{out}')
106
+ cmds.append('--no-source-map')
107
+ checked = subprocess.run(cmds, check=True)
108
+ log_step(status="Done")
109
+
110
+ # Handle the common directories. Because ComfyUI loads under /extensions/rgthree-comfy we can't
111
+ # easily share sources outside of the `DIR_WEB_COMFYUI` _and_ allow typescript to resolve them in
112
+ # src view, so we set the path in the tsconfig to map an import of "rgthree/common" to the
113
+ # "src_web/common" directory, but then need to rewrite the comfyui JS files to load from
114
+ # "../../rgthree/common" (which we map correctly in rgthree_server.py).
115
+ log_step(msg='Cleaning Imports')
116
+ js_files = glob(os.path.join(DIR_WEB, '**', '*.js'), recursive=True)
117
+ for file in js_files:
118
+ rel_path = file.replace(f'{DIR_WEB}/', "")
119
+ with open(file, 'r', encoding="utf-8") as f:
120
+ filedata = f.read()
121
+ num = rel_path.count(os.sep)
122
+ if rel_path.startswith('comfyui'):
123
+ filedata = re.sub(r'(from\s+["\'])rgthree/', f'\\1{"../" * (num + 1)}rgthree/', filedata)
124
+ filedata = re.sub(r'(from\s+["\'])scripts/', f'\\1{"../" * (num + 1)}scripts/', filedata)
125
+ # Dynamic Imports
126
+ filedata = re.sub(r'(import\(["\'])rgthree/', f'\\1{"../" * (num + 1)}rgthree/', filedata)
127
+ else:
128
+ filedata = re.sub(r'(from\s+["\'])rgthree/', f'\\1{"../" * num}', filedata)
129
+ filedata = re.sub(r'(from\s+["\'])scripts/', f'\\1{"../" * (num + 1)}scripts/', filedata)
130
+ # Dynamic Imports
131
+ filedata = re.sub(r'(import\(["\'])rgthree/', f'\\1{"../" * num}', filedata)
132
+
133
+ filedata, n = re.subn(r'(\s*from [\'"](?!.*[.]js[\'"]).*?)([\'"];)', '\\1.js\\2', filedata)
134
+ if n > 0:
135
+ filename = file.split('rgthree-comfy')[1]
136
+ log_step_info(
137
+ f'{filename} has {n} import{"s" if n > 1 else ""} that do not end in ".js"', 'warn')
138
+ with open(file, 'w', encoding="utf-8") as f:
139
+ f.write(filedata)
140
+ log_step(status="Done")
141
+
142
+
143
+ if __name__ == "__main__":
144
+ parser = argparse.ArgumentParser()
145
+ parser.add_argument("-t", "--no-tests", default=False, action="store_true")
146
+ parser.add_argument("-f", "--fix", default=False, action="store_true")
147
+ args = parser.parse_args()
148
+
149
+ start = time.time()
150
+ build(without_tests=args.no_tests, fix=args.fix)
151
+ print(f'Finished all in {round(time.time() - start, 3)}s')
custom_nodes/rgthree-comfy/__commit__.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import subprocess
4
+ import os
5
+ import re
6
+ import datetime
7
+ import time
8
+ import argparse
9
+
10
+ from __build__ import build, log_step, log_step_info
11
+
12
+ _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
13
+ _FILE_PY_PROJECT = os.path.join(_THIS_DIR, 'pyproject.toml')
14
+
15
+ parser = argparse.ArgumentParser()
16
+ parser.add_argument(
17
+ "-m", "--message", help="The git commit message", required=True, action="store", type=str
18
+ )
19
+ args = parser.parse_args()
20
+
21
+ start = time.time()
22
+ build()
23
+
24
+ log_step(msg='Updating version in pyproject.toml')
25
+ py_project = ''
26
+ with open(_FILE_PY_PROJECT, "r", encoding='utf-8') as f:
27
+ py_project = f.read()
28
+
29
+ version = re.search(r'^\s*version\s*=\s*"(.*?)"', py_project, flags=re.MULTILINE)
30
+ version_old = version[1]
31
+
32
+ now = datetime.datetime.now()
33
+ version_new = version_old.split('.')
34
+ version_new[-1] = f'{str(now.year)[2:]}{now.month:02}{now.day:02}{now.hour:02}{now.minute:02}'
35
+ version_new = '.'.join(version_new)
36
+
37
+ log_step_info(f'Updating from v{version_old} to v{version_new}')
38
+ py_project = py_project.replace(version_old, version_new)
39
+ with open(_FILE_PY_PROJECT, "w", encoding='utf-8') as f:
40
+ f.write(py_project)
41
+ log_step(status="Done")
42
+
43
+ log_step('Running git add')
44
+ process = subprocess.Popen(['git', 'add', '.'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
45
+ stdout, stderr = process.communicate()
46
+ log_step(status="Done")
47
+
48
+ log_step('Running git commit')
49
+ process = subprocess.Popen(['git', 'commit', '-a', '-v', '-m', args.message],
50
+ stdout=subprocess.PIPE,
51
+ stderr=subprocess.PIPE)
52
+ stdout, stderr = process.communicate()
53
+ log_step(status="Done")
54
+
55
+ print(f'Finished all in {round(time.time() - start, 3)}s')
custom_nodes/rgthree-comfy/__init__.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ @author: rgthree
3
+ @title: Comfy Nodes
4
+ @nickname: rgthree
5
+ @description: A bunch of nodes I created that I also find useful.
6
+ """
7
+
8
+ from glob import glob
9
+ import json
10
+ import os
11
+ import shutil
12
+ import re
13
+ import random
14
+
15
+ import execution
16
+
17
+ from .py.log import log
18
+ from .py.config import get_config_value
19
+ from .py.server.rgthree_server import *
20
+
21
+ from .py.context import RgthreeContext
22
+ from .py.context_switch import RgthreeContextSwitch
23
+ from .py.context_switch_big import RgthreeContextSwitchBig
24
+ from .py.display_any import RgthreeDisplayAny, RgthreeDisplayInt
25
+ from .py.lora_stack import RgthreeLoraLoaderStack
26
+ from .py.seed import RgthreeSeed
27
+ from .py.sdxl_empty_latent_image import RgthreeSDXLEmptyLatentImage
28
+ from .py.power_prompt import RgthreePowerPrompt
29
+ from .py.power_prompt_simple import RgthreePowerPromptSimple
30
+ from .py.image_inset_crop import RgthreeImageInsetCrop
31
+ from .py.context_big import RgthreeBigContext
32
+ from .py.dynamic_context import RgthreeDynamicContext
33
+ from .py.dynamic_context_switch import RgthreeDynamicContextSwitch
34
+ from .py.ksampler_config import RgthreeKSamplerConfig
35
+ from .py.sdxl_power_prompt_postive import RgthreeSDXLPowerPromptPositive
36
+ from .py.sdxl_power_prompt_simple import RgthreeSDXLPowerPromptSimple
37
+ from .py.any_switch import RgthreeAnySwitch
38
+ from .py.context_merge import RgthreeContextMerge
39
+ from .py.context_merge_big import RgthreeContextMergeBig
40
+ from .py.image_comparer import RgthreeImageComparer
41
+ from .py.power_lora_loader import RgthreePowerLoraLoader
42
+ from .py.power_primitive import RgthreePowerPrimitive
43
+ from .py.image_or_latent_size import RgthreeImageOrLatentSize
44
+ from .py.image_resize import RgthreeImageResize
45
+ from .py.power_puter import RgthreePowerPuter
46
+
47
+ NODE_CLASS_MAPPINGS = {
48
+ RgthreeBigContext.NAME: RgthreeBigContext,
49
+ RgthreeContext.NAME: RgthreeContext,
50
+ RgthreeContextSwitch.NAME: RgthreeContextSwitch,
51
+ RgthreeContextSwitchBig.NAME: RgthreeContextSwitchBig,
52
+ RgthreeContextMerge.NAME: RgthreeContextMerge,
53
+ RgthreeContextMergeBig.NAME: RgthreeContextMergeBig,
54
+ RgthreeDisplayInt.NAME: RgthreeDisplayInt,
55
+ RgthreeDisplayAny.NAME: RgthreeDisplayAny,
56
+ RgthreeLoraLoaderStack.NAME: RgthreeLoraLoaderStack,
57
+ RgthreeSeed.NAME: RgthreeSeed,
58
+ RgthreeImageInsetCrop.NAME: RgthreeImageInsetCrop,
59
+ RgthreePowerPrompt.NAME: RgthreePowerPrompt,
60
+ RgthreePowerPromptSimple.NAME: RgthreePowerPromptSimple,
61
+ RgthreeKSamplerConfig.NAME: RgthreeKSamplerConfig,
62
+ RgthreeSDXLEmptyLatentImage.NAME: RgthreeSDXLEmptyLatentImage,
63
+ RgthreeSDXLPowerPromptPositive.NAME: RgthreeSDXLPowerPromptPositive,
64
+ RgthreeSDXLPowerPromptSimple.NAME: RgthreeSDXLPowerPromptSimple,
65
+ RgthreeAnySwitch.NAME: RgthreeAnySwitch,
66
+ RgthreeImageComparer.NAME: RgthreeImageComparer,
67
+ RgthreePowerLoraLoader.NAME: RgthreePowerLoraLoader,
68
+ RgthreePowerPrimitive.NAME: RgthreePowerPrimitive,
69
+ RgthreeImageOrLatentSize.NAME: RgthreeImageOrLatentSize,
70
+ RgthreeImageResize.NAME: RgthreeImageResize,
71
+ RgthreePowerPuter.NAME: RgthreePowerPuter,
72
+ }
73
+
74
+ if get_config_value('unreleased.dynamic_context.enabled') is True:
75
+ NODE_CLASS_MAPPINGS[RgthreeDynamicContext.NAME] = RgthreeDynamicContext
76
+ NODE_CLASS_MAPPINGS[RgthreeDynamicContextSwitch.NAME] = RgthreeDynamicContextSwitch
77
+
78
+ # WEB_DIRECTORY is the comfyui nodes directory that ComfyUI will link and auto-load.
79
+ WEB_DIRECTORY = "./web/comfyui"
80
+
81
+ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
82
+ DIR_WEB = os.path.abspath(f'{THIS_DIR}/{WEB_DIRECTORY}')
83
+ DIR_PY = os.path.abspath(f'{THIS_DIR}/py')
84
+
85
+ # remove old directories
86
+ OLD_DIRS = [
87
+ os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree'),
88
+ os.path.abspath(f'{THIS_DIR}/../../web/extensions/rgthree-comfy'),
89
+ ]
90
+ for old_dir in OLD_DIRS:
91
+ if os.path.exists(old_dir):
92
+ shutil.rmtree(old_dir)
93
+
94
+ __all__ = ['NODE_CLASS_MAPPINGS', 'WEB_DIRECTORY']
95
+
96
+ NOT_NODES = ['constants', 'log', 'utils', 'rgthree', 'rgthree_server', 'image_clipbaord', 'config']
97
+
98
+ nodes = []
99
+ for file in glob(os.path.join(DIR_PY, '*.py')) + glob(os.path.join(DIR_WEB, '*.js')):
100
+ name = os.path.splitext(os.path.basename(file))[0]
101
+ if name in NOT_NODES or name in nodes:
102
+ continue
103
+ if name.startswith('_') or name.startswith('base') or 'utils' in name:
104
+ continue
105
+ nodes.append(name)
106
+ if name == 'display_any':
107
+ nodes.append('display_int')
108
+
109
+ print()
110
+ adjs = ['exciting', 'extraordinary', 'epic', 'fantastic', 'magnificent']
111
+ log(f'Loaded {len(nodes)} {random.choice(adjs)} nodes. 🎉', color='BRIGHT_GREEN')
112
+ print()
113
+
114
+ if get_config_value('announcements.comfy-nodes-20.incompatible', True):
115
+ message = (
116
+ "ComfyUI's new Node 2.0 rendering may be incompatible with some rgthree-comfy nodes "
117
+ "and features, breaking some rendering as well as losing the ability to "
118
+ "access a node's properties (a vital part of many nodes). It also appears to run MUCH more "
119
+ "slowly spiking CPU usage and causing jankiness and unresponsiveness, especially with large "
120
+ "workflows. Personally I am not planning to use the new Nodes 2.0 and, unfortunately, am not "
121
+ "able to invest the time to investigate and overhaul rgthree-comfy where needed. "
122
+ "If you have issues when Nodes 2.0 is enabled, I'd urge you to switch it off as well and "
123
+ "join me in hoping ComfyUI is not planning to deprecate the existing, stable canvas rendering "
124
+ "all together.\n"
125
+ )
126
+ log(message, color='YELLOW', id='announcements.comfy-nodes-20.incompatible', at_most_secs=60)
custom_nodes/rgthree-comfy/__update_comfy__.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ # A nicer output for git pulling custom nodes (and ComfyUI).
4
+ # Quick shell version: ls | xargs -I % sh -c 'echo; echo %; git -C % pull'
5
+
6
+ import os
7
+ from subprocess import Popen, PIPE, STDOUT
8
+
9
+
10
+ def pull_path(path):
11
+ p = Popen(["git", "-C", path, "pull"], stdout=PIPE, stderr=STDOUT)
12
+ output, error = p.communicate()
13
+ return output.decode()
14
+
15
+ THIS_DIR=os.path.dirname(os.path.abspath(__file__))
16
+
17
+ def show_output(output):
18
+ if output.startswith('Already up to date'):
19
+ print(f' \33[32m🗸 {output}\33[0m', end ='')
20
+ elif output.startswith('error:'):
21
+ print(f' \33[31m🞫 Error.\33[0m \n {output}')
22
+ else:
23
+ print(f' \33[33m🡅 Needs update.\33[0m \n {output}', end='')
24
+
25
+
26
+ os.chdir(THIS_DIR)
27
+ os.chdir("../")
28
+
29
+ # Get the list or custom nodes, so we can format the output a little more nicely.
30
+ custom_extensions = []
31
+ custom_extensions_name_max = 0
32
+ for directory in os.listdir(os.getcwd()):
33
+ if os.path.isdir(directory) and directory != "__pycache__": #and directory != "rgthree-comfy" :
34
+ custom_extensions.append({
35
+ 'directory': directory
36
+ })
37
+ if len(directory) > custom_extensions_name_max:
38
+ custom_extensions_name_max = len(directory)
39
+
40
+ if len(custom_extensions) == 0:
41
+ custom_extensions_name_max = 15
42
+ else:
43
+ custom_extensions_name_max += 6
44
+
45
+ # Update ComfyUI itself.
46
+ label = "{0:.<{max}}".format('Updating ComfyUI ', max=custom_extensions_name_max)
47
+ print(label, end = '')
48
+ show_output(pull_path('../'))
49
+
50
+ # If we have custom nodes, update them as well.
51
+ if len(custom_extensions) > 0:
52
+ print(f'\nUpdating custom_nodes ({len(custom_extensions)}):')
53
+ for custom_extension in custom_extensions:
54
+ directory = custom_extension['directory']
55
+ label = "{0:.<{max}}".format(f'🗀 {directory} ', max=custom_extensions_name_max)
56
+ print(label, end = '')
57
+ show_output(pull_path(directory))
custom_nodes/rgthree-comfy/docs/rgthree_advanced.png ADDED

Git LFS Details

  • SHA256: 77d88a50847fa76d95a1470bf0315b035a06e3c87a8d4e8132d49d8d5b8a31ce
  • Pointer size: 131 Bytes
  • Size of remote file: 456 kB
custom_nodes/rgthree-comfy/docs/rgthree_advanced_metadata.png ADDED

Git LFS Details

  • SHA256: c33be251bd628225b9e29249b766b45539a62a9f94f2e0085b10c469f5ef0956
  • Pointer size: 131 Bytes
  • Size of remote file: 491 kB
custom_nodes/rgthree-comfy/docs/rgthree_context.png ADDED

Git LFS Details

  • SHA256: 4d7c4401b0f4b6958d75b9eca531b0c16d8eeb121f3ec8092ccf1336966c43cd
  • Pointer size: 131 Bytes
  • Size of remote file: 544 kB
custom_nodes/rgthree-comfy/docs/rgthree_context_metadata.png ADDED

Git LFS Details

  • SHA256: 0f89ef9decfee6b65831780a85235f6bfedd99ff1f16eaaf0567a17d94997ba7
  • Pointer size: 132 Bytes
  • Size of remote file: 1.69 MB
custom_nodes/rgthree-comfy/docs/rgthree_router.png ADDED
custom_nodes/rgthree-comfy/docs/rgthree_seed.png ADDED
custom_nodes/rgthree-comfy/package-lock.json ADDED
@@ -0,0 +1,568 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "rgthree-comfy",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "devDependencies": {
8
+ "@comfyorg/comfyui-frontend-types": "^1.32.4",
9
+ "prettier": "^3.3.3",
10
+ "sass": "^1.77.8",
11
+ "typescript": "^5.5.4",
12
+ "web-tree-sitter": "0.25.6"
13
+ }
14
+ },
15
+ "node_modules/@babel/helper-string-parser": {
16
+ "version": "7.25.9",
17
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
18
+ "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
19
+ "dev": true,
20
+ "peer": true,
21
+ "engines": {
22
+ "node": ">=6.9.0"
23
+ }
24
+ },
25
+ "node_modules/@babel/helper-validator-identifier": {
26
+ "version": "7.25.9",
27
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
28
+ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
29
+ "dev": true,
30
+ "peer": true,
31
+ "engines": {
32
+ "node": ">=6.9.0"
33
+ }
34
+ },
35
+ "node_modules/@babel/parser": {
36
+ "version": "7.27.0",
37
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
38
+ "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
39
+ "dev": true,
40
+ "peer": true,
41
+ "dependencies": {
42
+ "@babel/types": "^7.27.0"
43
+ },
44
+ "bin": {
45
+ "parser": "bin/babel-parser.js"
46
+ },
47
+ "engines": {
48
+ "node": ">=6.0.0"
49
+ }
50
+ },
51
+ "node_modules/@babel/types": {
52
+ "version": "7.27.0",
53
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
54
+ "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
55
+ "dev": true,
56
+ "peer": true,
57
+ "dependencies": {
58
+ "@babel/helper-string-parser": "^7.25.9",
59
+ "@babel/helper-validator-identifier": "^7.25.9"
60
+ },
61
+ "engines": {
62
+ "node": ">=6.9.0"
63
+ }
64
+ },
65
+ "node_modules/@comfyorg/comfyui-frontend-types": {
66
+ "version": "1.32.4",
67
+ "resolved": "https://registry.npmjs.org/@comfyorg/comfyui-frontend-types/-/comfyui-frontend-types-1.32.4.tgz",
68
+ "integrity": "sha512-9EBFwQPaSMLI151caWN5nA0qVmY5S9BR9lHP5NboPnTsmkFwnWdrAPadAnafwmZcK40ZWntKJZxO0Uuju0jw/w==",
69
+ "dev": true,
70
+ "peerDependencies": {
71
+ "vue": "^3.5.13",
72
+ "zod": "^3.23.8"
73
+ }
74
+ },
75
+ "node_modules/@jridgewell/sourcemap-codec": {
76
+ "version": "1.5.0",
77
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
78
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
79
+ "dev": true,
80
+ "peer": true
81
+ },
82
+ "node_modules/@vue/compiler-core": {
83
+ "version": "3.5.13",
84
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
85
+ "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
86
+ "dev": true,
87
+ "peer": true,
88
+ "dependencies": {
89
+ "@babel/parser": "^7.25.3",
90
+ "@vue/shared": "3.5.13",
91
+ "entities": "^4.5.0",
92
+ "estree-walker": "^2.0.2",
93
+ "source-map-js": "^1.2.0"
94
+ }
95
+ },
96
+ "node_modules/@vue/compiler-dom": {
97
+ "version": "3.5.13",
98
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
99
+ "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
100
+ "dev": true,
101
+ "peer": true,
102
+ "dependencies": {
103
+ "@vue/compiler-core": "3.5.13",
104
+ "@vue/shared": "3.5.13"
105
+ }
106
+ },
107
+ "node_modules/@vue/compiler-sfc": {
108
+ "version": "3.5.13",
109
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
110
+ "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
111
+ "dev": true,
112
+ "peer": true,
113
+ "dependencies": {
114
+ "@babel/parser": "^7.25.3",
115
+ "@vue/compiler-core": "3.5.13",
116
+ "@vue/compiler-dom": "3.5.13",
117
+ "@vue/compiler-ssr": "3.5.13",
118
+ "@vue/shared": "3.5.13",
119
+ "estree-walker": "^2.0.2",
120
+ "magic-string": "^0.30.11",
121
+ "postcss": "^8.4.48",
122
+ "source-map-js": "^1.2.0"
123
+ }
124
+ },
125
+ "node_modules/@vue/compiler-ssr": {
126
+ "version": "3.5.13",
127
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
128
+ "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
129
+ "dev": true,
130
+ "peer": true,
131
+ "dependencies": {
132
+ "@vue/compiler-dom": "3.5.13",
133
+ "@vue/shared": "3.5.13"
134
+ }
135
+ },
136
+ "node_modules/@vue/reactivity": {
137
+ "version": "3.5.13",
138
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
139
+ "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
140
+ "dev": true,
141
+ "peer": true,
142
+ "dependencies": {
143
+ "@vue/shared": "3.5.13"
144
+ }
145
+ },
146
+ "node_modules/@vue/runtime-core": {
147
+ "version": "3.5.13",
148
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
149
+ "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
150
+ "dev": true,
151
+ "peer": true,
152
+ "dependencies": {
153
+ "@vue/reactivity": "3.5.13",
154
+ "@vue/shared": "3.5.13"
155
+ }
156
+ },
157
+ "node_modules/@vue/runtime-dom": {
158
+ "version": "3.5.13",
159
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
160
+ "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
161
+ "dev": true,
162
+ "peer": true,
163
+ "dependencies": {
164
+ "@vue/reactivity": "3.5.13",
165
+ "@vue/runtime-core": "3.5.13",
166
+ "@vue/shared": "3.5.13",
167
+ "csstype": "^3.1.3"
168
+ }
169
+ },
170
+ "node_modules/@vue/server-renderer": {
171
+ "version": "3.5.13",
172
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
173
+ "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
174
+ "dev": true,
175
+ "peer": true,
176
+ "dependencies": {
177
+ "@vue/compiler-ssr": "3.5.13",
178
+ "@vue/shared": "3.5.13"
179
+ },
180
+ "peerDependencies": {
181
+ "vue": "3.5.13"
182
+ }
183
+ },
184
+ "node_modules/@vue/shared": {
185
+ "version": "3.5.13",
186
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
187
+ "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
188
+ "dev": true,
189
+ "peer": true
190
+ },
191
+ "node_modules/anymatch": {
192
+ "version": "3.1.3",
193
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
194
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
195
+ "dev": true,
196
+ "dependencies": {
197
+ "normalize-path": "^3.0.0",
198
+ "picomatch": "^2.0.4"
199
+ },
200
+ "engines": {
201
+ "node": ">= 8"
202
+ }
203
+ },
204
+ "node_modules/binary-extensions": {
205
+ "version": "2.2.0",
206
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
207
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
208
+ "dev": true,
209
+ "engines": {
210
+ "node": ">=8"
211
+ }
212
+ },
213
+ "node_modules/braces": {
214
+ "version": "3.0.3",
215
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
216
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
217
+ "dev": true,
218
+ "dependencies": {
219
+ "fill-range": "^7.1.1"
220
+ },
221
+ "engines": {
222
+ "node": ">=8"
223
+ }
224
+ },
225
+ "node_modules/chokidar": {
226
+ "version": "3.5.3",
227
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
228
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
229
+ "dev": true,
230
+ "funding": [
231
+ {
232
+ "type": "individual",
233
+ "url": "https://paulmillr.com/funding/"
234
+ }
235
+ ],
236
+ "dependencies": {
237
+ "anymatch": "~3.1.2",
238
+ "braces": "~3.0.2",
239
+ "glob-parent": "~5.1.2",
240
+ "is-binary-path": "~2.1.0",
241
+ "is-glob": "~4.0.1",
242
+ "normalize-path": "~3.0.0",
243
+ "readdirp": "~3.6.0"
244
+ },
245
+ "engines": {
246
+ "node": ">= 8.10.0"
247
+ },
248
+ "optionalDependencies": {
249
+ "fsevents": "~2.3.2"
250
+ }
251
+ },
252
+ "node_modules/csstype": {
253
+ "version": "3.1.3",
254
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
255
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
256
+ "dev": true,
257
+ "peer": true
258
+ },
259
+ "node_modules/entities": {
260
+ "version": "4.5.0",
261
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
262
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
263
+ "dev": true,
264
+ "peer": true,
265
+ "engines": {
266
+ "node": ">=0.12"
267
+ },
268
+ "funding": {
269
+ "url": "https://github.com/fb55/entities?sponsor=1"
270
+ }
271
+ },
272
+ "node_modules/estree-walker": {
273
+ "version": "2.0.2",
274
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
275
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
276
+ "dev": true,
277
+ "peer": true
278
+ },
279
+ "node_modules/fill-range": {
280
+ "version": "7.1.1",
281
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
282
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
283
+ "dev": true,
284
+ "dependencies": {
285
+ "to-regex-range": "^5.0.1"
286
+ },
287
+ "engines": {
288
+ "node": ">=8"
289
+ }
290
+ },
291
+ "node_modules/fsevents": {
292
+ "version": "2.3.3",
293
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
294
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
295
+ "dev": true,
296
+ "hasInstallScript": true,
297
+ "optional": true,
298
+ "os": [
299
+ "darwin"
300
+ ],
301
+ "engines": {
302
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
303
+ }
304
+ },
305
+ "node_modules/glob-parent": {
306
+ "version": "5.1.2",
307
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
308
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
309
+ "dev": true,
310
+ "dependencies": {
311
+ "is-glob": "^4.0.1"
312
+ },
313
+ "engines": {
314
+ "node": ">= 6"
315
+ }
316
+ },
317
+ "node_modules/immutable": {
318
+ "version": "4.3.5",
319
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
320
+ "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==",
321
+ "dev": true
322
+ },
323
+ "node_modules/is-binary-path": {
324
+ "version": "2.1.0",
325
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
326
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
327
+ "dev": true,
328
+ "dependencies": {
329
+ "binary-extensions": "^2.0.0"
330
+ },
331
+ "engines": {
332
+ "node": ">=8"
333
+ }
334
+ },
335
+ "node_modules/is-extglob": {
336
+ "version": "2.1.1",
337
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
338
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
339
+ "dev": true,
340
+ "engines": {
341
+ "node": ">=0.10.0"
342
+ }
343
+ },
344
+ "node_modules/is-glob": {
345
+ "version": "4.0.3",
346
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
347
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
348
+ "dev": true,
349
+ "dependencies": {
350
+ "is-extglob": "^2.1.1"
351
+ },
352
+ "engines": {
353
+ "node": ">=0.10.0"
354
+ }
355
+ },
356
+ "node_modules/is-number": {
357
+ "version": "7.0.0",
358
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
359
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
360
+ "dev": true,
361
+ "engines": {
362
+ "node": ">=0.12.0"
363
+ }
364
+ },
365
+ "node_modules/magic-string": {
366
+ "version": "0.30.17",
367
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
368
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
369
+ "dev": true,
370
+ "peer": true,
371
+ "dependencies": {
372
+ "@jridgewell/sourcemap-codec": "^1.5.0"
373
+ }
374
+ },
375
+ "node_modules/nanoid": {
376
+ "version": "3.3.11",
377
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
378
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
379
+ "dev": true,
380
+ "funding": [
381
+ {
382
+ "type": "github",
383
+ "url": "https://github.com/sponsors/ai"
384
+ }
385
+ ],
386
+ "peer": true,
387
+ "bin": {
388
+ "nanoid": "bin/nanoid.cjs"
389
+ },
390
+ "engines": {
391
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
392
+ }
393
+ },
394
+ "node_modules/normalize-path": {
395
+ "version": "3.0.0",
396
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
397
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
398
+ "dev": true,
399
+ "engines": {
400
+ "node": ">=0.10.0"
401
+ }
402
+ },
403
+ "node_modules/picocolors": {
404
+ "version": "1.1.1",
405
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
406
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
407
+ "dev": true,
408
+ "peer": true
409
+ },
410
+ "node_modules/picomatch": {
411
+ "version": "2.3.1",
412
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
413
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
414
+ "dev": true,
415
+ "engines": {
416
+ "node": ">=8.6"
417
+ },
418
+ "funding": {
419
+ "url": "https://github.com/sponsors/jonschlinkert"
420
+ }
421
+ },
422
+ "node_modules/postcss": {
423
+ "version": "8.5.3",
424
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
425
+ "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
426
+ "dev": true,
427
+ "funding": [
428
+ {
429
+ "type": "opencollective",
430
+ "url": "https://opencollective.com/postcss/"
431
+ },
432
+ {
433
+ "type": "tidelift",
434
+ "url": "https://tidelift.com/funding/github/npm/postcss"
435
+ },
436
+ {
437
+ "type": "github",
438
+ "url": "https://github.com/sponsors/ai"
439
+ }
440
+ ],
441
+ "peer": true,
442
+ "dependencies": {
443
+ "nanoid": "^3.3.8",
444
+ "picocolors": "^1.1.1",
445
+ "source-map-js": "^1.2.1"
446
+ },
447
+ "engines": {
448
+ "node": "^10 || ^12 || >=14"
449
+ }
450
+ },
451
+ "node_modules/prettier": {
452
+ "version": "3.3.3",
453
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
454
+ "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
455
+ "dev": true,
456
+ "bin": {
457
+ "prettier": "bin/prettier.cjs"
458
+ },
459
+ "engines": {
460
+ "node": ">=14"
461
+ },
462
+ "funding": {
463
+ "url": "https://github.com/prettier/prettier?sponsor=1"
464
+ }
465
+ },
466
+ "node_modules/readdirp": {
467
+ "version": "3.6.0",
468
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
469
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
470
+ "dev": true,
471
+ "dependencies": {
472
+ "picomatch": "^2.2.1"
473
+ },
474
+ "engines": {
475
+ "node": ">=8.10.0"
476
+ }
477
+ },
478
+ "node_modules/sass": {
479
+ "version": "1.77.8",
480
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
481
+ "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
482
+ "dev": true,
483
+ "dependencies": {
484
+ "chokidar": ">=3.0.0 <4.0.0",
485
+ "immutable": "^4.0.0",
486
+ "source-map-js": ">=0.6.2 <2.0.0"
487
+ },
488
+ "bin": {
489
+ "sass": "sass.js"
490
+ },
491
+ "engines": {
492
+ "node": ">=14.0.0"
493
+ }
494
+ },
495
+ "node_modules/source-map-js": {
496
+ "version": "1.2.1",
497
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
498
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
499
+ "dev": true,
500
+ "engines": {
501
+ "node": ">=0.10.0"
502
+ }
503
+ },
504
+ "node_modules/to-regex-range": {
505
+ "version": "5.0.1",
506
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
507
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
508
+ "dev": true,
509
+ "dependencies": {
510
+ "is-number": "^7.0.0"
511
+ },
512
+ "engines": {
513
+ "node": ">=8.0"
514
+ }
515
+ },
516
+ "node_modules/typescript": {
517
+ "version": "5.9.2",
518
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
519
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
520
+ "dev": true,
521
+ "bin": {
522
+ "tsc": "bin/tsc",
523
+ "tsserver": "bin/tsserver"
524
+ },
525
+ "engines": {
526
+ "node": ">=14.17"
527
+ }
528
+ },
529
+ "node_modules/vue": {
530
+ "version": "3.5.13",
531
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
532
+ "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
533
+ "dev": true,
534
+ "peer": true,
535
+ "dependencies": {
536
+ "@vue/compiler-dom": "3.5.13",
537
+ "@vue/compiler-sfc": "3.5.13",
538
+ "@vue/runtime-dom": "3.5.13",
539
+ "@vue/server-renderer": "3.5.13",
540
+ "@vue/shared": "3.5.13"
541
+ },
542
+ "peerDependencies": {
543
+ "typescript": "*"
544
+ },
545
+ "peerDependenciesMeta": {
546
+ "typescript": {
547
+ "optional": true
548
+ }
549
+ }
550
+ },
551
+ "node_modules/web-tree-sitter": {
552
+ "version": "0.25.6",
553
+ "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.6.tgz",
554
+ "integrity": "sha512-WG+/YGbxw8r+rLlzzhV+OvgiOJCWdIpOucG3qBf3RCBFMkGDb1CanUi2BxCxjnkpzU3/hLWPT8VO5EKsMk9Fxg==",
555
+ "dev": true
556
+ },
557
+ "node_modules/zod": {
558
+ "version": "3.24.2",
559
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
560
+ "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
561
+ "dev": true,
562
+ "peer": true,
563
+ "funding": {
564
+ "url": "https://github.com/sponsors/colinhacks"
565
+ }
566
+ }
567
+ }
568
+ }
custom_nodes/rgthree-comfy/package.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "devDependencies": {
3
+ "prettier": "^3.3.3",
4
+ "typescript": "^5.5.4",
5
+ "sass": "^1.77.8",
6
+ "@comfyorg/comfyui-frontend-types": "^1.32.4",
7
+ "web-tree-sitter": "0.25.6"
8
+ },
9
+ "scripts": {
10
+ "build": "./__build__.py || python .\\__build__.py"
11
+ }
12
+ }
custom_nodes/rgthree-comfy/pnpm-lock.yaml ADDED
@@ -0,0 +1,507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .:
10
+ devDependencies:
11
+ '@comfyorg/comfyui-frontend-types':
12
+ specifier: ^1.32.4
13
+ version: 1.32.4(vue@3.5.24(typescript@5.9.3))(zod@3.25.76)
14
+ prettier:
15
+ specifier: ^3.3.3
16
+ version: 3.6.2
17
+ sass:
18
+ specifier: ^1.77.8
19
+ version: 1.93.3
20
+ typescript:
21
+ specifier: ^5.5.4
22
+ version: 5.9.3
23
+ web-tree-sitter:
24
+ specifier: 0.25.6
25
+ version: 0.25.6
26
+
27
+ packages:
28
+
29
+ '@babel/helper-string-parser@7.27.1':
30
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
31
+ engines: {node: '>=6.9.0'}
32
+
33
+ '@babel/helper-validator-identifier@7.28.5':
34
+ resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
35
+ engines: {node: '>=6.9.0'}
36
+
37
+ '@babel/parser@7.28.5':
38
+ resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
39
+ engines: {node: '>=6.0.0'}
40
+ hasBin: true
41
+
42
+ '@babel/types@7.28.5':
43
+ resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
44
+ engines: {node: '>=6.9.0'}
45
+
46
+ '@comfyorg/comfyui-frontend-types@1.32.4':
47
+ resolution: {integrity: sha512-9EBFwQPaSMLI151caWN5nA0qVmY5S9BR9lHP5NboPnTsmkFwnWdrAPadAnafwmZcK40ZWntKJZxO0Uuju0jw/w==}
48
+ peerDependencies:
49
+ vue: ^3.5.13
50
+ zod: ^3.23.8
51
+
52
+ '@jridgewell/sourcemap-codec@1.5.5':
53
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
54
+
55
+ '@parcel/watcher-android-arm64@2.5.1':
56
+ resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
57
+ engines: {node: '>= 10.0.0'}
58
+ cpu: [arm64]
59
+ os: [android]
60
+
61
+ '@parcel/watcher-darwin-arm64@2.5.1':
62
+ resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
63
+ engines: {node: '>= 10.0.0'}
64
+ cpu: [arm64]
65
+ os: [darwin]
66
+
67
+ '@parcel/watcher-darwin-x64@2.5.1':
68
+ resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
69
+ engines: {node: '>= 10.0.0'}
70
+ cpu: [x64]
71
+ os: [darwin]
72
+
73
+ '@parcel/watcher-freebsd-x64@2.5.1':
74
+ resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
75
+ engines: {node: '>= 10.0.0'}
76
+ cpu: [x64]
77
+ os: [freebsd]
78
+
79
+ '@parcel/watcher-linux-arm-glibc@2.5.1':
80
+ resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
81
+ engines: {node: '>= 10.0.0'}
82
+ cpu: [arm]
83
+ os: [linux]
84
+
85
+ '@parcel/watcher-linux-arm-musl@2.5.1':
86
+ resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
87
+ engines: {node: '>= 10.0.0'}
88
+ cpu: [arm]
89
+ os: [linux]
90
+
91
+ '@parcel/watcher-linux-arm64-glibc@2.5.1':
92
+ resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
93
+ engines: {node: '>= 10.0.0'}
94
+ cpu: [arm64]
95
+ os: [linux]
96
+
97
+ '@parcel/watcher-linux-arm64-musl@2.5.1':
98
+ resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
99
+ engines: {node: '>= 10.0.0'}
100
+ cpu: [arm64]
101
+ os: [linux]
102
+
103
+ '@parcel/watcher-linux-x64-glibc@2.5.1':
104
+ resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
105
+ engines: {node: '>= 10.0.0'}
106
+ cpu: [x64]
107
+ os: [linux]
108
+
109
+ '@parcel/watcher-linux-x64-musl@2.5.1':
110
+ resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
111
+ engines: {node: '>= 10.0.0'}
112
+ cpu: [x64]
113
+ os: [linux]
114
+
115
+ '@parcel/watcher-win32-arm64@2.5.1':
116
+ resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
117
+ engines: {node: '>= 10.0.0'}
118
+ cpu: [arm64]
119
+ os: [win32]
120
+
121
+ '@parcel/watcher-win32-ia32@2.5.1':
122
+ resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
123
+ engines: {node: '>= 10.0.0'}
124
+ cpu: [ia32]
125
+ os: [win32]
126
+
127
+ '@parcel/watcher-win32-x64@2.5.1':
128
+ resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
129
+ engines: {node: '>= 10.0.0'}
130
+ cpu: [x64]
131
+ os: [win32]
132
+
133
+ '@parcel/watcher@2.5.1':
134
+ resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
135
+ engines: {node: '>= 10.0.0'}
136
+
137
+ '@vue/compiler-core@3.5.24':
138
+ resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==}
139
+
140
+ '@vue/compiler-dom@3.5.24':
141
+ resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==}
142
+
143
+ '@vue/compiler-sfc@3.5.24':
144
+ resolution: {integrity: sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==}
145
+
146
+ '@vue/compiler-ssr@3.5.24':
147
+ resolution: {integrity: sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==}
148
+
149
+ '@vue/reactivity@3.5.24':
150
+ resolution: {integrity: sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==}
151
+
152
+ '@vue/runtime-core@3.5.24':
153
+ resolution: {integrity: sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==}
154
+
155
+ '@vue/runtime-dom@3.5.24':
156
+ resolution: {integrity: sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==}
157
+
158
+ '@vue/server-renderer@3.5.24':
159
+ resolution: {integrity: sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==}
160
+ peerDependencies:
161
+ vue: 3.5.24
162
+
163
+ '@vue/shared@3.5.24':
164
+ resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==}
165
+
166
+ braces@3.0.3:
167
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
168
+ engines: {node: '>=8'}
169
+
170
+ chokidar@4.0.3:
171
+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
172
+ engines: {node: '>= 14.16.0'}
173
+
174
+ csstype@3.1.3:
175
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
176
+
177
+ detect-libc@1.0.3:
178
+ resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
179
+ engines: {node: '>=0.10'}
180
+ hasBin: true
181
+
182
+ entities@4.5.0:
183
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
184
+ engines: {node: '>=0.12'}
185
+
186
+ estree-walker@2.0.2:
187
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
188
+
189
+ fill-range@7.1.1:
190
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
191
+ engines: {node: '>=8'}
192
+
193
+ immutable@5.1.4:
194
+ resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==}
195
+
196
+ is-extglob@2.1.1:
197
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
198
+ engines: {node: '>=0.10.0'}
199
+
200
+ is-glob@4.0.3:
201
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
202
+ engines: {node: '>=0.10.0'}
203
+
204
+ is-number@7.0.0:
205
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
206
+ engines: {node: '>=0.12.0'}
207
+
208
+ magic-string@0.30.21:
209
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
210
+
211
+ micromatch@4.0.8:
212
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
213
+ engines: {node: '>=8.6'}
214
+
215
+ nanoid@3.3.11:
216
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
217
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
218
+ hasBin: true
219
+
220
+ node-addon-api@7.1.1:
221
+ resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
222
+
223
+ picocolors@1.1.1:
224
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
225
+
226
+ picomatch@2.3.1:
227
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
228
+ engines: {node: '>=8.6'}
229
+
230
+ postcss@8.5.6:
231
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
232
+ engines: {node: ^10 || ^12 || >=14}
233
+
234
+ prettier@3.6.2:
235
+ resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
236
+ engines: {node: '>=14'}
237
+ hasBin: true
238
+
239
+ readdirp@4.1.2:
240
+ resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
241
+ engines: {node: '>= 14.18.0'}
242
+
243
+ sass@1.93.3:
244
+ resolution: {integrity: sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==}
245
+ engines: {node: '>=14.0.0'}
246
+ hasBin: true
247
+
248
+ source-map-js@1.2.1:
249
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
250
+ engines: {node: '>=0.10.0'}
251
+
252
+ to-regex-range@5.0.1:
253
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
254
+ engines: {node: '>=8.0'}
255
+
256
+ typescript@5.9.3:
257
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
258
+ engines: {node: '>=14.17'}
259
+ hasBin: true
260
+
261
+ vue@3.5.24:
262
+ resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==}
263
+ peerDependencies:
264
+ typescript: '*'
265
+ peerDependenciesMeta:
266
+ typescript:
267
+ optional: true
268
+
269
+ web-tree-sitter@0.25.6:
270
+ resolution: {integrity: sha512-WG+/YGbxw8r+rLlzzhV+OvgiOJCWdIpOucG3qBf3RCBFMkGDb1CanUi2BxCxjnkpzU3/hLWPT8VO5EKsMk9Fxg==}
271
+
272
+ zod@3.25.76:
273
+ resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
274
+
275
+ snapshots:
276
+
277
+ '@babel/helper-string-parser@7.27.1': {}
278
+
279
+ '@babel/helper-validator-identifier@7.28.5': {}
280
+
281
+ '@babel/parser@7.28.5':
282
+ dependencies:
283
+ '@babel/types': 7.28.5
284
+
285
+ '@babel/types@7.28.5':
286
+ dependencies:
287
+ '@babel/helper-string-parser': 7.27.1
288
+ '@babel/helper-validator-identifier': 7.28.5
289
+
290
+ '@comfyorg/comfyui-frontend-types@1.32.4(vue@3.5.24(typescript@5.9.3))(zod@3.25.76)':
291
+ dependencies:
292
+ vue: 3.5.24(typescript@5.9.3)
293
+ zod: 3.25.76
294
+
295
+ '@jridgewell/sourcemap-codec@1.5.5': {}
296
+
297
+ '@parcel/watcher-android-arm64@2.5.1':
298
+ optional: true
299
+
300
+ '@parcel/watcher-darwin-arm64@2.5.1':
301
+ optional: true
302
+
303
+ '@parcel/watcher-darwin-x64@2.5.1':
304
+ optional: true
305
+
306
+ '@parcel/watcher-freebsd-x64@2.5.1':
307
+ optional: true
308
+
309
+ '@parcel/watcher-linux-arm-glibc@2.5.1':
310
+ optional: true
311
+
312
+ '@parcel/watcher-linux-arm-musl@2.5.1':
313
+ optional: true
314
+
315
+ '@parcel/watcher-linux-arm64-glibc@2.5.1':
316
+ optional: true
317
+
318
+ '@parcel/watcher-linux-arm64-musl@2.5.1':
319
+ optional: true
320
+
321
+ '@parcel/watcher-linux-x64-glibc@2.5.1':
322
+ optional: true
323
+
324
+ '@parcel/watcher-linux-x64-musl@2.5.1':
325
+ optional: true
326
+
327
+ '@parcel/watcher-win32-arm64@2.5.1':
328
+ optional: true
329
+
330
+ '@parcel/watcher-win32-ia32@2.5.1':
331
+ optional: true
332
+
333
+ '@parcel/watcher-win32-x64@2.5.1':
334
+ optional: true
335
+
336
+ '@parcel/watcher@2.5.1':
337
+ dependencies:
338
+ detect-libc: 1.0.3
339
+ is-glob: 4.0.3
340
+ micromatch: 4.0.8
341
+ node-addon-api: 7.1.1
342
+ optionalDependencies:
343
+ '@parcel/watcher-android-arm64': 2.5.1
344
+ '@parcel/watcher-darwin-arm64': 2.5.1
345
+ '@parcel/watcher-darwin-x64': 2.5.1
346
+ '@parcel/watcher-freebsd-x64': 2.5.1
347
+ '@parcel/watcher-linux-arm-glibc': 2.5.1
348
+ '@parcel/watcher-linux-arm-musl': 2.5.1
349
+ '@parcel/watcher-linux-arm64-glibc': 2.5.1
350
+ '@parcel/watcher-linux-arm64-musl': 2.5.1
351
+ '@parcel/watcher-linux-x64-glibc': 2.5.1
352
+ '@parcel/watcher-linux-x64-musl': 2.5.1
353
+ '@parcel/watcher-win32-arm64': 2.5.1
354
+ '@parcel/watcher-win32-ia32': 2.5.1
355
+ '@parcel/watcher-win32-x64': 2.5.1
356
+ optional: true
357
+
358
+ '@vue/compiler-core@3.5.24':
359
+ dependencies:
360
+ '@babel/parser': 7.28.5
361
+ '@vue/shared': 3.5.24
362
+ entities: 4.5.0
363
+ estree-walker: 2.0.2
364
+ source-map-js: 1.2.1
365
+
366
+ '@vue/compiler-dom@3.5.24':
367
+ dependencies:
368
+ '@vue/compiler-core': 3.5.24
369
+ '@vue/shared': 3.5.24
370
+
371
+ '@vue/compiler-sfc@3.5.24':
372
+ dependencies:
373
+ '@babel/parser': 7.28.5
374
+ '@vue/compiler-core': 3.5.24
375
+ '@vue/compiler-dom': 3.5.24
376
+ '@vue/compiler-ssr': 3.5.24
377
+ '@vue/shared': 3.5.24
378
+ estree-walker: 2.0.2
379
+ magic-string: 0.30.21
380
+ postcss: 8.5.6
381
+ source-map-js: 1.2.1
382
+
383
+ '@vue/compiler-ssr@3.5.24':
384
+ dependencies:
385
+ '@vue/compiler-dom': 3.5.24
386
+ '@vue/shared': 3.5.24
387
+
388
+ '@vue/reactivity@3.5.24':
389
+ dependencies:
390
+ '@vue/shared': 3.5.24
391
+
392
+ '@vue/runtime-core@3.5.24':
393
+ dependencies:
394
+ '@vue/reactivity': 3.5.24
395
+ '@vue/shared': 3.5.24
396
+
397
+ '@vue/runtime-dom@3.5.24':
398
+ dependencies:
399
+ '@vue/reactivity': 3.5.24
400
+ '@vue/runtime-core': 3.5.24
401
+ '@vue/shared': 3.5.24
402
+ csstype: 3.1.3
403
+
404
+ '@vue/server-renderer@3.5.24(vue@3.5.24(typescript@5.9.3))':
405
+ dependencies:
406
+ '@vue/compiler-ssr': 3.5.24
407
+ '@vue/shared': 3.5.24
408
+ vue: 3.5.24(typescript@5.9.3)
409
+
410
+ '@vue/shared@3.5.24': {}
411
+
412
+ braces@3.0.3:
413
+ dependencies:
414
+ fill-range: 7.1.1
415
+ optional: true
416
+
417
+ chokidar@4.0.3:
418
+ dependencies:
419
+ readdirp: 4.1.2
420
+
421
+ csstype@3.1.3: {}
422
+
423
+ detect-libc@1.0.3:
424
+ optional: true
425
+
426
+ entities@4.5.0: {}
427
+
428
+ estree-walker@2.0.2: {}
429
+
430
+ fill-range@7.1.1:
431
+ dependencies:
432
+ to-regex-range: 5.0.1
433
+ optional: true
434
+
435
+ immutable@5.1.4: {}
436
+
437
+ is-extglob@2.1.1:
438
+ optional: true
439
+
440
+ is-glob@4.0.3:
441
+ dependencies:
442
+ is-extglob: 2.1.1
443
+ optional: true
444
+
445
+ is-number@7.0.0:
446
+ optional: true
447
+
448
+ magic-string@0.30.21:
449
+ dependencies:
450
+ '@jridgewell/sourcemap-codec': 1.5.5
451
+
452
+ micromatch@4.0.8:
453
+ dependencies:
454
+ braces: 3.0.3
455
+ picomatch: 2.3.1
456
+ optional: true
457
+
458
+ nanoid@3.3.11: {}
459
+
460
+ node-addon-api@7.1.1:
461
+ optional: true
462
+
463
+ picocolors@1.1.1: {}
464
+
465
+ picomatch@2.3.1:
466
+ optional: true
467
+
468
+ postcss@8.5.6:
469
+ dependencies:
470
+ nanoid: 3.3.11
471
+ picocolors: 1.1.1
472
+ source-map-js: 1.2.1
473
+
474
+ prettier@3.6.2: {}
475
+
476
+ readdirp@4.1.2: {}
477
+
478
+ sass@1.93.3:
479
+ dependencies:
480
+ chokidar: 4.0.3
481
+ immutable: 5.1.4
482
+ source-map-js: 1.2.1
483
+ optionalDependencies:
484
+ '@parcel/watcher': 2.5.1
485
+
486
+ source-map-js@1.2.1: {}
487
+
488
+ to-regex-range@5.0.1:
489
+ dependencies:
490
+ is-number: 7.0.0
491
+ optional: true
492
+
493
+ typescript@5.9.3: {}
494
+
495
+ vue@3.5.24(typescript@5.9.3):
496
+ dependencies:
497
+ '@vue/compiler-dom': 3.5.24
498
+ '@vue/compiler-sfc': 3.5.24
499
+ '@vue/runtime-dom': 3.5.24
500
+ '@vue/server-renderer': 3.5.24(vue@3.5.24(typescript@5.9.3))
501
+ '@vue/shared': 3.5.24
502
+ optionalDependencies:
503
+ typescript: 5.9.3
504
+
505
+ web-tree-sitter@0.25.6: {}
506
+
507
+ zod@3.25.76: {}
custom_nodes/rgthree-comfy/prestartup_script.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import folder_paths
2
+
3
+ # Removed Saved Prompts feature; No sure it worked any longer. UI should fail gracefully.
4
+ # TODO: See if anyone actually used this.
5
+ # folder_paths.folder_names_and_paths['saved_prompts'] = ([], set(['.txt']))
custom_nodes/rgthree-comfy/py/__init__.py ADDED
File without changes
custom_nodes/rgthree-comfy/py/any_switch.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .context_utils import is_context_empty
2
+ from .constants import get_category, get_name
3
+ from .utils import FlexibleOptionalInputType, any_type
4
+
5
+
6
+ def is_none(value):
7
+ """Checks if a value is none. Pulled out in case we want to expand what 'None' means."""
8
+ if value is not None:
9
+ if isinstance(value, dict) and 'model' in value and 'clip' in value:
10
+ return is_context_empty(value)
11
+ return value is None
12
+
13
+
14
+ class RgthreeAnySwitch:
15
+ """The dynamic Any Switch. """
16
+
17
+ NAME = get_name("Any Switch")
18
+ CATEGORY = get_category()
19
+
20
+ @classmethod
21
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
22
+ return {
23
+ "required": {},
24
+ "optional": FlexibleOptionalInputType(any_type),
25
+ }
26
+
27
+ RETURN_TYPES = (any_type,)
28
+ RETURN_NAMES = ('*',)
29
+ FUNCTION = "switch"
30
+
31
+ def switch(self, **kwargs):
32
+ """Chooses the first non-empty item to output."""
33
+ any_value = None
34
+ for key, value in kwargs.items():
35
+ if key.startswith('any_') and not is_none(value):
36
+ any_value = value
37
+ break
38
+ return (any_value,)
custom_nodes/rgthree-comfy/py/config.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+
4
+ from .utils import get_dict_value, set_dict_value, dict_has_key, load_json_file
5
+ from .pyproject import VERSION
6
+
7
+
8
+ def get_config_value(key, default=None):
9
+ return get_dict_value(RGTHREE_CONFIG, key, default)
10
+
11
+
12
+ def extend_config(default_config, user_config):
13
+ """ Returns a new config dict combining user_config into defined keys for default_config."""
14
+ cfg = {}
15
+ for key, value in default_config.items():
16
+ if key not in user_config:
17
+ cfg[key] = value
18
+ elif isinstance(value, dict):
19
+ cfg[key] = extend_config(value, user_config[key])
20
+ else:
21
+ cfg[key] = user_config[key] if key in user_config else value
22
+ return cfg
23
+
24
+
25
+ def set_user_config(data: dict):
26
+ """ Sets the user configuration."""
27
+ count = 0
28
+ for key, value in data.items():
29
+ if dict_has_key(DEFAULT_CONFIG, key):
30
+ set_dict_value(USER_CONFIG, key, value)
31
+ set_dict_value(RGTHREE_CONFIG, key, value)
32
+ count += 1
33
+ if count > 0:
34
+ write_user_config()
35
+
36
+
37
+ def get_rgthree_default_config():
38
+ """ Gets the default configuration."""
39
+ return load_json_file(DEFAULT_CONFIG_FILE, default={})
40
+
41
+
42
+ def get_rgthree_user_config():
43
+ """ Gets the user configuration."""
44
+ return load_json_file(USER_CONFIG_FILE, default={})
45
+
46
+
47
+ def write_user_config():
48
+ """ Writes the user configuration."""
49
+ with open(USER_CONFIG_FILE, 'w+', encoding='UTF-8') as file:
50
+ json.dump(USER_CONFIG, file, sort_keys=True, indent=2, separators=(",", ": "))
51
+
52
+
53
+ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
54
+ DEFAULT_CONFIG_FILE = os.path.join(THIS_DIR, '..', 'rgthree_config.json.default')
55
+ USER_CONFIG_FILE = os.path.join(THIS_DIR, '..', 'rgthree_config.json')
56
+
57
+ DEFAULT_CONFIG = {}
58
+ USER_CONFIG = {}
59
+ RGTHREE_CONFIG = {}
60
+
61
+
62
+ def refresh_config():
63
+ """Refreshes the config."""
64
+ global DEFAULT_CONFIG, USER_CONFIG, RGTHREE_CONFIG
65
+ DEFAULT_CONFIG = get_rgthree_default_config()
66
+ USER_CONFIG = get_rgthree_user_config()
67
+
68
+ # Migrate old config options into "features"
69
+ needs_to_write_user_config = False
70
+ if 'patch_recursive_execution' in USER_CONFIG:
71
+ del USER_CONFIG['patch_recursive_execution']
72
+ needs_to_write_user_config = True
73
+
74
+ if 'features' in USER_CONFIG and 'patch_recursive_execution' in USER_CONFIG['features']:
75
+ del USER_CONFIG['features']['patch_recursive_execution']
76
+ needs_to_write_user_config = True
77
+
78
+ if 'show_alerts_for_corrupt_workflows' in USER_CONFIG:
79
+ if 'features' not in USER_CONFIG:
80
+ USER_CONFIG['features'] = {}
81
+ USER_CONFIG['features']['show_alerts_for_corrupt_workflows'] = USER_CONFIG[
82
+ 'show_alerts_for_corrupt_workflows']
83
+ del USER_CONFIG['show_alerts_for_corrupt_workflows']
84
+ needs_to_write_user_config = True
85
+
86
+ if 'monitor_for_corrupt_links' in USER_CONFIG:
87
+ if 'features' not in USER_CONFIG:
88
+ USER_CONFIG['features'] = {}
89
+ USER_CONFIG['features']['monitor_for_corrupt_links'] = USER_CONFIG['monitor_for_corrupt_links']
90
+ del USER_CONFIG['monitor_for_corrupt_links']
91
+ needs_to_write_user_config = True
92
+
93
+ if needs_to_write_user_config is True:
94
+ print('writing new user config.')
95
+ write_user_config()
96
+
97
+ RGTHREE_CONFIG = {"version": VERSION} | extend_config(DEFAULT_CONFIG, USER_CONFIG)
98
+
99
+ if "unreleased" in USER_CONFIG and "unreleased" not in RGTHREE_CONFIG:
100
+ RGTHREE_CONFIG["unreleased"] = USER_CONFIG["unreleased"]
101
+
102
+ if "debug" in USER_CONFIG and "debug" not in RGTHREE_CONFIG:
103
+ RGTHREE_CONFIG["debug"] = USER_CONFIG["debug"]
104
+
105
+
106
+ def get_config():
107
+ """Returns the congfig."""
108
+ return RGTHREE_CONFIG
109
+
110
+
111
+ refresh_config()
custom_nodes/rgthree-comfy/py/constants.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ NAMESPACE='rgthree'
3
+
4
+ def get_name(name):
5
+ return '{} ({})'.format(name, NAMESPACE)
6
+
7
+ def get_category(sub_dirs = None):
8
+ if sub_dirs is None:
9
+ return NAMESPACE
10
+ else:
11
+ return "{}/utils".format(NAMESPACE)
custom_nodes/rgthree-comfy/py/context.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The Context node."""
2
+ from .context_utils import (ORIG_CTX_OPTIONAL_INPUTS, ORIG_CTX_RETURN_NAMES, ORIG_CTX_RETURN_TYPES,
3
+ get_orig_context_return_tuple, new_context)
4
+ from .constants import get_category, get_name
5
+
6
+
7
+ class RgthreeContext:
8
+ """The initial Context node.
9
+
10
+ For now, this nodes' outputs will remain as-is, as they are perfect for most 1.5 application, but
11
+ is also backwards compatible with other Context nodes.
12
+ """
13
+
14
+ NAME = get_name("Context")
15
+ CATEGORY = get_category()
16
+
17
+ @classmethod
18
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
19
+ return {
20
+ "required": {},
21
+ "optional": ORIG_CTX_OPTIONAL_INPUTS,
22
+ "hidden": {
23
+ "version": "FLOAT"
24
+ },
25
+ }
26
+
27
+ RETURN_TYPES = ORIG_CTX_RETURN_TYPES
28
+ RETURN_NAMES = ORIG_CTX_RETURN_NAMES
29
+ FUNCTION = "convert"
30
+
31
+ def convert(self, base_ctx=None, **kwargs): # pylint: disable = missing-function-docstring
32
+ ctx = new_context(base_ctx, **kwargs)
33
+ return get_orig_context_return_tuple(ctx)
custom_nodes/rgthree-comfy/py/context_big.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The Conmtext big node."""
2
+ from .constants import get_category, get_name
3
+ from .context_utils import (ALL_CTX_OPTIONAL_INPUTS, ALL_CTX_RETURN_NAMES, ALL_CTX_RETURN_TYPES,
4
+ new_context, get_context_return_tuple)
5
+
6
+
7
+ class RgthreeBigContext:
8
+ """The Context Big node.
9
+
10
+ This context node will expose all context fields as inputs and outputs. It is backwards compatible
11
+ with other context nodes and can be intertwined with them.
12
+ """
13
+
14
+ NAME = get_name("Context Big")
15
+ CATEGORY = get_category()
16
+
17
+ @classmethod
18
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name,missing-function-docstring
19
+ return {
20
+ "required": {},
21
+ "optional": ALL_CTX_OPTIONAL_INPUTS,
22
+ "hidden": {},
23
+ }
24
+
25
+ RETURN_TYPES = ALL_CTX_RETURN_TYPES
26
+ RETURN_NAMES = ALL_CTX_RETURN_NAMES
27
+ FUNCTION = "convert"
28
+
29
+ def convert(self, base_ctx=None, **kwargs): # pylint: disable = missing-function-docstring
30
+ ctx = new_context(base_ctx, **kwargs)
31
+ return get_context_return_tuple(ctx)
custom_nodes/rgthree-comfy/py/context_merge.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The Context Switch (Big)."""
2
+ from .constants import get_category, get_name
3
+ from .context_utils import (ORIG_CTX_RETURN_TYPES, ORIG_CTX_RETURN_NAMES, merge_new_context,
4
+ get_orig_context_return_tuple, is_context_empty)
5
+ from .utils import FlexibleOptionalInputType
6
+
7
+
8
+ class RgthreeContextMerge:
9
+ """The Context Merge node."""
10
+
11
+ NAME = get_name("Context Merge")
12
+ CATEGORY = get_category()
13
+
14
+ @classmethod
15
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
16
+ return {
17
+ "required": {},
18
+ "optional": FlexibleOptionalInputType("RGTHREE_CONTEXT"),
19
+ }
20
+
21
+ RETURN_TYPES = ORIG_CTX_RETURN_TYPES
22
+ RETURN_NAMES = ORIG_CTX_RETURN_NAMES
23
+ FUNCTION = "merge"
24
+
25
+ def get_return_tuple(self, ctx):
26
+ """Returns the context data. Separated so it can be overridden."""
27
+ return get_orig_context_return_tuple(ctx)
28
+
29
+ def merge(self, **kwargs):
30
+ """Merges any non-null passed contexts; later ones overriding earlier."""
31
+ ctxs = [
32
+ value for key, value in kwargs.items()
33
+ if key.startswith('ctx_') and not is_context_empty(value)
34
+ ]
35
+ ctx = merge_new_context(*ctxs)
36
+
37
+ return self.get_return_tuple(ctx)
custom_nodes/rgthree-comfy/py/context_merge_big.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The Context Switch (Big)."""
2
+ from .constants import get_category, get_name
3
+ from .context_utils import (ALL_CTX_RETURN_TYPES, ALL_CTX_RETURN_NAMES, get_context_return_tuple)
4
+ from .context_merge import RgthreeContextMerge
5
+
6
+
7
+ class RgthreeContextMergeBig(RgthreeContextMerge):
8
+ """The Context Merge Big node."""
9
+
10
+ NAME = get_name("Context Merge Big")
11
+ RETURN_TYPES = ALL_CTX_RETURN_TYPES
12
+ RETURN_NAMES = ALL_CTX_RETURN_NAMES
13
+
14
+ def get_return_tuple(self, ctx):
15
+ """Returns the context data. Separated so it can be overridden."""
16
+ return get_context_return_tuple(ctx)
custom_nodes/rgthree-comfy/py/context_switch.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The original Context Switch."""
2
+ from .constants import get_category, get_name
3
+ from .context_utils import (ORIG_CTX_RETURN_TYPES, ORIG_CTX_RETURN_NAMES, is_context_empty,
4
+ get_orig_context_return_tuple)
5
+ from .utils import FlexibleOptionalInputType
6
+
7
+
8
+ class RgthreeContextSwitch:
9
+ """The (original) Context Switch node."""
10
+
11
+ NAME = get_name("Context Switch")
12
+ CATEGORY = get_category()
13
+
14
+ @classmethod
15
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
16
+ return {
17
+ "required": {},
18
+ "optional": FlexibleOptionalInputType("RGTHREE_CONTEXT"),
19
+ }
20
+
21
+ RETURN_TYPES = ORIG_CTX_RETURN_TYPES
22
+ RETURN_NAMES = ORIG_CTX_RETURN_NAMES
23
+ FUNCTION = "switch"
24
+
25
+ def get_return_tuple(self, ctx):
26
+ """Returns the context data. Separated so it can be overridden."""
27
+ return get_orig_context_return_tuple(ctx)
28
+
29
+ def switch(self, **kwargs):
30
+ """Chooses the first non-empty Context to output."""
31
+ ctx = None
32
+ for key, value in kwargs.items():
33
+ if key.startswith('ctx_') and not is_context_empty(value):
34
+ ctx = value
35
+ break
36
+ return self.get_return_tuple(ctx)
custom_nodes/rgthree-comfy/py/context_switch_big.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The Context Switch (Big)."""
2
+ from .constants import get_category, get_name
3
+ from .context_utils import (ALL_CTX_RETURN_TYPES, ALL_CTX_RETURN_NAMES, get_context_return_tuple)
4
+ from .context_switch import RgthreeContextSwitch
5
+
6
+
7
+ class RgthreeContextSwitchBig(RgthreeContextSwitch):
8
+ """The Context Switch Big node."""
9
+
10
+ NAME = get_name("Context Switch Big")
11
+ RETURN_TYPES = ALL_CTX_RETURN_TYPES
12
+ RETURN_NAMES = ALL_CTX_RETURN_NAMES
13
+
14
+ def get_return_tuple(self, ctx):
15
+ """Overrides the RgthreeContextSwitch `get_return_tuple` to return big context data."""
16
+ return get_context_return_tuple(ctx)
custom_nodes/rgthree-comfy/py/context_utils.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """A set of constants and utilities for handling contexts.
2
+
3
+ Sets up the inputs and outputs for the Context going forward, with additional functions for
4
+ creating and exporting context objects.
5
+ """
6
+ import comfy.samplers
7
+ import folder_paths
8
+
9
+ _all_context_input_output_data = {
10
+ "base_ctx": ("base_ctx", "RGTHREE_CONTEXT", "CONTEXT"),
11
+ "model": ("model", "MODEL", "MODEL"),
12
+ "clip": ("clip", "CLIP", "CLIP"),
13
+ "vae": ("vae", "VAE", "VAE"),
14
+ "positive": ("positive", "CONDITIONING", "POSITIVE"),
15
+ "negative": ("negative", "CONDITIONING", "NEGATIVE"),
16
+ "latent": ("latent", "LATENT", "LATENT"),
17
+ "images": ("images", "IMAGE", "IMAGE"),
18
+ "seed": ("seed", "INT", "SEED"),
19
+ "steps": ("steps", "INT", "STEPS"),
20
+ "step_refiner": ("step_refiner", "INT", "STEP_REFINER"),
21
+ "cfg": ("cfg", "FLOAT", "CFG"),
22
+ "ckpt_name": ("ckpt_name", folder_paths.get_filename_list("checkpoints"), "CKPT_NAME"),
23
+ "sampler": ("sampler", comfy.samplers.KSampler.SAMPLERS, "SAMPLER"),
24
+ "scheduler": ("scheduler", comfy.samplers.KSampler.SCHEDULERS, "SCHEDULER"),
25
+ "clip_width": ("clip_width", "INT", "CLIP_WIDTH"),
26
+ "clip_height": ("clip_height", "INT", "CLIP_HEIGHT"),
27
+ "text_pos_g": ("text_pos_g", "STRING", "TEXT_POS_G"),
28
+ "text_pos_l": ("text_pos_l", "STRING", "TEXT_POS_L"),
29
+ "text_neg_g": ("text_neg_g", "STRING", "TEXT_NEG_G"),
30
+ "text_neg_l": ("text_neg_l", "STRING", "TEXT_NEG_L"),
31
+ "mask": ("mask", "MASK", "MASK"),
32
+ "control_net": ("control_net", "CONTROL_NET", "CONTROL_NET"),
33
+ }
34
+
35
+ force_input_types = ["INT", "STRING", "FLOAT"]
36
+ force_input_names = ["sampler", "scheduler", "ckpt_name"]
37
+
38
+
39
+ def _create_context_data(input_list=None):
40
+ """Returns a tuple of context inputs, return types, and return names to use in a node"s def"""
41
+ if input_list is None:
42
+ input_list = _all_context_input_output_data.keys()
43
+ list_ctx_return_types = []
44
+ list_ctx_return_names = []
45
+ ctx_optional_inputs = {}
46
+ for inp in input_list:
47
+ data = _all_context_input_output_data[inp]
48
+ list_ctx_return_types.append(data[1])
49
+ list_ctx_return_names.append(data[2])
50
+ ctx_optional_inputs[data[0]] = tuple([data[1]] + ([{
51
+ "forceInput": True
52
+ }] if data[1] in force_input_types or data[0] in force_input_names else []))
53
+
54
+ ctx_return_types = tuple(list_ctx_return_types)
55
+ ctx_return_names = tuple(list_ctx_return_names)
56
+ return (ctx_optional_inputs, ctx_return_types, ctx_return_names)
57
+
58
+
59
+ ALL_CTX_OPTIONAL_INPUTS, ALL_CTX_RETURN_TYPES, ALL_CTX_RETURN_NAMES = _create_context_data()
60
+
61
+ _original_ctx_inputs_list = [
62
+ "base_ctx", "model", "clip", "vae", "positive", "negative", "latent", "images", "seed"
63
+ ]
64
+ ORIG_CTX_OPTIONAL_INPUTS, ORIG_CTX_RETURN_TYPES, ORIG_CTX_RETURN_NAMES = _create_context_data(
65
+ _original_ctx_inputs_list)
66
+
67
+
68
+ def new_context(base_ctx, **kwargs):
69
+ """Creates a new context from the provided data, with an optional base ctx to start."""
70
+ context = base_ctx if base_ctx is not None else None
71
+ new_ctx = {}
72
+ for key in _all_context_input_output_data:
73
+ if key == "base_ctx":
74
+ continue
75
+ v = kwargs[key] if key in kwargs else None
76
+ new_ctx[key] = v if v is not None else context[
77
+ key] if context is not None and key in context else None
78
+ return new_ctx
79
+
80
+
81
+ def merge_new_context(*args):
82
+ """Creates a new context by merging provided contexts with the latter overriding same fields."""
83
+ new_ctx = {}
84
+ for key in _all_context_input_output_data:
85
+ if key == "base_ctx":
86
+ continue
87
+ v = None
88
+ # Move backwards through the passed contexts until we find a value and use it.
89
+ for ctx in reversed(args):
90
+ v = ctx[key] if not is_context_empty(ctx) and key in ctx else None
91
+ if v is not None:
92
+ break
93
+ new_ctx[key] = v
94
+ return new_ctx
95
+
96
+
97
+ def get_context_return_tuple(ctx, inputs_list=None):
98
+ """Returns a tuple for returning in the order of the inputs list."""
99
+ if inputs_list is None:
100
+ inputs_list = _all_context_input_output_data.keys()
101
+ tup_list = [
102
+ ctx,
103
+ ]
104
+ for key in inputs_list:
105
+ if key == "base_ctx":
106
+ continue
107
+ tup_list.append(ctx[key] if ctx is not None and key in ctx else None)
108
+ return tuple(tup_list)
109
+
110
+
111
+ def get_orig_context_return_tuple(ctx):
112
+ """Returns a tuple for returning from a node with only the original context keys."""
113
+ return get_context_return_tuple(ctx, _original_ctx_inputs_list)
114
+
115
+
116
+ def is_context_empty(ctx):
117
+ """Checks if the provided ctx is None or contains just None values."""
118
+ return not ctx or all(v is None for v in ctx.values())
custom_nodes/rgthree-comfy/py/display_any.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from .constants import get_category, get_name
3
+ from .utils import any_type, get_dict_value
4
+
5
+
6
+ class RgthreeDisplayAny:
7
+ """Display any data node."""
8
+
9
+ NAME = get_name('Display Any')
10
+ CATEGORY = get_category()
11
+
12
+ @classmethod
13
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
14
+ return {
15
+ "required": {
16
+ "source": (any_type, {}),
17
+ },
18
+ "hidden": {
19
+ "unique_id": "UNIQUE_ID",
20
+ "extra_pnginfo": "EXTRA_PNGINFO",
21
+ },
22
+ }
23
+
24
+ RETURN_TYPES = ()
25
+ FUNCTION = "main"
26
+ OUTPUT_NODE = True
27
+
28
+ def main(self, source=None, unique_id=None, extra_pnginfo=None):
29
+ value = 'None'
30
+ if isinstance(source, str):
31
+ value = source
32
+ elif isinstance(source, (int, float, bool)):
33
+ value = str(source)
34
+ elif source is not None:
35
+ try:
36
+ value = json.dumps(source)
37
+ except Exception:
38
+ try:
39
+ value = str(source)
40
+ except Exception:
41
+ value = 'source exists, but could not be serialized.'
42
+
43
+ # Save the output to the pnginfo so it's pre-filled when loading the data.
44
+ if extra_pnginfo and unique_id:
45
+ for node in get_dict_value(extra_pnginfo, 'workflow.nodes', []):
46
+ if str(node['id']) == str(unique_id):
47
+ node['widgets_values'] = [value]
48
+ break
49
+
50
+ return {"ui": {"text": (value,)}}
51
+
52
+
53
+ class RgthreeDisplayInt:
54
+ """Old DisplayInt node.
55
+
56
+ Can be ported over to DisplayAny if https://github.com/comfyanonymous/ComfyUI/issues/1527 fixed.
57
+ """
58
+
59
+ NAME = get_name('Display Int')
60
+ CATEGORY = get_category()
61
+
62
+ @classmethod
63
+ def INPUT_TYPES(s):
64
+ return {
65
+ "required": {
66
+ "input": ("INT", {
67
+ "forceInput": True
68
+ }),
69
+ },
70
+ }
71
+
72
+ RETURN_TYPES = ()
73
+ FUNCTION = "main"
74
+ OUTPUT_NODE = True
75
+
76
+ def main(self, input=None):
77
+ return {"ui": {"text": (input,)}}
custom_nodes/rgthree-comfy/py/dynamic_context.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The Dynamic Context node."""
2
+ from mimetypes import add_type
3
+ from .constants import get_category, get_name
4
+ from .utils import ByPassTypeTuple, FlexibleOptionalInputType
5
+
6
+
7
+ class RgthreeDynamicContext:
8
+ """The Dynamic Context node.
9
+
10
+ Similar to the static Context and Context Big nodes, this allows users to add any number and
11
+ variety of inputs to a Dynamic Context node, and return the outputs by key name.
12
+ """
13
+
14
+ NAME = get_name("Dynamic Context")
15
+ CATEGORY = get_category()
16
+
17
+ @classmethod
18
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name,missing-function-docstring
19
+ return {
20
+ "required": {},
21
+ "optional": FlexibleOptionalInputType(add_type),
22
+ "hidden": {},
23
+ }
24
+
25
+ RETURN_TYPES = ByPassTypeTuple(("RGTHREE_DYNAMIC_CONTEXT",))
26
+ RETURN_NAMES = ByPassTypeTuple(("CONTEXT",))
27
+ FUNCTION = "main"
28
+
29
+ def main(self, **kwargs):
30
+ """Creates a new context from the provided data, with an optional base ctx to start.
31
+
32
+ This node takes a list of named inputs that are the named keys (with an optional "+ " prefix)
33
+ which are to be stored within the ctx dict as well as a list of keys contained in `output_keys`
34
+ to determine the list of output data.
35
+ """
36
+
37
+ base_ctx = kwargs.get('base_ctx', None)
38
+ output_keys = kwargs.get('output_keys', None)
39
+
40
+ new_ctx = base_ctx.copy() if base_ctx is not None else {}
41
+
42
+ for key_raw, value in kwargs.items():
43
+ if key_raw in ['base_ctx', 'output_keys']:
44
+ continue
45
+ key = key_raw.upper()
46
+ if key.startswith('+ '):
47
+ key = key[2:]
48
+ new_ctx[key] = value
49
+
50
+ print(new_ctx)
51
+
52
+ res = [new_ctx]
53
+ output_keys = output_keys.split(',') if output_keys is not None else []
54
+ for key in output_keys:
55
+ res.append(new_ctx[key] if key in new_ctx else None)
56
+ return tuple(res)
custom_nodes/rgthree-comfy/py/dynamic_context_switch.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The original Context Switch."""
2
+ from .constants import get_category, get_name
3
+ from .context_utils import is_context_empty
4
+ from .utils import ByPassTypeTuple, FlexibleOptionalInputType
5
+
6
+
7
+ class RgthreeDynamicContextSwitch:
8
+ """The initial Context Switch node."""
9
+
10
+ NAME = get_name("Dynamic Context Switch")
11
+ CATEGORY = get_category()
12
+
13
+ @classmethod
14
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
15
+ return {
16
+ "required": {},
17
+ "optional": FlexibleOptionalInputType("RGTHREE_DYNAMIC_CONTEXT"),
18
+ }
19
+
20
+ RETURN_TYPES = ByPassTypeTuple(("RGTHREE_DYNAMIC_CONTEXT",))
21
+ RETURN_NAMES = ByPassTypeTuple(("CONTEXT",))
22
+ FUNCTION = "switch"
23
+
24
+ def switch(self, **kwargs):
25
+ """Chooses the first non-empty Context to output."""
26
+
27
+ output_keys = kwargs.get('output_keys', None)
28
+
29
+ ctx = None
30
+ for key, value in kwargs.items():
31
+ if key.startswith('ctx_') and not is_context_empty(value):
32
+ ctx = value
33
+ break
34
+
35
+ res = [ctx]
36
+ output_keys = output_keys.split(',') if output_keys is not None else []
37
+ for key in output_keys:
38
+ res.append(ctx[key] if ctx is not None and key in ctx else None)
39
+ return tuple(res)
custom_nodes/rgthree-comfy/py/image_comparer.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from nodes import PreviewImage
2
+
3
+ from .constants import get_category, get_name
4
+
5
+
6
+ class RgthreeImageComparer(PreviewImage):
7
+ """A node that compares two images in the UI."""
8
+
9
+ NAME = get_name('Image Comparer')
10
+ CATEGORY = get_category()
11
+ FUNCTION = "compare_images"
12
+ DESCRIPTION = "Compares two images with a hover slider, or click from properties."
13
+
14
+ @classmethod
15
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
16
+ return {
17
+ "required": {},
18
+ "optional": {
19
+ "image_a": ("IMAGE",),
20
+ "image_b": ("IMAGE",),
21
+ },
22
+ "hidden": {
23
+ "prompt": "PROMPT",
24
+ "extra_pnginfo": "EXTRA_PNGINFO"
25
+ },
26
+ }
27
+
28
+ def compare_images(self,
29
+ image_a=None,
30
+ image_b=None,
31
+ filename_prefix="rgthree.compare.",
32
+ prompt=None,
33
+ extra_pnginfo=None):
34
+
35
+ result = { "ui": { "a_images":[], "b_images": [] } }
36
+ if image_a is not None and len(image_a) > 0:
37
+ result['ui']['a_images'] = self.save_images(image_a, filename_prefix, prompt, extra_pnginfo)['ui']['images']
38
+
39
+ if image_b is not None and len(image_b) > 0:
40
+ result['ui']['b_images'] = self.save_images(image_b, filename_prefix, prompt, extra_pnginfo)['ui']['images']
41
+
42
+ return result
custom_nodes/rgthree-comfy/py/image_inset_crop.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image Inset Crop, with percentages."""
2
+ from .log import log_node_info
3
+ from .constants import get_category, get_name
4
+ from nodes import MAX_RESOLUTION
5
+
6
+
7
+ def get_new_bounds(width, height, left, right, top, bottom):
8
+ """Returns the new bounds for an image with inset crop data."""
9
+ left = 0 + left
10
+ right = width - right
11
+ top = 0 + top
12
+ bottom = height - bottom
13
+ return (left, right, top, bottom)
14
+
15
+
16
+ class RgthreeImageInsetCrop:
17
+ """Image Inset Crop, with percentages."""
18
+
19
+ NAME = get_name('Image Inset Crop')
20
+ CATEGORY = get_category()
21
+
22
+ @classmethod
23
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
24
+ return {
25
+ "required": {
26
+ "image": ("IMAGE",),
27
+ "measurement": (['Pixels', 'Percentage'],),
28
+ "left": ("INT", {
29
+ "default": 0,
30
+ "min": 0,
31
+ "max": MAX_RESOLUTION,
32
+ "step": 8
33
+ }),
34
+ "right": ("INT", {
35
+ "default": 0,
36
+ "min": 0,
37
+ "max": MAX_RESOLUTION,
38
+ "step": 8
39
+ }),
40
+ "top": ("INT", {
41
+ "default": 0,
42
+ "min": 0,
43
+ "max": MAX_RESOLUTION,
44
+ "step": 8
45
+ }),
46
+ "bottom": ("INT", {
47
+ "default": 0,
48
+ "min": 0,
49
+ "max": MAX_RESOLUTION,
50
+ "step": 8
51
+ }),
52
+ },
53
+ }
54
+
55
+ RETURN_TYPES = ("IMAGE",)
56
+ FUNCTION = "crop"
57
+
58
+ # pylint: disable = too-many-arguments
59
+ def crop(self, measurement, left, right, top, bottom, image=None):
60
+ """Does the crop."""
61
+
62
+ _, height, width, _ = image.shape
63
+
64
+ if measurement == 'Percentage':
65
+ left = int(width - (width * (100 - left) / 100))
66
+ right = int(width - (width * (100 - right) / 100))
67
+ top = int(height - (height * (100 - top) / 100))
68
+ bottom = int(height - (height * (100 - bottom) / 100))
69
+
70
+ # Snap to 8 pixels
71
+ left = left // 8 * 8
72
+ right = right // 8 * 8
73
+ top = top // 8 * 8
74
+ bottom = bottom // 8 * 8
75
+
76
+ if left == 0 and right == 0 and bottom == 0 and top == 0:
77
+ return (image,)
78
+
79
+ inset_left, inset_right, inset_top, inset_bottom = get_new_bounds(width, height, left, right,
80
+ top, bottom)
81
+ if inset_top > inset_bottom:
82
+ raise ValueError(
83
+ f"Invalid cropping dimensions top ({inset_top}) exceeds bottom ({inset_bottom})")
84
+ if inset_left > inset_right:
85
+ raise ValueError(
86
+ f"Invalid cropping dimensions left ({inset_left}) exceeds right ({inset_right})")
87
+
88
+ log_node_info(
89
+ self.NAME, f'Cropping image {width}x{height} width inset by {inset_left},{inset_right}, ' +
90
+ f'and height inset by {inset_top}, {inset_bottom}')
91
+ image = image[:, inset_top:inset_bottom, inset_left:inset_right, :]
92
+
93
+ return (image,)
custom_nodes/rgthree-comfy/py/image_or_latent_size.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .utils import FlexibleOptionalInputType, any_type
2
+ from .constants import get_category, get_name
3
+
4
+
5
+ class RgthreeImageOrLatentSize:
6
+ """The ImageOrLatentSize Node."""
7
+
8
+ NAME = get_name('Image or Latent Size')
9
+ CATEGORY = get_category()
10
+
11
+ @classmethod
12
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
13
+ return {
14
+ "required": {},
15
+ "optional": FlexibleOptionalInputType(any_type),
16
+ }
17
+
18
+ RETURN_TYPES = ("INT", "INT")
19
+ RETURN_NAMES = ('WIDTH', 'HEIGHT')
20
+ FUNCTION = "main"
21
+
22
+ def main(self, **kwargs):
23
+ """Does the node's work."""
24
+ image_or_latent_or_mask = kwargs.get('input', None)
25
+
26
+ if isinstance(image_or_latent_or_mask, dict) and 'samples' in image_or_latent_or_mask:
27
+ count, _, height, width = image_or_latent_or_mask['samples'].shape
28
+ return (width * 8, height * 8)
29
+
30
+ batch, height, width, channel = image_or_latent_or_mask.shape
31
+ return (width, height)
custom_nodes/rgthree-comfy/py/image_resize.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import comfy.utils
3
+ import nodes
4
+
5
+ from .constants import get_category, get_name
6
+
7
+
8
+ class RgthreeImageResize:
9
+ """Image Resize."""
10
+
11
+ NAME = get_name("Image Resize")
12
+ CATEGORY = get_category()
13
+
14
+ @classmethod
15
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
16
+ return {
17
+ "required": {
18
+ "image": ("IMAGE",),
19
+ "measurement": (["pixels", "percentage"],),
20
+ "width": (
21
+ "INT", {
22
+ "default": 0,
23
+ "min": 0,
24
+ "max": nodes.MAX_RESOLUTION,
25
+ "step": 1,
26
+ "tooltip": (
27
+ "The width of the desired resize. A pixel value if measurement is 'pixels' or a"
28
+ " 100% scale percentage value if measurement is 'percentage'. Passing '0' will"
29
+ " calculate the dimension based on the height."
30
+ ),
31
+ },
32
+ ),
33
+ "height": ("INT", {
34
+ "default": 0,
35
+ "min": 0,
36
+ "max": nodes.MAX_RESOLUTION,
37
+ "step": 1
38
+ }),
39
+ "fit": (["crop", "pad", "contain"], {
40
+ "tooltip": (
41
+ "'crop' resizes so the image covers the desired width and height, and center-crops the"
42
+ " excess, returning exactly the desired width and height."
43
+ "\n'pad' resizes so the image fits inside the desired width and height, and fills the"
44
+ " empty space returning exactly the desired width and height."
45
+ "\n'contain' resizes so the image fits inside the desired width and height, and"
46
+ " returns the image with it's new size, with one side liekly smaller than the desired."
47
+ "\n\nNote, if either width or height is '0', the effective fit is 'contain'."
48
+ )
49
+ },
50
+ ),
51
+ "method": (nodes.ImageScale.upscale_methods,),
52
+ },
53
+ }
54
+
55
+ RETURN_TYPES = ("IMAGE", "INT", "INT",)
56
+ RETURN_NAMES = ("IMAGE", "WIDTH", "HEIGHT",)
57
+ FUNCTION = "main"
58
+ DESCRIPTION = """Resize the image."""
59
+
60
+ def main(self, image, measurement, width, height, method, fit):
61
+ """Resizes the image."""
62
+ _, H, W, _ = image.shape
63
+
64
+ if measurement == "percentage":
65
+ width = round(width * W / 100)
66
+ height = round(height * H / 100)
67
+
68
+ if (width == 0 and height == 0) or (width == W and height == H):
69
+ return (image, W, H)
70
+
71
+ # If one dimension is 0, then calculate the desired value from the ratio of the set dimension.
72
+ # This also implies a 'contain' fit since the width and height will be scaled with a locked
73
+ # aspect ratio.
74
+ if width == 0 or height == 0:
75
+ width = round(height / H * W) if width == 0 else width
76
+ height = round(width / W * H) if height == 0 else height
77
+ fit = "contain"
78
+
79
+ # At this point, width and height are our output height, but our resize sizes will be different.
80
+ resized_width = width
81
+ resized_height = height
82
+ if fit == "crop":
83
+ # If we resize against the opposite ratio, then choose the ratio that has the overhang.
84
+ if (height / H * W) > width:
85
+ resized_width = round(height / H * W)
86
+ elif (width / W * H) > height:
87
+ resized_height = round(width / W * H)
88
+ elif fit == "contain" or fit == "pad":
89
+ # If we resize against the opposite ratio, then choose the ratio that has the overhang.
90
+ if (height / H * W) > width:
91
+ resized_height = round(width / W * H)
92
+ elif (width / W * H) > height:
93
+ resized_width = round(height / H * W)
94
+
95
+ out_image = comfy.utils.common_upscale(
96
+ image.clone().movedim(-1, 1), resized_width, resized_height, method, crop="disabled"
97
+ ).movedim(1, -1)
98
+ OB, OH, OW, OC = out_image.shape
99
+
100
+ if fit != "contain":
101
+ # First, we crop, then we pad; no need to check fit (other than not 'contain') since the size
102
+ # should already be correct.
103
+ if OW > width:
104
+ out_image = out_image.narrow(-2, (OW - width) // 2, width)
105
+ if OH > height:
106
+ out_image = out_image.narrow(-3, (OH - height) // 2, height)
107
+
108
+ OB, OH, OW, OC = out_image.shape
109
+ if width != OW or height != OH:
110
+ padded_image = torch.zeros((OB, height, width, OC), dtype=image.dtype, device=image.device)
111
+ x = (width - OW) // 2
112
+ y = (height - OH) // 2
113
+ for b in range(OB):
114
+ padded_image[b, y:y + OH, x:x + OW, :] = out_image[b]
115
+ out_image = padded_image
116
+
117
+ return (out_image, out_image.shape[2], out_image.shape[1])
custom_nodes/rgthree-comfy/py/ksampler_config.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Some basic config stuff I use for SDXL."""
2
+
3
+ from .constants import get_category, get_name
4
+ from nodes import MAX_RESOLUTION
5
+ import comfy.samplers
6
+
7
+
8
+ class RgthreeKSamplerConfig:
9
+ """Some basic config stuff I started using for SDXL, but useful in other spots too."""
10
+
11
+ NAME = get_name('KSampler Config')
12
+ CATEGORY = get_category()
13
+
14
+ @classmethod
15
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
16
+ return {
17
+ "required": {
18
+ "steps_total": ("INT", {
19
+ "default": 30,
20
+ "min": 1,
21
+ "max": MAX_RESOLUTION,
22
+ "step": 1,
23
+ }),
24
+ "refiner_step": ("INT", {
25
+ "default": 24,
26
+ "min": 1,
27
+ "max": MAX_RESOLUTION,
28
+ "step": 1,
29
+ }),
30
+ "cfg": ("FLOAT", {
31
+ "default": 8.0,
32
+ "min": 0.0,
33
+ "max": 100.0,
34
+ "step": 0.5,
35
+ }),
36
+ "sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
37
+ "scheduler": (comfy.samplers.KSampler.SCHEDULERS,),
38
+ #"refiner_ascore_pos": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01}),
39
+ #"refiner_ascore_neg": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 1000.0, "step": 0.01}),
40
+ },
41
+ }
42
+
43
+ RETURN_TYPES = ("INT", "INT", "FLOAT", comfy.samplers.KSampler.SAMPLERS,
44
+ comfy.samplers.KSampler.SCHEDULERS)
45
+ RETURN_NAMES = ("STEPS", "REFINER_STEP", "CFG", "SAMPLER", "SCHEDULER")
46
+ FUNCTION = "main"
47
+
48
+ def main(self, steps_total, refiner_step, cfg, sampler_name, scheduler):
49
+ """main"""
50
+ return (
51
+ steps_total,
52
+ refiner_step,
53
+ cfg,
54
+ sampler_name,
55
+ scheduler,
56
+ )
custom_nodes/rgthree-comfy/py/log.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import time
3
+ from .pyproject import NAME
4
+
5
+ # https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
6
+ # https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
7
+ COLORS = {
8
+ 'BLACK': '\33[30m',
9
+ 'RED': '\33[31m',
10
+ 'GREEN': '\33[32m',
11
+ 'YELLOW': '\33[33m',
12
+ 'BLUE': '\33[34m',
13
+ 'MAGENTA': '\33[35m',
14
+ 'CYAN': '\33[36m',
15
+ 'WHITE': '\33[37m',
16
+ 'GREY': '\33[90m',
17
+ 'BRIGHT_RED': '\33[91m',
18
+ 'BRIGHT_GREEN': '\33[92m',
19
+ 'BRIGHT_YELLOW': '\33[93m',
20
+ 'BRIGHT_BLUE': '\33[94m',
21
+ 'BRIGHT_MAGENTA': '\33[95m',
22
+ 'BRIGHT_CYAN': '\33[96m',
23
+ 'BRIGHT_WHITE': '\33[97m',
24
+ # Styles.
25
+ 'RESET': '\33[0m', # Note, Portainer doesn't like 00 here, so we'll use 0. Should be fine...
26
+ 'BOLD': '\33[01m',
27
+ 'NORMAL': '\33[22m',
28
+ 'ITALIC': '\33[03m',
29
+ 'UNDERLINE': '\33[04m',
30
+ 'BLINK': '\33[05m',
31
+ 'BLINK2': '\33[06m',
32
+ 'SELECTED': '\33[07m',
33
+ # Backgrounds
34
+ 'BG_BLACK': '\33[40m',
35
+ 'BG_RED': '\33[41m',
36
+ 'BG_GREEN': '\33[42m',
37
+ 'BG_YELLOW': '\33[43m',
38
+ 'BG_BLUE': '\33[44m',
39
+ 'BG_MAGENTA': '\33[45m',
40
+ 'BG_CYAN': '\33[46m',
41
+ 'BG_WHITE': '\33[47m',
42
+ 'BG_GREY': '\33[100m',
43
+ 'BG_BRIGHT_RED': '\33[101m',
44
+ 'BG_BRIGHT_GREEN': '\33[102m',
45
+ 'BG_BRIGHT_YELLOW': '\33[103m',
46
+ 'BG_BRIGHT_BLUE': '\33[104m',
47
+ 'BG_BRIGHT_MAGENTA': '\33[105m',
48
+ 'BG_BRIGHT_CYAN': '\33[106m',
49
+ 'BG_BRIGHT_WHITE': '\33[107m',
50
+ }
51
+
52
+
53
+ def log_node_success(node_name, message, msg_color='RESET'):
54
+ """Logs a success message."""
55
+ _log_node("BRIGHT_GREEN", node_name, message, msg_color=msg_color)
56
+
57
+
58
+ def log_node_info(node_name, message, msg_color='RESET'):
59
+ """Logs an info message."""
60
+ _log_node("CYAN", node_name, message, msg_color=msg_color)
61
+
62
+
63
+ def log_node_error(node_name, message, msg_color='RESET'):
64
+ """Logs an info message."""
65
+ _log_node("RED", node_name, message, msg_color=msg_color)
66
+
67
+
68
+ def log_node_warn(node_name, message, msg_color='RESET'):
69
+ """Logs an warn message."""
70
+ _log_node("YELLOW", node_name, message, msg_color=msg_color)
71
+
72
+
73
+ def log_node(node_name, message, msg_color='RESET'):
74
+ """Logs a message."""
75
+ _log_node("CYAN", node_name, message, msg_color=msg_color)
76
+
77
+
78
+ def _log_node(color, node_name, message, msg_color='RESET'):
79
+ """Logs for a node message."""
80
+ log(message, color=color, prefix=node_name.replace(" (rgthree)", ""), msg_color=msg_color)
81
+
82
+ LOGGED = {}
83
+
84
+ def log(message, color=None, msg_color=None, prefix=None, id=None, at_most_secs=None):
85
+ """Basic logging."""
86
+ now = int(time.time())
87
+ if id:
88
+ if at_most_secs is None:
89
+ raise ValueError('at_most_secs should be set if an id is set.')
90
+ if id in LOGGED:
91
+ last_logged = LOGGED[id]
92
+ if now < last_logged + at_most_secs:
93
+ return
94
+ LOGGED[id] = now
95
+ color = COLORS[color] if color is not None and color in COLORS else COLORS["BRIGHT_GREEN"]
96
+ msg_color = COLORS[msg_color] if msg_color is not None and msg_color in COLORS else ''
97
+ prefix = f'[{prefix}]' if prefix is not None else ''
98
+ msg = f'{color}[{NAME}]{prefix}'
99
+ msg += f'{msg_color} {message}{COLORS["RESET"]}'
100
+ print(msg)
custom_nodes/rgthree-comfy/py/lora_stack.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .constants import get_category, get_name
2
+ from nodes import LoraLoader
3
+ import folder_paths
4
+
5
+
6
+ class RgthreeLoraLoaderStack:
7
+
8
+ NAME = get_name('Lora Loader Stack')
9
+ CATEGORY = get_category()
10
+
11
+ @classmethod
12
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
13
+ return {
14
+ "required": {
15
+ "model": ("MODEL",),
16
+ "clip": ("CLIP", ),
17
+
18
+ "lora_01": (['None'] + folder_paths.get_filename_list("loras"), ),
19
+ "strength_01":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
20
+
21
+ "lora_02": (['None'] + folder_paths.get_filename_list("loras"), ),
22
+ "strength_02":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
23
+
24
+ "lora_03": (['None'] + folder_paths.get_filename_list("loras"), ),
25
+ "strength_03":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
26
+
27
+ "lora_04": (['None'] + folder_paths.get_filename_list("loras"), ),
28
+ "strength_04":("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
29
+ }
30
+ }
31
+
32
+ RETURN_TYPES = ("MODEL", "CLIP")
33
+ FUNCTION = "load_lora"
34
+
35
+ def load_lora(self, model, clip, lora_01, strength_01, lora_02, strength_02, lora_03, strength_03, lora_04, strength_04):
36
+ if lora_01 != "None" and strength_01 != 0:
37
+ model, clip = LoraLoader().load_lora(model, clip, lora_01, strength_01, strength_01)
38
+ if lora_02 != "None" and strength_02 != 0:
39
+ model, clip = LoraLoader().load_lora(model, clip, lora_02, strength_02, strength_02)
40
+ if lora_03 != "None" and strength_03 != 0:
41
+ model, clip = LoraLoader().load_lora(model, clip, lora_03, strength_03, strength_03)
42
+ if lora_04 != "None" and strength_04 != 0:
43
+ model, clip = LoraLoader().load_lora(model, clip, lora_04, strength_04, strength_04)
44
+
45
+ return (model, clip)
46
+
custom_nodes/rgthree-comfy/py/power_lora_loader.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import folder_paths
2
+
3
+ from typing import Union
4
+
5
+ from nodes import LoraLoader
6
+ from .constants import get_category, get_name
7
+ from .power_prompt_utils import get_lora_by_filename
8
+ from .utils import FlexibleOptionalInputType, any_type
9
+ from .server.utils_info import get_model_info_file_data
10
+ from .log import log_node_warn
11
+
12
+ NODE_NAME = get_name('Power Lora Loader')
13
+
14
+
15
+ class RgthreePowerLoraLoader:
16
+ """ The Power Lora Loader is a powerful, flexible node to add multiple loras to a model/clip."""
17
+
18
+ NAME = NODE_NAME
19
+ CATEGORY = get_category()
20
+
21
+ @classmethod
22
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
23
+ return {
24
+ "required": {
25
+ },
26
+ # Since we will pass any number of loras in from the UI, this needs to always allow an
27
+ "optional": FlexibleOptionalInputType(type=any_type, data={
28
+ "model": ("MODEL",),
29
+ "clip": ("CLIP",),
30
+ }),
31
+ "hidden": {},
32
+ }
33
+
34
+ RETURN_TYPES = ("MODEL", "CLIP")
35
+ RETURN_NAMES = ("MODEL", "CLIP")
36
+ FUNCTION = "load_loras"
37
+
38
+ def load_loras(self, model=None, clip=None, **kwargs):
39
+ """Loops over the provided loras in kwargs and applies valid ones."""
40
+ for key, value in kwargs.items():
41
+ key = key.upper()
42
+ if key.startswith('LORA_') and 'on' in value and 'lora' in value and 'strength' in value:
43
+ strength_model = value['strength']
44
+ # If we just passed one strength value, then use it for both, if we passed a strengthTwo
45
+ # as well, then our `strength` will be for the model, and `strengthTwo` for clip.
46
+ strength_clip = value['strengthTwo'] if 'strengthTwo' in value else None
47
+ if clip is None:
48
+ if strength_clip is not None and strength_clip != 0:
49
+ log_node_warn(NODE_NAME, 'Recieved clip strength eventhough no clip supplied!')
50
+ strength_clip = 0
51
+ else:
52
+ strength_clip = strength_clip if strength_clip is not None else strength_model
53
+ if value['on'] and (strength_model != 0 or strength_clip != 0):
54
+ lora = get_lora_by_filename(value['lora'], log_node=self.NAME)
55
+ if model is not None and lora is not None:
56
+ model, clip = LoraLoader().load_lora(model, clip, lora, strength_model, strength_clip)
57
+
58
+ return (model, clip)
59
+
60
+ @classmethod
61
+ def get_enabled_loras_from_prompt_node(cls,
62
+ prompt_node: dict) -> list[dict[str, Union[str, float]]]:
63
+ """Gets enabled loras of a node within a server prompt."""
64
+ result = []
65
+ for name, lora in prompt_node['inputs'].items():
66
+ if name.startswith('lora_') and lora['on']:
67
+ lora_file = get_lora_by_filename(lora['lora'], log_node=cls.NAME)
68
+ if lora_file is not None: # Add the same safety check
69
+ lora_dict = {
70
+ 'name': lora['lora'],
71
+ 'strength': lora['strength'],
72
+ 'path': folder_paths.get_full_path("loras", lora_file)
73
+ }
74
+ if 'strengthTwo' in lora:
75
+ lora_dict['strength_clip'] = lora['strengthTwo']
76
+ result.append(lora_dict)
77
+ return result
78
+
79
+ @classmethod
80
+ def get_enabled_triggers_from_prompt_node(cls, prompt_node: dict, max_each: int = 1):
81
+ """Gets trigger words up to the max for enabled loras of a node within a server prompt."""
82
+ loras = [l['name'] for l in cls.get_enabled_loras_from_prompt_node(prompt_node)]
83
+ trained_words = []
84
+ for lora in loras:
85
+ info = get_model_info_file_data(lora, 'loras', default={})
86
+ if not info or not info.keys():
87
+ log_node_warn(
88
+ NODE_NAME,
89
+ f'No info found for lora {lora} when grabbing triggers. Have you generated an info file'
90
+ ' from the Power Lora Loader "Show Info" dialog?'
91
+ )
92
+ continue
93
+ if 'trainedWords' not in info or not info['trainedWords']:
94
+ log_node_warn(
95
+ NODE_NAME,
96
+ f'No trained words for lora {lora} when grabbing triggers. Have you fetched data from'
97
+ 'civitai or manually added words?'
98
+ )
99
+ continue
100
+ trained_words += [w for wi in info['trainedWords'][:max_each] if (wi and (w := wi['word']))]
101
+ return trained_words
custom_nodes/rgthree-comfy/py/power_primitive.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ from .utils import FlexibleOptionalInputType, any_type
4
+ from .constants import get_category, get_name
5
+
6
+
7
+ def cast_to_str(x):
8
+ """Handles our cast to a string."""
9
+ if x is None:
10
+ return ''
11
+ try:
12
+ return str(x)
13
+ except (ValueError, TypeError):
14
+ return ''
15
+
16
+
17
+ def cast_to_float(x):
18
+ """Handles our cast to a float."""
19
+ try:
20
+ return float(x)
21
+ except (ValueError, TypeError):
22
+ return 0.0
23
+
24
+
25
+ def cast_to_bool(x):
26
+ """Handles our cast to a bool."""
27
+ try:
28
+ return bool(float(x))
29
+ except (ValueError, TypeError):
30
+ return str(x).lower() not in ['0', 'false', 'null', 'none', '']
31
+
32
+
33
+ output_to_type = {
34
+ 'STRING': {
35
+ 'cast': cast_to_str,
36
+ 'null': '',
37
+ },
38
+ 'FLOAT': {
39
+ 'cast': cast_to_float,
40
+ 'null': 0.0,
41
+ },
42
+ 'INT': {
43
+ 'cast': lambda x: int(cast_to_float(x)),
44
+ 'null': 0,
45
+ },
46
+ 'BOOLEAN': {
47
+ 'cast': cast_to_bool,
48
+ 'null': False,
49
+ },
50
+ # This can be removed soon, there was a bug where this should have been BOOLEAN
51
+ 'BOOL': {
52
+ 'cast': cast_to_bool,
53
+ 'null': False,
54
+ },
55
+ }
56
+
57
+
58
+ class RgthreePowerPrimitive:
59
+ """The Power Primitive Node."""
60
+
61
+ NAME = get_name('Power Primitive')
62
+ CATEGORY = get_category()
63
+
64
+ @classmethod
65
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
66
+ return {
67
+ "required": {},
68
+ "optional": FlexibleOptionalInputType(any_type),
69
+ }
70
+
71
+ RETURN_TYPES = (any_type,)
72
+ RETURN_NAMES = ('*',)
73
+ FUNCTION = "main"
74
+
75
+ def main(self, **kwargs):
76
+ """Outputs the expected type."""
77
+ output = kwargs.get('value', None)
78
+ output_type = re.sub(r'\s*\([^\)]*\)\s*$', '', kwargs.get('type', ''))
79
+ output_type = output_to_type[output_type]
80
+ cast = output_type['cast']
81
+ output = cast(output)
82
+
83
+ return (output,)
custom_nodes/rgthree-comfy/py/power_prompt.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from .log import log_node_warn, log_node_info, log_node_success
4
+
5
+ from .constants import get_category, get_name
6
+ from .power_prompt_utils import get_and_strip_loras
7
+ from nodes import LoraLoader, CLIPTextEncode
8
+ import folder_paths
9
+
10
+ NODE_NAME = get_name('Power Prompt')
11
+
12
+
13
+ class RgthreePowerPrompt:
14
+
15
+ NAME = NODE_NAME
16
+ CATEGORY = get_category()
17
+
18
+ @classmethod
19
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
20
+ # Removed Saved Prompts feature; No sure it worked any longer. UI should fail gracefully,
21
+ # TODO: Rip out saved prompt input data
22
+ SAVED_PROMPTS_FILES=[]
23
+ SAVED_PROMPTS_CONTENT=[]
24
+ return {
25
+ 'required': {
26
+ 'prompt': ('STRING', {
27
+ 'multiline': True,
28
+ 'dynamicPrompts': True
29
+ }),
30
+ },
31
+ 'optional': {
32
+ "opt_model": ("MODEL",),
33
+ "opt_clip": ("CLIP",),
34
+ 'insert_lora': (['CHOOSE', 'DISABLE LORAS'] +
35
+ [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('loras')],),
36
+ 'insert_embedding': ([
37
+ 'CHOOSE',
38
+ ] + [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('embeddings')],),
39
+ 'insert_saved': ([
40
+ 'CHOOSE',
41
+ ] + SAVED_PROMPTS_FILES,),
42
+ },
43
+ 'hidden': {
44
+ 'values_insert_saved': (['CHOOSE'] + SAVED_PROMPTS_CONTENT,),
45
+ }
46
+ }
47
+
48
+ RETURN_TYPES = (
49
+ 'CONDITIONING',
50
+ 'MODEL',
51
+ 'CLIP',
52
+ 'STRING',
53
+ )
54
+ RETURN_NAMES = (
55
+ 'CONDITIONING',
56
+ 'MODEL',
57
+ 'CLIP',
58
+ 'TEXT',
59
+ )
60
+ FUNCTION = 'main'
61
+
62
+ def main(self,
63
+ prompt,
64
+ opt_model=None,
65
+ opt_clip=None,
66
+ insert_lora=None,
67
+ insert_embedding=None,
68
+ insert_saved=None,
69
+ values_insert_saved=None):
70
+ if insert_lora == 'DISABLE LORAS':
71
+ prompt, loras, skipped, unfound = get_and_strip_loras(prompt, log_node=NODE_NAME, silent=True)
72
+ log_node_info(
73
+ NODE_NAME,
74
+ f'Disabling all found loras ({len(loras)}) and stripping lora tags for TEXT output.')
75
+ elif opt_model is not None and opt_clip is not None:
76
+ prompt, loras, skipped, unfound = get_and_strip_loras(prompt, log_node=NODE_NAME)
77
+ if len(loras) > 0:
78
+ for lora in loras:
79
+ opt_model, opt_clip = LoraLoader().load_lora(opt_model, opt_clip, lora['lora'],
80
+ lora['strength'], lora['strength'])
81
+ log_node_success(NODE_NAME, f'Loaded "{lora["lora"]}" from prompt')
82
+ log_node_info(NODE_NAME, f'{len(loras)} Loras processed; stripping tags for TEXT output.')
83
+ elif '<lora:' in prompt:
84
+ prompt, loras, skipped, unfound = get_and_strip_loras(prompt, log_node=NODE_NAME, silent=True)
85
+ total_loras = len(loras) + len(skipped) + len(unfound)
86
+ if total_loras:
87
+ log_node_warn(
88
+ NODE_NAME, f'Found {len(loras)} lora tags in prompt but model & clip were not supplied!')
89
+ log_node_info(NODE_NAME, 'Loras not processed, keeping for TEXT output.')
90
+
91
+ conditioning = None
92
+ if opt_clip is not None:
93
+ conditioning = CLIPTextEncode().encode(opt_clip, prompt)[0]
94
+
95
+ return (conditioning, opt_model, opt_clip, prompt)
custom_nodes/rgthree-comfy/py/power_prompt_simple.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import folder_paths
3
+ from nodes import CLIPTextEncode
4
+ from .constants import get_category, get_name
5
+ from .power_prompt import RgthreePowerPrompt
6
+
7
+ class RgthreePowerPromptSimple(RgthreePowerPrompt):
8
+
9
+ NAME=get_name('Power Prompt - Simple')
10
+ CATEGORY = get_category()
11
+
12
+ @classmethod
13
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
14
+ # Removed Saved Prompts feature; No sure it worked any longer. UI should fail gracefully,
15
+ # TODO: Rip out saved prompt input data
16
+ SAVED_PROMPTS_FILES=[]
17
+ SAVED_PROMPTS_CONTENT=[]
18
+ return {
19
+ 'required': {
20
+ 'prompt': ('STRING', {'multiline': True, 'dynamicPrompts': True}),
21
+ },
22
+ 'optional': {
23
+ "opt_clip": ("CLIP", ),
24
+ 'insert_embedding': (['CHOOSE',] + [os.path.splitext(x)[0] for x in folder_paths.get_filename_list('embeddings')],),
25
+ 'insert_saved': (['CHOOSE',] + SAVED_PROMPTS_FILES,),
26
+ },
27
+ 'hidden': {
28
+ 'values_insert_saved': (['CHOOSE'] + SAVED_PROMPTS_CONTENT,),
29
+ }
30
+ }
31
+
32
+ RETURN_TYPES = ('CONDITIONING', 'STRING',)
33
+ RETURN_NAMES = ('CONDITIONING', 'TEXT',)
34
+ FUNCTION = 'main'
35
+
36
+ def main(self, prompt, opt_clip=None, insert_embedding=None, insert_saved=None, values_insert_saved=None):
37
+ conditioning=None
38
+ if opt_clip != None:
39
+ conditioning = CLIPTextEncode().encode(opt_clip, prompt)[0]
40
+
41
+ return (conditioning, prompt)
42
+
custom_nodes/rgthree-comfy/py/power_prompt_utils.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utilities for Power Prompt nodes."""
2
+ import re
3
+ import os
4
+ import folder_paths
5
+
6
+ from .log import log_node_warn, log_node_info
7
+
8
+
9
+ def get_and_strip_loras(prompt, silent=False, log_node="Power Prompt"):
10
+ """Collects and strips lora tags from a prompt."""
11
+ pattern = r'<lora:([^:>]*?)(?::(-?\d*(?:\.\d*)?))?>'
12
+ lora_paths = folder_paths.get_filename_list('loras')
13
+
14
+ matches = re.findall(pattern, prompt)
15
+
16
+ loras = []
17
+ unfound_loras = []
18
+ skipped_loras = []
19
+ for match in matches:
20
+ tag_path = match[0]
21
+
22
+ strength = float(match[1] if len(match) > 1 and len(match[1]) else 1.0)
23
+ if strength == 0:
24
+ if not silent:
25
+ log_node_info(log_node, f'Skipping "{tag_path}" with strength of zero')
26
+ skipped_loras.append({'lora': tag_path, 'strength': strength})
27
+ continue
28
+
29
+ lora_path = get_lora_by_filename(tag_path, lora_paths, log_node=None if silent else log_node)
30
+ if lora_path is None:
31
+ unfound_loras.append({'lora': tag_path, 'strength': strength})
32
+ continue
33
+
34
+ loras.append({'lora': lora_path, 'strength': strength})
35
+
36
+ return (re.sub(pattern, '', prompt), loras, skipped_loras, unfound_loras)
37
+
38
+
39
+ # pylint: disable = too-many-return-statements, too-many-branches
40
+ def get_lora_by_filename(file_path, lora_paths=None, log_node=None):
41
+ """Returns a lora by filename, looking for exactl paths and then fuzzier matching."""
42
+ lora_paths = lora_paths if lora_paths is not None else folder_paths.get_filename_list('loras')
43
+
44
+ if file_path in lora_paths:
45
+ return file_path
46
+
47
+ lora_paths_no_ext = [os.path.splitext(x)[0] for x in lora_paths]
48
+
49
+ # See if we've entered the exact path, but without the extension
50
+ if file_path in lora_paths_no_ext:
51
+ found = lora_paths[lora_paths_no_ext.index(file_path)]
52
+ return found
53
+
54
+ # Same check, but ensure file_path is without extension.
55
+ file_path_force_no_ext = os.path.splitext(file_path)[0]
56
+ if file_path_force_no_ext in lora_paths_no_ext:
57
+ found = lora_paths[lora_paths_no_ext.index(file_path_force_no_ext)]
58
+ return found
59
+
60
+ # See if we passed just the name, without paths.
61
+ lora_filenames_only = [os.path.basename(x) for x in lora_paths]
62
+ if file_path in lora_filenames_only:
63
+ found = lora_paths[lora_filenames_only.index(file_path)]
64
+ if log_node is not None:
65
+ log_node_info(log_node, f'Matched Lora input "{file_path}" to "{found}".')
66
+ return found
67
+
68
+ # Same, but force the input to be without paths
69
+ file_path_force_filename = os.path.basename(file_path)
70
+ lora_filenames_only = [os.path.basename(x) for x in lora_paths]
71
+ if file_path_force_filename in lora_filenames_only:
72
+ found = lora_paths[lora_filenames_only.index(file_path_force_filename)]
73
+ if log_node is not None:
74
+ log_node_info(log_node, f'Matched Lora input "{file_path}" to "{found}".')
75
+ return found
76
+
77
+ # Check the filenames and without extension.
78
+ lora_filenames_and_no_ext = [os.path.splitext(os.path.basename(x))[0] for x in lora_paths]
79
+ if file_path in lora_filenames_and_no_ext:
80
+ found = lora_paths[lora_filenames_and_no_ext.index(file_path)]
81
+ if log_node is not None:
82
+ log_node_info(log_node, f'Matched Lora input "{file_path}" to "{found}".')
83
+ return found
84
+
85
+ # And, one last forcing the input to be the same
86
+ file_path_force_filename_and_no_ext = os.path.splitext(os.path.basename(file_path))[0]
87
+ if file_path_force_filename_and_no_ext in lora_filenames_and_no_ext:
88
+ found = lora_paths[lora_filenames_and_no_ext.index(file_path_force_filename_and_no_ext)]
89
+ if log_node is not None:
90
+ log_node_info(log_node, f'Matched Lora input "{file_path}" to "{found}".')
91
+ return found
92
+
93
+ # Finally, super fuzzy, we'll just check if the input exists in the path at all.
94
+ for index, lora_path in enumerate(lora_paths):
95
+ if file_path in lora_path:
96
+ found = lora_paths[index]
97
+ if log_node is not None:
98
+ log_node_warn(log_node, f'Fuzzy-matched Lora input "{file_path}" to "{found}".')
99
+ return found
100
+
101
+ if log_node is not None:
102
+ log_node_warn(log_node, f'Lora "{file_path}" not found, skipping.')
103
+
104
+ return None
custom_nodes/rgthree-comfy/py/power_puter.py ADDED
@@ -0,0 +1,842 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The Power Puter is a powerful node that can compute and evaluate Python-like code safely allowing
2
+ for complex operations for primitives and workflow items for output. From string concatenation, to
3
+ math operations, list comprehension, and node value output.
4
+
5
+ Originally based off https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/aac13aa7ce35b07d43633c3bbe654a38c00d74f5/py/math_expression.py
6
+ under an MIT License https://github.com/pythongosssss/ComfyUI-Custom-Scripts/blob/aac13aa7ce35b07d43633c3bbe654a38c00d74f5/LICENSE
7
+ """
8
+
9
+ import math
10
+ import ast
11
+ import json
12
+ import random
13
+ import dataclasses
14
+ import re
15
+ import time
16
+ import operator as op
17
+ import datetime
18
+ import numpy as np
19
+
20
+ from typing import Any, Callable, Iterable, Optional, Union
21
+ from types import MappingProxyType
22
+
23
+ from .constants import get_category, get_name
24
+ from .utils import ByPassTypeTuple, FlexibleOptionalInputType, any_type, get_dict_value
25
+ from .log import log_node_error, log_node_warn, log_node_info
26
+
27
+ from .power_lora_loader import RgthreePowerLoraLoader
28
+
29
+ from nodes import ImageBatch
30
+ from comfy_extras.nodes_latent import LatentBatch
31
+
32
+
33
+ class LoopBreak(Exception):
34
+ """A special error type that is caught in a loop for correct breaking behavior."""
35
+
36
+ def __init__(self):
37
+ super().__init__('Cannot use "break" outside of a loop.')
38
+
39
+
40
+ class LoopContinue(Exception):
41
+ """A special error type that is caught in a loop for correct continue behavior."""
42
+
43
+ def __init__(self):
44
+ super().__init__('Cannot use "continue" outside of a loop.')
45
+
46
+
47
+ @dataclasses.dataclass(frozen=True) # Note, kw_only=True is only python 3.10+
48
+ class Function():
49
+ """Function data.
50
+
51
+ Attributes:
52
+ name: The name of the function as called from the node.
53
+ call: The callable (reference, lambda, etc), or a string if on _Puter instance.
54
+ args: A tuple that represents the minimum and maximum number of args (or arg for no limit).
55
+ """
56
+
57
+ name: str
58
+ call: Union[Callable, str]
59
+ args: tuple[int, Optional[int]]
60
+
61
+
62
+ def purge_vram(purge_models=True):
63
+ """Purges vram and, optionally, unloads models."""
64
+ import gc
65
+ import torch
66
+ gc.collect()
67
+ if torch.cuda.is_available():
68
+ torch.cuda.empty_cache()
69
+ torch.cuda.ipc_collect()
70
+ if purge_models:
71
+ import comfy
72
+ comfy.model_management.unload_all_models()
73
+ comfy.model_management.soft_empty_cache()
74
+
75
+
76
+ def batch(*args):
77
+ """Batches multiple image or latents together."""
78
+
79
+ def check_is_latent(item) -> bool:
80
+ return isinstance(item, dict) and 'samples' in item
81
+
82
+ args = list(args)
83
+ result = args.pop(0)
84
+ is_latent = check_is_latent(result)
85
+ node = LatentBatch() if is_latent else ImageBatch()
86
+
87
+ for arg in args:
88
+ if is_latent != check_is_latent(arg):
89
+ raise ValueError(
90
+ f'batch() error: Expecting "{"LATENT" if is_latent else "IMAGE"}"'
91
+ f' but got "{"IMAGE" if is_latent else "LATENT"}".'
92
+ )
93
+ result = node.batch(result, arg)[0]
94
+ return result
95
+
96
+
97
+ _BUILTIN_FN_PREFIX = '__rgthreefn.'
98
+
99
+
100
+ def _get_built_in_fn_key(fn: Function) -> str:
101
+ """Returns a key for a built-in function."""
102
+ return f'{_BUILTIN_FN_PREFIX}{hash(fn.name)}'
103
+
104
+
105
+ def _get_built_in_fn_by_key(fn_key: str):
106
+ """Returns the `Function` for the provided key (purposefully, not name)."""
107
+ if not fn_key.startswith(_BUILTIN_FN_PREFIX) or fn_key not in _BUILT_INS_BY_NAME_AND_KEY:
108
+ raise ValueError('No built in function found.')
109
+ return _BUILT_INS_BY_NAME_AND_KEY[fn_key]
110
+
111
+
112
+ _BUILT_IN_FNS_LIST = [
113
+ Function(name="round", call=round, args=(1, 2)),
114
+ Function(name="ceil", call=math.ceil, args=(1, 1)),
115
+ Function(name="floor", call=math.floor, args=(1, 1)),
116
+ Function(name="sqrt", call=math.sqrt, args=(1, 1)),
117
+ Function(name="min", call=min, args=(2, None)),
118
+ Function(name="max", call=max, args=(2, None)),
119
+ Function(name=".random_int", call=random.randint, args=(2, 2)),
120
+ Function(name=".random_choice", call=random.choice, args=(1, 1)),
121
+ Function(name=".random_seed", call=random.seed, args=(1, 1)),
122
+ Function(name="re", call=re.compile, args=(1, 1)),
123
+ Function(name="len", call=len, args=(1, 1)),
124
+ Function(name="enumerate", call=enumerate, args=(1, 1)),
125
+ Function(name="range", call=range, args=(1, 3)),
126
+ # Casts
127
+ Function(name="int", call=int, args=(1, 1)),
128
+ Function(name="float", call=float, args=(1, 1)),
129
+ Function(name="str", call=str, args=(1, 1)),
130
+ Function(name="bool", call=bool, args=(1, 1)),
131
+ Function(name="list", call=list, args=(1, 1)),
132
+ Function(name="tuple", call=tuple, args=(1, 1)),
133
+ # Special
134
+ Function(name="dir", call=dir, args=(1, 1)),
135
+ Function(name="type", call=type, args=(1, 1)),
136
+ Function(name="print", call=print, args=(0, None)),
137
+ # Comfy Specials
138
+ Function(name="node", call='_get_node', args=(0, 1)),
139
+ Function(name="nodes", call='_get_nodes', args=(0, 1)),
140
+ Function(name="input_node", call='_get_input_node', args=(0, 1)),
141
+ Function(name="purge_vram", call=purge_vram, args=(0, 1)),
142
+ Function(name="batch", call=batch, args=(2, None)),
143
+ ]
144
+
145
+ _BUILT_INS_BY_NAME_AND_KEY = {
146
+ fn.name: fn for fn in _BUILT_IN_FNS_LIST
147
+ } | {
148
+ key: fn for fn in _BUILT_IN_FNS_LIST if (key := _get_built_in_fn_key(fn))
149
+ }
150
+
151
+ _BUILT_INS = MappingProxyType(
152
+ {fn.name: key for fn in _BUILT_IN_FNS_LIST if (key := _get_built_in_fn_key(fn))} | {
153
+ 'random':
154
+ MappingProxyType({
155
+ 'int': _get_built_in_fn_key(_BUILT_INS_BY_NAME_AND_KEY['.random_int']),
156
+ 'choice': _get_built_in_fn_key(_BUILT_INS_BY_NAME_AND_KEY['.random_choice']),
157
+ 'seed': _get_built_in_fn_key(_BUILT_INS_BY_NAME_AND_KEY['.random_seed']),
158
+ }),
159
+ }
160
+ )
161
+
162
+ # A dict of types to blocked attributes/methods. Used to disallow file system access or other
163
+ # invocations we may want to block. Necessary for any instance type that is possible to create from
164
+ # the code or standard ComfyUI inputs.
165
+ #
166
+ # For instance, a user does not have access to the numpy module directly, so they cannot invoke
167
+ # `numpy.save`. However, a user can access a numpy.ndarray instance from a tensor and, from there,
168
+ # an attempt to call `tofile` or `dump` etc. would need to be blocked.
169
+ _BLOCKED_METHODS_OR_ATTRS = MappingProxyType({np.ndarray: ['tofile', 'dump']})
170
+
171
+ # Special functions by class type (called from the Attrs.)
172
+ _SPECIAL_FUNCTIONS = {
173
+ RgthreePowerLoraLoader.NAME: {
174
+ # Get a list of the enabled loras from a power lora loader.
175
+ "loras": RgthreePowerLoraLoader.get_enabled_loras_from_prompt_node,
176
+ "triggers": RgthreePowerLoraLoader.get_enabled_triggers_from_prompt_node,
177
+ }
178
+ }
179
+
180
+ # Series of regex checks for usage of a non-deterministic function. Using these is fine, but means
181
+ # the output can't be cached because it's either random, or is associated with another node that is
182
+ # not connected to ours (like looking up a node in the prompt). Using these means downstream nodes
183
+ # would always be run; that is fine for something like a final JSON output, but less so for a prompt
184
+ # text.
185
+ _NON_DETERMINISTIC_FUNCTION_CHECKS = [r'(?<!input_)(nodes?)\(',]
186
+
187
+ _OPERATORS = {
188
+ # operator
189
+ ast.Add: op.add,
190
+ ast.Sub: op.sub,
191
+ ast.Mult: op.mul,
192
+ ast.MatMult: op.matmul,
193
+ ast.Div: op.truediv,
194
+ ast.Mod: op.mod,
195
+ ast.Pow: op.pow,
196
+ ast.RShift: op.rshift,
197
+ ast.LShift: op.lshift,
198
+ ast.BitOr: op.or_,
199
+ ast.BitXor: op.xor,
200
+ ast.BitAnd: op.and_,
201
+ ast.FloorDiv: op.floordiv,
202
+ # boolop
203
+ ast.And: lambda a, b: a and b,
204
+ ast.Or: lambda a, b: a or b,
205
+ # unaryop
206
+ ast.Invert: op.invert,
207
+ ast.Not: lambda a: 0 if a else 1,
208
+ ast.USub: op.neg,
209
+ # cmpop
210
+ ast.Eq: op.eq,
211
+ ast.NotEq: op.ne,
212
+ ast.Lt: op.lt,
213
+ ast.LtE: op.le,
214
+ ast.Gt: op.gt,
215
+ ast.GtE: op.ge,
216
+ ast.Is: op.is_,
217
+ ast.IsNot: op.is_not,
218
+ ast.In: lambda a, b: a in b,
219
+ ast.NotIn: lambda a, b: a not in b,
220
+
221
+ }
222
+
223
+ _NODE_NAME = get_name("Power Puter")
224
+
225
+
226
+ def _update_code(code: str, unique_id: str, log=False):
227
+ """Updates the code to either newer syntax or general cleaning."""
228
+
229
+ # Change usage of `input_node` so the passed variable is a string, if it isn't. So, instead of
230
+ # `input_node(a)` it needs to be `input_node('a')`
231
+ code = re.sub(r'input_node\(([^\'"].*?)\)', r'input_node("\1")', code)
232
+
233
+ # Update use of `random_int` to `random.int`
234
+ srch = re.compile(r'random_int\(')
235
+ if re.search(srch, code):
236
+ if log:
237
+ log_node_warn(
238
+ _NODE_NAME, f"Power Puter node #{unique_id} should update to use the `random.int`"
239
+ " built-in instead of `random_int`."
240
+ )
241
+ code = re.sub(srch, 'random.int(', code)
242
+
243
+ # Update use of `random_choice` to `random.choice`
244
+ srch = re.compile(r'random_choice\(')
245
+ if re.search(srch, code):
246
+ if log:
247
+ log_node_warn(
248
+ _NODE_NAME, f"Power Puter node #{unique_id} should update to use the `random.choice`"
249
+ " built-in instead of `random_choice`."
250
+ )
251
+ code = re.sub(srch, 'random.choice(', code)
252
+ return code
253
+
254
+
255
+ class RgthreePowerPuter:
256
+ """A powerful node that can compute and evaluate expressions and output as various types."""
257
+
258
+ NAME = _NODE_NAME
259
+ CATEGORY = get_category()
260
+
261
+ @classmethod
262
+ def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring
263
+ return {
264
+ "required": {},
265
+ "optional": FlexibleOptionalInputType(any_type),
266
+ "hidden": {
267
+ "unique_id": "UNIQUE_ID",
268
+ "extra_pnginfo": "EXTRA_PNGINFO",
269
+ "prompt": "PROMPT",
270
+ "dynprompt": "DYNPROMPT"
271
+ },
272
+ }
273
+
274
+ RETURN_TYPES = ByPassTypeTuple((any_type,))
275
+ RETURN_NAMES = ByPassTypeTuple(("*",))
276
+ FUNCTION = "main"
277
+
278
+ @classmethod
279
+ def IS_CHANGED(cls, **kwargs):
280
+ """Forces a changed state if we could be unaware of data changes (like using `node()`)."""
281
+
282
+ code = _update_code(kwargs['code'], unique_id=kwargs['unique_id'])
283
+ # Strip string literals and comments.
284
+ code = re.sub(r"'[^']+?'", "''", code)
285
+ code = re.sub(r'"[^"]+?"', '""', code)
286
+ code = re.sub(r'#.*\n', '\n', code)
287
+
288
+ # If we have a non-deterministic function, then we'll always consider ourself changed since we
289
+ # cannot be sure that the data would be the same (random, another unconnected node, etc).
290
+ for check in _NON_DETERMINISTIC_FUNCTION_CHECKS:
291
+ matches = re.search(check, code)
292
+ if matches:
293
+ log_node_warn(
294
+ _NODE_NAME,
295
+ f"Note, Power Puter (node #{kwargs['unique_id']}) cannot be cached b/c it's using a"
296
+ f" non-deterministic function call. Matches function call for '{matches.group(1)}'."
297
+ )
298
+ return time.time()
299
+
300
+ # Advanced checks.
301
+ has_rand_seed = re.search(r'random\.seed\(', code)
302
+ has_rand_int_or_choice = re.search(r'(?<!\.)(random\.(int|choice))\(', code)
303
+ if has_rand_int_or_choice:
304
+ if not has_rand_seed or has_rand_seed.span()[0] > has_rand_int_or_choice.span()[0]:
305
+ log_node_warn(
306
+ _NODE_NAME,
307
+ f"Note, Power Puter (node #{kwargs['unique_id']}) cannot be cached b/c it's using a"
308
+ " non-deterministic function call. Matches function call for"
309
+ f" `{has_rand_int_or_choice.group(1)}`."
310
+ )
311
+ return time.time()
312
+ if has_rand_seed:
313
+ log_node_info(
314
+ _NODE_NAME,
315
+ f"Power Puter node #{kwargs['unique_id']} WILL be cached eventhough it's using"
316
+ f" a non-deterministic random call `{has_rand_int_or_choice.group(1)}` because it also"
317
+ f" calls `random.seed` first. NOTE: Please ensure that the seed value is deterministic."
318
+ )
319
+
320
+ return 42
321
+
322
+ def main(self, **kwargs):
323
+ """Does the nodes' work."""
324
+ code = kwargs['code']
325
+ unique_id = kwargs['unique_id']
326
+ pnginfo = kwargs['extra_pnginfo']
327
+ workflow = pnginfo["workflow"] if "workflow" in pnginfo else {"nodes": []}
328
+ prompt = kwargs['prompt']
329
+ dynprompt = kwargs['dynprompt']
330
+
331
+ outputs = get_dict_value(kwargs, 'outputs.outputs', None)
332
+ if not outputs:
333
+ output = kwargs.get('output', None)
334
+ if not output:
335
+ output = 'STRING'
336
+ outputs = [output]
337
+
338
+ ctx = {}
339
+ # Set variable names, defaulting to None instead of KeyErrors
340
+ for c in list('abcdefghijklmnopqrstuvwxyz'):
341
+ ctx[c] = kwargs[c] if c in kwargs else None
342
+
343
+ code = _update_code(kwargs['code'], unique_id=kwargs['unique_id'], log=True)
344
+
345
+ eva = _Puter(
346
+ code=code,
347
+ ctx=ctx,
348
+ workflow=workflow,
349
+ prompt=prompt,
350
+ dynprompt=dynprompt,
351
+ unique_id=unique_id
352
+ )
353
+ values = eva.execute()
354
+
355
+ # Check if we have multiple outputs that the returned value is a tuple and raise if not.
356
+ if len(outputs) > 1 and not isinstance(values, tuple):
357
+ t = re.sub(r'^<[a-z]*\s(.*?)>$', r'\1', str(type(values)))
358
+ msg = (
359
+ f"When using multiple node outputs, the value from the code should be a 'tuple' with the"
360
+ f" number of items equal to the number of outputs. But value from code was of type {t}."
361
+ )
362
+ log_node_error(_NODE_NAME, f'{msg}\n')
363
+ raise ValueError(msg)
364
+
365
+ if len(outputs) == 1:
366
+ values = (values,)
367
+
368
+ if len(values) > len(outputs):
369
+ log_node_warn(
370
+ _NODE_NAME,
371
+ f"Expected value from code to be tuple with {len(outputs)} items, but value from code had"
372
+ f" {len(values)} items. Extra values will be dropped."
373
+ )
374
+ elif len(values) < len(outputs):
375
+ log_node_warn(
376
+ _NODE_NAME,
377
+ f"Expected value from code to be tuple with {len(outputs)} items, but value from code had"
378
+ f" {len(values)} items. Extra outputs will be null."
379
+ )
380
+
381
+ # Now, we'll go over out return tuple, and cast as the output types.
382
+ response = []
383
+ for i, output in enumerate(outputs):
384
+ value = values[i] if len(values) > i else None
385
+ if value is not None:
386
+ if output == 'INT':
387
+ value = int(value)
388
+ elif output == 'FLOAT':
389
+ value = float(value)
390
+ # Accidentally defined "BOOL" when should have been "BOOLEAN."
391
+ # TODO: Can prob get rid of BOOl after a bit when UIs would be updated from sending
392
+ # BOOL incorrectly.
393
+ elif output in ('BOOL', 'BOOLEAN'):
394
+ value = bool(value)
395
+ elif output == 'STRING':
396
+ if isinstance(value, (dict, list)):
397
+ value = json.dumps(value, indent=2)
398
+ else:
399
+ value = str(value)
400
+ elif output == '*':
401
+ # Do nothing, the output will be passed as-is. This could be anything and it's up to the
402
+ # user to control the intended output, like passing through an input value, etc.
403
+ pass
404
+ response.append(value)
405
+ return tuple(response)
406
+
407
+
408
+ class _Puter:
409
+ """The main computation evaluator, using ast.parse the code.
410
+
411
+ See https://www.basicexamples.com/example/python/ast for examples.
412
+ """
413
+
414
+ def __init__(self, *, code: str, ctx: dict[str, Any], workflow, prompt, dynprompt, unique_id):
415
+ ctx = ctx or {}
416
+ self._ctx = {**ctx}
417
+ self._code = code
418
+ self._workflow = workflow
419
+ self._prompt = prompt
420
+ self._unique_id = unique_id
421
+ self._dynprompt = dynprompt
422
+ # These are now expanded lazily when needed.
423
+ self._prompt_nodes = None
424
+ self._prompt_node = None
425
+
426
+ def execute(self, code=Optional[str]) -> Any:
427
+ """Evaluates a the code block."""
428
+
429
+ # Always store random state and initialize a new seed. We'll restore the state later.
430
+ initial_random_state = random.getstate()
431
+ random.seed(datetime.datetime.now().timestamp())
432
+ last_value = None
433
+ try:
434
+ code = code or self._code
435
+ node = ast.parse(self._code)
436
+ ctx = {**self._ctx}
437
+ for body in node.body:
438
+ last_value = self._eval_statement(body, ctx)
439
+ # If we got a return, then that's it folks.
440
+ if isinstance(body, ast.Return):
441
+ break
442
+ except:
443
+ random.setstate(initial_random_state)
444
+ raise
445
+ random.setstate(initial_random_state)
446
+ return last_value
447
+
448
+ def _get_prompt_nodes(self):
449
+ """Expands the prompt nodes lazily from the dynamic prompt.
450
+
451
+ https://github.com/comfyanonymous/ComfyUI/blob/fc657f471a29d07696ca16b566000e8e555d67d1/comfy_execution/graph.py#L22
452
+ """
453
+ if self._prompt_nodes is None:
454
+ self._prompt_nodes = []
455
+ if self._dynprompt:
456
+ all_ids = self._dynprompt.all_node_ids()
457
+ self._prompt_nodes = [{'id': k} | {**self._dynprompt.get_node(k)} for k in all_ids]
458
+ return self._prompt_nodes
459
+
460
+ def _get_prompt_node(self):
461
+ if self._prompt_nodes is None:
462
+ self._prompt_node = [n for n in self._get_prompt_nodes() if n['id'] == self._unique_id][0]
463
+ return self._prompt_node
464
+
465
+ def _get_nodes(self, node_id: Union[int, str, re.Pattern, None] = None) -> list[Any]:
466
+ """Get a list of the nodes that match the node_id, or all the nodes in the prompt."""
467
+ nodes = self._get_prompt_nodes().copy()
468
+ if not node_id:
469
+ return nodes
470
+
471
+ if isinstance(node_id, re.Pattern):
472
+ found = [n for n in nodes if re.search(node_id, get_dict_value(n, '_meta.title', ''))]
473
+ else:
474
+ node_id = str(node_id)
475
+ found = None
476
+ if re.match(r'\d+$', node_id):
477
+ found = [n for n in nodes if node_id == n['id']]
478
+ if not found:
479
+ found = [n for n in nodes if node_id == get_dict_value(n, '_meta.title', '')]
480
+ return found
481
+
482
+ def _get_node(self, node_id: Union[int, str, re.Pattern, None] = None) -> Union[Any, None]:
483
+ """Returns a prompt-node from the hidden prompt."""
484
+ if node_id is None:
485
+ return self._get_prompt_node()
486
+ nodes = self._get_nodes(node_id)
487
+ if nodes and len(nodes) > 1:
488
+ log_node_warn(_NODE_NAME, f"More than one node found for '{node_id}'. Returning first.")
489
+ return nodes[0] if nodes else None
490
+
491
+ def _get_input_node(self, input_name, node=None):
492
+ """Gets the (non-muted) node of an input connection from a node (default to the power puter)."""
493
+ node = node if node else self._get_prompt_node()
494
+ try:
495
+ connected_node_id = node['inputs'][input_name][0]
496
+ return [n for n in self._get_prompt_nodes() if n['id'] == connected_node_id][0]
497
+ except (TypeError, IndexError, KeyError):
498
+ log_node_warn(_NODE_NAME, f'No input node found for "{input_name}". ')
499
+ return None
500
+
501
+ def _eval_statement(self, stmt: ast.AST, ctx: dict, prev_stmt: Union[ast.AST, None] = None):
502
+ """Evaluates an ast.stmt."""
503
+
504
+ if '__returned__' in ctx:
505
+ return ctx['__returned__']
506
+
507
+ # print('\n\n----: _eval_statement')
508
+ # print(type(stmt))
509
+ # print(ctx)
510
+
511
+ if isinstance(stmt, (ast.FormattedValue, ast.Expr)):
512
+ return self._eval_statement(stmt.value, ctx=ctx)
513
+
514
+ if isinstance(stmt, (ast.Constant, ast.Num)):
515
+ return stmt.n
516
+
517
+ if isinstance(stmt, ast.BinOp):
518
+ left = self._eval_statement(stmt.left, ctx=ctx)
519
+ right = self._eval_statement(stmt.right, ctx=ctx)
520
+ return _OPERATORS[type(stmt.op)](left, right)
521
+
522
+ if isinstance(stmt, ast.BoolOp):
523
+ is_and = isinstance(stmt.op, ast.And)
524
+ is_or = isinstance(stmt.op, ast.Or)
525
+ stmt_value_eval = None
526
+ for stmt_value in stmt.values:
527
+ stmt_value_eval = self._eval_statement(stmt_value, ctx=ctx)
528
+ # If we're an and operator and have a falsyt value, then we stop and return. Likewise, if
529
+ # we're an or operator and have a truthy value, we can stop and return.
530
+ if (is_and and not stmt_value_eval) or (is_or and stmt_value_eval):
531
+ return stmt_value_eval
532
+ # Always return the last if we made it here w/o success.
533
+ return stmt_value_eval
534
+
535
+ if isinstance(stmt, ast.UnaryOp):
536
+ return _OPERATORS[type(stmt.op)](self._eval_statement(stmt.operand, ctx=ctx))
537
+
538
+ if isinstance(stmt, (ast.Attribute, ast.Subscript)):
539
+ # Like: node(14).inputs.sampler_name (Attribute)
540
+ # Like: node(14)['inputs']['sampler_name'] (Subscript)
541
+ item = self._eval_statement(stmt.value, ctx=ctx)
542
+ attr = None
543
+ # if hasattr(stmt, 'attr'):
544
+ if isinstance(stmt, ast.Attribute):
545
+ attr = stmt.attr
546
+ else:
547
+ # Slice could be a name or a constant; evaluate it
548
+ attr = self._eval_statement(stmt.slice, ctx=ctx)
549
+ # Check if we're blocking access to this attribute/method on this item type.
550
+ for typ, names in _BLOCKED_METHODS_OR_ATTRS.items():
551
+ if isinstance(item, typ) and isinstance(attr, str) and attr in names:
552
+ raise ValueError(f'Disallowed access to "{attr}" for type {typ}.')
553
+ try:
554
+ val = item[attr]
555
+ except (TypeError, IndexError, KeyError):
556
+ try:
557
+ val = getattr(item, attr)
558
+ except AttributeError:
559
+ # If we're a dict, then just return None instead of error; saves time.
560
+ if isinstance(item, dict):
561
+ # Any special cases in the _SPECIAL_FUNCTIONS
562
+ class_type = get_dict_value(item, "class_type")
563
+ if class_type in _SPECIAL_FUNCTIONS and attr in _SPECIAL_FUNCTIONS[class_type]:
564
+ val = _SPECIAL_FUNCTIONS[class_type][attr]
565
+ # If our previous statment was a Call, then send back a tuple of the callable and
566
+ # the evaluated item, and it will make the call; perhaps also adding other arguments
567
+ # only it knows about.
568
+ if isinstance(prev_stmt, ast.Call):
569
+ return (val, item)
570
+ val = val(item)
571
+ else:
572
+ val = None
573
+ else:
574
+ raise
575
+ return val
576
+
577
+ if isinstance(stmt, (ast.List, ast.Tuple)):
578
+ value = []
579
+ for elt in stmt.elts:
580
+ value.append(self._eval_statement(elt, ctx=ctx))
581
+ return tuple(value) if isinstance(stmt, ast.Tuple) else value
582
+
583
+ if isinstance(stmt, ast.Dict):
584
+ the_dict = {}
585
+ if stmt.keys:
586
+ if len(stmt.keys) != len(stmt.values):
587
+ raise ValueError('Expected same number of keys as values for dict.')
588
+ for i, k in enumerate(stmt.keys):
589
+ item_key = self._eval_statement(k, ctx=ctx)
590
+ item_value = self._eval_statement(stmt.values[i], ctx=ctx)
591
+ the_dict[item_key] = item_value
592
+ return the_dict
593
+
594
+ # f-strings: https://www.basicexamples.com/example/python/ast-JoinedStr
595
+ # Note, this will str() all evaluated items in the fstrings, and doesn't handle f-string
596
+ # directives, like padding, etc.
597
+ if isinstance(stmt, ast.JoinedStr):
598
+ vals = [str(self._eval_statement(v, ctx=ctx)) for v in stmt.values]
599
+ val = ''.join(vals)
600
+ return val
601
+
602
+ if isinstance(stmt, ast.Slice):
603
+ if not stmt.lower or not stmt.upper:
604
+ raise ValueError('Unhandled Slice w/o lower or upper.')
605
+ slice_lower = self._eval_statement(stmt.lower, ctx=ctx)
606
+ slice_upper = self._eval_statement(stmt.upper, ctx=ctx)
607
+ if stmt.step:
608
+ slice_step = self._eval_statement(stmt.step, ctx=ctx)
609
+ return slice(slice_lower, slice_upper, slice_step)
610
+ return slice(slice_lower, slice_upper)
611
+
612
+ if isinstance(stmt, ast.Name):
613
+ if stmt.id in ctx:
614
+ val = ctx[stmt.id]
615
+ return val
616
+ if stmt.id in _BUILT_INS:
617
+ val = _BUILT_INS[stmt.id]
618
+ return val
619
+ raise NameError(f"Name not found: {stmt.id}")
620
+
621
+ if isinstance(stmt, ast.For):
622
+ for_iter = self._eval_statement(stmt.iter, ctx=ctx)
623
+ for item in for_iter:
624
+ # Set the for var(s)
625
+ if isinstance(stmt.target, ast.Name):
626
+ ctx[stmt.target.id] = item
627
+ elif isinstance(stmt.target, ast.Tuple): # dict, like `for k, v in d.entries()`
628
+ for i, elt in enumerate(stmt.target.elts):
629
+ ctx[elt.id] = item[i]
630
+ bodies = stmt.body if isinstance(stmt.body, list) else [stmt.body]
631
+ breaked = False
632
+ for body in bodies:
633
+ # Catch any breaks or continues and handle inside the loop normally.
634
+ try:
635
+ value = self._eval_statement(body, ctx=ctx)
636
+ except (LoopBreak, LoopContinue) as e:
637
+ breaked = isinstance(e, LoopBreak)
638
+ break
639
+ if breaked:
640
+ break
641
+ return None
642
+
643
+ if isinstance(stmt, ast.While):
644
+ while self._eval_statement(stmt.test, ctx=ctx):
645
+ bodies = stmt.body if isinstance(stmt.body, list) else [stmt.body]
646
+ breaked = False
647
+ for body in bodies:
648
+ # Catch any breaks or continues and handle inside the loop normally.
649
+ try:
650
+ value = self._eval_statement(body, ctx=ctx)
651
+ except (LoopBreak, LoopContinue) as e:
652
+ breaked = isinstance(e, LoopBreak)
653
+ break
654
+ if breaked:
655
+ break
656
+ return None
657
+
658
+ if isinstance(stmt, ast.ListComp):
659
+ # Like: [v.lora for name, v in node(19).inputs.items() if name.startswith('lora_')]
660
+ # Like: [v.lower() for v in lora_list]
661
+ # Like: [v for v in l if v.startswith('B')]
662
+ # Like: [v.lower() for v in l if v.startswith('B') or v.startswith('F')]
663
+ # ---
664
+ # Like: [l for n in nodes(re('Loras')).values() if (l := n.loras)]
665
+ final_list = []
666
+
667
+ gen_ctx = {**ctx}
668
+
669
+ generators = [*stmt.generators]
670
+
671
+ def handle_gen(generators: list[ast.comprehension]):
672
+ gen = generators.pop(0)
673
+ if isinstance(gen.target, ast.Name):
674
+ gen_ctx[gen.target.id] = None
675
+ elif isinstance(gen.target, ast.Tuple): # dict, like `for k, v in d.entries()`
676
+ for elt in gen.target.elts:
677
+ gen_ctx[elt.id] = None
678
+ else:
679
+ raise ValueError('Na')
680
+
681
+ gen_iters = None
682
+ # A call, like my_dct.items(), or a named ctx list
683
+ if isinstance(gen.iter, ast.Call):
684
+ gen_iters = self._eval_statement(gen.iter, ctx=gen_ctx)
685
+ elif isinstance(gen.iter, (ast.Name, ast.Attribute, ast.List, ast.Tuple)):
686
+ gen_iters = self._eval_statement(gen.iter, ctx=gen_ctx)
687
+
688
+ if not isinstance(gen_iters, Iterable):
689
+ raise ValueError('No iteraors found for list comprehension')
690
+
691
+ for gen_iter in gen_iters:
692
+ if_ctx = {**gen_ctx}
693
+ if isinstance(gen.target, ast.Tuple): # dict, like `for k, v in d.entries()`
694
+ for i, elt in enumerate(gen.target.elts):
695
+ if_ctx[elt.id] = gen_iter[i]
696
+ else:
697
+ if_ctx[gen.target.id] = gen_iter
698
+ good = True
699
+ for ifcall in gen.ifs:
700
+ if not self._eval_statement(ifcall, ctx=if_ctx):
701
+ good = False
702
+ break
703
+ if not good:
704
+ continue
705
+ gen_ctx.update(if_ctx)
706
+ if len(generators):
707
+ handle_gen(generators)
708
+ else:
709
+ final_list.append(self._eval_statement(stmt.elt, gen_ctx))
710
+ generators.insert(0, gen)
711
+
712
+ handle_gen(generators)
713
+ return final_list
714
+
715
+ if isinstance(stmt, ast.Call):
716
+ call = None
717
+ args = []
718
+ kwargs = {}
719
+ if isinstance(stmt.func, ast.Attribute):
720
+ call = self._eval_statement(stmt.func, prev_stmt=stmt, ctx=ctx)
721
+ if isinstance(call, tuple):
722
+ args.append(call[1])
723
+ call = call[0]
724
+ if not call:
725
+ raise ValueError(f'No call for ast.Call {stmt.func}')
726
+
727
+ name = ''
728
+ if isinstance(stmt.func, ast.Name):
729
+ name = stmt.func.id
730
+ if name in _BUILT_INS:
731
+ call = _BUILT_INS[name]
732
+
733
+ if isinstance(call, str) and call.startswith(_BUILTIN_FN_PREFIX):
734
+ fn = _get_built_in_fn_by_key(call)
735
+ call = fn.call
736
+ if isinstance(call, str):
737
+ call = getattr(self, call)
738
+ num_args = len(stmt.args)
739
+ if num_args < fn.args[0] or (fn.args[1] is not None and num_args > fn.args[1]):
740
+ toErr = " or more" if fn.args[1] is None else f" to {fn.args[1]}"
741
+ raise SyntaxError(f"Invalid function call: {fn.name} requires {fn.args[0]}{toErr} args")
742
+
743
+ if not call:
744
+ raise ValueError(f'No call for ast.Call {name}')
745
+
746
+ for arg in stmt.args:
747
+ args.append(self._eval_statement(arg, ctx=ctx))
748
+ for kwarg in stmt.keywords:
749
+ kwargs[kwarg.arg] = self._eval_statement(kwarg.value, ctx=ctx)
750
+ return call(*args, **kwargs)
751
+
752
+ if isinstance(stmt, ast.Compare):
753
+ l = self._eval_statement(stmt.left, ctx=ctx)
754
+ r = self._eval_statement(stmt.comparators[0], ctx=ctx)
755
+ if isinstance(stmt.ops[0], ast.Eq):
756
+ return 1 if l == r else 0
757
+ if isinstance(stmt.ops[0], ast.NotEq):
758
+ return 1 if l != r else 0
759
+ if isinstance(stmt.ops[0], ast.Gt):
760
+ return 1 if l > r else 0
761
+ if isinstance(stmt.ops[0], ast.GtE):
762
+ return 1 if l >= r else 0
763
+ if isinstance(stmt.ops[0], ast.Lt):
764
+ return 1 if l < r else 0
765
+ if isinstance(stmt.ops[0], ast.LtE):
766
+ return 1 if l <= r else 0
767
+ if isinstance(stmt.ops[0], ast.In):
768
+ return 1 if l in r else 0
769
+ if isinstance(stmt.ops[0], ast.Is):
770
+ return 1 if l is r else 0
771
+ if isinstance(stmt.ops[0], ast.IsNot):
772
+ return 1 if l is not r else 0
773
+ raise NotImplementedError("Operator " + stmt.ops[0].__class__.__name__ + " not supported.")
774
+
775
+ if isinstance(stmt, (ast.If, ast.IfExp)):
776
+ value = self._eval_statement(stmt.test, ctx=ctx)
777
+ if value:
778
+ # ast.If is a list, ast.IfExp is an object.
779
+ bodies = stmt.body if isinstance(stmt.body, list) else [stmt.body]
780
+ for body in bodies:
781
+ value = self._eval_statement(body, ctx=ctx)
782
+ elif stmt.orelse:
783
+ # ast.If is a list, ast.IfExp is an object. TBH, I don't know why the If is a list, it's
784
+ # only ever one item AFAICT.
785
+ orelses = stmt.orelse if isinstance(stmt.orelse, list) else [stmt.orelse]
786
+ for orelse in orelses:
787
+ value = self._eval_statement(orelse, ctx=ctx)
788
+ return value
789
+
790
+ # Assign a variable and add it to our ctx.
791
+ if isinstance(stmt, (ast.Assign, ast.AugAssign)):
792
+ if isinstance(stmt, ast.AugAssign):
793
+ left = self._eval_statement(stmt.target, ctx=ctx)
794
+ right = self._eval_statement(stmt.value, ctx=ctx)
795
+ value = _OPERATORS[type(stmt.op)](left, right)
796
+ target = stmt.target
797
+ else:
798
+ value = self._eval_statement(stmt.value, ctx=ctx)
799
+ if len(stmt.targets) != 1:
800
+ raise ValueError('Expected length of assign targets to be 1')
801
+ target = stmt.targets[0]
802
+
803
+ if isinstance(target, ast.Tuple): # like `a, z = (1,2)` (ast.Assign only)
804
+ for i, elt in enumerate(target.elts):
805
+ ctx[elt.id] = value[i]
806
+ elif isinstance(target, ast.Name): # like `a = 1``
807
+ ctx[target.id] = value
808
+ elif isinstance(target, ast.Subscript) and isinstance(target.value, ast.Name): # `a[0] = 1`
809
+ ctx[target.value.id][self._eval_statement(target.slice, ctx=ctx)] = value
810
+ else:
811
+ raise ValueError('Unhandled target type for Assign.')
812
+ return value
813
+
814
+ # For assigning a var in a list comprehension.
815
+ # Like [name for node in node_list if (name := node.name)]
816
+ if isinstance(stmt, ast.NamedExpr):
817
+ value = self._eval_statement(stmt.value, ctx=ctx)
818
+ ctx[stmt.target.id] = value
819
+ return value
820
+
821
+ if isinstance(stmt, ast.Return):
822
+ if stmt.value is None:
823
+ value = None
824
+ else:
825
+ value = self._eval_statement(stmt.value, ctx=ctx)
826
+ # Mark that we have a return value, as we may be deeper in evaluation, like going through an
827
+ # if condition's body.
828
+ ctx['__returned__'] = value
829
+ return value
830
+
831
+ # Raise an error for break or continue, which should be caught and handled inside of loops,
832
+ # otherwise the error will be raised (which is desired when used outside of a loop).
833
+ if isinstance(stmt, ast.Break):
834
+ raise LoopBreak()
835
+ if isinstance(stmt, ast.Continue):
836
+ raise LoopContinue()
837
+
838
+ # Literally nothing.
839
+ if isinstance(stmt, ast.Pass):
840
+ return None
841
+
842
+ raise TypeError(stmt)