Skip to content

Commit

Permalink
Add Load Image Output node (#6790)
Browse files Browse the repository at this point in the history
* add LoadImageOutput node

* add route for input/output/temp files

* update node_typing.py

* use literal type for image_folder field

* mark node as beta
  • Loading branch information
christian-byrne authored Feb 18, 2025
1 parent acc152b commit afc85cd
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 1 deletion.
17 changes: 16 additions & 1 deletion api_server/routes/internal/internal_routes.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from aiohttp import web
from typing import Optional
from folder_paths import folder_names_and_paths
from folder_paths import folder_names_and_paths, get_directory_by_type
from api_server.services.terminal_service import TerminalService
import app.logger
import os

class InternalRoutes:
'''
Expand Down Expand Up @@ -50,6 +51,20 @@ async def get_folder_paths(request):
response[key] = folder_names_and_paths[key][0]
return web.json_response(response)

@self.routes.get('/files/{directory_type}')
async def get_files(request: web.Request) -> web.Response:
directory_type = request.match_info['directory_type']
if directory_type not in ("output", "input", "temp"):
return web.json_response({"error": "Invalid directory type"}, status=400)

directory = get_directory_by_type(directory_type)
sorted_files = sorted(
(entry for entry in os.scandir(directory) if entry.is_file()),
key=lambda entry: -entry.stat().st_mtime
)
return web.json_response([entry.name for entry in sorted_files], status=200)


def get_app(self):
if self._app is None:
self._app = web.Application()
Expand Down
21 changes: 21 additions & 0 deletions comfy/comfy_types/node_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ def __ne__(self, value: object) -> bool:
b = frozenset(value.split(","))
return not (b.issubset(a) or a.issubset(b))

class RemoteInputOptions(TypedDict):
route: str
"""The route to the remote source."""
refresh_button: bool
"""Specifies whether to show a refresh button in the UI below the widget."""
control_after_refresh: Literal["first", "last"]
"""Specifies the control after the refresh button is clicked. If "first", the first item will be automatically selected, and so on."""
timeout: int
"""The maximum amount of time to wait for a response from the remote source in milliseconds."""
max_retries: int
"""The maximum number of retries before aborting the request."""
refresh: int
"""The TTL of the remote input's value in milliseconds. Specifies the interval at which the remote input's value is refreshed."""

class InputTypeOptions(TypedDict):
"""Provides type hinting for the return type of the INPUT_TYPES node function.
Expand Down Expand Up @@ -113,6 +126,14 @@ class InputTypeOptions(TypedDict):
# defaultVal: str
dynamicPrompts: bool
"""Causes the front-end to evaluate dynamic prompts (``STRING``)"""
# class InputTypeCombo(InputTypeOptions):
image_upload: bool
"""Specifies whether the input should have an image upload button and image preview attached to it. Requires that the input's name is `image`."""
image_folder: Literal["input", "output", "temp"]
"""Specifies which folder to get preview images from if the input has the ``image_upload`` flag.
"""
remote: RemoteInputOptions
"""Specifies the configuration for a remote input."""


class HiddenInputTypeDict(TypedDict):
Expand Down
32 changes: 32 additions & 0 deletions nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,36 @@ def VALIDATE_INPUTS(s, image):

return True


class LoadImageOutput(LoadImage):
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("COMBO", {
"image_upload": True,
"image_folder": "output",
"remote": {
"route": "/internal/files/output",
"refresh_button": True,
"control_after_refresh": "first",
},
}),
}
}

DESCRIPTION = "Load an image from the output folder. When the refresh button is clicked, the node will update the image list and automatically select the first image, allowing for easy iteration."
EXPERIMENTAL = True
FUNCTION = "load_image_output"

def load_image_output(self, image):
return self.load_image(f"{image} [output]")

@classmethod
def VALIDATE_INPUTS(s, image):
return True


class ImageScale:
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
crop_methods = ["disabled", "center"]
Expand Down Expand Up @@ -1949,6 +1979,7 @@ def expand_image(self, image, left, top, right, bottom, feathering):
"PreviewImage": PreviewImage,
"LoadImage": LoadImage,
"LoadImageMask": LoadImageMask,
"LoadImageOutput": LoadImageOutput,
"ImageScale": ImageScale,
"ImageScaleBy": ImageScaleBy,
"ImageInvert": ImageInvert,
Expand Down Expand Up @@ -2049,6 +2080,7 @@ def expand_image(self, image, left, top, right, bottom, feathering):
"PreviewImage": "Preview Image",
"LoadImage": "Load Image",
"LoadImageMask": "Load Image (as Mask)",
"LoadImageOutput": "Load Image (from Outputs)",
"ImageScale": "Upscale Image",
"ImageScaleBy": "Upscale Image By",
"ImageUpscaleWithModel": "Upscale Image (using Model)",
Expand Down

3 comments on commit afc85cd

@HelloClyde
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are image_upload and image_folder still retained in the COMBO type? This seems to have no direct connection with the COMBO type, and it appears they are coupled together. Is this for maintaining compatibility?

@christian-byrne
Copy link
Contributor Author

@christian-byrne christian-byrne commented on afc85cd Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you referring to the types only? The image_upload and image_folder tell the frontend to turn the node into a node that can upload images (via combo value selection, drag-and-drop, copy-paste, and file upload button). The assumption is that the value of the input corresponds to a valid filename in the given folder. They could be applied to STRING inputs as well but it's just not supported currently.

Or are you just suggesting they be moved into a nested dict? In that case I would agree with you but image_upload has already existed for a long time.

@HelloClyde
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you referring to the types only? The image_upload and image_folder tell the frontend to turn the node into a node that can upload images (via combo value selection, drag-and-drop, copy-paste, and file upload button). The assumption is that the value of the input corresponds to a valid filename in the given folder. They could be applied to STRING inputs as well but it's just not supported currently.

Or are you just suggesting they be moved into a nested dict? In that case I would agree with you but image_upload has already existed for a long time.

Thank you for the explanation. I have learned about the historical background.

Please sign in to comment.