プロクラシスト

今日の寄り道 明日の近道

【Alexa】ゼロから解説!Amazon Echoでメモアプリを作ってみた


スポンサーリンク

こんにちは、ほけきよです。

Amazon Echoが一般発売になりましたね。 これで、だれもが簡単にスマートスピーカーに触れられるようになったわけです。

スマートスピーカーが手に入ると、することはただ一つですね。 そう、ハックです。 自分でいかようにも操作できるようになるので、Smarter, SmartestなAmazon Echoに仕上げましょう。 今回は、Amazon Echoをハックする方法を、ゼロから説明しようと思います。

今回は、こんな感じのアプリを作っていきます。(画質音質荒くてスミマセン!)

www.youtube.com

それではいきましょう!

Amazon Echoとは

Amazonが出しているスマートスピーカーAmazon Music等、Amazonの持つサービスと密に結びついているのが良きポイント。

3種類のラインナップがある。三者三様なのでお好きなのをどうぞ。

シリーズ 値段 特徴
Amazon Echo Dot (Newモデル)、ブラック 5980 持ち運び便利、micro usbで動く。音質はいまいち
Amazon Echo (Newモデル)、チャコール (ファブリック) 11980 デザインが一番おしゃれ。音質も良くバランスが良い
Amazon Echo Plus (Newモデル)、スマートホームハブ内蔵、ブラック 17980 でかい。低音の響きはやはり抜群

また、Echoを持っていなくてもAlexa開発は可能です。

Alexaとは

Echoを賢くしている脳みそみたいなもの。ある意味スマートスピーカーの本体。 開発でさらに賢くもできる。

登録しなければならない3つのサイト

始めるにあたって結構混乱するのが、ここ。3つのサイトを使いながら開発をしていく。これがかなり面倒。 簡単に役割と特徴、登録方法を見ていく

サイト 役割 登録方法
Alexa設定ページ Echo購入後、初期設定する用の画面。後々、自分で作ったアプリの登録を行う。 こちらのセットアップ解説記事がとてもわかりやすい
開発者コンソール(Alexa Skill Kit : ASK) Alexaをどう呼び出すのか、どういう命令を入力するかなどの辞書作りを行う。 公式テキスト6~9ページがわかりやすい
AWSコンソール Alexaから入力された命令を処理する(Lambda)。 公式テキスト18~20ページがわかりやすい

※注 : 開発者コンソールにおいて、英語アカウントと同じemailとパスワードを使っていると、勝手に英語アカウントと紐づいてややこしいことになるので、違うPWにしておくこと。

【開発】メモ帳を作る!

まず読め!

  • ハンズオンに使うテキストは非常によくできている。今回の記事もこれに準拠する形で開発を進めているので、必ず読んだほうが良いとてもよくわかる
  • ただ、こまかいところはハマる可能性があるので、一度ハンズオンに行くのがおすすめ。とてもよくわかる

1. Skillを作成する

【開発者コンソール(Alexa Skill Kit : ASK)】を使います。

  • まず、ASKにアクセス
  • 『スキル開発を始める』ボタンを押す
  • 『スキルの作成』ボタンを押す
  • スキルの名前を決める。また、スキル作成時のデフォルトを日本語にしておき、『次へ』を押す
  • 『カスタム』を選択し『スキルを作成』を押す

するとこんな画面になる

ここから、ようやくスタートです。

2. Alexaに言葉を覚えさせる

Alexaを呼び出すときの言葉は次のような感じです。

Alexa、メモを開いて
メモしたい内容は何ですか?
明日はブログを書く、とメモをする

コチラをざっと分解してみると以下の通り。

言葉 名前 役割
Alexa ウェイクワード Alexaを起動させる
メモ 呼び出し名 Alexaに用意した命令(Intent)を実行させるトリガー
メモしたい内容はなんですか? レスポンス Alexaからの返答。変数(slot)を入力するための準備
{}とメモをする インテント Alexaに実行させる命令(関数)
明日はブログを書く スロット Alexa入力時、自由に入力できる変数部分

尚、すでに初めから入っているインテントもあります。例えば「Alexa、とめて」というと、Alexaの動作がストップします。これを「AMAZON.CancelIntent」といい、はじめから備わっています。

  • 呼び出し名 : AlexaにIntentを実行させる準備をする段階です。今回は「メモ」と入力します
  • インテントを作成します。 MemoIntentという名前で登録します。
  • インテントをどうやって呼び出すかです。様々なパターンに耐えられるよう、なるべくたくさん書いておきましょう。
  • {contents}はスロットです。変数もスロットタイプといって、日付や地域など、初めから特別な意味を持たせることもできます。今回のアプリの変数は特に意味を持たせなくても良い文字列なので、SearchQueryにしてみました。*1

