mcpField
C
comfyui-node-outputs

comfyui-node-outputs

github.com/jtydhr88开发308121 次安装2 周前更新
代码仓库

关于 comfyui-node-outputs

comfyui-node-outputs 是 ComfyUI 节点开发者的输出处理指南,介绍通过 NodeOutput 返回数据和界面内容。它涵盖多媒体预览、文件保存及临时预览与持久化输出的区别。

主要功能

  1. 01配置节点的多种输出类型
  2. 02通过 UI 助手显示图像、音频和视频预览
  3. 03保存图像与音频文件
  4. 04输出文本和 3D 模型预览
  5. 05区分临时预览与持久化保存

适用场景

  1. 01为节点返回数据并显示预览
  2. 02构建图像或音频保存节点
  3. 03展示视频、文本或 3D 模型结果
  4. 04选择临时目录或输出目录保存文件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jtydhr88/comfyui-custom-node-skills --skill comfyui-node-outputs

安装后在 Claude Code 中输入/comfyui-node-outputs确认已加载。

详细介绍

ComfyUI 节点输出

节点通过 io.NodeOutput 返回数据。V3 提供内置 UI 助手,用于预览和保存文件。

基本输出

class SimpleNode(io.ComfyNode):
    @classmethod
    def define_schema(cls):
        return io.Schema(
            node_id="SimpleNode",
            display_name="Simple Node",
            category="example",
            inputs=[io.Float.Input("a"), io.Float.Input("b")],
            outputs=[
                io.Float.Output("SUM"),
                io.Float.Output("PRODUCT"),
            ],
        )

    @classmethod
    def execute(cls, a, b):
        # Values must match output order
        return io.NodeOutput(a + b, a * b)

输出配置

io.Schema(
    outputs=[
        io.Image.Output("IMAGE"),                    # basic output
        io.Int.Output("COUNT"),                      # integer output
        io.Float.Output("VALUE", display_name="Result"),  # custom display name
        io.String.Output("TEXT", tooltip="The processed text"),
        io.Image.Output("FRAMES", is_output_list=True),  # outputs a list
    ],
)

NodeOutput 变体

# Data only
return io.NodeOutput(image_tensor, mask_tensor)

# UI only (output node with no data outputs)
return io.NodeOutput(ui=ui.PreviewImage(images, cls=cls))

# Data + UI
return io.NodeOutput(image_tensor, ui=ui.PreviewImage(images, cls=cls))

# No output
return io.NodeOutput()

# Block execution
return io.NodeOutput(block_execution="Reason for blocking")

# Node expansion (positional args are outputs, not result= keyword)
return io.NodeOutput(output_ref, expand=graph.finalize())

UI 预览助手

从 comfy_api.latest 导入 ui:

from comfy_api.latest import io, ui
PreviewImage

在节点上显示图像预览。会自动保存到临时目录。

# Constructor: PreviewImage(image, animated=False, cls=None)
class PreviewNode(io.ComfyNode):
    @classmethod
    def define_schema(cls):
        return io.Schema(
            node_id="PreviewNode",
            display_name="Preview Image",
            category="image",
            is_output_node=True,
            inputs=[io.Image.Input("images")],
            outputs=[],
            hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
        )

    @classmethod
    def execute(cls, images):
        return io.NodeOutput(ui=ui.PreviewImage(images, cls=cls))
PreviewMask
# Constructor: PreviewMask(mask, animated=False, cls=None)
# Auto-converts mask to 3-channel grayscale for display
return io.NodeOutput(ui=ui.PreviewMask(masks, cls=cls))
PreviewAudio
# Constructor: PreviewAudio(audio, cls=None)
# Saves as FLAC to temp directory
return io.NodeOutput(ui=ui.PreviewAudio(audio, cls=cls))
PreviewVideo
# Constructor: PreviewVideo(values: list[SavedResult | dict])
return io.NodeOutput(ui=ui.PreviewVideo(saved_video_results))
PreviewText

显示文本输出:

return io.NodeOutput(ui=ui.PreviewText(value))
PreviewUI3D

显示 3D 模型预览:

