2025年6月30日月曜日

Blogger API v3 で OAuth2.0 の認証トークンをリフレッシュする方法

Blogger API v3 で OAuth2.0 の認証トークンをリフレッシュする方法

概要

毎回ブラウザ認証する or トークンが期限切れになったらブラウザ認証は面倒なので refresh_token を使って認証トークンを再取得できます

環境

  • macOS 15.5
  • Python 3.12.11
  • google-api-python-client 2.174.0

Python スクリプト

  • vim app.py
import os

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build


class BloggerClient:
    # スコープ(Blogger のフルアクセス)
    SCOPES = ["https://www.googleapis.com/auth/blogger"]
    # 対象のブログID
    BLOG_ID = "1234567890"

    def __init__(
        self, client_secrets_file="client_secrets.json", token_file="token.json"
    ):
        self.client_secrets_file = client_secrets_file
        self.token_file = token_file
        self.creds = self._authenticate()
        self.service = self._build_service()

    def _authenticate(self):
        # 認証情報の取得
        if os.path.exists("token.json"):
            creds = Credentials.from_authorized_user_file("token.json", self.SCOPES)
            if not creds.valid:
                if creds.expired and creds.refresh_token:
                    # トークンが有効期限切れの場合は更新
                    creds.refresh(Request())
                else:
                    # トークンが無効な場合は再認証
                    os.remove("token.json")
                    return self._authenticate()
        else:
            # token.json がない場合はブラウザを使って初回認証する
            flow = InstalledAppFlow.from_client_secrets_file(
                "client_secrets.json", self.SCOPES
            )
            creds = flow.run_local_server(port=0, access_type="offline")
            with open("token.json", "w") as token:
                token.write(creds.to_json())
        return creds

    def _build_service(self):
        # Blogger API サービスのビルド
        return build("blogger", "v3", credentials=self.creds)

    def post_to_blog(self, title, content) -> str:
        # ブログに投稿する
        body = {
            "kind": "blogger#post",
            "title": title,
            "content": content,
        }
        # 投稿の実行
        result = self.service.posts().insert(blogId=self.BLOG_ID, body=body).execute()
        return result["url"]

ちょっと解説

ポイントは _authenticate メソッドです
token.json がある場合はそれを再利用しますが token.json にあるトークンがすでに期限切れの場合には refresh メソッドをコールします
こうすることで毎回ブラウザで認証する必要がなくなります

最後に

OAuth2.0 は大抵の場合 refresh_token があるので CLI などの環境ではこれを使ってトークンを更新するようにしましょう

参考サイト

2025年6月29日日曜日

Blogger API v3 を使って記事を更新する方法

Blogger API v3 を使って記事を更新する方法

概要

前回は記事を投稿する方法を紹介しました
今回は更新する方法を紹介します

環境

  • macOS 15.5
  • Python 3.12.11
  • google-api-python-client 2.174.0

Python スクリプト

  • vim app.py
import os

from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

# スコープ(Blogger のフルアクセス)
SCOPES = ["https://www.googleapis.com/auth/blogger"]


if os.path.exists("token.json"):
    creds = Credentials.from_authorized_user_file("token.json", SCOPES)
else:
    flow = InstalledAppFlow.from_client_secrets_file("client_secrets.json", SCOPES)
    creds = flow.run_local_server(port=8080, access_type="offline")
    with open("token.json", "w") as token:
        token.write(creds.to_json())

# 以降のコードで自動リフレッシュ対応済み
service = build("blogger", "v3", credentials=creds)

# 対象のブログID
blog_id = "1234567890"

# ブログの一覧を取得したい場合は以下のコードを使用
# 対象ブログの取得(複数ある場合はリストされる)
# blogs = service.blogs().listByUser(userId='self').execute()
# for blog in blogs['items']:
#     print(f"Blog ID: {blog['id']}, Title: {blog['name']}")
#
# blog_id = blogs['items'][0]['id']  # 1つ目のブログを使う

body = {
    "kind": "blogger#post",
    "title": "テスト投稿",
    "content": "<p>これは Python からの Blogger API を使ったテスト投稿です。</p>",
}

# 投稿の実行
result = service.posts().insert(blogId=blog_id, body=body).execute()

print(f"Post published: {result['url']}")

# 投稿IDを取得
post_id = result["id"]

body = {
    "kind": "blogger#post",
    "title": "テスト投稿(更新)",
    "content": "<p><b>これは Python からの Blogger API を使ったテスト投稿です。</b></p>",
}

# 投稿の更新
result = service.posts().update(blogId=blog_id, postId=post_id, body=body).execute()

print(f"Post updated: {result['url']}")

動作確認

  • pipenv run python app.py

記事が更新されていることを確認しましょう

最後に

投稿した記事に付与される permalink は Blogger API v3 では更新できないので GUI から手動でやるしかないようです

参考サイト

2025年6月28日土曜日

