【Amazon BedRock】boto3経由でStable Diffusionを触ってみる

## はじめに

こんにちは！2年目エンジニアの @Yu_yukk_Y です。
先日プライベートでハッカソンに参加した際、Amazon BedRockのAPI経由でStable Diffusionを利用したので、その手順を記録しておこうと思います:grin:
boto3を使ってPython経由で使ってみました。ソースコードのサンプルも掲載しています！

## そもそもAmazon BedRockとは？

Amazonと生成AIの分野で主要なスタートアップが連携することで実現した、生成AIモデルをAPI経由でマネージドに実行できるサービスです。2023年10月15日時点ではGPTとStable Diffusionが下記のプロバイダ経由で利用できるようになっています。
- AI21 labs
- Amazon
- Anthropic
- Cohere
- Stability AI

2023年10月15日現在、このうちStability AIが提供するモデル`stability.stable-diffusion-xl-v0`がStable Diffusionのモデルとして利用できます。

なお、**現在はリージョンによって利用できるプロバイダが異なり、現在はStability AIは下記2リージョンでしか利用できませんし、使えるのはpreview版のモデルのみです** 。

- バージニア北部（ us-east-1 ）
- オレゴン（ us-west-2 ）

## 手順

### 1. 対応しているリージョンのBedRockでStabilityAIを有効にする

自分は **オレゴン( us-west-2 )** で進めていきました。

まず`Amazon BedRock` > `Model Access`と進みます。

![スクリーンショット 2023-10-16 13.00.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091686/ec9a504a-e934-3e78-614d-bb8dae118fba.png)

`Edit`を押して、Stability AIの利用するモデルを選択し、`Save Changes`を押します。

![スクリーンショット 2023-10-16 13.02.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091686/05b601b8-80d9-90c6-81ec-b6596169d5bc.png)

ここで利用の審査が始まります。審査が始まると下記のように`In Progress`と表示され

![スクリーンショット 2023-10-18 18.37.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091686/cc61fa74-0e4f-6226-698d-6bd5d8703e50.png)

審査が通過すると、下記のように`Access Granted`と表示されます。

![スクリーンショット 2023-10-18 18.38.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091686/6e091f8d-8441-5fea-6dd8-7d417cefdb02.png)

自分の場合は10秒くらいで審査が完了しました。

### 2. アクセス用のロールにIAMポリシーを付与する

Amazon BedRockの利用には**利用するモデルに応じたIAM ポリシーの付与が必須です**
今回は`stability.stable-diffusion-xl-v0`を利用したかったので、下記のようなIAMポリシーを作成し、利用するIAMロールに付与しました。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "bedrock:InvokeModel",
            "Resource": "arn:aws:bedrock:us-west-2::foundation-model/stability.stable-diffusion-xl-v0",
            "Effect": "Allow"
        }
    ]
}
```

### 3. 実際にアクセスしてみる

ここまでくればあとはアクセスしてみるだけです！
簡単にFastAPIと組み合わせて、S3の署名付きURLから配信してみます！

コード実行時の`Dockerfile`と`compose.yaml`、`requirements.txt`、`.env`を下記に記します。

:::note warn

AWSの鍵情報はもっと最適な管理方法がありますが、この記事のサンプルコードはローカル環境でサクッと試すことを目的としているため、取り急ぎ`.env`に必要情報を記載しています。
:::


```Dockerfile
FROM python:3.9
 
WORKDIR /code
 
COPY ./requirements.txt /code/requirements.txt
 
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
 
COPY ./app /code/app
 
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
```

```yaml:compose.yaml
services:
  api-server:
    build: .
    env_file:
      - .env
    ports:
      - 80:80
    tty: true
    stdin_open: true
```

```.env
AWS_ACCESS_KEY_ID=${access-key}
AWS_SECRET_ACCESS_KEY=${secret-access-key}
AWS_DEFAULT_REGION=${default-region}
S3_BUCKET_NAME=${bucket-no-name}
```

```requirements.txt
fastapi>=0.103.0,<0.104.0
pydantic>=2.4.2,<2.5.0
uvicorn>=0.23.0,<0.24.0
boto3>=1.28.0,<1.29.0
python-multipart>=0.0.6,<0.1.0
Pillow>=10.0.1,<10.1.0
```
そして実際のPythonソースコードがこちらです。

```python:app/main.py
from typing import List, Annotated
from datetime import datetime, timezone
import os
import random
import json
import io
import base64
import traceback

from fastapi import FastAPI, UploadFile, File, Form
from fastapi.responses import JSONResponse
import boto3
from pydantic import BaseModel
from PIL import Image

