技術革新が絶えず巻き起こる昨今。テクノロジーの躍動はとどまることを知らず、中でも人工知能(AI)の可能性には驚かされるばかりです。

AIとは、言うなれば、コンピューターシステムを用いた人間の知能の再現。具体的には、学習、推論、問題解決、知覚、言語理解、意思決定などがこれに該当します。

今日、個人であるか企業であるかを問わず、多くの分野で人間を凌駕するタスク実行AIモデルの開発や訓練が盛んに行われています。AIの無数にある活用例の中でも、特に興味をそそられる分野のひとつが、AIを利用した画像生成ではないでしょうか。

今回構築するもの

こちらの記事では、Node.jsバックエンドを介してOpenAI DALL-E APIとシームレスに統合し、テキストプロンプトに基づいて魅力的な画像を生成するReactアプリケーションの構築方法をご紹介します。

AI image generator in action, producing vivid and creative images using DALL-E API
DALL-E APIを使用してクリエイティブな画像を生成するAI画像ジェネレータの動作の様子

前提条件

このプロジェクトを進めるには、以下のものが必要です。

OpenAI DALL-E APIとは

OpenAI APIはクラウドベースのプラットフォームで、DALL-EやGPT-3といったOpenAIのAIモデルを開発者に提供しています(以前、このモデルを使用しこちらのGitリポジトリにあるコードでChatGPTクローンを構築しました)。これにより、モデルを自力で開発・訓練することなく、要約、翻訳、画像生成、修正などのAI機能をプログラムに追加できます。

OpenAI APIを使用するには、OpenAIのウェブサイトでGoogleアカウントまたはメールアドレスを使用してアカウントを作成し、APIキーを取得します。APIキーを生成するには、ウェブサイトの右上にある「Personal」をクリックし、「View API keys」を選択します。

 The process of creating an OpenAI API secret key
OpenAI APIシークレットキーの作成手順

Create new secret key」ボタンをクリックし、キーを保存します。このアプリケーションでは、このキーを使ってOpenAIのDALL-E APIとやり取りを行います。

開発環境のセットアップ

ゼロからReactアプリケーションを作成し、独自のインターフェースを開発することも、以下の手順に従ってGitスターターテンプレートを利用することもできます。

  1. このプロジェクトのGitHubリポジトリにアクセス
  2. Use this template」>「Create a new repository」を選択して、スターターコードを GitHubアカウント内のリポジトリにコピー(「Include all branches」を選択)
  3. リポジトリをローカルコンピュータにプルし、次のコマンドを使ってstarter-filesブランチに切り替える:git checkout starter-files
  1. コマンドnpm installを実行して、必要な依存関係をインストールする

インストールが完了したら、npm run startを使ってローカルコンピュータ上でプロジェクトを起動することができます。これにより、プロジェクトがhttp://localhost:3000/でアクセス可能になります。

User interface of an AI image generator application showcasing the power of artificial intelligence in image creation
画像作成における人工知能の力を紹介するAI画像生成アプリケーションのユーザーインターフェース

プロジェクトファイルを理解する

このプロジェクトでは、Reactアプリケーションに必要な依存関係をすべて追加しました。インストールしたものの概要を以下にご紹介します。

  • file-server:このユーティリティライブラリにより、生成された画像をダウンロードするプロセスが簡素化できます。ダウンロードボタンにリンクすることで、スムーズなユーザー体験が確保されます。
  • uuid:このライブラリは各画像に一意の識別子を割り当てるものです。これにより、画像が同じデフォルトファイル名を共有する可能性を排除できます。
  • react-icons:プロジェクトに統合されるライブラリで、アイコンを簡単に組み込むことが可能になります。アプリケーションの視覚的な魅力が高まります。

Reactアプリケーションの中核には、srcフォルダがあります。ここには、Webpackに不可欠なJavaScriptコードが格納されます。srcフォルダ内のファイルとフォルダを簡単に理解しておきましょう。以下をご覧ください。

  • assets:このディレクトリには、プロジェクト全体で使用される画像や読み込み中であることを示すGIFが保存されます。
  • data:このフォルダには、30個のプロンプトの配列をエクスポートするindex.jsファイルがあります。これらのプロンプトは、多様でランダムな画像を生成するのに使用します。必要に応じて自由に編集してください。
  • index.css:プロジェクトで使用するスタイルがここに保存されます。

Utilsフォルダの中身を理解する

このフォルダの中のindex.jsファイルで2つの再利用可能な関数を定義しています。最初の関数は、さまざまな画像を説明するプロンプトの選択をランダム化するものです。

import { randomPrompts } from '../data';

