メインコンテンツへスキップ
見出し画像

[ComfyMaster53] カスタムノードを自作!(前編) 〜ComfyUIの仕組みとカスタムノードの基礎理解〜 #ComfyUI

    これまでComfyUIを学んできた皆さんは、既にComfyUIを使いこなし、ノードを組み合わせながら画像生成や加工のワークフローを管理している方も多いでしょう。本記事では、そうした既存の知識を前提に、「カスタムノード」の仕組みをもう一段深く掘り下げます。具体的には、ComfyUI内部で使われるノード機構の構造や、フロントエンドとバックエンドがどのようにメッセージをやりとりしているかについてソースコード付きで解説し、画面上のワークフローを支える技術的な背景を理解していただくことを目指します。

    標準ノードだけでもさまざまな処理が可能ですが、さらに独自の画像処理ロジックやUI上のインタラクション、特定のモデルの読み込みなどを柔軟に実装したければ、「カスタムノード」の作成が大いに役立ちます。本記事の後編では、実際に簡単なカスタムノードを開発してComfyUIに組み込み、動作させる具体的な手順をチュートリアル形式で紹介します。まずはこの前編を通じて、カスタムノードに関わる基本的な構造やメッセージ通信の全体像をしっかり把握し、後編の実装へ向けた下準備を整えていきましょう。

    では、ComfyUIの内部構造を大まかにつかむところから始めていきましょう。

    1. フロントエンドとバックエンドの仕組み

    1.1 クライアント(フロントエンド)とサーバー(バックエンド)の役割

    ComfyUIは、表面上は1つのアプリのように見えますが、実際には「サーバー」と「フロントエンド(クライアント)」が連携して動作する構造になっています。多くの場合、ユーザーはWebブラウザを使い、HTML/JavaScriptベースのUIを操作します。これがフロントエンド(クライアント)であり、いわゆる「画面」を提供する役割です。一方、画像生成や画像加工の実処理はPythonで書かれたサーバー側が担い、フロントエンドから発行されたコマンドや入力内容をもとに処理を実行して結果を返します。

    画像

    一般的なウェブアプリ開発と同じような「フロントエンド-バックエンド」構造を取っているため、画面の見た目やユーザビリティに関わる部分(UIデザインや操作ロジック)はJavaScriptやHTML/CSSで扱い、データ処理・ロジック・ライブラリ依存部分はPython側で行うという分担が自然に実現できます。ComfyUIでは、Stable Diffusionモデルをロードし推論を行う部分はPythonのライブラリ(PyTorchなど)で集中して処理され、UIには処理結果(生成画像など)が返されるだけです。

    画像

    カスタムノードの開発では、Python側(バックエンド)だけで完結するケースもあれば、JavaScript側(フロントエンド)に特別なUIを追加したいケースもあります。前編では、フロントエンドとバックエンドがそもそもどのように分かれているのかを理解し、どのあたりで「カスタムノード」による拡張が可動領域になるのかを掴んでください。

    1.2 Client-ServerモデルのメリットとAPIモード

    • 「再利用性が高い」
      Client-Serverモデルでは、サーバー(バックエンド)の機能をAPIとして公開し、フロントエンド(クライアント)はそのAPIを呼び出すだけで済みます。この仕組みは、ComfyUIのUI以外からもPythonサーバー機能を呼び出すAPIモードがある、という形で表れます。つまり、ComfyUI標準のUIを使わず、別のUIやスクリプトからサーバー機能を叩いて同じワークフローを実行することも可能なのです。

    画像
    • 「非同期処理が扱いやすい」
      フロントエンドの操作とバックエンドの長時間処理は、REST APIやWebSocketなどを使って非同期的に連携できます。Stable Diffusionの生成プロセスは数秒以上かかることが多いため、UIは待ち時間を表示したり、途中経過を受け取りながら進捗バーやログを更新できます。

    REST API
    REST (Representational State Transfer) APIは、Webサービス間の通信を実現する設計原則とアーキテクチャスタイルです。シンプルで標準的なHTTPプロトコルを使用することで、異なるシステム間での効率的なデータのやり取りを可能にします。
    REST APIは現代のWeb開発において不可欠な要素となっており、モバイルアプリケーション、Webアプリケーション、IoTデバイスなど、様々なシステム間の通信に広く活用されています。

    WebSocket
    WebSocketは、クライアントとサーバー間で双方向のリアルタイム通信を可能にするプロトコルです。従来のHTTP通信と異なり、一度接続を確立すると、継続的な双方向通信が可能になります。
    WebSocketは、リアルタイム性が求められるモダンなWebアプリケーションにおいて、重要な技術として広く採用されています。特に、即時性が重要なアプリケーションでは、従来のHTTP通信の代替として効果的に機能します。

    画像
    • 「コードの分割・セキュリティ」
      フロント側で直接モデルデータを触る必要がなく、安全かつ複数人での開発体制にも向いています。また、バージョンが異なるPythonライブラリの問題もフロント側には基本的に影響しにくく、UI開発と処理ロジック開発を分担しやすいという利点もあります。

    画像

    1.3 フロントエンドからバックエンドへの受け渡しの概要

    ComfyUIのフロントエンドは、ワークフローを「ノードとその接続(線)」という形で保持し、ユーザーの操作内容をサーバーに送信します。たとえば「ノードXの入力Aに値100を与えてください」という設定がある場合、その設定をJSONなどの形式でサーバーへ送り、サーバーは対応するノードオブジェクトのパラメータを更新します。実行のトリガーが引かれるとバックエンド側がノードを順番に処理し、生成された画像やログ情報をHTTPレスポンスやWebSocket経由で返却し、フロントエンド画面に表示される――というのが基本的な流れです。

    画像

    後ほど解説するカスタムノードでは、この「サーバー内に登録されたノードオブジェクトとして動くPythonクラス」に対して、任意の処理を書き込むことになります。フロントエンドからパラメータを受け取って処理し、結果を次のノードへ渡す。あるいはフロントエンドへ向けて独自のメッセージを送る、という仕組みです。ただし、実際にはComfyUIが抽象化しているため、我々が「APIを直接書く」イメージではなく、「ComfyUIの仕様に沿ってPythonクラスを定義する」イメージでカスタムノードを作ることができます。

    2. ComfyUIにおけるノードの基本

    2.1 ノードの実態(Pythonクラスとしての素地)

    ComfyUIでいう「ノード」の正体は、Pythonコードとしてのクラスです。主要な要素としては以下のような定数・メソッドが必須/推奨で用意されています。

    • CATEGORY: ノードがUI上のどのカテゴリに属するかを指定する文字列。

    • INPUT_TYPES: ノードが受け取る入力データの定義。引数名やデータ型(IMAGE、FLOAT、STRINGなど)、UI上の入力ウィジェットの型などが書かれます。

    • RETURN_TYPES: ノードが出力するデータの型をタプルで指定。

    • FUNCTION: 実行時に呼び出されるメソッド名を文字列で指定。

    • メインの実装関数: FUNCTIONで指定した名前のメソッドが、実際の処理を行う。

    • NODE_CLASS_MAPPINGS: モジュールの__init__.pyで定義する辞書。クラス名の文字列をキーに、対応するPythonクラスを値にしたもの。

    これらは後述する「カスタムノードの構成要素」で詳しく触れますが、大枠としては「1つのPythonクラス = 1つのノード」という位置づけです。カスタムノードを作りたければ、新たなPythonクラスを定義し、ちゃんとComfyUIに認識させるためにNODE_CLASS_MAPPINGSに登録する……という手順になります。

    2.2 ComfyUI内で定義されるデータ型

    ノード間でのデータ受け渡しには、ComfyUIでいくつかの「型」が定義されています。例えば次のようなものがよく使われます。

    • IMAGE: 画像バッチ(torch.Tensor形式)。サイズ[B, H, W, C]で、Bがバッチ数、Cが3ならRGBというイメージです。

    • MASK: マスク画像(基本的に白黒)を表すもの。形状[B, H, W]か、あるいは必要に応じて別の次元構造を持つ場合も。

    • LATENT: いわゆる潜在空間データ。Stable Diffusion本体が扱う特徴マップを保持する辞書型データで、内部にはtorch.Tensorが入っている。

    • INT, FLOAT, STRING, BOOLなどのプリミティブ型。UI上のスライダーやテキストボックス、チェックボックス等と結びつきやすい。

    カスタムノードを作るとき、INPUT_TYPESやRETURN_TYPESにはこうした既定の型名称を記載し、ComfyUI側に「このノードはIMAGEを受け取ってINTとSTRINGを返す」などと知らせる必要があります。ユーザーがUI上でノード同士を繋ぐとき、型が一致しないと接続できないように制限がかかる仕掛けがあるため、開発者はノードの入出力型を正しく指定しないとなりません。

    後編のチュートリアルでは、たとえばIMAGE型を入力して何らかの画像加工を行い、再びIMAGE型として出力するノードなどを例にしていく予定です。

    2.3 Node ExpansionやLazy Evaluationへのイントロ

    本記事(前編)では詳細には踏み込みませんが、ComfyUIには次のような高度な機能も存在します。

    • Lazy Evaluation
      入力の一部を「必要なときだけ」評価するという仕組み。「あるパラメータが特定の値なら、別のノード計算を省略できるため実行しないでおこう」といった最適化が可能です。あとでサンプルコードを示します。

    • Node Expansion
      実行時に「サブグラフ」を動的に生成すること。例えばループや条件分岐をノードの中で展開する、といった高度な使い方が考えられます。以下がサンプルコードになります。あとでサンプルコードを示します。

    これらはカスタムノードの自由度を大きく広げる機能です。ただし、理解して正しく使うにはComfyUIの実行フローを深く把握する必要があり、またフロントとバックエンドのやり取りにも気を使わなければなりません。先に基礎を押さえてから取り組むと、応用がしやすいでしょう。

    3. カスタムノードの構成要素

    3.1 Pythonクラスの必須属性とメソッド

    カスタムノードは前述の通り、Pythonクラスとして実装します。代表的な要素を以下に列挙します。

    1. CATEGORY

      • ノードがUIの「Add Node」メニューでどこに属するかを示す文字列。

      • 例: example, image/filtersなど。

      • サブフォルダ扱いにしたい場合は「image/filters」のようにスラッシュ区切りにする手もある。

    2. INPUT_TYPES

      • ノードが受け取る入力の定義。「@classmethod」で定義するのが慣例。

      • 「required」は必須入力、「optional」は未接続でも動作可能な入力。

      • 値のタプルに入れる(「IMAGE」、「{"default":0.5}」など)部分は「型」と「ウィジェットのオプション」。

      • 「hidden」というキーも使え、「PROMPT」, 「UNIQUE_ID」といった特殊な内部データを取得できる。

    3. RETURN_TYPES

      • ノードが出力するデータ型をタプルで書く。単一出力の場合でもタプルなので注意。

      • 例: `RETURN_TYPES = ("IMAGE", )`

    4. FUNCTION

      • ノード実行時に呼ばれる自前メソッドの名前。

      • 例えば `FUNCTION = "process_image"` なら、`def process_image(self, images, threshold, mask=None): ...` と定義する。

    5. メインの処理メソッド

      • FUNCTIONにて指定したメソッド。ここがノードの実装本体。

      • 引数はINPUT_TYPESで定義したものと一致させる。戻り値は`(出力,)`のようにタプルで返す。

    6. NODE_CLASS_MAPPINGS

      • ComfyUIがクラスを認識するため、モジュールの`init.py`に書く辞書。

    これらの要素を含んだバックエンドのサンプルコードは以下になります。

    # シンプルな画像処理ノードの例
    # - 画像を受け取り、強度パラメータに基づいて処理を行う
    # - 必要に応じてマスク画像を使用して処理を適用
    class SimpleImageProcessor:
        # ノードのカテゴリ指定
        # - UI上で "image/filters" カテゴリに表示される
        CATEGORY = "image/filters"
    
        # ComfyUI の入力タイプを定義
        @classmethod
        def INPUT_TYPES(cls):
            return {
                "required": {  # 必須の入力
                    # 画像入力
                    # - ComfyUI の IMAGE 型(torch.Tensor[B, H, W, C])
                    "image": ("IMAGE",),
    
                    # 強度パラメータ(画像の処理の強さを決める)
                    # - FLOAT 型(小数)
                    # - UI ではスライダーとして表示される
                    "strength": ("FLOAT", {
                        "default": 0.5,  # デフォルト値(初期値 0.5)
                        "min": 0.0,      # 最小値 0.0
                        "max": 1.0,      # 最大値 1.0
                        "step": 0.01     # スライダーの刻み幅
                    }),
                },
                "optional": {  # オプションの入力
                    # マスク画像(MASK 型)
                    # - 画像の一部に対して処理を適用するためのマスク
                    # - MASK 型のデータは通常、白黒画像(torch.Tensor[B, H, W])として表現される
                    "mask": ("MASK",),
                }
            }
    
        # 出力のデータ型を定義
        # - IMAGE 型の画像を返す
        RETURN_TYPES = ("IMAGE",)
    
        # 出力の名前(UIで表示される)
        # - 例えば、"processed_image" という名前で出力ノードが表示される
        RETURN_NAMES = ("processed_image",)
    
        # 実行時に呼び出されるメソッド名
        # - このクラスの "process_image" メソッドが実行される
        FUNCTION = "process_image"
    
        # メイン処理を行うメソッド
        # - 入力画像を "strength" に基づいて処理する
        # - 必要に応じて "mask" を使用して処理を適用する
        def process_image(self, image, strength, mask=None):
            # 画像の強度を調整(単純な輝度調整)
            # - 入力画像のピクセル値を "strength" 倍する
            processed = image * strength
            
            # マスクがある場合、画像をブレンドする
            if mask is not None:
                # - マスクが 0 の部分は元の画像
                # - マスクが 1 の部分は強度調整後の画像
                processed = image * (1 - mask) + processed * mask
            
            # 処理した画像をタプルで返す(ComfyUI の仕様に合わせる)
            return (processed,)
    

    3.2 custom_nodesフォルダと__init__.py

    ComfyUIのルート下にある`custom_nodes`フォルダが、ユーザー作成のカスタムノードをまとめる場です。以下のような構成イメージになります。

    ComfyUI/
      ├─ custom_nodes/
      │   ├─ my_custom_node/
      │   │   ├─ __init__.py
      │   │   ├─ my_node_file.py
      │   │   ├─ js/
      │   │   │   └─ some_extension.js
      │   │   └─ example_workflows/
      │   │       ├─ my_example.json
      │   │       └─ my_example.jpg
      │   └─ (他にも色々…)
      └─ (本体のフォルダ他…)
    

    __init__.pyを置くことでPythonモジュールとして認識され、NODE_CLASS_MAPPINGSを通じてComfyUIにノードを読み込ませられます。WEB_DIRECTORYを定義すればJavaScriptファイルをフロントエンドに読み込めるようにもなり、インストール作業は“フォルダを配置するだけ”でOKとなります。そのため、開発が終わって配布する際にも「自作フォルダをまるごと配ってください」と案内しやすく便利です。

    __init__.pyのサンプルコードは以下になります。

    # カスタムノードの Python ファイル(my_node_file.py など)からクラスをインポート
    from .my_node_file import MyCustomNode
    
    # ComfyUI にノードを登録するための辞書
    # - "My Custom Node" という名前で `MyCustomNode` クラスを ComfyUI に登録
    NODE_CLASS_MAPPINGS = {
        "My Custom Node": MyCustomNode
    }
    
    # モジュールのエクスポートを定義(外部から `NODE_CLASS_MAPPINGS` のみインポート可能にする)
    __all__ = ["NODE_CLASS_MAPPINGS"]
    

    4. フロントエンド連携の基礎

    4.1 サーバーからフロントエンドへのメッセージ送信

    Pythonクラス(カスタムノード)から直接、ブラウザ側へメッセージを送りたいことがあります。たとえば「このノードが実行されたらユーザーにポップアップを表示したい」などです。ComfyUIでは以下のように「PromptServer.instance.send_sync」を用いてメッセージ送信が可能です。

    # ComfyUI のサーバー機能を利用するためのモジュールをインポート
    from server import PromptServer
    
    def process_image(self, images, threshold):
    
        # 何らかの処理 ...
    
        # サーバーからフロントエンドにメッセージを送信
        PromptServer.instance.send_sync(
            "mynode.imageprocess.message",    # メッセージの種類(カスタムタグ)
            {"detail": "画像処理が完了しました"}  # メッセージの内容(辞書形式でデータ送信)
        )
        # 処理した画像を返す(ComfyUI の仕様に従いタプル形式)
        return (processed_image,)
    

    ここでは第1引数に「メッセージタイプ」を示す文字列("mynode.imageprocess.message"など)を入れ、第2引数に辞書形式で好きな情報を入れて送信できます。クライアント側はJavaScriptでこのメッセージタイプをリッスンし、ポップアップ表示など自由に挙動を定義することができます。

    4.2 JavaScriptでのメッセージ受信(フロントの拡張)

    フロントエンドのコードは、「WEB_DIRECTORY」で指定したフォルダに「.js」ファイルを置き、以下のように“拡張”を登録します。

    // ComfyUI のフロントエンド機能を拡張する JavaScript ファイル
    // - "WEB_DIRECTORY" で指定したフォルダに配置する
    // - ComfyUI の UI をカスタマイズするためのスクリプト
    
    // ComfyUI の "app.js" と "api.js" をインポート
    import { app } from "../../../scripts/app.js";  // ComfyUI のメインアプリ
    import { api } from "../../../scripts/api.js";  // API インターフェース(イベント処理用)
    
    // フロントエンド拡張を登録
    app.registerExtension({
      // 拡張の一意な名前(ComfyUI で識別するための名称)
      name: "mynode.imageprocess",
    
      // フロントエンド拡張の初期セットアップ関数
      async setup() {
        // メッセージを受信したときの処理
        function handleMessage(event) {
          // event.detail?.detail にサーバーからのメッセージ内容が含まれているか確認
          if (event.detail?.detail) {
            // メッセージの内容をアラート表示
            alert(`サーバーからの通知: ${event.detail.detail}`);
          }
        }
    
        // ComfyUI の API にリスナーを登録
        // - "mynode.imageprocess.message" というメッセージイベントを監視
        // - メッセージを受信すると handleMessage 関数が実行される
        api.addEventListener("mynode.imageprocess.message", handleMessage);
      }
    });

    上記の例では「mynode.imageprocess.message」というイベントに対してリスナを登録し、受信したデータをアラート表示しています。これにより、サーバー側でsend_syncしたタイミングでユーザーのブラウザにメッセージが現れるわけです。

    カスタムノードでこうした仕組みを活用すると、「ノード実行後にUIへ特定の通知を送りたい」「UIを動的に更新したい」というちょっとしたインタラクションが作れます。ComfyUIの標準ノードにはない、自分だけのUI拡張がやりやすくなるポイントと言えるでしょう。

    5. フロントエンドの拡張とUI要素

    5.1 WEB_DIRECTORYとJavaScriptファイル配置

     
     

    AICU

     
     
    生成AI時代に「つくる人をつくる」をビジョンに活動する日本最大のクリエイティブAIコミュニティ。 AIクリエイターさんとともに、「わかるAI」を伝えていきます。

    あなたへのおすすめ