Blogger API v3 を使って Python スクリプトから記事を投稿する方法

Blogger API v3 を使って Python スクリプトから記事を投稿する方法

概要

前回 OAuth2.0 を実装しました
今回は認証後に記事を投稿する部分を実装します

環境

  • macOS 15.5
  • Python 3.12.11
  • google-api-python-client 2.174.0

Python スクリプト

blog_id の部分は適宜変更してください

  • vim app.py
import os

from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

# スコープ(Blogger のフルアクセス)
SCOPES = ["https://www.googleapis.com/auth/blogger"]


if os.path.exists("token.json"):
    creds = Credentials.from_authorized_user_file("token.json", SCOPES)
else:
    flow = InstalledAppFlow.from_client_secrets_file("client_secrets.json", SCOPES)
    creds = flow.run_local_server(port=8080, access_type="offline")
    with open("token.json", "w") as token:
        token.write(creds.to_json())

# 以降のコードで自動リフレッシュ対応済み
service = build("blogger", "v3", credentials=creds)

# 対象のブログID
blog_id = "1234567890"

# ブログの一覧を取得したい場合は以下のコードを使用
# 対象ブログの取得(複数ある場合はリストされる)
# blogs = service.blogs().listByUser(userId='self').execute()
# for blog in blogs['items']:
#     print(f"Blog ID: {blog['id']}, Title: {blog['name']}")
#
# blog_id = blogs['items'][0]['id']  # 1つ目のブログを使う

body = {
    "kind": "blogger#post",
    "title": "テスト投稿",
    "content": "<p>これは Python からの Blogger API を使ったテスト投稿です。</p>",
}

# 投稿の実行
result = service.posts().insert(blogId=blog_id, body=body).execute()

print(f"Post published: {result['url']}")

動作確認

  • pipenv run python app.py

で記事を投稿し問題なく記事があることを確認しましょう
記事の URL などを変更したい場合は post では行えず put する必要があります

最後に

Blogger API v3 を使って記事を投稿する方法を紹介しました
Blogger API v3 では投稿した記事の URL 部分 (permalink) を変更することはできないようです
permalink の変更は GUI からのみ行えます

参考サイト

2025年6月26日木曜日

Blogger API v3 を使うために OAuth2.0 認証を Python スクリプトで行う方法

Blogger API v3 を使うために OAuth2.0 認証を Python スクリプトで行う方法

概要

Blogger API v3 を使って記事を投稿するためには OAuth2.0 認証を使わなければなりません
OAuth の場合一度ブラウザで認証しトークンを取得する必要がありますがそうしないと記事の投稿はできないようです

今回は前準備として Python スクリプトで Google API を使うための OAuth2.0 認証を行う方法を紹介します

環境

  • macOS 15.5
  • Python 3.12.11
  • google-api-python-client 2.174.0

インストール

  • pipenv install google-api-python-client google_auth_oauthlib

client_secrets.json の取得

API とサービス -> 認証情報 -> 認証情報の作成 -> OAuth クライアント ID

  • アプリケーションの種類 -> ウェブアプリケーション
  • 承認済みのリダイレクト URI http://localhost:8080/

で作成し client_secret_1234567890-xxx.apps.googleusercontent.com.json というファイルをダウンロードします
以下のコードでは client_secrets.json に名前を変更しています

Python スクリプト

認証し成功したらトークンをファイルに保存します
トークンファイルが既にローカルにある場合はそれを使うためブラウザで認証するのは初回のみになります

  • vim app.py
import os

from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

# スコープ(Blogger のフルアクセス)
SCOPES = ['https://www.googleapis.com/auth/blogger']


if os.path.exists("token.json"):
    creds = Credentials.from_authorized_user_file("token.json", SCOPES)
else:
    flow = InstalledAppFlow.from_client_secrets_file("client_secrets.json", SCOPES)
    creds = flow.run_local_server(port=8080, access_type='offline')
    with open("token.json", "w") as token:
        token.write(creds.to_json())

# 以降のコードで自動リフレッシュ対応済み
service = build("blogger", "v3", credentials=creds)

動作確認

  • piepnv run python app.py

実行するとブラウザが起動し Google アカウントでログインすることになります
アプリが Blogger にアクセスすることを許可する画面になるので許可します

GCP の画面でブランディングなど設定するとアプリ名などを設定できます

認証後に Python スクリプト側に token.json があれば認証成功です
再度実行してみるとブラウザは起動せずにクライアントオブジェクトが設定できることを確認できるはずです

最後に

Blogger API v3 で記事を投稿するための準備段階として OAuth2.0 認証を Python スクリプトで行う方法を紹介しました
OAuth2.0 はセキュアなのですがブラウザ認証しなければならないので面倒です

参考サイト

2025年6月25日水曜日

Youtube のチャンネル登録を一括で削除する方法

Youtube のチャンネル登録を一括で削除する方法

