• 24
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

この投稿は 今年もやるよ!AWS Lambda縛り Advent Calendar 2015 - Qiitaの18日目の記事です。
Lambda縛りだけど、この記事はAmaozon Echo、Alexa Skills Kitの成分多めとなっています…

バルスとは?

言わずと知れた、世にも恐ろしい滅びの言葉。
先人が既に破壊の限りをつくすコマンドを作り世に出しています
https://github.com/qphoney/balus
今回紹介するのは、そこまで無慈悲なものではありません。

Amazon Echoとは?

音声認識によって質問に答えてくれたり、サービスと連携したり出来るものです。
Amazon Skills Kitを利用することで、機能を追加することができます。
サードパーティー製のSkillは審査を経て、専用のサイトからインストールして使うことができます。
https://youtu.be/7Jc82wIL7m4
IMAGE ALT TEXT HERE

Amazon Skills Kitとは?

AlexaはAmazon Echoに採用されているクラウドベースの音声認識サービスです。
Alexa Skill KitはAlexaが利用できる機能(Skill)を簡単に構築するために必要な環境を提供してくれます。
Amazon Alexa Skills Kitを調べてみる

Alexa Skills Kit(ASK)のLambdaファンクションを作成

先日のre:InventでLambdaがPythonに対応したと発表がありましたが、ASKのファンクションでもLambdaが使えるようになりました。
ただし、ASKはus-east-1リージョンにしか対応していないため、他のリージョンにはLambdaのテンプレート一覧にASKのテンプレは出てきません。

スクリーンショット 2015-12-15 16.35.37.png

今回はこのテンプレートを少し改造してバルスを実装してみました。

# -*- coding: utf-8 -*-
from __future__ import print_function
import boto3
from time import gmtime, strftime

client = boto3.client('ec2')

# エントリーポイント
def lambda_handler(event, context):
    print(strftime('%a, %d %b %Y %H:%M:%S +0000', gmtime()))
    print(event)

    if event['request']['type'] == "LaunchRequest":
        # skill開始のリクエスト
        return on_launch(event['request'], event['session'])
    elif event['request']['type'] == "IntentRequest":
        # インテントの呼び出し
        return on_intent(event['request'], event['session'])

    print("nothing and finish")
    return get_finish_response()

def on_launch(launch_request, session):
    # まじないの言葉を取得する
    return get_charm_response()

def on_intent(intent_request, session):

    intent = intent_request['intent']
    intent_name = intent_request['intent']['name']
    # 滅びの言葉を含まなければ終わり
    if 'Barusu' not in intent['slots'] or \
       'value' not in intent['slots']['Barusu'] or \
       not intent_name == "RunHorobi":
        return get_finish_response()

    print(intent['slots']['Barusu']['value'])

    stop_instance()
    return get_horobi_response()

# おまじないの言葉のレスポンスを生成
def get_charm_response():

    session_attributes = {}
    card_title = "Charm"
    audio_url = "https://url/to/your/audio.mp3"
    should_end_session = False
    return build_response(session_attributes, build_audio_response(
        card_title, speech_output, audio_url, should_end_session))

# 滅びの言葉を言われたら返すレスポンスを生成
def get_horobi_response():

    session_attributes = {}
    card_title = "Horobi"
    speech_output = "Megaaaaa!"
    reprompt_text = speech_output
    should_end_session = True
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))

# なにもしない時のレスポンスを生成
def get_finish_response():

    session_attributes = {}
    card_title = "Words that should not be used"
    speech_output = "Don't say the word of horobee"
    reprompt_text = speech_output
    should_end_session = True
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))

# バルスで停止するインスタンスを取得
def get_instances():
    response = client.describe_instances(
        Filters=[
            {
                'Name': 'tag-value','Values': [
                    'laputa',
                ]
            }
        ]
    )
    instance_ids = []
    for res in response['Reservations']:
        for item in res['Instances']:
            instance_ids.append(item['InstanceId'])
    return instance_ids

# インスタンスの開始
def start_instance():
    print('start_instance')
    response = client.start_instances(
        InstanceIds=get_instances()
    )

# インスタンスを終了
def stop_instance():
    print('stop_instance')
    response = client.stop_instances(
        InstanceIds=get_instances()
    )

# 戻り値のJSONを生成
def build_speechlet_response(title, output, reprompt_text, should_end_session):
    return {
        'outputSpeech': {
            'type': 'PlainText',
            'text': output
        },
        'card': {
            'type': 'Simple',
            'title': 'SessionSpeechlet - ' + title,
            'content': 'SessionSpeechlet - ' + output
        },
        'reprompt': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': reprompt_text
            }
        },
        'shouldEndSession': should_end_session
    }

