LINE botで画像生成アプリを作ってみた【Amazon Bedrock/LINE bot/AWS CDK v2】

## はじめに

生成AIとても流行っていますね。  
私もかねてから生成AI、特に画像生成AIに興味があったため Amazon Bedrock を利用して画像生成アプリを作ってみました。

本内容は2024年11月時点での情報を元に記載しています。  
最新の情報はAWSの公式ドキュメントを参照してください。

## Amazon Bedrock とは

[Amazon Bedrock](https://aws.amazon.com/jp/bedrock/) とは、主要な基盤モデルを選択して利用できるサービスで、セキュリティやプライバシーに配慮された生成AIアプリケーションを構築できます。  
開発者はAPIを通じて生成AIを呼び出すことができ、アプリに簡単にAI機能を組み込むことができます。  
さらに、サーバーレスであるためインフラストラクチャの管理が不要です。

## 構成図

本アプリの構成図は以下になっています。  
![構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37998/a0e6740b-b83f-3caf-9e15-fc5f2732e9e2.png)


## 実装

ソースコードの全体像は [こちら](https://github.com/kawasaki-n/bedrock-image-generator) になります。  
AWSリソースはCDKで作成していきます。

### モデルの有効化

Amazon Bedrockで利用する基盤モデルを有効化します。  
今回画像生成に利用するTitanモデルは東京リージョンだと利用できないため、バージニア北部リージョンを利用します。

1. AWSコンソールにログインしAmazon Bedrockに移動
2. リージョンを「バージニア北部(us-east-1)」に変更
3. 「モデルアクセス」から「モデルアクセスを変更」を選択
   ![モデルの有効化1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37998/d76865f8-b5d1-65d5-0e9f-3b95fba0d710.png)
4. 「Titan Image Generator G1 v2」にチェックを入れて次へ
   ![モデルの有効化2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37998/beba32c3-c7ac-80eb-71e6-4b3a18bd6155.png)
5. そのまま送信
   ![モデルの有効化3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37998/de441f12-42f1-ff08-fca1-b0e391306bbb.png)

6. アクセスのステータスが「アクセスが付与されました」になっていればOK
   ![モデルの有効化4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37998/91fc3e13-f2d4-0d40-bebd-9b779e827f19.png)


### S3バケットの作成

生成した画像を保存するS3バケットを作成します。

```ts
const accountId = cdk.Stack.of(this).account;
const region = cdk.Stack.of(this).region;

const bucket = new s3.Bucket(this, `BedrockBucket`, {
  bucketName: `bedrock-bucket-${accountId}-${region}`,
  accessControl: s3.BucketAccessControl.PRIVATE,
  objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED,
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
  autoDeleteObjects: true,
  encryption: s3.BucketEncryption.KMS_MANAGED,
});
```

### Lambdaの作成

LambdaからBedrockのモデルの呼び出しとS3のアップロード/ダウンロードができるようにRoleを作成します。  
Model ARNはAmazon Bedrockのプロバイダーから確認できます。  
![lambdaの作成1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37998/1a3fd87d-ccb2-f2b5-7163-63fcb1951580.png)


```ts
const bedrockLambdaFunctionRole = new iam.Role(this, 'BedrockLambdaFunctionRole', {
  roleName: 'bedrock-lambda-function-role',
  assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
  ],
  inlinePolicies: {
    BedrockInvokeModel: new iam.PolicyDocument({
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ['bedrock:InvokeModel'],
          resources: [
            'arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-image-generator-v2:0',
          ],
        }),
      ],
    }),
    S3PutObject: new iam.PolicyDocument({
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ['s3:GetObject', 's3:PutObject'],
          resources: [bucket.arnForObjects('*')],
        }),
      ],
    }),
  },
});
```

続いてLambdaでライブラリを利用したいためレイヤーを作成します。  
今回はAWSが提供しているLambdaレイヤーのAWS SDK for Pandasを利用することにしました。  
Layer Arnは[こちら](https://aws-sdk-pandas.readthedocs.io/en/stable/layers.html)から確認することができます。

```ts
const lambdaLayer = lambda.LayerVersion.fromLayerVersionArn(
  this,
  'BedrockLambdaLayer',
  'arn:aws:lambda:us-east-1:336392948345:layer:AWSSDKPandas-Python312-Arm64:6'
);
```

続いてLambda関数を作成します。  
Lambdaの処理で利用する値を環境変数に渡しています。

```ts
const bedrockLambdaFunction = new lambda.Function(this, 'BedrockLambdaFunction', {
  functionName: `bedrock-function`,
  code: lambda.Code.fromAsset(path.join(__dirname, `../src/lambda/bedrock`)),
  handler: 'index.lambda_handler',
  runtime: lambda.Runtime.PYTHON_3_12,
  timeout: cdk.Duration.seconds(60),
  architecture: lambda.Architecture.ARM_64, //X86_64,
  environment: {
    S3_BUCKET_NAME: bucket.bucketName,
    LOG_LEVEL: 'INFO',
    LINE_CHANNEL_SECRET: process.env.LINE_CHANNEL_SECRET || '',
    LINE_CHANNEL_ACCESS_TOKEN: process.env.LINE_CHANNEL_ACCESS_TOKEN || '',
  },
  role: bedrockLambdaFunctionRole,
  layers: [lambdaLayer],
});
```

続いてLambda関数の実装です。  
Amazon Bedrockの基盤モデルへAPIリクエストし画像を生成します。  
APIリクエストの例はAmazon Bedrockのプロバイダーから確認できます。
![lambdaの作成2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37998/74abdd23-a8b9-27e4-50c9-c6f1cd5f4f03.png)  
画質、画像サイズなどは自分好みに調整してください。  
seed値は固定にしておくと同じ入力で同じ画像を生成することができます。  
私は色々な画像生成を楽しみたかったので、ランダム値にしています。

```python
def invoke_titan_image(prompt):
    try:
        request = json.dumps(
            {
                "taskType": "TEXT_IMAGE",
                "textToImageParams": {"text": prompt},
                "imageGenerationConfig": {
                    "numberOfImages": 1,
                    "quality": "standard",
                    "cfgScale": 8.0,
                    "height": 512,
                    "width": 512,
                    "seed": random.randint(0, 2147483646),
                },
            }
        )
        response = bedrock_runtime.invoke_model(
            modelId="amazon.titan-image-generator-v2:0", body=request
        )
        response_body = json.loads(response["body"].read())
        base64_image_data = response_body["images"][0]
        return base64_image_data
    except ClientError as e:
        logger.error(f"Couldn't invoke Titan Image generator: {e}")
        raise
```

生成した画像をS3にアップロードします。

```python
image_data = base64.b64decode(base64_image_data)
object_key = (
    f"generated_images/{datetime.now().strftime("%Y%m%d%H%M%S")}_{random_uuid}/image_{input_text.replace(' ', '_')}.jpg"
)

s3.put_object(
    Bucket=bucket_name,
    Key=object_key,
    Body=image_data,
    ContentType="image/jpeg",
)
```

### API Gatewayの作成

LINE botからLabmda関数を実行できるようにするためにAPI Gatewayを作成します。

```ts
const api = new apigwv2.HttpApi(this, 'BedrockLambdaApi', {
  apiName: 'BedrockExecuteApi',
  corsPreflight: {
    allowHeaders: ['*'],
    allowOrigins: ['*'],
    allowMethods: [apigwv2.CorsHttpMethod.ANY],
  },
});
api.addRoutes({
  path: '/',
  methods: [apigwv2.HttpMethod.POST],
  integration: new HttpLambdaIntegration('BedrockLambdaIntegration', bedrockLambdaFunction),
});
```

### LINE botの作成

以前はLINE DevelopersコンソールからMessageing APIチャネルを作成できたのですが、[2024/09/04以降できなくなった](https://developers.line.biz/ja/news/2024/09/04/no-longer-possible-to-create-messaging-api-channels-from-console/)ようです。  
[公式の手順](https://developers.line.biz/ja/docs/messaging-api/getting-started/)を参考にMessage APIの利用を有効にします。

Message APIを有効にしたら、LINE Developersコンソールからbotを作成します。  
[こちら](https://developers.line.biz/ja/docs/messaging-api/building-bot/)を参考にチャネルアクセストークンの作成とWebhook URLを設定します。  
チャネルアクセストークンには少し試したいだけなら短期のチャネルアクセストークンを発行しておけば良いかと思います。  
Webhook URLにはAPI Gatewayで作成したエンドポイントを指定しましょう。  
LINE Developersコンソールで確認できるチャネルシークレットとチャネルアクセストークンを.envファイルに記載しておきます。

### LINE botからの署名検証

[LINE botから送信されたメッセージの送信者の確認を行う](https://developers.line.biz/ja/docs/messaging-api/receiving-messages/#verify-signature)ため、署名の検証をLambdaに実装します。

```python
def verify_signature(event):
    try:
        header_signature = event["headers"]["x-line-signature"]
        body = event["body"]
        channel_secret = os.environ.get("LINE_CHANNEL_SECRET")
        hash = hmac.new(
            channel_secret.encode("utf-8"), body.encode("utf-8"), hashlib.sha256
        ).digest()
        signature = base64.b64encode(hash)
        return header_signature == signature.decode('utf-8')
    except ClientError as e:
        logger.error(f"Error happnend when verified signature: {e}")
        raise
```

### LINE botへ生成した画像の送信

[こちら](https://developers.line.biz/ja/docs/messaging-api/sending-messages/#reply-messages)を参考に、botに対して生成した画像を送信します。

```python
# S3から署名付きURLを取得
presigned_url = s3.generate_presigned_url(
    "get_object",
    Params={"Bucket": bucket_name, "Key": object_key},
    ExpiresIn=3600,
)

# LINEに返信する
data = {
    "replyToken": event.get("replyToken"),
    "messages": [
        {
            "type": "image",
            "originalContentUrl": presigned_url,
            "previewImageUrl": presigned_url
        }
    ]
}
try:
    channel_access_token = os.environ.get("LINE_CHANNEL_ACCESS_TOKEN")
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer {}'.format(channel_access_token)
    }
    response = requests.post(REPLY_ENDPOINT, json=data, headers=headers)
    logger.info(f"response: {response}")
except ClientError as e:
    logger.error(f"Error happnend when reply: {e}")
    raise
```

以上で一通りの実装は終了です。

## 動作確認

LINE DevelopersコンソールからQAコードを読み取り、botを友だち登録します。  
botにメッセージを送ると、生成した画像が返ってくるか確認してみましょう。  
うまく実装できていれば以下のようになります。  
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37998/535cc82f-37c2-5dda-6f15-eb10cf141504.jpeg" width="50%">



注意点として、Titanモデルは日本語対応できていません。  
送るメッセージは英語で送信しましょう。

## 実装時にハマったポイント

当初LINE botからの署名検証にはLambda Authorizerを利用しようとしました。  
署名検証にはrequest bodyが必要なのですが、Lambda Authorizerではrequest bodyを受け取ることができないようでしたので断念し、全ての処理を1つのLambdaにまとめました。

## 応用例

[こちらの記事](https://aws.amazon.com/jp/blogs/news/fine-tune-your-amazon-titan-image-generator-g1-model-using-amazon-bedrock-model-customization/) を参考にすることで、Amazon Bedrock のモデルをファインチューニングすることもできるとのことです。  
自分好みの画像を生成したい場合はチャレンジしてみてはいかがでしょうか。

## 最後に

このようにAWSが提供している基盤モデルを利用して簡単に画像生成アプリを作ることができました。  
この記事が生成AIを触るきっかけになれれば嬉しいです。

## 参考にさせていただいたサイト

- https://zenn.dev/issy/articles/zenn-bedrock-apigw-tried-it