見出し画像

ComfyUIのカスタムノードを作るには

はじめに

まだ全部書き終わっていないのですが、必要な人がいれば。
ComfyUIでカスタムノードを作成する方法をまとめてみました。
カスタムノードの作り方については公式ドキュメントが存在しません。
私の方でComfyUIのコードを解析したり、他のカスタムノードのコードを解析した結果を記載します。
解析で記載しているので突然ComfyUI側の挙動が変わる可能性もありますし、誤っている可能性も否定できません。あしからず。

カスタムノードのフォルダ構成

基本

フォルダ構成は以下の通り

./ComfyUI/custom_nodes/カスタムノードフォルダ/

カスタムノードフォルダはモジュールとして扱われるので、__init__.pyというファイルが必要です。ComfyUI起動時に各フォルダをモジュールとして読み込むので、__init__.pyファイルを読み込みます。

__init__.pyに全てのコードを書くことも可能ですが、可読性を上げるためにノードの実態のソースコードは別のファイルに記載しておくほうがよいでしょう。どちらかというとpython的な話ですが。

行儀が悪い手法

./ComfyUI/custom_nodes/pythonファイル

行儀は悪いですが直接pythonのファイルを置いても読み込んでもらえます。
この場合は全てのpyファイルを読み込みます。

コーディング基本

__init__.py

基本

このファイルはComfyUI起動時に読み込まれます。ここで必ず実施しなければいけないのは、 dict型のNODE_CLASS_MAPPINGS変数を設定すること です。
サンプルとして以下のコードをおいておきます。

from .text_util import StringNodeClass

NODE_CLASS_MAPPINGS = {
    "StringNode": StringNodeClass
}

このサンプルでは、NODE_CLASS_MAPPINGS変数にdictを設定しています。
keyはComfyUI上で表示されるノード名。valueは実態となるクラスです。
複数のノードを定義するのであれば複数のkey-valueを並べてください。

クラスの実態を__init__.pyに書いてもいいですが煩雑になるので text_util.pyというファイルに記載することにしました。スクリプトの冒頭でfrom import を呼び出しています。

応用

__init__.pyはComfyUI起動時に読み込まれて実行されます。
ここにpip installを実行するコードを記載しておくことで、ノード実行時に必要なモジュールをインストールさせるような動きもできます。
一般に公開してComfyUI Managerなどに登録してもらうならそこまで書くべきですが、他の方のカスタムノードを見ていると「インストール後に手作業でpip installを実行してモジュールを入れておいてくれ」という人もいるので、コーディングが面倒ならそれもありだと思います。

クラスの実態

クラスの実態のサンプルを置きます。
このノードは入力された文字列を出力に渡すだけのシンプルなノードです

class StringNodeClass:
    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
                "raw_text": ("STRING", {"multiline": True})
            }
        }
    RETURN_TYPES = ("STRING", )
    RETURN_NAMES = ("output", )
    FUNCTION = "run"
    OUTPUT_NODE = True

    CATEGORY = "sample_node"


    def run(self, raw_text):
        return (raw_text, )

クラスメソッド INPUT_TYPES

    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
                "raw_text": ("STRING", {"multiline": True})
            }
        }

このメソッドはユーザがノードを置いた時に実行されます。
ただし、実行されるのは最初の1個だけです。2個目が置かれても実行されません。また「refresh」ボタンを押したときに、ノードが置いてあれば1回だけ実行されます。

ここにはノードの 入力 を実装する必要があります。メソッドの返り値としてdictを返すと 入力 が実装されます。
keyは文字列"required"。valueに 入力 を定義したdictを入れます

入力

"raw_text": ("STRING", {"multiline": True})

入力はさらにdict型です。
keyはノードの入力名です。入力の名称として出てくる文字列ですね。
valueには入力のタイプを定義します。

入力のタイプ

("STRING", {"multiline": True})

入力の属性はタプルです。
1つ目にはデータ型を文字列で記載します。
2つ目は属性を記載します

属性は下記のように省略可能です。

("STRING")

属性

入力ノードには属性を設定することができます。
まず考えるべきはその入力ノードは「ユーザに設定させるか?」あるいは「別のノードからの入力にするか?」は考えておきましょう。

  • forceInput属性:bool型

"raw_text": ("STRING", {"forceInput": True}),

これをTrueに指定するとノード上でのユーザからの入力が強制になります。Falseの場合はメニューから他のノードから入力させるよう設定できます。

  • default属性:any型

"raw_text": ("STRING", {"default": ""}),

forceInput属性がFalseかつPrimitiveな値を入力にする場合(INTとかFLOATとかSTRING)はこの属性が必須です。この属性を設定していないとノード設置時にjavascriptがエラーを出力します。
意味はその名の通り、デフォルトの値を設定するだけです。

  • multiline属性: bool型

"raw_text": ("STRING", {"multiline": True})

これをTrueに設定することで複数行の入力を可能とします。
この実装でノードを配置すると以下のようになります

UI上で文字列を入力させることができるようになります。

クラス変数

    RETURN_TYPES = ("STRING", )
    RETURN_NAMES = ("output", )
    FUNCTION = "run"
    OUTPUT_NODE = True

    CATEGORY = "sample_node"

RETURN_NAMESおよびRETURN_TYPES

RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("output", )

ノードの出力を定義します。両方ともタプルで設定します。
RETURN_NAMESは出力ノード名。
RETURN_TYPESは出力ノードのデータ型を記載します。
RETURN_NAMESは未定義でも動作します。
タプルの最後は必ず省略して終わらせてください。

FUNCTION

FUNCTION = "run"

