画像のaltをAIで自動生成した|ブログ12枚を一括処理した実測(装飾は空欄が正解)

2026.07.01 約 26 分で読めます

画像の alt 属性、空欄のままサボっていませんか。SEO にもアクセシビリティにも効くのに、1 枚ずつ書くのが面倒で後回し——正直なところ、わたしもそうでした。そこで自分のブログの画像 12 枚を Claude の画像認識に通し、alt を自動生成して WP-CLI で一括で書き戻しました。手作業なら 20 分ぶんが、数秒です。「装飾画像は空欄が正解」という落とし穴まで、実測で書きます。

結論:埋めるなら AI で十分。ただし外せない条件が2つ

結論から言うと、空欄の alt を埋める用途なら AI 一択で問題ありませんでした。自分のブログにある画像 12 枚を分類して、空欄だった 7 枚に alt を生成し、WP-CLI で書き戻すところまで自動化しています。書き戻し自体は 7 枚で約 5 秒。手作業なら 20 分前後かかる仕事です。

ただし、ここを外すと逆効果になる条件が2つあります。

  • 全部の画像に alt を付けない。意味を持たない装飾画像は、空の alt のままが正解です
  • 画像の中の文字は、字面をそのまま写さない。「その絵が何を伝えたいか」を書きます

この2つは、あとで実例を見せます。とくに2つ目は、文字が崩れた画像をネタにしているこのブログだと、かなり効いてきました。

題材:自分のブログの画像 12 枚(手書き 5・空欄 7)

合成データではなく、実際に運用しているブログの uploads をそのまま題材にしました。いわゆるドッグフーディングです。中身を WP-CLI で数えると、こうなっていました。

  • 画像 attachment:12 枚(記事のアイキャッチと、本文に貼った検証用スクショ)
  • すでに自分で alt を書いていたもの:5 枚
  • alt が空欄のまま放置されていたもの:7 枚

放置していた 7 枚は、新しい記事を上げるたびに「あとで書こう」と思って忘れていた分です。心当たりのある人は多いと思います。この 7 枚を埋めるのが今回のゴールです。

仕組み:一覧を取る → Claude が見て説明する → WP-CLI で書き戻す

やっていることは3ステップです。難しい部品はありません。

  1. 添付一覧を取得wp post list --post_type=attachment --post_mime_type=image で画像だけ列挙し、各 ID の現在の alt(_wp_attachment_image_alt)を確認します
  2. Claude が画像を見て判定:1 枚ずつ画像認識にかけ、「装飾か内容か」を分類し、内容画像には日本語の alt を生成します
  3. WP-CLI で書き戻すwp post meta update <ID> _wp_attachment_image_alt "..." で添付メタに直接書き込みます

ポイントは2ステップ目の「装飾か内容か」を AI 自身に判定させるところです。alt の世界では、意味のない飾り画像にまで説明を付けると、読み上げソフトの利用者にとってノイズになります。だから「装飾なら空、内容なら説明」を返させます。

やってみた:7 枚を一括生成して書き戻し(手作業 20 分が数秒に)

実際に 12 枚を流した結果です。まず分類は、12 枚すべてが「内容画像」でした。装飾はゼロ。アイキャッチも検証用スクショも、何かを伝えている画像だったので当然と言えば当然です。このあたりは落とし穴の節でもう一度触れます。

空欄だった 7 枚に生成された alt を書き戻しました。たとえば、文字が完全に崩れたこの画像。

「業務効率化」と指示したのに文字が5つの疑問符に完全崩壊した、多数のクリップアートに囲まれたgpt-image-2の失敗例
生成された alt:「業務効率化」と指示したのに文字が5つの疑問符に完全崩壊した、多数のクリップアートに囲まれた gpt-image-2 の失敗例

崩れた「?????」をそのまま書いても、読み上げでは無意味です。代わりに「指示と違って文字化けした失敗例」と、絵の意図を言語化してくれました。これが地味に賢いところでした。

記事のアイキャッチも、ちゃんと中身を読んでいます。

請求書をスキャナーに通すと明細が表計算ソフトとCSVに自動で書き出され、合計が検算で照合される様子を描いた手描きイラスト
生成された alt:請求書をスキャナーに通すと明細が表計算ソフトと CSV に自動で書き出され、合計が検算で照合される様子を描いた手描きイラスト

所要時間を計測すると、こうでした。

