LoginSignup
18
20

More than 1 year has passed since last update.

pythonでアレクサに好きな情報を好きなように喋らせたい -1.真面目にスキル開発編-

Last updated at Posted at 2021-07-16

はじめに

数年前、「アレクサ」「オーケーグーグル」なんてフレーズが流行りましたが、最近はあまりきかなくなりましたね。
そんなスマートブームも下火になった昨今ですが、ようやく私もアレクサを導入しました。
(ケーズデンキでAmazon Echo dotの3世代を2000円で買いました!安い!)

さて、この記事は、

「アレクサに話しかけて自分の好きな情報を喋らせよう」
「クレジットカード登録とか一切せずに、完全無料ですませよう」

という至ってシンプルな目的のもとで書いています。
目次としては、以下の通りです。いくつかの記事に分けて投稿予定です。

1.スキル開発で好きな情報を喋らせる!
  →△ 申請に引っかかってできない申請めんどくさい。
2.iphone×pythonista×ショートカットを踏み台にして好きな情報を喋らせる!
  →△ 既存スキルの問題で喋らせられる文字数が少ない。
3.twitterを踏み台にして好きな情報を喋らせる!
  →? いけるんちゃうか???

余談

今回やりたいことはアレクサに任意のお話をさせることです。
ですがこれがなかなか制限があって難しい。

Amazon的な最適解としては、スキル開発で自分の好きなものを作れば良いのですが、
アレクサのスキルは全て公開される性質上、スキルの審査があるので
変なものを作ると審査を却下されるのでちょっとめんどくさい。
それならばと、既存のスキルで行おうとすると
スキルの性能の制限があったりして中々理想通りとはいきません。

その逆に、アレクサを入力端末・トリガーとして動かすことはできそうです。
ifTTTでのgoogleなどの他webサービスへの連携や、ラズパイと連携してシェルを叩く等、
かなり奥が深そう。時間の有るときにでも「node-red」を使ってみたいです。

1.つくってみる

開発自体はかんたんです。びびります。

環境作成

https://developer.amazon.com/alexa/console/ask
上記URLの「スキルの作成」をクリックします。

カスタムを選択します。
image.png

「ユーザー定義のプロビジョン」と「ホスト(python/Node.js)」の二種類があります。

image.png

AWSのアカウントを持っている方は「ユーザー定義のプロビジョン」で良いでしょう。
この場合、実際に稼働するスクリプトはAWS Lambdaで作成するようです。

しかし私の場合。
「AWS?なんかクレジットカード使ってアカウント登録するの怖い・・・」
「気づいたら無料分超えて有料になってそうで怖い・・・」
という、わからないものがとにかく怖いスマホ怖がりお爺さんのような思考をしているので、
AWS Lambdaは使わずに無料で使える「ホスト」の設定で開発します。

ドキュメントよりアレクサのホストスキルの詳細をみてみます。
曰く、開発者のAWSを介さずにAmazonのAWSからスクリプトを呼び出せるようです。
使用できるスキルの数は75個、作成できるスキルはカスタム音声対話モデル限定、使用量にも制限があるようです。

アレクサのスキルは公開されるので、人気出そうな良好スキルを公開すると
アクセスが多すぎて制限が入るかもしませんね。
まぁそれでも便利なことに代わりはないし、
もしそうなれば切り替えれば良いだけなので「Alexa-hosted (Python)」の設定で使っていきます。

image.png

テンプレートはスクラッチで作ってみます。
指定されたワードを言うと、決まったワードを返すという至ってシンプルなサンプルです。

2.ソースをみてみる

コードさえ見れば大体のことがわかるはず。ということで見てみます。
適当なテンプレートでプロジェクトを立ち上げてスキル開発画面の「コードエディタ」を見てみます。

image.png

ここが最初に関数を呼び出す場所です。

これらの関数の中では、入力された発話に対して
1.どのような「リクエスト」が発生したかを振り分けたあと。
2.受信した言葉がどの「インテント」に分類されるかを振り分けて、
3.各インテントごとにどのような出力を行うかを設定しています。

もう少し詳しく見てみましょう。

2-1.入力の処理

インターフェース:AbstractRequestHandler