# SSML形式の戻り値JSONを生成
def build_audio_response(title, output, audio_url, should_end_session):
    return {
        'outputSpeech': {
            'type': 'SSML',
            'ssml': '<speak><audio src="{0}" /></speak>'.format(audio_url)
        },
        'card': {
            'type': 'Simple',
            'title': 'SessionSpeechlet - ' + title,
            'content': 'SessionSpeechlet - ' + output
        },
        'reprompt': {
            'outputSpeech': {
                'type': 'SSML',
                'ssml': '<speak><audio src="{0}" /></speak>'.format(audio_url)
            }
        },
        'shouldEndSession': should_end_session
    }

# 戻り値の全体
def build_response(session_attributes, speechlet_response):
    return {
        'version': '1.0',
        'sessionAttributes': session_attributes,
        'response': speechlet_response
    }

このスクリプトでは、Alexa, run laputaとAmazon Echoに話しかけると、困ったときのおまじないリテ・ラトバリタ・ウルス・アリアロス・バル・ネトリールを答えてくれます。

Alexa Skillsはテキストを返却してAmazon Echoで喋らすのと、SSML(Speech Synthesis Markup Language)を使って喋らす方法がありますが、先日の発表で任意のオーディオデータを扱うことも可能になりました。
Alexaの発音だと、英語ベースとなってしまうので、音声合成したmp3ファイルをSSMLで指定してます。(build_audio_responseのところで)

オーディオファイルの制限

Amazon Echoが再生できるオーディオのファイルは細かい制限があり、以下の様なフォーマットである必要があります。

  • 有効なMP3ファイル(MPEG version 2)
  • 90秒以内
  • ビットレートは、48kbps
  • サンプルレートは、16000 Hz
  • httpsでアクセスできて、信頼できるSSL証明書であること(オレオレはダメ)
  • 個人情報や機密情報など含んではならない

macでffmpegを使うと以下の様なコマンド

ffmpeg -i input.mp3 -ac 2 -codec:a libmp3lame -b:a 48k -ar 16000 output.mp3

Alexa Skillsを追加する

Skill information

https://developer.amazon.com/edw/home.html#/
ダッシュボードからAlexa Skills Kitを選択して、Add a New Skillから新規で登録します。
スクリーンショット 2015-12-15 18.09.39.png
EndpointでLambdaを選択し、先ほど作成したLambdaファンクションのARNを指定します。

Intent Schema

{
  "intents": [
    {
      "intent": "RunHorobi",
      "slots": [
        {
          "name": "Barusu",
          "type": "LIST_OF_BARUSU"
        }
      ]
    }
  ]
}

インテント(Lambdaファンクション側の機能)に付随するキーワードの定義を設定。

Custom Slot Types

barusu
bars
barus
barsu

対応するキーワードを登録する。(ただし、ここに登録されていないキーワードでも反応してLambdaファンクションを読みだしてしまうのは、ワークショップで聞いた限り現状しょうがないらしい。)

Sample Utterances

RunHorobi {Barusu}

発話されたワードに合わせてどのインテントを呼び出すかを指定している。
実際にユーザーがどのような発話をするかパターンがいくつかあるので、それを想定して複数の文章をインテントに紐付けておくと、それだけASK側で識別しやすくなる。

テスト

ASKにはAmazon Echoを利用しなくてもSkillのテストが出来る仕組みがあります。
スクリーンショット 2015-12-15 19.06.37.png
これを使ってLambdaファンクションのテストを行えば、デバッグでひたすらEchoに対して話しかけなくて済みます。
右下にある再生ボタンをクリックすると、戻り値のテキストを喋ってくれます。
(ただし、現在はSSMLには対応出来ていない)

実機確認

自分のデバイスとして登録されているEchoで、実際に発話した言葉とかその結果がダッシュボードで確認することができます。
スクリーンショット 2015-12-15 19.11.29.png

https://youtu.be/7Jc82wIL7m4
IMAGE ALT TEXT HERE

まとめ

Amazon Echoを使うことで、よりリアルな「バルス」が出来るようになりました。
(音声認識の飛行石があると、より気分が盛り上がります)
しかし、Amazon Echoはまだ日本語に対応していないので、「目がーーー!」の表現が残念すぎますね。早く対応してくれることを期待します。

真面目な話

LambdaがPythonで書けるようになったので、個人的にはすごくコーディングしやすくなりました。
Skillのテストとシミュレータが追加されたので、Amazon Echoが無くても結構いいところまで作り込めるようになりました。

この投稿は 今年もやるよ!AWS Lambda縛り Advent Calendar 201519日目の記事です。