top of page

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

更新日:4月10日


これまで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ファイル配置

先ほど諸所で触れたように、__init__.pyにおいて次のように書くと、サーバー起動時に該当ディレクトリ内の「.js」ファイルがすべて読み込まれます。


WEB_DIRECTORY = "./js"
__all__ = ["NODE_CLASS_MAPPINGS", "WEB_DIRECTORY"]

「./js」という相対パスは、「my_custom_node/」ディレクトリから見ての指定になります。手軽にUIへ新機能を差し込めるので、カスタムノード作成の際にはよく使われます。なお、「.js」以外のファイル(「.css」や画像など)は自動では読み込まれないため、必要なら拡張の中で手動で読み込むよう工夫します。


5.2 beforeRegisterNodeDef等のフックを活用したUI操作

ComfyUIのフロントエンドは、LiteGraph(グラフエディタライブラリ)などの仕組みの上に構築されています。「app.registerExtension」ではさまざまなフックが提供されており、そのなかでも「beforeRegisterNodeDef」や「nodeCreated」といったタイミングで処理を差し込めます。例えば以下のように「ノードの定義が登録される前」にUIをカスタマイズすることが可能です。


async beforeRegisterNodeDef(nodeType, nodeData, app) {
  if (nodeType.ComfyClass === "MyNodeClass") {
    // ここでnodeType.prototypeをオーバーライドして
    // onConnectionsChangeなど挙動をフックできる
    const original = nodeType.prototype.onConnectionsChange;
    nodeType.prototype.onConnectionsChange = function(...args) {
      // 元の処理を呼び出す
      const result = original?.apply(this, args);
      // カスタムの処理を追加
      console.log("接続が変わりました");
      return result;
    };
  }
}

これにより、特定ノードに対してフロントエンド上での動きを詳しく制御できます。さまざまなUI要素を追加したり、ノードの表示ラベルを変更したりすることも技術的には可能です。単なるメッセージ表示にとどまらない、よりインタラクティブな拡張を行いたい方には必須のテクニックでしょう。


5.3 ハイブリッド開発:バックエンド処理とUI改変の組み合わせ

カスタムノード開発において、Pythonばかり触れば良いケース(例えばデータ処理ロジック中心)と、JavaScriptを活用しなければ実現できないUI拡張(独自のウィジェット追加など)が混在します。後編では実際にサンプルコードを交え、このハイブリッドな開発手順を詳しく説明しますが、まずは「ComfyUIのフロントエンド上でノード操作を行う部分はJavaScriptで、それをサーバーにどう伝えるかをComfyUIの仕組みが吸収してくれている」という理解を持っておくとよいでしょう。


6. 高度機能の概要

6.1 Lazy Evaluationの発想

Lazy Evaluationを用いると、ノードが受け取る入力の一部について「実際に使うときだけ評価する」という動きを実装できます。たとえばノードの引数が10個あるとして、そのうち1個の値によっては他9個の計算が不要になる場合、わざわざすべてのノードを計算せずにスキップさせたい――といった状況を想像してください。


Lazy Evaluationは、カスタムノードに属性「INPUT_IS_LIST」や「OUTPUT_IS_LIST」、および「check_lazy_status」メソッドを使って実現されます。内部実装としては、必要な入力のみを評価するためのフラグを返し、ComfyUI本体が「このノードの場合はAの入力しか使わないからBの入力はスキップしよう」と最適化を行います。

Stable Diffusionのように計算負荷が高い処理に対しては、Lazy Evaluationが大きな差を生む可能性があり、うまく組み込めばワークフロー全体の効率が上がることもあります。


