Amazon Bedrockの画像生成をWebアプリに組み込む時に苦しんだこと



## はじめに
[2023 Japan AWS Jr. Champions Advent Calendar](https://qiita.com/advent-calendar/2023/jr-champions)の24日目の記事です。2023年もあと数日になりましたが、この1年間で多くのJapan AWS Jr. Championsからさまざまな知識や視点がアウトプットされました。私もこの流れに乗るために、ここ最近の学びをシェアしたいと思います。

普段はAzureの生成AIサービスであるAzure OpenAI Serviceを使ったWebアプリの開発をしています。『このWebアプリにAWSの生成AIも組み込みたい』というニーズも多くありました。そのため、AWSの生成AIサービスを統合しようとしましたが、Azure OpenAIのAPI仕様と異なる部分があり、かなり苦労をしました。

そこで、本記事ではAmazon Bedrockの画像生成をWebアプリに組み込む時に苦しんだことを共有します。今日はクリスマスイブということで、同じような問題に直面するかもしれない皆さんへの**クリスマスプレゼントになれば幸いです。**

それではまず、Amazon Bedrockについて簡単に説明します。

## Amazon Bedrockとは？

[Amazon Bedrock](https://aws.amazon.com/jp/bedrock/)とは、AI21 Labs、Anthropic、Cohere、Meta、Stability AI、AmazonといったトップAI企業が提供する基幹モデルを統一されたAPIでアクセス可能にするフルマネージド型サービスです。Amazon Bedrockの特徴は以下の通りです。

* Amazon Bedrockはサーバーレスのサービスのため、インフラストラクチャの管理は不要
* 単一のAPIリクエストでテキストの生成や画像の生成が可能

本記事では特に画像生成について説明します。

### Amazon Bedrockを使って画像を生成してみる

コンソールにプロンプトを入力して"RUN"をクリックすることで、画像を生成することができます。今日はクリスマスイブなのでサンタさんを生成してみます。~~断じて「寂しいクリスマスイブを紛らわすために女性サンタにしよう」とか思っているわけではありません。~~

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2961370/e1232ae5-0376-98f9-3c67-11da998ccc1d.png)


**数秒待つだけで上記のような画像が生成されます。凄いです。**
**ただし、この画面でリロードすると、、、**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2961370/60d0ae56-55e9-9558-d95a-52fc0b028abf.png)

**生成した画像が消えてしまいます。悲しいです。**

これを解消すべく、今回はAmazon Bedrockの画像生成をWebアプリに組み込み、リロード後も過去に生成した画像を確認できるWebアプリの開発をしました。

## 作成したWebアプリ

Vue3/Nuxt3で書かれている[chatgpt-nuxtのリポジトリ](https://github.com/lianginx/chatgpt-nuxt)をベースにして、画像生成のモデルを選べるようなUIを実装しました (まだこのリポジトリへのcontributeはしていません)。DALL·E 2や3は過去に実装済みです （これもcontribuはまだできてません)。

Stable diffusion XL v0.9のモデルを選択して、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2961370/2e7aea20-f54e-1197-df14-807c68a0538d.png)

プロンプトを送信すると・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2961370/cc99d4a4-3c48-d465-e132-044cdaf6ebb9.png)

このように画像が表示されます。
~~断じて「寂しいクリスマスイブを紛らわすために(略)」とか思っていないです(２回目)~~
画像の情報をDBに保存しているために、リロード後も消えずに表示されます。


### 構成イメージ

システムの構成イメージです。フロントエンドから画像生成プロンプトが送信されたら、サーバサイドがAmazon Bedrockに画像生成リクエストを実行します。返ってきた画像情報をDBに保存しつつ、ユーザに画像情報を返却します。Azureのリソースを使った場合の構成は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2961370/3c948d62-6d6d-2a00-3841-720e688c3f1f.png)

「え?Japan AWS Jr. ChampionsなのになんでAzure使ってるの？」って声が聞こえてきそうですが、今回はAzureのOpenAI Serviceを先にWebアプリに組み込んでいたため、このようなAzureを基盤とした構成としてます。
もしAWS内で完結させたい場合は以下のような構成になるかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2961370/36c7c97e-e06a-789f-e7a8-e3e5e71e5eeb.png)

