Amazon Nova Canvasを使った画像生成チャットアプリを作ってみた

# はじめに
2024年のAWS re:Inventにて発表されたAmazon Novaシリーズには、画像生成が可能な **Nova Canvas** が含まれています。
これまではAmazon Bedrockから画像生成を行うためには```Amazon Titan```シリーズや```Stable Diffusion```シリーズだけでしたが、ここにNova Canvasという選択肢が追加されます。

本記事ではNova Canvasでできることを確認しつつ、画像生成チャットボットの作り方を展開したいと思います。

# Nova Canvasの特徴
Nova Canvasは、プロンプトとして与えられたテキストや画像から新しい画像を生成する生成AIモデルです。
Nova Canvasの特徴は以下の通りです。

| 機能                          | 説明                                                                                     |
|-------------------------------|------------------------------------------------------------------------------------------|
| 参照画像の提供                | 画像や動画の生成に役立つ参照画像を提供可能                                               |
| カラーパレットの決定          | テキスト入力を使用して画像の配色や「カラーパレット」を決定                               |
| 画像編集                      | テキストプロンプトを使用して入力画像内のオブジェクトや背景を置き換えることが可能         |
| 背景削除                      | 背景を簡単に削除して、画像の被写体を変更せずに残すことが可能                             |
| 安全性、責任ある AI、補償     | トレーサビリティ、コンテンツモデレーション、補償のためのウォーターマークが含まれる       |

https://aws.amazon.com/jp/ai/generative-ai/nova/creative/


## Nova Canvas vs Titan
以下はNovaシリーズとTitanシリーズの比較です。
Novaシリーズでは、全体的により長文なコンテンツと文章を処理できるようになりました。

| 特徴                | Titanモデル                       | Novaモデル                         |
|---------------------|----------------------------------|------------------------------------|
| 最適用途            | 一般的なテキスト生成、画像タスク、埋め込み | 長文コンテンツと複雑な文書処理       |
| 使用シーン          | 画像生成や埋め込みが必要な場合       | 非常に長い文書を処理する場合         |
| 強み                | テキスト、画像、埋め込みの多用途性 | より大きなコンテキストウィンドウ（最大300Kトークン） |
| コスト              | 比較的低コスト                      | 高コスト                           |
| アプリケーション統合 | 幅広い統合が可能                    | 特定の用途に最適化されている場合が多い 
| 標準タスクの低遅延  | はい                              | いいえ                             |
| 生産ワークロードに最適化 | はい                              | いいえ                             |
| 標準コンテキスト長   | はい                              | いいえ                             |
| 高速応答時間が必要な場合 | はい                              | いいえ                             |


### Nova Canvasのオンデマンド料金
以下はNova CanvasとTitan Image Generatorのus-east-1（バージニア北部）での利用料金です。
画像サイズにもよりますが、Nova CanvasではTitanの数倍のコストがかかるため、ユースケースに応じた使い分けが必要となりそうです。

Titan Image Generatorにはv1もありますが、調査時点での上記リージョンのコストは同じでした。

| モデル                | 画像解像度               | 標準品質で生成された画像1枚あたりの価格 | プレミアム品質で生成された画像1枚あたりの価格 |
|-----------------------|-------------------------|--------------------------------------|--------------------------------------------|
| Amazon Nova Canvas    | 1024 x 1024 まで        | USD 0.04                             | USD 0.06                                   |
| Amazon Nova Canvas    | 2048 x 2048 まで        | USD 0.06                             | USD 0.08                                   |
| Amazon Titan Image Generator v2| 512 x 512 よりも小さい  | USD 0.008                            | USD 0.01                                   |
| Amazon Titan Image Generator v2| 1024 x 1024 よりも大きい| USD 0.01                             | USD 0.012                                  |

https://aws.amazon.com/jp/bedrock/pricing/


# Nova Canvasチャットアプリを作ろう！
それでは、Nova Canvasを利用したチャットボットアプリを作っていきましょう！
利用する技術スタックはこんな感じです。

|項目|内容|備考|
|--|--|--|
|クライアント側|Streamlit|Pythonで実装されたWebフレームワーク|
|サーバー側|Cloud9|AWS上の統合IDE環境ですが、新規利用は停止|
|言語|Python|3.9以上を利用|

:::note info
Cloud9の新規利用が停止してしまったので、サーバー側はVSCodeサーバーや**Amazon SageMaker Studioコードエディタ**などに置き換えてください。もちろんローカル環境でも動作します。
:::

