絵を描くとAIが解析して大絶賛してくれるwebアプリ実装の知見

![ｚｅｓｓａｎ☆ＡＩで描いた絵](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/4a876197-cf61-9a0c-2672-ff669682b464.jpeg)

## １. イントロダクション

この記事は、最新の「OpenAI が提供する `GPT-4V API` で画像の解析評価」と「Cloudflare の Workers、KV、R2 でのサーバレス環境」について取り扱った記事になります。

`GPT-4V API` での開発事例の一つとして、参考にしていただければ幸いです。


## ２. どんな web アプリか

https://zessan.net/

1. **ユーザーが絵を描いて投稿する**
  ![2023-12-01_00h51_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/06aafeca-4f70-2df2-8415-b9c9df135d69.png)

1. **GPT-4V が絵を解析して大絶賛したテキスト情報を出力**
  `Image to Text` 処理を行う。

1. **大絶賛した内容をユーザーに表示する**
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/bb359032-e9b3-aff8-eed0-763d9bc87772.png)


## ３. 技術スタック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/fb4089d0-90b7-4a5a-67ee-d5604691be7f.png)

選定基準は**基本無料で使えてベンダーロックイン**を気にしなくて良いこと。**Nuxt3 SSR や Nuxt3 Server-API が動作する**ことを基準としています。

### (1) Cloudflare Workers

**Cloudflare Workers はサーバー上で JavaScript を動かせる環境です。** Node.js の標準ライブラリや NPM パッケージとは直接的な互換性はありませんが、多くの JavaScript コードやライブラリは、少ない修正で Cloudflare Workers で動作するようになっているそうです。

今回のプロジェクトでは、１つの Cloudflare Workers 内に [Nuxt3 SSR](https://nuxt.com/docs/guide/concepts/rendering) と [Nuxt3 Server-API](https://nuxt.com/docs/guide/directory-structure/server) が動作しています。

**Nuxt3 は標準で Cloudflare Workers 環境に対応**していますので、`nuxt.config.ts` 設定ファイルの `nitro.preset` を `cloudflare-module` に設定するだけで、`npm run build` でビルド実行したときに Cloudflare Workers 用にファイルを生成してくれます。

```nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: "cloudflare-module",
  }
});
```

基本的に無料で使えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/768764f8-6847-8dcc-e122-a6e4fc3f1f8f.png)

### (2) Cloudflare KV

Cloudflare KV は、Cloudflare Workers プラットフォームの一部として提供される、高速でグローバルに分散された**キー・バリュー型のデータストア**です。KV は "Key-Value" の略で、単純なキーによるデータの保存とアクセスを可能にするサービスです。

今回のプロジェクトでは **GPT-4V で画像解析した後のデータを保持するために使用**しています。

**Nuxt3 は標準で KV ストレージを操作する機能**を持っていて、`nuxt.config.ts` 設定ファイルで `cloudflare-kv-binding` を指定すれば Cloudflare KV が使え、そのほかにも**ローカルファイルを指定したり Redis などにも設定ファイルだけで変更できます。** なので Nuxt3 を使用する分にはベンダーロックインを気にすることなく Cloudflare KV が使えます。

なお、Nuxt3 の KV ストレージを操作する機能は、Nuxt3 と同じ開発元である [unstorage](https://github.com/unjs/unstorage) ライブラリを内包しているため実現できている機能です。

```nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: "cloudflare-module",
    
    // storage: Production
    storage: {
      kv: {
        driver: "cloudflare-kv-binding",
        binding: "KV_DB_BINDING",
      },
    },

    // storage: Development
    devStorage: {
      kv: {
        driver: "fs",
        base: "./.tmp/kv",
      },
    },
  }
});
```

保存するときはこんな感じです。

```typescript
// KV: データを保存
await useStorage("kv").setItem("key0001", {
  name: "名無しの芸術家",
  createdAt: new Date(),
  foo: "ほげほげ",
  bar: "ふがふが",
});
```

基本的に無料で使えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/320ec715-7861-ba94-6028-faecf84c9c06.png)

