結論から:Jev単体では画像を認識できない

前回の記事で紹介したTypeSafe AIの「Jev」は、ソフトウェアが直接使える型付きの判断を返すモデルです。「写真に人が写っているか判断させたい」と考えるのは自然な発想ですが、結論から言うとJevはテキスト専用モデルで、画像をそのまま渡すことはできません。

公式ドキュメントにはこう明記されています。

Jev accepts text only. State must be a string, JSON object, or array of text values. Images, audio, and video are not supported (yet).

同時に、公式は代わりの方法もはっきり示しています。

Pre-process non-text inputs (images, audio, video, binaries) into text or structured fields before sending them as \state\.

つまり、画像を先にVision対応モデルで文章に変換し、その文章をJevの\state\として渡すのが公式の推奨パターンです。この記事では、この2段構成で「写真に人がいるかいないか」を判定するプログラムを作ります。

全体の設計:認識はVisionに、判断はJevに

処理を2つの役割に分けます。

  1. 認識(Vision):画像に何が写っているかを、判断を交えずに事実だけ文章化する
  2. 判断(Jev):その文章を材料に、型付きの答え(Yes/No、選択肢)と確信度を返す

この分業には理由があります。Jevのドキュメントは、Jevに渡す指示は「明確で文字通り」であるべきだと繰り返し述べています。もしVisionモデルに「人がいるか教えて」と直接聞いてしまうと、Yes/Noの判断基準や確信度の出し方がモデルごとにバラバラになり、後工程のコードで扱いにくくなります。Visionモデルには事実の描写だけをさせ、「人物が写っているかどうか」という判断そのものはJevの型付きの質問(Noul・Choice)に任せることで、確信度による自動化のしきい値設計が可能になります。

このパターンは、TypeSafeの公式クックブックにある「SDE Cascade(抽出モデルで下書きし、Jevの確信度で検証・エスカレーションする)」とも同じ考え方です。抽出とJevによる判断を分けるという設計思想は共通しています。

使うもの

  • Claude API(Vision対応モデル):画像を文章に変換する
  • TypeSafe AIのJev(\typesafe-sdk\):文章から型付きの判断を出す

Claude APIのインストールとセットアップは以下の通りです。

