OpenAI Visionを使って、画像からJSONを作成した

[こちら](https://dev.classmethod.jp/articles/using-gpt-4-vision-previewapi-to-generate-md-and-json-from-images/)の記事を参考に、[OpenAIのVison](https://platform.openai.com/docs/guides/vision)を使用して、画像データの解析結果をJSON形式で返却してもらいました。

### 実際に作ったもの

https://shelf-sheriff.vercel.app

[Github](https://github.com/daisuke0926dev/ShelfSheriff)

本棚の内容から、いくつかの診断+オススメのジャンルを提案してくれるWebアプリです。
アドベンチャーゲーム風に進行します。
<img src="https://github.com/daisuke0926dev/ShelfSheriff/assets/90462400/029edbb2-f96c-4c5d-a314-387fdfc74ec1" width="500">

### 概要(API周りに着目)

|クライアント側|React|画像データを読み取り&Base64エンコード|
|-|-|-|
|サーバー側|Python|データURLの形式でOpenAIのAPIにBase64画像データを渡す|


### 実装時に気をつけた点
- Base64エンコード
    [公式マニュアルにも書かれています](https://platform.openai.com/docs/guides/vision/uploading-base-64-encoded-images)が、ローカルデータをVisonのモデルに渡すにはBase64でエンコードを行います。インターネットに転がっている画像を参照したい場合以外は、この方法になるということです。
- MAXトークン数
    無尽蔵にトークンを使用されると、金銭面で嬉しくないのでトークン数を絞ります。
    しかし、JSONの特性上、途中でデータが切れてしまうとデコードできないので、プロンプトで"○文字以内"と調整すると良いです。    
- 画像形式
    `.png`, `.jpeg`, `.jpg`, `.webp`, `.gif`がサポート対象です。([公式](https://platform.openai.com/docs/guides/vision/what-type-of-files-can-i-upload))
- 画像サイズ
    1画像あたり20MBまでの制限があります。([公式](https://platform.openai.com/docs/guides/vision/is-there-a-limit-to-the-size-of-the-image-i-can-upload))

あとは、実際にAPIを叩くと親切なエラー文が返ってくるので、適宜対応できるかと思います。

<details><summary>クライアント側のコード一部</summary><div>

```react:aws-lambda
  //ファイルアップロード処理
  const handleFileUpload = async (event) => {
    const file = event.target.files[0];
    if (file) {
      setIsUploading(true);
      try {
        const base64 = await toBase64(file);
        const apiEndpoint = process.env.REACT_APP_API_ENDPOINT;

        const response = await axios.post(apiEndpoint, { image: base64 });
        onComplete(response.data);
      } catch (error) {
        console.error("File upload failed:", error);
      } finally {
        setIsUploading(false);
      }
    }
  };

  // エンコード処理：画像ファイル->Base64データ
  const toBase64 = (file) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result.split(',')[1]);
    reader.onerror = error => reject(error);
  });
```
</div></details>

<details><summary>サーバー側のコード全文</summary><div>

```python:aws-lambda
from decimal import Decimal
import json, os, openai

def decimal_to_int(obj):
    if isinstance(obj, Decimal):
        return int(obj)

def lambda_handler(event, context):
    openai.api_key = os.environ['API_Key']
    base64_string = event["image"]
    
    print(base64_string)
    response = openai.ChatCompletion.create(
        model="gpt-4-vision-preview",
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": "あなたはユーモアのあるプロファイリングを行うプロです。口調は口数の少ないミステリアスな探偵。この本棚の持ち主に対して以下内容を推測してください。-character：性格(ユーモアのある表現と根拠)(hint...少ない本こそ関心が深い可能性もあります) - interesting：興味のある分野と根拠 -recommend：これから興味を持つことをお勧めする意外な分野と根拠。character,interesting,recommendという3つのkeyに対してそれぞれ70字以上100字以下のvalueを紐づけたJSONデータを返却してください。jsonファイル出力結果はstart,endで終わるようにしてください。"},
                    {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_string}",}},
                ],
            }
        ],
        max_tokens=500,
    )
    response_content = response["choices"][0]["message"]["content"]
    response_content = response_content[5:-3] #文頭にあるstart,文末にあるendの文字を削除
    response_content = json.loads(response_content)
    print("Received response:" + json.dumps(response, default=decimal_to_int, ensure_ascii=False))
    return response_content    
```
</div></details>