また、今回は**日本語で指示が可能なシンプルなアイコンを作成できるチャットボット**を作ってみます。
作成する画像の設定はシステムプロンプトやネガティブプロンプトを設定するだけなので、自分の作りたい画像イメージに合わせてプロンプトを変更してみてください。


## 1. 開発環境を構築する
まずは開発環境を整えましょう。
```Python 3.9```以上をインストールし、開発ディレクトリを作りましょう。

```pip install -r requirements.txt```で以下パッケージをインストールしましょう。

```requirements.txt
boto3==1.38.6
streamlit==1.45.0
```

:::note warn
バージョンは最新でも問題なさそうですが、確認の上実施ください。
:::

## 2. チャットアプリの作成
### システムアーキテクチャ
今回作成するチャットアプリのアーキテクチャです。
最初はStreamlitからNova Canvasをboto3経由で呼び出すだけでも良いかと思いましたが、画像生成でのプロンプト指示の仕方を取り入れるため、チャットでの指示文章からキーワードを抽出処理を追加しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3706974/fcabd67b-6f14-49f9-b243-aeef5baf3d34.png)

:::note info
生成AIによる画像生成ですが、少しコツがあるので初心者向けにTipsを共有します。
既に画像生成のプロンプト指示に慣れている方は、読み飛ばしてください。

<details><summary>画像生成プロンプトTips</summary>

### 文章ではなく、単語で指示する
生成AIはプロンプト文を一定のコンテキスト長で区切って理解するため、重要な単語をカンマ区切りで指示した方が良いでしょう。
もちろん、Nova CanvasはTitanと比べて長いコンテキスト理解が強みですが、生成AIモデルが理解しやすい記述方法を用いることで、イメージ通りの画像を作成できます。
* 例：驚いた表情の子供、カラフルな遊び場、アニメ風（Surprised child, colorful playground, anime style）

### 重要な単語を先頭に持ってくる
描きたいもののうち、重要な表現を先頭に持ってくることが必要です。
今回は**シンプルなアイコン生成**をメインテーマにしているため、システムプロンプトとして定義した内容をプロンプト文の先頭に持ってくるようにしています。

### ネガティブプロンプト（負のプロンプト）を活用する
画像生成では、ネガティブプロンプトが重要になります。
特に通常のプロンプト文では```not（～ではない）```や```no（～がない）```などの否定表現はせずに、含めたくないものをネガティブプロンプトで記述することが重要です。
否定表現がプロンプトに含まれていると、生成AIモデルはその単語を含んだイメージを生成してしまう可能性があります。

今回のスクリプトではシンプルなアイコンを生成したいので、デフォルトのネガティブプロンプトには```3D, color, photo```などを追加しています。
またアイコン生成を指示すると1つの画像に複数のアイコンセットを作成される場合があるため、```multiple images```もネガティブプロンプトに含めました。

### 英語で指示する
画像生成モデルは、通常のモデルよりも英語で指示した方がイメージ通りの画像が生成できます。
今回のチャットアプリでは、チャット内容を英語に翻訳したうえで単語抽出を行っていますので、日本語でも対応可能です。
</details>
:::


### チャットアプリコード
作成したチャットアプリのコードです。

<details><summary>nova_canvas_chat_app.py</summary>