概要

Google のマイアクティビティから削除します
API などはないようです

環境

  • Youtube (2025/06/25 時点)
  • Google マイアクティビティ (2025/06/25 時点)

方法

ここ https://myactivity.google.com/page?page=youtube_subscriptions にアクセスし削除します
おそらく現状はこれが一番簡単です

2025年6月23日月曜日

pipenv run で TAB でコマンドやファイル名の補完が行われないときの対処方法

pipenv run で TAB でコマンドやファイル名の補完が行われないときの対処方法

概要

zsh や補完ファイルの設定は全く同じなのに補完してくれない環境があったので対応しました

環境

  • macOS 15.5
  • Python 3.12.11
  • pyenv 2.6.3

対処方法

基本的には pipenv は pyenv でインストールされた python を使って pipenv も実行されることを想定しています
pyenv でインストールした python でインストールされた pipenv を使っている場合はちゃんと補完してくれます
しかし pyenv でインストールした python でインストールされていない pipenv を使っている場合には補完してくれません

具体的には以下のとおりです

  • 補完してくれる pipenv のパス
/Users/user01/.pyenv/shims/pipenv
  • 補完してくれない pipenv のパス
/opt/homebrew/bin/pipenv

つまり Homebrew でインストールした pipenv を優先して使ってしまっているために補完してくれていませんでした
単純に Homebrew でインストールした pipenv アンインストールすれば OK です

  • brew uninstall pipenv

Homebrew 側の pipenv をアンインストールしたくない場合は PATH の優先順位を変更しても OK です
その場合は .zsh などを確認してください

最後に

システムで使用するツールが Python を使用する場合にはシステムグローバルな Python をインストールする必要がありますが基本的には仮想環境ごとに Python のバージョンを指定して使えるようにするのがいいと思います

2025年6月20日金曜日

Bitbucket で self hosted な Runner を起動する方法

Bitbucket で self hosted な Runner を起動する方法

概要

M1 Mac を Runner にする方法を紹介します
docker で Runner を起動するのが一番簡単なので docker を使います

環境

  • macOS 15.5
  • docker 28.2.2
  • bitbucket runner

Runner の追加

workspace/settings/pipelines/account-runners でワークスペース Runner を追加します
右上の歯車 -> Workspace settings から設定します

Add Runner をクリックします

Runner の設置画面になります

  • System and architecture -> Linux Docker (x86_64)
  • Runner name -> test
  • Runner labels -> self.hosted, linux

で Next をクリックします

docker コマンドが表示されるのでこれをそのまま実行しましょう
認証情報などが含まれているので扱いには注意しましょう

self-hosted Runner を実行するには bitbucket-pipelines.yml に runs-on という定義を追加しなければなりません
ここに先程のラベル情報と同じ情報を記載すれば self-hosted Runner でパイプラインが実行されます

あとは docker コマンドを実行しパイプラインを実行して問題なく Mac 上でパイプラインが実行されることを確認できれば OK です

最後に

無料プランだと50分/月が上限なのでどうしてもパイプラインを流したい場合は self-hosted Runner を構築すると簡単に流せます
ラベルなしの self-hosted Runner は作成することができないっぽいのでどうしても bitbuckt-pipelines.yml を書き換える必要はあるのが面倒であります

参考サイト

2025年6月19日木曜日

certbot を docker compose で使う

certbot を docker compose で使う

概要

これでホスト側に certbot やら nginx をインストールしないで済みます
今回は renew でかつ webroot モードを使います

P.S あとから気づいたのですが nginx-proxy 1.7.0 であればデフォルトで .well-known/acme-challenge/ へのアクセス経路は /etc/nginx/conf.d/default.conf に定義されているので nginx/default は不要です (あっても OK)
なので proxy コンテナへの nginx/default のマウントは不要です

環境

certbot の compose.yml

certbot:
  image: certbot/certbot
  volumes:
    - /etc/letsencrypt:/etc/letsencrypt
    - /var/lib/letsencrypt:/var/lib/letsencrypt
  command: renew --non-interactive --agree-tos --webroot -w /var/lib/letsencrypt
  restart: "no"
  • 発行された証明書はホスト側の /etc/letsencrypt に保存します
  • /var/lib/letsencrypt は webroot モードで発行される acme-challenge ファイルを配置するパスです、ホスト側にも acme-challenge ファイルを渡すことで各種アプリからも acme-challenge ファイル を参照できるようにします

nginx-proxy の compose.yml

