AWS Chalice を使って、Slack Appを作成してみた (Stable Diffusion編)

2023.07.07

AWS事業本部の梶原@福岡オフィスです。

先日作成したSlack Appに対して、Stable Diffusionの検証の為に、応答部分を変更してSlack Appを作成して、大量の猫画像を生成してみます

尚、検証にあたり以下をコンセプトに各サービスやフレームワークを選定して作ってみました。

  • なるべく標準的なフレームワークや純正によせる。
  • なるべくAWSによせる。
  • 本番想定でなく、検証用に環境を作る
  • 一撃♪(後片付けも)

ということで、以前作成したSlackで受け取ったメンションから、Stable Diffusion API から画像を生成し、返信する Slack Appを作成してみます

はじめに

以下の技術、サービスを使っています。詳しくは参考情報や、リンクなどをご参照ください

  • Slack (Slack Bolt for Python)
    • Slack 純正のSDK
  • AWS Chalice(Python Serverless Microframework for AWS)
    • ちょっと情報は少ないですが、デプロイと削除が1撃で手軽にできるので選定しました
  • Stable Diffusion
    • 画像生成AIといえばこれ!
    • Stability AI が提供する画像生成ツールです
    • Stability AI APIを使用していますので作成する際はDreamStudioからAPI Keyを生成して取得しておいてください

前提条件

  • AWS アカウント(以下サービスを使用しています)また権限が必要です
    • AWS IAM
    • AWS Lambda
    • Amazon API Gateway
    • Amazon S3
    • AWS Chalice
  • Slack
    • アプリケーションがインストール可能なこと
  • Stable Diffusion
    • API キーを取得してください

事前準備

AWS 環境設定

AWS Chaliceでデプロイをおこなうので、事前にCredential等を設定してください

$ aws configure
AWS Access Key ID [None]: 
AWS Secret Access Key [None]: 
Default region name [ap-northeast-1]: 
Default output format [None]:

Pythonの環境設定

Python 3.10 の環境設定をします。

以降の処理はPython3.10 のvenv環境での実行を想定しています

$ python3.10 -m venv .venv310
$ source .venv310/bin/activate
$ python --version
Python 3.10.12
$ pip --version
pip 23.0.1 from .venv310/lib/python3.10/site-packages/pip (python 3.10)

各種pipパッケージのインストール

Slack, AWS Chalice, Boto3, Stability AI SDKのパッケージをインストールしておきます。

なお、OpenAI は本来不要なのですが、ちょっと小細工をするために入れています。

尚、requirements.txt に記載したファイルはAWS Lambdaにデプロイされる際にLambda Layerに一緒にパッケージングされます。便利!

<br />$ pip install -r requirements.txt

requirements.txt

slack_bolt
boto3
chalice
openai
stability-sdk

AWS Chalice、Slackについて

動作確認までは以下エントリーでご案内していますのでご参考ください

AWS Chalice を使って、Slack App (Slack Bolt for Python) を作成してみた

やってみた

メンションの応答の実装部分に、メンションされた内容に対して OpenAIでプロンプト生成して、Stability AI APIに渡して画像を生成して、返信するようにしてみます

なお、コードはStability AIのPythonのサンプルコード(下記) を参考にしています。

Python gRPC SDK Text-to-Image

環境変数設定

AWS Chalice(AWS Lambda)の環境変数にOPENAI_API_KEY, STABILITY_HOST, STABILITY_KEYを追加します。作成されたLambda関数で環境変数から取得しています

config.json

