SuperRealCo commited on
Commit
ea75430
·
verified ·
1 Parent(s): a1a0c38

Delete comfy_execution

Browse files
comfy_execution/caching.py DELETED
@@ -1,472 +0,0 @@
1
- import itertools
2
- from typing import Sequence, Mapping, Dict
3
- from comfy_execution.graph import DynamicPrompt
4
- from abc import ABC, abstractmethod
5
-
6
- import nodes
7
-
8
- from comfy_execution.graph_utils import is_link
9
-
10
- NODE_CLASS_CONTAINS_UNIQUE_ID: Dict[str, bool] = {}
11
-
12
-
13
- def include_unique_id_in_input(class_type: str) -> bool:
14
- if class_type in NODE_CLASS_CONTAINS_UNIQUE_ID:
15
- return NODE_CLASS_CONTAINS_UNIQUE_ID[class_type]
16
- class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
17
- NODE_CLASS_CONTAINS_UNIQUE_ID[class_type] = "UNIQUE_ID" in class_def.INPUT_TYPES().get("hidden", {}).values()
18
- return NODE_CLASS_CONTAINS_UNIQUE_ID[class_type]
19
-
20
- class CacheKeySet(ABC):
21
- def __init__(self, dynprompt, node_ids, is_changed_cache):
22
- self.keys = {}
23
- self.subcache_keys = {}
24
-
25
- @abstractmethod
26
- async def add_keys(self, node_ids):
27
- raise NotImplementedError()
28
-
29
- def all_node_ids(self):
30
- return set(self.keys.keys())
31
-
32
- def get_used_keys(self):
33
- return self.keys.values()
34
-
35
- def get_used_subcache_keys(self):
36
- return self.subcache_keys.values()
37
-
38
- def get_data_key(self, node_id):
39
- return self.keys.get(node_id, None)
40
-
41
- def get_subcache_key(self, node_id):
42
- return self.subcache_keys.get(node_id, None)
43
-
44
- class Unhashable:
45
- def __init__(self):
46
- self.value = float("NaN")
47
-
48
- def to_hashable(obj):
49
- # So that we don't infinitely recurse since frozenset and tuples
50
- # are Sequences.
51
- if isinstance(obj, (int, float, str, bool, type(None))):
52
- return obj
53
- elif isinstance(obj, Mapping):
54
- return frozenset([(to_hashable(k), to_hashable(v)) for k, v in sorted(obj.items())])
55
- elif isinstance(obj, Sequence):
56
- return frozenset(zip(itertools.count(), [to_hashable(i) for i in obj]))
57
- else:
58
- # TODO - Support other objects like tensors?
59
- return Unhashable()
60
-
61
- class CacheKeySetID(CacheKeySet):
62
- def __init__(self, dynprompt, node_ids, is_changed_cache):
63
- super().__init__(dynprompt, node_ids, is_changed_cache)
64
- self.dynprompt = dynprompt
65
-
66
- async def add_keys(self, node_ids):
67
- for node_id in node_ids:
68
- if node_id in self.keys:
69
- continue
70
- if not self.dynprompt.has_node(node_id):
71
- continue
72
- node = self.dynprompt.get_node(node_id)
73
- self.keys[node_id] = (node_id, node["class_type"])
74
- self.subcache_keys[node_id] = (node_id, node["class_type"])
75
-
76
- class CacheKeySetInputSignature(CacheKeySet):
77
- def __init__(self, dynprompt, node_ids, is_changed_cache):
78
- super().__init__(dynprompt, node_ids, is_changed_cache)
79
- self.dynprompt = dynprompt
80
- self.is_changed_cache = is_changed_cache
81
-
82
- def include_node_id_in_input(self) -> bool:
83
- return False
84
-
85
- async def add_keys(self, node_ids):
86
- for node_id in node_ids:
87
- if node_id in self.keys:
88
- continue
89
- if not self.dynprompt.has_node(node_id):
90
- continue
91
- node = self.dynprompt.get_node(node_id)
92
- self.keys[node_id] = await self.get_node_signature(self.dynprompt, node_id)
93
- self.subcache_keys[node_id] = (node_id, node["class_type"])
94
-
95
- async def get_node_signature(self, dynprompt, node_id):
96
- signature = []
97
- ancestors, order_mapping = self.get_ordered_ancestry(dynprompt, node_id)
98
- signature.append(await self.get_immediate_node_signature(dynprompt, node_id, order_mapping))
99
- for ancestor_id in ancestors:
100
- signature.append(await self.get_immediate_node_signature(dynprompt, ancestor_id, order_mapping))
101
- return to_hashable(signature)
102
-
103
- async def get_immediate_node_signature(self, dynprompt, node_id, ancestor_order_mapping):
104
- if not dynprompt.has_node(node_id):
105
- # This node doesn't exist -- we can't cache it.
106
- return [float("NaN")]
107
- node = dynprompt.get_node(node_id)
108
- class_type = node["class_type"]
109
- class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
110
- signature = [class_type, await self.is_changed_cache.get(node_id)]
111
- if self.include_node_id_in_input() or (hasattr(class_def, "NOT_IDEMPOTENT") and class_def.NOT_IDEMPOTENT) or include_unique_id_in_input(class_type):
112
- signature.append(node_id)
113
- inputs = node["inputs"]
114
- for key in sorted(inputs.keys()):
115
- if is_link(inputs[key]):
116
- (ancestor_id, ancestor_socket) = inputs[key]
117
- ancestor_index = ancestor_order_mapping[ancestor_id]
118
- signature.append((key,("ANCESTOR", ancestor_index, ancestor_socket)))
119
- else:
120
- signature.append((key, inputs[key]))
121
- return signature
122
-
123
- # This function returns a list of all ancestors of the given node. The order of the list is
124
- # deterministic based on which specific inputs the ancestor is connected by.
125
- def get_ordered_ancestry(self, dynprompt, node_id):
126
- ancestors = []
127
- order_mapping = {}
128
- self.get_ordered_ancestry_internal(dynprompt, node_id, ancestors, order_mapping)
129
- return ancestors, order_mapping
130
-
131
- def get_ordered_ancestry_internal(self, dynprompt, node_id, ancestors, order_mapping):
132
- if not dynprompt.has_node(node_id):
133
- return
134
- inputs = dynprompt.get_node(node_id)["inputs"]
135
- input_keys = sorted(inputs.keys())
136
- for key in input_keys:
137
- if is_link(inputs[key]):
138
- ancestor_id = inputs[key][0]
139
- if ancestor_id not in order_mapping:
140
- ancestors.append(ancestor_id)
141
- order_mapping[ancestor_id] = len(ancestors) - 1
142
- self.get_ordered_ancestry_internal(dynprompt, ancestor_id, ancestors, order_mapping)
143
-
144
- class BasicCache:
145
- def __init__(self, key_class):
146
- self.key_class = key_class
147
- self.initialized = False
148
- self.dynprompt: DynamicPrompt
149
- self.cache_key_set: CacheKeySet
150
- self.cache = {}
151
- self.subcaches = {}
152
-
153
- async def set_prompt(self, dynprompt, node_ids, is_changed_cache):
154
- self.dynprompt = dynprompt
155
- self.cache_key_set = self.key_class(dynprompt, node_ids, is_changed_cache)
156
- await self.cache_key_set.add_keys(node_ids)
157
- self.is_changed_cache = is_changed_cache
158
- self.initialized = True
159
-
160
- def all_node_ids(self):
161
- assert self.initialized
162
- node_ids = self.cache_key_set.all_node_ids()
163
- for subcache in self.subcaches.values():
164
- node_ids = node_ids.union(subcache.all_node_ids())
165
- return node_ids
166
-
167
- def _clean_cache(self):
168
- preserve_keys = set(self.cache_key_set.get_used_keys())
169
- to_remove = []
170
- for key in self.cache:
171
- if key not in preserve_keys:
172
- to_remove.append(key)
173
- for key in to_remove:
174
- del self.cache[key]
175
-
176
- def _clean_subcaches(self):
177
- preserve_subcaches = set(self.cache_key_set.get_used_subcache_keys())
178
-
179
- to_remove = []
180
- for key in self.subcaches:
181
- if key not in preserve_subcaches:
182
- to_remove.append(key)
183
- for key in to_remove:
184
- del self.subcaches[key]
185
-
186
- def clean_unused(self):
187
- assert self.initialized
188
- self._clean_cache()
189
- self._clean_subcaches()
190
-
191
- def _set_immediate(self, node_id, value):
192
- assert self.initialized
193
- cache_key = self.cache_key_set.get_data_key(node_id)
194
- self.cache[cache_key] = value
195
-
196
- def _get_immediate(self, node_id):
197
- if not self.initialized:
198
- return None
199
- cache_key = self.cache_key_set.get_data_key(node_id)
200
- if cache_key in self.cache:
201
- return self.cache[cache_key]
202
- else:
203
- return None
204
-
205
- async def _ensure_subcache(self, node_id, children_ids):
206
- subcache_key = self.cache_key_set.get_subcache_key(node_id)
207
- subcache = self.subcaches.get(subcache_key, None)
208
- if subcache is None:
209
- subcache = BasicCache(self.key_class)
210
- self.subcaches[subcache_key] = subcache
211
- await subcache.set_prompt(self.dynprompt, children_ids, self.is_changed_cache)
212
- return subcache
213
-
214
- def _get_subcache(self, node_id):
215
- assert self.initialized
216
- subcache_key = self.cache_key_set.get_subcache_key(node_id)
217
- if subcache_key in self.subcaches:
218
- return self.subcaches[subcache_key]
219
- else:
220
- return None
221
-
222
- def recursive_debug_dump(self):
223
- result = []
224
- for key in self.cache:
225
- result.append({"key": key, "value": self.cache[key]})
226
- for key in self.subcaches:
227
- result.append({"subcache_key": key, "subcache": self.subcaches[key].recursive_debug_dump()})
228
- return result
229
-
230
- class HierarchicalCache(BasicCache):
231
- def __init__(self, key_class):
232
- super().__init__(key_class)
233
-
234
- def _get_cache_for(self, node_id):
235
- assert self.dynprompt is not None
236
- parent_id = self.dynprompt.get_parent_node_id(node_id)
237
- if parent_id is None:
238
- return self
239
-
240
- hierarchy = []
241
- while parent_id is not None:
242
- hierarchy.append(parent_id)
243
- parent_id = self.dynprompt.get_parent_node_id(parent_id)
244
-
245
- cache = self
246
- for parent_id in reversed(hierarchy):
247
- cache = cache._get_subcache(parent_id)
248
- if cache is None:
249
- return None
250
- return cache
251
-
252
- def get(self, node_id):
253
- cache = self._get_cache_for(node_id)
254
- if cache is None:
255
- return None
256
- return cache._get_immediate(node_id)
257
-
258
- def set(self, node_id, value):
259
- cache = self._get_cache_for(node_id)
260
- assert cache is not None
261
- cache._set_immediate(node_id, value)
262
-
263
- async def ensure_subcache_for(self, node_id, children_ids):
264
- cache = self._get_cache_for(node_id)
265
- assert cache is not None
266
- return await cache._ensure_subcache(node_id, children_ids)
267
-
268
- class LRUCache(BasicCache):
269
- def __init__(self, key_class, max_size=100):
270
- super().__init__(key_class)
271
- self.max_size = max_size
272
- self.min_generation = 0
273
- self.generation = 0
274
- self.used_generation = {}
275
- self.children = {}
276
-
277
- async def set_prompt(self, dynprompt, node_ids, is_changed_cache):
278
- await super().set_prompt(dynprompt, node_ids, is_changed_cache)
279
- self.generation += 1
280
- for node_id in node_ids:
281
- self._mark_used(node_id)
282
-
283
- def clean_unused(self):
284
- while len(self.cache) > self.max_size and self.min_generation < self.generation:
285
- self.min_generation += 1
286
- to_remove = [key for key in self.cache if self.used_generation[key] < self.min_generation]
287
- for key in to_remove:
288
- del self.cache[key]
289
- del self.used_generation[key]
290
- if key in self.children:
291
- del self.children[key]
292
- self._clean_subcaches()
293
-
294
- def get(self, node_id):
295
- self._mark_used(node_id)
296
- return self._get_immediate(node_id)
297
-
298
- def _mark_used(self, node_id):
299
- cache_key = self.cache_key_set.get_data_key(node_id)
300
- if cache_key is not None:
301
- self.used_generation[cache_key] = self.generation
302
-
303
- def set(self, node_id, value):
304
- self._mark_used(node_id)
305
- return self._set_immediate(node_id, value)
306
-
307
- async def ensure_subcache_for(self, node_id, children_ids):
308
- # Just uses subcaches for tracking 'live' nodes
309
- await super()._ensure_subcache(node_id, children_ids)
310
-
311
- await self.cache_key_set.add_keys(children_ids)
312
- self._mark_used(node_id)
313
- cache_key = self.cache_key_set.get_data_key(node_id)
314
- self.children[cache_key] = []
315
- for child_id in children_ids:
316
- self._mark_used(child_id)
317
- self.children[cache_key].append(self.cache_key_set.get_data_key(child_id))
318
- return self
319
-
320
-
321
- class DependencyAwareCache(BasicCache):
322
- """
323
- A cache implementation that tracks dependencies between nodes and manages
324
- their execution and caching accordingly. It extends the BasicCache class.
325
- Nodes are removed from this cache once all of their descendants have been
326
- executed.
327
- """
328
-
329
- def __init__(self, key_class):
330
- """
331
- Initialize the DependencyAwareCache.
332
-
333
- Args:
334
- key_class: The class used for generating cache keys.
335
- """
336
- super().__init__(key_class)
337
- self.descendants = {} # Maps node_id -> set of descendant node_ids
338
- self.ancestors = {} # Maps node_id -> set of ancestor node_ids
339
- self.executed_nodes = set() # Tracks nodes that have been executed
340
-
341
- async def set_prompt(self, dynprompt, node_ids, is_changed_cache):
342
- """
343
- Clear the entire cache and rebuild the dependency graph.
344
-
345
- Args:
346
- dynprompt: The dynamic prompt object containing node information.
347
- node_ids: List of node IDs to initialize the cache for.
348
- is_changed_cache: Flag indicating if the cache has changed.
349
- """
350
- # Clear all existing cache data
351
- self.cache.clear()
352
- self.subcaches.clear()
353
- self.descendants.clear()
354
- self.ancestors.clear()
355
- self.executed_nodes.clear()
356
-
357
- # Call the parent method to initialize the cache with the new prompt
358
- await super().set_prompt(dynprompt, node_ids, is_changed_cache)
359
-
360
- # Rebuild the dependency graph
361
- self._build_dependency_graph(dynprompt, node_ids)
362
-
363
- def _build_dependency_graph(self, dynprompt, node_ids):
364
- """
365
- Build the dependency graph for all nodes.
366
-
367
- Args:
368
- dynprompt: The dynamic prompt object containing node information.
369
- node_ids: List of node IDs to build the graph for.
370
- """
371
- self.descendants.clear()
372
- self.ancestors.clear()
373
- for node_id in node_ids:
374
- self.descendants[node_id] = set()
375
- self.ancestors[node_id] = set()
376
-
377
- for node_id in node_ids:
378
- inputs = dynprompt.get_node(node_id)["inputs"]
379
- for input_data in inputs.values():
380
- if is_link(input_data): # Check if the input is a link to another node
381
- ancestor_id = input_data[0]
382
- self.descendants[ancestor_id].add(node_id)
383
- self.ancestors[node_id].add(ancestor_id)
384
-
385
- def set(self, node_id, value):
386
- """
387
- Mark a node as executed and store its value in the cache.
388
-
389
- Args:
390
- node_id: The ID of the node to store.
391
- value: The value to store for the node.
392
- """
393
- self._set_immediate(node_id, value)
394
- self.executed_nodes.add(node_id)
395
- self._cleanup_ancestors(node_id)
396
-
397
- def get(self, node_id):
398
- """
399
- Retrieve the cached value for a node.
400
-
401
- Args:
402
- node_id: The ID of the node to retrieve.
403
-
404
- Returns:
405
- The cached value for the node.
406
- """
407
- return self._get_immediate(node_id)
408
-
409
- async def ensure_subcache_for(self, node_id, children_ids):
410
- """
411
- Ensure a subcache exists for a node and update dependencies.
412
-
413
- Args:
414
- node_id: The ID of the parent node.
415
- children_ids: List of child node IDs to associate with the parent node.
416
-
417
- Returns:
418
- The subcache object for the node.
419
- """
420
- subcache = await super()._ensure_subcache(node_id, children_ids)
421
- for child_id in children_ids:
422
- self.descendants[node_id].add(child_id)
423
- self.ancestors[child_id].add(node_id)
424
- return subcache
425
-
426
- def _cleanup_ancestors(self, node_id):
427
- """
428
- Check if ancestors of a node can be removed from the cache.
429
-
430
- Args:
431
- node_id: The ID of the node whose ancestors are to be checked.
432
- """
433
- for ancestor_id in self.ancestors.get(node_id, []):
434
- if ancestor_id in self.executed_nodes:
435
- # Remove ancestor if all its descendants have been executed
436
- if all(descendant in self.executed_nodes for descendant in self.descendants[ancestor_id]):
437
- self._remove_node(ancestor_id)
438
-
439
- def _remove_node(self, node_id):
440
- """
441
- Remove a node from the cache.
442
-
443
- Args:
444
- node_id: The ID of the node to remove.
445
- """
446
- cache_key = self.cache_key_set.get_data_key(node_id)
447
- if cache_key in self.cache:
448
- del self.cache[cache_key]
449
- subcache_key = self.cache_key_set.get_subcache_key(node_id)
450
- if subcache_key in self.subcaches:
451
- del self.subcaches[subcache_key]
452
-
453
- def clean_unused(self):
454
- """
455
- Clean up unused nodes. This is a no-op for this cache implementation.
456
- """
457
- pass
458
-
459
- def recursive_debug_dump(self):
460
- """
461
- Dump the cache and dependency graph for debugging.
462
-
463
- Returns:
464
- A list containing the cache state and dependency graph.
465
- """
466
- result = super().recursive_debug_dump()
467
- result.append({
468
- "descendants": self.descendants,
469
- "ancestors": self.ancestors,
470
- "executed_nodes": list(self.executed_nodes),
471
- })
472
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
comfy_execution/graph.py DELETED
@@ -1,314 +0,0 @@
1
- from __future__ import annotations
2
- from typing import Type, Literal
3
-
4
- import nodes
5
- import asyncio
6
- import inspect
7
- from comfy_execution.graph_utils import is_link
8
- from comfy.comfy_types.node_typing import ComfyNodeABC, InputTypeDict, InputTypeOptions
9
-
10
- class DependencyCycleError(Exception):
11
- pass
12
-
13
- class NodeInputError(Exception):
14
- pass
15
-
16
- class NodeNotFoundError(Exception):
17
- pass
18
-
19
- class DynamicPrompt:
20
- def __init__(self, original_prompt):
21
- # The original prompt provided by the user
22
- self.original_prompt = original_prompt
23
- # Any extra pieces of the graph created during execution
24
- self.ephemeral_prompt = {}
25
- self.ephemeral_parents = {}
26
- self.ephemeral_display = {}
27
-
28
- def get_node(self, node_id):
29
- if node_id in self.ephemeral_prompt:
30
- return self.ephemeral_prompt[node_id]
31
- if node_id in self.original_prompt:
32
- return self.original_prompt[node_id]
33
- raise NodeNotFoundError(f"Node {node_id} not found")
34
-
35
- def has_node(self, node_id):
36
- return node_id in self.original_prompt or node_id in self.ephemeral_prompt
37
-
38
- def add_ephemeral_node(self, node_id, node_info, parent_id, display_id):
39
- self.ephemeral_prompt[node_id] = node_info
40
- self.ephemeral_parents[node_id] = parent_id
41
- self.ephemeral_display[node_id] = display_id
42
-
43
- def get_real_node_id(self, node_id):
44
- while node_id in self.ephemeral_parents:
45
- node_id = self.ephemeral_parents[node_id]
46
- return node_id
47
-
48
- def get_parent_node_id(self, node_id):
49
- return self.ephemeral_parents.get(node_id, None)
50
-
51
- def get_display_node_id(self, node_id):
52
- while node_id in self.ephemeral_display:
53
- node_id = self.ephemeral_display[node_id]
54
- return node_id
55
-
56
- def all_node_ids(self):
57
- return set(self.original_prompt.keys()).union(set(self.ephemeral_prompt.keys()))
58
-
59
- def get_original_prompt(self):
60
- return self.original_prompt
61
-
62
- def get_input_info(
63
- class_def: Type[ComfyNodeABC],
64
- input_name: str,
65
- valid_inputs: InputTypeDict | None = None
66
- ) -> tuple[str, Literal["required", "optional", "hidden"], InputTypeOptions] | tuple[None, None, None]:
67
- """Get the input type, category, and extra info for a given input name.
68
-
69
- Arguments:
70
- class_def: The class definition of the node.
71
- input_name: The name of the input to get info for.
72
- valid_inputs: The valid inputs for the node, or None to use the class_def.INPUT_TYPES().
73
-
74
- Returns:
75
- tuple[str, str, dict] | tuple[None, None, None]: The input type, category, and extra info for the input name.
76
- """
77
-
78
- valid_inputs = valid_inputs or class_def.INPUT_TYPES()
79
- input_info = None
80
- input_category = None
81
- if "required" in valid_inputs and input_name in valid_inputs["required"]:
82
- input_category = "required"
83
- input_info = valid_inputs["required"][input_name]
84
- elif "optional" in valid_inputs and input_name in valid_inputs["optional"]:
85
- input_category = "optional"
86
- input_info = valid_inputs["optional"][input_name]
87
- elif "hidden" in valid_inputs and input_name in valid_inputs["hidden"]:
88
- input_category = "hidden"
89
- input_info = valid_inputs["hidden"][input_name]
90
- if input_info is None:
91
- return None, None, None
92
- input_type = input_info[0]
93
- if len(input_info) > 1:
94
- extra_info = input_info[1]
95
- else:
96
- extra_info = {}
97
- return input_type, input_category, extra_info
98
-
99
- class TopologicalSort:
100
- def __init__(self, dynprompt):
101
- self.dynprompt = dynprompt
102
- self.pendingNodes = {}
103
- self.blockCount = {} # Number of nodes this node is directly blocked by
104
- self.blocking = {} # Which nodes are blocked by this node
105
- self.externalBlocks = 0
106
- self.unblockedEvent = asyncio.Event()
107
-
108
- def get_input_info(self, unique_id, input_name):
109
- class_type = self.dynprompt.get_node(unique_id)["class_type"]
110
- class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
111
- return get_input_info(class_def, input_name)
112
-
113
- def make_input_strong_link(self, to_node_id, to_input):
114
- inputs = self.dynprompt.get_node(to_node_id)["inputs"]
115
- if to_input not in inputs:
116
- raise NodeInputError(f"Node {to_node_id} says it needs input {to_input}, but there is no input to that node at all")
117
- value = inputs[to_input]
118
- if not is_link(value):
119
- raise NodeInputError(f"Node {to_node_id} says it needs input {to_input}, but that value is a constant")
120
- from_node_id, from_socket = value
121
- self.add_strong_link(from_node_id, from_socket, to_node_id)
122
-
123
- def add_strong_link(self, from_node_id, from_socket, to_node_id):
124
- if not self.is_cached(from_node_id):
125
- self.add_node(from_node_id)
126
- if to_node_id not in self.blocking[from_node_id]:
127
- self.blocking[from_node_id][to_node_id] = {}
128
- self.blockCount[to_node_id] += 1
129
- self.blocking[from_node_id][to_node_id][from_socket] = True
130
-
131
- def add_node(self, node_unique_id, include_lazy=False, subgraph_nodes=None):
132
- node_ids = [node_unique_id]
133
- links = []
134
-
135
- while len(node_ids) > 0:
136
- unique_id = node_ids.pop()
137
- if unique_id in self.pendingNodes:
138
- continue
139
-
140
- self.pendingNodes[unique_id] = True
141
- self.blockCount[unique_id] = 0
142
- self.blocking[unique_id] = {}
143
-
144
- inputs = self.dynprompt.get_node(unique_id)["inputs"]
145
- for input_name in inputs:
146
- value = inputs[input_name]
147
- if is_link(value):
148
- from_node_id, from_socket = value
149
- if subgraph_nodes is not None and from_node_id not in subgraph_nodes:
150
- continue
151
- _, _, input_info = self.get_input_info(unique_id, input_name)
152
- is_lazy = input_info is not None and "lazy" in input_info and input_info["lazy"]
153
- if (include_lazy or not is_lazy) and not self.is_cached(from_node_id):
154
- node_ids.append(from_node_id)
155
- links.append((from_node_id, from_socket, unique_id))
156
-
157
- for link in links:
158
- self.add_strong_link(*link)
159
-
160
- def add_external_block(self, node_id):
161
- assert node_id in self.blockCount, "Can't add external block to a node that isn't pending"
162
- self.externalBlocks += 1
163
- self.blockCount[node_id] += 1
164
- def unblock():
165
- self.externalBlocks -= 1
166
- self.blockCount[node_id] -= 1
167
- self.unblockedEvent.set()
168
- return unblock
169
-
170
- def is_cached(self, node_id):
171
- return False
172
-
173
- def get_ready_nodes(self):
174
- return [node_id for node_id in self.pendingNodes if self.blockCount[node_id] == 0]
175
-
176
- def pop_node(self, unique_id):
177
- del self.pendingNodes[unique_id]
178
- for blocked_node_id in self.blocking[unique_id]:
179
- self.blockCount[blocked_node_id] -= 1
180
- del self.blocking[unique_id]
181
-
182
- def is_empty(self):
183
- return len(self.pendingNodes) == 0
184
-
185
- class ExecutionList(TopologicalSort):
186
- """
187
- ExecutionList implements a topological dissolve of the graph. After a node is staged for execution,
188
- it can still be returned to the graph after having further dependencies added.
189
- """
190
- def __init__(self, dynprompt, output_cache):
191
- super().__init__(dynprompt)
192
- self.output_cache = output_cache
193
- self.staged_node_id = None
194
-
195
- def is_cached(self, node_id):
196
- return self.output_cache.get(node_id) is not None
197
-
198
- async def stage_node_execution(self):
199
- assert self.staged_node_id is None
200
- if self.is_empty():
201
- return None, None, None
202
- available = self.get_ready_nodes()
203
- while len(available) == 0 and self.externalBlocks > 0:
204
- # Wait for an external block to be released
205
- await self.unblockedEvent.wait()
206
- self.unblockedEvent.clear()
207
- available = self.get_ready_nodes()
208
- if len(available) == 0:
209
- cycled_nodes = self.get_nodes_in_cycle()
210
- # Because cycles composed entirely of static nodes are caught during initial validation,
211
- # we will 'blame' the first node in the cycle that is not a static node.
212
- blamed_node = cycled_nodes[0]
213
- for node_id in cycled_nodes:
214
- display_node_id = self.dynprompt.get_display_node_id(node_id)
215
- if display_node_id != node_id:
216
- blamed_node = display_node_id
217
- break
218
- ex = DependencyCycleError("Dependency cycle detected")
219
- error_details = {
220
- "node_id": blamed_node,
221
- "exception_message": str(ex),
222
- "exception_type": "graph.DependencyCycleError",
223
- "traceback": [],
224
- "current_inputs": []
225
- }
226
- return None, error_details, ex
227
-
228
- self.staged_node_id = self.ux_friendly_pick_node(available)
229
- return self.staged_node_id, None, None
230
-
231
- def ux_friendly_pick_node(self, node_list):
232
- # If an output node is available, do that first.
233
- # Technically this has no effect on the overall length of execution, but it feels better as a user
234
- # for a PreviewImage to display a result as soon as it can
235
- # Some other heuristics could probably be used here to improve the UX further.
236
- def is_output(node_id):
237
- class_type = self.dynprompt.get_node(node_id)["class_type"]
238
- class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
239
- if hasattr(class_def, 'OUTPUT_NODE') and class_def.OUTPUT_NODE == True:
240
- return True
241
- return False
242
-
243
- # If an available node is async, do that first.
244
- # This will execute the asynchronous function earlier, reducing the overall time.
245
- def is_async(node_id):
246
- class_type = self.dynprompt.get_node(node_id)["class_type"]
247
- class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
248
- return inspect.iscoroutinefunction(getattr(class_def, class_def.FUNCTION))
249
-
250
- for node_id in node_list:
251
- if is_output(node_id) or is_async(node_id):
252
- return node_id
253
-
254
- #This should handle the VAEDecode -> preview case
255
- for node_id in node_list:
256
- for blocked_node_id in self.blocking[node_id]:
257
- if is_output(blocked_node_id):
258
- return node_id
259
-
260
- #This should handle the VAELoader -> VAEDecode -> preview case
261
- for node_id in node_list:
262
- for blocked_node_id in self.blocking[node_id]:
263
- for blocked_node_id1 in self.blocking[blocked_node_id]:
264
- if is_output(blocked_node_id1):
265
- return node_id
266
-
267
- #TODO: this function should be improved
268
- return node_list[0]
269
-
270
- def unstage_node_execution(self):
271
- assert self.staged_node_id is not None
272
- self.staged_node_id = None
273
-
274
- def complete_node_execution(self):
275
- node_id = self.staged_node_id
276
- self.pop_node(node_id)
277
- self.staged_node_id = None
278
-
279
- def get_nodes_in_cycle(self):
280
- # We'll dissolve the graph in reverse topological order to leave only the nodes in the cycle.
281
- # We're skipping some of the performance optimizations from the original TopologicalSort to keep
282
- # the code simple (and because having a cycle in the first place is a catastrophic error)
283
- blocked_by = { node_id: {} for node_id in self.pendingNodes }
284
- for from_node_id in self.blocking:
285
- for to_node_id in self.blocking[from_node_id]:
286
- if True in self.blocking[from_node_id][to_node_id].values():
287
- blocked_by[to_node_id][from_node_id] = True
288
- to_remove = [node_id for node_id in blocked_by if len(blocked_by[node_id]) == 0]
289
- while len(to_remove) > 0:
290
- for node_id in to_remove:
291
- for to_node_id in blocked_by:
292
- if node_id in blocked_by[to_node_id]:
293
- del blocked_by[to_node_id][node_id]
294
- del blocked_by[node_id]
295
- to_remove = [node_id for node_id in blocked_by if len(blocked_by[node_id]) == 0]
296
- return list(blocked_by.keys())
297
-
298
- class ExecutionBlocker:
299
- """
300
- Return this from a node and any users will be blocked with the given error message.
301
- If the message is None, execution will be blocked silently instead.
302
- Generally, you should avoid using this functionality unless absolutely necessary. Whenever it's
303
- possible, a lazy input will be more efficient and have a better user experience.
304
- This functionality is useful in two cases:
305
- 1. You want to conditionally prevent an output node from executing. (Particularly a built-in node
306
- like SaveImage. For your own output nodes, I would recommend just adding a BOOL input and using
307
- lazy evaluation to let it conditionally disable itself.)
308
- 2. You have a node with multiple possible outputs, some of which are invalid and should not be used.
309
- (I would recommend not making nodes like this in the future -- instead, make multiple nodes with
310
- different outputs. Unfortunately, there are several popular existing nodes using this pattern.)
311
- """
312
- def __init__(self, message):
313
- self.message = message
314
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
comfy_execution/graph_utils.py DELETED
@@ -1,139 +0,0 @@
1
- def is_link(obj):
2
- if not isinstance(obj, list):
3
- return False
4
- if len(obj) != 2:
5
- return False
6
- if not isinstance(obj[0], str):
7
- return False
8
- if not isinstance(obj[1], int) and not isinstance(obj[1], float):
9
- return False
10
- return True
11
-
12
- # The GraphBuilder is just a utility class that outputs graphs in the form expected by the ComfyUI back-end
13
- class GraphBuilder:
14
- _default_prefix_root = ""
15
- _default_prefix_call_index = 0
16
- _default_prefix_graph_index = 0
17
-
18
- def __init__(self, prefix = None):
19
- if prefix is None:
20
- self.prefix = GraphBuilder.alloc_prefix()
21
- else:
22
- self.prefix = prefix
23
- self.nodes = {}
24
- self.id_gen = 1
25
-
26
- @classmethod
27
- def set_default_prefix(cls, prefix_root, call_index, graph_index = 0):
28
- cls._default_prefix_root = prefix_root
29
- cls._default_prefix_call_index = call_index
30
- cls._default_prefix_graph_index = graph_index
31
-
32
- @classmethod
33
- def alloc_prefix(cls, root=None, call_index=None, graph_index=None):
34
- if root is None:
35
- root = GraphBuilder._default_prefix_root
36
- if call_index is None:
37
- call_index = GraphBuilder._default_prefix_call_index
38
- if graph_index is None:
39
- graph_index = GraphBuilder._default_prefix_graph_index
40
- result = f"{root}.{call_index}.{graph_index}."
41
- GraphBuilder._default_prefix_graph_index += 1
42
- return result
43
-
44
- def node(self, class_type, id=None, **kwargs):
45
- if id is None:
46
- id = str(self.id_gen)
47
- self.id_gen += 1
48
- id = self.prefix + id
49
- if id in self.nodes:
50
- return self.nodes[id]
51
-
52
- node = Node(id, class_type, kwargs)
53
- self.nodes[id] = node
54
- return node
55
-
56
- def lookup_node(self, id):
57
- id = self.prefix + id
58
- return self.nodes.get(id)
59
-
60
- def finalize(self):
61
- output = {}
62
- for node_id, node in self.nodes.items():
63
- output[node_id] = node.serialize()
64
- return output
65
-
66
- def replace_node_output(self, node_id, index, new_value):
67
- node_id = self.prefix + node_id
68
- to_remove = []
69
- for node in self.nodes.values():
70
- for key, value in node.inputs.items():
71
- if is_link(value) and value[0] == node_id and value[1] == index:
72
- if new_value is None:
73
- to_remove.append((node, key))
74
- else:
75
- node.inputs[key] = new_value
76
- for node, key in to_remove:
77
- del node.inputs[key]
78
-
79
- def remove_node(self, id):
80
- id = self.prefix + id
81
- del self.nodes[id]
82
-
83
- class Node:
84
- def __init__(self, id, class_type, inputs):
85
- self.id = id
86
- self.class_type = class_type
87
- self.inputs = inputs
88
- self.override_display_id = None
89
-
90
- def out(self, index):
91
- return [self.id, index]
92
-
93
- def set_input(self, key, value):
94
- if value is None:
95
- if key in self.inputs:
96
- del self.inputs[key]
97
- else:
98
- self.inputs[key] = value
99
-
100
- def get_input(self, key):
101
- return self.inputs.get(key)
102
-
103
- def set_override_display_id(self, override_display_id):
104
- self.override_display_id = override_display_id
105
-
106
- def serialize(self):
107
- serialized = {
108
- "class_type": self.class_type,
109
- "inputs": self.inputs
110
- }
111
- if self.override_display_id is not None:
112
- serialized["override_display_id"] = self.override_display_id
113
- return serialized
114
-
115
- def add_graph_prefix(graph, outputs, prefix):
116
- # Change the node IDs and any internal links
117
- new_graph = {}
118
- for node_id, node_info in graph.items():
119
- # Make sure the added nodes have unique IDs
120
- new_node_id = prefix + node_id
121
- new_node = { "class_type": node_info["class_type"], "inputs": {} }
122
- for input_name, input_value in node_info.get("inputs", {}).items():
123
- if is_link(input_value):
124
- new_node["inputs"][input_name] = [prefix + input_value[0], input_value[1]]
125
- else:
126
- new_node["inputs"][input_name] = input_value
127
- new_graph[new_node_id] = new_node
128
-
129
- # Change the node IDs in the outputs
130
- new_outputs = []
131
- for n in range(len(outputs)):
132
- output = outputs[n]
133
- if is_link(output):
134
- new_outputs.append([prefix + output[0], output[1]])
135
- else:
136
- new_outputs.append(output)
137
-
138
- return new_graph, tuple(new_outputs)
139
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
comfy_execution/progress.py DELETED
@@ -1,347 +0,0 @@
1
- from typing import TypedDict, Dict, Optional
2
- from typing_extensions import override
3
- from PIL import Image
4
- from enum import Enum
5
- from abc import ABC
6
- from tqdm import tqdm
7
- from typing import TYPE_CHECKING
8
- if TYPE_CHECKING:
9
- from comfy_execution.graph import DynamicPrompt
10
- from protocol import BinaryEventTypes
11
- from comfy_api import feature_flags
12
-
13
-
14
- class NodeState(Enum):
15
- Pending = "pending"
16
- Running = "running"
17
- Finished = "finished"
18
- Error = "error"
19
-
20
-
21
- class NodeProgressState(TypedDict):
22
- """
23
- A class to represent the state of a node's progress.
24
- """
25
-
26
- state: NodeState
27
- value: float
28
- max: float
29
-
30
-
31
- class ProgressHandler(ABC):
32
- """
33
- Abstract base class for progress handlers.
34
- Progress handlers receive progress updates and display them in various ways.
35
- """
36
-
37
- def __init__(self, name: str):
38
- self.name = name
39
- self.enabled = True
40
-
41
- def set_registry(self, registry: "ProgressRegistry"):
42
- pass
43
-
44
- def start_handler(self, node_id: str, state: NodeProgressState, prompt_id: str):
45
- """Called when a node starts processing"""
46
- pass
47
-
48
- def update_handler(
49
- self,
50
- node_id: str,
51
- value: float,
52
- max_value: float,
53
- state: NodeProgressState,
54
- prompt_id: str,
55
- image: Optional[Image.Image] = None,
56
- ):
57
- """Called when a node's progress is updated"""
58
- pass
59
-
60
- def finish_handler(self, node_id: str, state: NodeProgressState, prompt_id: str):
61
- """Called when a node finishes processing"""
62
- pass
63
-
64
- def reset(self):
65
- """Called when the progress registry is reset"""
66
- pass
67
-
68
- def enable(self):
69
- """Enable this handler"""
70
- self.enabled = True
71
-
72
- def disable(self):
73
- """Disable this handler"""
74
- self.enabled = False
75
-
76
-
77
- class CLIProgressHandler(ProgressHandler):
78
- """
79
- Handler that displays progress using tqdm progress bars in the CLI.
80
- """
81
-
82
- def __init__(self):
83
- super().__init__("cli")
84
- self.progress_bars: Dict[str, tqdm] = {}
85
-
86
- @override
87
- def start_handler(self, node_id: str, state: NodeProgressState, prompt_id: str):
88
- # Create a new tqdm progress bar
89
- if node_id not in self.progress_bars:
90
- self.progress_bars[node_id] = tqdm(
91
- total=state["max"],
92
- desc=f"Node {node_id}",
93
- unit="steps",
94
- leave=True,
95
- position=len(self.progress_bars),
96
- )
97
-
98
- @override
99
- def update_handler(
100
- self,
101
- node_id: str,
102
- value: float,
103
- max_value: float,
104
- state: NodeProgressState,
105
- prompt_id: str,
106
- image: Optional[Image.Image] = None,
107
- ):
108
- # Handle case where start_handler wasn't called
109
- if node_id not in self.progress_bars:
110
- self.progress_bars[node_id] = tqdm(
111
- total=max_value,
112
- desc=f"Node {node_id}",
113
- unit="steps",
114
- leave=True,
115
- position=len(self.progress_bars),
116
- )
117
- self.progress_bars[node_id].update(value)
118
- else:
119
- # Update existing progress bar
120
- if max_value != self.progress_bars[node_id].total:
121
- self.progress_bars[node_id].total = max_value
122
- # Calculate the update amount (difference from current position)
123
- current_position = self.progress_bars[node_id].n
124
- update_amount = value - current_position
125
- if update_amount > 0:
126
- self.progress_bars[node_id].update(update_amount)
127
-
128
- @override
129
- def finish_handler(self, node_id: str, state: NodeProgressState, prompt_id: str):
130
- # Complete and close the progress bar if it exists
131
- if node_id in self.progress_bars:
132
- # Ensure the bar shows 100% completion
133
- remaining = state["max"] - self.progress_bars[node_id].n
134
- if remaining > 0:
135
- self.progress_bars[node_id].update(remaining)
136
- self.progress_bars[node_id].close()
137
- del self.progress_bars[node_id]
138
-
139
- @override
140
- def reset(self):
141
- # Close all progress bars
142
- for bar in self.progress_bars.values():
143
- bar.close()
144
- self.progress_bars.clear()
145
-
146
-
147
- class WebUIProgressHandler(ProgressHandler):
148
- """
149
- Handler that sends progress updates to the WebUI via WebSockets.
150
- """
151
-
152
- def __init__(self, server_instance):
153
- super().__init__("webui")
154
- self.server_instance = server_instance
155
-
156
- def set_registry(self, registry: "ProgressRegistry"):
157
- self.registry = registry
158
-
159
- def _send_progress_state(self, prompt_id: str, nodes: Dict[str, NodeProgressState]):
160
- """Send the current progress state to the client"""
161
- if self.server_instance is None:
162
- return
163
-
164
- # Only send info for non-pending nodes
165
- active_nodes = {
166
- node_id: {
167
- "value": state["value"],
168
- "max": state["max"],
169
- "state": state["state"].value,
170
- "node_id": node_id,
171
- "prompt_id": prompt_id,
172
- "display_node_id": self.registry.dynprompt.get_display_node_id(node_id),
173
- "parent_node_id": self.registry.dynprompt.get_parent_node_id(node_id),
174
- "real_node_id": self.registry.dynprompt.get_real_node_id(node_id),
175
- }
176
- for node_id, state in nodes.items()
177
- if state["state"] != NodeState.Pending
178
- }
179
-
180
- # Send a combined progress_state message with all node states
181
- self.server_instance.send_sync(
182
- "progress_state", {"prompt_id": prompt_id, "nodes": active_nodes}
183
- )
184
-
185
- @override
186
- def start_handler(self, node_id: str, state: NodeProgressState, prompt_id: str):
187
- # Send progress state of all nodes
188
- if self.registry:
189
- self._send_progress_state(prompt_id, self.registry.nodes)
190
-
191
- @override
192
- def update_handler(
193
- self,
194
- node_id: str,
195
- value: float,
196
- max_value: float,
197
- state: NodeProgressState,
198
- prompt_id: str,
199
- image: Optional[Image.Image] = None,
200
- ):
201
- # Send progress state of all nodes
202
- if self.registry:
203
- self._send_progress_state(prompt_id, self.registry.nodes)
204
- if image:
205
- # Only send new format if client supports it
206
- if feature_flags.supports_feature(
207
- self.server_instance.sockets_metadata,
208
- self.server_instance.client_id,
209
- "supports_preview_metadata",
210
- ):
211
- metadata = {
212
- "node_id": node_id,
213
- "prompt_id": prompt_id,
214
- "display_node_id": self.registry.dynprompt.get_display_node_id(
215
- node_id
216
- ),
217
- "parent_node_id": self.registry.dynprompt.get_parent_node_id(
218
- node_id
219
- ),
220
- "real_node_id": self.registry.dynprompt.get_real_node_id(node_id),
221
- }
222
- self.server_instance.send_sync(
223
- BinaryEventTypes.PREVIEW_IMAGE_WITH_METADATA,
224
- (image, metadata),
225
- self.server_instance.client_id,
226
- )
227
-
228
- @override
229
- def finish_handler(self, node_id: str, state: NodeProgressState, prompt_id: str):
230
- # Send progress state of all nodes
231
- if self.registry:
232
- self._send_progress_state(prompt_id, self.registry.nodes)
233
-
234
-
235
- class ProgressRegistry:
236
- """
237
- Registry that maintains node progress state and notifies registered handlers.
238
- """
239
-
240
- def __init__(self, prompt_id: str, dynprompt: "DynamicPrompt"):
241
- self.prompt_id = prompt_id
242
- self.dynprompt = dynprompt
243
- self.nodes: Dict[str, NodeProgressState] = {}
244
- self.handlers: Dict[str, ProgressHandler] = {}
245
-
246
- def register_handler(self, handler: ProgressHandler) -> None:
247
- """Register a progress handler"""
248
- self.handlers[handler.name] = handler
249
-
250
- def unregister_handler(self, handler_name: str) -> None:
251
- """Unregister a progress handler"""
252
- if handler_name in self.handlers:
253
- # Allow handler to clean up resources
254
- self.handlers[handler_name].reset()
255
- del self.handlers[handler_name]
256
-
257
- def enable_handler(self, handler_name: str) -> None:
258
- """Enable a progress handler"""
259
- if handler_name in self.handlers:
260
- self.handlers[handler_name].enable()
261
-
262
- def disable_handler(self, handler_name: str) -> None:
263
- """Disable a progress handler"""
264
- if handler_name in self.handlers:
265
- self.handlers[handler_name].disable()
266
-
267
- def ensure_entry(self, node_id: str) -> NodeProgressState:
268
- """Ensure a node entry exists"""
269
- if node_id not in self.nodes:
270
- self.nodes[node_id] = NodeProgressState(
271
- state=NodeState.Pending, value=0, max=1
272
- )
273
- return self.nodes[node_id]
274
-
275
- def start_progress(self, node_id: str) -> None:
276
- """Start progress tracking for a node"""
277
- entry = self.ensure_entry(node_id)
278
- entry["state"] = NodeState.Running
279
- entry["value"] = 0.0
280
- entry["max"] = 1.0
281
-
282
- # Notify all enabled handlers
283
- for handler in self.handlers.values():
284
- if handler.enabled:
285
- handler.start_handler(node_id, entry, self.prompt_id)
286
-
287
- def update_progress(
288
- self, node_id: str, value: float, max_value: float, image: Optional[Image.Image]
289
- ) -> None:
290
- """Update progress for a node"""
291
- entry = self.ensure_entry(node_id)
292
- entry["state"] = NodeState.Running
293
- entry["value"] = value
294
- entry["max"] = max_value
295
-
296
- # Notify all enabled handlers
297
- for handler in self.handlers.values():
298
- if handler.enabled:
299
- handler.update_handler(
300
- node_id, value, max_value, entry, self.prompt_id, image
301
- )
302
-
303
- def finish_progress(self, node_id: str) -> None:
304
- """Finish progress tracking for a node"""
305
- entry = self.ensure_entry(node_id)
306
- entry["state"] = NodeState.Finished
307
- entry["value"] = entry["max"]
308
-
309
- # Notify all enabled handlers
310
- for handler in self.handlers.values():
311
- if handler.enabled:
312
- handler.finish_handler(node_id, entry, self.prompt_id)
313
-
314
- def reset_handlers(self) -> None:
315
- """Reset all handlers"""
316
- for handler in self.handlers.values():
317
- handler.reset()
318
-
319
- # Global registry instance
320
- global_progress_registry: ProgressRegistry = None
321
-
322
- def reset_progress_state(prompt_id: str, dynprompt: "DynamicPrompt") -> None:
323
- global global_progress_registry
324
-
325
- # Reset existing handlers if registry exists
326
- if global_progress_registry is not None:
327
- global_progress_registry.reset_handlers()
328
-
329
- # Create new registry
330
- global_progress_registry = ProgressRegistry(prompt_id, dynprompt)
331
-
332
-
333
- def add_progress_handler(handler: ProgressHandler) -> None:
334
- registry = get_progress_state()
335
- handler.set_registry(registry)
336
- registry.register_handler(handler)
337
-
338
-
339
- def get_progress_state() -> ProgressRegistry:
340
- global global_progress_registry
341
- if global_progress_registry is None:
342
- from comfy_execution.graph import DynamicPrompt
343
-
344
- global_progress_registry = ProgressRegistry(
345
- prompt_id="", dynprompt=DynamicPrompt({})
346
- )
347
- return global_progress_registry
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
comfy_execution/utils.py DELETED
@@ -1,46 +0,0 @@
1
- import contextvars
2
- from typing import Optional, NamedTuple
3
-
4
- class ExecutionContext(NamedTuple):
5
- """
6
- Context information about the currently executing node.
7
-
8
- Attributes:
9
- node_id: The ID of the currently executing node
10
- list_index: The index in a list being processed (for operations on batches/lists)
11
- """
12
- prompt_id: str
13
- node_id: str
14
- list_index: Optional[int]
15
-
16
- current_executing_context: contextvars.ContextVar[Optional[ExecutionContext]] = contextvars.ContextVar("current_executing_context", default=None)
17
-
18
- def get_executing_context() -> Optional[ExecutionContext]:
19
- return current_executing_context.get(None)
20
-
21
- class CurrentNodeContext:
22
- """
23
- Context manager for setting the current executing node context.
24
-
25
- Sets the current_executing_context on enter and resets it on exit.
26
-
27
- Example:
28
- with CurrentNodeContext(node_id="123", list_index=0):
29
- # Code that should run with the current node context set
30
- process_image()
31
- """
32
- def __init__(self, prompt_id: str, node_id: str, list_index: Optional[int] = None):
33
- self.context = ExecutionContext(
34
- prompt_id= prompt_id,
35
- node_id= node_id,
36
- list_index= list_index
37
- )
38
- self.token = None
39
-
40
- def __enter__(self):
41
- self.token = current_executing_context.set(self.context)
42
- return self
43
-
44
- def __exit__(self, exc_type, exc_val, exc_tb):
45
- if self.token is not None:
46
- current_executing_context.reset(self.token)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
comfy_execution/validation.py DELETED
@@ -1,39 +0,0 @@
1
- from __future__ import annotations
2
-
3
-
4
- def validate_node_input(
5
- received_type: str, input_type: str, strict: bool = False
6
- ) -> bool:
7
- """
8
- received_type and input_type are both strings of the form "T1,T2,...".
9
-
10
- If strict is True, the input_type must contain the received_type.
11
- For example, if received_type is "STRING" and input_type is "STRING,INT",
12
- this will return True. But if received_type is "STRING,INT" and input_type is
13
- "INT", this will return False.
14
-
15
- If strict is False, the input_type must have overlap with the received_type.
16
- For example, if received_type is "STRING,BOOLEAN" and input_type is "STRING,INT",
17
- this will return True.
18
-
19
- Supports pre-union type extension behaviour of ``__ne__`` overrides.
20
- """
21
- # If the types are exactly the same, we can return immediately
22
- # Use pre-union behaviour: inverse of `__ne__`
23
- if not received_type != input_type:
24
- return True
25
-
26
- # Not equal, and not strings
27
- if not isinstance(received_type, str) or not isinstance(input_type, str):
28
- return False
29
-
30
- # Split the type strings into sets for comparison
31
- received_types = set(t.strip() for t in received_type.split(","))
32
- input_types = set(t.strip() for t in input_type.split(","))
33
-
34
- if strict:
35
- # In strict mode, all received types must be in the input types
36
- return received_types.issubset(input_types)
37
- else:
38
- # In non-strict mode, there must be at least one type in common
39
- return len(received_types.intersection(input_types)) > 0