【AWS】Amazon Transcribe + Terraformで作る音声認識システム
はじめに
u-hyszkと申します!
本記事では、AWSが公開しているフルマネージド型の自動音声認識サービスである「Amazon Transcribe」を使って、AWS上にミニマルな自動音声認識システムを構築します。
システムを構築する際には、Infrastructure as Code(IaC)ソフトウェアツール「Terraform」を使って、インフラ構成を自動的に構築します。
また、Lambda関数をコンテナイメージから作成する方法についてもご紹介します。
システムの概要
本記事で作成するシステムのアーキテクチャは上記のとおりです。
このシステムの入力はS3バケット上にアップロードされた音声で、出力は認識結果が記述されたjsonファイルが同じバケット上にアップロードされます。具体的な処理の流れは以下の通りです。
- S3バケットの
inputs
フォルダに音声がアップロードされる - 音声ファイルのアップロードを検知してLambda関数を呼び出す
- Lambda関数がAmazon Transcribeのjobを開始する
- 非同期でS3バケットの
transcriptions
フォルダに認識結果がアップロードされる
Lambda関数はECRにデプロイしたコンテナイメージを利用し、インフラの構築にはTerraformを使用します。
Amazon Transcribeの概要とメリット
Amazon Transcribeはフルマネージド型の自動音声認識サービスです。
音声認識システムを構築する際には、通常、大規模な深層学習モデル(Whisper・faster-whisper・WavLMなど)を運用する必要があります。このため、モデルを低料金・低レイテンシで安定的に運用するための開発コストや、その運用コストが肥大化することが課題となります。
Amazon Transcribeのようなフルマネージド型のサービスを使用することによって、簡単に高品質な認識結果を得ることができるため、特にプロダクト開発初期の段階で有力な手段となると思われます。
# たった数行のコードで音声認識の結果を得ることができます
import boto3
transcribe = boto3.client("transcribe")
if __name__ == "__main__":
transcribe.start_transcription_job(
TranscriptionJobName="example_job",
Media={"MediaFileUri": "media_file_uri_in_s3"},
MediaFormat="wav",
LanguageCode="ja-JP",
OutputBucketName="output_s3_bucket_name"
)
項目 | 説明 |
---|---|
フォーマット | mp3・mp4・wav・flac・ogg・amr・webm |
言語 | 言語コード換算で39言語(日本語・英語を含む) |
サンプルレート | 8 ~ 48kHz |
チャネル数 | 1 ~ 2 |
料金 | リンク先を参照 |
無料利用枠 | 12か月間、1か月あたり60分 |
Terraformの概要とメリット
TerraformはHashiCorp社が提供しているInfrastructure as Code(IaC)ソフトウェアツールです。
HashiCorp Configuration Language(HCL)と呼ばれる宣言型言語でインフラを定義することで、AWS・GCP・Azureなどのクラウド上にインフラを展開することができます。
また、HCLではモジュールを作成することができるので、これを効果的に利用することで再利用性と保守性を向上させることができます。
AWS Cloud FormationやAWS Cloud Development KitなどのAWS依存のツールを利用する場合と比較して、他のクラウドベンダーのサービスの併用や移行も可能な点がTerraformの魅力です。
Lambda関数をコンテナイメージから作成するメリット
Lambda関数を作成する方法として、zipファイルアーカイブをデプロイする方法がありますが、以下のようなデメリットもあります。
デメリット | 説明 |
---|---|
外部パッケージの導入が難しい | 外部パッケージの利用するには、各パッケージのzipファイルアーカイブも同時にデプロイする必要があり、手続きが煩雑である |
デプロイできるファイルのサイズは最大で250MBまでの制限があるため、サイズが大きいパッケージは単純に導入できない | |
サイズが大きいパッケージをLambda関数のLayerとして取り入れる方法もあるが、Layer数は最大で5つに制限されているため、パッケージ数も5つに制限される | |
バージョン管理が煩雑 | AWSのコンソール上でLambda関数が編集可能になるため、バージョン管理が煩雑になる |
そこで本記事では、システムの拡張性を考慮して、ECRのレポジトリに格納されているコンテナイメージからLambda関数を作成します。
これにより、
- 外部パッケージもコンテナイメージに内包することで容易に外部パッケージを利用できる
- コンテナイメージのサイズは10GBまでなら問題ない
- ECRレポジトリのバージョン管理により、Lambda関数のバージョン管理を代替できる
といったメリットを享受することができます。
前準備
前提知識
- AWSのコンソール上で基本的な操作ができること(特にIAM・S3)
- Pythonの基本的な構文を理解していること
- Dockerfileの基本的な仕様を理解していること
- Terraformの
init
・plan
・apply
・destroy
を理解していること
開発環境
- Darwin v23.6.0
- aws-cli v2.21.2
- Terraform v1.5.7
- Docker v20.10.21
IAMユーザーの作成とポリシーのアタッチ
事前にIAMユーザーの作成・アクセスキーの発行を行い、AWS CLIのconfigure
として登録しておいてください。以下の記事が非常に分かりやすいかと思います。
また、作成したIAMユーザーには以下のポリシーを付与しておいてください。
AmazonEC2ContainerRegistryFullAccess
AmazonS3FullAccess
AmazonTranscribeFullAccess
AWSLambda_FullAccess
AWSLambdaRole
IAMFullAccess
インフラの構築(Terraform)
まずはインフラから作成していきます。
インフラ構築ができたら中身のLambda関数などを作成します。
ディレクトリ構造
/
├─ lambda/
└─ infra
├─ envs
│ └─ dev
│ ├─ main.tf
│ ├─ variables.tf
│ └─ terraform.tfvars
└─ modules
├─ iam/
├─ lambda_s3_handler/
└─ s3
├─ variables.tf
├─ main.tf
└─ outputs.tf
インフラ全体のディレクトリ構造は上記とおりです。
最も重要なファイルはdev
直下のmain.tf
であり、このファイルに以下のモジュールをインポートします。
-
iam
: システムを動かすのに必要なIAMの権限設定 -
lambda_s3_handler
: S3からのイベントを受け取って実行するLambda関数 -
s3
: S3バケットの作成
各モジュールは以下の3つのファイルから構成されます。
-
variables.tf
: モジュールの引数を定義します -
main.tf
: モジュール本体の処理を記述します -
outputs.tf
: モジュールの返り値を定義します
また、dev
直下のmain.tf
には、同じくdev
直下のvariables.tf
およびterraform.tfvars
に記述された変数が代入されます。
ディレクトリ構成は以下の文献を参考にしました。
メインファイル
まずはdev
直下のmain.tf
から作成していきます。コードは以下の通りです。
# 1. プロバイダーの権限設定
provider "aws" {
region = var.region # 定義した変数
shared_credentials_files = ["~/.aws/credentials"]
profile = "default" # FIXME
}
# 2. プロバイダーとTerraform本体のバージョン設定
terraform {
required_version = "~> 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# 3. システムを動かすのに必要なIAMの権限設定
module "iam" {
source = "../../modules/iam" # モジュールのディレクトリ
# FIXME: IAMロールの名前
}
# 4. S3バケットの作成
module "s3" {
source = "../../modules/s3" # モジュールのディレクトリ
# FIXME: バケットの名前
# FIXME: (開発用のため)バケット削除時に中身も全削除するオプション
}
# 5. S3からのイベントを受け取って実行されるLambda関数
module "lambda_s3_handler" {
source = "../../modules/lambda_s3_handler" # モジュールのディレクトリ
# FIXME: Lambda関数の名前
# FIXME: Lambda関数のイメージの場所
# FIXME: 監視対象のS3バケット
# FIXME: 実行権限(Role)
# FIXME: Lambda関数が実行される条件
}
最初の2つのブロックは、プロバイダーの権限やバージョンについての設定になります。
プロファイルはdefault
としていますが、前準備でポリシーを付与したIAMユーザーのプロファイルに変更してください。
var.region
には、同じディレクトリ内にあるvariables.tf
およびterraform.tfvars
に定義した変数が入ります。
variables.tf
では変数とその型を定義し、terraform.tfvars
でその値を記述します。ここでは<REGION>
と書かれている場所にリージョン(ex. ap-northeast-1
)を記述してください。
variable "region" {
type = string
description = "The region in which the resources will be created"
}
region = "<REGION>"
残りの3ブロックはすべてモジュールになります。それぞれのモジュールの役割と想定される引数の種類をコメントで記載しています。
source
には作成したいモジュールのファイル一式(main.tf
・variables.tf
・outputs.tf
)が格納されたディレクトリのパスを記述します。
ここからはこれらのモジュールの中身を作成していきます。
IAM
iam
モジュールでは、システムを動かすのに必要なIAMの権限設定を行います。
# 1. 一時的な認証情報を引き受けるIAMロールの作成
resource "aws_iam_role" "lambda_role" {
name = "${var.name}-lambda-role"
assume_role_policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Action" : "sts:AssumeRole",
"Principal" : {
"Service" : ["lambda.amazonaws.com"]
},
"Effect" : "Allow"
}
]
})
}
# 2. システムの実行に必要なポリシーを定義
resource "aws_iam_policy" "lambda_policy" {
name = "${var.name}-lambda-policy"
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Action" : [
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:PutLogEvents"
],
"Effect" : "Allow",
"Resource" : "arn:aws:logs:*:*:*"
},
{
"Action" : [
"s3:GetObject",
"s3:PutObject",
"transcribe:StartTranscriptionJob",
"transcribe:GetTranscriptionJob"
],
"Effect" : "Allow",
"Resource" : "*"
}
]
})
}
# 3.作成したIAMロールにポリシーを一括で付与する
resource "aws_iam_role_policy_attachment" "lambda_role_attachment" {
role = aws_iam_role.lambda_role.name
policy_arn = aws_iam_policy.lambda_policy.arn
}
1つ目のブロックでは一時的な認証情報を引き受けるロールをsts:AssumeRole
というポリシーを利用して作成しています。
2つ目のブロックでは、作成したロールに付与したいポリシーを宣言しています。ここではCloud Watchのログ・S3・Amazon Transcribeのアクセス権限を付与しています。
3つ目のブロックで、作成したロールにポリシーを付与しています。
モジュールの引数としてvariables.tf
にname
を定義します。これにより、main.tf
からvar.name
として参照でき、作成したロール・ポリシーを既存のものと区別できるようになります。
variable "name" {
description = "Your name for IAM role prefix"
type = string
}
また、モジュールの出力としてAmazonリソースネーム(ARN)であるlambda_role_arn
を定義します。
outputs.tf
で定義された変数はmodule.iam.lambda_role_arn
のような形でアクセスできるようになります。
これにより、他のモジュールやリソースからロールを参照できるようになります。
output "lambda_role_arn" {
value = aws_iam_role.lambda_role.arn
}
S3
s3
モジュールでは、S3バケットの作成を行います。
resource "aws_s3_bucket" "this"{
bucket = var.name
force_destroy = var.force_destroy # (開発用のため)バケット削除時に中身も全削除するオプション
}
S3バケットのモジュールは非常にシンプルです。
今回は開発用の環境のため、バケット削除時に中身も全削除するオプションを変数で選択できるようにします。
モジュールの引数はS3バケットの名前であるname
とバケット削除時に中身も全て削除するオプションであるforce_destroy
です。
variable "name" {
description = "Your name for S3 bucket"
type = string
}
variable "force_destroy" {
description = "A boolean that indicates all objects should be deleted from the bucket so that the bucket can be destroyed without error."
type = bool
default = false
}
モジュールの返り値はバケットの名前bucket_name
、バケットのIDbucket_id
、バケットのARNbucket_arn
を定義します。
これらの値のうち、bucket_name
とbucket_arn
はLambda関数を作成する際に使用します。
output "bucket_name" {
value = aws_s3_bucket.this.bucket
description = "The name of the bucket"
}
output "bucket_id" {
value = aws_s3_bucket.this.id
description = "The name of the bucket"
}
output "bucket_arn" {
value = aws_s3_bucket.this.arn
description = "The ARN of the bucket"
}
Lambda
lambda_s3_handler
モジュールでは、S3からのイベントを受け取って実行されるLambda関数を作成します。
# 1. Lambda関数本体の定義
resource "aws_lambda_function" "s3_handler" {
package_type = "Image"
function_name = var.function_name
image_uri = var.image_uri
role = var.role
}
# 2. S3バケットの通知設定
resource "aws_s3_bucket_notification" "s3_notification" {
bucket = var.bucket_name
lambda_function {
lambda_function_arn = aws_lambda_function.s3_handler.arn
events = var.events
filter_prefix = var.filter_prefix
filter_suffix = var.filter_suffix
}
}
# 3. S3に対してLambda関数を呼び出すことを許可する
resource "aws_lambda_permission" "allow_s3_invoke" {
principal = "s3.amazonaws.com"
statement_id = "AllowS3Invoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.s3_handler.function_name
source_arn = var.source_arn
}
1つ目のブロックではLambda関数本体を定義します。今回はコンテナイメージから作成するのでimage_uri
を指定します。先ほど作成したIAMロールもここで必要になります。
2つ目のブロックでは、S3バケットのイベント通知設定を行います。bucket
で指定したS3バケット内において、events
・filter_prefix
・filter_suffix
の条件が満たされると、lambda_function_arn
で指定したLambda関数が呼び出されます。それぞれのオプションの役割は以下の通りです。
- events: 通知を発生させるイベントのリスト(ex:
["s3:ObjectCreated:*"]
) - filter_prefix: 指定した接頭辞の条件を満たす場合のみ通知する
- filter_suffix: 指定した接尾辞の条件を満たす場合のみ通知する
3つ目のブロックで、S3に対してLambda関数を呼び出すことを許可します。
モジュールの引数としてはLambda関数の作成に必要なfunction_name
・image_uri
・role
の他に、イベント通知のために必要なbucket_name
・source_arn
を定義します。
また、オプション引数としてevents
・filter_prefix
・filter_suffix
を定義します。
# --- Required --- #
variable "function_name" {
type = string
description = "The name of the Lambda function"
}
variable "image_uri" {
type = string
description = "The URI of the container image"
}
variable "role" {
type = string
description = "The ARN of the IAM role that the Lambda function assumes when it executes"
}
variable "bucket_name" {
type = string
description = "The name of the S3 bucket to receive notifications"
}
variable "source_arn" {
type = string
description = "The ARN of the S3 bucket to receive notifications"
}
# --- Optional --- #
variable "events" {
type = list(string)
description = "A list of S3 event types"
default = []
}
variable "filter_prefix" {
type = string
description = "The prefix filter"
default = ""
}
variable "filter_suffix" {
type = string
description = "The suffix filter"
default = ""
}
モジュールの返り値はLambda関数の名前function_name
とARNfunction_arn
を定義します。
output "function_name" {
value = aws_lambda_function.s3_handler.function_name
description = "The name of the Lambda function"
}
output "function_arn" {
value = aws_lambda_function.s3_handler.arn
description = "The ARN of the Lambda function"
}
仕上げ
最後に作成した3つのモジュールをdev
直下のmain.tf
に反映させていきます。まずはmain.tf
を編集します。
module "iam" {
source = "../../modules/iam"
+ name = var.name
}
module "s3" {
source = "../../modules/s3"
+ name = var.name
+ force_destroy = true
}
module "lambda_s3_handler" {
source = "../../modules/lambda_s3_handler"
+ function_name = var.name
+ image_uri = var.s3_wav_transcriber_image_uri
+ role = module.iam.lambda_role_arn
+ bucket_name = module.s3.bucket_name
+ source_arn = module.s3.bucket_arn
+ events = ["s3:ObjectCreated:*"] # オブジェクトが作成されたときに通知する
+ filter_prefix = "inputs/" # 接頭辞が"inputs/"のときのみ通知する
+ filter_suffix = ".wav" # 接尾辞が".wav"のときのみ通知する
}
先ほど作成したモジュールの引数を埋めていきます。
lambda_s3_handler
のイベントはinputs
フォルダの下に.wav
で終わるファイルが作成されたときのみ通知されるように設定しました。
また、各リソースの名前は変数name
で柔軟に変えられるようにします。Lambda関数のコンテナイメージのURIs3_wav_transcriber_image_uri
も変数にします。
variable "region" {
type = string
description = "The region in which the resources will be created"
}
+variable "name" {
+ type = string
+ description = "The name of the environment"
+}
+variable "s3_wav_transcriber_image_uri" {
+ type = string
+ description = "The URI of the 's3_wav_transcriber' container image"
+}
各変数の値は先ほどと同様にterraform.tfvars
で与えます。<AWSアカウントID>
はご自身のものを使用してください。
region = "<REGION>"
+name = "aws-transcriber-dev"
+s3_wav_transcriber_image_uri = "<AWSアカウントID>.dkr.ecr.<REGION>.amazonaws.com/s3_wav_transcriber_dev:latest"
これでインフラの構築は完了です!
Lambda関数の作成
続いてLambda関数とそのコンテナイメージを作成していきます。
ディレクトリ構造
/
├─ infra/
└─ lambda
└─ s3_wav_transcriber
├─ Dockerfile
├─ lambda_function.py
└─ requirements.txt
全体のディレクトリ構成は上の通りです。Dockerfile
でlambda_function.py
を呼び出すコンテナイメージを作成することでLambda関数として動作させることができます。
Dockerfile
# AWSのベースイメージを使用する
# NOTE: ARM64アーキテクチャの場合は`--platform linux/arm64`に変更
FROM public.ecr.aws/lambda/python:3.9
# requirements.txtをコピーしてライブラリをインストール
COPY requirements.txt ${LAMBDA_TASK_ROOT} # LAMBDA_TASK_ROOTはLambda側が定義する環境変数
RUN pip install -r requirements.txt
# Lambda関数本体をコピー
COPY lambda_function.py ${LAMBDA_TASK_ROOT}
# lambda_functions.pyのhandler関数を実行する
CMD [ "lambda_function.handler" ]
Dockerfile
の中身は上の通りです。ほとんど公式のテンプレートに従ったものになっており、AWSのベースイメージから作成しています。
開発環境のプラットフォームによって先頭のFROM
のオプションが変わることに注意してください。
lambda関数本体(Python)
import datetime
import os
import re
from urllib.parse import unquote_plus
+import boto3
+transcribe = boto3.client("transcribe")
def handler(event, context):
# 1. イベントの情報を取得する
+ input_bucket = event["Records"][0]["s3"]["bucket"]["name"]
+ input_key = unquote_plus(
+ event["Records"][0]["s3"]["object"]["key"], encoding="utf-8"
+ )
+ input_file_uri = f"s3://{input_bucket}/{input_key}"
wav_name = os.path.splitext(os.path.basename(input_key))[0]
print(f"Received event for {input_key} from bucket {input_bucket}.")
# 2. ジョブの名称と出力先のバケット・ファイル名を決める
output_bucket = input_bucket
output_key = f"transcriptions/{wav_name}.json"
output_file_uri = f"s3://{output_bucket}/{output_key}"
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
transcription_job_name = f"transcription-{wav_name}-{timestamp}"
transcription_job_name = re.sub(
r"[^0-9a-zA-Z._-]", "_", transcription_job_name
) # Transcribe job name must be alphanumeric
print(f"Output file URI: {output_file_uri}")
# 3. 音声認識を行うジョブを開始する
try:
+ transcribe.start_transcription_job(
+ TranscriptionJobName=transcription_job_name, # ジョブの名前
+ Media={"MediaFileUri": input_file_uri}, # 入力音声のURI
+ MediaFormat="wav", # 入力音声のフォーマット
+ LanguageCode="ja-JP", # 入力音声の言語
+ OutputBucketName=output_bucket, # 認識結果の出力先となるバケット
+ OutputKey=output_key, # 出力ファイルの名前
+ )
print(f"Transcription job {transcription_job_name} started.")
return {
"statusCode": 200,
"body": f"Transcription job {transcription_job_name} started for {input_key}.",
}
except Exception as e:
print(
f"Error starting transcription job for object {input_key} from bucket {input_bucket}."
"Make sure they exist and your bucket is in the same region as this function."
)
raise e
細かく出力先やジョブの名称を変更していますが、重要なのは強調表示した「1. イベントの情報を取得する」部分とtranscribe.start_transcription_job(...)
です。
handler
の引数となっているevent
の中に、入力音声のバケットinput_bucket
やファイル名input_key
に関する情報が含まれています。
これをinput_file_uri
の形式にすることで、入力音声を参照できるようにします。
transcribe.start_transcription_job(...)
では、S3バケットの中にある音声を取得し、認識結果のjsonファイルをS3バケットに格納するジョブを開始しています。
なお、今回は外部ライブラリとしてboto3
を使用するので、requirements.txt
に記載しておきます。
boto3
これでLambda関数の作成も完了です!
デプロイ
作成したLambda関数とインフラ環境をデプロイしていきます。
ECRレポジトリの作成
ECRにコンテナイメージを格納するレポジトリを作成します。このレポジトリに先ほど作成したLambda関数のコンテナイメージをデプロイします。
まずはECRにAWS CLIを通じてログインしましょう。<AWSアカウントID>
と<REGION>
はご自身のものに変更してください。
aws ecr get-login-password \
--region <REGION> \
| docker login --username AWS --password-stdin <AWSアカウントID>.dkr.ecr.<REGION>.amazonaws.com
ログインに成功したらECRのレポジトリを作成します。今回はイメージタグの重複を許すMUTABLE
の設定で作成します。
aws ecr create-repository \
--repository-name s3_wav_transcriber_dev \
--region <REGION> \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability MUTABLE
レポジトリの作成ができたらAWSコンソール上で確認しましょう。
ECRレポジトリの作成確認
問題なく作成できてますね!
イメージのbuild・push
続いて先ほど作成したDockerfile
とlambda_function.py
を利用してコンテナイメージのビルドを行います。
イメージの名前にはECRのURIを使用します。今回はタグにlatest
を使用します。
docker build ./lambda/s3_wav_transcriber -t <AWSアカウントID>.dkr.ecr.<REGION>.amazonaws.com:latest
ビルドに成功したら、イメージをECRにプッシュしましょう。
docker push <AWSアカウントID>.dkr.ecr.<REGION>.amazonaws.com:latest
ECRレポジトリへのコンテナイメージのプッシュ確認
こちらも問題なくプッシュできています!
terraform apply
最後にTerraformで定義したインフラ環境を適用します。
まずは初期化を行います。-chdir
オプションで実行するディレクトリを変更しています。
terraform -chdir=./infra/envs/dev init
続いてplan
で構築されるリソースに問題がないことを確認します。
terraform -chdir=./infra/envs/dev plan
最後にapply
で実際に環境を適用します。
terraform -chdir=./infra/envs/dev apply
S3バケットとLambda関数の作成ができていることを確認しましょう。
S3バケットの作成確認
Lambda関数の作成確認
実行確認
構築したシステムが正常に動作するか確認します。
S3バケットにinputs
フォルダを作成し、音声ファイル(u_greeting.wav
)をアップロードします。
S3のinputsフォルダに音声を入力
すると、S3バケット内にtranscriptions
フォルダが自動的に作成されます。
S3にtranscriptionsフォルダが作成されている
フォルダの中身を確認すると、一時ファイルの他にu_greeting.json
というファイルが格納されています。
jsonファイルが作成されている
このファイルの中身は以下のようになっています。
jsonファイルの中身
これで正常に音声認識が行われたことを確認できました👋
音声認識の結果
片付け
作業が終わったら全てのリソースを破棄し、余計な出費を減らしましょう。
terraform -chdir=./infra/envs/dev destroy
aws ecr delete-repository \
--repository-name s3_wav_transcriber_dev \
--region <REGION> \
--force
おわりに
本記事では、Amazon TranscribeとTerraformを使って、AWS上にミニマルな自動音声認識システムを構築しました。
考えられる改善点としては(1)CI/CDの整備(2)Step functionsの利用(3)Map Distributionの利用による並列化(4)VPCやサブネットを利用したセキュリティ向上などが挙げられると思います。
機械学習エンジニア/データサイエンティストとして働く予定ですが、インフラ・バックエンド周りも強くなって、より良い意思決定に繋げたいですね...!
ご覧いただきありがとうございました🎄
参考文献
本記事で作成したアーキテクチャは以下の記事を参考にしました。
Discussion