mcpField
C
comfyui-node-migration

comfyui-node-migration

github.com/jtydhr88開発308108 回インストール2 週間前に更新
リポジトリ

comfyui-node-migration について

既存の ComfyUI V1 ノードを V3 API に移行するためのガイドスキルです。旧形式のスキーマ、メソッド、入出力、登録方法を V3 のパターンに対応付け、例と注意点を示します。

主な機能

  1. 01V1プロパティをV3スキーマ項目に対応付け
  2. 02入力型をV3の型付き入力に変換
  3. 03実行と検証のメソッドを更新
  4. 04タプルの戻り値をNodeOutputに変更
  5. 05ノード登録をComfyExtensionに移行
  6. 06V3の動作と移行時の注意点を確認

活用シーン

  1. 01V1カスタムノードをV3 APIへ移植
  2. 02アップグレード時にノードの入出力を変換
  3. 03実行、検証、隠し入力へのアクセスを更新
  4. 04移行前にノード登録方法の変更を確認

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/comfyui-node-migrationを入力して読み込まれたか確認します。

詳細

ComfyUI V1 → V3 移行ガイド

既存のV1ノードを最新のV3 APIに移行します。V3ではクラスメソッド、型付きの入力/出力、ComfyExtensionによる登録を使用します。

移行チェックリスト

  1. 基底クラスをio.ComfyNodeに変更します
  2. INPUT_TYPES()を、io.Schemaを返すdefine_schema()に置き換えます
  3. 実行関数の名前をexecuteに変更し、@classmethodを付けます
  4. 戻り値のタプルをio.NodeOutput(...)に置き換えます
  5. IS_CHANGEDをfingerprint_inputsに置き換えます
  6. VALIDATE_INPUTSをvalidate_inputsに置き換えます
  7. check_lazy_statusを@classmethodにします
  8. NODE_CLASS_MAPPINGSをComfyExtensionとcomfy_entrypoint()に置き換えます
  9. 隠し入力には、kwargsではなくcls.hiddenからアクセスします
  10. __init__メソッドを削除します(V3ではインスタンス状態を使用しません)

新旧の比較

V1(変更前)
import torch

class ImageInvertV1:
    CATEGORY = "image"
    FUNCTION = "invert"
    RETURN_TYPES = ("IMAGE",)
    RETURN_NAMES = ("image",)
    OUTPUT_TOOLTIPS = ("The inverted image",)
    DESCRIPTION = "Inverts image colors"

    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
                "image": ("IMAGE",),
                "strength": ("FLOAT", {
                    "default": 1.0,
                    "min": 0.0,
                    "max": 1.0,
                    "step": 0.01,
                }),
            },
            "optional": {
                "mask": ("MASK",),
            },
            "hidden": {
                "unique_id": "UNIQUE_ID",
            },
        }

    @classmethod
    def IS_CHANGED(s, image, strength, mask=None, unique_id=None):
        return strength

    @classmethod
    def VALIDATE_INPUTS(s, image, strength, mask=None, unique_id=None):
        if strength < 0:
            return "Strength must be non-negative"
        return True

    def invert(self, image, strength, mask=None, unique_id=None):
        inverted = 1.0 - image
        result = image * (1 - strength) + inverted * strength
        if mask is not None:
            result = image * (1 - mask.unsqueeze(-1)) + result * mask.unsqueeze(-1)
        return (result,)

NODE_CLASS_MAPPINGS = {"ImageInvertV1": ImageInvertV1}
NODE_DISPLAY_NAME_MAPPINGS = {"ImageInvertV1": "Invert Image"}
V3(変更後)
import torch
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io

class ImageInvertV3(io.ComfyNode):
    @classmethod
    def define_schema(cls):
        return io.Schema(
            node_id="ImageInvertV3",
            display_name="Invert Image",
            description="Inverts image colors",
            category="image",
            inputs=[
                io.Image.Input("image"),
                io.Float.Input("strength", default=1.0, min=0.0, max=1.0, step=0.01),
                io.Mask.Input("mask", optional=True),
            ],
            outputs=[
                io.Image.Output("IMAGE", tooltip="The inverted image"),
            ],
            hidden=[io.Hidden.unique_id],
        )

    @classmethod
    def fingerprint_inputs(cls, image, strength, mask=None):
        return strength

    @classmethod
    def validate_inputs(cls, image, strength, mask=None):
        if strength < 0:
            return "Strength must be non-negative"
        return True

    @classmethod
    def execute(cls, image, strength, mask=None):
        node_id = cls.hidden.unique_id  # access hidden via cls.hidden

        inverted = 1.0 - image
        result = image * (1 - strength) + inverted * strength
        if mask is not None:
            result = image * (1 - mask.unsqueeze(-1)) + result * mask.unsqueeze(-1)
        return io.NodeOutput(result)


class MyExtension(ComfyExtension):
    @override
    async def get_node_list(self) -> list[type[io.ComfyNode]]:
        return [ImageInvertV3]

async def comfy_entrypoint() -> MyExtension:
    return MyExtension()

プロパティの対応表