proxy:
  image: jwilder/nginx-proxy
  restart: always
  ports:
    - "80:80"
    - "443:443"
  environment:
    - SSL_POLICY=AWS-TLS-1-2-2017-01
  volumes:
    - "/var/run/docker.sock:/tmp/docker.sock:ro"
    - "/var/lib/letsencrypt:/usr/share/nginx/html:ro"
    - "/home/your_domain/nginx:/etc/nginx/vhost.d"
    - "/etc/letsencrypt/live/your_domain.com/privkey.pem:/etc/nginx/certs/your_domain.com.key"
    - "/etc/letsencrypt/live/your_domain.com/fullchain.pem:/etc/nginx/certs/your_domain.com.crt"
  • /var/lib/letsencrypt を /usr/share/nginx/html にマウントすることで nginx から acme-challenge ファイルを見えるようにします
  • /home/your_domain/nginx/default ファイルを /etc/nginx/vhost.d/default にマウントします、こうすることですべての VirtualHost の設定に対してルールが適用されます

/home/your_domain/nginx/default ファイル

location ^~ /.well-known/acme-challenge/ {
    root /usr/share/nginx/html;  # docker run 時にボリュームマウントする
    try_files $uri =404;
}
  • 各アプリの /.well-known/acme-challenge/ へのアクセスを /usr/share/nginx/html にします
  • certbot の webroot モードでは /.well-known/acme-challenge/ にアクセスが来て acme-challenge ファイルが見えれば認証 OK となります
  • 基本は 404 にしておきます

cron 設定

5 0 5,16 * * cd /home/your_domain; docker compose start certbot

定期的に certbot コンテナを実行するようにしておけば OK です

動作確認

  • docker compose up -d

or

  • docker compose start certbot

で実際にコンテナを動かせば OK です
あとは acme-challenge テキストが見れるかどうかを curl などでテストしておいてもいいかもです

  • docker compose run --rm --entrypoint="" certbot mkdir -p /var/lib/letsencrypt/.well-known/acme-challenge
  • docker compose run --rm --entrypoint="" certbot touch /var/lib/letsencrypt/.well-known/acme-challenge/test.txt
  • docker compose exec proxy cat /usr/share/nginx/html/.well-known/acme-challenge/test.txt
  • curl http://your-domain.com/.well-known/acme-challenge/test

最後に

certbot をコンテナで実行し証明書を更新する方法を紹介しました
これでホスト側で certbot をインストールしたりする必要がなくなります
初回時のみ以下のように certonly する必要があるので注意しましょう

docker compose run --rm certbot \
  certonly --webroot -w /var/lib/letsencrypt \
  -d your_domain.com \
  -d www1.your_domain.com \
  --email your@email.com \
  --agree-tos --non-interactive

またモード (nginx など) によってやり方がだいぶ変わるのでその辺りも注意してください
今回紹介したのは webroot モードといって既存のアプリを使って ACME チャレンジする方式となっています

参考サイト

2025年6月18日水曜日

AudioCraft で MAGNeT を使って音楽生成してみた

AudioCraft で MAGNeT を使って音楽生成してみた

概要

過去に MusicGen を使いました
今回は MAGNeT を使ってみます

環境

  • macOS 15.5
  • AudioCraft
  • Python 3.9.23

インストール

過去の記事と同じです
Python のバージョンは 3.9 系の最新を使いました

  • pyenv install 3.9.23
  • git clone https://github.com/facebookresearch/audiocraft
  • cd audiocraft
  • pyenv local 3.9.17
  • pip install "torch==2.1.0"
  • pip install -r requirements.txt

MAGNeT を使って音楽生成

まずは MAGNeT 用の WebUI を起動します

  • python -m demos.magnet_app --share

起動したら http://127.0.0.1:7860 にアクセスします

以下のような画面が表示されたら Submit しましょう
デフォルトでプロンプトも入力されておりモデルも初回は自動でダウンロードしてくれます

あとは右ペインに生成された音楽をクリックすれば聞くことができます
デフォルトでは10秒分の音楽が生成されます

TypeError: argument of type ‘bool’ is not iterable

というエラーになる場合は以下を実行してください

  • pip install pydantic==2.10.6

が MPS サポートされていない

https://github.com/facebookresearch/audiocraft/issues/396

いずれサポートされれば Mac + AudioCraft + MAGNeT で音楽生成できるはずです# 最後に MAGNeT はまだ Apple Sillicon はサポートされていないようです
MusicGen は使えるのでしばらくは MusicGen を使うことになりそうです

モデルのパス

内部的に hf なので以下にあります

.cache/huggingface/hub/models--facebook--magnet-small-10secs 

参考サイト

2025年6月17日火曜日

M2 mac で ComfyUI を試してみる

M2 mac で ComfyUI を試してみる

概要

macOS クライアントアプリがあるのでそれを試してみました
画像を生成するところまで紹介します

環境

  • macOS 15.5
  • Comfyui 0.4.51,2505290u29zbcgf
  • Python 3.12.9

インストール

  • brew install comfyui

起動

アプリケーションの一覧に ComfyUI があるのでクリックしてインストールします
警告が出るので「開く」で OK です

起動すると設定画面になります
ちゃんと GPU バックエンドに Metal が選択できるようになっていました

