Skip to content

Commit 3696c5b

Browse files
authored
Add has_intermediate_output flag for nodes with interactive UI (#13048)
1 parent 3a56201 commit 3696c5b

File tree

6 files changed

+54
-5
lines changed

6 files changed

+54
-5
lines changed

comfy_api/latest/_io.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,6 +1373,7 @@ class NodeInfoV1:
13731373
price_badge: dict | None = None
13741374
search_aliases: list[str]=None
13751375
essentials_category: str=None
1376+
has_intermediate_output: bool=None
13761377

13771378

13781379
@dataclass
@@ -1496,6 +1497,16 @@ class Schema:
14961497
"""When True, all inputs from the prompt will be passed to the node as kwargs, even if not defined in the schema."""
14971498
essentials_category: str | None = None
14981499
"""Optional category for the Essentials tab. Path-based like category field (e.g., 'Basic', 'Image Tools/Editing')."""
1500+
has_intermediate_output: bool=False
1501+
"""Flags this node as having intermediate output that should persist across page refreshes.
1502+
1503+
Nodes with this flag behave like output nodes (their UI results are cached and resent
1504+
to the frontend) but do NOT automatically get added to the execution list. This means
1505+
they will only execute if they are on the dependency path of a real output node.
1506+
1507+
Use this for nodes with interactive/operable UI regions that produce intermediate outputs
1508+
(e.g., Image Crop, Painter) rather than final outputs (e.g., Save Image).
1509+
"""
14991510

15001511
def validate(self):
15011512
'''Validate the schema:
@@ -1595,6 +1606,7 @@ def get_v1_info(self, cls) -> NodeInfoV1:
15951606
category=self.category,
15961607
description=self.description,
15971608
output_node=self.is_output_node,
1609+
has_intermediate_output=self.has_intermediate_output,
15981610
deprecated=self.is_deprecated,
15991611
experimental=self.is_experimental,
16001612
dev_only=self.is_dev_only,
@@ -1886,6 +1898,14 @@ def OUTPUT_NODE(cls): # noqa
18861898
cls.GET_SCHEMA()
18871899
return cls._OUTPUT_NODE
18881900

1901+
_HAS_INTERMEDIATE_OUTPUT = None
1902+
@final
1903+
@classproperty
1904+
def HAS_INTERMEDIATE_OUTPUT(cls): # noqa
1905+
if cls._HAS_INTERMEDIATE_OUTPUT is None:
1906+
cls.GET_SCHEMA()
1907+
return cls._HAS_INTERMEDIATE_OUTPUT
1908+
18891909
_INPUT_IS_LIST = None
18901910
@final
18911911
@classproperty
@@ -1978,6 +1998,8 @@ def GET_SCHEMA(cls) -> Schema:
19781998
cls._API_NODE = schema.is_api_node
19791999
if cls._OUTPUT_NODE is None:
19802000
cls._OUTPUT_NODE = schema.is_output_node
2001+
if cls._HAS_INTERMEDIATE_OUTPUT is None:
2002+
cls._HAS_INTERMEDIATE_OUTPUT = schema.has_intermediate_output
19812003
if cls._INPUT_IS_LIST is None:
19822004
cls._INPUT_IS_LIST = schema.is_input_list
19832005
if cls._NOT_IDEMPOTENT is None:

comfy_extras/nodes_glsl.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,7 @@ def define_schema(cls) -> io.Schema:
813813
"u_resolution (vec2) is always available."
814814
),
815815
is_experimental=True,
816+
has_intermediate_output=True,
816817
inputs=[
817818
io.String.Input(
818819
"fragment_shader",

comfy_extras/nodes_images.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def define_schema(cls):
5959
display_name="Image Crop",
6060
category="image/transform",
6161
essentials_category="Image Tools",
62+
has_intermediate_output=True,
6263
inputs=[
6364
IO.Image.Input("image"),
6465
IO.BoundingBox.Input("crop_region", component="ImageCrop"),

comfy_extras/nodes_painter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def define_schema(cls):
3030
node_id="Painter",
3131
display_name="Painter",
3232
category="image",
33+
has_intermediate_output=True,
3334
inputs=[
3435
io.Image.Input(
3536
"image",

execution.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,19 @@ def format_value(x):
411411
else:
412412
return str(x)
413413

414+
def _is_intermediate_output(dynprompt, node_id):
415+
class_type = dynprompt.get_node(node_id)["class_type"]
416+
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
417+
return getattr(class_def, 'HAS_INTERMEDIATE_OUTPUT', False)
418+
419+
def _send_cached_ui(server, node_id, display_node_id, cached, prompt_id, ui_outputs):
420+
if server.client_id is None:
421+
return
422+
cached_ui = cached.ui or {}
423+
server.send_sync("executed", { "node": node_id, "display_node": display_node_id, "output": cached_ui.get("output", None), "prompt_id": prompt_id }, server.client_id)
424+
if cached.ui is not None:
425+
ui_outputs[node_id] = cached.ui
426+
414427
async def execute(server, dynprompt, caches, current_item, extra_data, executed, prompt_id, execution_list, pending_subgraph_results, pending_async_nodes, ui_outputs):
415428
unique_id = current_item
416429
real_node_id = dynprompt.get_real_node_id(unique_id)
@@ -421,11 +434,7 @@ async def execute(server, dynprompt, caches, current_item, extra_data, executed,
421434
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
422435
cached = await caches.outputs.get(unique_id)
423436
if cached is not None:
424-
if server.client_id is not None:
425-
cached_ui = cached.ui or {}
426-
server.send_sync("executed", { "node": unique_id, "display_node": display_node_id, "output": cached_ui.get("output",None), "prompt_id": prompt_id }, server.client_id)
427-
if cached.ui is not None:
428-
ui_outputs[unique_id] = cached.ui
437+
_send_cached_ui(server, unique_id, display_node_id, cached, prompt_id, ui_outputs)
429438
get_progress_state().finish_progress(unique_id)
430439
execution_list.cache_update(unique_id, cached)
431440
return (ExecutionResult.SUCCESS, None, None)
@@ -767,6 +776,16 @@ async def execute_async(self, prompt, prompt_id, extra_data={}, execute_outputs=
767776
self.caches.outputs.poll(ram_headroom=self.cache_args["ram"])
768777
else:
769778
# Only execute when the while-loop ends without break
779+
# Send cached UI for intermediate output nodes that weren't executed
780+
for node_id in dynamic_prompt.all_node_ids():
781+
if node_id in executed:
782+
continue
783+
if not _is_intermediate_output(dynamic_prompt, node_id):
784+
continue
785+
cached = await self.caches.outputs.get(node_id)
786+
if cached is not None:
787+
display_node_id = dynamic_prompt.get_display_node_id(node_id)
788+
_send_cached_ui(self.server, node_id, display_node_id, cached, prompt_id, ui_node_outputs)
770789
self.add_message("execution_success", { "prompt_id": prompt_id }, broadcast=False)
771790

772791
ui_outputs = {}

server.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,11 @@ def node_info(node_class):
709709
else:
710710
info['output_node'] = False
711711

712+
if hasattr(obj_class, 'HAS_INTERMEDIATE_OUTPUT') and obj_class.HAS_INTERMEDIATE_OUTPUT == True:
713+
info['has_intermediate_output'] = True
714+
else:
715+
info['has_intermediate_output'] = False
716+
712717
if hasattr(obj_class, 'CATEGORY'):
713718
info['category'] = obj_class.CATEGORY
714719

0 commit comments

Comments
 (0)