amazon-bedrock-audio-summarizer を使ってみた


# はじめに

今年の5月にAmazonのCTOであるワーナー ・ヴォゲルスさんのブログにて「会議音声を要約するアプリ」が紹介されていました。
GitHub上で公開されていたため、今回試しにこちらを利用してみました。

https://www.allthingsdistributed.com/2024/05/hacking-our-way-to-better-team-meetings.html

https://github.com/aws-samples/amazon-bedrock-audio-summarizer

# 構成と処理フロー

構成と処理フローは下記のようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333262/71fc4d7e-d366-7cfc-2c4a-d78643ec466c.png)

1. 音声ファイルをS3バケットに配置
1. 音声ファイルからTranscribeで文字起こしを行い、テキストファイルをS3バケットに配置
1. 文字起こしされたテキストをBedrockで要約し、要約テキストのファイルをS3バケットに配置

# AWS料金について

気になるのはこのアプリの利用によって発生するAWS利用料金です。
これまでの経験から感覚的にLambda、S3、EventBridge、Bedrockについては数回試す程度であれば多くても数ドルで済むくらいだと想像がつきましたが、Transcribeについては未経験であったため調べてみました。

ざっくり理解したところでは下記のようでした。

- 文字起こしを行った音声の秒数に基づいて従量課金で料金が発生
- 1秒単位で課金、1リクエストあたりの最小料金は15秒分の料金となる
- 東京リージョンのスタンダードバッチは最初の250,000分までは$0.024/分
- 最初の文字起こしリクエストを作成した日から12か月間は1か月あたり60分まで無料

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

とりあえず試す分には無料になりそうです。

# やってみた

基本的にはReadMeの [Quickstart](https://github.com/aws-samples/amazon-bedrock-audio-summarizer?tab=readme-ov-file#quickstart) に書かれている手順と同じです。
PCはWindows10、ターミナルはGitBashの環境にて実施しました。
各種ツールのバージョンは下記のとおりです。（※ 環境に関する具体的な条件についてはリポジトリの [Prerequisites](https://github.com/aws-samples/amazon-bedrock-audio-summarizer?tab=readme-ov-file#prerequisites) をご覧ください）
```shell
$ aws --version
aws-cli/2.15.20 Python/3.11.6 Windows/10 exe/AMD64 prompt/off

$ python -V
Python 3.12.4

$ node -v
v20.14.0

$ cdk --version
2.156.0 (build 2966832)
```

リポジトリをクローンします。
```shell
git clone https://github.com/aws-samples/amazon-bedrock-audio-summarizer 
cd amazon-bedrock-audio-summarizer
```

Pythonの仮想環境を作成します。
```shell
python3 -m venv venv
```

仮想環境をアクティベートします。
```shell
source venv/Scripts/activate
```

Pythonの依存パッケージをインストールします。
```shell
pip install -r requirements.txt
```

AWS一時クレデンシャルを取得します。
```shell
aws sso login --profile xxx
```
※私の環境ではIdentiy Centerを利用していたため、`aws sso`コマンドでクレデンシャルを取得しました。クレデンシャルを手動で設定する必要が無くなるので便利です。説明としては以下の記事がわかりやすいのでお勧めです。


https://aws.amazon.com/jp/builders-flash/202306/handle-aws-cli/


CDKのブートストラップを行います。※プロファイル内で東京リージョンを指定

```shell
cdk bootstrap --profile xxx
```

CDKのデプロイを行います。※プロファイル内で東京リージョンを指定

```shell
cdk deploy --profile xxx
```

デプロイ直後はS3バケットは空であるため、`source`というフォルダを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333262/8bb8ec6f-bc29-3414-e96f-40657adecd15.png)

今回はここに30分ほどの長さのMP4ファイルを置いてみたところ、4分ほどでTranscribeのジョブが完了しました。
※Transcribeの対応フォーマットについては下記ドキュメントをご参照ください。

https://docs.aws.amazon.com/ja_jp/transcribe/latest/dg/how-input.html#how-input-audio

Transcribeによる文字起こしが完了すると、`transcription`フォルダ配下に文字起こしされたテキストファイルが出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333262/b90f0775-6dc5-2b35-6b60-693ab6f49457.png)