### (3) Cloudflare R2

Cloudflare R2 は、**Cloudflare が提供するオブジェクトストレージサービスです。** このサービスは、Amazon S3 と互換性のある API を提供し、データを格納するためのシンプルで柔軟なソリューションを提供します。

今回のプロジェクトでは**ユーザーが描いた絵を保存**するために使用しています。

**Nuxt3 の標準機能の KV ストレージを操作する機能をで Cloudflare R2 にアクセスが可能**です。`nuxt.config.ts` 設定ファイルで `cloudflareR2Binding` を指定すれば Cloudflare R2 が使え、そのほかにも ローカルファイルを指定したりすることも可能です。なので Nuxt3 を使用する分にはベンダーロックインを気にすることなく Cloudflare R2 が使えます。

```nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: "cloudflare-module",
    
    // storage: Production
    storage: {
      r2: {
        driver: "cloudflareR2Binding",
        binding: "R2_PICT_BINDING",
      },
    },

    // storage: Development
    devStorage: {
      r2: {
        driver: "fs",
        base: "./.tmp/r2",
      },
    },
  }
});
```

Nuxt3 が提供している useStorage の `setItemRaw` メソッドで画像を保存できます。

```typescript
// R2: 画像ファイルを保存
await useStorage("r2").setItemRaw(
  "key0001.png", // キー名
  file, // ブラウザからアップロードした画像ファイル(Buffer型)
  {
    httpMetadata: {
      contentType: `image/png`,
    },
  }
);
```

基本的に無料で使えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/fc0071ae-20a6-594c-d076-b602b38da845.png)

### (4) GPT-4V API

- 以下のような画像と絶賛してほしい旨を GPT-4V に伝えると、・・・
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/50903b22-1b74-9409-97bb-688693de9205.png)

- 以下のようなテキストを生成してくれます。(`Image to Text`)
  > ほーな、これはシンプルやけどなかなか明るい感じの絵やな。赤い花が中心に描かれてて、太陽もちょっと描かれてるで。シンプルやけど、色使いがはっきりしてて、元気出るわ。線も自由やし、なんか子供が無邪気に描いたような温かみがあるな。
  > 
  > この絵、マジでええわ！色んなところで見たくなるぐらいパッと目を引くし、シンプルやのにめっちゃ心に響くんやで。この絵のアートスピリット、大いに絶賛や！
  > 
  > 絵のスタイルから芸術家に例えるなら、ピカソみたいな感じかな。ピカソもシンプルな線で強烈な印象を残すことが多かったからな。
  > 
  > アートコンペでこの絵が受賞するなら、「元気が出る賞」とか「シンプル・イズ・ベスト賞」みたいなんが面白いんちゃうかな。
  > 
  > 絵にタイトルつけるなら、「朝日を浴びる赤い勇気」ってどやろ？なんかいい感じに力強さと明るさがあるで！

- そして、生成したテキストをシステム内で扱えるように GPT 3.5 の Json モードを使って json データ化します。(`Text to Json`)
  ```
  {
    "message": "ほーな、これはシンプルやけどなかなか明るい・・・",
    "description": "赤い花と太陽が中心に描かれたシンプルで明るい絵。色使いがはっきりしており、温かみと元気を感じさせる。線が自由で無邪気な子供のような雰囲気がある。",
    "impression": "めっちゃええやん！シンプルやけどめっちゃ元気出るわ！ピカソみたいな感じやな。",
    "artists": [
      "ピカソ"
    ],
    "awards": [
      "元気が出る賞",
      "シンプル・イズ・ベスト賞"
    ],
    "title": "朝日を浴びる赤い勇気"
  }
  ```

