Amazon Lex を AWS SDK for Ruby から試す

 日本でも Amazon Echo の発売が発表されました。私もとりあえず招待メールをリクエストしておいたので、購入できたら Alexa のスキルを色々試してみたいと思ってますが、その前に、今更感もありますが Amazon Lex を理解するためにチュートリアルなど試してみたので、ついでに AWS SDK for Ruby から Lex にリクエストを投げる処理を書いてみました。

Lex のチュートリアル

 AWSの公式ドキュメントには Lex で Bot を作成するチュートリアルが用意されています。

docs.aws.amazon.com

 Blueprint から Bot を作成してそのまま動かしてみるケースや、 AWS Lambda を Fulfillment としてゼロから作成するケースなどが用意されています。今回は後者のケースでコンソールから作成した PizzaOrderingBot に対して AWS SDK for Ruby からリクエストを投げてみたいと思います。

PizzaOrderingBot の処理内容

 PizzaOrderingBot はピザの注文を受け付けて処理する想定の Bot です。今回はチュートリアルの内容そのままに作成したので、詳細はドキュメントをご覧いただくこととして割愛しますが、内容としてはピザの種別(ベジタブル or チーズ)、大きさ(大、中、小)、クラスト(皮)の種類(厚い or 薄い)を音声入力もしくはテキスト入力で受け付け、その内容を AWS Lambda に渡して処理します。

docs.aws.amazon.com

 今回はこの PizzaOrderingBot へリクエストを投げる Ruby スクリプトを Raspberry Pi 上で動かしてみます。本当は実際に喋った内容をキャプチャして入力として使いたかったのですが、 Raspberry Pi での音声入力の扱いがうまく行かなかったので、今回は模擬的に Amazon Polly でテキストを音声ファイルにして、それを Lex の入力として使ってみました。

AWS SDK for Ruby のインストール

 AWS SDK for Ruby は Version 3 から gem が各サービスごとの gem に分割されました。

File: README — AWS SDK for Ruby V3

 今まで通り全て一括でインストールすることもできますが、不要なものはインストールしないに越したことはないので、今回は Lex と Polly の gem を指定してインストールします。 Gemfile は下記のような内容にしています。

# frozen_string_literal: true
source "https://rubygems.org"

gem 'aws-sdk-lex', '~> 1'
gem 'aws-sdk-polly', '~> 1'

サンプル実装

 それでは Ruby スクリプトの実装です。まずはスクリプト全体を掲載しておきます。

require 'bundler/setup'
require 'aws-sdk-lex'
require 'aws-sdk-polly'
require 'open3'

class LexSample
  REGION          = 'us-east-1'.freeze
  LEX_INPUT_FILE  = 'lex_input.pcm'.freeze
  LEX_OUTPUT_FILE = 'lex_output.mp3'.freeze

  def initialize
    @lex_client   = Aws::Lex::Client.new(region: REGION)
    @polly_client = Aws::Polly::Client.new(region: REGION)
  end

  def make_lex_input_file(text)
    resp = @polly_client.synthesize_speech({
      output_format: 'pcm', 
      sample_rate:   '8000', 
      text:          text, 
      text_type:     'text', 
      voice_id:      'Joanna', 
    })
    puts resp.to_h

    File.open(LEX_INPUT_FILE, 'wb') do |f|
      f.write(resp[:audio_stream].read)
    end
  end

  def post_request
    input_stream = File.open(LEX_INPUT_FILE, 'rb')
    resp = @lex_client.post_content(
      bot_name:     'PizzaOrderingBot',
      bot_alias:    'BETA',
      user_id:      'hiroaki.akanuma',
      content_type: 'audio/lpcm; sample-rate=8000; sample-size-bits=16; channel-count=1; is-big-endian=false',
      accept:       'audio/mpeg',
      input_stream: input_stream
    )
    puts resp.to_h

    File.open(LEX_OUTPUT_FILE, 'wb') do |f|
      f.write(resp[:audio_stream].read)
    end
  end

  def read_output
    Open3.capture3("mpg321 #{LEX_OUTPUT_FILE}")
  end
end

if $PROGRAM_NAME == __FILE__
  sample = LexSample.new

  sample.make_lex_input_file('I want to order a pizza')
  sample.post_request
  sample.read_output

  # ピザの種類を選択
  sample.make_lex_input_file('cheese')
  sample.post_request
  sample.read_output

  # ピザの大きさを選択
  sample.make_lex_input_file('large')
  sample.post_request
  sample.read_output

  # ピザクラストを選択
  sample.make_lex_input_file('thick')
  sample.post_request
  sample.read_output