各種設定をし起動すると uv で仮想環境を作成し各種ライブラリのインストールが始まります
その後 GUI が起動すれば OK です

Python は 3.12.9 が使われていました

画像を生成してみる

テンプレートがあるのでそれを使ってとりあえず画像を生成してみます
テンプレートの一覧から「画像」を選択します

初回はモデルのダウンロードが必要なのでダウンロードします
ComfyUI に最適化された Stable Diffusion のモデルを使います

モデルのダウンロードが完了すると以下のようなノードが線で繋がれたフローが表示されます
ComfyUI はこんな感じでノードごとを線でつなぎ画像生成までのフローを作成します
プロンプトやサンプラーなど Stable Diffusion WebUI でも定義する値が ComfyUI のノードとして定義されていることが確認できると思います

プロンプトの内容を変更してもいいですがとりあえずこの状態で「実行する」をクリックすると画像が生成できます
生成された画像は最後のノードとしてフローに追加されます

ComfyUI の場合今何を処理しているかがノードの経過として目視することができます
デフォルトのテンプレートはシード値が設定されているので同じような画像が毎回生成されます
ランダムに生成したい場合は「0」を設定しましょう

最後に

とりあえず M2 mac 上で ComfyUI を動作させてみました
基本的には Stable Diffusion WebUI で設定する値と同じ値を ComfyUI でも設定しますが ComfyUI では各項目をノードとして定義できるため管理がしやすくなります

またノードの種類もたくさんあり画像生成後に自動で保存したりフローの途中で外部の API をコールしたり自分でノード自体を定義することもできます

LoRA や ControlNet など画像の生成に関するノードもあらかじめノードライブラリに用意されておりかつカテゴライズもされているので探しやすいのも嬉しい点かなと思います

参考サイト

2025年6月12日木曜日

brew upgrade --cask --greedy でパスワードの入力を求められないようにする方法

brew upgrade --cask --greedy でパスワードの入力を求められないようにする方法

概要

sudoer として登録します

環境

  • macOS 15.5

方法

  • sudo visudo

以下を記載します
名前と権限の間はタブで区切ります
user01 は自身のユーザ名に置き換えてください
ALL などの権限部分はちゃんとスペースなどを以下の通りに入力してください

user01    ALL = (ALL) NOPASSWD:SETENV: /usr/sbin/installer, /bin/rm, /bin/ln

2025年6月11日水曜日

stable-diffusion-webui-forge で API を使ってテキストから画像を生成する方

stable-diffusion-webui-forge で API を使ってテキストから画像を生成する方

概要

stable-diffusion-webui-forge の REST API を使って画像を生成する方法を紹介します

環境

  • macOS 15.5
  • stable-diffusion-web-ui-forge f2.0.1v1.10.1-previous-664-gd557aef9

API の起動

  • ./webui.sh --force-upcast-attention --api

--api オプションを付与して起動します

API リクエストに必要なペイロードを生成できるようにする

https://github.com/huchenlei/sd-webui-api-payload-display をインストールします

これで画像を生成後にその画像を API で生成した際の JSON ペイロードが確認できるようになります

生成されるペイロードには拡張機能用のペイロードを含まれるので不要なものは削除して OK です

以下のサンプルではすべてのペイロードを表示しています

ライブラリインストール

  • pipenv install Pillow requests

テキストから画像生成

レスポンスの兼ね合いで Python などのプログラムから実行することをおすすめします
ペイロードは JSON 文字列なのでプロンプトなどに改行コードが含まれる場合は削除してください
また true/false/null は True/False/None に置き換えて Python の辞書として使えるようにしておきましょう

import base64
import io

import requests
from PIL import Image, PngImagePlugin

