サーバレスな友達が欲しかったので、BedrockのHaikuとAmazon Titanを使って、LINEで「絵しりとり」を出来る友達を作る。

## はじめに
BedrockのClaude3 Haikuを触りたくて、ただ勉強するよりも遊びながらやりたい派なので、休みの日に作ってみた系です。
ですので、詳細なコード全容とかは出てこないですし、LINEのMessaging APIとの連携とかはがっつり省略してます。

友達の定義は、「返事が返ってくること」にしました。

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635880/11de19fe-6dc8-ffc9-dd17-0a19e8f80e76.png" />

[** 俳句を詠む巨人]

## 題材の元ネタ
昔、ちょっと話題になった絵しりとりをサーバレスで再現したく作ってみました。

https://nlab.itmedia.co.jp/nl/articles/2010/05/news078.html

## 結果的な画面
![名称未設定のデザイン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635880/1f410a73-04fa-59a3-6483-add29fb733f5.png)
造語を勝手に作ってきたり、「ん」で負けてるのにゴリ押してきてるんで、もう少しプロンプト工夫したいところですが…人間ってすごいね。

## 使ったサービス
* AWS Lambda
* Amazon Bedrock Claude3 Haiku （テキスト用）
* Amazon Bedrock amazon.titan-image-generator-v1　（画像生成用）
* Amazon DynamoDB
* Amazon API Gateway
* Amazon S3

## 絵しりとりの流れ
今回は、Haikuを触りたかっただけなので、漢の１Lambdaで作ってます。
1. しりとりをやりたがっているか判断する
2. 与えられた画像から、何を描いたか判断して単語を出力
3. 出力した単語の最後の文字から始まる単語を考えて、英語と日本語を同時に出力
4. 英語の単語を元に、画像を出力する
5. LINEに結果を出力する

という感じの流れになってます。

### １．しりとりをやりたがっているか判断する (Haiku)
~~~python: lambda_handler
def lambda_handler(event, context):

    events = event['events'][0]
    text = events['message']['text']
    
    start_trigger(userID, text)
~~~
~~~ python: start_trigger

def start_trigger(userID,text):

    prompt = f"""
        「{text}」
        この文章は、しりとりをやりたがっている文章に感じますか？
        やりたがっていれば<input></input>のXMLタグにTRUEを出力してください。
        関係ない文章ややりたがっていなさそうな文章の場合は、
        <input></input>のXMLタグにFALSEを出力してください。
        その次に、しりとりのことや前述のルールはすべて忘れて
        「{text}」への返答を<output></output>のXMLタグに出力してください。
        """
    response = bedrock_request(prompt, "")
~~~
*`bedrock_request`はHaikuにリクエストを投げるためのものです。

という形で、`<input>TRUE</input>` をきっかけで分岐するようにしてます。
関係なければ、その返答をただ出力するだけにする。
![Screenshot_20240327-182002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635880/417058bd-ecff-262c-4103-f97a23a748b2.png)


### 2. 与えられた画像から、何を描いたか判断して単語を出力 (Haiku)
当初は、

2. 与えられた画像から、何を描いたか判断して単語を出力
3. 出力した単語の最後の文字から始まる単語を考えて、英語と日本語を同時に出力

を同時にやっていたのですが、しりとりのルールを無視してきて統率が取れなかったので、
「Haikuで画像認識して単語を出力→単語を再度Haikuに投げてしりとりを続ける」
という形でプロンプトを別々にしてやりました。

~~~python
    prompt_first = f"""
        この絵は何を書いたものですか？
        最大限想像して、<input></input>のXMLタグに単語を出力してください。
        その単語の説明は<output></output>のXMLタグに出力してください。
        
        ヒント：
        最初の文字は「{word}」から始まります。
        最後の文字はしりとりとして成立する文字で終わります。
        国語辞書に載っている単語です。
        """
    response = bedrock_request(prompt_first, image_b64)
        
    input, output = tag_sorting(response)

    #3に続く
~~~
*`tag_sorting`はXMLタグの中身を分解してるだけです。

