Amazon Transcribeによる音声認識をポーリング方式(Step Functions + Lambda)でやってみた

Step FunctionsとLambdaを使って**ポーリング方式**によりAmazon Transcribeを利用する仕組みを作ってみたのでご紹介したいと思います。
2019.12.02

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部の若槻です。

本エントリは、AWS LambdaとServerless #1 Advent Calendar 2019の2日目のエントリです。

Amazon Transcribeは、音声認識ジョブの開始から完了までが非同期実行となるため、ジョブの結果取得には主に以下のような方式をとることになります。(AWS機能を利用する場合)

今回は上記のうちStep FunctionsとLambdaを使ってポーリング方式によりAmazon Transcribeを利用する仕組みを作ってみたのでご紹介したいと思います。

作ってみる

今回の構成

以下のようなステートマシンフローの構成を作成します。

  1. startTranscriptJob:Amazon Transcribeの音声認識ジョブを開始するLambdaの実行
  2. wait:30秒のウェイト
  3. getTranscriptJobStatus:音声認識ジョブのステータスを取得するLambdaの実行
  4. jobStatusCheck:直前に取得したジョブステータスのチェック。ジョブ未完了なら2に送る。ジョブ完了済みなら5に送る。
  5. getTranscriptJobResult:音声認識ジョブの実行結果を取得するLambdaの実行

上記のうち234の処理がいわゆるポーリング部分となります。

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は先月より日本語音声への対応が実装され、今後国内での採用事例も増えてくるかと思います。本記事がどなたかの参考になれば幸いです。

参考