1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【成功報告】Claude 3.7 Sonnet (Bedrock) を東京リージョンでVision動作させた話

Last updated at Posted at 2025-10-15

〜 API Gateway+Lambda+HTML連携で「Could not process image」地獄を突破 〜

🎯 結論

✅ Claude 3.7 Sonnet(ap-northeast-1)でも Vision(画像入力)機能は動く!
ただし API Gateway のバイナリ設定が必須。

HTML → API Gateway → Lambda → Bedrock の構成で、
Claude が複数画像を正しく解析して日本語で説明できるようになった。

🧩 構成概要

[HTMLブラウザ]
↓ (multipart/form-data)
[API Gateway (binary対応)]
↓ (Base64 encoded)
[Lambda (Python3.12)]
↓ (invoke_model)
[AWS Bedrock: Claude 3.7 Sonnet]

⚙️ 問題点:「Could not process image」

最初、LambdaからClaudeへ送った画像で以下のエラーが出続けた👇

ValidationException: Could not process image

原因を掘ると、
API Gateway が画像データを文字列化して Lambda に渡していた ことが判明。
Claudeに届いたBase64が壊れており、デコード不能になっていた。

💡 対処の流れ

ステップ 内容
① Lambdaで multipart/form-data を正確にパースする処理を作成
② HTMLで fetch(FormData) 送信(Content-Type自動設定)
③ API Gateway の Binary Media Types に / を設定
④ Lambdaで isBase64Encoded=True を判定して base64.b64decode()
⑤ Claude呼び出しJSONを "type": "image"+Base64形式に統一

✅ Lambda コード(完成版)

import json
import boto3
import base64
from email.parser import BytesParser
from email.policy import default

def lambda_handler(event, context):
    if event.get("requestContext", {}).get("http", {}).get("method") == "OPTIONS":
        return {"statusCode": 200, "headers": cors_headers(), "body": json.dumps({"message": "CORS OK"})}

    bedrock = boto3.client("bedrock-runtime", region_name="ap-northeast-1")
    model_id = "apac.anthropic.claude-3-7-sonnet-20250219-v1:0"
    system_prompt = "必ず日本語で答えてください"
    max_tokens = 1000

    user_input = ""
    images = []

    try:
        headers = event.get("headers", {}) or {}
        content_type = headers.get("content-type") or headers.get("Content-Type")
        body_raw = event.get("body", "")
        is_b64 = event.get("isBase64Encoded", False)

        if content_type and "multipart/form-data" in str(content_type).lower():
            body_bytes = base64.b64decode(body_raw) if is_b64 else bytes(body_raw, "latin-1", errors="ignore")
            parser = BytesParser(policy=default)
            msg = parser.parsebytes(b"Content-Type: " + content_type.encode() + b"\n\n" + body_bytes)

            for part in msg.iter_parts():
                name = part.get_param("name", header="content-disposition")
                if name == "message":
                    raw = part.get_payload(decode=True)
                    if raw:
                        user_input = raw.decode(errors="ignore").strip()
                elif name == "images":
                    data = part.get_payload(decode=True)
                    mime = part.get_content_type()
                    if mime in ["image/png", "image/jpeg"]:
                        b64 = base64.b64encode(data).decode().replace("\n", "")
                        images.append({"mime": mime, "data": b64})

    except Exception as e:
        return error_response(f"リクエスト解析エラー: {str(e)}")

    if not user_input.strip():
        user_input = "画像を解析してください。"

    user_content = [{"type": "text", "text": user_input}]
    for img in images:
        user_content.append({
            "type": "image",
            "source": {"type": "base64", "media_type": img["mime"], "data": img["data"]}
        })

    payload = {
        "anthropic_version": "bedrock-2023-05-31",
        "system": system_prompt,
        "max_tokens": max_tokens,
        "messages": [{"role": "user", "content": user_content}]
    }

    response = bedrock.invoke_model(modelId=model_id, body=json.dumps(payload))
    body = response.get("body")
    if hasattr(body, "read"):
        body = body.read()

    res_json = json.loads(body)
    output_text = res_json["content"][0]["text"]

    return {"statusCode": 200, "headers": cors_headers(), "body": json.dumps({"reply": output_text}, ensure_ascii=False)}

def error_response(msg):
    return {"statusCode": 400, "headers": cors_headers(), "body": json.dumps({"error": msg}, ensure_ascii=False)}

def cors_headers():
    return {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
        "Access-Control-Allow-Headers": "Content-Type,x-amz-date,authorization,x-api-key,x-amz-security-token"
    }

🌐 HTML 側(複数画像対応版)

<input type="file" id="imageInput" multiple accept="image/*">
<textarea id="inputText"></textarea>
<button id="sendBtn">送信</button>
<div id="reply"></div>

<script>
const API_URL = "https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/●/●";

document.getElementById("sendBtn").onclick = async () => {
  const msg = document.getElementById("inputText").value.trim();
  const files = document.getElementById("imageInput").files;

  const formData = new FormData();
  formData.append("message", msg);
  for (let f of files) formData.append("images", f, f.name);

  const res = await fetch(API_URL, { method: "POST", body: formData });
  const text = await res.text();
  document.getElementById("reply").textContent = text;
};
</script>

🧱 API Gateway 設定ポイント

設定 → Binary Media Types → / を追加

→ デプロイ後、Lambdaに isBase64Encoded=True が渡るようになる。

📸 動作結果

複数の画像(Windows 10/11 のサポート情報)を送信した結果:

Claude が両方の画像を認識し、
各バージョンのサポート終了日を表にまとめて説明。

結果は自然な日本語で、画像間の比較も自動で行われた!

🏁 まとめ

要素 対応状況
Claude 3.7 Sonnet (ap-northeast-1) ✅ 画像解析OK
API Gateway (Binary設定) ✅ 有効
Lambda (multipart処理) ✅ 正常
HTML (fetch+FormData) ✅ 複数画像送信OK
🐘 感想

「Could not process image」エラーに何日も悩んだけど、
原因はAPI Gatewayが画像を文字列化してたことだった。

Binary Media Typeを / にした瞬間、
Claudeがちゃんと画像を読んで答えてくれた。

正直、成功したとき泣いた。

🧑‍💻 環境

AWS Lambda (Python 3.12)

Amazon Bedrock Claude 3.7 Sonnet

API Gateway HTTP API

東京リージョン (ap-northeast-1)

ブラウザ: Chrome 141

🎉 これで、

「AWS Bedrock × Claude 3.7 Sonnet × 画像解析」を東京リージョンで動かした最初の成功例のひとつ!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?