Skip to content

Commit afc85cd

Browse files
Add Load Image Output node (#6790)
* 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
1 parent acc152b commit afc85cd

File tree

3 files changed

+69
-1
lines changed

3 files changed

+69
-1
lines changed

api_server/routes/internal/internal_routes.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from aiohttp import web
22
from typing import Optional
3-
from folder_paths import folder_names_and_paths
3+
from folder_paths import folder_names_and_paths, get_directory_by_type
44
from api_server.services.terminal_service import TerminalService
55
import app.logger
6+
import os
67

78
class InternalRoutes:
89
'''
@@ -50,6 +51,20 @@ async def get_folder_paths(request):
5051
response[key] = folder_names_and_paths[key][0]
5152
return web.json_response(response)
5253

54+
@self.routes.get('/files/{directory_type}')
55+
async def get_files(request: web.Request) -> web.Response:
56+
directory_type = request.match_info['directory_type']
57+
if directory_type not in ("output", "input", "temp"):
58+
return web.json_response({"error": "Invalid directory type"}, status=400)
59+
60+
directory = get_directory_by_type(directory_type)
61+
sorted_files = sorted(
62+
(entry for entry in os.scandir(directory) if entry.is_file()),
63+
key=lambda entry: -entry.stat().st_mtime
64+
)
65+
return web.json_response([entry.name for entry in sorted_files], status=200)
66+
67+
5368
def get_app(self):
5469
if self._app is None:
5570
self._app = web.Application()

comfy/comfy_types/node_typing.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ def __ne__(self, value: object) -> bool:
6666
b = frozenset(value.split(","))
6767
return not (b.issubset(a) or a.issubset(b))
6868

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

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

117138

118139
class HiddenInputTypeDict(TypedDict):

nodes.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,6 +1763,36 @@ def VALIDATE_INPUTS(s, image):
17631763

17641764
return True
17651765

1766+
1767+
class LoadImageOutput(LoadImage):
1768+
@classmethod
1769+
def INPUT_TYPES(s):
1770+
return {
1771+
"required": {
1772+
"image": ("COMBO", {
1773+
"image_upload": True,
1774+
"image_folder": "output",
1775+
"remote": {
1776+
"route": "/internal/files/output",
1777+
"refresh_button": True,
1778+
"control_after_refresh": "first",
1779+
},
1780+
}),
1781+
}
1782+
}
1783+
1784+
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."
1785+
EXPERIMENTAL = True
1786+
FUNCTION = "load_image_output"
1787+
1788+
def load_image_output(self, image):
1789+
return self.load_image(f"{image} [output]")
1790+
1791+
@classmethod
1792+
def VALIDATE_INPUTS(s, image):
1793+
return True
1794+
1795+
17661796
class ImageScale:
17671797
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
17681798
crop_methods = ["disabled", "center"]
@@ -1949,6 +1979,7 @@ def expand_image(self, image, left, top, right, bottom, feathering):
19491979
"PreviewImage": PreviewImage,
19501980
"LoadImage": LoadImage,
19511981
"LoadImageMask": LoadImageMask,
1982+
"LoadImageOutput": LoadImageOutput,
19521983
"ImageScale": ImageScale,
19531984
"ImageScaleBy": ImageScaleBy,
19541985
"ImageInvert": ImageInvert,
@@ -2049,6 +2080,7 @@ def expand_image(self, image, left, top, right, bottom, feathering):
20492080
"PreviewImage": "Preview Image",
20502081
"LoadImage": "Load Image",
20512082
"LoadImageMask": "Load Image (as Mask)",
2083+
"LoadImageOutput": "Load Image (from Outputs)",
20522084
"ImageScale": "Upscale Image",
20532085
"ImageScaleBy": "Upscale Image By",
20542086
"ImageUpscaleWithModel": "Upscale Image (using Model)",

0 commit comments

Comments
 (0)