model_name = "AnythingXL_xl.safetensors"
payload = {
    "alwayson_scripts": {
        "ADetailer": {
            "args": [
                False,
                False,
                {
                    "ad_cfg_scale": 7,
                    "ad_checkpoint": "Use same checkpoint",
                    "ad_clip_skip": 1,
                    "ad_confidence": 0.3,
                    "ad_controlnet_guidance_end": 1,
                    "ad_controlnet_guidance_start": 0,
                    "ad_controlnet_model": "None",
                    "ad_controlnet_module": "None",
                    "ad_controlnet_weight": 1,
                    "ad_denoising_strength": 0.4,
                    "ad_dilate_erode": 4,
                    "ad_inpaint_height": 512,
                    "ad_inpaint_only_masked": True,
                    "ad_inpaint_only_masked_padding": 32,
                    "ad_inpaint_width": 512,
                    "ad_mask_blur": 4,
                    "ad_mask_filter_method": "Area",
                    "ad_mask_k": 0,
                    "ad_mask_max_ratio": 1,
                    "ad_mask_merge_invert": "None",
                    "ad_mask_min_ratio": 0,
                    "ad_model": "face_yolov8n.pt",
                    "ad_model_classes": "",
                    "ad_negative_prompt": "",
                    "ad_noise_multiplier": 1,
                    "ad_prompt": "",
                    "ad_restore_face": False,
                    "ad_sampler": "DPM++ 2M",
                    "ad_scheduler": "Use same scheduler",
                    "ad_steps": 28,
                    "ad_tab_enable": True,
                    "ad_use_cfg_scale": False,
                    "ad_use_checkpoint": False,
                    "ad_use_clip_skip": False,
                    "ad_use_inpaint_width_height": False,
                    "ad_use_noise_multiplier": False,
                    "ad_use_sampler": False,
                    "ad_use_steps": False,
                    "ad_use_vae": False,
                    "ad_vae": "Use same VAE",
                    "ad_x_offset": 0,
                    "ad_y_offset": 0,
                    "is_api": [],
                },
                {
                    "ad_cfg_scale": 7,
                    "ad_checkpoint": "Use same checkpoint",
                    "ad_clip_skip": 1,
                    "ad_confidence": 0.3,
                    "ad_controlnet_guidance_end": 1,
                    "ad_controlnet_guidance_start": 0,
                    "ad_controlnet_model": "None",
                    "ad_controlnet_module": "None",
                    "ad_controlnet_weight": 1,
                    "ad_denoising_strength": 0.4,
                    "ad_dilate_erode": 4,
                    "ad_inpaint_height": 512,
                    "ad_inpaint_only_masked": True,
                    "ad_inpaint_only_masked_padding": 32,
                    "ad_inpaint_width": 512,
                    "ad_mask_blur": 4,
                    "ad_mask_filter_method": "Area",
                    "ad_mask_k": 0,
                    "ad_mask_max_ratio": 1,
                    "ad_mask_merge_invert": "None",
                    "ad_mask_min_ratio": 0,
                    "ad_model": "None",
                    "ad_model_classes": "",
                    "ad_negative_prompt": "",
                    "ad_noise_multiplier": 1,
                    "ad_prompt": "",
                    "ad_restore_face": False,
                    "ad_sampler": "DPM++ 2M",
                    "ad_scheduler": "Use same scheduler",
                    "ad_steps": 28,
                    "ad_tab_enable": True,
                    "ad_use_cfg_scale": False,
                    "ad_use_checkpoint": False,
                    "ad_use_clip_skip": False,
                    "ad_use_inpaint_width_height": False,
                    "ad_use_noise_multiplier": False,
                    "ad_use_sampler": False,
                    "ad_use_steps": False,
                    "ad_use_vae": False,
                    "ad_vae": "Use same VAE",
                    "ad_x_offset": 0,
                    "ad_y_offset": 0,
                    "is_api": [],
                },
                {
                    "ad_cfg_scale": 7,
                    "ad_checkpoint": "Use same checkpoint",
                    "ad_clip_skip": 1,
                    "ad_confidence": 0.3,
                    "ad_controlnet_guidance_end": 1,
                    "ad_controlnet_guidance_start": 0,
                    "ad_controlnet_model": "None",
                    "ad_controlnet_module": "None",
                    "ad_controlnet_weight": 1,
                    "ad_denoising_strength": 0.4,
                    "ad_dilate_erode": 4,
                    "ad_inpaint_height": 512,
                    "ad_inpaint_only_masked": True,
                    "ad_inpaint_only_masked_padding": 32,
                    "ad_inpaint_width": 512,
                    "ad_mask_blur": 4,
                    "ad_mask_filter_method": "Area",
                    "ad_mask_k": 0,
                    "ad_mask_max_ratio": 1,
                    "ad_mask_merge_invert": "None",
                    "ad_mask_min_ratio": 0,
                    "ad_model": "None",
                    "ad_model_classes": "",
                    "ad_negative_prompt": "",
                    "ad_noise_multiplier": 1,
                    "ad_prompt": "",
                    "ad_restore_face": False,
                    "ad_sampler": "DPM++ 2M",
                    "ad_scheduler": "Use same scheduler",
                    "ad_steps": 28,
                    "ad_tab_enable": True,
                    "ad_use_cfg_scale": False,
                    "ad_use_checkpoint": False,
                    "ad_use_clip_skip": False,
                    "ad_use_inpaint_width_height": False,
                    "ad_use_noise_multiplier": False,
                    "ad_use_sampler": False,
                    "ad_use_steps": False,
                    "ad_use_vae": False,
                    "ad_vae": "Use same VAE",
                    "ad_x_offset": 0,
                    "ad_y_offset": 0,
                    "is_api": [],
                },
                {
                    "ad_cfg_scale": 7,
                    "ad_checkpoint": "Use same checkpoint",
                    "ad_clip_skip": 1,
                    "ad_confidence": 0.3,
                    "ad_controlnet_guidance_end": 1,
                    "ad_controlnet_guidance_start": 0,
                    "ad_controlnet_model": "None",
                    "ad_controlnet_module": "None",
                    "ad_controlnet_weight": 1,
                    "ad_denoising_strength": 0.4,
                    "ad_dilate_erode": 4,
                    "ad_inpaint_height": 512,
                    "ad_inpaint_only_masked": True,
                    "ad_inpaint_only_masked_padding": 32,
                    "ad_inpaint_width": 512,
                    "ad_mask_blur": 4,
                    "ad_mask_filter_method": "Area",
                    "ad_mask_k": 0,
                    "ad_mask_max_ratio": 1,
                    "ad_mask_merge_invert": "None",
                    "ad_mask_min_ratio": 0,
                    "ad_model": "None",
                    "ad_model_classes": "",
                    "ad_negative_prompt": "",
                    "ad_noise_multiplier": 1,
                    "ad_prompt": "",
                    "ad_restore_face": False,
                    "ad_sampler": "DPM++ 2M",
                    "ad_scheduler": "Use same scheduler",
                    "ad_steps": 28,
                    "ad_tab_enable": True,
                    "ad_use_cfg_scale": False,
                    "ad_use_checkpoint": False,
                    "ad_use_clip_skip": False,
                    "ad_use_inpaint_width_height": False,
                    "ad_use_noise_multiplier": False,
                    "ad_use_sampler": False,
                    "ad_use_steps": False,
                    "ad_use_vae": False,
                    "ad_vae": "Use same VAE",
                    "ad_x_offset": 0,
                    "ad_y_offset": 0,
                    "is_api": [],
                },
            ]
        },
        "API payload": {"args": []},
        "ControlNet": {
            "args": [
                {
                    "batch_image_dir": "",
                    "batch_input_gallery": None,
                    "batch_mask_dir": "",
                    "batch_mask_gallery": None,
                    "control_mode": "Balanced",
                    "enabled": False,
                    "generated_image": None,
                    "guidance_end": 1.0,
                    "guidance_start": 0.0,
                    "hr_option": "Both",
                    "image": None,
                    "image_fg": None,
                    "input_mode": "simple",
                    "mask_image": None,
                    "mask_image_fg": None,
                    "model": "None",
                    "module": "None",
                    "pixel_perfect": False,
                    "processor_res": -1,
                    "resize_mode": "Crop and Resize",
                    "save_detected_map": True,
                    "threshold_a": -1,
                    "threshold_b": -1,
                    "use_preview_as_input": False,
                    "weight": 1,
                },
                {
                    "batch_image_dir": "",
                    "batch_input_gallery": None,
                    "batch_mask_dir": "",
                    "batch_mask_gallery": None,
                    "control_mode": "Balanced",
                    "enabled": False,
                    "generated_image": None,
                    "guidance_end": 1.0,
                    "guidance_start": 0.0,
                    "hr_option": "Both",
                    "image": None,
                    "image_fg": None,
                    "input_mode": "simple",
                    "mask_image": None,
                    "mask_image_fg": None,
                    "model": "None",
                    "module": "None",
                    "pixel_perfect": False,
                    "processor_res": -1,
                    "resize_mode": "Crop and Resize",
                    "save_detected_map": True,
                    "threshold_a": -1,
                    "threshold_b": -1,
                    "use_preview_as_input": False,
                    "weight": 1,
                },
                {
                    "batch_image_dir": "",
                    "batch_input_gallery": None,
                    "batch_mask_dir": "",
                    "batch_mask_gallery": None,
                    "control_mode": "Balanced",
                    "enabled": False,
                    "generated_image": None,
                    "guidance_end": 1.0,
                    "guidance_start": 0.0,
                    "hr_option": "Both",
                    "image": None,
                    "image_fg": None,
                    "input_mode": "simple",
                    "mask_image": None,
                    "mask_image_fg": None,
                    "model": "None",
                    "module": "None",
                    "pixel_perfect": False,
                    "processor_res": -1,
                    "resize_mode": "Crop and Resize",
                    "save_detected_map": True,
                    "threshold_a": -1,
                    "threshold_b": -1,
                    "use_preview_as_input": False,
                    "weight": 1,
                },
            ]
        },
        "Dynamic Prompts v2.17.1": {
            "args": [
                True,
                False,
                1,
                False,
                False,
                False,
                1.1,
                1.5,
                100,
                0.7,
                False,
                False,
                True,
                False,
                False,
                0,
                "Gustavosta/MagicPrompt-Stable-Diffusion",
                "",
            ]
        },
        "DynamicThresholding (CFG-Fix) Integrated": {
            "args": [
                False,
                7,
                1,
                "Constant",
                0,
                "Constant",
                0,
                1,
                "enable",
                "MEAN",
                "AD",
                1,
            ]
        },
        "Extra options": {"args": []},
        "FreeU Integrated (SD 1.x, SD 2.x, SDXL)": {
            "args": [False, 1.01, 1.02, 0.99, 0.95, 0, 1]
        },
        "Kohya HRFix Integrated": {
            "args": [False, 3, 2, 0, 0.35, True, "bicubic", "bicubic"]
        },
        "LatentModifier Integrated": {
            "args": [
                False,
                0,
                "anisotropic",
                0,
                "reinhard",
                100,
                0,
                "subtract",
                0,
                0,
                "gaussian",
                "add",
                0,
                100,
                127,
                0,
                "hard_clamp",
                5,
                0,
                "None",
                "None",
            ]
        },
        "MultiDiffusion Integrated": {
            "args": [False, "MultiDiffusion", 768, 768, 64, 4]
        },
        "Never OOM Integrated": {"args": [False, False]},
        "PerturbedAttentionGuidance Integrated": {"args": [False, 3, 0, 0, 1]},
        "Refiner": {"args": [False, "", 0.8]},
        "Sampler": {"args": [20, "DPM++ 2M SDE", "Karras"]},
        "Seed": {"args": [-1, False, -1, 0, 0, 0]},
        "SelfAttentionGuidance Integrated (SD 1.x, SD 2.x, SDXL)": {
            "args": [False, 0.5, 2, 1]
        },
        "StyleAlign Integrated": {"args": [False, 1]},
    },
    "batch_size": 1,
    "cfg_scale": 5,
    "comments": {},
    "denoising_strength": 0.7,
    "disable_extra_networks": False,
    "distilled_cfg_scale": 3.5,
    "do_not_save_grid": False,
    "do_not_save_samples": False,
    "enable_hr": False,
    "height": 1152,
    "hr_additional_modules": ["Use same choices"],
    "hr_cfg": 5,
    "hr_distilled_cfg": 3.5,
    "hr_negative_prompt": "",
    "hr_prompt": "",
    "hr_resize_x": 0,
    "hr_resize_y": 0,
    "hr_scale": 2,
    "hr_second_pass_steps": 0,
    "hr_upscaler": "Latent",
    "n_iter": 1,
    "negative_prompt": "low quality, worst quality, lowres, blurry, jpeg artifacts, text, watermark, error, extra limbs, mutated hands, poorly drawn hands, bad anatomy, unrealistic eyes, cloned face, multiple heads, bad proportions, out of frame, heavy makeup",
    "override_settings": {
        "sd_model_checkpoint": model_name,
    },
    "override_settings_restore_afterwards": True,
    "prompt": "masterpiece, best quality, ultra-detailed, 8k, cinematic lighting,\nbeautiful japanese young women, 20age, fair skin, light makeup, realistic face,\nlooking at viewer,\nbrown short hair,\nsmile,\npark background,\nsleeveless blouse, flare skirt,",
    "restore_faces": False,
    "s_churn": 0,
    "s_min_uncond": 0,
    "s_noise": 1,
    "s_tmax": None,
    "s_tmin": 0,
    "sampler_name": "DPM++ 2M SDE",
    "scheduler": "Karras",
    "script_args": [],
    "script_name": None,
    "seed": 1752460840,
    "seed_enable_extras": True,
    "seed_resize_from_h": -1,
    "seed_resize_from_w": -1,
    "steps": 20,
    "styles": [],
    "subseed": 33927003,
    "subseed_strength": 0,
    "tiling": False,
    "width": 896,
}