{... 
  "stages": {
    "dev": {
      "environment_variables": {
    ...
    "OPENAI_API_KEY": "sk-xxxxxxxxxxxxxxxxxxxxxxxxx",
    "STABILITY_HOST": "grpc.stability.ai:443",
    "STABILITY_KEY": "sk-xxxxxxxxxxxxxxxxx"
}

実装部分

既存の処理はSlackアプリに対して、メンションされた場合(”app_mention”)に、say()でメッセージを返していますので、この部分に変更を行います

元のコード

@bolt_app.event("app_mention")
def handle_app_mentions(body, say, logger):
    logger.info(body)
    say("What's up? I'm a Chalice app :wave:")

Stability AIでメンションから画像を作成するコードに置き換えます

  1. メンションでAppが受け取ったメッセージからメンション部分を削除
  2. OpenAIでメンションに猫成分を注入
  3. Stability AI API で画像生成
  4. スレッドに画像送信する
import openai

from PIL import Image
from stability_sdk import client
import stability_sdk.interfaces.gooseai.generation.generation_pb2 as generation

@bolt_app.event("app_mention")
def handle_app_mentions(body, say, logger):
    logger.info(body)

    event = body["event"]
    channel = event["channel"]
    ts = event["ts"]

    # Slackでメンションを受け取ったらメッセージからメンションを取り除く
    mention_text = re.sub(r'<@.*?> ', "", event["text"])

    # 応答を生成
    openai.api_key = os.getenv("OPENAI_API_KEY")

    prompt = f"""「{mention_text}」から連想する画像生成AIのプロンプトを作成してください。
    10ほどの英語のキーワードを1行でカンマ区切りで羅列してください。
    cat, masterpieceのキーワードは必ず入れてください"""

    response = openai.Completion.create(
        model="text-davinci-003",
        prompt=prompt,
        temperature=0,
        max_tokens=150,
        top_p=1.0,
        frequency_penalty=0.0,
        presence_penalty=0.0
    )
    logger.info(response)

ここから、Stability AI の画像生成処理になります

    stability_api = client.StabilityInference(
        key=os.environ['STABILITY_KEY'], # API Key reference.
        verbose=True, # Print debug messages.
        engine="stable-diffusion-512-v2-1", # Set the engine to use for generation.
        # Available engines: stable-diffusion-xl-1024-v0-9 stable-diffusion-v1 stable-diffusion-v1-5 stable-diffusion-512-v2-0 stable-diffusion-768-v2-0
        # stable-diffusion-512-v2-1 stable-diffusion-768-v2-1 stable-diffusion-xl-beta-v2-2-2 stable-inpainting-v1-0 stable-inpainting-512-v2-0
    )

    sd_prompt = response.choices[0].text
    say(f"generate:{sd_prompt}", thread_ts = ts)

    # Set up our initial generation parameters.
    answers = stability_api.generate(
        prompt= sd_prompt,
        steps=15, # Amount of inference steps performed on image generation. Defaults to 30.
        cfg_scale=8.0, # Influences how strongly your generation is guided to match your prompt.
                    # Setting this value higher increases the strength in which it tries to match your prompt.
                    # Defaults to 7.0 if not specified.
        width=512, # Generation width, if not included defaults to 512 or 1024 depending on the engine.
        height=512, # Generation height, if not included defaults to 512 or 1024 depending on the engine.
        samples=1 # Number of images to generate, defaults to 1 if not included.
        # sampler=generation.SAMPLER_K_DPMPP_2M # Choose which sampler we want to denoise our generation with.
                                                    # Defaults to k_dpmpp_2m if not specified. Clip Guidance only supports ancestral samplers.
                                                    # (Available Samplers: ddim, plms, k_euler, k_euler_ancestral, k_heun, k_dpm_2, k_dpm_2_ancestral, k_dpmpp_2s_ancestral, k_lms, k_dpmpp_2m, k_dpmpp_sde)
    )

    file_uploads = []

    # Set up our warning to print to the console if the adult content classifier is tripped.
    # If adult content classifier is not tripped, save generated images.
    for resp in answers:
        for artifact in resp.artifacts:
            if artifact.finish_reason == generation.FILTER:
                warnings.warn(
                    "Your request activated the API's safety filters and could not be processed."
                    "Please modify the prompt and try again.")
                say("Your request activated the API's safety filters and could not be processed. Please modify the prompt and try again.", thread_ts = ts)
            if artifact.type == generation.ARTIFACT_IMAGE:
                # img = Image.open(io.BytesIO(artifact.binary))
                # filename = str(artifact.seed)+ ".png"
                # img.save("/tmp/" + filename) # Save our generated images with their seed number as the filename.

                # 一時的なファイルを作成してデータを書き込む
                decoded_data = io.BytesIO(artifact.binary).read()
                with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
                    temp_file.write(decoded_data)

                file_uploads.append(
                    {
                        "file": temp_file.name,
                        "title": temp_file.name,
                        "filetype": "PNG",
                    }
                )

    logger.info(file_uploads)

    # New way with multiple files!
    response = bolt_app.client.files_upload_v2(
        file_uploads=file_uploads,
        channels=channel,
        initial_comment=mention_text,
        thread_ts=ts,
    )

デプロイ

相変わらず、ChaliceではめんどくさいLambdaのLayerも自動で作成して、更新デプロイしてくれるのでとても助かります。

$ chalice deploy
...
Creating Rest API
Resources deployed:
  - Lambda Layer ARN: arn:aws:lambda:ap-northeast-1:123456789012:layer:bolt-python-chalice-dev-managed-layer:1
  - Lambda ARN: arn:aws:lambda:ap-northeast-1:123456789012:function:bolt-python-chalice-dev
  - Rest API URL: https://hoge.execute-api.ap-northeast-1.amazonaws.com/api/

動作確認

インストールが正常にいけば、任意の部屋に追加したSlack Appを追加して、メンションしてください。

以下のようにStability AIのAPIから猫画像が生成されており、スレッドに返信されていれば成功です。

リソース削除

以下コマンドでAWS上のリソースはきれいに削除できます

$ chalice delete

まとめ

ここまでできれば、大量の猫画像が生成できるかと思います。samples=1を10に変えてください!コード上複数の画像生成にも対応しています!

クラスメソッド創立20周年目なので、猫のマスコットを作成しようかとおもったのですが、もうちょっとプロンプトの勉強が必要そうです。

参考情報

Stable DiffusionのAPIを使ってテキストから画像(txt2img)を作ったり、画像から画像(img2img)を作ってみる | DevelopersIO

AWS Chalice https://github.com/aws/chalice