その後、Bedrockによる要約が行われ、要約テキストが`processed`フォルダ配下に出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333262/3140b098-79c2-2372-0348-dbdd9aee0ed4.png)


このアプリの動作としては以上となります。

# カスタマイズしてみた

下記のカスタマイズを実施してみました。
- モデルをClaude 3.5 Sonnetに変更
- 日本語の文章を生成するよう指示
- 要約テキストをSlackに投稿

## モデルをClaude 3.5 Sonnetに変更

リポジトリ上のコードではモデルにClaude 3 Sonnetが利用されていました。
先月、Claude 3.5 Sonnetが東京リージョンでも利用可能になったためモデルを変更してみました。

https://aws.amazon.com/jp/about-aws/whats-new/2024/08/claude-3-5-sonnet-haiku-more-regions/

ドキュメントでモデルIDを確認します。Claude 3.5 SonnetのモデルIDは`anthropic.claude-3-5-sonnet-20240620-v1:0`です。

https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/model-ids.html

修正箇所は [lambda/eventbridge-bedrock-inference/lambda_function.py#L199](https://github.com/aws-samples/amazon-bedrock-audio-summarizer/blob/main/lambda/eventbridge-bedrock-inference/lambda_function.py#L199) です。

```python:lambda/eventbridge-bedrock-inference/lambda_function.py
response = bedrock_client.invoke_model(
    modelId="anthropic.claude-3-5-sonnet-20240620-v1:0", body=body
)
```

コード修正後、`cdk deploy`で修正を反映しました。

## 日本語の文章を生成するよう指示

実は最初に要約を試した際、Bedrockによって要約されたテキストは英語になっていました。
そこで、Bedrockに渡しているプロンプトを確認してみました。

プロンプトは [eventbridge-bedrock-inference/lambda_function.py#L158-L168](https://github.com/aws-samples/amazon-bedrock-audio-summarizer/blob/main/lambda/eventbridge-bedrock-inference/lambda_function.py#L158-L168) にて定義されています。

```python:lambda/eventbridge-bedrock-inference/lambda_function.py
prompt = f"""Summarize the following transcript into one or more clear and 
readable paragraphs. Speakers in the transcript could be denoted by their name,
or by "spk_x", where `x` is a number. These represent distinct speakers in the 
conversation. When you refer to a speaker, you may refer to them by "Speaker 1"
in the case of "spk_1", "Speaker 2" in the case of "spk_2", and so forth. 
When you summarize, capture any ideas discussed, any hot topics you identify, 
or any other interesting parts of the conversation between the speakers. 
At the end of your summary, give a bullet point list of the key action 
items, to-do's, and followup activities:
```

冒頭の文章に`in Japanese`を加え、`Summarize the following transcript into one or more clear in Japanese`としてみたところ、日本語の要約テキストを得ることができました。

なお、Transcribeによる文字起こしについてはTranscribeが言語を自動認識してくれるため、文字起こしされた文章も日本語でした。

https://docs.aws.amazon.com/ja_jp/transcribe/latest/dg/lang-id.html

## 要約テキストをSlackに投稿

Slackへの要約テキストの自動連携についてはブログの中でも触れられていましたが、公開されているコードには実装されていなかったため、追加してみました。

まず、文字起こしや要約のLambdaを参考に、Slackへコメントを投稿するコードを書きます。
```python:lambda/s3-trigger-slack/lambda_function.py
import os
import logging
import boto3
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from botocore.exceptions import ClientError

logger = logging.getLogger()
logger.setLevel("INFO")

client = WebClient(token=os.environ['SLACK_BOT_TOKEN'])
s3_client = boto3.client("s3")

def lambda_handler(event, context):

    logger.info(f"event : {event}")

    bucket = event["Records"][0]["s3"]["bucket"]["name"]
    key = event["Records"][0]["s3"]["object"]["key"]
    filename = key.split("/")[-1]
    temp_file_path = "/tmp/" + filename

    if key == "processed/":
        logger.info("Processed folder, skipping")
        return {
            "statusCode": 200,
            "body": "Processed folder, skipping"
        }
    
    try:
        s3_client.download_file(bucket, key, temp_file_path)
        logger.info(f"Downloaded {filename} from {bucket}")
    except ClientError as e:
        logger.error(f"Error downloading s3://{bucket}/processed/{filename}: {e}")
        return {
            "statusCode": 400,
            "body": f"Error downloading s3://{bucket}/processed/{filename}: {e}"
        }
    
    with open(temp_file_path, 'r', encoding='utf-8') as file:
        content = file.read()

    try:
        response = client.chat_postMessage(
            channel = os.environ["SLACK_CHANNEL_ID"],
            text = content,
            blocks=[
                {
                    "type": "header",
                    "text": {
                        "type": "plain_text",
                        "text": f":pencil: {filename.split(".")[0]} :pencil:"
                    }
                },
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": content
                    }
                }
            ]
        )
        logger.info(f'SlackResponse: {response}')
    except SlackApiError as e:
        logger.error(f"Error post to slack: {e}")
```

SlackのSDKを利用するためインストールします。

```shell
mkdir -p lambda/layer/python
pip install slack_sdk -t lambda/layer/python
```

下記よりSlackアプリを作成し、Slackのワークスペースにインストールします。（※詳細手順は割愛）

https://api.slack.com/apps

SlackアプリのボットトークンをSystems Managerのパラメータストアに設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333262/e79be8b1-4ad3-b210-d631-23f56571b1c4.png)


ここから`summarizer/summarizer_stack.py`に修正を加えていきます。

`aws_cdk`からのインポートに`aws_ssm`を追加します。
```python:summarizer/summarizer_stack.py
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    aws_lambda as lambda_,
    triggers,
    aws_iam as iam,
    aws_ec2 as ec2,
    aws_logs as logs,

    aws_events as events,
    aws_events_targets as targets,
    aws_s3_notifications as s3_notifications,
    aws_ssm as ssm,

    Duration,
    RemovalPolicy,
)
```

先ほどインストールしたSlack SDKのパッケージを含むLambdaレイヤーを作成します。

```python:summarizer/summarizer_stack.py
layer = lambda_.LayerVersion(
    self,
    "Layer",
    code=lambda_.Code.from_asset("lambda/layer"),
    compatible_architectures=[lambda_.Architecture.ARM_64]
)
```

Slack通知用Lambdaを作るための関数を追加します。

```python:summarizer/summarizer_stack.py
def create_lambda_s3_trigger_slack_function(self, layer):

    lambda_function = lambda_.Function(
        self,
        "S3TriggerSlack",
        function_name="s3-trigger-slack",
        runtime=lambda_.Runtime.PYTHON_3_12,
        code=lambda_.Code.from_asset(os.path.join(os.getcwd(), "lambda", "s3-trigger-slack")), 
        handler="lambda_function.lambda_handler",
        architecture=lambda_.Architecture.ARM_64,
        memory_size=128,
        timeout=Duration.seconds(30),
        environment={
            "SLACK_CHANNEL_ID": "XXXXXXXXXXX",
            "SLACK_BOT_TOKEN": ssm.StringParameter.value_for_string_parameter(self, "/amazon-bedrock-audio-summarizer/slack-bot-token")
        },
        log_retention=logs.RetentionDays.ONE_WEEK,
        layers=[layer]
    )

    lambda_function.apply_removal_policy(
        RemovalPolicy.DESTROY
    )

    # Attach the shared policies
    lambda_function.role.attach_inline_policy(self.s3_policy)

    # Event source
    lambda_function.add_event_source(eventsources.S3EventSource(self.bucket,
        events=[s3.EventType.OBJECT_CREATED],
        filters=[s3.NotificationKeyFilter(prefix="processed/")]
    ))

    return lambda_function
```

ポイントは下記の点です。
- `processed/`フォルダ配下に要約テキストファイルが配置されたことをトリガーとしてLambdaを実行
- 通知先のSlackチャンネルはLambdaの環境変数`SLACK_CHANNEL_ID`にて設定
- パラメータストアに設定したボットトークンの値をLambdaの環境変数`SLACK_BOT_TOKEN`に設定

引数にレイヤーを指定して関数を呼び出します。

```python:summarizer/summarizer_stack.py
self.lambda_s3_trigger_slack_function = self.create_lambda_s3_trigger_slack_function(layer)
```


`cdk deploy`で変更を反映すれば修正は完了です。

音声ファイルをS3に配置したところ、Slackに要約コメントを投稿することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333262/5a814334-75dd-2430-349d-f9da93752e20.png)