MODEL_ID = "stability.stable-diffusion-xl-v0"

boto3_bedrock = boto3.client("bedrock-runtime", region_name="us-west-2")
s3 = boto3.client("s3")

app = FastAPI()


class Prompt(BaseModel):
    text: str
    weight: float


class Prompts(BaseModel):
    prompts: List[Prompt]


def base64_to_image(base64_str):
    return Image.open(io.BytesIO(base64.decodebytes(bytes(base64_str, "utf-8"))))


def image_to_base64(image, mime_type):
    if mime_type == "image/jpeg":
        format = "JPEG"
    else:
        format = "PNG"
    buffer = io.BytesIO()
    image.save(buffer, format=format)
    return base64.b64encode(buffer.getvalue()).decode("utf-8")


@app.post("/stable_diffusion")
async def stable_diffusion(
    file: Annotated[UploadFile, File()], prompts_file: Annotated[UploadFile, File()]
):
    if file.content_type != "image/jpeg" and file.content_type != "image/png":
        return JSONResponse(
            status_code=400,
            content={"message": f"Invalid file type: {file.content_type}"},
        )
    if prompts_file.content_type != "application/json":
        return JSONResponse(
            status_code=400,
            content={"message": f"Invalid file type: {prompts_file.content_type}"},
        )
    content = await file.read()
    json_content = json.loads(await prompts_file.read())
    prompts = Prompts.parse_obj(json_content)

    try:
        # Stable Diffusionは32の倍数のサイズの画像しか利用できないため、サイズ変換が必要
        resized_image = Image.open(io.BytesIO(content)).resize((512, 512))

        body = json.dumps(
            {
                "text_prompts": [prompt.model_dump() for prompt in prompts.prompts],
                "cfg_scale": 7,
                "seed": random.randint(0, 1000),
                "start_schedule": 0.6,
                "steps": 50,
                "style_preset": "comic-book",
                "init_image": image_to_base64(resized_image, file.content_type),
            }
        )

        image_response = boto3_bedrock.invoke_model(body=body, modelId=MODEL_ID)

        response_body = json.loads(image_response.get("body").read())
        output_image = base64_to_image(response_body["artifacts"][0].get("base64"))

        filename_key = (
            f"{int(datetime.now(timezone.utc).timestamp() * 1000)}_{file.filename}"
        )

        if file.content_type == "image/jpeg":
            format = "JPEG"
        else:
            format = "PNG"
        buffer = io.BytesIO()
        output_image.save(buffer, format=format)

        s3.put_object(
            Body=buffer.getvalue(),
            Bucket=os.environ["S3_BUCKET_NAME"],
            Key=filename_key,
        )

        s3_url = s3.generate_presigned_url(
            ClientMethod="get_object",
            Params={"Bucket": os.environ["S3_BUCKET_NAME"], "Key": filename_key},
            ExpiresIn=3600,
        )

        return {"image_url": s3_url}
    except Exception as e:
        print(traceback.format_exc())
        return JSONResponse(status_code=400, content={"message": str(e)})
```

これらを実行すると、画像とプロンプトを送信することでStable Diffusionが生成した画像を返してくれるサーバーを立ち上げることができます！

#### 実行方法

下記のようにしてコンテナプロセスを立ち上げた後
```
docker compose build
docker compose up -d
```

`http://localhost:80/stable_diffusion`のエンドポイントに対して、`file`に画像ファイルを、`prompts_file`に下記のようなjsonファイルを指定して

```json:sample.json
{ "prompts": [
    { "text": "no angry", "weight": 0.5 },
    { "text": "very joyfull", "weight": 1.7 },
    { "text": "no suprise", "weight": 0.5 },
    { "text": "no sorrowful", "weight": 0.5 },
    { "text": "自分の住所", "weight": 1.7 }
]}
```

`multipart/form-data`でPOSTリクエストを送信します。すると、`{ "image_url": ${実際のURL}}`といった感じで生成後の画像の配信URLを含むJSONが返ってきます。

```json
{
    "image_url": "https://xxxx..."
}
```

下記はリクエストのサンプルです。
```shell
curl --location 'http://localhost:80/stable_diffusion' \
--form 'file=@"image.JPG"' \
--form 'prompts_file=@"sample.json";type=application/json'
```

#### ちょっと解説

##### 画像のリサイズ

```python
# Stable Diffusionは32の倍数のサイズの画像しか利用できないため、サイズ変換が必要
resized_image = Image.open(io.BytesIO(content)).resize((512, 512))
```

コメントに書いてあるとおり、**Stable Diffusionでは32の倍数のサイズの画像しか入力として利用することができません**。そこで、APIに渡す前に32の倍数である`512 × 512`のサイズにリサイズしています。

