32
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Amazon Echo で TVリモコン操作への道 (RPizero & AWSIoT & Lambda & AlexaSkill) 後編

Last updated at Posted at 2017-11-27

前回の記事
Amazon Echo で TVリモコン操作への道 (RPizero & AWSIoT & Lambda & AlexaSkill) 前編
からの続き。

  • RaspberryPiでのリモコン送信機の作成
  • AWS IoTとの接続
  • AWS Lambdaからの操作

ここまでが完了している前提。
ここからはいよいよ本丸のAlexaSkillの作成と連携。
しかも先日ついに日本語バージョンがリリースされたので、日本語での操作を前提とします。

#1 構成

##1.1 全体構成

alexa-qiita2.jpg

ピタゴラスイッチ並みの連携。
Amazon Echo -> Alexa Skill への接続が目標。

AlexaSkillでテレビリモコンを実現するには、
テレビや証明などの公式の製品、デバイス向けに標準化された

今回はより柔軟で、汎用性の高い、カスタムスキルにて構築を前提とする。
実はスマートホームスキルで当初進めていたのだが、使い勝手の問題や日本語化未実装の部分があったりと、やりたいことの実現が今の自分の技術では難しそうだったので、カスタムスキルに変更した経緯もあり。。
この辺りは機会があれば後々紹介しようかと思います。

##1.2 操作対象

今回実現したいアレクサ向けの命令セット

  • 電源のON/OFF
    • 電源をつけて/オンにして 、 電源を消して/オフにして
  • 音量UP/Down
    • 音量をあげて/大きくして 、 音量をさげて/小さくして
  • チャンネル操作(局名)
    • フジテレビにして/フジテレビに変えて 、 (日テレ/TBS/テレ朝/テレ東)
  • チャンネル操作(チャンネル番号)
    • 8にして/8に変えて 、 (4/6/5/7)

#2 Alexa開発ツール

##2.1 Amazon 開発コンソールへログイン

AlexaのSKill開発にはAmazon Developerにアカウント登録が必要。
アカウント作成は無料なのでサクッと登録。

スクリーンショット 2017-11-26 02.34.19.png

ログイン後、Alexaのタブから「Alexa Skills Kit」を選択

スクリーンショット 2017-11-26 02.35.56.png

##2.2 新しいスキルを追加

「新しいスキルを追加する」から新規作成
スクリーンショット 2017-11-26 02.38.07.png
 
 

##2.3 スキル情報の入力

スキルの種類 : カスタム対話モデル
言語 : Japanese
スキル名 : myHomeTV-Ctrl (任意の名前)
呼び出し名 : テレビリモコン ※

※この呼び出し名が、AlexaへのSkill発動文言となる。
 

スクリーンショット 2017-11-26 02.54.54.png

##2.4 対話モデル

対話モデルに進むと、Builderを使うかどうかの選択を迫られる。
(まだBATA扱いなので選択可能だが、今後はこちらが標準となるかもしれない。)
今回はBuilderを使って作成する。

スクリーンショット 2017-11-26 02.56.16.png

Builderを開くと、いきなりUIが変わり初めは面食らうが、、そのうち慣れるので気にしない。

スクリーンショット 2017-11-26 02.57.07.png  

slotの追加

slotとはAlexaから、Lambda側に引き渡す際の変数定義、のようなもの。
電源のオフ、オンであれば、
電源を(オフ / オン) の ()の部分を変数として定義して、Lambda側での処理に利用する。

ここでは
slot : power
として定義。

スクリーンショット 2017-11-26 03.01.33.png

 

また、power に想定されるvalue=値(この場合文言)を定義しておく。

ちなみに、SYNONYMSとは類義語?のようなもので、同じ意味合いの別単語を登録しておくと良いらしい。(、、ただ、私自身まだよくわかってない、、)

スクリーンショット 2017-11-26 03.02.26.png

 
 

Intentsの追加
次にIntentsの追加。ドキュメントによれば
インテントとは、ユーザーの音声によるリクエストを満たすアクション
とのこと。