※S3に出力された要約テキストのファイル名をタイトルにとりあえず入れてみましたが、これでは何の会議かわからないので、改善点の一つです・・・

# 精度について

感覚的な評価ではありますが、要点は押さえてくれているような印象を受けました。
それでも取りこぼしはありそうなため、実際に利用していく場合は

- 議事メモの叩き台として利用し、人が必要に応じて補足を入れる
- 会議資料もセットで参照できるようにする

等の対処が必要かもしれません。

また、Transcribeによって文字起こしされたテキストには正しく書き起こせていない箇所がまあまああったため、LLMがある程度うまく解釈してくれている部分があるのだと思いました。そのため、LLMによる要約部分ではなく音声をテキスト化する部分に改善の余地があるのかもしれません。LLMがAPI経由で音声も受け付けるようになったときにどうなるかは気になるところです。
なお、Transcribeの精度についてはカスタム語彙を利用することで多少なりとも改善が見込めそうです。（「CCoE」が「年寄り」と文字起こしされていましたので・・・）

https://docs.aws.amazon.com/ja_jp/transcribe/latest/dg/custom-vocabulary.html?icmpid=docs_console_unmapped

# さいごに

会議音声を要約するようなサービスは続々と世に登場してきていますが、今回AWSのソリューションをデプロイしてみて感じたのは、このような仕組みの実装が、AWSマネージドサービスを組み合わせることで**比較的容易にできる**ということです。また、ソリューションをカスタマイズし独自の機能を付け加えることで、更に**使い勝手の良いものに改善していくことができる**点が魅力的だと感じました。

さいごに、ワーナー ・ヴォゲルスさんの記事で印象に残った箇所を引用します。

> Remember, AI is not perfect. Some of the summaries we get back, the above included, have errors that need manual adjustment. But that’s okay, because it still speeds up our processes. It’s simply a reminder that we must still be discerning and involved in the process. Critical thinking is as important now as it has ever been.


> （Google翻訳）
AI は完璧ではないことを忘れないでください。上記を含め、返される要約の一部には、手動で調整する必要があるエラーがあります。しかし、それでもプロセスが高速化されるため、問題ありません。これは、私たちが依然として識別力を持ち、プロセスに関与する必要があることを思い出させるだけです。批判的思考は、これまでと同様に今も重要です。

AIに業務のすべてを代替してもらうのではなく、業務の一部分でも代替させることができれば、それは有意義なことなのだと感じました。

以上です。
お読みいただき、ありがとうございました。

# 弊社では一緒に働く仲間を募集中です
現在、様々な職種を募集しております。
カジュアル面談も可能ですので、ご連絡お待ちしております！

募集内容等詳細は、是非採用サイトをご確認ください。

https://engineer.po-holdings.co.jp/