ノード実行時に呼び出すメソッド名を定義します。このサンプルではrunメソッドが呼びされます。

OUTPUT_NODE

Trueに設定しておくと終端ノードとして取り扱われるようです。
ComfyUIは終端ノードに向かって実行していくため出力側にノードを接続しないようなカスタムノードはこの値をTrueに設定しておくほうがよいです。

CATEGORY

CATEGORY = "sample_node"

ノードがどのカテゴリに配置されるかを定義します。
既存のカテゴリを指定してもよいですし、新規のカテゴリでも可能です。

実行メソッド

    def run(self, raw_text):
        return (raw_text, )

クラス変数のFUNCTIONで定義されたメソッドです。

引数

def run(self, raw_text):


引数には入力から渡されたものが並びます。
Comfyというかpythonの基本ですがメソッドの先頭はself。それ以降に入力が並びます。
入力ノードが複数あれば複数の引数を並べてください。

返り値

return (raw_text, )

返り値はdictで渡すパターンとタプルで渡すパターンがあります。
このサンプルはタプルで渡しています。

ノードの出力をタプルで並べて渡してあげれば先に繋がっているノードにデータを渡すことができます。このタプルの最後は必ず省略するようにしてください。

コーディング基本の実装結果

ここまで紹介したノードは文字列を渡すだけの非常にシンプルなものなので、Encode処理からテキストを分離するくらいにしか使えません。しかしメソッド・入力・出力を変えることでいろいろな処理を実装できるようになります。

データ型

入力ノードと出力ノードで使われるデータ型です。
データ型は文字列なので無限に型が定義できます。
とはいえあまり使われない型をノードの入出力に定義しても使い道がないため、
ComfyUIのソースコード内で使われているデータ型とpythonの型を列挙しておきます。

  • STRING str型

  • IMAGE     torch.Tensor型

  • INT           int型

  • FLOAT  float型

  • MASK 調査中

  • LATENT 調査中(list)

  • CONDITIONING     torch.Tensor型

  • MODEL     調査中

  • CLIP     ComfyUI内のCLIPクラスのインスタンス

  • VAE     ComfyUI内のVAEクラスのインスタンス

  • CONTROL_NET     調査中

  • CLIP_VISION     調査中 

  • CLIP_VISION_OUTPUT     調査中

  • STYLE_MODEL  調査中

  • GLIGEN    調査中

基本的にこれらの型を使う方が汎用性が高くなるはずです。

リストを入出力させたい場合

クラス内にて下記のクラス変数を設定することで実現可能
INPUT_IS_LIST
OUTPUT_IS_LIST

    INPUT_IS_LIST = (True, )
    OUTPUT_IS_LIST = (True,)

上記例のようにタプルで設定して、リストなのかどうかを設定することでリストを入力として受け取ることが可能となります。

入力ノードのコツ

リスト選択

Ksamplerのsampler_nameのように複数の文字列から選択させたいケースがあるかと思います。
その場合はデータ型の代わりにリストを渡してあげることで実現可能です。
`"scheduler": (["euler", "dpm_2", "dpm_2mm"], ),`
ノードを実行した時の引数には選択された文字列が格納されてきます。(インデックス番号ではありません)

ファイル選択

これはリスト選択の応用です
TODO:今後執筆します

入力規制

入力として数値を入れる場合はその範囲について規制を行いたい場合があります。
これは属性で解決します。
`"height": ("INT", {"default": 512, "min": 64, "max": 1024, "step": 8}),`

この例は読んで字の如くデフォルト512。64から1024までの値を入力することができるようになります。またマウスで動かした時は8づつ変化するようになります。

seed

`"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),`

seedという名称でINT型を設定すると、自動でcontrol_after_generateがあわせて入力ノードに付与されます。乱数制御には非常に便利です。

init.py のコツ

NODE_DISPLAY_NAME_MAPPINGS

TODO:今後執筆します

WEB_DIRECTORY

TODO:今後執筆します

クラス実装のコツ

インスタンス化されるタイミング

ノードクラスは実行時にはインスタンス化されて動きます。それは以下のように動作します。

  • 該当ノードが初回実行される際にインスタンス化されます

  • ノードを複数おいてあると、それぞれインスタンス化されます

  • 二回目以降実行されるときはすでにあるインスタンスが使われます

モデルファイルを読み込んでから動作するなど、初回実行に時間のかかるノードを作りたい場合は以下のようなロジックがよいかと思います。

class FlagNodeClass:
    @classmethod
    def INPUT_TYPES(s):
        return {
            "required": {
                "index": ("INT", {"default": 0, "min": 0, "max": 3})
            }
        }
    RETURN_TYPES = ("STRING", )
    RETURN_NAMES = ("output", )
    FUNCTION = "run"
    OUTPUT_NODE = True

    CATEGORY = "sample_node"

    def __init__(self):
        #   無効値に初期化しておく
        self.model = None
        self.before_index = -1

    def run(self, index):
        #   無効値あるいは前回と入力の条件が異なる時は初期化処理を実施
        if self.before_index != index:
            self.model = init_model(index)
        #   初期化後のルーチンを以降に記載(サンプルでは省略)
        return (raw_text, )
    
    def init_model(self, index):
        #   初期化処理を実装(サンプルでは省略)
        pass

入力条件が異なるときだけ重い初期化処理を実行するようにさせることで、2回目以降の処理を高速にさせます。ComfyUIのLoraLoaderはこのような実装がされています。

ノードの実行結果をweb画面に出力する

TODO:今後執筆します

参考

ComfyUI公式リポジトリのnodes.py

この記事が気に入ったらサポートをしてみませんか?