```nova_canvas_chat_app.py
import base64
import json
import os
import random
import boto3
import streamlit as st

REGION = "us-east-1"
IMG_MODEL_ID = "amazon.nova-canvas-v1:0"
TXT_MODEL_ID = "anthropic.claude-3-haiku-20240307-v1:0"

@st.cache_resource
def get_bedrock_client():
    return boto3.client(service_name="bedrock-runtime", region_name=REGION)

def generate_image(native_message, image_size, image_num, system_prompt, ng_text):
    """画像生成を行う
    """
    message = system_prompt + native_message
    print(f'textToImageParams: {message}')
    
    seed = random.randint(0, 858993460)
    native_request = {
        "taskType": "TEXT_IMAGE",
        "textToImageParams": {
            "text": message,
            "negativeText": ng_text
        },
        "imageGenerationConfig": {
            "seed": seed,
            "quality": "standard",
            "height": image_size,
            "width": image_size,
            "cfgScale": 10,
            "numberOfImages": image_num,
        },
    }
    request = json.dumps(native_request)
    
    bedrock_client = get_bedrock_client()
    response = bedrock_client.invoke_model(modelId=IMG_MODEL_ID, body=request)
    model_response = json.loads(response["body"].read())
    
    image_path_list = []
    for base64_image_data in model_response["images"]:

        # ローカルフォルダに画像イメージを保存
        i, output_dir = 1, "output"
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)
        while os.path.exists(os.path.join(output_dir, f"nova_canvas_{i}.png")):
            i += 1
        
        image_data = base64.b64decode(base64_image_data)        
        image_path = os.path.join(output_dir, f"nova_canvas_{i}.png")
        with open(image_path, "wb") as file:
            file.write(image_data)
        
        print(f"The generated image has been saved to {image_path}")
        image_path_list.append(image_path)
        
    return image_path_list


def textract_from_input(message):
    """チャットで指示された日本を英語に翻訳し、単語の切り出しを行う
    本処理はNova Canvasではなく、HaikuやSonnetなどの文章処理の基盤モデルを用いる
    """
    bedrock_client = get_bedrock_client()
    system_prompt = "あなたは日本語から英語への翻訳をし、"\
    "翻訳した文の中で重要な意味を持つ英単語を10個未満で抽出してください。"\
    "英単語は意味のある3単語未満でカンマで区切ってください。"\
    "抽出した英単語は、カンマ区切りで出力してください。"\
    "抽出する単語は前置詞や冠詞は含んではいけません。"\
    "<examleInput>かわいい猫と女の子が楽しく遊んでいる</examleInput>"\
    "<examleOutput>cute cat, girl, play, joyful</examleOutput>"
    
    message = {
        "role": "user",
        "content": [{"text": message}]
    }
    messages = [message]
    system_prompts = [{"text" : system_prompt}]
    
    inference_config = {"temperature": 0.5}
    additional_model_fields = {"top_k": 200}
    
    response = bedrock_client.converse(
        modelId=TXT_MODEL_ID,
        messages=messages,
        system=system_prompts,
        inferenceConfig=inference_config,
        additionalModelRequestFields=additional_model_fields
    )
    words = response["output"]["message"]["content"][0]["text"]
    return words


def display_history(messages):
    """チャットの履歴表示
    """
    for message in messages:
        display_img_content(message)


def display_img_content(message):
    """メッセージと画像を表示
    Novaで複数画像を生成した場合にも対応
    """
    contents = message["content"]
    print(f'message内容: {contents}')
    with st.chat_message(message["role"]):
        for content in contents:
            if content.get('text', None) != None:
                st.write(content["text"])
            else:
                st.image(content["image"])


def sidebar():
    """サイドバーを表示
    """
    with st.sidebar:
        st.sidebar.title("画像設定")
        
        # 画像サイズ
        image_size = st.sidebar.slider(
            "画像サイズ",
            min_value=320,
            max_value=960,
            step=64,
            value=320
        )
        
        # 画像数
        image_number = st.sidebar.slider(
            "画像数",
            min_value=1,
            max_value=5,
            step=1,
            value=1
        )
    
        # システムプロンプト
        system_prompt = st.sidebar.text_area(
            "システムプロンプト",
            value="minimalist icon, simple, flat design, dual-line design, 1.5px stroke weight line, "\
            "solid background, monochromatic, 2D, ",
            height=200
        )
        
        # 負のプロンプト
        negative_text = st.sidebar.text_area(
            "ネガティブプロンプト",
            value="3D, color, photo, multiple images",
            height=100
        )
        
        return image_size, image_number, system_prompt, negative_text


def main():
    """メイン処理
    """
    st.title("Simple Icon Generator by Nova Canvas")
    img_size, img_num, system_prompt, negative_text = sidebar()

    if "messages" not in st.session_state:
        st.session_state.messages = []

    display_history(st.session_state.messages)

    if prompt := st.chat_input("What's up?"):
        input_msg = {"role": "user", "content": [{"text": prompt}]}
        display_img_content(input_msg)
        st.session_state.messages.append(input_msg)
        print(f'st.session_state.messages: {st.session_state.messages}')
        
        # 入力された内容の拡張
        print(f'input_msg: {input_msg}')
        textract_resp = textract_from_input(input_msg["content"][0]["text"])
        print(f'texttract: {textract_resp}')

        # 画像生成
        resp_img_path_list = generate_image(
            textract_resp, img_size, img_num, system_prompt, negative_text
        )
        
        resp_img_contents = []
        for resp_img_path in resp_img_path_list:
            resp_img_contents.append({"text": f"Image created! filepath: {resp_img_path}"})
            resp_img_contents.append({"image": resp_img_path})
        resp_msg = {
            "role": "assistant",
            "content": resp_img_contents
        }
        display_img_content(resp_msg)
        st.session_state.messages.append(resp_msg)


if __name__ == "__main__":
    main()
```
</details>