# LazyMixImages クラス: 2つの画像をマスクを使用して補間するノード
# - image1 と image2 は遅延評価される可能性がある
# - mask の値に応じて、必要のない画像の評価をスキップする
class LazyMixImages:
    # クラスメソッド: ノードの入力タイプを定義
    # - `lazy: True` を指定すると、その入力は遅延評価される
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "image1": ("IMAGE", {"lazy": True}),  # 遅延評価される画像1
                "image2": ("IMAGE", {"lazy": True}),  # 遅延評価される画像2
                "mask": ("MASK",),  # 常に評価されるマスク
            },
        }
    # ノードの出力タイプを定義
    # - 出力は1つの画像
    RETURN_TYPES = ("IMAGE",)
    # 実行時に呼び出される関数の名前を定義
    FUNCTION = "mix"
    # このノードのカテゴリ(UIでの分類用)
    CATEGORY = "Examples"
    # check_lazy_status: 遅延評価が必要かどうかを判断するメソッド
    # - lazy: True に設定された入力が None の場合に呼び出される
    # - 返り値は、評価が必要な入力の名前のリスト
    def check_lazy_status(self, mask, image1, image2):
        # マスクの最小値と最大値を取得(全体が0.0や1.0の場合を判断するため)
        mask_min = mask.min()
        mask_max = mask.max()
        
        # 必要な入力を記録するリスト
        needed = []
        
        # image1 が未評価で、マスクが完全に1.0でない場合は評価が必要
        if image1 is None and (mask_min != 1.0 or mask_max != 1.0):
            needed.append("image1")
        
        # image2 が未評価で、マスクが完全に0.0でない場合は評価が必要
        if image2 is None and (mask_min != 0.0 or mask_max != 0.0):
            needed.append("image2")
        
        return needed  # 必要な入力のリストを返す
    # mix: 2つの画像をマスクに基づいてブレンドする関数
    # - mask の値に応じて、image1 と image2 を線形補間する
    def mix(self, mask, image1, image2):
        # マスクの最小値と最大値を取得
        mask_min = mask.min()
        mask_max = mask.max()
        
        # もしマスク全体が 0.0 なら image1 をそのまま返す(image2 は不要)
        if mask_min == 0.0 and mask_max == 0.0:
            return (image1,)
        
        # もしマスク全体が 1.0 なら image2 をそのまま返す(image1 は不要)
        elif mask_min == 1.0 and mask_max == 1.0:
            return (image2,)
        # マスクの値に基づいて image1 と image2 を線形補間
        # 画像の各ピクセルを (1 - mask) * image1 + mask * image2 でブレンド
        result = image1 * (1. - mask) + image2 * mask
        
        return (result,)  # 計算結果の画像をタプルで返す

6.2 Node Expansionによるサブグラフ動的生成

もう1つの特徴機能がNode Expansionです。通常は1ノード1クラスで固定的な入出力を持ちますが、ときには「ノードの内部でサブノードを作り、複数回繰り返し処理したい」といった局面が出てきます。Node Expansionを使うと、ノードが実行される段階で“新たにグラフノードを生成”し、フロー全体に組み込むことができるため、ループや条件分岐など高度なフロー制御をGUI上で扱えるようになります。


Node Expansionのコアは、「ノードが返すのは単なる処理結果だけでなく、“追加で生成するノードたち”の定義も含む」という構造です。これがComfyUI本体により解釈され、動的にワークフローが拡張されるイメージです。ただし複雑化しやすいため、まずは標準的なノード開発に慣れてから挑戦するのがおすすめでしょう。


# load_and_merge_checkpoints:
# - 2つのチェックポイント(モデル)をロードし、それらをマージする処理を定義
# - "Node Expansion" を活用し、複数のサブノードを生成して処理を行う
# - ComfyUI の "GraphBuilder" を使用してサブグラフを構築
def load_and_merge_checkpoints(self, checkpoint_path1, checkpoint_path2, ratio):
    # 必要なモジュールをインポート(通常はファイルの先頭で行う)
    from comfy_execution.graph_utils import GraphBuilder
    # サブグラフを構築するためのオブジェクトを作成
    graph = GraphBuilder()
    # "CheckpointLoaderSimple" ノードを作成し、指定されたパスのチェックポイントをロード
    # - これは ComfyUI のノードで、モデル(MODEL)、クリップ(CLIP)、VAE をロードする
    checkpoint_node1 = graph.node("CheckpointLoaderSimple", checkpoint_path=checkpoint_path1)
    checkpoint_node2 = graph.node("CheckpointLoaderSimple", checkpoint_path=checkpoint_path2)
    # "ModelMergeSimple" ノードを作成し、2つのモデル(checkpoint_node1 と checkpoint_node2)をブレンド
    # - model1: checkpoint_node1 の出力(MODEL, index 0)
    # - model2: checkpoint_node2 の出力(MODEL, index 0)
    # - ratio: どれくらいの割合でブレンドするか(0.0〜1.0)
    merge_model_node = graph.node(
        "ModelMergeSimple",
        model1=checkpoint_node1.out(0),
        model2=checkpoint_node2.out(0),
        ratio=ratio
    )
    # "ClipMergeSimple" ノードを作成し、2つの CLIP モデルをブレンド
    # - clip1: checkpoint_node1 の出力(CLIP, index 1)
    # - clip2: checkpoint_node2 の出力(CLIP, index 1)
    merge_clip_node = graph.node(
        "ClipMergeSimple",
        clip1=checkpoint_node1.out(1),
        clip2=checkpoint_node2.out(1),
        ratio=ratio
    )
    # 結果を辞書形式で返す
    return {
        "result": (
            merge_model_node.out(0),  # マージされたモデル(MODEL)
            merge_clip_node.out(0),   # マージされた CLIP
            checkpoint_node1.out(2)   # VAE は最初のチェックポイントから取得
        ),
        "expand": graph.finalize()  # サブグラフを確定して展開可能な形にする
    }