export const getRandomPrompt = () => {
    const randomIndex = Math.floor(Math.random() * randomPrompts.length);
    const randomPrompt = randomPrompts[randomIndex];

    return randomPrompt;
}

2番目の関数は、file-saverの依存関係を利用して、生成された画像のダウンロードを処理します。両方の関数はモジュール性と効率性を確保するもので、必要に応じてコンポーネントにインポートすることができます。

import FileSaver from 'file-saver';
import { v4 as uuidv4 } from 'uuid';

export async function downloadImage(photo) {
    const _id = uuidv4();
    FileSaver.saveAs(photo, `download-${_id}.jpg`);
}

上のコードでは、uuid依存関係が生成された各画像ファイルに一意のIDを与えているので、同じファイル名を持つことはありません。

コンポーネントを理解する

コンポーネントは、コードの保守や理解を容易にする、小さなコードブロックです。このプロジェクトでは、Header.jsxFooter.jsxForm.jsxという3つのコンポーネントを作成します。主なコンポーネントはFormコンポーネントで、ここで入力情報を受け取り、「Generate Image」ボタンのonClickイベントとして追加されたgenerateImage関数とともにApp.jsxファイルに渡されます。

Formコンポーネントでは、プロンプトを保存して更新するためのstateを用意します。さらに、アイコンをクリックしてランダムなプロンプトを生成する機能もあります。これは、handleRandomPrompt関数によって可能になります。getRandomPrompt関数は、すでに設定されています。アイコンをクリックすると、ランダムなプロンプトが取得され、そのプロンプトでstateが更新される仕組みです。

const handleRandomPrompt = () => {
    const randomPrompt = getRandomPrompt();
    setPrompt(randomPrompt)
}

App.jsxファイルを理解する

ほとんどのコンポーネントがここに集約されます。また、生成した画像を表示する指定領域もあります。画像生成前には、プレースホルダー画像(プレビュー画像)が表示されます。

このファイルの中では、2つのstateが管理されることになります。

  • isGenerating:画像が生成されているかどうかを随時追跡(デフォルトではfalseに設定されています)
  • generatedImage:このstateには、生成された画像に関する情報が保存される

さらに、downloadImageユーティリティ関数がインポートされることで、「Download」ボタンをクリックしたときに生成した画像のダウンロードをトリガーできます。

<button
    className="btn"
    onClick={() => downloadImage(generatedImage.photo)}
>

ここまでで、スターターファイルを理解し、プロジェクトをセットアップしました。続いては、このアプリケーションのロジックを扱っていきましょう。

OpenAIのDALL-E APIで画像を生成する

OpenAIのDALL-E APIの機能を利用するために、Node.jsを使ってサーバーを構築し、サーバー内にPOSTルートを作成します。このルートは、Reactアプリケーションから送信されたプロンプトテキストを受信し、画像を生成するために利用します。

以下のコマンドを実行して、プロジェクトディレクトリに必要な依存関係をインストールしましょう。

npm i express cors openai

さらに、以下をdevの依存関係としてインストールしてください。ここで用意する各種ツールが、Node.jsサーバーのセットアップを支援してくれます。

npm i -D dotenv nodemon

インストールする依存関係について簡単にご説明します。

  • express:Node.jsでサーバーを構築するのに便利なライブラリ
  • cors:ドメイン間の安全な通信を容易にする
  • openai:この依存関係が、OpenAIのDALL-E APIへのアクセスを許可
  • dotenv:環境変数の管理をサポート
  • nodemon:ファイルの変更を監視し、サーバーを自動で再起動する開発ツール

インストールが成功したら、プロジェクトのルートにserver.jsファイルを作成します。ここに全てのサーバーコードが保存されます。

server.jsファイルの中で、先ほどインストールしたライブラリをインポートし、インスタンス化してください。

// Import the necessary libraries
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const OpenAI = require('openai');

// Create an instance of the Express application
const app = express();

// Enable Cross-Origin Resource Sharing (CORS)
app.use(cors());

// Configure Express to parse JSON data and set a data limit
app.use(express.json({ limit: '50mb' }));

// Create an instance of the OpenAI class and provide your API key
const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY,
});

// Define a function to start the server
const startServer = async () => {
    app.listen(8080, () => console.log('Server started on port 8080'));
};

// Call the startServer function to begin listening on the specified port
startServer();

上記のコードでは、必要なライブラリをインポートしています。次に、const app = express();を使用してExpressアプリケーションのインスタンスを確立します。その後、CORSを有効にします。次に、受信するJSONデータを処理するようにExpressを構成し、データサイズの上限を50mbに指定します。

