Flutter + NCMB + OpenAIで画像生成アプリを作る（その2: OpenAI APIの実行）

NCMBでは公式SDKとしてSwift/Objective-C/Kotlin/Java/Unity/JavaScript SDKを用意しています。また、それ以外にもコミュニティSDKとして、非公式ながらFlutter/React Native/Google Apps Script/C#/Ruby/Python/PHPなど幅広い言語向けにSDKが開発されています。

今回はコミュニティSDKの一つ、Flutter/Dart SDKを使ってOpenAIの画像生成APIを用いたAI画像生成アプリを作ってみます。[前回は画面の仕様とSDKの初期化について解説しました](https://qiita.com/goofmint/items/b078f7a71b50a03dbfba)ので、今回は画像生成に関わるスクリプトの処理について解説します。

## コード

今回のコードは [NCMBMania/flutter-image-gen](https://github.com/NCMBMania/flutter-image-gen) にアップロードしてあります。実装時の参考にしてください。

## スクリプトについて

スクリプトというのはNCMBの一機能になります。サーバー側にプログラミングコードを置いて、それをクライアントから呼び出せます。クライアントでは結果だけを受け取れるので、スクリプトのプログラミングコードは非公開にできます。

今回のようにAPIキーを隠蔽したいケース、ロジックを隠蔽したりiOSとAndroidで共通化したいケースなどで利用されます。

## OpenAI APIについて

[OpenAIではNode.js向けにライブラリを提供しています](https://www.npmjs.com/package/openai)が、NCMBのスクリプトでは自由にライブラリが使える訳ではありません。そこで今回はREST APIを直接実行します。

APIドキュメントは[API Reference - OpenAI API](https://platform.openai.com/docs/api-reference/introduction)にあります。今回はユーザーの入力に応じた画像を生成するので[Images](https://platform.openai.com/docs/api-reference/images)を使います。

## スクリプトの実装

### 必要なライブラリの読み込み

今回はHTTPアクセスを行う `superagent` とファイルストアへの保存用に `ncmb` を読み込みます。

```jsx
// superagent と ncmb をインポートする
const request = require('superagent');
const NCMB = require("ncmb");
```

### APIキー・URLの定義

OpenAIのAPIキーやURL、NCMBのアプリケーションキーとクライアントキーを定義します。各種キーは自分のものと書き換えてください。

```jsx
// OpenAI API のエンドポイントと API キーを定義する
const url = 'https://api.openai.com/v1/images/generations';
const apiKey = 'YOUR_OPENAI_API_KEY';

// NCMB のアプリケーションキーとクライアントキーを定義する
const applicationKey = 'YOUR_APPLICATION_KEY';
const clientKey = 'YOUR_CLIENT_KEY';
// NCMBの初期化
const ncmb = new NCMB(applicationKey, clientKey);
```

### リクエスト用の変数を定義

画像作成時に利用する固定パラメーターを定義しておきます。画像サイズは512、1024も指定できますが、画像サイズが大きくなるので注意してください（画像はPNGで作成されるようなので、サイズが大きくなりがちです）。

```jsx
// 生成する画像の数
const n = 1;
// 生成する画像のサイズ
const size = 256;
// レスポンスのフォーマット
const response_format = 'b64_json';
```

### スクリプト処理の呼び出し関数を定義

スクリプト処理では関数を定義し、これをエクスポートします。HTTPアクセスがあると、この関数が呼ばれる仕組みです。

```jsx
// 関数をエクスポートする
module.exports = async function(req, res) {
　　// この中に処理を実装
}
```

以下はこの関数内に記述していきます。

### リクエスト内容を作成

クライアントから画像生成用の文字列を受け取ります。それらを使って、OpenAI APIにアクセスするリクエストデータを作成します。

```jsx
// リクエストデータを作成する
const { prompt } = req.body;
const params = {
  prompt,
  n,
  size: `${size}x${size}`,
  response_format,
};
```

生成用の文字列を `prompt` というキーがあり、後は生成する画像の数やサイズ、レスポンスフォーマットを指定します。

### OpenAI APIにリクエスト

作成したリクエストデータを送信します。

```jsx
// OpenAI API に POST リクエストを送信し、レスポンスを受け取る
const response = await request.post(url)
  .set('Content-Type', 'application/json') // ヘッダーに Content-Type を設定する
  .set('Authorization', `Bearer ${apiKey}`) // ヘッダーに API キーを設定する
  .send(JSON.stringify(params)); // JSON 形式のパラメータを送信する
```

### レスポンスをBufferに変換

レスポンスはBase64エンコードされた文字列なので、Bufferに変換します。

```jsx
// レスポンスは base64 エンコードされているので、デコードする
const base64String = response.body.data[0].b64_json;
// デコードしたデータをバッファに変換する
const buffer = Buffer.from(base64String, "base64");
```

### ファイルストアに保存

受け取ったBufferをNCMBのファイルストアに保存します。

```jsx
// バッファをファイルストアに保存する
const fileName = `${Math.random().toString(32).substring(2)}.png`;
await ncmb.File.upload(fileName, buffer);
```

### レスポンスの返却

最後にファイルストアに保存したファイル名をスクリプトのレスポンスとして返却します。

```jsx
// ファイル名をレスポンスとして返す
res.json({fileName});
```

### スクリプト全体のコード

全体のコードは以下の通りです。

```jsx
// superagent と ncmb をインポートする
const request = require('superagent');
const NCMB = require("ncmb");

// OpenAI API のエンドポイントと API キーを定義する
const url = 'https://api.openai.com/v1/images/generations';
const apiKey = 'YOUR_OPENAI_API_KEY';

// NCMB のアプリケーションキーとクライアントキーを定義する
const applicationKey = 'YOUR_APPLICATION_KEY';
const clientKey = 'YOUR_CLIENT_KEY';
// NCMBの初期化
const ncmb = new NCMB(applicationKey, clientKey);
// 生成する画像の数
const n = 1;
// 生成する画像のサイズ
const size = 256;
// レスポンスのフォーマット
const response_format = 'b64_json';

// 関数をエクスポートする
module.exports = async function(req, res) {
  // リクエストデータを作成する
  const { prompt } = req.body;
  const params = {
    prompt,
    n,
    size: `${size}x${size}`,
    response_format,
  };
  // OpenAI API に POST リクエストを送信し、レスポンスを受け取る
  const response = await request.post(url)
    .set('Content-Type', 'application/json') // ヘッダーに Content-Type を設定する
    .set('Authorization', `Bearer ${apiKey}`) // ヘッダーに API キーを設定する
    .send(JSON.stringify(params)); // JSON 形式のパラメータを送信する
  // レスポンスは base64 エンコードされているので、デコードする
  const base64String = response.body.data[0].b64_json;
  // デコードしたデータをバッファに変換する
  const buffer = Buffer.from(base64String, "base64");
  // バッファをファイルストアに保存する
  const fileName = `${Math.random().toString(32).substring(2)}.png`;
  await ncmb.File.upload(fileName, buffer);
  // ファイル名をレスポンスとして返す
  res.json({fileName});
}
```

## 管理画面にアップロード

できあがったスクリプトをNCMBの管理画面でアップロードします。管理画面でテスト実行もできるので確認してください。今回はスクリプトを `image.js` として、POSTメソッドで動作するようにしています。

## 開発時の注意

スクリプトを開発している際にはテスト実行したくなると思います。そのための環境を[NCMBMania/node-script-dev-env](https://github.com/NCMBMania/node-script-dev-env) に用意していますのでご利用ください。 `npm start` した後で以下のようなcurlコマンドで実行結果を確認できます。

```
$ curl -XPOST -H "Content-Type: application/json" \
-d '{"prompt": "窓のそばにいる猫"}' \
http://localhost:3000/

{"fileName":"abcdef.png"}

```

## まとめ

今回はAI画像生成アプリにおけるスクリプト機能について解説しました。スクリプト機能を使えばAPIキーを隠蔽化して安全にコードをクラウドで実行できます。ぜひ利用してください。

[次回はチャット画面を作成](https://qiita.com/goofmint/items/c44d35a4130ce2458040)し、AI画像生成アプリを完成させます。