OpenAI 画像生成API チュートリアル gpt-4-vision編(3/4)

# Table of Contents
- [Table of Contents](#table-of-contents)
- [🤖 **OpenAI Image Generation API Tutorial: OpenAIの画像生成APIチュートリアル**](#-openai-image-generation-api-tutorial-openaiの画像生成apiチュートリアル)
  - [📌 **Project Overview: プロジェクトの概要**](#-project-overview-プロジェクトの概要)
  - [🚀 **Implementation with gpt-4-vision-preview: gpt-4-vision-previewを利用した実装**](#-implementation-with-gpt-4-vision-preview-gpt-4-vision-previewを利用した実装)
    - [📁 **Folder Structure: フォルダ構成**](#-folder-structure-フォルダ構成)
    - [📝 **Implementation: 実装**](#-implementation-実装)
      - [1. 第二回で作成したフォルダに移動して、VSCodeで開きます。](#1-第二回で作成したフォルダに移動してvscodeで開きます)
      - [2. まず、index.htmlにURL入力項目を追加します。また、gpt-4-visionで生成される画像の説明を表示するためのdiv要素を追加します。](#2-まずindexhtmlにurl入力項目を追加しますまたgpt-4-visionで生成される画像の説明を表示するためのdiv要素を追加します)
      - [3. 次に、script.jsを編集して、idを取得します。また、プロンプトの入力フィールドのイベントリスナーの設定。submitボタンのイベントリスナーの編集を行う。](#3-次にscriptjsを編集してidを取得しますまたプロンプトの入力フィールドのイベントリスナーの設定submitボタンのイベントリスナーの編集を行う)
      - [4. 次に、generateVision関数の実装を行います。](#4-次にgeneratevision関数の実装を行います)
      - [5. 次に、server.jsを変更して、gpt-4-vision-previewの実装を行います。](#5-次にserverjsを変更してgpt-4-vision-previewの実装を行います)
      - [6. 最後に、サーバーを起動します。](#6-最後にサーバーを起動します)
      - [7. index.htmlをブラウザで開きます。](#7-indexhtmlをブラウザで開きます)
  - [🖥️ **Complete Code Overview: 今回のコード全体**](#️-complete-code-overview-今回のコード全体)
  - [📝 **Conclusion: まとめ**](#-conclusion-まとめ)

# 🤖 **OpenAI Image Generation API Tutorial: OpenAIの画像生成APIチュートリアル**
第一回では、環境構築を行いました。
第二回では、画像生成(DALL-E 3)の実装を行いました。
第三回では、画像分析(gpt-4-vision)の実装を行います。
第四回では、おまけとして画像編集と画像複製(DALL-E2)を行います。

今回は第三回ということで、画像分析(gpt-4-vision)の実装を行います。

## 📌 **Project Overview: プロジェクトの概要**
- **第三回 - gpt-4-visionによる画像分析Webアプリケーション作成:**
  - ユーザーが画像URLを入力
  - 入力された画像に基づいてgpt-4-visionが画像を分析

他の詳細については第一回を参照してください。

## 🚀 **Implementation with gpt-4-vision-preview: gpt-4-vision-previewを利用した実装**
今回の実装では、gpt-4-vision-previewを使用して画像分析を行います。

### 📁 **Folder Structure: フォルダ構成**
このプロジェクトのフォルダ構成は最終的に以下の通りになります：
```
.
├── node_modules/
│   └── ... # 依存関係のモジュール
├── index.html # エントリポイントHTMLファイル
├── package-lock.json # 自動生成される依存関係の正確なバージョンを記録するファイル
├── package.json # プロジェクトのメタデータと依存関係を定義するファイル
├── script.js # クライアントサイド JavaScript
├── server.js # Node.jsサーバー
└── style.css # CSSスタイルシート
```

### 📝 **Implementation: 実装**
このチュートリアルでは、第一回、第二回の続きとして実装していきます。
もしくは、後述するgithubのリポジトリをクローンして実装を確認してください。

#### 1. 第二回で作成したフォルダに移動して、VSCodeで開きます。

  ```powershell
  cd dalle3-tutorial
  code .
  ```

#### 2. まず、index.htmlにURL入力項目を追加します。また、gpt-4-visionで生成される画像の説明を表示するためのdiv要素を追加します。
   
  `index.html`の前回までのコードは以下の通りです。
  ```html
  <form>
    <input type="text" placeholder="Enter a prompt" />
    <button type="submit">Generate</button>
  </form>
  ```

  - **URL入力項目**: ユーザーが画像のURLを入力できるようにするために、新しいテキスト入力フィールドを追加します。
  
  - **ID属性の追加**: JavaScriptで容易に操作できるように、各入力エリアに一意のIDを付与します。
  
  - **結果表示用div**: gpt-4-visionから得られる結果（画像の説明）を表示するための場所として、新しいdiv要素を設けます。

  更新されたHTMLフォームは次のようになります：

  ```html
  <form>
    <input type="text" id="prompt-input" placeholder="Enter a prompt" />
    <!-- URL入力エリア -->
    <input type="text" id="url-input" placeholder="Enter a URL" />
    <button type="submit">Generate</button>
  </form>

  <div id="vision-container"></div>
  ```

- `id="prompt-input"`: プロンプト用の入力フィールドを特定します。
- `id="url-input"`: 新しく追加されたURL入力フィールドを特定します。
- `id="vision-container"`: gpt-4-visionによって生成された画像の説明文を表示するための空のdivです。

#### 3. 次に、script.jsを編集して、idを取得します。また、プロンプトの入力フィールドのイベントリスナーの設定。submitボタンのイベントリスナーの編集を行う。

  `script.js`の前回までのコードは以下の通りです。
  ```javascript
  const form = document.querySelector('form');
  const input = document.querySelector('input');
  const imageContainer = document.getElementById('image-container');

  form.addEventListener('submit', (event) => {
    event.preventDefault();
    const prompt = input.value;
    input.value = '';
    generateImage(prompt);
  });
  ```

  - **IDによる要素の取得**: `id`を使用してフォームと入力フィールドを特定し、操作しやすくします。

  - **インタラクティブな入力フィールド**: 一方の入力フィールドに文字がある場合、他方を無効化することでユーザーの混乱を防ぎます。

  - **submitイベントのカスタマイズ**: フォームが提出された際に、どちらかの入力がある場合はその情報を使用し、両方の入力フィールドをクリアして次の操作を待ち受けます。

  更新されたコードは次のようになります：

  ```javascript
  const form = document.querySelector('form');
  const promptInput = document.getElementById('prompt-input');
  const urlInput = document.getElementById('url-input');
  const imageContainer = document.getElementById('image-container');
  const visionContainer = document.getElementById('vision-container');

  // プロンプト入力フィールドのイベントリスナー
  promptInput.addEventListener('input', () => {
    if (promptInput.value !== '') {
      urlInput.disabled = true;  // URL入力を無効化
    } else {
      urlInput.disabled = false; // URL入力を有効化
    }
  });

  // URL入力フィールドのイベントリスナー
  urlInput.addEventListener('input', () => {
    if (urlInput.value !== '') {
      promptInput.disabled = true;  // プロンプト入力を無効化
    } else {
      promptInput.disabled = false; // プロンプト入力を有効化
    }
  });

  form.addEventListener('submit', (event) => {
    event.preventDefault();

    const promptValue = promptInput.value;
    const urlValue = urlInput.value;

    // 入力によって、関数を変更
    if (promptValue) {
      generateImage(promptValue);
    } else if (urlValue) {
      generateVision(urlValue);
    }

    // 入力フィールドを再度有効化
    promptInput.disabled = false;
    urlInput.disabled = false;

    // 入力フィールドをクリア
    promptInput.value = '';
    urlInput.value = '';
  });
  ```

  一気にコードが増えましたが、大丈夫です。一つずつ見ていきましょう。

  - プロンプト入力フィールドに何か入力されているときは、URL入力フィールドを無効化します。
  - URL入力フィールドに何か入力されているときは、プロンプト入力フィールドを無効化します。

  この変更で、ユーザーは一度にひとつのオプションしか使えないようにしています。

  ```javascript
  promptInput.addEventListener('input', () => {
    if (promptInput.value !== '') {
      urlInput.disabled = true;  // URL入力を無効化
    } else {
      urlInput.disabled = false; // URL入力を有効化
    }
  });
  ```

  - 関数`generateImage`または`generateVision`は、フォーム送信時に適切なAPIエンドポイントにリクエストを送信します。
  - 各入力フィールドは、操作後に再度利用可能になり、以前の入力値はクリアされます。
  
  ```javascript
  form.addEventListener('submit', (event) => {
    event.preventDefault();

    const promptValue = promptInput.value;
    const urlValue = urlInput.value;

    // 入力によって、関数を変更
    if (promptValue) {
      generateImage(promptValue);
    } else if (urlValue) {
      generateVision(urlValue);
    }

    // 入力フィールドを再度有効化
    promptInput.disabled = false;
    urlInput.disabled = false;

    // 入力フィールドをクリア
    promptInput.value = '';
    urlInput.value = '';
  });
  ```

  これは、submitボタンが押された時に、入力によって関数を変更しています。
  また、入力フィールドを再度有効化し、入力フィールドをクリアしています。

#### 4. 次に、generateVision関数の実装を行います。

  フロントエンドにおいて、ユーザーが提供したURLから画像の説明を生成するための`generateVision`関数を実装します。

  ```javascript
  async function generateVision(url) {
    try {
      const response = await fetch('http://localhost:3000/generateVision', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url: url })
      });

      if (!response.ok) {
        throw new Error(errorMessage);
      }

      const data = await response.json();
      if (data.message.content) {
        const visionElement = document.createElement('p');
        visionElement.textContent = data.message.content;
        visionContainer.appendChild(visionElement);
      } else {
        throw new Error('説明文がレスポンスに含まれていません。');
      }

    } catch (error) {
      console.error('説明文の生成に失敗しました。', error);
    }
  }
  ```

  1. **非同期通信**: `fetch` APIを使って、指定されたエンドポイントに対して非同期のPOSTリクエストを行います。
  2. **エラーハンドリング**: レスポンスが正常でない場合や、期待されるデータが含まれていない場合には、例外を投げてエラーを通知します。
  3. **結果の表示**: 正常な応答が返ってきた場合、その内容を解析して新しいp要素に挿入し、`visionContainer`に追加します。

#### 5. 次に、server.jsを変更して、gpt-4-vision-previewの実装を行います。

  ```javascript
  app.post('/generateVision', async (req, res) => {
    try {
      const url = req.body.url;
      const response = await openai.chat.completions.create({
        model: "gpt-4-vision-preview",
        messages: [
          {
            role: "user",
            content: [
              { type: "text", text: "What’s in this image?" },
              { type: "image_url", image_url: { "url": url } },
            ],
          },
        ],
      });
      res.send(response.choices[0]);
    } catch (error) {
      res.status(500).send({
        error: '説明文の生成に失敗しました。',
        details: error.message
      });
    }
  });
  ```

  1. **POSTリクエスト**: `/generateVision`エンドポイントは、クライアントからのPOSTリクエストを受け取ります。
  2. **APIへの問い合わせ**: `openai.chat.completions.create`メソッドを使用して、GPT-4 Visionモデルにクエリを行います。この際、画像の内容についての質問と、対象となる画像のURLがリクエストに含まれます。
  3. **レスポンスの処理**: 正常に応答が得られた場合、その内容をクライアントに送り返します。
  4. **エラーハンドリング**: 何らかの問題が発生した場合は、適切なHTTPステータスコードとともにエラーメッセージを返すことで、クライアント側に通知します。

#### 6. 最後に、サーバーを起動します。

  ```powershell
  node server.js
  ```

  package.jsonを変更していれば、以下のコマンドでもサーバーを起動できます。

  ```powershell
  npm start
  ```

  これで、サーバーが起動しました。

#### 7. index.htmlをブラウザで開きます。

  これで、画像分析(gpt-4-vision)の実装が完了しました。

  1. **画像のURLをコピー**: 生成された画像を右クリックして、「画像のURLをコピーする」を選択するか、「新しいタブで開く」を選んでアドレスバーからURLをコピーします。
  2. **Webアプリケーションでの入力**: `index.html`ファイルをブラウザで開き、先ほどコピーした画像のURLを適切な入力フィールドに貼り付けます。
  3. **説明文の生成**: フォームを送信すると、サーバーがgpt-4-vision APIに問い合わせを行い、画像の説明文を取得します。その後、この説明文がWebページ上に表示されます。

  「サンクロースの服を着た犬と猫」というプロンプトで作成した以下の画像に対して、gpt-4-visionで生成された画像の説明を表示するようにしています。

  ![img-jINHn1C6AZ2ZDyKaevN88aiP (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538434/9035d821-6e71-184f-9b27-21a57b7607e8.png)

  ![スクリーンショット 2023-11-19 154254.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538434/fb295b40-c96f-3c55-231f-f3fbdfdeabd5.png)

  何度も入力すると、以下のように様々な説明が生成されます。

  ![スクリーンショット 2023-11-19 155358.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538434/56c3375f-f798-4993-312a-cd941ff2f6a2.png)

  また、更に出力されたプロンプトを用いて生成を行うと以下のような結果になりました。

  This image shows a cat and a dog dressed in Christmas-themed costumes. The cat
  (この画像には、クリスマステーマの衣装を着た猫と犬が写っています。)

  ![img-zKrQJflt5Wr2aNSjgLqSenV4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538434/f2fe9bc6-bcc5-459e-b396-3e0e871607f8.png)

  In the image, there is a cat and a dog dressed in festive Santa Claus
  (画像には、お祝いムード溢れるサンタクロース服を着た猫と犬がいます。)

  ![img-97xWrzaF44sCb4Ops0Fm3hm6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538434/692f1fe8-ad81-14b9-a0c8-f484eea76538.png)

  This image shows a cat and a dog dressed in festive holiday attire, specifically Santa
  (この画像は、特にサンタの衣装を身にまとった、お祝い気分のホリデーコスチュームを着た猫と犬を示しています。)

  ![img-uxzclFfHJul4dL494uHWkjPC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3538434/8fd1e28a-3722-db6b-f53e-66c5a010aa25.png)

  また、説明文の生成において日本語を希望する場合は、gpt-4-vision APIに対するリクエストの`messages`の中の`text`フィールドに「Describe it in Japanese.」等の指示や、直接「この画像を説明してください。」みたいな感じでプログラムを修正すると日本語で返答が返ってきます。

  ```javascript
  const response = await openai.chat.completions.create({
    model: "gpt-4-vision-preview",
    messages: [
      {
        role: "user",
        content: [
          { type: "text", text: "What’s in this image? Describe it in Japanese." },
          { type: "image_url", image_url: { "url": url } },
        ],
      },
    ],
  });
  ```

## 🖥️ **Complete Code Overview: 今回のコード全体**

以下は、今回のコード全体です。

`index.html`ファイル
```html
<!DOCTYPE html>
<html lang="jp">

<head>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="style.css" />
  <title>DALL-E 3 Tutorial</title>
</head>

<body>

  <h1>DALL-E 3 Tutorial</h1>

  <form>
    <input type="text" id="prompt-input" placeholder="Enter a prompt" />
    <!-- URL入力エリア -->
    <input type="text" id="url-input" placeholder="Enter a URL" />
    <button type="submit">Generate</button>
  </form>

  <div id="vision-container"></div>

  <div id="image-container"></div>

  <script src="script.js"></script>

</body>

</html>
```

```javascript
const form = document.querySelector('form');
const promptInput = document.getElementById('prompt-input');
const urlInput = document.getElementById('url-input');
const imageContainer = document.getElementById('image-container');
const visionContainer = document.getElementById('vision-container');

// プロンプト入力フィールドのイベントリスナー
promptInput.addEventListener('input', () => {
  if (promptInput.value !== '') {
    urlInput.disabled = true;  // URL入力を無効化
  } else {
    urlInput.disabled = false; // URL入力を有効化
  }
});

// URL入力フィールドのイベントリスナー
urlInput.addEventListener('input', () => {
  if (urlInput.value !== '') {
    promptInput.disabled = true;  // プロンプト入力を無効化
  } else {
    promptInput.disabled = false; // プロンプト入力を有効化
  }
});

form.addEventListener('submit', (event) => {
  event.preventDefault();

  const promptValue = promptInput.value;
  const urlValue = urlInput.value;

  // 入力によって、関数を変更
  if (promptValue) {
    generateImage(promptValue);
  } else if (urlValue) {
    generateVision(urlValue);
  }

  // 入力フィールドを再度有効化
  promptInput.disabled = false;
  urlInput.disabled = false;

  // 入力フィールドをクリア
  promptInput.value = '';
  urlInput.value = '';
});

async function generateImage(prompt) {
  try {
    const response = await fetch('http://localhost:3000/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt: prompt })
    });

    if (!response.ok) {
      throw new Error(errorMessage);
    }

    const data = await response.json();
    if (data.image) {
      const imageElement = document.createElement('img');
      imageElement.src = data.image;
      imageContainer.appendChild(imageElement);
    } else {
      throw new Error('画像URLがレスポンスに含まれていません。');
    }

  } catch (error) {
    console.error('画像の生成に失敗しました。', error);
  }
}

async function generateVision(url) {
  try {
    const response = await fetch('http://localhost:3000/generateVision', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ url: url })
    });

    if (!response.ok) {
      throw new Error(errorMessage);
    }

    const data = await response.json();
    if (data.message.content) {
      const visionElement = document.createElement('p');
      visionElement.textContent = data.message.content;
      visionContainer.appendChild(visionElement);
    } else {
      throw new Error('説明文がレスポンスに含まれていません。');
    }

  } catch (error) {
    console.error('説明文の生成に失敗しました。', error);
  }
}
```

```javascript
const OpenAI = require('openai');
const cors = require('cors');
const express = require('express');

// デフォルトでOpenAI APIキーは環境変数から自動取得されます
const openai = new OpenAI();
const app = express();
const port = 3000;

app.use(cors());
app.use(express.json());

app.get('/', (req, res) => {
  res.send('Server is Running!');
});

app.post('/generate', async (req, res) => {
  try {
    const prompt = req.body.prompt;
    const imageResponse = await openai.images.generate({
      model: "dall-e-3",
      prompt: prompt,
      n: 1,
      size: "1024x1024",
    });

    const imageUrl = imageResponse.data[0].url;
    if (!imageUrl) {
      return res.status(500).send({ error: '画像の生成に失敗しました。' });
    }

    // 成功した場合、画像URLを含むオブジェクトを返す
    res.status(200).json({ image: imageUrl });
  } catch (error) {
    res.status(500).send({
      error: '画像の生成に失敗しました。',
      details: error.message
    });
  }
});

app.post('/generateVision', async (req, res) => {
  try {
    const url = req.body.url;
    const response = await openai.chat.completions.create({
      model: "gpt-4-vision-preview",
      messages: [
        {
          role: "user",
          content: [
            { type: "text", text: "What’s in this image?" },
            { type: "image_url", image_url: { "url": url } },
          ],
        },
      ],
    });
    res.send(response.choices[0]);
  } catch (error) {
    res.status(500).send({
      error: '説明文の生成に失敗しました。',
      details: error.message
    });
  }
});

app.listen(port, () => console.log('Listening on port', port));
```

以下はGitHubのリポジトリです。

https://github.com/yuyuyu2118/dalle3-tutorial-gpt4vision

以下のコマンドでクローンできます。

```powershell
git clone https://github.com/yuyuyu2118/dalle3-tutorial-gpt4vision
```

## 📝 **Conclusion: まとめ**
お疲れ様でした！以上で、OpenAIの画像生成APIチュートリアルの第三回は終了です。
この記事では、画像分析(gpt-4-vision)の実装を行いました。
次回は、おまけとして画像編集と画像複製(DALL-E2)を行います。