続いて、OpenAI APIキーを使用してOpenAIクラスのインスタンスが作成されます。プロジェクトのルートに.envファイルを作成、OPENAI_API_KEY変数を使用してAPIキーを追加します。最後に、非同期のstartServer関数を定義し、それを呼び出してサーバーを動かします。

これでserver.jsファイルの設定は完了です。このサーバーとやり取りするために、今度はReactアプリケーションで使用できるPOSTルートを作成しましょう。

app.post('/api', async (req, res) => {
    try {
        const { prompt } = req.body;
        const response = await openai.images.generate({
            prompt,
            n: 1,
            size: '1024x1024',
            response_format: 'b64_json',
        });
        const image = response.data[0].b64_json;
        res.status(200).json({ photo: image });
    } catch (error) {
        console.error(error);
    }
});

このコードでは、ルートは/apiに設定され、POSTリクエストを処理するようになっています。ルートのコールバック関数の内部で、req.bodyを使用してReactアプリから送信されたデータ、具体的にはpromptの値を受け取ります。

その後、OpenAIライブラリのimages.generateメソッドが呼び出されます。このメソッドは、提供されたプロンプトを受け取り、レスポンスとして画像を生成します。nのようなパラメータは、生成する画像の数を決定し(ここでは1つだけ)、sizeは画像の寸法を指定し、response_format はレスポンス提供の形式を示します(この場合はb64_json)。

画像を生成したら、レスポンスから画像データを抽出し、image変数に格納します。次に、生成された画像データを含むJSONレスポンスをReactアプリに送り返し、res.status(200).json({ photo: image })を使用してHTTPステータスを200(成功を示す)に設定します。

この処理中にエラーが発生した場合は、catchブロック内のコードが実行され、デバッグのためにコンソールにエラーが記録されます。

これでサーバーの準備は整いました。package.jsonファイルのscriptsオブジェクトに、サーバーの実行に使用するコマンドを指定してみましょう。

"scripts": {
  "dev:frontend": "react-scripts start",
  "dev:backend": "nodemon server.js",
  "build": "react-scripts build",
},

npm run dev:backendを実行すると、http://localhost:8080/でサーバーが起動し、npm run dev:frontendを実行すると、http://localhost:3000/ でReactアプリケーションが起動します。両方が異なるターミナルで実行されていることを確認してください。

ReactからNode.jsサーバーへのHTTPリクエスト

App.jsxファイルに、Form.jsxコンポーネントのGenerate ImageボタンがクリックされたときにトリガーされるgenerateImage関数を作成します。この関数は、Form.jsxコンポーネントからpromptsetPromptの2つのパラメータを受け取ります。

generateImage関数で、Node.js サーバーにHTTP POSTリクエストを行います。

const generateImage = async (prompt, setPrompt) => {
    if (prompt) {
        try {
            setIsGenerating(true);
            const response = await fetch(
                'http://localhost:8080/api',
                {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        prompt,
                    }),
                }
            );
            const data = await response.json();
            setGeneratedImage({
                photo: `data:image/jpeg;base64,${data.photo}`,
                altText: prompt,
            });
        } catch (err) {
            alert(err);
        } finally {
            setPrompt('');
            setIsGenerating(false);
        }
    } else {
        alert('Please provide proper prompt');
    }
};

上のコードでは、promptパラメータに値があるかどうかをチェックし、isGeneratingの状態をtrueに設定しています。App.jsxファイルには、読み込み中の表示を制御するコードがあるので、これで読み込み中であることが画面に表示されます。

{isGenerating && (
    <div> className="loader-comp">
        <img src={Loader} alt="" className='loader-img' />
    </div>
)}

次に、fetch()メソッドを使用して、http://localhost:8080/apiを使用してサーバーにPOSTリクエストを行いましょう。このために(別のURL上のAPIとやり取りするために)CORSをインストールしています。プロンプトをメッセージの本文として使用します。次に、Node.jsサーバーから返されたレスポンスを抽出し、generatedImage stateに設定します。

generatedImage stateに値が設定されると、画像が表示されます。

{generatedImage.photo ? (
    <img
        src={generatedImage.photo}
        alt={generatedImage.altText}
        className="imgg ai-img"
    />
) : (
    <img
        src={preview}
        alt="preview"
        className="imgg preview-img"
    />
)}

このようにして、App.jsxファイルが完成します。

import { Form, Footer, Header } from './components';
import preview from './assets/preview.png';
import Loader from './assets/loader-3.gif'
import { downloadImage } from './utils';
import { useState } from 'react';