V1のプロパティV3での相当項目
CATEGORY = "image"io.Schema(category="image")
FUNCTION = "my_func"常にexecute(固定名)
RETURN_TYPES = ("IMAGE",)outputs=[io.Image.Output()]
RETURN_NAMES = ("image",)outputs=[io.Image.Output(display_name="image")]
OUTPUT_TOOLTIPS = ("tip",)outputs=[io.Image.Output(tooltip="tip")]
OUTPUT_NODE = Trueio.Schema(is_output_node=True)
DEPRECATED = Trueio.Schema(is_deprecated=True)
EXPERIMENTAL = Trueio.Schema(is_experimental=True)
API_NODE = Trueio.Schema(is_api_node=True)
NOT_IDEMPOTENT = Trueio.Schema(not_idempotent=True)
DESCRIPTION = "..."io.Schema(description="...")
SEARCH_ALIASES = [...]io.Schema(search_aliases=[...])
INPUT_IS_LIST = Trueio.Schema(is_input_list=True)
OUTPUT_IS_LIST = (True,)io.Image.Output(is_output_list=True)
DEV_ONLY = Trueio.Schema(is_dev_only=True)
ESSENTIALS_CATEGORY = "Basic"io.Schema(essentials_category="Basic")

入力型の対応表

V1 InputV3 Input
("IMAGE",)io.Image.Input("id")
("MASK",)io.Mask.Input("id")
("LATENT",)io.Latent.Input("id")
("MODEL",)io.Model.Input("id")
("CLIP",)io.Clip.Input("id")
("VAE",)io.Vae.Input("id")
("CONDITIONING",)io.Conditioning.Input("id")
("INT", {"default": 0, ...})io.Int.Input("id", default=0, ...)
("FLOAT", {"default": 1.0, ...})io.Float.Input("id", default=1.0, ...)
("STRING", {"multiline": True})io.String.Input("id", multiline=True)
("BOOLEAN", {"default": True})io.Boolean.Input("id", default=True)
(["opt1", "opt2"],)io.Combo.Input("id", options=["opt1", "opt2"])
("CONTROL_NET",)io.ControlNet.Input("id")
("CLIP_VISION",)io.ClipVision.Input("id")
("CLIP_VISION_OUTPUT",)io.ClipVisionOutput.Input("id")
("STYLE_MODEL",)io.StyleModel.Input("id")
("GLIGEN",)io.Gligen.Input("id")
("UPSCALE_MODEL",)io.UpscaleModel.Input("id")
("AUDIO",)io.Audio.Input("id")
("VIDEO",)io.Video.Input("id")
("SAMPLER",)io.Sampler.Input("id")
("SIGMAS",)io.Sigmas.Input("id")
("NOISE",)io.Noise.Input("id")
("GUIDER",)io.Guider.Input("id")
("HOOKS",)io.Hooks.Input("id")
("LORA_MODEL",)io.LoraModel.Input("id")
("MESH",)io.Mesh.Input("id")
("VOXEL",)io.Voxel.Input("id")
("FILE_3D",)io.File3DAny.Input("id")
("FILE_3D_GLB",)io.File3DGLB.Input("id")
("SVG",)io.SVG.Input("id")
("COLOR",)io.Color.Input("id")
("BOUNDING_BOX",)io.BoundingBox.Input("id")
("CURVE",)io.Curve.Input("id")
("LATENT_UPSCALE_MODEL",)io.LatentUpscaleModel.Input("id")
("MODEL_PATCH",)io.ModelPatch.Input("id")
("HOOK_KEYFRAMES",)io.HookKeyframes.Input("id")
("AUDIO_ENCODER",)io.AudioEncoder.Input("id")
("AUDIO_ENCODER_OUTPUT",)io.AudioEncoderOutput.Input("id")
("TRACKS",)io.Tracks.Input("id")
("LOSS_MAP",)io.LossMap.Input("id")
("TIMESTEPS_RANGE",)io.TimestepsRange.Input("id")
("LATENT_OPERATION",)io.LatentOperation.Input("id")
("WEBCAM",)io.Webcam.Input("id")
("PHOTOMAKER",)io.Photomaker.Input("id")
("WAN_CAMERA_EMBEDDING",)io.WanCameraEmbedding.Input("id")
("LOAD_3D",)io.Load3D.Input("id")
("LOAD_3D_ANIMATION",)io.Load3DAnimation.Input("id")
("LOAD3D_CAMERA",)io.Load3DCamera.Input("id")
("FILE_3D_GLTF",)io.File3DGLTF.Input("id")
("FILE_3D_FBX",)io.File3DFBX.Input("id")
("FILE_3D_OBJ",)io.File3DOBJ.Input("id")
("FILE_3D_STL",)io.File3DSTL.Input("id")
("FILE_3D_USDZ",)io.File3DUSDZ.Input("id")
("FILE_3D_PLY",)io.File3DPLY.Input("id")
("FILE_3D_SPLAT",)io.File3DSPLAT.Input("id")
("FILE_3D_SPZ",)io.File3DSPZ.Input("id")
("FILE_3D_KSPLAT",)io.File3DKSPLAT.Input("id")
("FILE_3D_SPLAT_ANY",)io.File3DSplatAny.Input("id")
("FILE_3D_POINT_CLOUD_ANY",)io.File3DPointCloudAny.Input("id")
("SPLAT",)io.Splat.Input("id")
("LOAD3D_MODEL_INFO",)io.Load3DModelInfo.Input("id")
("BACKGROUND_REMOVAL",)io.BackgroundRemoval.Input("id")
("DICT",)io.Dict.Input("id")
("ARRAY",)io.Array.Input("id")
("COLORS",)io.Colors.Input("id")
("BOUNDING_BOXES",)io.BoundingBoxes.Input("id")
("RANGE",)io.Range.Input("id")
("HISTOGRAM",)io.Histogram.Input("id")
("POINT",)io.Point.Input("id")
("FACE_ANALYSIS",)io.FaceAnalysis.Input("id")
("BBOX",)io.BBOX.Input("id")
("SEGS",)io.SEGS.Input("id")
("IMAGECOMPARE",)io.ImageCompare.Input("id")
("LAYERS",)io.Layers.Input("id")
("COMPOSITOR",)io.Compositor.Input("id")
("CAMERA_INFO_STATE",)io.CameraInfoState.Input("id")
("VIDEO_EDIT",)io.VideoEdit.Input("id", features=["trim", "crop"])
("RESOLUTION_PREVIEW",)io.ResolutionPreview.Input("id")
("*",)io.AnyType.Input("id") or io.MultiType.Input("id", types=[...])

