【ComfyUI】バッチ処理で画像生成を自動化する方法と「RuntimeError」の解決策

問題の概要:バッチ処理の自動化におけるエラーと課題

ComfyUIは、ノードベースの直感的なインターフェースでStable Diffusionのワークフローを構築できる強力なツールです。しかし、単一の画像を生成するだけでなく、複数のプロンプトやシード値を使って大量の画像を自動生成したい(バッチ処理)場合、初心者から中級者のユーザーは以下のような課題に直面します。

  • Web UI上で手動でパラメータを変更するのは非効率で、ミスが発生しやすい。
  • APIやコマンドラインからの実行を試みると、RuntimeError: Expected all tensors to be on the same deviceKeyError: 'prompt' といったエラーが頻発する。
  • 生成された画像のファイル名と、使用したプロンプトやパラメータの対応関係が管理できない。
  • 処理中にメモリ不足(OOM)エラーが発生し、バッチ処理全体が中断してしまう。

本記事では、これらの一般的な問題を解決し、ComfyUIで安定してバッチ処理を自動化する方法を、具体的なコード例と共に解説します。

原因の解説:なぜエラーが発生するのか?

ComfyUIのバッチ処理自動化でエラーが発生する主な原因は以下の3点です。

1. APIリクエストの形式誤り

ComfyUIのAPIは、ワークフロー全体のノード構成(ワークフロージェソン)と、その中で変更したいノードのIDを正確に指定する必要があります。単純にPythonのリストを渡すだけでは KeyError が発生します。APIはpromptというキーに、ノードIDをキーとした詳細なディクショナリを要求します。

2. デバイス(CPU/GPU)の不一致

RuntimeError: Expected all tensors to be on the same device というエラーは、PyTorchのテンソルが異なるデバイス(例えば一部はGPU、他はCPU)に配置されている場合に発生します。カスタムスクリプトやノードを介してデータをロード・処理する過程で、デバイスを明示的に指定しないことが原因です。

3. メモリ管理の不備

バッチ処理は多くの画像を連続して生成するため、生成済みの画像のテンソルや中間データがメモリに累積され、最終的にGPUメモリ不足(OOM)を引き起こします。各画像生成後に明示的にキャッシュをクリアするなどの対策が必要です。

解決方法:ステップバイステップでの自動化実装

ステップ1:ワークフローの準備とノードIDの確認

まず、ComfyUIのWebインターフェースで目的のワークフロー(例:テキストプロンプトから画像を生成するシンプルな流れ)を構築し、保存します。その後、「ワークフロージェソンを読み込む」ボタンの隣にある「API用にプリミティブを出力」ボタンをクリックします。これにより、現在のワークフローのJSONと、各ノードに割り振られた一意のID(例:"6": {"class_type": "CLIPTextEncode", "inputs": {"text": "masterpiece" ...}})を確認できます。特に「KSampler」や「CLIP Text Encode (Prompt)」ノードのIDをメモしておきます。

ステップ2:バッチ処理用Pythonスクリプトの作成

以下のスクリプトは、複数のプロンプトを順番に処理する基本的なバッチ自動化スクリプトです。エラー処理とメモリ管理を組み込んでいます。

import json
import requests
import torch
import time
import sys
import os

class ComfyUIBatchProcessor:
    def __init__(self, server_address="127.0.0.1:8188"):
        self.server_address = server_address
        self.client_id = str(hash(time.time()))  # 簡易的なクライアントID

    def clear_memory(self):
        """推論後のキャッシュをクリアしてメモリ解放を促す"""
        if torch.cuda.is_available():
            torch.cuda.empty_cache()
            torch.cuda.synchronize()
        print("メモリキャッシュをクリアしました。")

    def load_workflow(self, json_file_path):
        """ワークフロージェソンファイルを読み込む"""
        with open(json_file_path, 'r', encoding='utf-8') as f:
            self.workflow = json.load(f)
        print(f"ワークフロー '{json_file_path}' を読み込みました。")
        return self.workflow

    def generate_image(self, prompt, negative_prompt="", seed=0, steps=20):
        """単一の画像生成リクエストを送信"""
        # ワークフローのコピーを作成(元データを変更しないため)
        prompt_data = json.loads(json.dumps(self.workflow))

        # ノードIDはワークフローファイルに応じて変更必須!
        # 例: "3"がポジティブプロンプト、"4"がネガティブプロンプト、"5"がKSamplerの場合
        text_encoder_node_id = "3"
        negative_node_id = "4"
        sampler_node_id = "5"

        # プロンプトを設定
        prompt_data[text_encoder_node_id]["inputs"]["text"] = prompt
        prompt_data[negative_node_id]["inputs"]["text"] = negative_prompt
        prompt_data[sampler_node_id]["inputs"]["seed"] = seed
        prompt_data[sampler_node_id]["inputs"]["steps"] = steps

        # APIリクエスト用のデータを構築
        api_payload = {
            "prompt": prompt_data,
            "client_id": self.client_id
        }

        try:
            # 画像生成リクエストを送信
            response = requests.post(
                f"http://{self.server_address}/prompt",
                json=api_payload
            )
            response.raise_for_status()  # HTTPエラーを例外として発生させる
            result = response.json()
            prompt_id = result['prompt_id']
            print(f"生成ジョブを送信しました。Prompt ID: {prompt_id}, プロンプト: '{prompt[:30]}...'")
            return prompt_id
        except requests.exceptions.RequestException as e:
            print(f"APIリクエストエラー: {e}")
            return None
        except KeyError as e:
            print(f"APIレスポンスの形式エラー。ノードIDが正しいか確認してください: {e}")
            return None

    def run_batch(self, prompts, output_dir="./batch_output"):
        """プロンプトのリストをバッチ処理"""
        os.makedirs(output_dir, exist_ok=True)
        prompt_log = []

        for i, prompt in enumerate(prompts):
            print(f"n--- 処理中 ({i+1}/{len(prompts)}) ---")
            seed = int(time.time() * 1000) % (2**32)  # 簡易的なシード生成

            prompt_id = self.generate_image(
                prompt=prompt,
                negative_prompt="low quality, blurry, bad anatomy",
                seed=seed,
                steps=20
            )

            if prompt_id:
                # ここでは簡略化のため、生成完了を待機(実際はWebSocketで監視が望ましい)
                time.sleep(7)  # 生成時間に応じて調整
                self.clear_memory()
                prompt_log.append({"index": i, "prompt": prompt, "seed": seed, "prompt_id": prompt_id})

        # プロンプトとシードのログを保存
        log_path = os.path.join(output_dir, "prompt_log.json")
        with open(log_path, 'w', encoding='utf-8') as f:
            json.dump(prompt_log, f, indent=2, ensure_ascii=False)
        print(f"nバッチ処理完了。ログを '{log_path}' に保存しました。")