https://alexa-skills-kit-python-sdk.readthedocs.io/ja/latest/REQUEST_PROCESSING.html#id4
AWSはアレクサから受け取った様々な要求に対して、それぞれの要求に対する処理を行わなければなりません。
例えば、ヘルプを聞きたいならヘルプの処理、キャンセルしたいならキャンセルの処理というふうに、
処理の振り分けが必要です。

そんな振り分け処理を「定義」するのが「AbstractRequestHandler」というインターフェースです。
インターフェースなので、自分で抽象メソッドを定義しないとつかえません。
抽象メソッドはhandleとcan_handleの2つです。

handle関数には、インテント・リクエストが来たときにしてほしいことを書きます。
(例えば、ヘルプが来たらヘルプの処理を行うというふうに。)

can_handle関数には、handle関数に実行してほしいインテント・リクエストの判定を行ってもらいます。
(例えば、ヘルプが来たのにキャンセルの処理が行われないように、種類の判定を行います。)

リクエスト

リクエストとはなにか。
例えば誰かに

「上げておいてー」

とだけ言われたとします。
これだけ言われても、誰だって「何を・・・?」となります。
本人は、「(nasに勤務表を)上げておいてー」という意味で言っていたとしても、
わかるわけがありません。

すなわち、誰か(AWS)にお願いする際には
どのような種類の「リクエスト」を行いたいかを明記する必要があるわけです。

そういった意味での「リクエスト」。
アレクサにお願い(リクエスト)できることは4つです。

主なリクエストの種類
LaunchRequest・・・・・・・話者『スキルが起動したい!』
IntentRequest・・・・・・・話者『発動済みのスキルに言葉を呼びかけたい!』
SessionEndedRequest・・・・Alexa『話者が途中で会話を放り出しちゃった。スキルを途中で中断するわ』
CanFulfillIntentRequest・・筆者『定型文に当てはまらない言葉をなげかけられても対応できるらしいけどやり方はよくわからん』

LaunchRequest、IntentRequestはガッツリ使うので覚えたほうがよいです。(以降に書きます)
SessionEndedRequestは、スキルが「想定外」に終わった場合に発生するリクエストです。
スキルが終わったあとに必ずやらないといけない処理がある場合は、
この処理はかなり重要かもしれません。設定を忘れると痛い目をみるリクエストかもしれません。
CanFulfillIntentRequestは知らん。

インテント