6.3 使い所と注意点

Lazy EvaluationやNode Expansionは非常に強力ですが、次のような留意事項もあります。


  • 他ノードからのデータ取得タイミングが通常と変わるので、デバッグが難しくなる。


  • ノードのキャッシュや実行順序最適化と絡んで予期せぬ挙動をする場合がある。


  • フロントエンド側UIが“展開後のノード”をどのように表示するかの工夫が必要。


とはいえ、高度なフローや負荷を抑えた実装に挑戦したい場合には強い味方となる技術です。後編で触れる本格チュートリアルにおいても、1つの応用例として紹介する予定です。


7. まとめ

ここまでの前編では以下のようなポイントを重視してきました。


  1. ComfyUIの基本構造

    フロントエンド(JavaScript/HTML)とバックエンド(Python)の分割、サーバーがStable DiffusionなどのAI処理を担当し、UIはユーザー操作と可視化部分を担当するというモデルを理解しました。


  2. ノードの正体

    ComfyUIにおいて1つのノードはPythonクラスとして定義され、入出力や処理内容を指定することでUIに自動表示される仕組みがあることを知りました。


  3. フロントエンドとの通信

    PromptServer.instance.send_syncによってサーバーからメッセージを送り、JavaScriptで受け取ってアラートを出すなどの簡単なユースケースを紹介しました。カスタムノードにおける連携の基本方針も見えてきたはずです。


  4. 高度機能のイントロ

    Lazy EvaluationやNode Expansionといった上級APIが存在し、複雑なワークフローの省力化や表現力を高められる可能性を持っていることに触れました。


前編では「カスタムノード開発」に取り組むうえで押さえるべき大枠の仕組みや概念的な話が中心でした。次回の後編(実装チュートリアル)では、実際にフォルダを作って__init__.pyを置き、簡単なPythonクラスを記述し、ComfyUIを立ち上げてノードがUIに追加される様子を確かめる――というハンズオン手順をじっくり解説します。さらにオプションを増やして複数の処理モードを切り替えるノードを作ったり、JavaScript側でイベントをリッスンしてUIに反映させる例に取り組む予定です。


また、今回紹介した「フロントエンドの拡張」は実際にコードを書き始めると“ちょっと難しい”と感じるかもしれません。しかし、サンプルを少しずつ改造しながら慣れていくことで、驚くほど自由にUIをカスタマイズできるようになっていきます。UIの一部に特別なボタンを追加したり、ノードの見た目を差し替えたり、あるいはユーザーが入力したテキストをリアルタイムでPython側へ送ったりといった、よりインタラクティブなNodeも不可能ではありません。


後編を読み進める前に、もし余裕があれば、ComfyUIに既に入っている標準ノードのソースコード(ComfyUIのリポジトリや`nodes.py`など)を眺めてみると理解が深まるはずです。シンプルな例としては「Invert Image Node」などがありますので、「こういう書き方をしているんだな」「ここでIMAGEを受け取り、1 - imageで画素を反転しているんだな」といった流れを確認しておくとよいでしょう。


それでは、ここまで読んでくださった方は、ComfyUIの構造やカスタムノードとはどういう物理的仕組みで動いているのか、フロントとバックエンドはどのようにやり取りをしているのか、という基礎的な部分のイメージをつかめたと思います。次回の後編では、いよいよ実際に手を動かしながら具体的なプログラムを書いていきましょう。前編で学んだ土台を活かしながら、最初のカスタムノードを完成させるステップへと進んでいきます。



Originally published at note.com/aicu on Feb 23, 2025.

Comments


bottom of page