\\\`bash

pip install anthropic typesafe-sdk

\\\`

Claude APIキー(\ANTHROPIC_API_KEY\)とTypeSafeのAPIキーをそれぞれ環境変数に設定しておいてください。

ステップ1:画像を文章化する

まず、画像をBase64エンコードしてClaude APIに送り、写っているものを客観的に描写させます。ポイントは、プロンプトで「人がいるか判断して」とは聞かず、「見えたものをそのまま説明して」と頼むことです。

\\\`python

import base64

from anthropic import Anthropic

anthropic_client = Anthropic()

def describe_photo(image_path: str) -> str:

code
with open(image_path, "rb") as f:
    image_data = base64.standard_b64encode(f.read()).decode("utf-8")

media_type = "image/jpeg" if image_path.lower().endswith((".jpg", ".jpeg")) else "image/png"

response = anthropic_client.messages.create(
    model="claude-opus-5",
    max_tokens=1024,
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "image",
                "source": {
                    "type": "base64",
                    "media_type": media_type,
                    "data": image_data,
                },
            },
            {
                "type": "text",
                "text": (
                    "この写真に写っているものを、判断を交えず事実だけ箇条書きで説明してください。"
                    "人物が写っていれば、人数・服装・姿勢など見た目の特徴も含めてください。"
                    "写っていない場合は無理に人物について触れる必要はありません。"
                ),
            },
        ],
    }],
)

return "".join(block.text for block in response.content if block.type == "text")

\\\`

ここで返ってくるのは、あくまで「見えたものの説明文」です。「人がいます」「いません」という最終判断はまだ出していません。

ステップ2:文章をJevに渡して型付きに判断する

次に、この説明文をJevの\state\として渡し、2つの質問を投げます。

  • Noul:「この説明は、写真に少なくとも1人の人物が写っていることを示しているか」(0〜1の確率)
  • Choice:「写っている人数はどれに当てはまるか」(0人・1人・複数人)

\\\`python

from typesafe_sdk import Choice, Noul, TypeSafeClient

typesafe_client = TypeSafeClient()

def check_person_in_photo(image_path: str) -> dict:

code
description = describe_photo(image_path)

result = typesafe_client.system_one(
    state=description,
    questions={
        "has_person": Noul(
            instructions="この説明は、写真に少なくとも1人の人物が写っていることを示している",
        ),
        "person_count": Choice(
            instructions="写真に写っている人物の人数はどれに当てはまるか",
            criteria={
                "none": "人物は写っていない",
                "one": "1人だけ写っている",
                "multiple": "2人以上写っている",
            },
        ),
    },
)

has_person = result.answers["has_person"]
person_count = result.answers["person_count"]

return {
    "description": description,
    "has_person": has_person.noul >= 0.5,
    "confidence": has_person.confidence,
    "person_count": person_count.choice,
    "person_count_confidence": person_count.confidence,
    # 確信度が低い場合は自動判定せず人の目でのチェックに回す
    "needs_review": has_person.confidence < 0.7,
}

\\\`

ステップ3:確信度でルーティングする

最後に、確信度に応じて挙動を分けます。しきい値は例なので、実際の用途(誤検知の許容度)に合わせて調整してください。

\\\`python

if __name__ == "__main__":

code
result = check_person_in_photo("sample.jpg")

if result["needs_review"]:
    print(f"⚠️ 確信度が低いため要確認(確信度: {result['confidence']:.2f})")
    print(f"   Visionの説明: {result['description']}")
elif result["has_person"]:
    print(f"✅ 人物あり({result['person_count']}, 確信度: {result['confidence']:.2f})")
else:
    print(f"➖ 人物なし(確信度: {result['confidence']:.2f})")

\\\`

実行結果のイメージは次のようになります。

\\\`

✅ 人物あり(one, 確信度: 0.97)

\\\`

\\\`

⚠️ 確信度が低いため要確認(確信度: 0.42)

Visionの説明: - 屋外の風景写真。手前に木々のシルエット...

\\\`

確信度が高ければ自動判定に任せ、低ければ人手のチェックに回す――これは前回の記事でも紹介した、Jevの基本的な運用パターンです。破壊的な操作(例:自動で写真を削除する、通知を送るなど)に使う場合は、しきい値をより高く設定してください。

なぜ「Visionだけ」で済ませないのか

ここまで読むと、「Claudeに直接『人はいますか?』と聞けば済むのでは」と思うかもしれません。単純な1枚の判定であれば、その通りです。この2段構成が効いてくるのは、次のような場面です。

  • 大量の写真を一括処理したい:Jevは1回の呼び出しで複数の質問を並列に評価でき、質問を増やしても応答時間がほとんど変わりません。人数の区分やシーン分類など、判断項目を増やしてもコストの伸びを抑えられます
  • 確信度に基づいて自動化と人手確認を切り分けたい:Vision単体の自由な文章では、しきい値による機械的な分岐が組みにくくなります
  • 判断ロジックをコード側に残しておきたい:Visionの描写とJevの判断基準を分けておくことで、「どういう写真を人物ありとみなすか」の基準を、プロンプトではなくJevの\criteria\として明示的に管理できます

逆に、1枚だけをその場で確認するようなカジュアルな用途では、Vision単体で十分な場合が多いです。

注意点

  • Visionモデルの説明が誤っていれば、Jevの判断もその誤りを引き継ぎます。Jevは文章に対する判断は較正されていますが、そもそもの画像認識精度はVisionモデル側に依存します
  • Jevは算数や数え上げが苦手です。今回は人数を「0人・1人・複数人」という選択式(Choice)にすることで、この弱点を回避しています。「正確に何人か」を数えさせたい場合は、Jevではなく別の手段(物体検出モデルなど)を検討してください
  • 確信度のしきい値は用途に応じて調整が必要です。プライバシーに関わる用途(人物が写っている写真を自動でぼかす、など)では、見逃しのリスクを考慮して慎重に設定してください

まとめ

  • Jevはテキスト専用モデルで、画像を直接扱うことはできない
  • 公式が推奨するのは、Visionモデルで画像を文章化してからJevに渡す構成
  • Visionには「事実の描写」だけをさせ、判断はJevの型付きの質問(Noul・Choice)に任せるのがポイント
  • 確信度をしきい値として使い、自動判定と人手確認を切り分けることで、大量の画像処理にも展開しやすくなる

「AIに画像を見せて何かを判断させる」という要件は、AIモデル1つで完結させず、認識と判断を役割分担すると設計しやすくなります。今回の「人物がいるか」の判定は最小限の例ですが、シーン分類やコンテンツモデレーションなど、他の画像判定タスクにも同じ構成を応用できます。

※本記事のコード例は、公開されているAnthropicおよびTypeSafe AIの公式ドキュメントの仕様に基づいています。実行には別途Claude APIキーとTypeSafe AIのAPIキーが必要です。