聖なる夜のプレゼントに困っているすべての人に手助けを

# はじめに
はじめまして、２児の父のまさおさんです。

今年も早いものでもう残り1週間です。
そして今日はなんとクリスマスイブです。

クリスマスイブといえば、、、そう！！プレゼントですね。ですよね。

私はプレゼントを選ぶのがめちゃくちゃ苦手です。
いつもどこかのサイトを見て参考にしています。

今年も子どもになんのプレゼントあげようかなーと悩み、探しましたがピンとこず。
悩んでいましたが「そうだ！Bedrockに聞こう！」
と思いつきました。

ということで Bedrock にプレゼントを検討してくれるようお願いすることにしました。
これで今年のプレゼントは最高のものを贈れるぜい。

## 動作イメージ
年齢、性別、あげる相手がどんな人なのか？をパラメーターで渡します。
その情報を元に4パターンくらいのプレゼントを考えてもらいます。
また文字だけでは少し寂しいので、そのプレゼントの画像も生成して実際のイメージを膨らませます！（？）

さぁどんな提案をしてくれるのか！？

## 構成イメージ
![サンタ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492457/eb6bc52f-8970-5d5c-4520-4a8ccc9409a1.png)


構成はシンプルにしました。
・Lambdaの関数URLを有効化
　curlでLambdaを実行、LambdaからBedrockを実行しています。
・パラメーターを元にBedrock（テキスト生成）を実行
・Bedrock（テキスト生成）を解析し、Bedrock（画像生成）を実行
・作成された画像データをS3に保存
となります。

Bedrockのモデルは
・テキスト生成「Titan Text G1 - Premier」（amazon.titan-text-premier-v1:0）
・画像生成「amazon.titan-image-generator-v2:0」
としました。


## 構築ポイント
・最初構築を「ap-northeast-1」でやっていましたがモデル数の問題もあり、途中から「us-east1」に変更しました。
・テキスト生成のモデルを「Titan Text G1 - Lite」「Titan Text G1 - Express」も試しましたが思うようなレスポンスが得られず、「Titan Text G1 - Premier」にすることでそれっぽい形になりました。
・Bedrockを実行した際に結構実行時間にばらつきがあり、急にタイムアウトすることが何回かありました。
　このあたりは何が原因で時間がかかったのかは理解できていないので、Lambdaのタイムアウト時間を5分まで伸ばして様子見しました。


## 実際に動かした結果（1回目）
構築が完了したので実際に動かしてみます。どきどき。

パラメーターには
・年齢 13歳
・性別 男
・どんな子？ 
　野球とゲームが好き、明るくて運動が好きな男の子。最近スマホを買ってもらった。好きな女の子がいるらしい。お肉とお寿司が好き。
 
 を与えました。

さぁ何を提案してくれるのか・・・！！！
出来上がった画像はこちらです！

![ゲームソフト-241223-21392110.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492457/2bd16998-3f18-01df-d298-b15448513488.png)
![ゲーム機-241223-21392869.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492457/1243c818-47f8-c8ae-58c0-952694a91193.png)
![スマホ-241223-21393638.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492457/7e71e582-ee40-2651-107b-7e63d2a99d4e.png)
![ゲームカード-241223-21394389.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492457/bb687f2f-db24-e1f8-eeb7-9523abba3704.png)

ちなみに画像生成する際のテキストは上から
「ゲームソフト」「ゲーム機」「スマホ」「ゲームカード」
でした。
うむむむ・・・。

そういえばプレゼントを贈る側の情報をインプットできていなかった。
プレゼントを贈るには関係性はめちゃくちゃ大事ですよね。


## 実際に動かした結果（2回目）
ということで先ほどのパラメーターに加えてみます。わくわく。
またここから先は妄想の話なので、この記事を書いている人とは関係ありません。
※ 実際はおじさんが子どもにプレゼントを送ろうとしています。

・プレゼントを贈る子
　女の子で手料理と編み物が得意です。
　また男の子とすごく仲良くなりたいと思っています。

 ![バッグボール-241223-21444193.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492457/ebf641d0-696a-2668-2deb-2118df2481e7.png)
![ゲームコンソール-241223-21444975.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492457/be74fa2f-f4c8-3445-8786-cd94f8c4fb21.png)
![バックパック-241223-21445729.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492457/26ea2711-005a-c099-ca7a-4ddae352559c.png)
![手料理-241223-21450500.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492457/1bdcf6c6-ca72-675b-311a-cc316d5be5fd.png)

ちなみに画像生成する際のテキストは上から
「バッグボール」「ゲームコンソール」「バックパック」「手料理」
でした。

ちゃんと与えた情報を元にプレゼントを選んでくれてますね。
ただあんまり関係ないものも含まれており、要改善に見えます。
とはいえ、一旦今回の検証はここまでとします。

