chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

Amazon Echo(Alexa)->Lambdaまでを試作したい->Lambaまではつながったが、細部は不明

例のAWS IoTは段々と使える知識は溜まってきたけど、UIというか、表示系、操作系も必要だ。一緒に調べてるメンバからは音声UIと接続させると言われているので、、Amazon Echo(Alexa)からLambdaまでをつないでみたい。それができたら、「アレクサ、LEDつけて」と発話すると、LambdaからAWS IoT Core経由でMQTTが流れて、ラズパイのLEDがチカっと光るはず。またその逆もできたら便利。

人 "LEDつけて"  ->  [Amazon Echo] -> (Alexa) -> (Lambda) -> (AWS IoT Core) -> *MQTT* -> [Laspberry Pi] -> LED

その逆で、、スイッチONしたら、Amazon Echoが 「スイッチ入りました!」と喋る

人 -> [SW]操作-> [Laspberry Pi] -> *MQTT* ->(AWS IoT Core) -> (Lambda) -> (Alexa) -> [Amazon Echo] -> "SW入りました" ->  人 

少し調べると、上記のようにAmazon Echoが発端でしゃべるのは基本的にAPIでは実装できないと思っています(タイマ動作のような仕組みはあるらしいけど)

またしても、、先人の記事があれこれあるのでまず読んでみる
zono_0様、Alexaスキル 入門 & VUI 設計 - Qiita

まずは、Amazon Developer Portal に行くらしい。。 AWSアカウントで使えるのかどうかも分からない。。

Apple Developer Portalでは普段のアマゾンアカウントが使えるようだった。ちょっと楽。 AWSアカウントとどういう関係なのか?はわからず。。

Alexaのスキルを開発する ~その1~ スキルの作成(Amazon Developer Console) – らくがき帖

■構築操作(Alexaカスタムスキル)

alexa developer consoleにアカウント登録、Alexaスキルを作成
ホスティング:自前を選択)
今回設定した対話モデル

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "ゴンザレス",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "HelloWorldIntent",
                    "slots": [],
                    "samples": [
                        "hello",
                        "ハロー",
                        "こんにちは"
                    ]
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "SwitchOnIntent",
                    "slots": [],
                    "samples": [
                        "スイッチをオン",
                        "スイッチを入れる",
                        "スイッチオンして"
                    ]
                },
                {
                    "name": "LEDOnIntent",
                    "slots": [],
                    "samples": [
                        "LEDを入れて",
                        "LEDを入れる",
                        "LEDをオン"
                    ]
                }
            ]
        }
    }
}

■Lambda側

Lambda側の関数定義も結構ややこしくて、以下にサンプルがあるのでこれを打ち込む
初めてのスキル開発 | Alexa Skills Kit
初めてのスキル開発 — Alexa Skills Kit SDK for Python-JA 1.10.0 ドキュメント


サンプルコード全文
サンプルスキル | Alexa Skills Kit

/usr/localを汚染しないように、ユーザのHome環境 /home/xxx 配下にフォルダを作って作業することを推奨されている。

$ virtualenv skill
$ source skill/bin/activate
$ pip install ask-sdk

次にサンプルソースを打ち込む。(Ubuntuとかの環境で走るのかどうかは不明。。試してもない)
moduleをtgzで固める(Ubuntu環境なので。。)。lambda_hander.pyと一緒にAlexa用SDK一式をZIPで固めなおしてLambda環境にアップロードする。単体でtest実行してモジュールエラーがないのを確認

dir: ~/lang/py/skill/lib/python3.8/site-packages
$tar cvfz ask_pkgs.tgz  ask_sdk  ask_sdk_core ask_sdk_model  ask_sdk_runtime

■2020/11/3の進捗
Alexa Developer Consoleからカスタムスキルを登録、Lambdaでカスタムスキルと紐づけられるlambda_functionを定義、Alexaのテスト環境から、「LEDをつけて」と入力、Lambda関数が呼び出される所まで確認。
しかし、、音声UI(Alexa)ってカスタムスキルが有効になっているのか、外れてしまっているのかとかよくわからないし、昔に定義した応答パターンが出てきたりと、、内部動作がブラックボックスでどういう状態で動いているのか全く分からない。。
以下の画面は、Alexa Developerのテスト画面で、LED点けてと入力している(ノートPCに音声入力がないので、、手打ち)

以下はAlexaから呼びされるLambda Function。サンプルソースをほとんどそのままに、LED操作とか少しだけ加えた。ソースコードが長くなっているのは、想定応答ごとに関数が用意されているため(HELLO応答、SW ON応答, LED ON応答, HELP応答, etc)。

# 
# 
# https://developer.amazon.com/ja-JP/docs/alexa/alexa-skills-kit-sdk-for-python/sample-skills.html#source-code-hello-world-sample-skill
#

import logging

from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.utils import is_request_type, is_intent_name
from ask_sdk_core.handler_input import HandlerInput

from ask_sdk_model.ui import SimpleCard
from ask_sdk_model import Response

sb = SkillBuilder()
lambda_handler = sb.lambda_handler()

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


@sb.request_handler(can_handle_func=is_request_type("LaunchRequest"))
def launch_request_handler(handler_input):
    """handler for start skill"""
    # type: (HandlerInput) -> Response
    speech_text = "こんにちは。ゴンザレスです"

    return handler_input.response_builder.speak(speech_text).set_card(
        SimpleCard("HelloWorld", speech_text)).set_should_end_session(
        False).response