ここまでして、「モデルをビルド」ボタンを押します。入力側はこれでOKです!

2. Lambda関数の作成

続いて、「Alexaから入力が入った後の処理」を作る段階です。ここでは、AWSlambdaを使います。手順は以下の通り

  • AWSにログイン後、Lambdaを開く
  • 「1から作る」か「設計図」を選ぶ
    • 本記事をコピペするなら「1から作る」でOK
    • 自分でいろいろ作りたいなら「設計図」を借りる。Pythonならalexa-skills-kit-color-expert-python がおすすめ*2
  • 関数を作成をクリック
  • 関数名を記入(今回はmemoxa_blogとする)
  • ランタイムは選択肢の中から自分が好きな言語を選ぶ(今回はPython3.6)
  • 「テンプレートから新しいロールを選択」する
    • ロール名は適当に入れる(myAlexaRoleなどでOK)
    • ポリシーテンプレートは使うサービスにあったものを選ぶ(今回は「シンプルなマイクロハーネスのアクセス権限」にしてみた*3 )

こんな画面になったら、「関数の作成」をポチ

3. LambdaとAlexaを結びつける

この段階でLambdaとAlexaはまだ連携されていません。連携するためには、先ほどのSkillのページから、Skill IDなるものをLambdaに覚えさせなければなりません。これは、下図の手順でやります。

LambdaがどのAlexaスキルを実行するかを指定できました。

3.5 Slackにメモを飛ばす方法

さて、紐づけができたので、実際にコードを書いていきます。 が、その前に準備。

今回はSlackにメモを飛ばすという機能を持たせたいので、webhookというものを使います。 slackにメッセージを送るためのURLを作るイメージですね。

こちらのQiita記事が非常にわかりやすいです。

こちらの記事ではslackwebというものを使っていますが、URLがわかってしまえば素直にPOSTするだけでもOKです。(詳しくは実装をご覧ください)

※注 : webhookのURLは他人にみられちゃダメです!

4. コードを書く

コードの中身を書きます。MemoIntentが呼び出され、Slotに入っている{contents}を聞き取ってslackに反映させるというコードを書いていきましょう。設計図に従っているので、大枠はすでに出来上がっています。ありがたい。

slackのwebhook URLをtake_memourlの中に入れれば、コピペで動くはずです。

コードをみる
#coding : utf-8
"""
This sample demonstrates a simple skill built with the Amazon Alexa Skills Kit.
The Intent Schema, Custom Slots, and Sample Utterances for this skill, as well
as testing instructions are located at http://amzn.to/1LzFrj6

For additional samples, visit the Alexa Skills Kit Getting Started guide at
http://amzn.to/1LGWsLG
"""

from __future__ import print_function
import urllib.parse
import urllib.request


# --------------- Helpers that build all of the responses ----------------------

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
    }


def build_response(session_attributes, speechlet_response):
    return {
        'version': '1.0',
        'sessionAttributes': session_attributes,
        'response': speechlet_response
    }


# --------------- Functions that control the skill's behavior ------------------

def get_welcome_response():
    """ If we wanted to initialize the session to have some attributes we could
    add those here
    """

    session_attributes = {}
    card_title = "Welcome"
    speech_output = "メモをしたい内容を教えてください"
    reprompt_text = "もう一度。"
    should_end_session = False
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))


def handle_session_end_request():
    card_title = "Session Ended"
    speech_output = "Thank you for trying the Alexa Skills Kit sample. " \
                    "Have a nice day! "
    # Setting this to true ends the session and exits the skill.
    should_end_session = True
    return build_response({}, build_speechlet_response(
        card_title, speech_output, None, should_end_session))


def take_memo(intent, session):
    try:
        memo = intent["slots"]["contents"].get("value")
        url = 'URL for webhook'
        params = urllib.parse.urlencode({"payload": {"text":":memo:"+memo}}).encode("utf-8")
        f = urllib.request.urlopen(url, params)
        speech_output = "Slackにメモをしました"
    except:
        speech_output = "失敗しました"

    session_attributes = {}
    reprompt_text = None
    should_end_session = True
    return build_response(session_attributes, build_speechlet_response(
        intent['name'], speech_output, reprompt_text, should_end_session))


# --------------- Events ------------------

def on_session_started(session_started_request, session):
    """ Called when the session starts """

    print("on_session_started requestId=" + session_started_request['requestId']
          + ", sessionId=" + session['sessionId'])


def on_launch(launch_request, session):
    """ Called when the user launches the skill without specifying what they
    want
    """

    print("on_launch requestId=" + launch_request['requestId'] +
          ", sessionId=" + session['sessionId'])
    # Dispatch to your skill's launch
    return get_welcome_response()