コードはこちらの記事を参考に改編しています。
https://qiita.com/ren8k/items/0191f5e3f02b5b824df0


## 3. アプリを動かしてみる
それでは、アプリを動かしてみましょう！
以下```streamlit```コマンドで作成したアプリを起動します。

```python
streamlit run nova_canvas_chat.py --server.port 8080
```

Cloud9の場合はツールバーにある```Preview```->```Preview Running Application```を開くことで、アプリを表示できます。

:::note info
ローカル環境のVSCode、VSCode Server、 SageMaker Studioコードエディタでも同様かと思います。
:::

こんな感じの起動画面が出てきたら、成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3706974/fb0045b1-981e-4659-9882-a37fd241541e.png)

それでは、チャットで作りたいアイコン画像を指示してみましょう。
今回は```自分のやりたいことを管理できるToDoリストのアイコンを作って。```と指示してみました。
せっかくなら複数提案してもらいたいので、サイドバーから画像数は```3```に設定しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3706974/4dff5c75-365a-4aaf-9f39-9f3c3d65943f.png)

良い感じでToDoリストのアイコンを作ってもらいました！
画像では2つまでですが3つの画像が生成されており、自分のイメージに合ったアイコンを選べます。

ちなみにチャットで指示した内容から、以下の英単語を抽出してもらいました。
多少システムプロンプトと重複していますが、いい感じで動いていそうですね。
```
manage, to-do, list, icon, task, organize, productivity
```

作成した画像は作成したプロジェクトフォルダ内の```output```フォルダに格納されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3706974/3139d29d-0fd4-4433-8c05-80bc0c509b77.png)


# アプリを発展させる
今回作成したチャットアプリの発展を考えてみましょう。

:::note warn
記載内容は未検証のため、以下実装が実現できることは担保いたしませんのでご注意ください。
:::

## 作ったチャットアプリを配信したい！
こちらの記事を参考に、ECS上のコンテナにデプロイすれば実現できます。
ただし、VPCやセキュリティグループ周りの設定で躓きがちなので、ご注意ください。
（個人的にはセキュリティグループのインバウンド設定などで色々とハマりました・・・）

https://qiita.com/minorun365/items/84bef6f06e450a310a6a

## 画像の保存先を変更したい！
ローカルに保存しているのが不便なので、S3バケットなどを保存先としましょう！
特にアプリをEC2やコンテナなどで配信する場合はサーバー上のローカルストレージではなく、S3などのファイルストレージを使うと管理しやすいです。
また画像のファイル名を作成時刻やUUIDなどにすることで、チャットアプリを配信して複数人で使ってもらう場合でもきちんと動作させることができます。

コンテナ化と合わせてアーキテクチャを考えてみると良いでしょう。
Cognitoでユーザー管理もできるといいかもしれません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3706974/50fad37d-48a3-463e-be63-de5edf914b33.png)


## 複数のユースケースに対応したい！
システムプロンプトとネガティブプロンプトをセットで管理することで、「アニメ風なアイコンメーカー」「3Dイラストメーカー」などのユースケースにも対応できます。
また、Bedrockには```Prompt Management```という機能もあるので、プロンプトをBedrock上で管理して利用もできそうな感じです。

https://dev.classmethod.jp/articles/amazon-bedrock-prompt-management-ga/

## 画像を入力して変換してほしい
Nova Canvaには、指定した画像の特定領域での画像の変換（犬を猫に変える）や背景の削除が可能です。
今回のチャットアプリを応用して入力を画像にし、タスクタイプを```INPAINTING```や```BACKGROUND_REMOVAL```にすることで実現可能そうです。

https://qiita.com/ren8k/items/94b5d9bdc513acde371e

# まとめ
Amazon Nova Canvasを使った画像生成チャットアプリを作ってみました。

画像生成自体がほぼ初めてだったので、プロンプトエンジニアリングの入門から始めたのでやや時間がかかりましたが、思い通りの画像が生成できた時の喜びは大きいですね！