Amazon Transcribeによる音声認識をポーリング方式(Step Functions + Lambda)でやってみた
こんにちは、CX事業本部の若槻です。
本エントリは、AWS LambdaとServerless #1 Advent Calendar 2019の2日目のエントリです。
Amazon Transcribeは、音声認識ジョブの開始から完了までが非同期実行となるため、ジョブの結果取得には主に以下のような方式をとることになります。(AWS機能を利用する場合)
- ポーリング方式(Step Functions)
- イベントドリブン方式(CloudWatch Events)
- イベントドリブン方式(S3 Events/SNS)
今回は上記のうちStep FunctionsとLambdaを使ってポーリング方式によりAmazon Transcribeを利用する仕組みを作ってみたのでご紹介したいと思います。
作ってみる
今回の構成
以下のようなステートマシンフローの構成を作成します。
startTranscriptJob
:Amazon Transcribeの音声認識ジョブを開始するLambdaの実行wait
:30秒のウェイトgetTranscriptJobStatus
:音声認識ジョブのステータスを取得するLambdaの実行jobStatusCheck
:直前に取得したジョブステータスのチェック。ジョブ未完了なら2
に送る。ジョブ完了済みなら5
に送る。getTranscriptJobResult
:音声認識ジョブの実行結果を取得するLambdaの実行
上記のうち2
、3
、4
の処理がいわゆるポーリング部分となります。
Lambdaファンクションの作成
ステートマシンのタスクとなるLambdaファンクションを作成します。
各ファンクションのパラメーターは、以下の設定を3ファンクション共通で行っています。
- ランタイム:Python 3.7
- 実行ロール:以下のAWS管理ポリシーをアタッチしたIAMロール
AmazonTranscribeFullAccess
AWSLambdaBasicExecutionRole
Amazon Transcribeの音声認識ジョブを開始するLambda
- 関数名:start_transcription_job_func
import boto3 client = boto3.client('transcribe') def lambda_handler(event, context): job_name = event['jobName'] file_uri = 'https://s3.amazonaws.com/' + event['backetName'] + '/' + event['fileName'] client.start_transcription_job( TranscriptionJobName=job_name, Media={ 'MediaFileUri':file_uri }, MediaFormat='mp3', MediaSampleRateHertz=24000, LanguageCode='ja-JP' ) return job_name
音声認識ジョブのステータスを取得するLambda
- 関数名:get_transcription_job_status_func
import boto3 client = boto3.client('transcribe') def lambda_handler(event, context): job_name = event['jobName'] get_transcription_job_res = client.get_transcription_job( TranscriptionJobName=job_name ) current_status = get_transcription_job_res['TranscriptionJob']['TranscriptionJobStatus'] return current_status
音声認識ジョブの実行結果を取得するLambda
- 関数名:get_transcription_job_result_func
import boto3 import json import urllib.request client = boto3.client('transcribe') def lambda_handler(event, context): job_name = event['jobName'] get_transcription_job_res = client.get_transcription_job( TranscriptionJobName=job_name ) result_url = get_transcription_job_res['TranscriptionJob']['Transcript']['TranscriptFileUri'] result_req = urllib.request.Request(result_url) result_res = json.loads(urllib.request.urlopen(result_req).read()) return result_res
ステートマシンの作成
Step Functionsで以下のステートマシンを作成します
- 実行ロール:以下のAWS管理ポリシーをアタッチしたIAMロール
AWSLambdaRole
- ステートマシン定義
{ "StartAt": "startTranscriptJob", "States": { "startTranscriptJob": { "Type": "Task", "Resource": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:start_transcription_job_func", "Next": "wait", "ResultPath": "$.jobName" }, "wait": { "Type": "Wait", "Seconds": 30, "Next": "getTranscriptJobStatus" }, "getTranscriptJobStatus": { "Type": "Task", "Resource": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:get_transcription_job_status_func", "ResultPath": "$.jobStatus", "Next": "jobStatusCheck" }, "jobStatusCheck": { "Type": "Choice", "Choices": [ { "Variable": "$.jobStatus", "StringEquals": "COMPLETED", "Next": "getTranscriptJobResult" } ], "Default": "wait" }, "getTranscriptJobResult": { "Type": "Task", "Resource": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXXX:function:get_transcription_job_result_func", "End": true } } }
- ステートマシンフロー図
音声ファイルを配置するS3バケットの作成
Amazon S3にバケット名に文字列transcript
が含まれるバケットを作成します。
- バケット名:
XXXXXX_transcript
その理由ですが、先ほどLambdaファンクションの実行ロールに付与したAWS管理ポリシーAmazonTranscribeFullAccess
のjson定義を見ると、以下のようになっています。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "transcribe:*" ], "Resource": "*" }, { "Effect": "Allow", "Action": [ "s3:GetObject" ], "Resource": [ "arn:aws:s3:::*transcribe*" ] } ] }
ハイライト部の記述から分かる通り、Amazon Transcribeがアクセスするバケットの名前にはtranscribe
の文字列が含まれている必要があります。
バケット名の制約を受けたくない場合は、関数実行ロールに別途S3バケットへの読み取り可能ポリシー(AWS管理ポリシーAmazonS3ReadOnlyAccess
など)を付与するか、S3バケット自体のポリシーを設定すれば良いです。
動作確認
音声ファイルの用意、S3バケットへの配置
今回は音声ファイルはAmazon Pollyを利用して以下の通り作成しました。
- テキスト:
こんにちは、ミズキです。読みたいテキストをここに入力してください。
(既定のテキスト) - 出力形式:
MP3
- サンプルレート:
24000Hz
作成した音声ファイルを適当にリネームし、先程作成したS3バケットに配置します。
ステートマシンの実行
Step Functionsのステートマシンを実行します。
実行時には以下のように入力を指定します。
- 入力
{ "jobName": "testjob1", "backetName": "XXXXX_transcribe", "fileName": "XXXXX_fileName.mp3" }
今回は開始から123828ms経過後にステートマシンの実行が完了して以下のような出力がされ、音声認識結果を得ることができました。
- 出力
{ "jobName": "testjob1", "accountId": "XXXXXXXXXXXX", "results": { "transcripts": [ { "transcript": "こんにちは 水城 です 読み たい テキスト を ここ に 入力 し て ください" } ], "items": [ { "start_time": "0.04", "end_time": "0.76", "alternatives": [ { "confidence": "1.0", "content": "こんにちは" } ], "type": "pronunciation" }, { "start_time": "0.99", "end_time": "1.43", "alternatives": [ { "confidence": "0.3835", "content": "水城" } ], "type": "pronunciation" }, { "start_time": "1.43", "end_time": "1.85", "alternatives": [ { "confidence": "0.999", "content": "です" } ], "type": "pronunciation" }, { "start_time": "2.24", "end_time": "2.59", "alternatives": [ { "confidence": "0.9988", "content": "読み" } ], "type": "pronunciation" }, { "start_time": "2.59", "end_time": "2.81", "alternatives": [ { "confidence": "0.9988", "content": "たい" } ], "type": "pronunciation" }, { "start_time": "2.81", "end_time": "3.31", "alternatives": [ { "confidence": "1.0", "content": "テキスト" } ], "type": "pronunciation" }, { "start_time": "3.31", "end_time": "3.41", "alternatives": [ { "confidence": "1.0", "content": "を" } ], "type": "pronunciation" }, { "start_time": "3.41", "end_time": "3.69", "alternatives": [ { "confidence": "0.9829", "content": "ここ" } ], "type": "pronunciation" }, { "start_time": "3.69", "end_time": "3.83", "alternatives": [ { "confidence": "1.0", "content": "に" } ], "type": "pronunciation" }, { "start_time": "3.83", "end_time": "4.32", "alternatives": [ { "confidence": "1.0", "content": "入力" } ], "type": "pronunciation" }, { "start_time": "4.32", "end_time": "4.44", "alternatives": [ { "confidence": "1.0", "content": "し" } ], "type": "pronunciation" }, { "start_time": "4.44", "end_time": "4.55", "alternatives": [ { "confidence": "1.0", "content": "て" } ], "type": "pronunciation" }, { "start_time": "4.55", "end_time": "5.13", "alternatives": [ { "confidence": "0.9614", "content": "ください" } ], "type": "pronunciation" } ] }, "status": "COMPLETED" }
備考
実システムでは、ステートマシンのタスクとしてさらに以下のような処理を入れても良さそうです。
- S3バケット内に音声ファイルがあるかどうかのチェック
- 同名の音声認識ジョブが既に作成されていないかどうかのチェック
- 音声認識ジョブが
FAILED
となったときの処理 - 音声認識結果取得後にS3バケットから音声ファイルを削除
おわりに
今回の方式はポーリングを行うことによる冗長さはありますが、「ジョブの開始」から「音声認識結果の取得」までの各処理のステートをStep Functionsで可視化できるのはメリットといえます。
Amazon Transcribeは先月より日本語音声への対応が実装され、今後国内での採用事例も増えてくるかと思います。本記事がどなたかの参考になれば幸いです。
参考
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/transcribe.html
- https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/how-step-functions-works.html
- https://dev.classmethod.jp/cloud/aws/amazontranscribe-japanese/
- https://dev.classmethod.jp/server-side/python/speech-to-text-amazon-transcribe-with-python-boto-sdk/
- https://dev.classmethod.jp/cloud/aws/amazon-transcribe-now-store-transcription-outputs-to-your-own-s3-buckets/
- https://dev.classmethod.jp/cloud/aws/first-aws-step-functions/
- https://dev.classmethod.jp/cloud/aws/step_stepfunctions_tutorial/