Nuxt3のアプリをAWS Amplifyに設定0でデプロイできる"Zero config deployment to AWS Amplify Hosting"という機能が、2023-11-21に[リリース](https://github.com/unjs/nitro/releases/tag/v2.8.0)されています。実際にこの機能を用いることで、AWS Amplifyに簡単にWebアプリをデプロイできるところまでは確認済みです。
しかし、サーバサイドとフロントエンドの環境変数周りの設定が思うようにいかず、絶賛トラシュー中です。設定できたら記事を更新します。

## Amazon Bedrockの画像生成APIを呼び出す

フロントエンドは、以下の情報を含む画像生成リクエストをサーバサイドに送信します。
* プロンプト (ex. サンタの画像)
* 画像のサイズ (ex. 512x512, 1024x1024など)
* モデルID (ex. Stable diffusion XL v0.9など)

サーバサイドで以下のようなコードを用いることでBerdrockのAPIを呼び出すことができます。

```typescript:API呼び出し部分
// main.ts
import {
  BedrockRuntime,
  InvokeModelCommand,
  InvokeModelCommandInput,
} from "@aws-sdk/client-bedrock-runtime";

const client = new BedrockRuntime({
  region: "us-east-1",
  credentials: {
    accessKeyId: awsAccessKey, 
    secretAccessKey: awsSecretKey, 
  },
});
const body = createBody(createImageBody, modelId);
const input: InvokeModelCommandInput = {
  contentType: "application/json",
  accept: "*/*",
  modelId: modelId, 
  body: JSON.stringify(body),
const command = new InvokeModelCommand(input);
const response = await client.send(command);

// requestのbodyを作成する
const createBody = (
  createImageBody: { prompt: string, width: number, height: number},
  modelId: string
) => {
  const prompt = createImageBody.prompt;
  if (modelId.includes("stability")) {
    return {
      text_prompts: [
        {
          text: `${prompt}`,
        },
      ],
      cfg_scale: 10,
      seed: 0,
      width: createImageBody.width
      height: createImageBody.height
      steps: 50,
    };
  }
  // else if (modelId.includes("taitan")) {
  // titanの実装が動いていないため、省略
}    
```

clientの作成にかかるオーバヘッド等が気になる方は、サーバ起動時にclientを作成して利用することを検討すると良いと思います。

*注意: このコードはAWS以外の環境でも動くようにしております。AWSだけで実装する場合は、アクセスキーではなくIAM RoleやIAM Policy等を用いてください。


## Webアプリに組み込む時に苦しんだこと

大きく4個あります。

*Azure OpenAIのAPIに慣れている人の目線で書いていますので、ご了承ください。

### 1. DALL·E APIとBedrock APIの返却データの相違点
DALL·E APIとBedrock APIとでは返却される画像データの形式が違います。DALL·E APIから得られるレスポンスでは、生成された画像が置かれているAzureのblobストレージのURLが返却されます。
```javascript:DALL·Eのreponse
{ 
  created: 1702544641,
  data: [
    {
      revised_prompt: '***',
      url: 'https://dalleprodsec.blob.core.windows.net/private/images/***/generated_00.png?se=2023-12-24T09%3A04%3A17Z&sig=***&ske=2023-12-29T10%3A10%3A28Z&skoid=***&sks=b&skt=2023-12-24T10%3A10%3A28Z&sktid=***&skv=2020-10-02&sp=r&spr=https&sr=b&sv=2020-10-02'
    }
  ]
}
```

urlには画像の場所以外にも、shared access signature (SAS) tokenが含まれています。(詳しくは[Construct a user delegation SAS](https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas#construct-a-user-delegation-sas)を参照してください)
クエリパラメータの1つに`se`というsigned Expiry (有効期限) が含まれており、画像生成リクエストから1日後の値が設定されていました。つまり1日後には生成した画像にアクセスすることはできません。


一方でBedrock APIでは、生成された画像の情報がresponseのbodyにUint8Arrayとして返却されます。DALL·EのAPIに慣れている人は、注意が必要です。

```javascript:Amazon Bedrockのreponse
{
  '$metadata': {
    httpStatusCode: 200,
    requestId: '***',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  contentType: 'application/json',
  body: Uint8ArrayBlobAdapter(463374) [Uint8Array] [
     65,  67,  65,  73,  65,  65,  65,  66, 55,  71, 107,  79,
    116,  65,  65,  65,
    ... 463274 more items
  ]
}
```

画像情報を取り出すには、以下のコードに示すようにresponse.bodyのバイナリデータをBufferオブジェクトとして作成し、その後toString(“utf-8”)によってUTF-8の文字列にデコードして、JSON形式のオブジェクトに変換する必要があります。

```typescript
const parsedData = JSON.parse(Buffer.from(response.body).toString("utf-8"));
```

この処理によって以下のようなオブジェクトを取得できます。base64のvalueが画像データです。

```javascript:bodyをデコードして得られる情報
parsedData {
  result: 'success',
  artifacts: [
    {
      seed: 0,
      base64: 'iV93***'... 471976 more characters,  // base64の画像データが取得できる。
      finishReason: 'SUCCESS'
    }
  ]
}
```

ちなみにbase64は、データを「a～z」「A～Z」「0～9」「+」「/」の64文字と「=」を組み合わせて表現する方法です。


### 2. Base64エンコード画像の対応

フロントエンドではイメージタグを使って画像を表示しています。DALL·Eの時は以下のように表示していました。
```vue:フロントエンドでの画像の表示
<img src="https://dalleprodsec.blob.core.windows.net/private/images/***/">
```

base64の画像を表示する場合ためには、サーバサイドで以下のように加工をしてから、フロントエンドに返却する必要があります。

```typescript:加工方法
const parsedData = JSON.parse(Buffer.from(response.body).toString("utf-8"));
const base64 = parsedData.artifacts[0].base64;
return {data: [{ url: `data:image/jpeg;base64,${base64}` }]}
```

```vue:フロントエンドでのbase64の表示方法
<img src="data:image/jpeg;base64,${base64}">
```


### 3. TextDecoderの処理が終わらない
これはNuxt3起因なのかもしれませんが、詳しい原因はわかってないです。

以下のコードをts-nodeで動かした時は正常に処理できるのですが、
このコードをNuxt3のサーバサイドに組み込むと、`textDecoder.decode`の処理が一生終わらないことがあります。

```typescript:getImage.ts
const blobAdapter = response.body;    
const textDecoder = new TextDecoder('utf-8');

const jsonString = textDecoder.decode(blobAdapter.buffer); 
const parsedData = JSON.parse(jsonString);
```

以下のようにdecodeする配列の大きさを減らした場合や、画像サイズを小さくした場合はdecode処理は動きました。

```typescript: 
// 本来は400000ぐらいの大きさだが、10000に減らす。
const jsonString = textDecoder.decode(blobAdapter.subarray(1, 10000));
```

しかし、それでは困るのでTextDecoderではなく、Bufferを用いるようにしました。
このコードによって画像サイズが大きくても、decode処理が動くようになりました(原因は解明中です。わかる人は教えてください)
```typescript
const parsedData = JSON.parse(Buffer.from(response.body).toString("utf-8"));
```



### 4. base64を使うとデータ転送量が増えて、Webアプリの動きが遅くなる

私が開発しているWebアプリを用いて、画像のURLを受信する場合とBase64の形式で受信する場合のデータ通信量を検証しました。

* 画像のURLを受信する場合: 2.7KB
* Base64形式の場合: 496KB

URLを用いる場合はおおよそ2.7KBの通信データで済むのに対して、Base64形式の場合は496KBと大幅にデータ量が増えました。

試しに私の環境で画像3枚分のbase64をWebアプリで表示させようとしたら3秒ほどかかりました。何回も画像をfetchすることを考えると、DBにはbase64ではなくs3のURLを格納しておくのが良いと思われます。(s3とCloudFrontを用いればキャッシュも効かせられるのでなお良い)


## まとめ

本記事ではAmazon Bedrockの画像生成に関する苦労を共有しました。DALL·E APIと異なり、Bedrock APIでは生成された画像がbase64形式で取得できます。しかしbase64を使用するとデータ転送量が増加し、Webアプリの動作が遅くなることを確認しました。
そのため、商用のチャットアプリとして使うのであれば、DBにはbase64形式で格納せずに、画像のURLを格納する方法が良いと思います。
これらの情報は、Amazon Bedrockを使ったWebアプリ開発の助けになることを願っています。


## 参考にしたURL

以下を参考にさせていただきました。ありがとうございました。

* [Amazon Bedrock](https://aws.amazon.com/jp/bedrock/)
* [chatgpt-nuxtのリポジトリ](https://github.com/lianginx/chatgpt-nuxt)
* [Construct a user delegation SAS](https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas#construct-a-user-delegation-sas)
* [Amazon Bedrockでテキスト＆画像生成してみた[with Node.js]](https://dev.classmethod.jp/articles/amazon-bedrock-text-image-with-node-js/)
* [Amazon Bedrockがリリース！ Lambdaから画像を10枚生成してS3に置くまでの流れをやってみた](https://dev.classmethod.jp/articles/bedrock-lambda-stable-diffusion/)