Alexaの入力と、LambdaへのSlot引き渡しの架け橋、のような位置付けのものと考えていいかと。

動作毎に1インテントを作成するとまとまりが良い。
電源 = tv_switch
音量 = tv_vol
局名指定 = tvch_name
など。

スクリーンショット 2017-11-26 03.00.58.png  

Sample Utterances = サンプル発話集の定義。
想定される発話フレーズセットを登録する。
不特定多数の人向けのSkillなら想像つく限りのフレーズを入れるんだろうけど、今回の用途だとあくまで個人利用前提なので、自身が話しかける予定のフレーズセット登録のみとする。

電源の場合のサンプル発話は

  • 電源を {power} = (つけて・オンにして)

のみ。
ちなみに、フレーズとSlotの間にスペースを入れないと、Build Modelで失敗するので、注意。
上記の場合は「電源を」 と「 {power} 」の間。
(私はこれで悩んだ。。)

スクリーンショット 2017-11-26 03.02.51.png

 

作成したインテントに関連付けたSlotの「Slot Type」を、指定したものに合わせる。
ここも指定し忘れると、Save自体で失敗するので注意。

スクリーンショット 2017-11-26 03.03.35.png

以上の要領で、同様に音量up/downの動作セットのIntentも登録。

Volome インテント
スクリーンショット 2017-11-26 15.40.09.png

Volume スロット
スクリーンショット 2017-11-26 15.41.28.png

 

channelは、
局名(フジテレビ・日テレ・テレ朝、など)で呼ぶ場合と、チャンネル数(8,4,5,6など)で呼ぶ場合があるので、Slotをそれぞれ分ける。

局名指定のインテント・スロット
スクリーンショット 2017-11-26 15.39.01.png

スロット名では各局の呼び名を一通り入力。
ちなみに、TBS/NHKなど大文字で入れているが、Lambdaと連携される際には、英小文字になっているのでLambada側での条件分岐の際には注意。

スクリーンショット 2017-11-26 15.39.16.png

 

局数指定のインテント・スロット
スクリーンショット 2017-11-26 15.39.33.png

単純な数などの場合は、標準提供されているSlot集のうち、AMAZON.NUMBERを利用すると、多少楽。
スクリーンショット 2017-11-26 15.44.26.png

インテント・スロットの設定が一通り完了したら、「Save Model」して、問題なければ、「Build Model」を行う。
この後設定を替えるたびにSave/Buildを実施する必要がある。

スクリーンショット 2017-11-26 15.52.41.png スクリーンショット 2017-11-26 15.53.31.png

これでビルダーでの作業は終了。

#3 AlexaSkill用 Lambda関数の作成

##3.1 Lambdaの作成

2017/11現在、日本語(Japanise)対応のLambdaを設置するリージョンとしては、**オレゴン(us-west-2)**が指定されているので、Lambdaはオレゴンにて作成する。

関数名 : myHomePi-Tv-ctrl
言語 : Python2.7
IAMPolycy : LambdaデフォルトのIAM権限+AWSIoTDataAccess
mem : 128NB
Timeout : 30s

基本的にはAWS Lambdaのブループリント(テンプレート) alexa-skills-kit-color-expert-python をベースにして作成。
ただ、今回は単純なデバイス操作のため、セッション維持は利用せず、単純に1回の応答処理のみを想定しているので、テンプレートの様なAlexaとの会話を想定している訳ではないので、比較的シンプルになっているかも。

lambda_function.py
# coding: UTF-8

from __future__ import print_function
import json
import boto3

ThingName = "myHomePi"

### Alexaへのresponse用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
    }

### Alexaへのresponse用関数
def build_response(session_attributes, speechlet_response):
    return {
        'version': '1.0',
        'sessionAttributes': session_attributes,
        'response': speechlet_response
    }

