Add output socket to save nodes (#13866)
Some checks are pending
Detect Unreviewed Merge / detect (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run

This commit is contained in:
Alexis Rolland 2026-06-22 10:15:28 +08:00 committed by GitHub
parent 0d8b7510bd
commit b0f9e326af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 52 additions and 58 deletions

View File

@ -158,7 +158,7 @@ class SaveAudio(IO.ComfyNode):
return IO.Schema( return IO.Schema(
node_id="SaveAudio", node_id="SaveAudio",
search_aliases=["export flac"], search_aliases=["export flac"],
display_name="Save Audio (FLAC) (Deprecated)", display_name="Save Audio (FLAC) (DEPRECATED)",
category="audio", category="audio",
essentials_category="Audio", essentials_category="Audio",
inputs=[ inputs=[
@ -166,8 +166,9 @@ class SaveAudio(IO.ComfyNode):
IO.String.Input("filename_prefix", default="audio/ComfyUI"), IO.String.Input("filename_prefix", default="audio/ComfyUI"),
], ],
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
is_output_node=True,
is_deprecated=True, is_deprecated=True,
is_output_node=True,
outputs=[IO.Audio.Output("audio")]
) )
@classmethod @classmethod
@ -175,11 +176,10 @@ class SaveAudio(IO.ComfyNode):
if audio is None: if audio is None:
raise ValueError("SaveAudio: input audio is None (source video may have no audio track).") raise ValueError("SaveAudio: input audio is None (source video may have no audio track).")
return IO.NodeOutput( return IO.NodeOutput(
audio,
ui=UI.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=format) ui=UI.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=format)
) )
save_flac = execute # TODO: remove
class SaveAudioMP3(IO.ComfyNode): class SaveAudioMP3(IO.ComfyNode):
@classmethod @classmethod
@ -187,7 +187,7 @@ class SaveAudioMP3(IO.ComfyNode):
return IO.Schema( return IO.Schema(
node_id="SaveAudioMP3", node_id="SaveAudioMP3",
search_aliases=["export mp3"], search_aliases=["export mp3"],
display_name="Save Audio (MP3) (Deprecated)", display_name="Save Audio (MP3) (DEPRECATED)",
category="audio", category="audio",
essentials_category="Audio", essentials_category="Audio",
inputs=[ inputs=[
@ -196,8 +196,9 @@ class SaveAudioMP3(IO.ComfyNode):
IO.Combo.Input("quality", options=["V0", "128k", "320k"], default="V0"), IO.Combo.Input("quality", options=["V0", "128k", "320k"], default="V0"),
], ],
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
is_output_node=True,
is_deprecated=True, is_deprecated=True,
is_output_node=True,
outputs=[IO.Audio.Output("audio")]
) )
@classmethod @classmethod
@ -205,13 +206,12 @@ class SaveAudioMP3(IO.ComfyNode):
if audio is None: if audio is None:
raise ValueError("SaveAudioMP3: input audio is None (source video may have no audio track).") raise ValueError("SaveAudioMP3: input audio is None (source video may have no audio track).")
return IO.NodeOutput( return IO.NodeOutput(
audio,
ui=UI.AudioSaveHelper.get_save_audio_ui( ui=UI.AudioSaveHelper.get_save_audio_ui(
audio, filename_prefix=filename_prefix, cls=cls, format=format, quality=quality audio, filename_prefix=filename_prefix, cls=cls, format=format, quality=quality
) )
) )
save_mp3 = execute # TODO: remove
class SaveAudioOpus(IO.ComfyNode): class SaveAudioOpus(IO.ComfyNode):
@classmethod @classmethod
@ -219,7 +219,7 @@ class SaveAudioOpus(IO.ComfyNode):
return IO.Schema( return IO.Schema(
node_id="SaveAudioOpus", node_id="SaveAudioOpus",
search_aliases=["export opus"], search_aliases=["export opus"],
display_name="Save Audio (Opus) (Deprecated)", display_name="Save Audio (Opus) (DEPRECATED)",
category="audio", category="audio",
inputs=[ inputs=[
IO.Audio.Input("audio"), IO.Audio.Input("audio"),
@ -227,8 +227,9 @@ class SaveAudioOpus(IO.ComfyNode):
IO.Combo.Input("quality", options=["64k", "96k", "128k", "192k", "320k"], default="128k"), IO.Combo.Input("quality", options=["64k", "96k", "128k", "192k", "320k"], default="128k"),
], ],
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
is_output_node=True,
is_deprecated=True, is_deprecated=True,
is_output_node=True,
outputs=[IO.Audio.Output("audio")]
) )
@classmethod @classmethod
@ -236,13 +237,12 @@ class SaveAudioOpus(IO.ComfyNode):
if audio is None: if audio is None:
raise ValueError("SaveAudioOpus: input audio is None (source video may have no audio track).") raise ValueError("SaveAudioOpus: input audio is None (source video may have no audio track).")
return IO.NodeOutput( return IO.NodeOutput(
audio,
ui=UI.AudioSaveHelper.get_save_audio_ui( ui=UI.AudioSaveHelper.get_save_audio_ui(
audio, filename_prefix=filename_prefix, cls=cls, format=format, quality=quality audio, filename_prefix=filename_prefix, cls=cls, format=format, quality=quality
) )
) )
save_opus = execute # TODO: remove
class SaveAudioAdvanced(IO.ComfyNode): class SaveAudioAdvanced(IO.ComfyNode):
@classmethod @classmethod
@ -258,10 +258,7 @@ class SaveAudioAdvanced(IO.ComfyNode):
IO.String.Input( IO.String.Input(
"filename_prefix", "filename_prefix",
default="audio/ComfyUI", default="audio/ComfyUI",
tooltip=( tooltip=("The prefix for the file to save. May include formatting tokens such as %date:yyyy-MM-dd%."),
"The prefix for the file to save. May include formatting tokens "
"such as %date:yyyy-MM-dd%."
),
), ),
IO.DynamicCombo.Input( IO.DynamicCombo.Input(
"format", "format",
@ -279,6 +276,7 @@ class SaveAudioAdvanced(IO.ComfyNode):
], ],
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
is_output_node=True, is_output_node=True,
outputs=[IO.Audio.Output("audio")],
) )
@classmethod @classmethod
@ -289,7 +287,7 @@ class SaveAudioAdvanced(IO.ComfyNode):
ui=UI.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=file_format, quality=quality) ui=UI.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=file_format, quality=quality)
else: else:
ui=UI.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=file_format) ui=UI.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=file_format)
return IO.NodeOutput(ui=ui) return IO.NodeOutput(audio, ui=ui)
class PreviewAudio(IO.ComfyNode): class PreviewAudio(IO.ComfyNode):
@ -305,13 +303,14 @@ class PreviewAudio(IO.ComfyNode):
], ],
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
is_output_node=True, is_output_node=True,
outputs=[IO.Audio.Output("audio")]
) )
@classmethod @classmethod
def execute(cls, audio) -> IO.NodeOutput: def execute(cls, audio) -> IO.NodeOutput:
if audio is None: if audio is None:
raise ValueError("PreviewAudio: input audio is None (source video may have no audio track).") raise ValueError("PreviewAudio: input audio is None (source video may have no audio track).")
return IO.NodeOutput(ui=UI.PreviewAudio(audio, cls=cls)) return IO.NodeOutput(audio, ui=UI.PreviewAudio(audio, cls=cls))
save_flac = execute # TODO: remove save_flac = execute # TODO: remove

