ComfyUIのAPIを利用してPythonで画像生成する

## 環境

* ComfyUI
* Windows
* Python


## はじめに

ComfyUIのAPIを利用してPythonで画像生成する方法を紹介します。なお、本記事のコードは、わかりやすさとシンプルさを重視しています。ベストプラクティスを示したものではない点をご了承ください。

## 事前準備について

### aiohttp のインストール

本記事でのコードは非同期処理に [aiohttp](https://docs.aiohttp.org/en/stable/) を使用しています (非同期処理については本題と逸れるため説明は割愛します)。サンプルコードをそのまま実行する場合は、aiohttpのインストールが必要です。

```console
pip install aiohttp
```


### ワークフローの保存

ComfyUIで作成したワークフローをAPI用のjsonファイルとして事前に保存する必要があります。メニューの ```Workflow > Export (API)``` から任意の場所に保存してください。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3965005/b3d95639-e683-4fc3-97c5-f151fdc4b9ec.jpeg">

記事ではテンプレートの「Image Generation」からExportしたjsonファイルを例として説明します。

## サンプルコード

```python
import asyncio
import json
import traceback
from pathlib import Path

import aiohttp


async def send_workflow(session, server_url, workflow):
    async with session.post(f"{server_url}/prompt", json={"prompt": workflow}) as response:
        return (await response.json())["prompt_id"]


async def get_urls(session, server_url, prompt_id):
    async with session.get(f"{server_url}/history") as response:
        history = await response.json()
        if prompt_id not in history:
            return []

        urls = []
        for node in history[prompt_id]["outputs"].values():
            if "images" not in node:
                continue

            for img in node["images"]:
                urls.append({"url": f"{server_url}/view?filename={img['filename']}", "filename": img["filename"]})
        return urls


async def save_images(session, image_urls, output_dir):
    output_dir.mkdir(parents=True, exist_ok=True)

    for img in image_urls:
        output_path = output_dir / img["filename"]
        async with session.get(img["url"]) as response:
            content = await response.read()
            with output_path.open("wb") as f:
                f.write(content)


async def main(server_url, workflow, output_dir):
    try:
        async with aiohttp.ClientSession() as session:
            print("1. ワークフローを読み込み、APIにリクエストを送信します...")
            prompt_id = await send_workflow(session, server_url, workflow)
            print(f"   送信完了\n")

            print("2. 生成された画像のURLを取得します...")
            while not (image_urls := await get_urls(session, server_url, prompt_id)):
                await asyncio.sleep(1)
            print("   取得完了\n")

            print("3. 画像をダウンロードします...")
            await save_images(session, image_urls, output_dir)
            print("   保存完了\n")
    except Exception as e:
        print(f"エラーが発生しました: {str(e)}")
        print(traceback.format_exc())


if __name__ == "__main__":
    with Path("<ワークフローのJSONのパス>").open("r", encoding="utf8") as f:
        workflow = json.load(f)

    asyncio.run(
        main(
            server_url="http://127.0.0.1:8188",
            workflow=workflow,
            output_dir=Path("<出力ディレクトリのパス>"),
        )
    )
```


### サンプルコードの概要

ワークフローのJSONファイルを読み込み、以下の3つの工程を順に実行して画像を生成するプログラムです。

#### 1. ワークフローの送信

```send_workflow``` 関数でワークフローをComfyUIに送信します。

#### 2. 生成された画像の情報取得

``` get_urls``` 関数でComfyUIで生成された画像のURLとファイル名を取得します。

#### 3. 画像のダウンロード

```save_images``` 関数で取得した画像を指定したディレクトリに保存します。

### パラメータ調整について

ワークフローのJSONはプログラム内で辞書型として扱われるため、ComfyUIに送信する前に値を調整できます。例えば下記のような形式でワークフローが保存されている場合

```json
{
  "3": {
    "inputs": {
      "seed": 0,
      ...
    },
    ...
  },
  ...
}
```

下記のようなコードで、実行時ごとにシード値を変更して画像を生成することが可能です。

```python
if __name__ == "__main__":
    with Path("<ワークフローのJSONのパス>").open("r", encoding="utf8") as f:
        workflow = json.load(f)

    # seed値のrandomize
    import random

    seed = random.randint(1, 2**32 - 1)
    workflow["3"]["inputs"]["seed"] = seed

    asyncio.run(
        main(
            server_url="http://127.0.0.1:8188",
            workflow=workflow,
            output_dir=Path("<出力ディレクトリのパス>"),
        )
    )
```

プロンプトなど、他のパラメータも同様に画像生成前に変更が可能です。


## 参考

* [ComfyUI](https://github.com/comfyanonymous/ComfyUI)
* [aiohttp](https://docs.aiohttp.org/en/stable/)