### Alexa LaunchRequest時のレスポンス用関数
def get_welcome_response():
    """ If we wanted to initialize the session to have some attributes we could
    add those here
    """

    session_attributes = {}
    card_title = "TVリモコン"
    speech_output = "ご命令を"
    # If the user either does not reply to the welcome message or says something
    # that is not understood, they will be prompted again with this text.
    reprompt_text = "やってほしいリモコン操作を教えてください " \
                    "電源をつけて"
    should_end_session = False
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))

### Alexa LaunchRequest時のレスポンス用関数        
def on_launch(launch_request):
    """ Called when the user launches the skill without specifying what they
    want
    """

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

### 聞き取れなかった場合の返答用スピーチセット
def retry_voice():
    response = {
        'version': '1.0',
        'response': {
            'outputSpeech': {
                'type': 'PlainText',
                'text':  "よく分かりません、もう一回言って。",
            }
        }
    }
    return response;
    
### コマンド完了後の返答用スピーチセット
def return_voice(state):
    response = {
        'version': '1.0',
        'response': {
            'outputSpeech': {
                'type': 'PlainText',
                'text':  state + "にしました",
            }
        }
    }
    return response;


### AWS IoTデバイスのシャドウ変更用関数   
def send_command(command):
    
    iot_client = boto3.client('iot-data', region_name='ap-northeast-1')
    
    result = iot_client.get_thing_shadow(
        thingName = ThingName
        )
    result_dict = json.loads(result['payload'].read())
 
    if ( result_dict["state"]["desired"][command] == 0 ):
        shadow = {
            'state': {
                'desired': {
                    command: 1
                }
            }
        }
        payload = json.dumps(shadow)
    else:
        shadow = {
            'state': {
                'desired': {
                    command: 0
                }
            }
        }
        payload = json.dumps(shadow)   
    
    response = iot_client.update_thing_shadow(
        thingName = ThingName,
        payload = payload
    )
    return True

### TV電源操作用関数    
def power_command(power):
    
    print(power)
    
    if(power == "つけて" or power =="オンにして" ):
       send_command("tv_on") 
       print("TV power on!" )
       return return_voice("電源をオン")
       
    elif(power == "消して" or power =="オフにして" ):
       send_command("tv_off") 
       print("TV power off!" )
       return return_voice("電源をオフ")
       
    else:
        print("Not Command")
        return retry_voice()

### TVボリューム操作用関数   
def vol_command(volume):
    
    print(volume)
    
    if(volume == "大きくして" or volume =="上げて" or volume == "あげて" ):
       send_command("volup") 
       print("TV volume up!" )
       return return_voice("ボリュームアップ")
       
    elif(volume == "小さくして" or volume =="下げて" or volume == "さげて" ):
       send_command("voldown") 
       print("TV volume down!" )
       return return_voice("ボリュームダウン") 
    
    else:
        print("Not Command")
        return retry_voice()

### TVチャンネル操作用関数    
def channel_command(tvch):
    
    if(tvch == "フジテレビ" or tvch == "フジ" or tvch =="8"):
       send_command("ch8") 
       print("change channel to " + tvch )
       return return_voice("フジテレビ")
       
    elif(tvch == "日テレ" or tvch =="4"):
       send_command("ch4") 
       print("change channel to " + tvch )
       return return_voice("日テレ")

    elif(tvch == "tbs" or tvch == "ティービーエス" or tvch =="6"):
       send_command("ch6") 
       print("change channel to " + tvch )
       return return_voice("TBS")
       
    elif(tvch == "テレビ朝日" or tvch == "テレ朝" or tvch =="5"):
       send_command("ch5") 
       print("change channel to " + tvch )
       return return_voice("テレ朝")
       
    elif(tvch == "テレビ東京" or tvch == "テレ東" or tvch =="7"):
       send_command("ch5") 
       print("change channel to " + tvch )
       return return_voice("テレ東")
       
    elif(tvch == "nhk" or tvch == "エヌエチケー" or tvch =="1"):
       send_command("ch1") 
       print("change channel to " + tvch )
       return return_voice("NHK")
       
    else:
        print("Not Command")
        return retry_voice()