View File

@ -214,11 +214,13 @@ class SaveAnimatedWEBP(IO.ComfyNode):
], ],
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
is_output_node=True, is_output_node=True,
outputs=[IO.Image.Output(display_name="images")]
) )
@classmethod @classmethod
def execute(cls, images, fps, filename_prefix, lossless, quality, method, num_frames=0) -> IO.NodeOutput: def execute(cls, images, fps, filename_prefix, lossless, quality, method, num_frames=0) -> IO.NodeOutput:
return IO.NodeOutput( return IO.NodeOutput(
images,
ui=UI.ImageSaveHelper.get_save_animated_webp_ui( ui=UI.ImageSaveHelper.get_save_animated_webp_ui(
images=images, images=images,
filename_prefix=filename_prefix, filename_prefix=filename_prefix,
@ -230,8 +232,6 @@ class SaveAnimatedWEBP(IO.ComfyNode):
) )
) )
save_images = execute # TODO: remove
class SaveAnimatedPNG(IO.ComfyNode): class SaveAnimatedPNG(IO.ComfyNode):
@ -249,11 +249,13 @@ class SaveAnimatedPNG(IO.ComfyNode):
], ],
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
is_output_node=True, is_output_node=True,
outputs=[IO.Image.Output(display_name="images")]
) )
@classmethod @classmethod
def execute(cls, images, fps, compress_level, filename_prefix="ComfyUI") -> IO.NodeOutput: def execute(cls, images, fps, compress_level, filename_prefix="ComfyUI") -> IO.NodeOutput:
return IO.NodeOutput( return IO.NodeOutput(
images,
ui=UI.ImageSaveHelper.get_save_animated_png_ui( ui=UI.ImageSaveHelper.get_save_animated_png_ui(
images=images, images=images,
filename_prefix=filename_prefix, filename_prefix=filename_prefix,
@ -263,8 +265,6 @@ class SaveAnimatedPNG(IO.ComfyNode):
) )
) )
save_images = execute # TODO: remove
class ImageStitch(IO.ComfyNode): class ImageStitch(IO.ComfyNode):
"""Upstreamed from https://github.com/kijai/ComfyUI-KJNodes""" """Upstreamed from https://github.com/kijai/ComfyUI-KJNodes"""
@ -513,6 +513,7 @@ class SaveSVGNode(IO.ComfyNode):
], ],
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
is_output_node=True, is_output_node=True,
outputs=[IO.SVG.Output("svg")],
) )
@classmethod @classmethod
@ -562,9 +563,7 @@ class SaveSVGNode(IO.ComfyNode):
results.append(UI.SavedResult(filename=file, subfolder=subfolder, type=IO.FolderType.output)) results.append(UI.SavedResult(filename=file, subfolder=subfolder, type=IO.FolderType.output))
counter += 1 counter += 1
return IO.NodeOutput(ui={"images": results}) return IO.NodeOutput(svg, ui={"images": results})
save_svg = execute # TODO: remove
class GetImageSize(IO.ComfyNode): class GetImageSize(IO.ComfyNode):
@ -1157,40 +1156,27 @@ class SaveImageAdvanced(IO.ComfyNode):
IO.String.Input( IO.String.Input(
"filename_prefix", "filename_prefix",
default="ComfyUI", default="ComfyUI",
tooltip=( tooltip=("The prefix for the file to save. May include formatting tokens such as %date:yyyy-MM-dd% or %Empty Latent Image.width%."),
"The prefix for the file to save. May include formatting tokens "
"such as %date:yyyy-MM-dd% or %Empty Latent Image.width%."
),
), ),
IO.DynamicCombo.Input( IO.DynamicCombo.Input(
"format", "format",
options=[ options=[
IO.DynamicCombo.Option("png", [ IO.DynamicCombo.Option("png", [
IO.Combo.Input("bit_depth", options=["8-bit", "16-bit"], IO.Combo.Input("bit_depth", options=["8-bit", "16-bit"], default="8-bit", advanced=True),
default="8-bit", advanced=True), IO.Combo.Input("input_color_space", options=["sRGB"], default="sRGB", advanced=True),
IO.Combo.Input("input_color_space", options=["sRGB"],
default="sRGB", advanced=True),
]), ]),
IO.DynamicCombo.Option("exr", [ IO.DynamicCombo.Option("exr", [
IO.Combo.Input("bit_depth", options=["32-bit float"], IO.Combo.Input("bit_depth", options=["32-bit float"], default="32-bit float", advanced=True),
default="32-bit float", advanced=True),
IO.Combo.Input( IO.Combo.Input(
"input_color_space", "input_color_space",
options=["sRGB", "HDR", "linear"], options=["sRGB", "HDR", "linear"],
default="sRGB", default="sRGB",
advanced=True, advanced=True,
tooltip=( tooltip=(
"Colorspace of the input tensor. The EXR is " "Colorspace of the input tensor. The EXR is always written as scene-linear in the matching gamut.\n"
"always written as scene-linear in the matching " "sRGB — input is sRGB-encoded Rec.709; the inverse sRGB EOTF is applied.\n"
"gamut.\n" "HDR — input is HLG-encoded Rec.2020 (BT.2100); the inverse HLG OETF is applied to get scene-linear light.\n"
" 'sRGB' — input is sRGB-encoded Rec.709; " "linear — input is already scene-linear (Rec.709 primaries); written through unchanged. Use this for renderer/compositor output."
"the inverse sRGB EOTF is applied.\n"
" 'HDR' — input is HLG-encoded Rec.2020 "
"(BT.2100); the inverse HLG OETF is applied "
"to get scene-linear light.\n"
" 'linear' — input is already scene-linear "
"(Rec.709 primaries); written through unchanged. "
"Use this for renderer/compositor output."
), ),
), ),
]), ]),
@ -1200,6 +1186,7 @@ class SaveImageAdvanced(IO.ComfyNode):
], ],
hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo], hidden=[IO.Hidden.prompt, IO.Hidden.extra_pnginfo],
is_output_node=True, is_output_node=True,
outputs=[IO.Image.Output(display_name="images")]
) )
@classmethod @classmethod
@ -1237,7 +1224,7 @@ class SaveImageAdvanced(IO.ComfyNode):
results.append({"filename": file, "subfolder": subfolder, "type": "output"}) results.append({"filename": file, "subfolder": subfolder, "type": "output"})
counter += 1 counter += 1
return IO.NodeOutput(ui={"images": results}) return IO.NodeOutput(images, ui={"images": results})
class ImagesExtension(ComfyExtension): class ImagesExtension(ComfyExtension):