return io.NodeOutput(ui=ui.PreviewUI3D(
    model_file=saved_result,   # SavedResult for the 3D file
    camera_info=camera_dict,   # camera position/target/zoom
    bg_image=image_tensor,     # optional background image (via **kwargs)
))
Saved3DModels(标准 3D 输出,v0.40+)

现在,所有核心 3D 保存/预览节点都会将模型作为标准 "3d" 输出项报告(与图像一样显示在队列/资源中):

# Constructor: Saved3DModels(results: list[SavedResult], camera_info=None, model_3d_info=None)
# -> {"3d": results, "camera_info": [camera_info], "model_3d_info": [...]}
return io.NodeOutput(ui=ui.Saved3DModels(
    [ui.SavedResult("model.glb", "", io.FolderType.output)],   # or FolderType.temp for previews
    camera_info=camera_dict,       # extended CameraInfo (quaternion/fov/near/far/frustum)
    model_3d_info=transforms,      # list[Model3DTransform]: position/quaternion/scale per model
))

ui.PreviewUI3DAdvanced(model_file, camera_info, model_3d_info, folder_type=None) 仍作为子类存在,但 model_file 现在是相对路径字符串("subfolder/model.glb",默认文件夹为 output),并且会输出上述 "3d" 格式,而不是 {"result": [...]}。

保存图像

使用 ImageSaveHelper

ui.ImageSaveHelper 类提供了用于多种图像格式的静态方法:

# Save as PNG (returns list[SavedResult])
results = ui.ImageSaveHelper.save_images(
    images,                              # tensor [B,H,W,C]
    filename_prefix="ComfyUI",
    folder_type=io.FolderType.output,    # output, temp, or input
    cls=cls,                             # node class (for metadata)
    compress_level=4,
)

# Save and get UI object directly (saves to output folder)
saved_ui = ui.ImageSaveHelper.get_save_images_ui(images, "ComfyUI", cls=cls)
return io.NodeOutput(ui=saved_ui)

# Save animated PNG
result = ui.ImageSaveHelper.save_animated_png(
    images, "anim", io.FolderType.output, cls=cls, fps=12.0, compress_level=4
)

# Save animated PNG and get UI
saved_ui = ui.ImageSaveHelper.get_save_animated_png_ui(images, "anim", cls=cls, fps=12.0, compress_level=4)

# Save animated WebP
result = ui.ImageSaveHelper.save_animated_webp(
    images, "anim", io.FolderType.output, cls=cls,
    fps=12.0, lossless=False, quality=80, method=4
)

# Save animated WebP and get UI
saved_ui = ui.ImageSaveHelper.get_save_animated_webp_ui(
    images, "anim", cls=cls, fps=12.0, lossless=False, quality=80, method=4
)

简单保存节点示例:

class SaveImageNode(io.ComfyNode):
    @classmethod
    def define_schema(cls):
        return io.Schema(
            node_id="SaveImageNode",
            display_name="Save Image",
            category="image",
            is_output_node=True,
            inputs=[
                io.Image.Input("images"),
                io.String.Input("filename_prefix", default="ComfyUI"),
            ],
            outputs=[],
            hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
        )

    @classmethod
    def execute(cls, images, filename_prefix):
        saved = ui.ImageSaveHelper.get_save_images_ui(images, filename_prefix, cls=cls)
        return io.NodeOutput(ui=saved)
手动保存图像
import os
import json
import numpy as np
from PIL import Image as PILImage
from PIL.PngImagePlugin import PngInfo
import folder_paths