end

 まずクラスの初期化時に Lex と Polly のクライアントインスタンスを生成しておきます。

  def initialize
    @lex_client   = Aws::Lex::Client.new(region: REGION)
    @polly_client = Aws::Polly::Client.new(region: REGION)
  end

 以降は下記ステップを複数回繰り返して、 Bot とのやり取りを進めています。

  • Polly にテキストを渡して音声ストリームをファイルに保存

  • 音声ファイルを入力にして Lex にリクエストしてレスポンスの音声ストリームをファイルに保存

  • 保存した Lex からのレスポンスを読み上げる

 Polly で音声合成を行うには synthesize_speech メソッドを使用します。今のところ Lex は日本語には対応していないので、英語での音声出力を作成します。出力フォーマットは pcm です。

    resp = @polly_client.synthesize_speech({
      output_format: 'pcm', 
      sample_rate:   '8000', 
      text:          text, 
      text_type:     'text', 
      voice_id:      'Joanna', 
    })

 レスポンスから音声ストリームを取り出してファイルに保存します。

    File.open(LEX_INPUT_FILE, 'wb') do |f|
      f.write(resp[:audio_stream].read)
    end

 保存した音声ファイルをオープンして Lex への入力にします。 Lex にリクエストを投げるには post_content メソッドを使用します。 post_content メソッドは音声・テキスト両方の入力に使用できます。もしテキストのみ扱うということでしたら、 post_text メソッドを使用することもできます。

 ちなみに post_content のドキュメントはこちらです。 Class: Aws::Lex::Client — AWS SDK for Ruby V3

 Polly では pcm フォーマットで出力したので、それに対応する content_type を指定します。また、 Raspberry Pi 上での再生をしやすいように、 accept に 'audio/mpeg' を指定することで Bot からのレスポンスの音声ストリームを MPEG 形式にしています。

    input_stream = File.open(LEX_INPUT_FILE, 'rb')
    resp = @lex_client.post_content(
      bot_name:     'PizzaOrderingBot',
      bot_alias:    'BETA',
      user_id:      'hiroaki.akanuma',
      content_type: 'audio/lpcm; sample-rate=8000; sample-size-bits=16; channel-count=1; is-big-endian=false',
      accept:       'audio/mpeg',
      input_stream: input_stream
    )

 保存された音声ファイルは mpg321 コマンドで再生します。

    Open3.capture3("mpg321 #{LEX_OUTPUT_FILE}")

 Bot に対して I want to order a pizza と言うことで会話が始まり、ボットからの質問に応じてピザの種類、大きさ、クラストの種類をそれぞれ音声で入力して、レスポンスを再生します。

実行してみる

 それでは実行してみます。デバッグ用にレスポンスを Hash として出力していますので、実行すると下記のような出力があり、Polly で生成した音声と Lex によってやり取りが行われ、最終的にピザのオーダーが完了します。 Bot からの質問に答えるにつれて slots の内容が埋まっていっているのがわかります。

pi@raspberrypi:~/lex_sample $ bundle exec ruby lex_sample.rb 
{:audio_stream=>#<StringIO:0x567a49b8>, :content_type=>"audio/pcm", :request_characters=>23}
{:content_type=>"audio/mpeg", :intent_name=>"OrderPizza", :slots=>{"pizzaKind"=>nil, "size"=>nil, "crust"=>nil}, :message=>"Do you want a veg or cheese pizza?", :dialog_state=>"ElicitSlot", :slot_to_elicit=>"pizzaKind", :input_transcript=>"i want to order a pizza", :audio_stream=>#<StringIO:0x569df3d8>}
{:audio_stream=>#<StringIO:0x56ada838>, :content_type=>"audio/pcm", :request_characters=>6}
{:content_type=>"audio/mpeg", :intent_name=>"OrderPizza", :slots=>{"pizzaKind"=>"cheese", "size"=>nil, "crust"=>nil}, :message=>"What size pizza?", :dialog_state=>"ElicitSlot", :slot_to_elicit=>"size", :input_transcript=>"cheese", :audio_stream=>#<StringIO:0x56e6dfd0>}
{:audio_stream=>#<StringIO:0x56ebf138>, :content_type=>"audio/pcm", :request_characters=>5}
{:content_type=>"audio/mpeg", :intent_name=>"OrderPizza", :slots=>{"pizzaKind"=>"cheese", "size"=>"large", "crust"=>nil}, :message=>"What kind of crust would you like?", :dialog_state=>"ElicitSlot", :slot_to_elicit=>"crust", :input_transcript=>"large", :audio_stream=>#<StringIO:0x56ee4080>}
{:audio_stream=>#<StringIO:0x56abe218>, :content_type=>"audio/pcm", :request_characters=>5}
{:content_type=>"audio/mpeg", :intent_name=>"OrderPizza", :slots=>{"pizzaKind"=>"cheese", "size"=>"large", "crust"=>"thick"}, :message=>"Okay, I have ordered your large cheese pizza on thick crust", :dialog_state=>"Fulfilled", :input_transcript=>"thick", :audio_stream=>#<StringIO:0x56fb4f40>}

まとめ

 Lex は主にチャットボットを作成するために使われることが多そうなので、今回のように SDK から使うケースはあまり多くないのかもしれませんが、 SDK で Lex へリクエストを投げることは簡単だったので、 Raspberry Pi で音声入力と組み合わせることができれば、色々なセンサー類との連携もできそうなので面白そうかなと思いました。

f:id:akanuma-hiroaki:20171113235330j:plain:w450

猫もswitchやる時代らしいです。