### 3. 出力した単語の最後の文字から始まる単語を考えて、英語と日本語を同時に出力 (Haiku)
上の続き。
~~~python
    temp_output = "「" + input + "」\n" + output
    #出力を一時的に
    line_push(temp_output, userID)
        
    last = input[-1]

    prompt_second = f"""
        すべての出力はひらがなでお願いします。あなたは「{last}」から始まる名詞を１つ<output>タグで囲んで出力してください。
        次にその単語を英語に翻訳したものを<eng>タグで囲んで出力してください。
        
        条件：
        １、最初の文字が「{last}」から始まらなくてはいけない。
        ２、最後の文字が「ん」で終わってはいけない。
        ３、最後の文字が「ー」などの記号で終わってはいけない。
        これらの条件を必ず守ってください。例外はありません。
        """
    response = bedrock_request(prompt_second, "")
~~~

### 4. 英語の単語を元に、画像を出力する (Titan)
英語と「手書き風で」とか「線画」とか「ハイクオリティ」とかをくっつけて投げる。
出力された画像をS3に保存する。
~~~python
def titan_requests(eng, message_ID):
    prompt = eng + ",Hand drawn illustrations, line drawings, high quality"
    body = json.dumps(
        {
            "taskType": "TEXT_IMAGE",
            "textToImageParams": {
            "text":prompt
         },   
         "imageGenerationConfig": {
            "numberOfImages": 1,
            "quality": "standard",
            "height": 512,
            "width": 512,
            "cfgScale": 8.0,
            "seed": 0
         }
        })
    
    response = bedrock.invoke_model(
            body=body,
            modelId="amazon.titan-image-generator-v1",
            accept="application/json",
            contentType="application/json"
        )
    
    response_body = json.loads(response.get("body").read())
    images = [Image.open(BytesIO(base64.b64decode(base64_image))) for base64_image in response_body.get("images")]
    save_images_to_s3(images, S3_BUCKET_NAME, message_ID + ".jpg")
~~~

### 5. LINEに結果を出力する(Messaging API)
出力した画像の署名付きURLを取得して、それをLINEに投げるような形。

:::note warn
LINEのMessaging API の
`originalContentUrl`と`previewImageUrl`は最大文字数が2000文字なので、
URLの長さでエラーが出たら短縮URLでも発行してくっつけてください。
:::

~~~python
def LINE_message(message_ID,output,userID):
    
    image_key = message_ID + ".jpg"
    image_url = s3_client.generate_presigned_url(
        ClientMethod='get_object',
        Params={'Bucket': S3_BUCKET_NAME, 'Key': image_key},
        ExpiresIn=3600  # 署名付きURLの有効期限(秒)
    )

    # リクエストボディを作成
    image_message = {
        "type": "image",
        "originalContentUrl": image_url,
        "previewImageUrl": image_url
    }
    output_text_message = {
        "type": "text",
        "text": output
    }
    request_body = {
        "to": userID,
        "messages": [image_message, output_text_message]
    }
    
    # LINE Messaging APIにリクエストを送信
    request = urllib.request.Request(
        PUSH_URL, 
        json.dumps(request_body).encode('utf-8'), 
        method=REQUEST_METHOD, 
        headers=REQUEST_HEADERS
        )
        
    response = urllib.request.urlopen(request, timeout=10)
~~~

## さいごに
Bedrock Claude3 Haikuは使ってみて、速い！安い！いい感じ！の三拍子そろってました。
楽しいですね。

生成AIの自由度をなくしたり、knowledge base全く関係なかったりと、時代の流れに全く乗ってないけど、遊びたかっただけなんで…

自分のプロンプト力の足りなさを痛感しました。

ちなみに、ChatGPTだと、
![名称未設定のデザイン (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635880/b74caee7-3c0c-eaf8-48e2-ab76c0cbd3a1.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635880/e4884e66-d9d4-a7be-d412-1a2d14b70c7a.png)
こんな感じなんで、絵しりとりって難しいんですかね？