const App = () => {
    const [isGenerating, setIsGenerating] = useState(false);
    const [generatedImage, setGeneratedImage] = useState({
        photo: null,
        altText: null,
    });

    const generateImage = async (prompt, setPrompt) => {
        if (prompt) {
            try {
                setIsGenerating(true);
                const response = await fetch(
                    'http://localhost:8080/api',
                    {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                        },
                        body: JSON.stringify({
                            prompt,
                        }),
                    }
                );
                const data = await response.json();
                setGeneratedImage({
                    photo: `data:image/jpeg;base64,${data.photo}`,
                    altText: prompt,
                });
            } catch (err) {
                alert(err);
            } finally {
                setPrompt('');
                setIsGenerating(false);
            }
        } else {
            alert('Please provide proper prompt');
        }
    };

    return (
        <div className='container'>
            <Header />
            <main className="flex-container">
                <Form generateImage={generateImage} prompt={prompt} />
                <div className="image-container">
                    {generatedImage.photo ? (
                        <img
                            src={generatedImage.photo}
                            alt={generatedImage.altText}
                            className="imgg ai-img"
                        />
                    ) : (
                        <img
                            src={preview}
                            alt="preview"
                            className="imgg preview-img"
                        />
                    )}
                    {isGenerating && (
                        <div className="loader-comp">
                            <img src={Loader} alt="" className='loader-img' />
                        </div>
                    )}
                    <button
                        className="btn"
                        onClick={() => downloadImage(generatedImage.photo)}
                    >
                        Download
                    </button>
                </div>
            </main>
            <Footer />
        </div>
    );
};

export default App;

フルスタックアプリケーションをKinstaにデプロイする

ここまでで、Node.jsとやり取りするReactアプリケーションのビルドが完了しました。それでは、このアプリケーションをKinstaにデプロイしてみましょう。

まず、Reactアプリケーションのビルドプロセスにより生成された静的ファイルを配信できるよう、サーバーの設定を行います。具体的には、pathモジュールをインポートし、これを使って静的ファイルを配信できます。

const path = require('path');

app.use(express.static(path.resolve(__dirname, './build')));

コマンドnpm run build && npm run dev:backendを実行すると、フルスタックのReactアプリケーションが http://localhost:8080/ で読み込まれます。これは、Reactアプリケーションがbuildフォルダ内の静的ファイルにコンパイルされるためです。このファイルは、静的ディレクトリとしてNode.jsサーバーで組み込まれます。その結果、Nodeサーバーを実行すると、アプリケーションにアクセスできるようになります。

コードを任意のGitサービス(BitbucketGitHub、またはGitLab)にデプロイする前に、App.jsxファイルのHTTPリクエストURLを変更することをお忘れなく。http://localhost:8080/api/apiに変更します。

最後に、package.jsonファイルに、デプロイに使用するNode.jsサーバー用のスクリプトコマンドを追加します。

"scripts": {
  // …
  "start": "node server.js",
},

次に、お好みのGitサービスにコードをプッシュし、以下の手順でKinstaにリポジトリをデプロイします。

  1. MyKinstaのKinstaアカウントにログイン。
  2. 左側のサイドバーで「アプリケーション」を選択し、「アプリケーションを追加」ボタンをクリック
  3. 表示されたポップアップで、デプロイしたいリポジトリを選択(複数のブランチがある場合は、必要なブランチを選択し、アプリケーションに名前を付けることができます)
  4. 利用可能なデータセンターから1つ選択
  5. 環境変数としてOPENAI_API_KEYを追加(Kinstaが自動でDockerfileをセットアップ)
  6. 最後に、startコマンドフィールドに、npm run build && npm run startを追加(Kinstaのシステムが、「package.json」からアプリケーションの依存関係をインストールし、アプリケーションをビルドしてデプロイします)

まとめ

今回の記事では、OpenAIのDALL-E APIを画像生成に利用する方法をご紹介しました。また、ReactとNode.jsを使って基本的なフルスタックアプリケーションを構築する方法もご理解いただけたと思います。

毎日のように新しいモデルが導入され、Kinstaのアプリケーションホスティングにデプロイできるプロジェクトの種類は多岐にわたります。AIの可能性は無限大です。

皆さんはどんなモデルのリリースを待ちわびていますか?また、次はどんなプロジェクトについての解説をご希望ですか?以下のコメント欄でお聞かせください。

Joel Olawanle Kinsta

Kinstaでテクニカルエディターとして働くフロントエンド開発者。オープンソースをこよなく愛する講師でもあり、JavaScriptとそのフレームワークを中心に200件以上の技術記事を執筆している。