インテント。わかりやすく曲解するなら、
アレクサに話しかけた言葉がどんな分類になるかを振り分けたものです。
```
https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/standard-built-in-intents.html#available-standard-built-in-intents

主なインテントの種類
AMAZON.FallbackIntent・・・話者が想定していないことを話したときに呼び出されるインテント
AMAZON.CancelIntent・・・・スキルをキャンセルされたときに呼び出されるインテント
AMAZON.StartOverIntent・・・アクションのやり直しを実行できるインテント
xxxxxxxxxxxxxIntent・・・・自分で定義したインテント

たとえば、
「アレクサ、ラジコ開いて」
というフレーズの場合、スキルを起動するフレーズなので「LaunchRequest」のインテントが発生します。

その直後に、
「おちんちんびろーん」
というフレーズを言った場合は、意図不明のフレーズなので「AMAZON.FallbackIntent」のインテントが発生します。

インテント・リクエストの動作をみてみる

実際に超シンプルな例でやってみます。

test.py
# -*- coding: utf-8 -*-

import logging
import ask_sdk_core.utils as ask_utils

from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.handler_input import HandlerInput

from ask_sdk_model import Response

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


class TestHandler(AbstractRequestHandler):
    """Handler for Skill Launch."""
    def can_handle(self, handler_input):
        return True

    def handle(self, handler_input):
        # global intent_name
        request_Launch = ask_utils.is_request_type("LaunchRequest")(handler_input)
        request_Intent = ask_utils.is_request_type("IntentRequest")(handler_input)

        intent_cansel = ask_utils.is_intent_name("AMAZON.CancelIntent")(handler_input)
        intent_Hello = ask_utils.is_intent_name("HelloWorldIntent")(handler_input)
        intent_Fall = ask_utils.is_intent_name("AMAZON.FallbackIntent")(handler_input)
        intent_Next = ask_utils.is_intent_name("AMAZON.NextIntent")(handler_input)

        speak_output = "インテントの種類を確認します。"
        if request_Launch:
            speak_output += ".ラウンチリクエスト"
        elif request_Intent:
            speak_output += ".インテントリクエスト"
            if intent_Hello:
                speak_output += "=ハロー"
            if intent_Fall:
                speak_output += "=フォール"
            if intent_cansel:
                speak_output += "=キャンセル"
        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask(speak_output)
                .response
        )

sb = SkillBuilder()
sb.add_request_handler(TestHandler())
lambda_handler = sb.lambda_handler()

image.png

ポイントは階層構造になっている点です。
インテントはIntentRequestのリクエスト上でしか発生しません。
そしてインテントは、言葉の意味ごとに分類されるということがわかります。

アレクサの引数=スロット

アレクサのインテントには、引数を選別する機能があります。
アレクサでは引数のことを「スロット」と呼び、
発話された言葉のどこを引数としてプログラム上で処理し、
また、内部でどのように変換して変数に格納するかをコントロールできます。

ということでいくつか試してみます。

引数:カスタムスロット

例えば、「アレクサ、日本語で挨拶して」というように発話された状況を考えます。
プログラムとしては何語で喋って欲しいのかを取得したい状況です。

test.py

import logging
import ask_sdk_core.utils as ask_utils

from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.handler_input import HandlerInput

from ask_sdk_model import Response

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


class TestHandler(AbstractRequestHandler):
    """Handler for Skill Launch."""
    def can_handle(self, handler_input):
        return True

    def handle(self, handler_input):
        # global intent_name
        request_Launch = ask_utils.is_request_type("LaunchRequest")(handler_input)
        request_Intent = ask_utils.is_request_type("IntentRequest")(handler_input)

        intent_cansel = ask_utils.is_intent_name("AMAZON.CancelIntent")(handler_input)
        intent_Hello = ask_utils.is_intent_name("HelloWorldIntent")(handler_input)
        intent_Hello = ask_utils.is_intent_name("HelloWorldIntent")(handler_input)
        intent_Fall = ask_utils.is_intent_name("AMAZON.FallbackIntent")(handler_input)
        intent_Next = ask_utils.is_intent_name("AMAZON.NextIntent")(handler_input)

        intent_Date = ask_utils.is_intent_name("DateIntent")(handler_input)

        speak_output = "インテントの種類を確認します。"
        if request_Launch:
            speak_output += ".ラウンチリクエスト"
        elif request_Intent:
            speak_output += ".インテントリクエスト"
            if intent_Hello:
                speak_output += ".HelloWorldIntent."
                slots = handler_input.request_envelope.request.intent.slots
                slot_lg = slots['Language']
                if slot_lg.value is None:
                    speak_output += "引数がないよ"
                elif slot_lg.value == "日本語":
                    speak_output += "引数が日本語だよ"
                elif slot_lg.value == "英語":
                    speak_output += "引数が英語だよ"
                elif slot_lg.value == "":
                    speak_output += "引数が空だよ"

            if intent_Fall:
                speak_output += "=フォール"
            if intent_cansel:
                speak_output += "=キャンセル"

        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask(speak_output)
                .response
        )

sb = SkillBuilder()
sb.add_request_handler(TestHandler())
lambda_handler = sb.lambda_handler()

「日本語」と「英語」のいずれかの引数を受け取るように設定しました。
下記の画像より、引数を識別して言語が何かを取得していることがわかります。

image.png

これを実現する手順を説明します。
これを実行するには「変数」と「型」の定義が必要です。
手順は以下のとおりです。

1.「ビルドモード」を選択。
image.png

2.対話モデル->スロットタイプで「スロットタイプ」を選択。
スロットタイプの画面は、このプロジェクトで使う型を指定する画面になります。
image.png

2.ここでは「独自の型」を指定します。「int hensuu」で言うところの「int」の部分です。
作りたい型としては、「日本語」「英語」以外に値をもたないような超特殊な型を定義します。
image.png

3.型に格納できる文字を定義します。
この定義だと、日本語と英語以外に格納できない型になります。
image.png

4.ここまでの手順では型を定義しただけなので、実際に型を使ってみます。
まず、どのインテントで引数を取得するかを選択します。
ここでは、「ハローテスト」スキルを司っている「HelloWorldIntent」を選択します。
image.png

4.次に、「変数」として「Language」を定義します。
「int hensuu」で言うところの「hennsuu」の部分です。
画像では「{Language}で喋れ」というフレーズが登録しました。
このフレーズを言うと{Language}の部分を変数に入れるという仕組みになっています。
image.png

5.変数を宣言したら、次は変数の型を選択します。
画像で言うところの「スロットタイプ」とあるところです。
先程定義した型を入れましょう。
image.png

引数:その他Amazonの既存の型

AMAZON.DATE
AMAZON.DURATION
AMAZON.FOUR_DIGIT_NUMBER

という型を使ってみます。
以下、各型の説明をコピペ

AMAZON.DATE
日付(「今日」、「明日」、または「7月」)を表す単語を日付形式(「2015-07-00T9」など)に変換します。

AMAZON.DURATION
期間 (「5 分」) を表す単語を数字で表した期間(「PT5M」)に変換します。

AMAZON.FOUR_DIGIT_NUMBER
4 桁の数字を表す単語(「六〇四五」)を数列 (「6045」)に変換します。

下準備として、ビルドモードで「DateIntent」というインテントを作ります。
image.png

各型を使用した変数を定義します。
image.png

そしてコードを変えて実行。

test.py

import logging
import ask_sdk_core.utils as ask_utils

from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.handler_input import HandlerInput

from ask_sdk_model import Response

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


class TestHandler(AbstractRequestHandler):
    """Handler for Skill Launch."""
    def can_handle(self, handler_input):
        return True

    def handle(self, handler_input):
        # global intent_name
        request_Launch = ask_utils.is_request_type("LaunchRequest")(handler_input)
        request_Intent = ask_utils.is_request_type("IntentRequest")(handler_input)

        intent_cansel = ask_utils.is_intent_name("AMAZON.CancelIntent")(handler_input)
        intent_Hello = ask_utils.is_intent_name("HelloWorldIntent")(handler_input)
        intent_Hello = ask_utils.is_intent_name("HelloWorldIntent")(handler_input)
        intent_Fall = ask_utils.is_intent_name("AMAZON.FallbackIntent")(handler_input)
        intent_Next = ask_utils.is_intent_name("AMAZON.NextIntent")(handler_input)

        intent_Date = ask_utils.is_intent_name("DateIntent")(handler_input)

        speak_output = "インテントの種類を確認します。"
        if request_Launch:
            speak_output += ".ラウンチリクエスト"
        elif request_Intent:
            speak_output += ".インテントリクエスト"
            if intent_Hello:
                speak_output += ".HelloWorldIntent."
                slots = handler_input.request_envelope.request.intent.slots
                slot_lg = slots['Language']
                if slot_lg.value is None:
                    speak_output += "引数がないよ"
                elif slot_lg.value == "日本語":
                    speak_output += "引数が日本語だよ"
                elif slot_lg.value == "英語":
                    speak_output += "引数が英語だよ"
                elif slot_lg.value == "":
                    speak_output += "引数が空だよ"


            if intent_Date:
                speak_output += "=DateIntent."
                slots = handler_input.request_envelope.request.intent.slots
                slot_dt = slots['Date']
                slot_am = slots['amount']
                if slot_dt.value is not None:
                    speak_output += slot_dt.value
                if slot_am.value is not None:
                    speak_output += slot_am.value

            if intent_Fall:
                speak_output += "=フォール"
            if intent_cansel:
                speak_output += "=キャンセル"

        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask(speak_output)
                .response
        )

sb = SkillBuilder()
sb.add_request_handler(TestHandler())
lambda_handler = sb.lambda_handler()

結果は以下のとおりです。
image.png

今日が"2021-07-16"、"四五"が45になっていることが確認できました。
型を指定することで、扱いやすい形式に文字を変換できることがわかります。

2-2.応答の処理

さて、ここまでは入力されたデータを元に色々処理を行ったわけですが、
アレクサにはその結果を受けた何かしらの出力をさせなければなりません。

test.py
# -*- coding: utf-8 -*-

import logging
import ask_sdk_core.utils as ask_utils

from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.handler_input import HandlerInput

from ask_sdk_model import Response
from ask_sdk_model.interfaces.display import (
    RenderTemplateDirective,
    BodyTemplate1)
from ask_sdk_core.response_helper import get_text_content

from ask_sdk_model.interfaces.display import (
    RenderTemplateDirective,
    BodyTemplate1)
from ask_sdk_model.directive import *
from ask_sdk_model.dialog import *
from ask_sdk_core.skill_builder import SkillBuilder
from ask_sdk_core.dispatch_components import AbstractRequestHandler
import ask_sdk_core.utils as ask_utils
from ask_sdk_core.handler_input import HandlerInput
from ask_sdk_model import Response
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


class TestHandler(AbstractRequestHandler):
    """Handler for Skill Launch."""
    def can_handle(self, handler_input):
        return True

    def handle(self, handler_input):
        # global intent_name
        request_Launch = ask_utils.is_request_type("LaunchRequest")(handler_input)
        request_Intent = ask_utils.is_request_type("IntentRequest")(handler_input)
        intent_cansel = ask_utils.is_intent_name("AMAZON.CancelIntent")(handler_input)
        intent_Hello = ask_utils.is_intent_name("HelloWorldIntent")(handler_input)
        intent_Fall = ask_utils.is_intent_name("AMAZON.FallbackIntent")(handler_input)
        intent_Next = ask_utils.is_intent_name("AMAZON.NextIntent")(handler_input)
        response_builder = handler_input.response_builder
        speak_output = "インテントの種類を確認します。"
        if request_Launch:
            speak_output += ".ラウンチリクエスト"
        elif request_Intent:
            speak_output += ".インテントリクエスト"
            if intent_Hello:
                speak_output += "=ハロー"
                response_builder.ask(speak_output)              #スキル継続
            if intent_Fall:
                speak_output += "=フォール"
                response_builder.set_should_end_session(True)   #スキル終了
            if intent_cansel:
                speak_output += "=キャンセル"
                response_builder.set_should_end_session(True)   #スキル終了
        # # 応答の設定
        # 何を話させるか
        response_builder.speak(speak_output)
        # スキルのカード情報に何を表示するか
        response_builder.set_card(SimpleCard("ハローワールド", speech_text))
        # ディレクティブに何を渡すか
        response_builder.add_directive(
            RenderTemplateDirective(
                BodyTemplate1(
                    title=speak_output,
                    text_content=get_text_content(primary_text=speak_output)
                )
            )
        )

        return response_builder.response
sb = SkillBuilder()
sb.add_request_handler(TestHandler())
lambda_handler = sb.lambda_handler()

応答の種類としては以下のとおりです。

test.py
主な応答の種類
speak・・・・・・・・・・ アレクサに話させる内容を指定
ask・・・・・・・・・・・ アレクサにラウンチしているスキルを継続さ瀬田状態で話させる
set_card・・・・・・・・・アレクサにスキルのプロフィール情報みたいな表示情報を指定
set_should_end_session・・アレクサにスキルを終了させる
add_directive・・・・・・ アレクサに送信するディレクティブを指定

ここでは、応答という言葉の文字通り、
AWSがアレクサに対して何を行うか指定することになります。

文字の応答としては、
speakで話す内容を指定して、askで引き続きスキルを発動するか指定して、
set_should_end_sessionでスキルを終了させる、・・・という一連の流れを行います。

一方、アレクサでは歌や動画を流す処理も存在します。
その場合、文字以外の情報のやり取りを行わなければなりません。
そんな処理を司るのがadd_directiveで、
ディレクティブと呼ばれるものを送信してやる必要があります。

上記のソースでは、RenderTemplateDirectiveというものを渡しています。
これはアレクサ既存のテンプレートに従った視覚情報を提供するもので、
BodyTemplate1というテンプレート(イメージは下記参照)に則って視覚情報をアレクサに送信します。
https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/display-template-reference.html#device-display-comparisonbodytemplate1
image.png

3.その後

ここまでの知識を生かして、ニュースを読ませるスキルを申請しました。
結果は・・・・・・・・・・申請通りませんでした。

4.結論

申請通るように修正めんどい!誰にでも使えるようにするの嫌だ!
ということで、別の方法を模索します。

次回につづく。

18
20
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
20