##### パラメータ設定

```python
body = json.dumps(
    {
        "text_prompts": prompts.model_dump(),
        "cfg_scale": 7,
        "seed": random.randint(0, 1000),
        "start_schedule": 0.6,
        "steps": 50,
        "style_preset": "comic-book",
        "init_image": image_to_base64(resized_image, file.content_type),
    }
)
```
ここではStable Diffusionで実際に画像生成に利用する各種パラメータを定義しています。
それぞれ説明すると
- `text_prompts`: 実際のプロンプトとその重みの配列です。`[{ 'text': 'cute cat.','weight': 1.7 }, { 
'text': 'sunny': 'weight': 1.2 }]` みたいにして渡します。
- `cfg_scale`: 入力した画像とプロンプトを最終的なアウトプットにどの程度反映するかを定義する値です。`0 ~ 30`からの値となっており、値が大きいほど反映度が高く、値が小さいほどランダム性が高くなります。
- `seed`: 初期ノイズ設定を決定する値です。推論が同じような画像を作成できるように、前回の実行と同じシードと同じ設定を使用します。また、この値を設定しない場合は乱数として設定されます。
- `start_schedule`: 拡散ステップの開始の割合をスキップし、init_imageが最終的に生成される画像に影響を与えるようにします。値が小さいほど`init_image`からの影響が大きくなり、値が大きいほど拡散ステップからの影響が大きくなります。(例えば、0を指定すると単にinit_imageが返され、1を指定すると全く異なる画像が返されます)。
- `steps`: 画像を何回サンプリングするかを決定する値です。ステップ数が多いほど、より正確な結果が得られます。
- style_preset: イメージ モデルを特定のスタイルに導くことができます。ここには`3d-model` `analog-film` `anime` `cinematic` `comic-book` `digital-art` `enhance` `fantasy-art` `isometric` `line-art` `low-poly` `modeling-compound` `neon-punk` `origami` `photographic` `pixel-art` `tile-texture` が設定可能です。
- imit_image: 生成のベースとなる画像データです。

といった感じです(より詳しくは[こちら](https://platform.stability.ai/docs/api-reference#tag/v1generation/operation/imageToImage)や[こちら](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters.html#model-parameters-diffusion))

##### 実行

```python
image_response = boto3_bedrock.invoke_model(body=body, modelId=MODEL_ID)
```

この行で実際にAPIを実行しています！実装時には先程作成したパラメータと利用するモデルのIDが必要です。

### 結果

自分の証明写真と下記のようなプロンプトを渡した時の生成結果です

```python
[ 
    { 'text': 'no angry', 'weight': 0.5 },
    { 'text': 'very joyfull', 'weight': 1,7 },
    { 'text': 'no suprise', 'weight': 0.5 },
    { 'text': 'no sorrowful', 'weight': 0.5 },
    { 'text': '自分の住所', 'weight': 1.7 }
]
```

#### "style_preset": "photographic"の場合

![1697310700151_IMG_2592.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091686/79a6818f-7c0b-20b1-41f1-dec8de7de9c0.jpeg)

#### "style_preset": "comic-book"の場合

![1697311059125_IMG_2592.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091686/f2653eaa-c1b8-b4a5-06d4-db0ab529e5d7.jpeg)

## 感想

通常はGPUをたくさん搭載したお値段の高いサーバが必要なStable Diffusionですが、API経由で従量課金で使えるようになったことで、個人のアプリ開発などでも気軽に使いやすくなったような気がします！！
もちろん、withのような企業で使えるかどうかは、情報管理の観点やAPI利用制限の観点など複数の観点から考えていくべきですが、Stable Diffusionを使う上での選択肢が大きく広がったのはとても嬉しいことだと思いました:blush:

## PR
withでは、新しいコンテンツを作っていく上で、新しい技術を採用することもあれば、安定した技術を採用することもあります。
作ろうとしているコンテンツ実現に最適なアーキテクチャを模索し、それらを使い実現しています。この最適なアーキテクチャを見つけだすのは、至難の技ではありますが、エンジニアとして面白いフェーズではないかな？と思っています。
このようなことにもご興味がある方は、ぜひ以下よりお問い合わせください！

https://enito.co.jp/careers/

## 参考文献

https://docs.aws.amazon.com/bedrock/

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-runtime.html

https://platform.stability.ai/docs/api-reference#tag/v1generation/operation/imageToImage

https://dev.classmethod.jp/articles/amazon-bedrokc-stable-diffusion-xl/

https://qiita.com/hamanuman/items/7de7a46bff390b15dfa8