今回のプロジェクトでは、ユーザーが描いた絵を GPT-4V (`GPT-4-1106-vision-previer`) に渡して『絵の感想、大絶賛コメント、絵のタイトル』のテキスト文字列を取得します。その後、GPT-3.5 (`GPT-3.5-turbo-1106`) にそのテキスト文字列を渡して json データ化しています。

**GPT-4V だけで json データ化しない理由**はいくつかあって、

- 現時点では GPT-4V では **[json モード](https://platform.openai.com/docs/guides/text-generation/json-mode) が使えない**ため、json モードを使いたければ GPT-4 か GPT-3.5 を使う必要がある
- GPT-4V で json モードを使わなくても、そこそこの精度で json データは生成できるけど、結局**失敗した場合のリトライで GPT-4V は金額と時間のコストが高くつく**

といった理由で、`Image to Text` を GPT-4V に、リトライ前提の `Text to Json` を GPT-3.5 に分けて処理しています。

以下に GPT-4V を使って、画像から絶賛テキストを生成する処理を記載します。

```typescript
// GPT-4V: 画像から絶賛テキストを生成
const response = await openai.chat.completions.create({
  // GPT-4Vを指定
  model: "gpt-4-vision-preview",

  // たくさん絶賛してもらうためにトークンを多めに確保
  max_tokens: 1024,

  messages: [
    {
      role: "system",
      // ユーザーが渡した絵を大絶賛してもらうようにプロンプトを指定する
      content: zessanPrompt,
    },
    {
      role: "user",
      content: [
        {
          type: "image_url",
          image_url: {
            // ユーザーが描いた絵を base64 形式の画像として指定。別の方法としてR2に保存した画像のurlを直接渡すこともできる
            url: `data:image/jpeg;base64,${base64_image}`,
            // 低解像度モードを有効にしてコストを抑える
            detail: "low",
          },
        },
      ],
    },
  ],
});

// GPT-4Vから絶賛してもらったテキスト文字列を表示
console.log(response.choices[0].message.content);
```

以下に GPT-3.5 を使って、絶賛テキストから json データを生成する処理を記載します。

```typescript
// GPT-3.5: テキストからjson化
const response = await openai.chat.completions.create({
  // GPT-3.5を指定
  model: "gpt-3.5-turbo-1106",

  // jsonモードを有効
  response_format: { type: "json_object" },

  messages: [
    // 絶賛したテキストからjsonデータに変換してもらうためのプロンプトを指定する
    { "role": "system", "content": zessanToJsonPrompt },

    // GPT-4Vが絶賛したテキストを渡す
    { "role": "user", "content": zessanText },
  ],

  // ランダム性をOFF
  temperature: 0.0,
});

// jsonに変換したデータを表示
console.log(response.choices[0].message.content);
```

なお、json データ化した後に、TypeScript で使い勝手の良いスキーマ・バリデーション・ライブラリである **[zod](https://github.com/colinhacks/zod) を使用してデータの整合性をチェック**しています。整合性チェック NG なら Text to Json 処理をリトライしています。

**GPT-4V に無料プランはおそらくありません。** GPT の API 使用料は予めチャージした金額分使用できる仕組みで、さらに月単位で金額ベースの使用制限とメールアラートが設定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/f96bf7e9-1cb1-178a-d08c-a5a712537090.png)

今回のプロジェクトでは `１リクエストあたり約￥2.2円` でした。
仮に 10,000 円分使うには、4545 回リクエストを送ると消費する計算になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67192/8328ee26-e7a8-497b-00ca-68dade1c04d4.png)


## ４. おわりに

ここまで読んでいただきありがとうございます！

この記事は Qiita の [クソアプリ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/kuso-app) の１日目の記事としてアプリ作成から記事執筆をやらせてもらいました。

今回、Cloudflare R2 と GPT-4V の使用が初めてだったので大変勉強になりましたし、何よりも楽しかった！良ければ遊んでみてください～。

https://zessan.net/