def on_intent(intent_request, session):
    """ Called when the user specifies an intent for this skill """

    print("on_intent requestId=" + intent_request['requestId'] +
          ", sessionId=" + session['sessionId'])

    intent = intent_request['intent']
    intent_name = intent_request['intent']['name']

    # Dispatch to your skill's intent handlers
    if intent_name == "MemoIntent":
        return take_memo(intent, session)
    elif intent_name == "AMAZON.HelpIntent":
        return get_welcome_response()
    elif intent_name == "AMAZON.CancelIntent" or intent_name == "AMAZON.StopIntent":
        return handle_session_end_request()
    else:
        raise ValueError("Invalid intent")


def on_session_ended(session_ended_request, session):
    """ Called when the user ends the session.

    Is not called when the skill returns should_end_session=true
    """
    print("on_session_ended requestId=" + session_ended_request['requestId'] +
          ", sessionId=" + session['sessionId'])
    # add cleanup logic here


# --------------- Main handler ------------------

def lambda_handler(event, context):
    """ Route the incoming request based on type (LaunchRequest, IntentRequest,
    etc.) The JSON body of the request is provided in the event parameter.
    """
    print("event.session.application.applicationId=" +
          event['session']['application']['applicationId'])

    """
    Uncomment this if statement and populate with your skill's application ID to
    prevent someone else from configuring a skill that sends requests to this
    function.
    """
    # if (event['session']['application']['applicationId'] !=
    #         "amzn1.echo-sdk-ams.app.[unique-value-here]"):
    #     raise ValueError("Invalid Application ID")

    if event['session']['new']:
        on_session_started({'requestId': event['request']['requestId']},
                           event['session'])

    if event['request']['type'] == "LaunchRequest":
        return on_launch(event['request'], event['session'])
    elif event['request']['type'] == "IntentRequest":
        return on_intent(event['request'], event['session'])
    elif event['request']['type'] == "SessionEndedRequest":
        return on_session_ended(event['request'], event['session'])

大事なところは3箇所

  • welcome_responseに、はじめのAlexaからのレスポンス(今回はメモの内容)を書いておく
def get_welcome_response():
    ...(中略)...
    session_attributes = {}
    card_title = "Welcome"
    speech_output = "メモをしたい内容を教えてください" #ココ!
    reprompt_text = "もう一度。"
    should_end_session = False
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))
  • on_intentに、MemoIntentが呼ばれた時の挙動を書く
def on_intent(intent_request, session):
    ...(中略)...
    # Dispatch to your skill's intent handlers
    if intent_name == "MemoIntent": #ココ!大文字小文字に注意!
        return take_memo(intent, session)
    elif intent_name == "AMAZON.HelpIntent":
        return get_welcome_response()
    ...(中略)...
  • MemoIntentで呼び出されたintentは辞書形式で値が入っているので、それを考慮した上で関数を作る。URL for webhookには自分がメモしたいslack URLを貼りましょう。
def take_memo(intent, session):
    try:
        memo = intent["slots"]["contents"].get("value") #ココ!
        url = "URL for webhook" #URLは自分で設定!
        params = urllib.parse.urlencode({"payload": {"text":":memo:"+memo}}).encode("utf-8")
        f = urllib.request.urlopen(url, params)
        speech_output = "Slackにメモをしました"
    except:
        speech_output = "失敗しました"

...(略)...

これを、Lambdaの関数コード欄にコピペします。

5. Alexaのエンドポイントを指定

Skill開発者コンソール画面に戻り、最後の仕上げをします。Alexaの命令をどこに送るかのエンドポイントの設定をします。

  • LambdaにはSkill IDを
  • SkillにはLambdaのARNを

それぞれ登録することで、1対1で関数が紐づくわけですね。登録の仕方は画像の通り。 ARNも絶対に他人に見られないようにしてくださいね。

6. 書いたLambda関数をEchoで使えるようにする

最後、EchoとSkillをつなぎます。

Alexa設定ページに行き、下図のように、作ったSkillを有効化しましょう。

6. 実行

最後に実行です。ご自身のEcho dotを使って、試してみましょう!

Echoを持っていなくても、テストをする環境はあるようです。詳しくはコチラ

まとめ

以上、いろんなプラットフォームを使うので、なかなか混乱しがちですが、1から順を追って進めていけば完成するはずです。

尚、自分のスキルを全世界に公開したい!となると、いろいろな制約があるようです。詳しくはこちらのページを参照。β版として自分自身で使うのとは別のことに注意しなければならないので、しっかりチェックしておきましょう!

本記事を参考に、思い思いのスキルを使ってみてはいかがでしょうか?

Alexaから広がる無限の可能性、ワクワクしますね!ではではっ

*1:他のスロットタイプのが良いなどあれば教えてください。

*2:自分のコードもこちらを参考にした

*3:DynamoDB使うわけじゃないから別のでもOKかも

PROCRASIST