diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index 1cbc8ed26..fdeffea2d 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -1373,6 +1373,7 @@ class NodeInfoV1: price_badge: dict | None = None search_aliases: list[str]=None essentials_category: str=None + has_intermediate_output: bool=None @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.""" essentials_category: str | None = None """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): '''Validate the schema: @@ -1595,6 +1606,7 @@ class Schema: category=self.category, description=self.description, output_node=self.is_output_node, + has_intermediate_output=self.has_intermediate_output, deprecated=self.is_deprecated, experimental=self.is_experimental, dev_only=self.is_dev_only, @@ -1886,6 +1898,14 @@ class _ComfyNodeBaseInternal(_ComfyNodeInternal): cls.GET_SCHEMA() 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 @final @classproperty @@ -1978,6 +1998,8 @@ class _ComfyNodeBaseInternal(_ComfyNodeInternal): cls._API_NODE = schema.is_api_node if cls._OUTPUT_NODE is None: 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: cls._INPUT_IS_LIST = schema.is_input_list if cls._NOT_IDEMPOTENT is None: diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 2a59a9285..1ee7e17ad 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -762,6 +762,7 @@ class GLSLShader(io.ComfyNode): "Apply GLSL ES fragment shaders to images. " "u_resolution (vec2) is always available." ), + has_intermediate_output=True, inputs=[ io.String.Input( "fragment_shader", diff --git a/comfy_extras/nodes_images.py b/comfy_extras/nodes_images.py index a8223cf8b..a77f0641f 100644 --- a/comfy_extras/nodes_images.py +++ b/comfy_extras/nodes_images.py @@ -59,6 +59,7 @@ class ImageCropV2(IO.ComfyNode): display_name="Image Crop", category="image/transform", essentials_category="Image Tools", + has_intermediate_output=True, inputs=[ IO.Image.Input("image"), IO.BoundingBox.Input("crop_region", component="ImageCrop"), diff --git a/comfy_extras/nodes_painter.py b/comfy_extras/nodes_painter.py index b9ecdf5ea..e104c8480 100644 --- a/comfy_extras/nodes_painter.py +++ b/comfy_extras/nodes_painter.py @@ -30,6 +30,7 @@ class PainterNode(io.ComfyNode): node_id="Painter", display_name="Painter", category="image", + has_intermediate_output=True, inputs=[ io.Image.Input( "image", diff --git a/execution.py b/execution.py index 1a6c3429c..04a8669ef 100644 --- a/execution.py +++ b/execution.py @@ -1071,7 +1071,7 @@ async def validate_prompt(prompt_id, prompt, partial_execution_list: Union[list[ } return (False, error, [], {}) - if hasattr(class_, 'OUTPUT_NODE') and class_.OUTPUT_NODE is True: + if (hasattr(class_, 'OUTPUT_NODE') and class_.OUTPUT_NODE is True) or getattr(class_, 'HAS_INTERMEDIATE_OUTPUT', False): if partial_execution_list is None or x in partial_execution_list: outputs.add(x) diff --git a/server.py b/server.py index 173a28376..27b14825e 100644 --- a/server.py +++ b/server.py @@ -709,6 +709,11 @@ class PromptServer(): else: 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'): info['category'] = obj_class.CATEGORY