日本でも Amazon Echo の発売が発表されました。私もとりあえず招待メールをリクエストしておいたので、購入できたら Alexa のスキルを色々試してみたいと思ってますが、その前に、今更感もありますが Amazon Lex を理解するためにチュートリアルなど試してみたので、ついでに AWS SDK for Ruby から Lex にリクエストを投げる処理を書いてみました。
Lex のチュートリアル
AWSの公式ドキュメントには Lex で Bot を作成するチュートリアルが用意されています。
Blueprint から Bot を作成してそのまま動かしてみるケースや、 AWS Lambda を Fulfillment としてゼロから作成するケースなどが用意されています。今回は後者のケースでコンソールから作成した PizzaOrderingBot に対して AWS SDK for Ruby からリクエストを投げてみたいと思います。
PizzaOrderingBot の処理内容
PizzaOrderingBot はピザの注文を受け付けて処理する想定の Bot です。今回はチュートリアルの内容そのままに作成したので、詳細はドキュメントをご覧いただくこととして割愛しますが、内容としてはピザの種別(ベジタブル or チーズ)、大きさ(大、中、小)、クラスト(皮)の種類(厚い or 薄い)を音声入力もしくはテキスト入力で受け付け、その内容を AWS Lambda に渡して処理します。
今回はこの 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 で音声入力と組み合わせることができれば、色々なセンサー類との連携もできそうなので面白そうかなと思いました。
猫もswitchやる時代らしいです。