メソッドの移行

Execute メソッド
# V1: instance method with custom name
class V1Node:
    FUNCTION = "process"
    def process(self, image, value):
        return (result,)

# V3: classmethod named "execute", returns NodeOutput
class V3Node(io.ComfyNode):
    @classmethod
    def execute(cls, image, value):
        return io.NodeOutput(result)
IS_CHANGED → fingerprint_inputs
# V1
@classmethod
def IS_CHANGED(s, **kwargs):
    return float("NaN")  # always re-execute

# V3
@classmethod
def fingerprint_inputs(cls, **kwargs):
    import time
    return time.time()  # always re-execute
VALIDATE_INPUTS → validate_inputs
# V1
@classmethod
def VALIDATE_INPUTS(s, input_types=None, **kwargs):
    return True

# V3
@classmethod
def validate_inputs(cls, input_types=None, **kwargs):
    return True
check_lazy_status
# V1: instance method
def check_lazy_status(self, **kwargs):
    return ["input_name"]

# V3: classmethod
@classmethod
def check_lazy_status(cls, **kwargs):
    return ["input_name"]
非表示の入力
# V1: received as kwargs
def execute(self, image, unique_id=None, prompt=None):
    node_id = unique_id

# V3: accessed via cls.hidden
@classmethod
def execute(cls, image):
    node_id = cls.hidden.unique_id
    prompt = cls.hidden.prompt

登録の移行

# V1
NODE_CLASS_MAPPINGS = {
    "Node1": Node1Class,
    "Node2": Node2Class,
}
NODE_DISPLAY_NAME_MAPPINGS = {
    "Node1": "Node One",
    "Node2": "Node Two",
}
WEB_DIRECTORY = "./js"

# V3
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io

class MyExtension(ComfyExtension):
    @override
    async def get_node_list(self) -> list[type[io.ComfyNode]]:
        return [Node1Class, Node2Class]

    @override
    async def on_load(self):
        # Optional: initialization logic
        pass

async def comfy_entrypoint() -> MyExtension:
    return MyExtension()

# WEB_DIRECTORY still works the same way for JS extensions
WEB_DIRECTORY = "./js"

出力ノードの移行

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

    def save(self, images, prefix):
        # ... save logic ...
        return {"ui": {"images": results}}

# V3
from comfy_api.latest import io, ui

class V3SaveNode(io.ComfyNode):
    @classmethod
    def define_schema(cls):
        return io.Schema(
            node_id="V3SaveNode",
            display_name="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):
        saved = ui.ImageSaveHelper.get_save_images_ui(images, prefix, cls=cls)
        return io.NodeOutput(ui=saved)

主な注意点

  1. インスタンスの状態はありません:V3 の execute はクラスメソッドです。self に状態を保存しないでください。必要に応じて外部ストレージを使用してください。
  2. メソッド名は固定です:必ず execute を使用してください。独自の名前は使用できません。
  3. 非表示の入力へのアクセス方法が変更されました:関数のパラメーターではなく、cls.hidden.prompt を使用してください。
  4. 戻り値の型が変更されました:(val,) ではなく io.NodeOutput(val) を使用してください。
  5. オプションの入力:別途 "optional" 辞書を使わず、execute のパラメーターに =None をデフォルト値として指定してください。
  6. 非同期処理のサポート:V3 の execute は async def execute(cls, ...) として定義できます。

関連項目

  • comfyui-node-basics - V3 ノードの基本
  • comfyui-node-packaging - プロジェクトの構成
  • comfyui-node-lifecycle - 実行ライフサイクルの違い

コメント