項目実測 / 見積り
WP-CLI の書き戻し1 枚あたり約 0.69 秒(実測)→ 7 枚で約 5 秒
alt の生成全 12 枚を 1 パスで処理(API 版なら並列化も可能)
手作業した場合(見積り)1 枚 1.5〜2 分 × 12 枚 = 約 18〜24 分
体感の山は「画像を見て文章を考えて書く」部分。そこを AI が肩代わりする。

正直、書き戻しの速さは本質ではありません。wp post meta update はもともと一瞬です。効くのは「1 枚ずつ画像を開いて、適切な説明を考えて、打ち込む」という、地味に消耗する 20 分ぶんの判断を任せられる点でした。

コストも出しておきます。再現スクリプト(後述)を Claude Haiku 4.5 で回した場合の概算で、画像 12 枚ぶんのトークンから計算すると 約 4 円(入力 ≈ 2.2 万トークン・出力 ≈ 700 トークン/1 USD = 160 円換算)でした。ワンコイン以下です。文字化けの機微まで丁寧に読ませたいなら Sonnet 4.6 に上げて約 3 倍、Batch API を使えば半額になります。

品質:AI の alt と、自分で書いた alt を比べた

気になるのは品質ですよね。ちょうど良いことに、このブログには自分で手書きした alt が 5 枚あります。これを上書きせず、同じ画像に AI も alt を書かせて、どちらが良いか見比べました。

画像自分の手書き altAI が書いた alt
5つの構成要素の図ノートに描かれた5つの仕掛けと湯気の立つコーヒーSkills・Hooks・MCP・Subagents・Plan を矢印でつないだ「5つの構成要素」図をノートに描き、横にコーヒーを添えた手描きイラスト
ネタ出しの採点図15枚のアイデアカードにチェック・星・バツの採点マークがついた手描きイラスト15枚のメモにチェック・バツ・星の採点マークが付き万年筆が添えられた、ネタを仕分ける手描きイラスト
スキルのパイプライン図Writer Gate Publisher Image のスキルパイプラインを描いた手描きイラストWriter→Gate→Publisher→Image と矢印でつないだスキルパイプライン図をノートに描いた手描きイラスト

5 枚中 3 枚で AI のほうが具体的、残り 2 枚は互角でした。とくに 1 枚目は、わたしが「5つの仕掛け」とぼかして書いた箱の中身を、AI は図のラベルまで読み取って書いています。アクセシビリティの観点では、情報量が多い AI 版のほうが概ね親切でした。これは少し驚きました。

ただし、具体的すぎることはリスクにもなります。図の中の文字を読み取って書くと、読み違えてもそのまま断定で書いてしまうからです。文字の多い図だけは、生成後に 1 枚だけ目視で確認するのが現実的でした。

コピペで動く実装(Anthropic 画像入力 API + WP-CLI)

自分のサイトに適用できるスクリプト全文です。画像 attachment を列挙し、Claude の画像認識で {装飾か否か, alt} を構造化出力で受け取り、WP-CLI で書き戻します。--dry-run を付ければ生成だけして書き戻しません。まずはこれで中身を確認するのが安全です。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
gen_alt_with_claude.py
WordPress にアップ済みの画像 attachment を列挙し、Claude の画像認識(vision)で
日本語の alt テキストを生成して `wp post meta update` で書き戻す再現スクリプト。

なぜ既定が Haiku か:
  - 既定は claude-haiku-4-5(入力 $1 / 出力 $5 per 1M)。alt 生成は
    「1枚の絵を見て短い説明を1つ書く」軽い vision タスクで、Haiku でも十分。
    全12枚でも $0.03 程度と安く、枚数が増えてもスケールしやすい。
  - 文字化け画像で「何を伝える絵か」を読む難所では --model claude-sonnet-4-6 が安定。
  - structured outputs は tools(JSON) で {decorative: bool, alt: str} を強制する。

前提:
  pip install anthropic
  set ANTHROPIC_API_KEY=sk-ant-...
  WP-CLI が PATH にある(--wp で実体を指定可)

使い方:
  python gen_alt_with_claude.py --dry-run      # 生成だけ。書き戻さない
  python gen_alt_with_claude.py                # alt 空のものだけ生成して書き戻す
  python gen_alt_with_claude.py --force        # 既存 alt も上書き(要注意)
  python gen_alt_with_claude.py --ids 34,35    # 対象 ID を限定
  python gen_alt_with_claude.py --model claude-sonnet-4-6