if __name__ == "__main__":
    # 使用例
    processor = ComfyUIBatchProcessor("127.0.0.1:8188")
    
    # ワークフローファイルのパスを指定(ComfyUIで保存したjsonファイル)
    processor.load_workflow("my_workflow_api.json")
    
    # バッチ処理するプロンプトのリスト
    batch_prompts = [
        "A beautiful sunset over a mountain landscape, digital art",
        "A cyberpunk city street at night, raining, neon lights",
        "A cute kitten sleeping in a basket, photorealistic",
        "An ancient castle in a mystical forest, fantasy style"
    ]
    
    processor.run_batch(batch_prompts, output_dir="./my_batch_results")

ステップ3:高度な制御とエラー回避

上記の基本スクリプトに加え、以下の実装を追加することでより堅牢なシステムを構築できます。

  • WebSocketによる進捗監視: time.sleep ではなく、ComfyUIのWebSocketサーバー(ws://127.0.0.1:8188/ws)に接続し、status メッセージを監視して画像生成の完了を正確に検知します。
  • デバイス明示化: カスタムノードやスクリプトを使用する場合、テンソルを生成・操作する際に .to('cuda') または .to('cpu') を明示的に指定し、デバイスの不一致エラーを予防します。
  • バッチサイズの最適化: 高解像度や複雑なモデルを使用する場合、スクリプト内で torch.cuda.memory_allocated() を監視し、メモリ使用量が閾値を超えたら一時停止やキャッシュクリアを行うロジックを追加します。

コード例・コマンド例

エラーハンドリングを強化した生成関数の例

def safe_generate_image(self, prompt, max_retries=3):
    """リトライ機能付きの画像生成"""
    for attempt in range(max_retries):
        try:
            prompt_id = self.generate_image(prompt)
            if prompt_id:
                return prompt_id
        except RuntimeError as e:
            if "Expected all tensors to be on the same device" in str(e):
                print(f"デバイス不一致エラー発生 (試行 {attempt+1}/{max_retries})。キャッシュをクリアします。")
                self.clear_memory()
                time.sleep(2)
            else:
                raise e  # その他のRuntimeErrorは再スロー
        except Exception as e:
            print(f"予期せぬエラー: {e}")
            break
    print(f"プロンプト '{prompt[:30]}...' の生成に失敗しました。")
    return None

コマンドラインからの直接実行(curl使用例)

単一の生成を簡単にテストしたい場合、curlコマンドも有用です。まず、prompt_api.jsonというファイルに、前述の方法で取得した正しいAPI形式のJSONを保存します。

# プロンプトを置き換えた一時ファイルを作成
sed "s/"masterpiece"/"A serene lake landscape"/g" prompt_api.json > temp_prompt.json

# APIリクエストを送信
curl -H "Content-Type: application/json" -X POST 
     -d @temp_prompt.json 
     http://127.0.0.1:8188/prompt

# ヒストリーを確認(オプション)
curl http://127.0.0.1:8188/history

まとめ・補足情報

ComfyUIでバッチ処理を自動化する核心は、正しい形式でAPIにワークフローデータを送信することと、生成プロセス中のリソースを適切に管理することにあります。本記事で紹介したスクリプトの骨格を自身のワークフローに合わせて調整し、WebSocket連携や詳細なロギング機能を追加することで、大量の画像生成タスクも安定して実行できるようになります。

重要な補足点:

  • ComfyUI Managerなどのカスタムノードを多用するワークフローでは、ノードのクラス名や入力形式が異なる場合があるため、API用プリミティブ出力を必ず確認してください。
  • 生成された画像は、デフォルトではComfyUIの出力フォルダ(ComfyUI/output)に保存されます。ファイル名にはPrompt IDが含まれるため、スクリプトで保存したログと照合することで、どの画像がどのパラメータで生成されたかを後から追跡可能です。
  • より大規模な自動化には、公式のWebSocket APIサンプルを参照し、非同期処理を実装することをお勧めします。

これらの手法を駆使すれば、ComfyUIを単なる対話型ツールから、本格的な画像生成パイプラインのコアエンジンへと進化させることができます。

この記事は役に立ちましたか?