View File

@ -27,6 +27,7 @@ class SaveWEBM(io.ComfyNode):
], ],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo], hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True, is_output_node=True,
outputs=[io.Image.Output(display_name="images")]
) )
@classmethod @classmethod
@ -69,7 +70,7 @@ class SaveWEBM(io.ComfyNode):
container.mux(stream.encode()) container.mux(stream.encode())
container.close() container.close()
return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)])) return io.NodeOutput(images, ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]))
class SaveVideo(io.ComfyNode): class SaveVideo(io.ComfyNode):
@classmethod @classmethod
@ -89,6 +90,7 @@ class SaveVideo(io.ComfyNode):
], ],
hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo], hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
is_output_node=True, is_output_node=True,
outputs=[io.Video.Output("video")],
) )
@classmethod @classmethod
@ -117,7 +119,7 @@ class SaveVideo(io.ComfyNode):
metadata=saved_metadata metadata=saved_metadata
) )
return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)])) return io.NodeOutput(video, ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)]))
class CreateVideo(io.ComfyNode): class CreateVideo(io.ComfyNode):

View File

@ -480,11 +480,13 @@ class SaveLatent:
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
return {"required": { "samples": ("LATENT", ), return { "required": {
"samples": ("LATENT",),
"filename_prefix": ("STRING", {"default": "latents/ComfyUI"})}, "filename_prefix": ("STRING", {"default": "latents/ComfyUI"})},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
} }
RETURN_TYPES = () RETURN_TYPES = ("LATENT",)
RETURN_NAMES = ("samples",)
FUNCTION = "save" FUNCTION = "save"
OUTPUT_NODE = True OUTPUT_NODE = True
@ -522,7 +524,7 @@ class SaveLatent:
output["latent_format_version_0"] = torch.tensor([]) output["latent_format_version_0"] = torch.tensor([])
comfy.utils.save_torch_file(output, file, metadata=metadata) comfy.utils.save_torch_file(output, file, metadata=metadata)
return { "ui": { "latents": results } } return { "ui": { "latents": results }, "result": (samples,) }
class LoadLatent: class LoadLatent:
@ -1627,14 +1629,18 @@ class SaveImage:
return { return {
"required": { "required": {
"images": ("IMAGE", {"tooltip": "The images to save."}), "images": ("IMAGE", {"tooltip": "The images to save."}),
"filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."}) "filename_prefix": ("STRING", {
"default": "ComfyUI",
"tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."
})
}, },
"hidden": { "hidden": {
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO" "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
}, },
} }
RETURN_TYPES = () RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "save_images" FUNCTION = "save_images"
OUTPUT_NODE = True OUTPUT_NODE = True
@ -1670,7 +1676,7 @@ class SaveImage:
}) })
counter += 1 counter += 1
return { "ui": { "images": results } } return { "ui": { "images": results }, "result" : (images,) }
class PreviewImage(SaveImage): class PreviewImage(SaveImage):
def __init__(self): def __init__(self):