はじめに
こんにちは!2年目エンジニアの @Yu_yukk_Y です。
先日プライベートでハッカソンに参加した際、Amazon BedRockのAPI経由でStable Diffusionを利用したので、その手順を記録しておこうと思います
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
と進みます。
Edit
を押して、Stability AIの利用するモデルを選択し、Save Changes
を押します。
ここで利用の審査が始まります。審査が始まると下記のようにIn Progress
と表示され
審査が通過すると、下記のようにAccess Granted
と表示されます。
自分の場合は10秒くらいで審査が完了しました。
2. アクセス用のロールにIAMポリシーを付与する
Amazon BedRockの利用には利用するモデルに応じたIAM ポリシーの付与が必須です
今回はstability.stable-diffusion-xl-v0
を利用したかったので、下記のようなIAMポリシーを作成し、利用するIAMロールに付与しました。
{
"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
を下記に記します。
AWSの鍵情報はもっと最適な管理方法がありますが、この記事のサンプルコードはローカル環境でサクッと試すことを目的としているため、取り急ぎ.env
に必要情報を記載しています。
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"]
services:
api-server:
build: .
env_file:
- .env
ports:
- 80:80
tty: true
stdin_open: true
AWS_ACCESS_KEY_ID=${access-key}
AWS_SECRET_ACCESS_KEY=${secret-access-key}
AWS_DEFAULT_REGION=${default-region}
S3_BUCKET_NAME=${bucket-no-name}
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ソースコードがこちらです。
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ファイルを指定して
{ "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が返ってきます。
{
"image_url": "https://xxxx..."
}
下記はリクエストのサンプルです。
curl --location 'http://localhost:80/stable_diffusion' \
--form 'file=@"image.JPG"' \
--form 'prompts_file=@"sample.json";type=application/json'
ちょっと解説
画像のリサイズ
# Stable Diffusionは32の倍数のサイズの画像しか利用できないため、サイズ変換が必要
resized_image = Image.open(io.BytesIO(content)).resize((512, 512))
コメントに書いてあるとおり、Stable Diffusionでは32の倍数のサイズの画像しか入力として利用することができません。そこで、APIに渡す前に32の倍数である512 × 512
のサイズにリサイズしています。
パラメータ設定
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: 生成のベースとなる画像データです。
実行
image_response = boto3_bedrock.invoke_model(body=body, modelId=MODEL_ID)
この行で実際にAPIを実行しています!実装時には先程作成したパラメータと利用するモデルのIDが必要です。
結果
自分の証明写真と下記のようなプロンプトを渡した時の生成結果です
[
{ '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"の場合
"style_preset": "comic-book"の場合
感想
通常はGPUをたくさん搭載したお値段の高いサーバが必要なStable Diffusionですが、API経由で従量課金で使えるようになったことで、個人のアプリ開発などでも気軽に使いやすくなったような気がします!!
もちろん、withのような企業で使えるかどうかは、情報管理の観点やAPI利用制限の観点など複数の観点から考えていくべきですが、Stable Diffusionを使う上での選択肢が大きく広がったのはとても嬉しいことだと思いました
PR
withでは、新しいコンテンツを作っていく上で、新しい技術を採用することもあれば、安定した技術を採用することもあります。
作ろうとしているコンテンツ実現に最適なアーキテクチャを模索し、それらを使い実現しています。この最適なアーキテクチャを見つけだすのは、至難の技ではありますが、エンジニアとして面白いフェーズではないかな?と思っています。
このようなことにもご興味がある方は、ぜひ以下よりお問い合わせください!
参考文献