class CustomSaveNode(io.ComfyNode):
    @classmethod
    def define_schema(cls):
        return io.Schema(
            node_id="CustomSaveNode",
            display_name="Custom Save",
            category="image",
            is_output_node=True,
            inputs=[
                io.Image.Input("images"),
                io.String.Input("prefix", default="output"),
            ],
            outputs=[],
            hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
        )

    @classmethod
    def execute(cls, images, prefix):
        output_dir = folder_paths.get_output_directory()
        results = []

        for i, image in enumerate(images):
            # Convert tensor to PIL
            img_array = np.clip(255.0 * image.cpu().numpy(), 0, 255).astype(np.uint8)
            pil_image = PILImage.fromarray(img_array)

            # Add metadata
            metadata = PngInfo()
            if cls.hidden.prompt:
                metadata.add_text("prompt", json.dumps(cls.hidden.prompt))
            if cls.hidden.extra_pnginfo:
                for k, v in cls.hidden.extra_pnginfo.items():
                    metadata.add_text(k, json.dumps(v))

            # Save with counter
            filename = f"{prefix}_{i:05d}.png"
            filepath = os.path.join(output_dir, filename)
            pil_image.save(filepath, pnginfo=metadata)

            results.append(ui.SavedResult(
                filename=filename,
                subfolder="",
                type=io.FolderType.output,
            ))

        return io.NodeOutput(ui=ui.SavedImages(results))

保存音频

ui.AudioSaveHelper 支持 FLAC、MP3 和 Opus 格式:

# Save audio (returns list[SavedResult])
results = ui.AudioSaveHelper.save_audio(
    audio,                               # {"waveform": Tensor, "sample_rate": int}
    filename_prefix="audio",
    folder_type=io.FolderType.output,
    cls=cls,
    format="flac",                       # "flac", "mp3", or "opus" (anything else raises ValueError)
    quality="128k",                      # MP3: "V0","128k","320k"; Opus: "64k"-"320k"
)

# Save and get UI object
saved_ui = ui.AudioSaveHelper.get_save_audio_ui(audio, "audio", cls=cls, format="flac", quality="128k")
return io.NodeOutput(ui=saved_ui)
class SaveAudioNode(io.ComfyNode):
    @classmethod
    def define_schema(cls):
        return io.Schema(
            node_id="SaveAudioNode",
            display_name="Save Audio",
            category="audio",
            is_output_node=True,
            inputs=[
                io.Audio.Input("audio"),
                io.String.Input("prefix", default="audio"),
                io.Combo.Input("format", options=["flac", "mp3", "opus"], default="flac"),
            ],
            outputs=[],
            hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo],
        )

    @classmethod
    def execute(cls, audio, prefix, format):
        saved = ui.AudioSaveHelper.get_save_audio_ui(audio, prefix, cls=cls, format=format)
        return io.NodeOutput(ui=saved)

临时预览与永久保存

  • 预览(PreviewImage 等)保存到 temp 目录,是临时的
  • 保存(ImageSaveHelper.save_images)永久保存到 output 目录
  • 使用 io.FolderType.temp、io.FolderType.output 或 io.FolderType.input

Core 如何对 UI 输出进行后处理

execute 之后,UI 字典中的每个文件条目({"filename", "subfolder", "type"})在发送/存储前都会得到补充:

  • metadata:对于图像/视频文件,除非条目已有 metadata,否则会添加 {"kind", "width", "height"}(视频还会添加 duration、fps、frame_count)(v0.40+)。
  • 启用资源系统时(默认仅在 Windows 独立构建中启用),会添加 id / preview_id;在缓存命中回放时,所有字典条目中的 id/preview_id 都会被移除,并且 asset_preview 键会由 Core 使用。
  • 每次运行都会对 UI 字典进行深拷贝。

因此,不要在 UI 条目中将 id、preview_id、metadata 或 asset_preview 用作自定义键,并确保 UI 值较小且可序列化为 JSON。

V1 输出模式(旧版参考)

class V1SaveNode:
    RETURN_TYPES = ()
    OUTPUT_NODE = True
    FUNCTION = "save"

    def save(self, images, prefix):
        # ... save logic ...
        return {
            "ui": {
                "images": [
                    {"filename": "out.png", "subfolder": "", "type": "output"}
                ]
            }
        }

# Data + UI in V1:
class V1PreviewAndOutput:
    RETURN_TYPES = ("IMAGE",)
    OUTPUT_NODE = True
    FUNCTION = "run"

    def run(self, image):
        # ... preview logic ...
        return {
            "ui": {"images": [...]},
            "result": (processed_image,),
        }

另请参阅

  • comfyui-node-basics - 节点结构和 Schema
  • comfyui-node-datatypes - 数据类型格式
  • comfyui-node-lifecycle - 执行流程

评论

支持 Markdown