mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-03-30 21:43:43 +08:00
Add has_intermediate_output flag for nodes with interactive UI
This commit is contained in:
parent
1dc64f3526
commit
bb2d1c824b
@ -1373,6 +1373,7 @@ class NodeInfoV1:
|
|||||||
price_badge: dict | None = None
|
price_badge: dict | None = None
|
||||||
search_aliases: list[str]=None
|
search_aliases: list[str]=None
|
||||||
essentials_category: str=None
|
essentials_category: str=None
|
||||||
|
has_intermediate_output: bool=None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -1496,6 +1497,16 @@ class Schema:
|
|||||||
"""When True, all inputs from the prompt will be passed to the node as kwargs, even if not defined in the schema."""
|
"""When True, all inputs from the prompt will be passed to the node as kwargs, even if not defined in the schema."""
|
||||||
essentials_category: str | None = None
|
essentials_category: str | None = None
|
||||||
"""Optional category for the Essentials tab. Path-based like category field (e.g., 'Basic', 'Image Tools/Editing')."""
|
"""Optional category for the Essentials tab. Path-based like category field (e.g., 'Basic', 'Image Tools/Editing')."""
|
||||||
|
has_intermediate_output: bool=False
|
||||||
|
"""Flags this node as having intermediate output that should persist across page refreshes.
|
||||||
|
|
||||||
|
Nodes with this flag behave like output nodes (their UI results are cached and resent
|
||||||
|
to the frontend) but do NOT automatically get added to the execution list. This means
|
||||||
|
they will only execute if they are on the dependency path of a real output node.
|
||||||
|
|
||||||
|
Use this for nodes with interactive/operable UI regions that produce intermediate outputs
|
||||||
|
(e.g., Image Crop, Painter) rather than final outputs (e.g., Save Image).
|
||||||
|
"""
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
'''Validate the schema:
|
'''Validate the schema:
|
||||||
@ -1595,6 +1606,7 @@ class Schema:
|
|||||||
category=self.category,
|
category=self.category,
|
||||||
description=self.description,
|
description=self.description,
|
||||||
output_node=self.is_output_node,
|
output_node=self.is_output_node,
|
||||||
|
has_intermediate_output=self.has_intermediate_output,
|
||||||
deprecated=self.is_deprecated,
|
deprecated=self.is_deprecated,
|
||||||
experimental=self.is_experimental,
|
experimental=self.is_experimental,
|
||||||
dev_only=self.is_dev_only,
|
dev_only=self.is_dev_only,
|
||||||
@ -1886,6 +1898,14 @@ class _ComfyNodeBaseInternal(_ComfyNodeInternal):
|
|||||||
cls.GET_SCHEMA()
|
cls.GET_SCHEMA()
|
||||||
return cls._OUTPUT_NODE
|
return cls._OUTPUT_NODE
|
||||||
|
|
||||||
|
_HAS_INTERMEDIATE_OUTPUT = None
|
||||||
|
@final
|
||||||
|
@classproperty
|
||||||
|
def HAS_INTERMEDIATE_OUTPUT(cls): # noqa
|
||||||
|
if cls._HAS_INTERMEDIATE_OUTPUT is None:
|
||||||
|
cls.GET_SCHEMA()
|
||||||
|
return cls._HAS_INTERMEDIATE_OUTPUT
|
||||||
|
|
||||||
_INPUT_IS_LIST = None
|
_INPUT_IS_LIST = None
|
||||||
@final
|
@final
|
||||||
@classproperty
|
@classproperty
|
||||||
@ -1978,6 +1998,8 @@ class _ComfyNodeBaseInternal(_ComfyNodeInternal):
|
|||||||
cls._API_NODE = schema.is_api_node
|
cls._API_NODE = schema.is_api_node
|
||||||
if cls._OUTPUT_NODE is None:
|
if cls._OUTPUT_NODE is None:
|
||||||
cls._OUTPUT_NODE = schema.is_output_node
|
cls._OUTPUT_NODE = schema.is_output_node
|
||||||
|
if cls._HAS_INTERMEDIATE_OUTPUT is None:
|
||||||
|
cls._HAS_INTERMEDIATE_OUTPUT = schema.has_intermediate_output
|
||||||
if cls._INPUT_IS_LIST is None:
|
if cls._INPUT_IS_LIST is None:
|
||||||
cls._INPUT_IS_LIST = schema.is_input_list
|
cls._INPUT_IS_LIST = schema.is_input_list
|
||||||
if cls._NOT_IDEMPOTENT is None:
|
if cls._NOT_IDEMPOTENT is None:
|
||||||
|
|||||||
@ -813,6 +813,7 @@ class GLSLShader(io.ComfyNode):
|
|||||||
"u_resolution (vec2) is always available."
|
"u_resolution (vec2) is always available."
|
||||||
),
|
),
|
||||||
is_experimental=True,
|
is_experimental=True,
|
||||||
|
has_intermediate_output=True,
|
||||||
inputs=[
|
inputs=[
|
||||||
io.String.Input(
|
io.String.Input(
|
||||||
"fragment_shader",
|
"fragment_shader",
|
||||||
|
|||||||
@ -59,6 +59,7 @@ class ImageCropV2(IO.ComfyNode):
|
|||||||
display_name="Image Crop",
|
display_name="Image Crop",
|
||||||
category="image/transform",
|
category="image/transform",
|
||||||
essentials_category="Image Tools",
|
essentials_category="Image Tools",
|
||||||
|
has_intermediate_output=True,
|
||||||
inputs=[
|
inputs=[
|
||||||
IO.Image.Input("image"),
|
IO.Image.Input("image"),
|
||||||
IO.BoundingBox.Input("crop_region", component="ImageCrop"),
|
IO.BoundingBox.Input("crop_region", component="ImageCrop"),
|
||||||
|
|||||||
@ -30,6 +30,7 @@ class PainterNode(io.ComfyNode):
|
|||||||
node_id="Painter",
|
node_id="Painter",
|
||||||
display_name="Painter",
|
display_name="Painter",
|
||||||
category="image",
|
category="image",
|
||||||
|
has_intermediate_output=True,
|
||||||
inputs=[
|
inputs=[
|
||||||
io.Image.Input(
|
io.Image.Input(
|
||||||
"image",
|
"image",
|
||||||
|
|||||||
29
execution.py
29
execution.py
@ -411,6 +411,19 @@ def format_value(x):
|
|||||||
else:
|
else:
|
||||||
return str(x)
|
return str(x)
|
||||||
|
|
||||||
|
def _is_intermediate_output(dynprompt, node_id):
|
||||||
|
class_type = dynprompt.get_node(node_id)["class_type"]
|
||||||
|
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
|
||||||
|
return getattr(class_def, 'HAS_INTERMEDIATE_OUTPUT', False)
|
||||||
|
|
||||||
|
def _send_cached_ui(server, node_id, display_node_id, cached, prompt_id, ui_outputs):
|
||||||
|
if server.client_id is None:
|
||||||
|
return
|
||||||
|
cached_ui = cached.ui or {}
|
||||||
|
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)
|
||||||
|
if cached.ui is not None:
|
||||||
|
ui_outputs[node_id] = cached.ui
|
||||||
|
|
||||||
async def execute(server, dynprompt, caches, current_item, extra_data, executed, prompt_id, execution_list, pending_subgraph_results, pending_async_nodes, ui_outputs):
|
async def execute(server, dynprompt, caches, current_item, extra_data, executed, prompt_id, execution_list, pending_subgraph_results, pending_async_nodes, ui_outputs):
|
||||||
unique_id = current_item
|
unique_id = current_item
|
||||||
real_node_id = dynprompt.get_real_node_id(unique_id)
|
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,
|
|||||||
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
|
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
|
||||||
cached = await caches.outputs.get(unique_id)
|
cached = await caches.outputs.get(unique_id)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
if server.client_id is not None:
|
_send_cached_ui(server, unique_id, display_node_id, cached, prompt_id, ui_outputs)
|
||||||
cached_ui = cached.ui or {}
|
|
||||||
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)
|
|
||||||
if cached.ui is not None:
|
|
||||||
ui_outputs[unique_id] = cached.ui
|
|
||||||
get_progress_state().finish_progress(unique_id)
|
get_progress_state().finish_progress(unique_id)
|
||||||
execution_list.cache_update(unique_id, cached)
|
execution_list.cache_update(unique_id, cached)
|
||||||
return (ExecutionResult.SUCCESS, None, None)
|
return (ExecutionResult.SUCCESS, None, None)
|
||||||
@ -767,6 +776,16 @@ class PromptExecutor:
|
|||||||
self.caches.outputs.poll(ram_headroom=self.cache_args["ram"])
|
self.caches.outputs.poll(ram_headroom=self.cache_args["ram"])
|
||||||
else:
|
else:
|
||||||
# Only execute when the while-loop ends without break
|
# Only execute when the while-loop ends without break
|
||||||
|
# Send cached UI for intermediate output nodes that weren't executed
|
||||||
|
for node_id in dynamic_prompt.all_node_ids():
|
||||||
|
if node_id in executed:
|
||||||
|
continue
|
||||||
|
if not _is_intermediate_output(dynamic_prompt, node_id):
|
||||||
|
continue
|
||||||
|
cached = await self.caches.outputs.get(node_id)
|
||||||
|
if cached is not None:
|
||||||
|
display_node_id = dynamic_prompt.get_display_node_id(node_id)
|
||||||
|
_send_cached_ui(self.server, node_id, display_node_id, cached, prompt_id, ui_node_outputs)
|
||||||
self.add_message("execution_success", { "prompt_id": prompt_id }, broadcast=False)
|
self.add_message("execution_success", { "prompt_id": prompt_id }, broadcast=False)
|
||||||
|
|
||||||
ui_outputs = {}
|
ui_outputs = {}
|
||||||
|
|||||||
@ -709,6 +709,11 @@ class PromptServer():
|
|||||||
else:
|
else:
|
||||||
info['output_node'] = False
|
info['output_node'] = False
|
||||||
|
|
||||||
|
if hasattr(obj_class, 'HAS_INTERMEDIATE_OUTPUT') and obj_class.HAS_INTERMEDIATE_OUTPUT == True:
|
||||||
|
info['has_intermediate_output'] = True
|
||||||
|
else:
|
||||||
|
info['has_intermediate_output'] = False
|
||||||
|
|
||||||
if hasattr(obj_class, 'CATEGORY'):
|
if hasattr(obj_class, 'CATEGORY'):
|
||||||
info['category'] = obj_class.CATEGORY
|
info['category'] = obj_class.CATEGORY
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user