@sb.request_handler(can_handle_func=is_intent_name("HelloWorldIntent"))
def hello_world_intent_handler(handler_input):
    """HelloWorldIntent Handler"""
    # type: (HandlerInput) -> Response
    speech_text = "Welcome to Python World"
    return handler_input.response_builder.speak(speech_text).set_card(
        SimpleCard("HelloWorld", speech_text)).set_should_end_session(
        True).response

@sb.request_handler(can_handle_func=is_intent_name("SwitchOnIntent"))
def switch_on_intent_handler(handler_input):
    """HelloWorldIntent Handler"""
    # type: (HandlerInput) -> Response
    speech_text = "スイッチを入れますよ"
    return handler_input.response_builder.speak(speech_text).set_card(
        SimpleCard("HelloWorld", speech_text)).set_should_end_session(
        True).response

@sb.request_handler(can_handle_func=is_intent_name("LEDOnIntent"))
def led_on_intent_handler(handler_input):
    """HelloWorldIntent Handler"""
    # type: (HandlerInput) -> Response
    speech_text = "LEDをオンしますね"
    return handler_input.response_builder.speak(speech_text).set_card(
        SimpleCard("HelloWorld", speech_text)).set_should_end_session(
        True).response




@sb.request_handler(can_handle_func=is_intent_name("AMAZON.NavigateHomeIntent"))
def hello_Navigate_handler(handler_input):
    """Navigate  Intent Handler"""
    # type: (HandlerInput) -> Response
    speech_text = "navi navi 1 2 3"
    return handler_input.response_builder.speak(speech_text).set_card(
        SimpleCard("HelloWorld", speech_text)).set_should_end_session(
        True).response
        



@sb.request_handler(can_handle_func=is_intent_name("AMAZON.HelpIntent"))
def help_intent_handler(handler_input):
    """Help Intent Handeler"""
    # type: (HandlerInput) -> Response
    speech_text = "Please help by yourself"

    return handler_input.response_builder.speak(speech_text).ask(
        speech_text).set_card(SimpleCard(
            "HelloWorld", speech_text)).response


@sb.request_handler(
    can_handle_func=lambda handler_input:
        is_intent_name("AMAZON.CancelIntent")(handler_input) or
        is_intent_name("AMAZON.StopIntent")(handler_input))
def cancel_and_stop_intent_handler(handler_input):
    """intent handler for stop or cancel"""
    # type: (HandlerInput) -> Response
    speech_text = "see you again"

    return handler_input.response_builder.speak(speech_text).set_card(
        SimpleCard("HelloWorld", speech_text)).response


@sb.request_handler(can_handle_func=is_intent_name("AMAZON.FallbackIntent"))
def fallback_handler(handler_input):
    """AMAZON.FallbackIntent available locales"""
    # type: (HandlerInput) -> Response
    speech = (
        "HelloWorld sorry, help you ")
    reprompt = "say hello please"
    handler_input.response_builder.speak(speech).ask(reprompt)
    return handler_input.response_builder.response


@sb.request_handler(can_handle_func=is_request_type("SessionEndedRequest"))
def session_ended_request_handler(handler_input):
    """handler for closing session"""
    # type: (HandlerInput) -> Response
    return handler_input.response_builder.response


@sb.exception_handler(can_handle_func=lambda i, e: True)
def all_exception_handler(handler_input, exception):
    """exception hander
    """
    # type: (HandlerInput, Exception) -> Response
    logger.error(exception, exc_info=True)

    speech = "Im sorry i cant help you... perdon me"
    handler_input.response_builder.speak(speech).ask(speech)

    return handler_input.response_builder.response
####

しまい込んで普段使っていなかったAmazonEchoを取り出して、開発用アカウントに紐づけると、試作中のカスタムスキルが使えた。(*ウエイクワード後*)「LEDをON」を発話すると、「LEDをオンしますね」と返事する。

次にやりたいのは、、「LEDをON」と発話したら、AWS IoTを経由してMQTTでラズパイに記事配信されて、ラズパイのLEDがチカっとなるやつ。MQTTに記事を投稿するのは、boto3を叩けばいいのだけど、上記のLambdaのハンドラからboto3を叩いていいのだろうか。。仕様上は動くだろうけど、AWS IoT + Alexaってそういう連携と解釈していいのか?それとも、もっとエレガントで高尚なフレームワークがあるのか!?

いきなりデバイスめがけて叩くのではなく、仮想デバイス(シャドーとかいうの)に対してAPIで呼び出せよと言われそうだ。
シャドーの説明は以下。仮想デバイスを使うと少しは抽象化されるが。。これを音声用のハンドラから叩いていいのか!?
AWS IoT Device Shadow サービス - AWS(AWS) IoT コア

やっぱり、、ハンドラのLambdaにラズパイのデバイスIDが書かれているのは場当たり実装すぎるだろう。DBかKVSを介してデバイスを抽象化しないと、汎用性の全くない実装になってしまう。まぁ当たり前といえば当たり前だ。。それがまた待たせないために非同期処理ということなるのか。だったら、Message Queueとかでキューイングして、、とかになるのか。。どんどん複雑になるな。。


Alexa, AWS IoT Coreを組み合わせた実装例
AWS IoT でスマートホームを自作する 〜ドアフォン編〜 - builders.flash☆ - 変化を求めるデベロッパーを応援するウェブマガジン | AWS

分かりやすい解説(Amazon制作)
Alexa | アレクサ | Alexaスキル開発トレーニング