comfyui-node-outputs について
comfyui-node-outputsは、NodeOutputでデータや画面表示を返す方法を解説するComfyUIノード開発者向けガイドです。メディアのプレビュー、ファイル保存、一時プレビューと永続出力の違いを扱います。
主な機能
- 01複数のノード出力形式の設定
- 02UIヘルパーによる画像・音声・動画のプレビュー
- 03画像と音声ファイルの保存
- 04テキストと3Dモデルのプレビュー表示
- 05一時プレビューと永続保存の区別
活用シーン
- 01プレビューとともにノードデータを返す
- 02画像または音声の保存ノードの作成
- 03動画、テキスト、3Dモデルの結果表示
- 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)になり、{"result": [...]} ではなく、上記の "3d" 形式で出力します。
画像の保存
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を使用します
コアによる 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キーはコアによって消費されます。 - 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- ノードの構造とスキーマcomfyui-node-datatypes- データ型の形式comfyui-node-lifecycle- 実行フロー