### インテント処理関数
def on_intent(intent_request):
    print(intent_request)
    
    intent = intent_request['intent']
    intent_name = intent_request['intent']['name']
    
    if intent_name == "tv_switch" :
        
        if  str(intent_request['intent']['slots'].get('power')) != "None" :
            print("Chanege TV power.")
            
            if str(intent_request['intent']['slots']['power'].get('value')) != "None":
                power = str(intent_request['intent']['slots']['power']['value'])
                response = power_command(power)
                return response
            else:
                print("Dont't Tv ctrl.")
                response = retry_voice()
                return response
    
    elif intent_name == "tv_vol" :
        
        if  str(intent_request['intent']['slots'].get('volume')) != "None" :
            print("Chanege TV volume.")
            
            if str(intent_request['intent']['slots']['volume'].get('value')) != "None":
                volume = str(intent_request['intent']['slots']['volume']['value'])
                response = vol_command(volume)
                return response
            else:
                print("Dont't Tv ctrl.")
                response = retry_voice()
                return response
    
    elif intent_name == "tvchannel" or intent_name =="tvchannel_num" :
    
        if  str(intent_request['intent']['slots'].get('AMAZON.NUMBER')) != "None" :
            print("Chanege TV channel.")
            
            if str(intent_request['intent']['slots']['AMAZON.NUMBER'].get('value')) != "None":
                tvch = str(intent_request['intent']['slots']['AMAZON.NUMBER']['value'])
                response = channel_command(tvch)
                return response
            else:
                print("Dont't Tv ctrl.")
                response = retry_voice()
                return response
                
        elif str(intent_request['intent']['slots'].get('tvch_name')) != "None" :
            print("Change TV channel.")
            
            if str(intent_request['intent']['slots']['tvch_name'].get('value')) != "None":
                tvch = str(intent_request['intent']['slots']['tvch_name']['value'])
                response = channel_command(tvch)
                return response
            else:
                print("Dont't Tv ctrl.")
                response = retry_voice()
                return response

### メイン処理
def lambda_handler(event, context):
    if event['request']['type'] == "LaunchRequest":
        return on_launch(event['request'])
    elif event['request']['type'] == "IntentRequest":
        return on_intent(event['request'])
        

ザックリとした処理内容

  • AlexaからSkill呼び出し司令=「テレビリモコンを開いて」が発生したら、Lambdaに対して、LaunchRequestが発生。
  • LambdaはlaunchRequestの場合は、セッション開始応答と、初回スピーチ内容のjsonを返答する。
    • テレビリモコンのスキルが開始したら「ご命令を」と言って、待機状態になる
  • ユーザからの発話の種類に応じて、Alexaスキル側でインテントを選択。
    • 電源のオフオン or 音量up/down or チャンネルの変更 のインテントを判定
  • Lambda側は各インテント毎に処理を分けて、それぞれで正常処理、エラーハンドリングを行い、結果セットを返す。
  • 正常にIoT処理を行なった場合は、〇〇しました、というスピーチセットを返す。指示文言が不明で処理が実行できなかった場合は「もう一度最初から指示してください」というスピーチセットを返し、ユーザにやり直しさせる。

##3.2 Lambda ARNを AlexaSkillに登録
登録したLambdaのARNをメモし、Alexaスキルの「設定」 - グローバルフィールドのデフォルトエンドポイントに入力する。

スクリーンショット 2017-11-26 15.57.06.png

##3.3 Alexa - Lambda間の動作テスト

「テスト」にて、実際にAlexaを経由せずに、ブラウザ上でシミュレーションをすることが可能。

以下の例は、
「電源をつけて」
と発話した場合の応答結果が、どの様にLambdaから返ってくるかを確認している例。
この際にLambda側でエラーが発生し正常処理されないと、レスポンスは返ってこない。

