Claudeでaltテキストを画像から一括生成する手順 — 文脈を渡して検証まで
画像だけでなく周辺の見出し・本文・リンク先もClaudeに渡し、装飾画像は空altにする判定まで任せるCSV運用の手順です。生成後の機械検証スクリプトも付けます。
数百枚ある画像のalt属性を埋めるとき、画像だけをClaudeに投げて「説明を書いて」と頼むと、どの画像にも同じ調子の描写が並びます。alt属性は画像の見た目の説明ではなく、そのページでその画像が果たす役割を言葉にしたものです。役割は周辺の文脈で決まるので、文脈ごと渡さなければ正しい答えは出ません。
この記事では、画像1枚につき1行のCSVを作り、画像と文脈をセットでClaudeに渡して、alt案と判定を別々の列で受け取る流れを示します。最後に、生成結果を手元のスクリプトで機械的に検査する部分まで扱います。
altは画像の描写ではなく、置かれた場所での役割で決まる
W3CのWAIが示すaltの判断木は、画像ごとに次の順で問いを立てます。ここがそのままClaudeへの指示の骨格になります。
altの方針を決める問いの順番
- 1
画像に文字が含まれるか
近くに同じ文字が本文として載っているなら空alt。文字に機能(アイコンなど)があるなら機能を書く。文字が他のどこにも無いなら、その文字をaltに入れます。
- 2
リンクやボタンの中の画像か
画像が無いとリンク先や操作の意味が分からなくなるなら、リンク先や実行される操作をaltに書きます。
- 3
そのページに意味を足しているか
単純な図や写真なら短い説明。グラフや複雑な図なら、情報そのものを本文のどこかにも載せます。近くの本文と重複するだけなら空altです。
- 4
純粋な装飾か
装飾やユーザー向けでない画像は、空のalt属性(
alt="")にします。
注意したいのは、同じ写真でも置かれ方でaltが変わる点です。W3Cの例では、盲導犬の首輪に付いた鈴の写真が、鈴の目的を説明する本文の隣にあるときは「犬の首輪に付いた鈴」のような短い説明で足ります。一方、すでに本文で十分に説明されている眠る犬の写真は、同じ内容を繰り返す必要がないため空altが適切とされています。
つまりClaudeに頼むのは「描写」ではなく「この文脈での分類と、分類に応じたalt」です。画像ファイルだけを渡す運用では、この判断の材料が欠けています。
入力CSVは画像1枚=1行にして文脈の列を持たせる
画像の抽出元はCMSの書き出しでもクローラーの出力でもかまいません。列は次の6つあれば足ります。
id,image_path,page_heading,surrounding_text,link_text,existing_alt
p012-03,images/p012/staff.jpg,スタッフ紹介,"当店の店長は…",,
p012-04,images/p012/line.png,お問い合わせ,"最新情報はLINEで配信します",LINEで友だち追加,
p013-01,images/p013/divider.png,料金,"",,page_heading: 画像が属する節の見出し。ページ全体のタイトルより、直近のh2・h3のほうが効きますsurrounding_text: 画像の前後の本文。長すぎる場合は前後で数文ずつに絞りますlink_text: 画像が<a>や<button>の中にあるときの、リンク先名や周辺のラベル。無ければ空existing_alt: 既存のalt。比較用で、上書きするかは検証後に決めます
3行目のように本文が空の画像は、装飾の可能性が高い行です。それでも判定自体はClaudeに出させ、人が後で見る対象に回します。
画像と文脈をセットで送る一括生成スクリプト
Messages APIでは、画像をimageコンテンツブロックとして渡せます。ソースは、base64、URL、Files APIのfile_idの3種類です。社内の素材などURLで公開していない画像が多いと想定して、ここではbase64で送ります。Amazon BedrockとGoogle Cloudではbase64のみ使えます。
ブロックの並びは、画像を先に、テキストを後にします。Anthropicのドキュメントは、画像がテキストの前にあるほうがうまく動くとしています。
import base64, csv, json, mimetypes, sys
from pathlib import Path
import anthropic
MODEL = "claude-opus-5-5"
client = anthropic.Anthropic()
INSTRUCTION = """\
あなたはWebアクセシビリティの担当者です。上の画像が、下の文脈で
どんな役割を持つかを判定し、alt属性の案をJSONだけで返してください。
kind は次の5つから1つ選びます。
- decorative: 装飾。近くの本文と内容が重複する場合も含む。alt は ""
- informative: 本文にない情報を足す写真や図。alt は短い一文
- functional: リンクやボタンの中の画像。alt はリンク先や操作
- text: 画像内の文字が他のどこにも無い。alt は画像内の文字
- complex: グラフや表など。alt は何の図かだけ。詳細は本文側で補う
返すJSON: {"kind":"...","alt":"...","text_in_image":"...","reason":"..."}
text_in_image は画像内の文字をそのまま書く。無ければ ""。
読み取れない文字は推測で埋めず "[判読不可]" と書く。
人物の名前は画像から推測しない。"""
SUPPORTED = {"image/jpeg", "image/png", "image/gif", "image/webp"}
def image_block(path: Path) -> dict:
media = mimetypes.guess_type(path.name)[0]
if media not in SUPPORTED:
raise ValueError(f"対応外の形式: {path.suffix}")
data = base64.standard_b64encode(path.read_bytes()).decode()
return {"type": "image",
"source": {"type": "base64", "media_type": media, "data": data}}
def ask(row: dict, root: Path) -> dict:
context = (f"節の見出し: {row['page_heading']}\n"
f"前後の本文: {row['surrounding_text']}\n"
f"リンク/ボタンのラベル: {row['link_text']}")
try:
block = image_block(root / row["image_path"])
except ValueError as e:
return {"kind": "SKIP_FORMAT", "alt": "", "text_in_image": "",
"reason": str(e)}
msg = client.messages.create(
model=MODEL, max_tokens=512,
messages=[{"role": "user", "content": [
block,
{"type": "text", "text": context + "\n\n" + INSTRUCTION},
]}],
)
text = msg.content[0].text
try:
return json.loads(text)
except json.JSONDecodeError:
return {"kind": "PARSE_ERROR", "alt": "", "text_in_image": "",
"reason": text[:200]}
if __name__ == "__main__":
src, root, dst = sys.argv[1], Path(sys.argv[2]), sys.argv[3]
rows = list(csv.DictReader(open(src, encoding="utf-8")))
fields = list(rows[0]) + ["kind", "alt", "text_in_image", "reason"]
with open(dst, "w", encoding="utf-8", newline="") as f:
w = csv.DictWriter(f, fields)
w.writeheader()
for r in rows:
w.writerow({**r, **ask(r, root)})pip install anthropic
export ANTHROPIC_API_KEY=sk-ant-...
python gen_alt.py pages.csv ./site-assets alt_out.csv出力CSVは、例えば次のような形になります。以下は書式を示すための例で、実際の出力ではありません。
id,kind,alt,text_in_image,reason
p012-03,informative,店の前で笑顔で立つ店長,,本文は店長を紹介しており顔写真が役割を果たす
p012-04,functional,LINEで友だち追加,LINEで友だち追加,リンク内の画像でラベルが操作を示す
p013-01,decorative,,,区切り線で本文に情報なしkindとaltを別の列にしているのがこの設計の要です。判定とaltが矛盾していないかを、次の検査で機械的に見られるからです。
生成結果を機械で検査する
Claudeの出力は、そのままCMSに流し込みません。ルール化できる矛盾は、別のスクリプトで拾います。次は判定と文脈の食い違いを見る検査です。
import csv, sys
BAD_START = ("画像", "写真", "イメージ")
MAX_LEN = 80 # このスクリプトで仮に置いた閾値。サイトの方針に合わせて変える
def check(r: dict) -> list[str]:
kind, alt = r["kind"], r["alt"].strip()
text, near = r["text_in_image"].strip(), r["surrounding_text"]
issues = []
if kind == "PARSE_ERROR":
return ["JSONとして読めない"]
if kind == "SKIP_FORMAT":
return ["対応外の画像形式(変換するか人が書く)"]
if kind == "decorative" and alt:
issues.append("装飾なのにaltが入っている")
if kind != "decorative" and not alt:
issues.append(f"{kind}なのにaltが空")
if alt.startswith(BAD_START):
issues.append("altが「画像」「写真」で始まる")
if len(alt) > MAX_LEN:
issues.append(f"altが{MAX_LEN}字超")
if alt.lower().endswith((".jpg", ".png", ".webp", ".gif")):
issues.append("altがファイル名のまま")
if kind == "text" and text and text not in alt:
issues.append("画像内の文字がaltに入っていない")
if text and text != "[判読不可]" and text in near and kind != "decorative":
issues.append("画像内の文字が本文にもある(空altの候補)")
if "[判読不可]" in text:
issues.append("判読不可の文字がある(目視で確認)")
if alt and alt in near:
issues.append("altが前後の本文の重複")
return issues
for r in csv.DictReader(open(sys.argv[1], encoding="utf-8")):
for msg in check(r):
print(f"{r['id']}\t{msg}")この検査のうち、「画像内の文字が本文にもあるのに装飾扱いになっていない」は、判断木の最初の問いをそのままコードにしたものです。MAX_LENの80字は、W3Cの資料に数値が載っているわけではありません。このスクリプトで仮に置いた閾値で、サイトの方針に合わせて動かしてください。
検査に掛かった行だけをClaudeに差し戻す方法もあります。その際は、issuesの文言を指示に添えて同じ画像を再送します。再送しても直らない行は、人が見る列に回してください。
人が見るべき行を先に決めておく
全件を目視する必要はありませんが、次の行は機械検査を通っても人が確認する対象です。
complexの画像: グラフや複雑な図は、altだけで情報を伝えるものではありません。W3Cの判断木は、図に含まれる情報をページの別の場所にも載せるよう求めています。Claudeにはその説明文の下書きまで頼めますが、本文への追加は別作業です- 人物が写る画像: Claudeは画像の人物の名前を挙げることを拒否します。名前が必要なら、キャプションや本文など画像の外にある情報をCSVに足して渡します
- 文字入り画像: 画像内の文字は、読み違えることがあります。
text_in_imageの列は、元画像と見比べる対象です - 小さい・回転した・粗い画像: 低品質、回転している、200ピクセル未満といった画像では、Claudeが誤った解釈を返すことがあるとドキュメントが警告しています。ロゴのような小さな画像も、この対象です
画像内の文字を読ませたいときは、文字が判読できる解像度で送ります。ドキュメントは、重要な文字が入っているなら読みやすい大きさにし、文字を大きく見せたいだけで周囲の文脈を切り落とさないようにと説明しています。強いJPEG圧縮は文字を読みにくくします。
枚数・サイズ・コストの上限
一括処理で引っかかる数値は、次のとおりです。
API経由で画像を送るときの上限
1リクエストの画像数(200kコンテキストのモデル)
100枚
それ以外のモデルは600枚
1枚あたりの最大寸法
8000×8000px
21枚以上では制限がさらに厳しくなる
1枚あたりの最大サイズ
10MB
base64後。BedrockとGoogle Cloudは5MB
制限を超えた画像はinvalid_request_errorで拒否されます。上のスクリプトは1行1リクエストなので、枚数や寸法の制限には当たりにくい構成です。
形式にも制約があります。Claudeが読めるのはJPEG、PNG、GIF、WebPの4つで、GIFのアニメーションは最初のフレームだけが使われます。サイトの素材にはSVGやAVIF、ICOが混ざっていることが多く、そのまま送ると対応外としてAPIに拒否されます。上のスクリプトは対応外の拡張子をSKIP_FORMATとして出力に残し、API呼び出しを飛ばします。検査スクリプトがその行を拾うので、PNGなどへ変換して再実行するか、人がaltを書く行に回してください。
トークン消費は、画像を28×28ピクセルのパッチに分けた数で決まります。1000×1000pxの画像は、どのモデルでも1296トークンです。長辺が大きい画像は、モデルによって縮小の上限が違います。
| 画像サイズ | 標準ティア(縮小後/トークン) | 高解像度ティア(縮小後/トークン) |
|---|---|---|
| 1920×1080px | 標準ティア(縮小後/トークン)1456×819px / 1560 | 高解像度ティア(縮小後/トークン)縮小なし / 2691 |
| 3840×2160px | 標準ティア(縮小後/トークン)1456×819px / 1560 | 高解像度ティア(縮小後/トークン)2576×1449px / 4784 |
高解像度ティアはClaude 4.7以降のモデルが対象です。alt生成では細部の解像度より、画像が何を表すかの理解が主役なので、数千ピクセルの原本をそのまま送る必要はありません。送る前に長辺を1500px程度に縮めると、トークン消費と通信量を抑えられます。ただし文字入り画像は、縮めると文字が潰れるため例外にします。
つまずきやすい点
- JSON以外の文章が混ざる: 前置きが付くと
json.loadsが失敗します。上のスクリプトはPARSE_ERRORとして行を残し、検査スクリプトが拾います。繰り返し起きるなら、指示の末尾に「JSON以外は出力しない」と足してから再実行します - 日本語の文字の扱い:
MAX_LENは文字数で数えており、読み上げソフトでの長さではありません。一覧画面など同じaltが繰り返し読まれる場所は、短めに設定します - 対応外の形式で行が止まる: SVGやAVIFなどを拡張子だけ見て
image/jpegと偽って送ると、中身と形式が食い違い、拒否されるおそれがあります。形式の判定は拡張子で行い、4形式以外は送らない作りにしておくと、一括処理が途中で止まりません - 画像のメタデータは読まれない: Claudeは画像のメタデータを受け取りません。ファイル名やEXIFに手掛かりがある場合は、CSVの列として自分で渡します
- アップロードした画像は残らない: 画像はAPIリクエストの処理中だけ使われ、処理後に自動で削除されます。一方で、同じ画像を何度も送るなら、Files APIで一度アップロードして
file_idで参照する方法が使えます
画像を読ませて表に起こす別の例として、納品書のスキャン画像と請求データを突き合わせる手順と、競合店のチラシ画像から価格を読み取る手順があります。ページの引っ越しに伴うURLの突き合わせはサイトリニューアルの301対応表で扱っています。
まとめ
alt生成の質は、モデルの性能よりも、何を渡すかで決まります。画像と一緒に見出し・本文・リンクのラベルを渡し、「描写」ではなく「この場所での役割の分類」を先に出させると、装飾画像を空altにする判断まで同じ流れに乗ります。出力をkindとaltに分けておけば、矛盾はスクリプトで拾えます。残った複雑な図と人物写真が、人の仕事です。