"""
import argparse, base64, json, mimetypes, os, subprocess, sys

try:
    from anthropic import Anthropic
except ImportError:
    sys.exit("anthropic SDK が未インストールです: pip install anthropic")

DEFAULT_UPLOADS_DIR = os.environ.get(
    "WP_UPLOADS_DIR",
    r"C:\path\to\wp\public\wp-content\uploads",  # 自分の環境に合わせる
)

ALT_TOOL = {
    "name": "set_alt",
    "description": "画像の alt テキストと装飾判定を返す",
    "input_schema": {
        "type": "object",
        "properties": {
            "decorative": {"type": "boolean",
                "description": "純粋に装飾的で意味を持たない画像なら true(alt は空文字)"},
            "alt": {"type": "string",
                "description": "記事文脈で画像が伝える意味を自然な日本語で。120字以内・"
                "理想40〜80字。『〜の画像/写真』等の冗長な接頭辞は付けない。"
                "画像内の文字は字面の転写でなく『何を伝える絵か』を書く。"
                "decorative=true のときは空文字。"},
        },
        "required": ["decorative", "alt"],
    },
}

SYSTEM = (
    "あなたはWordPressブログのアクセシビリティ担当です。アップロードされた画像に対し、"
    "スクリーンリーダー利用者に役立つ日本語の代替テキスト(alt)を1つ生成します。"
    "ルール: (1)簡潔に120字以内・理想40〜80字。(2)『〜の画像』『〜の写真』等の冗長な"
    "接頭辞は不要(読み上げが既に『画像』と言う)。(3)キーワード詰め込み禁止、自然な日本語。"
    "(4)画像内に文字がある場合、崩れた字面を転写せず『その絵が伝える要点・意図』を書く。"
    "(5)純粋に装飾的で意味を持たない画像は decorative=true・alt=空文字。必ず set_alt で返す。"
)


def wp(args, wp_cmd):
    full = list(wp_cmd) + args
    res = subprocess.run(full, capture_output=True, text=True, encoding="utf-8")
    if res.returncode != 0:
        raise RuntimeError(f"WP-CLI 失敗: {full}\n{res.stderr}")
    return res.stdout.strip()


def list_image_attachments(wp_cmd):
    out = wp(["post", "list", "--post_type=attachment", "--post_mime_type=image",
              "--fields=ID", "--format=json"], wp_cmd)
    items = []
    for row in json.loads(out):
        i = row["ID"]
        relfile = wp(["post", "meta", "get", str(i), "_wp_attached_file"], wp_cmd)
        alt = wp(["post", "meta", "get", str(i), "_wp_attachment_image_alt"], wp_cmd)
        items.append((i, relfile, alt))
    return items


def gen_alt(client, model, path):
    mime = mimetypes.guess_type(path)[0] or "image/png"
    with open(path, "rb") as f:
        data = base64.standard_b64encode(f.read()).decode("ascii")
    msg = client.messages.create(
        model=model, max_tokens=400, system=SYSTEM,
        tools=[ALT_TOOL], tool_choice={"type": "tool", "name": "set_alt"},
        messages=[{"role": "user", "content": [
            {"type": "image", "source": {"type": "base64", "media_type": mime, "data": data}},
            {"type": "text", "text": "この画像の代替テキストを set_alt で返してください。"},
        ]}],
    )
    block = next(b for b in msg.content if b.type == "tool_use")
    return block.input, msg.usage


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--model", default="claude-haiku-4-5")
    ap.add_argument("--uploads-dir", default=DEFAULT_UPLOADS_DIR)
    ap.add_argument("--wp", default="wp", help="WP-CLI コマンド(空白区切り可)")
    ap.add_argument("--ids", default="")
    ap.add_argument("--force", action="store_true")
    ap.add_argument("--dry-run", action="store_true")
    args = ap.parse_args()

    wp_cmd = args.wp.split()
    only = set(int(x) for x in args.ids.split(",")) if args.ids else None
    client = Anthropic()
    tot_in = tot_out = 0

    for att_id, relfile, cur_alt in list_image_attachments(wp_cmd):
        if only and att_id not in only:
            continue
        if cur_alt and not args.force:
            print(f"[skip] {att_id} 既に alt あり: {cur_alt[:30]}…")
            continue
        path = os.path.join(args.uploads_dir, relfile.replace("/", os.sep))
        if not os.path.exists(path):
            print(f"[warn] {att_id} ファイルが無い: {path}")
            continue
        result, usage = gen_alt(client, args.model, path)
        tot_in += usage.input_tokens
        tot_out += usage.output_tokens
        decorative = bool(result.get("decorative"))
        alt = "" if decorative else result.get("alt", "").strip()
        print(f"[{'装飾→空' if decorative else '内容'}] {att_id} {relfile}\n    alt: {alt!r}")
        if args.dry_run:
            continue
        wp(["post", "meta", "update", str(att_id), "_wp_attachment_image_alt", alt], wp_cmd)
        verified = wp(["post", "meta", "get", str(att_id), "_wp_attachment_image_alt"], wp_cmd)
        print(f"    書き戻し[{'OK' if verified == alt else 'MISMATCH'}]")

    price = {"claude-haiku-4-5": (1.0, 5.0), "claude-sonnet-4-6": (3.0, 15.0)}
    pin, pout = price.get(args.model, (1.0, 5.0))
    cost = tot_in / 1e6 * pin + tot_out / 1e6 * pout
    print(f"\n=== input={tot_in} output={tot_out} tokens / 概算 ${cost:.5f} ({args.model}) ===")


if __name__ == "__main__":
    main()

このリポジトリのように WP-CLI を PHP + phar のラッパーで動かしている場合は、--wp に起動コマンドを渡すか、wp.bat を用意してください。画像を一括で WebP に変換したときの記事(AI生成画像を一括 WebP 最適化した実測)と同じく、Node や Python の小さなスクリプト 1 本で運用が回ります。

落とし穴:装飾の扱い・文字入り画像・長すぎる alt

うまくいった話だけだとフェアじゃないので、つまずいた点も書きます。

「装飾は空 alt」は正しいが、該当ゼロのこともある

今回 12 枚はすべて内容画像で、装飾はゼロでした。ルール自体は正しくても、サイトによっては「空にすべき装飾画像」が 1 枚も無いことがあります。ちなみに、わたしのテーマだと装飾にあたるのは似顔絵アイコン(avatar.png)くらいですが、これは添付ファイルではないので今回の対象外でした。「装飾は空」を頭に入れつつ、実際の枚数は分類してみないと分からない、というのが実地の感想です。

文字入り画像は、字面を写すと事故る

最大の難所がここでした。このブログには「日本語が文字化けした生成画像」を並べた記事があり、その画像の文字をそのまま読むと「?????」や別字の羅列になります。それを alt にしても意味がありません。AI には「崩れた字面ではなく、その絵が示す意図を書く」とプロンプトで明示しました。W3C WAI の画像チュートリアルでも、alt は見た目の説明ではなく「文脈で果たす役割」を書くものとされています。ここは軽いモデルだと外しやすく、Sonnet など強いモデルのほうが安定しました。

放っておくと alt が長くなる

説明させると、つい長くなります。読み上げが冗長になるので、120 字を上限・40〜80 字を理想としてプロンプトで縛りました。あわせて「〜の画像」「〜の写真」のような接頭辞も禁止しています。読み上げソフトは先に「画像」と言うので、二重になって邪魔になるからです。同じ「Claude に一括処理させて実測する」流れは、顧客リストの名寄せをやらせた記事でも書きました。あわせてどうぞ。

まとめ

  • 空欄の alt を埋めるなら AI で十分。自分のブログ 12 枚で、手作業 20 分ぶんが数秒の書き戻しになった
  • 「全部に付ける」は誤り。装飾は空・内容だけ生成を AI 自身に判定させる
  • 文字入りの画像は字面を写さず「意図」を書かせる。文字が多い図だけ 1 枚目視で確認
  • コストは Haiku で全 12 枚 約 4 円。枚数が多いほど元が取れる(Batch で半額)
  • まず --dry-run で中身を見てから書き戻すと安全

自分のサイトでもやってみたい方へ

「やりたいけれど、スクリプトを自分の環境に合わせるのが不安」という方へ。サイト改善や SEO の内部対策の導入支援もやっています。alt の一括整備のような地味だけど効く作業を、まとめて片付けたい方は気軽にどうぞ。30 分の無料相談から始められます。 → サービス詳細

こま

こま

元SE × AI開発 × 業務自動化の実践者

SE 4年 + プログラミング歴 8年。業務自動化と AI 組み込み開発を仕事にしながら、現場で使えるものだけを書いています。 最新のAIの現場を発信中!!