## ソースコード
今回実行したソースコードです。

```python:lambda_function.py
import base64
import boto3
import datetime
import json
import random
import sys
from botocore.exceptions import ClientError

BUCKET_NAME = 'xxxxx-xxxxx-bucket'
s3 = boto3.client('s3', region_name="us-east-1")
bedrock = boto3.client(service_name='bedrock-runtime')

class ImageError(Exception):
    "Custom exception for errors returned by the model"

    def __init__(self, message):
        self.message = message

def lambda_handler(event, context):
    try:
        # bedrock の モデルを指定
        model_id = "amazon.titan-text-premier-v1:0"

        age = get_parameter("age", event)
        gender = get_parameter("gender", event)
        donnako = get_parameter("donnako", event)
        okuruhito = get_parameter("okuruhito", event)
        
        # プロンプトの設定
        # bedrock に投げる質問を作成
        prompt = f"""
        以下の条件に合う子に相応しいプレゼントの候補を出してほしい。
        ・年齢：{age}
        ・性別：{gender}
        ・どんな子： {donnako}
        ・贈る人：{okuruhito}

        プレゼントは具体的な商品名だけを書いてください。
        1つ目. 予算100円で買えるもの
        2つ目. 予算3000円で買えるもの
        3つ目. 予算1000円で買えるもの
        4つ目. 手作りのもの
        """
        
        # Bedrock呼び出し
        res_text = generate_text(model_id, create_text_request_body(prompt))
        logger.info(f"{res_text=}")

        # 受け取ったテキストを分割
        presents = res_text.replace("1つ目", "").replace("2つ目", "").replace("3つ目", "").replace("4つ目", "").split("\n")
        logger.info(f"{presents=}")

        for present in presents:
            now = datetime.datetime.now()
            formatted_date = now.strftime('%y%m%d-%H%M%S%f')[:-4]
            file_name = f"{present}-{formatted_date}.png"
            generate_image(present, file_name)

    except ClientError as err:
        message = err.response["Error"]["Message"]
        logger.error("A client error occurred: %s", message)
    except ImageError as err:
        logger.error(err.message)

    return {
        'statusCode': 200,
        'body': json.dumps(prompt)
    }

def create_text_request_body(prompt):
    """titan-textのリクエストするbody作成

    Args:
        prompt : 質問内容

    Returns:
        dict : リクエストボディ
    """    

    return json.dumps({
        "inputText": prompt,
        "textGenerationConfig": {
            "maxTokenCount": 3072,
            "stopSequences": [],
            "temperature": 0,
            "topP": 1
        }
    })

def create_image_request_body(prompt, seed):
    """amazon.titan-image-generator-v2:0にリクエストするbody作成

    Args:
        prompt : 質問内容
        seed : seed値

    Returns:
        dict : リクエストボディ
    """    
    return json.dumps({
        "textToImageParams": {
            "text": prompt
        },
        "taskType": "TEXT_IMAGE",
        "imageGenerationConfig": {
            "cfgScale": 5,
            "seed": seed,
            "quality": "standard",
            "width": 512,
            "height": 512,
            "numberOfImages": 1
        }
    })

def generate_text(model_id, body):
    """テキスト生成

    Args:
        model_id : Bedrockのモデル
        body : リクエストボディ

    Returns:
        str: Bedrockから戻ってきたテキスト
    """    

    accept = "application/json"
    content_type = "application/json"

    # bedrockの実行
    response = bedrock.invoke_model(
        body=body, modelId=model_id, accept=accept, contentType=content_type
    )

    # レスポンスデータから戻り値を取得
    response_body = json.loads(response.get("body").read())    
    res = ""
    for result in response_body['results']:
        res += result['outputText']
    
    return res

def generate_image(prompt_data, file_name):
    """画像の生成

    Args:
        prompt_data : 質問内容
        file_name : 画像のファイル名
    """    

    modelId = "amazon.titan-image-generator-v2:0" 
    accept = "application/json"
    contentType = "application/json"

    # 画像のランダム性を与えるためシードを生成
    seed = random.randint(0, 2147483646)

    req_body = create_image_request_body(prompt_data, seed)
    response = bedrock.invoke_model(
        body=req_body, modelId=modelId, accept=accept, contentType=contentType
    )

    model_response = json.loads(response["body"].read())
    base64_image_data = model_response["images"][0]
    image_data = base64.b64decode(base64_image_data)

    # S3バケットへ出力
    s3.put_object(Bucket=BUCKET_NAME, Key=file_name, Body=image_data)

```

## まとめ
今回お試しでやってみましたが、上手くBedrockに情報を渡せていなかったからか
「これだ！！」っていうプレゼントには巡り会えませんでした。
生成AIは使う側のスキルも必要かつ、使う場面はよく考える必要がありますね。