その場合は、通常のLambda関数開発同様にLambdaのエラーログをCloudWatchLogで適宜確認しながらデバックしていく。

スクリーンショット 2017-11-26 16.22.51.png

ここで想定する文言を全て確認しておく。

##3.4 テスト公開準備

以上で、一通りのSkill設定は完了だが、実際にAmazon EchoなどAlexa端末で利用する為には、
公開情報を入力し、「Skill Bata Testing」をアクティブにする必要がある。

まずは公開情報のフィールドに全て入力する。

スクリーンショット 2017-11-26 16.28.58.png スクリーンショット 2017-11-26 16.29.12.png スクリーンショット 2017-11-26 16.29.21.png

画像アイコンはとりあえず、指定されたpixel数の画像を用意しておくこと。

プライバシー・コンプライアンス設定を適宜行う。

スクリーンショット 2017-11-26 16.30.49.png

13際未満の子供を対象としている、に「はい」とすると、、テストできないので注意。。

画像登録すると、スキル一覧にも画像が表示される。
スクリーンショット 2017-11-25 23.33.00.png

取り急ぎ、会社ロゴ入れたけど、なんの機能だか一目じゃわからないことこの上ないので、
ちゃんとTVリモコンアイコンを入れればよかった。

#4 Skillテスト公開

これで以下の通り、今までグレーアウトされていた、スキルの「ベータテスト」が選択可能になっているはず。

スクリーンショット 2017-11-25 23.32.52.png

公開をクリック。
 

##4.1 Skillテスト招待

動かしたいAlexaが登録されているアカウントへ招待メールを送付する。

スクリーンショット 2017-11-25 23.33.41.png

招待メールが、
「You're invited to beta test a new Alexa skill」というサブジェクトでメールが届くので、その中のリンク、
「JP Customers: To get started.」 のリンクをクリックする。

IMG_3358.jpg IMG_3292.PNG

以上で、テスト公開完了。
招待したユーザが管理しているAlexa端末から該当のSkillが使える。
 

ちなみにテスト中はステータスがアクティブとなる。

スクリーンショット 2017-11-28 06.42.28.png

#5 AlexaでSkill実行

これでAlexa側Skill実装も完了しているので、一通りの準備内容を確認

  • RaspberryPiの赤外線送信準備が整っていること(lircdが稼働していること)
pi@raspberrypi:~ $ sudo /etc/init.d/lircd start
[ ok ] Starting lircd (via systemctl): lircd.service.
  • AWSIoTにデバイス登録が完了している。
  • RaspberryPiでAWSIoTとの連携プログラム(Tv.py)がバックグラウンドで稼働していること
pi@raspberrypi:~/python_aws_iot $ python Tv.py 
start readConfig func
start connct shadow
shadow connect
satrt subscribe shadow

実際にAlexaから操作した際の実行例


ユーザー:アレクサ、テレビリモコンを開いて。

カスタムスキル発動

アレクサ: 「ご命令を。」

ユーザー: 電源をつけて。

TVが付く

アレクサ:「はい、電源をつけました。」


ユーザー: アレクサ、テレビリモコンを開いて。

アレクサ: 「ご命令を」

ユーザー: フジテレビに変えて。

フジテレビに変わる

アレクサ: はい。

アレクサ: 「フジテレビに変えました。」


サンプル動画置いてみました。

 
 
#雑感

実際、動くとかなり感動。
結果、自分が、というか子供が大喜びで使ってる。
リモコンがすぐそこにもあるのに、わざわざAlexaに声をかける始末w

デバイスからの準備はなかなか大変だけど、やってみる価値はある。
しかもAlexa端末と、RaspberryPiさえあれば、他、部品代は数百円で、理論的には家中のリモコン操作可能な製品は、Alexa操作が可能になる。

利用方法によっては、夢の広がる技術であることがわかる。
今回はその一端が感じられることが出来たので、とても有意義だった。

今後も改善して行くので、改善ごは追記して行こうかなと。

32
28
0

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
32
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?