url = "http://127.0.0.1:7860"
response = requests.post(url=f"{url}/sdapi/v1/txt2img", json=payload)

r = response.json()

for index, image_info in enumerate(r["images"]):
    image = Image.open(io.BytesIO(base64.b64decode(image_info.split(",", 1)[0])))

    png_payload = {"image": "data:image/png;base64," + image_info}
    response_png_info = requests.post(url=f"{url}/sdapi/v1/png-info", json=png_payload)

    pnginfo = PngImagePlugin.PngInfo()
    pnginfo.add_text("parameters", response_png_info.json().get("info"))
    image.save(f"output_{index}.png", pnginfo=pnginfo)

override_settings は指定しない場合 sd-webui 上で指定しているモデルが使われます
基本は指定することをおすすめします

レスポンスはバイト情報なのでそこから画像を生成します
API では stable-diffusion-webui 側に画像は自動で保存されません

ただわざわざ保存しないでも stable-diffusion-webui 側にはちゃんと保存されているのでそれを参照するのであればわざわざレスポンスを保存しないでも OK です

API リファレンス

http://localhost:7860/docs にアクセスすると確認できます

最後に

stable-diffusion-webui-forge の REST API を使って Python スクリプトから画像を生成してみました
プロンプト情報などもコードで管理できるようになるのでかなり便利です
cron など自動実行などを仕掛けておけば勝手ガチャしてくれるようになります

Python スクリプトや流れは複雑になりますが XYZ-plot や Hires Fix などもできるので仕上げまで全自動にすることも可能かなと思います

参考サイト