AWS初心者向けハンズオンでAIサービスを触ってみる

今回は、AIサービスを触ったことがないのでAWS 初心者向けハンズオンで紹介されている下記をやってみます。
[AWS Lambda と AWS AI Services を組み合わせて作る音声文字起こし & 感情分析パイプライン](https://aws.amazon.com/jp/aws-jp-introduction/aws-jp-webinar-hands-on/?trk=aws_blog#:~:text=AWS%20Lambda%20%E3%81%A8%20AWS%20AI%20Services%20%E3%82%92%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%81%A6%E4%BD%9C%E3%82%8B%E9%9F%B3%E5%A3%B0%E6%96%87%E5%AD%97%E8%B5%B7%E3%81%93%E3%81%97%20%26%20%E6%84%9F%E6%83%85%E5%88%86%E6%9E%90%E3%83%91%E3%82%A4%E3%83%97%E3%83%A9%E3%82%A4%E3%83%B3%20%C2%BB)

Terraformを最近触り始めたので、こちらでリソース作成しながら進めてみようと思います！

# 動作環境
Cloud9 
Terraform v0.13.0
+ provider registry.terraform.io/hashicorp/archive v2.2.0
+ provider registry.terraform.io/hashicorp/aws v3.69.0


# Amazon TranscribeとAmazon Comprehendってなに？
Amazon Transcribeとは、音声をテキストに変換してくれる**文字起こし**サービス
保存された音声ファイルだけでなく、リアルタイム変換も可能

・ユースケースの例
　・記者会見等の録音データを文字起こし
　・動画コンテンツの字幕作成
　・コールセンター業務の通話の文字起こし

Amazon Connect等のサービスと組み合わせれば色々と使えそうですね。

Amazon Comprehendとは、機械学習を使用した**自然言語処理**サービス
テキストの中の有用な情報を発見・分析やキーフレーズやエンティティ(場所・日付)の取得、感情分析(ポジネガ分析)が可能

・ユースケースの例
　・社内ドキュメントのキーフレーズやエンティティを検索エンジンでインデックス
　・レビューやアンケート結果、ソーシャルメディアからのFeedback を感情分析

感情分析ができるのは知っていたのですが、キーフレーズやエンティティの取得も可能なのは知らなかったです…
ドキュメントの分類等にも使えて便利そうですね。

# 構成図
![AWS Design 2 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/940736/74d7c879-d041-90cc-685e-82e81eaffcf0.png)

折角なので、cacooを使ってハンズオン構成図を真似ました。
使い慣れてないと難しい…

**処理の流れ**
1. Pollyから音声ファイル(mp3)を作成
2. S3(input用)に保存(トリガー)
3. Lambda実行(transcribe-function) 音声から文字生成
4. 3の結果を、S3(output用)にjsonファイルで保存(トリガー)
5. Lambda実行(comprehend-function) 文字から感情分析
6. 実行結果がcloudwatchlogsに保存される。(/aws/lambda/comprehend-function)


# Terraform環境準備
ちょうど弊社のアドベントカレンダーに環境準備の記事があるので参考にどうぞ
[Cloud9 + CodeCommitでサクッとterraform実行環境を作成する](https://qiita.com/d_e_n/items/1539623175e2370a60cd)

# ディレクトリ構成
```
.
├── build
│   ├── function1
│   └── function2
├── build.sh
├── lambda
│   ├── function1.zip
│   └── function2.zip
│   
├── main.tf
├── src1
│   └── transcribe-function.py
├── src2
│   └── comprehend-function.py
├── terraform.tfstate
└── terraform.tfstate.backup

```
src1 src2 にLambdaで実行するコードを保存しています。
build.shを実行するとアップロード用にbuildされたzipファイルが作成されます。

# ディレクトリ作成
```bash
mkdir -p handson/lambda handson/src1 handson/src2 
```
まずは、ハンズオン用ディレクトリ、Lambdaにコードをアップロード時に必要となるディレクトリを作成します。
<br>



# Lambdaアップロード準備
### pythonファイル作成
```bash
vi /src1/transcribe-function.py
vi /comprehend-function.py
```

```python:transcribe-function.py
import json
import urllib.parse
import boto3
import datetime

s3 = boto3.client('s3')
transcribe = boto3.client('transcribe')

def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    try:
        transcribe.start_transcription_job(
            TranscriptionJobName= datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '_Transcription',
            LanguageCode='ja-JP',
            Media={
                'MediaFileUri': 'https://s3.ap-northeast-1.amazonaws.com/' + bucket + '/' + key
            },
            #OutputBucketName='*Your Output Bucket*'
            OutputBucketName='h4b-serverless-t-output'
        )
    except Exception as e:
        print(e)
        print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket))
        raise e
```

```python:comprehend-function.py
import json
import urllib.parse
import boto3

s3 = boto3.client('s3')
comprehend = boto3.client('comprehend')

def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    try:
        response = s3.get_object(Bucket=bucket, Key=key)

        body = json.load(response['Body'])
        transcript = body['results']['transcripts'][0]['transcript']

        sentiment_response = comprehend.detect_sentiment(
            Text=transcript,
            LanguageCode='ja'
        )

        sentiment_score = sentiment_response.get('SentimentScore')
        print(sentiment_score)
    except Exception as e:
        print(e)
        print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket))
        raise e
```

```bash
vi /src1/transcribe-function.py
vi /comprehend-function.py
```

Lambdaで実行させるコードを準備します。
S3Bucket名はユニークである必要があるため、transcribe-function.pyに記載してる**OutputBucketName**は適宜変更してください。
変更した場合は、tfファイルの方でも変更が必要となります。

### アップロード準備
```bash
vi bulid.sh
```
```bash:build.sh
#!/bin/bash

if [ -d build ]; then
  rm -rf build
fi

# Recreate build directory
mkdir -p build/function1/ build/function2/

# Copy source files
echo "Copy source files"
cp -r src1 build/function1/
cp -r src2 build/function2/ 
```
```bash
sh build.sh
```
build用のスクリプトを作成。
pythonファイル作成は終わってるので実行。


# tfファイル作成
```bash
vi main.tf
```

```terraform:main.tf
# Terraform Setting
terraform {
  required_version = "0.13.0"
}

# Role Policy

data "aws_iam_policy" "AmazonS3FullAccess" {
  arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}

data "aws_iam_policy" "AmazonTranscribeFullAccess" {
  arn = "arn:aws:iam::aws:policy/AmazonTranscribeFullAccess"
}

data "aws_iam_policy" "ComprehendFullAccess" {
  arn = "arn:aws:iam::aws:policy/ComprehendFullAccess"
}

data "aws_iam_policy" "CloudWatchLogsFullAccess" {
  arn = "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
}

data "aws_iam_policy_document" "lambda_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "transcribe_function_role" {
  name               = "transcribe-function-role"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
}

resource "aws_iam_role_policy_attachment" "attache_ts_role1" {
  role       = aws_iam_role.transcribe_function_role.name
  policy_arn = data.aws_iam_policy.AmazonS3FullAccess.arn
}

resource "aws_iam_role_policy_attachment" "attache_ts_role2" {
  role       = aws_iam_role.transcribe_function_role.name
  policy_arn = data.aws_iam_policy.AmazonTranscribeFullAccess.arn
}

resource "aws_iam_role_policy_attachment" "attache_ts_role3" {
  role       = aws_iam_role.transcribe_function_role.name
  policy_arn = data.aws_iam_policy.CloudWatchLogsFullAccess.arn
}

resource "aws_iam_role" "comprehend_function_role" {
  name               = "comprehend-function-role"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
}

resource "aws_iam_role_policy_attachment" "attache_ch_role1" {
  role       = aws_iam_role.comprehend_function_role.name
  policy_arn = data.aws_iam_policy.AmazonS3FullAccess.arn
}

resource "aws_iam_role_policy_attachment" "attache_ch_role2" {
  role       = aws_iam_role.comprehend_function_role.name
  policy_arn = data.aws_iam_policy.ComprehendFullAccess.arn
}

resource "aws_iam_role_policy_attachment" "attache_ch_role3" {
  role       = aws_iam_role.comprehend_function_role.name
  policy_arn = data.aws_iam_policy.CloudWatchLogsFullAccess.arn
}

# S3 input output
 change
resource "aws_s3_bucket" "h4b-serverless-t-input" {
  bucket        = "h4b-serverless-t-input" #適宜変更
  force_destroy = true
}

resource "aws_s3_bucket" "h4b-serverless-t-output" {
  bucket        = "h4b-serverless-t-output" #適宜変更
  force_destroy = true
}



# Lambda transcribe

resource "aws_lambda_permission" "allow_bucket1" {
  statement_id  = "AllowExecutionFromS3Bucket"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.transcribe-function.arn
  principal     = "s3.amazonaws.com"
  source_arn    = aws_s3_bucket.h4b-serverless-t-input.arn
}

resource "aws_s3_bucket_notification" "bucket_notification" {
  bucket = aws_s3_bucket.h4b-serverless-t-input.id

  lambda_function {
    lambda_function_arn = aws_lambda_function.transcribe-function.arn
    events              = ["s3:ObjectCreated:*"]
  }

  depends_on = [aws_lambda_permission.allow_bucket1]
}



data "archive_file" "function_zip1" {
  type        = "zip"
  source_dir  = "build/function1"
  output_path = "lambda/function1.zip"
}

resource "aws_lambda_function" "transcribe-function" {
  function_name = "transcribe-function"

  handler  = "src1/transcribe-function.lambda_handler"
  filename = data.archive_file.function_zip1.output_path
  runtime  = "python3.7"
  role     = aws_iam_role.transcribe_function_role.arn
}

# Lambda Comprehend

resource "aws_lambda_permission" "allow_bucket2" {
  statement_id  = "AllowExecutionFromS3Bucket"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.comprehend-function.arn
  principal     = "s3.amazonaws.com"
  source_arn    = aws_s3_bucket.h4b-serverless-t-output.arn
}

resource "aws_s3_bucket_notification" "bucket_notification2" {
  bucket = aws_s3_bucket.h4b-serverless-t-output.id

  lambda_function {
    lambda_function_arn = aws_lambda_function.comprehend-function.arn
    events              = ["s3:ObjectCreated:*"]
    filter_suffix       = ".json"
  }

  depends_on = [aws_lambda_permission.allow_bucket2]
}


data "archive_file" "function_zip2" {
  type        = "zip"
  source_dir  = "build/function2"
  output_path = "lambda/function2.zip"
}

resource "aws_lambda_function" "comprehend-function" {
  function_name = "comprehend-function"

  handler  = "src2/comprehend-function.lambda_handler"
  filename = data.archive_file.function_zip2.output_path
  runtime  = "python3.7"
  role     = aws_iam_role.comprehend_function_role.arn
}

```
Bucket名を変更した場合は、こちらに記載しているBucket名も変更してください。

# 環境構築
```bash
terraform init
terraform plan
terraform apply
```
環境を構築します。
init でワークスペースを初期化後
plan で問題なければ applyします

# 音声を保存する
![polly1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/940736/0664762e-45f0-c8b1-0483-a4b3d288a114.png)
![2021-12-17_20h10_29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/940736/2640c0df-5448-3edd-a46b-3eb6780f79bc.png)
コンソールから、Pollyを使用し音声をS3(input)に保存します。

#　分析結果
![2021-12-17_20h13_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/940736/202b6bf5-5402-b144-c186-bbb615924c2d.png)
comprehendから分析結果が返ってきてますね！
おぉ、Negativeが強い文章だとなってます。
Comprehendすごい！！

# 感想
今回は、AWS Hands-on for BeginnersからAWS Lambda と AWS AI Services を組み合わせて作る音声文字起こし & 感情分析パイプラインをTerraformで構築し触ってみました。
AI関連のサービスを触ったことがなかったのでとても良い経験になりました。
Terraform部分はもっときれいに効率良く書けるように関数やmoduleそしてファイル構成を考えないといけないですね…
AWS Hands-on for Beginnersは要点がきれいにまとまっていてわかりやすく、色んなサービスのハンズオンもあるので触ったことないサービスを触る前に一度見てみることをおすすめします。


# 参考

[AWS Lambda と AWS AI Services を組み合わせて作る音声文字起こし & 感情分析パイプライン](https://aws.amazon.com/jp/aws-jp-introduction/aws-jp-webinar-hands-on/?trk=aws_blog#:~:text=AWS%20Lambda%20%E3%81%A8%20AWS%20AI%20Services%20%E3%82%92%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%81%A6%E4%BD%9C%E3%82%8B%E9%9F%B3%E5%A3%B0%E6%96%87%E5%AD%97%E8%B5%B7%E3%81%93%E3%81%97%20%26%20%E6%84%9F%E6%83%85%E5%88%86%E6%9E%90%E3%83%91%E3%82%A4%E3%83%97%E3%83%A9%E3%82%A4%E3%83%B3%20%C2%BB)
[TerraformでLambda[Python]のデプロイするときのプラクティス](https://dev.classmethod.jp/articles/terraform-lambda-deployment/)