サンタクロースが領空侵犯？してきたときに教えてくれるAlexaスキルをつくる

普段はLIFULL HOME'SのiOS版を担当しています。LIFULLの山手です。
プライベートでは、VUI(Voice user interface)関連、中でもAmazon Alexaに力を入れて活動しています。

# 前置き
今日はクリスマス・イブです。
子供の頃は、年に1度もらえるサンタさんからのプレゼントを楽しみにしていました。
しかし、子供の頃から今に至るまでサンタクロースの本当の姿を見たことはありません。
噂によると、鈴をシャンシャン鳴らしてトナカイと一緒に海を超え飛んでくるそうなのです。
ただ、今となって考えると**サンタクロースってレーダーから見ると正体不明機の扱いで、実は領空侵犯してるんじゃない？**と思うことが多々あります。

[不明機はサンタ、どう対処？　「対領空侵犯措置」の観点から見る日本の聖夜](https://trafficnews.jp/post/62058)

こんな記事もあるので、厳密には領空侵犯扱いになるのでしょう。大人たちが日本の空を守りつつサンタクロースであることを確認しているから日本の子どもたちにプレゼントが届いているんだと思います。
そんな国防に関わるかもしれない情報でもサンタクロースが日本にやってきたときに知りたいですよね？
そんな夢を叶えるリアルタイムでサンタクロースが日本に来たことを教えてくれるAlexaスキルを今回つくってみました。

# サンタクロースの追跡する技術
前置きはさておき、技術的にサンタクロースをどう追跡するか？
今回はNORAD(北米航空宇宙防衛司令部)が提供するサンタクロース追跡情報を活用します。
NORADは、約60年前に掛かってきた間違い電話以来ずっとサンタクロース追跡のプロです。もちろん普段は防衛のプロです。
現在は下記URLのNORAD Tracks Santaというサイトで、サンタクロースの現在位置情報を発信し続けています。
[NORAD Tracks Santa](https://www.noradsanta.org)

ありがたいことにTwitterアカウントも開設されており、Twitterからも情報を取得することが可能です。今回はこちらのアカウントに投稿される内容を元にサンタクロースが来日したか判別したいと思います。

<img width="598" alt="スクリーンショット 2019-12-18 21.49.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114168/2faa8c82-e9ca-c1be-df7b-9a747c8e2c14.png">

最初、公式サイトをスクレイピングして情報を得ようとも考えましたが、利用規約以前にアメリカ政府のサイトをスクレイピングする事自体が恐怖でしかなかったのでやめました。

## Tweetを取得する
今回は、NORADSantaのつぶやきを取得したいので``statuses/user_timeline``を利用して取得します。

```tweet_getter.py
import json, csv, re
import configparser
from requests_oauthlib import OAuth1Session

CONFIG_FILE_PATH = 'santa_guard.ini'

REQUEST_ENDPOINT = 'https://api.twitter.com/1.1/statuses/user_timeline.json'
USER_SCREEN_NAME = 'UserName'

# Tweetを取得します
def get_tweets ():
    config = configparser.ConfigParser()
    config.read(CONFIG_FILE_PATH, 'UTF-8')

    consumer_key = config['Twitter']['ConsumerKey']
    consumer_secret = config['Twitter']['ConsumerSecret']
    access_token = config['Twitter']['AccessToken']
    access_secret_token = config['Twitter']['AccessSecretToken']

    # OAuth認証
    twitter = OAuth1Session(consumer_key, consumer_secret, access_token, access_secret_token)
    # 取得済みのTweetのIDをtxtファイルから取得する
    last_id = get_latest_tweet_id()
    # パラメーターを設定
    search_parameter = {'screen_name': USER_SCREEN_NAME , 'since': last_id}
    req = twitter.get(REQUEST_ENDPOINT , params = search_parameter)
    # レスポンス200のとき処理
    if req.status_code == 200:
        # Userのタイムラインを取得
        user_timelines = json.loads(req.text)

        # 最新のIDを取得し、txtファイルに書き込む
        latest_id = user_timelines[0]['id']
        update_latest_tweet_id(str(latest_id))

        # tweetを抽出する
        tweets = list(map(lambda x: extraction_tweet(x), user_timelines))

        return tweets

# Tweet内容を抜き出します
def extraction_tweet (tweet):
    tweet_text = tweet['text']
    return tweet_text

# 保存済みの最新のTweetIDを保存します
def get_latest_tweet_id ():
    file = open("./parms/latest_tweet_id.txt")
    latest_id = file.read()
    file.close()
    return latest_id
    
# 取得済みの最新のTweetIDを保存する
def update_latest_tweet_id (latest_id):
    file = open("./parms/latest_tweet_id.txt", "w")
    file.write(latest_id)
    file.close()
    print('latest id update' + latest_id)

if __name__ == '__main__':
    tweets = get_tweets()
    print(tweets)
```
各シークレットキーは、``santa_guard.ini``に記載されています。
TwitterAPIの利用方法はこちらの記事を参考にさせていただきました。ありがとうございます。
[PythonでTwitter API を利用していろいろ遊んでみる - bakiraさん](https://qiita.com/bakira/items/00743d10ec42993f85eb)

処理としては簡単で、認証処理後取得済みのTweetの最新のIDと検索したいユーザーのスクリーンネームをパラメーターとして``statuses/user_timeline``に対して送付することでタイムラインのTweetを最大100件取得することができます。
上記のスクリプトを単体で、動かした場合下記のような結果が出力されます。

```sh
$ python3 tweet_getter.py
latest id update1207360210958012416
["#Santa tracking starts with our North Warning System! It's 47 powerful #radars spanning the #Arctic from Alaska thr… https://t.co/2RkBb9OOVl", "How high can #Santa jump? Find out by playing Santa Tower Jump in today's Countdown Calendar Game #18, at… https://t.co/USpvlfpMn2", 'Got your official #NORADTracksSanta gear? \n\nOrder by Dec 17 to get it by Dec.24. https://t.co/tEMWpVyTEU https://t.co/XmZGVqQ6MS', '@RCAF_ARC @CanadianForces @CFOperations @NORADCommand @usairforce @CanMNews @CAFinUS Thanks team!!!', 'Meet the @RCAF_ARC pilots &amp; trackers who are working with #Santa #ChristmasEve! These teams #Defend North America e… https://t.co/dmHF9ozNPX', '@CanEmbUSA @NORADCommand @CAFinUS Thank you!!!', "Play Ski Rush in today's Countdown Calendar Game #17, at https://t.co/pdLergThH4. Avoid the obstacles and gather th… https://t.co/DqQGWjGPXQ", '@Tracker224 We try to make it a combination of both.  We have lots of stuff with just #Santa.', '@starree Thank you!!!', 'The @RCAF picks a special team to escort #Santa through Canadian airspace on #ChristmasEve! Meet these special Cana… https://t.co/Gr1n2rXn1i', "@Tracker224 We don't militarize the program, this is who we are... We ARE U.S. &amp; Canadian military forces, doing ho… https://t.co/NiE5OJUlzE", '#NORAD tracks #Santa uses the same technology (#jets, #radars, &amp; #satellites) that #defends the skies over North Am… https://t.co/ipZq8valYa', 'RT @HQ_AFMC: In less than 8 days, our friends at @NoradSanta will add an extra mission to the watch, as they track #Santa across the globe.…', "Play #Santa on Skates in today's Countdown Calendar Game #16, at https://t.co/pdLergThH4  \nMake sure you turn on th… https://t.co/E5TH7i5Slz", "Today's Countdown Calendar Game #15 is a tricky one! Play Narrow Passage at https://t.co/pdLergThH4\n#NoradSanta… https://t.co/WNyzjNNmAl", 'RT @meshbox: Founder Lynn Fredricks interviewed by #Renderosity Magazine about Toon Santa, NORAD Tracks #Santa and #PDX https://t.co/495WC4…', 'RT @1stAF: Tracking Santa continues at a recent community event attended by NORAD’s Continental U.S. Region based at Tyndall Air Force Base…', "Would you like to play a game? How about a nice game of Tic Tac Toe! Today's Countdown Calendar Game #14, at… https://t.co/Comlr7YTEG", "RT @cradlepoint: Helping keep NORAD's Santa Tracks headquarters connected is one of the coolest pop-up network stories around. @NoradSanta…", "Play #Santa Claus Jump in today's #NORAD Tracks #Santa Countdown Calendar Game #13, at https://t.co/pdLergThH4… https://t.co/mlajMh4KOY"]
```
これで、NORADのTweetを取得できるようになりました。
Tweetに**Japan**であったり**Tokyo**といった日本の地名が含まれているか判別することで、サンタクロースが大体どこらへんにいるのかわかるようになります。
現状取得できているTweetは自然言語の文字列でしかないため、実際に解釈するには形態素解析を行い意味を持つ最小の単位まで単語を分割し、その単語が名詞であるか否か判別してから日本の地名リストとマッチングするか確認することが必要です。

## Polyglotを用いて品詞推定を行う
日本語の形態素解析を行う際は、[MeCab](https://taku910.github.io/mecab/)が形態素解析ツールとして有名ですが、英文となるとあまり日本での事例は多くないと思います。
今回は英文の形態素解析ツールのPolyglotを用いて形態素解析と品詞推定を行います。
また、今回サンタクロースが日本に来たタイミングでAlexaに能動的に発話させるためにAlexa gadgets tool kitを用いるので、解析処理はRaspberry pi上で実行します。
PolyglotをRaspberry pi上で動かすには少しコツがありますので、下記記事にまとめています。別途ご参照ください。

[PolyglotをRaspberry Piで動かして英文の形態素解析を行う](https://qiita.com/MYamate_jp/items/f2085d2925515b181511)

```named_entity.py
from polyglot.text import Text
import cities

# 日本が含まれているか確認する
def match_japan(tags):
    isMatch = False
    for token in tags:
        if token[0] == "Japan" and token[1] == "PROPN":
            isMatch = True
    return isMatch

# 日本の都市名が含まれているか確認する
def match_city(tokens, cities):
    # 初期返却値はNone
    result = None
    for token in tokens:
        # 固有名詞のときだけ処理する
        if token[1] == "PROPN":
            for city in cities:
                if city[0] == token[0]:
                    result = city[1]
    return result

# 品詞タグ付けを行います
def named_entity(text):
    tokens = Text(text)
    return tokens.pos_tags

if __name__ == '__main__':
    cities = cities.get_list()
    tags = named_entity("Hello world Japan and Tokyo")
    print("--TAGS--")
    print(tags)
    print("--Japan_match--")
    print(match_japan(tags))
    print("--Match_city--")
    print(match_city(tags, cities))
```
まず、Tweetを文字列として受け取り、Polyglotを用いて文字列を分解、品詞推定を行います。
別に英表記の都市名と日本語の都市名の対象表を``cities.get_list()``から取得し、解析結果の各要素と英表記の都市名が一致するか確認し、要素が品詞である場合はTweetに含まれていた単語の要素が日本の都市名であると判別し、サンタクロースがいる日本の都市として採用する流れです。
上記のスクリプトを実行すると下記のような結果を取得することができます。

```sh
$ python3 named_entity.py
--TAGS--
[('Hello', 'INTJ'), ('world', 'NOUN'), ('Japan', 'PROPN'), ('and', 'CONJ'), ('Tokyo', 'PROPN')]
--Japan_match--
True
--Match_city--
東京都
```

英表記の都市名と日本語の都市名の対照表は、Wikipediaより拝借しております。そのため公開は無しです。
[List of cities in Japan - Wikipedia](https://en.wikipedia.org/wiki/List_of_cities_in_Japan)

形態素解析の品詞推定の精度は辞書によって左右されるためマイナーな地名の固有名詞として認識されず判別出来ない場合もありますが、過去の追跡記録を見る限りNORADの追跡力では知名度のある都市にいるときにしかサンタクロースがいるという報告ができないようなので、NORADの追跡力であれば都市名を判別することが出来ると思います。

## Alexaを能動的に発話させてサンタクロースの接近を通知する
さて、サンタクロースがいる都市名まで分かってしまえば、あとはAlexaに発話してもらうだけです。
### サンタトラッカーのスキルはある
すでに、NORADが提供しているサンタトラッカーのスキルは公式で存在しています。
<img width="810" alt="スクリーンショット 2019-12-21 11.35.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114168/c99ce919-598c-d5e3-ab89-284f50ccdf19.png">
[NORAD - サンタトラッカー](https://www.amazon.co.jp/NORAD-サンタトラッカー/dp/B077D2GY1Z)
これでは公式と変わりません。僕たちは、サンタクロースの接近を聞いて知りたいのではなく、サンタクロースが来日したタイミングでどこにサンタクロースがいるか知りたいのです！

### Alexa gadgets tool kitを用いて能動的に発話させる
従来のAlexaスキルは、ユーザーが発話してそれに対してAlexaが答えを発話する会話のキャッチボールでスキルのシナリオを進めていました。
Alexa gadgets tool kitの登場によりガジェットのセンサー入力値を元にAlexaスキルにイベントを渡すことが可能になり、スキルが起動していることが前提にはなるもののAlexaから能動的に発話させることが可能になりました。
能動的に発話させる方法に関しては、11月にAAJUG京都で発表した内容をご覧ください。

[Alexa Gadgets Toolkitを使ってAlexaスキルのセッションを永続化させる](https://qiita.com/MYamate_jp/items/0271f5b5e0b993a083ae)
[Alexa Gadgets Toolkitから広がるVUIの可能性 - SlideShare](https://www.slideshare.net/MasamiYamate1/alexa-gadgets-toolkitvui)

### ガジェット側の実装

```santa_guard.py
import json
import logging
import sys
import threading
import signal
import time
import cities
import tweet_getter
import morphological

from agt import AlexaGadget

logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logger = logging.getLogger(__name__)

class SantaGuard(AlexaGadget):
    def __init__(self):
        super().__init__()

    # サンタクロースの追跡開始時のタスク
    def on_custom_santaguard_start(self, directive):
        self.is_persistence_run = True

        # セッションの永続化を開始する
        self.session_persistence_thread = threading.Thread(target=self.session_persistence)
        self.session_persistence_thread.start()

        # サンタクロースの追跡を開始する
        self.santaradar_thread = threading.Thread(target=self.santa_radar)
        self.santaradar_thread.start()

    # サンタクロース追跡終了時のタスク
    def on_custom_santaguard_end(self):
        self.is_persistence_run = False

        self.session_persistence_thread.clear()
        self.santaradar_thread.clear()

    # サンタクロースの追跡を行う定期タスク
    def santa_radar(self):
        while self.is_persistence_run:
            print('tweet')
            tweets = tweet_getter.get_tweets()
            tweet = tweets[-1]

            cities_list = cities.get_list()
            tokens = morphological.morphological(tweet)
            is_match_japan = morphological.match_japan(tokens)
            city_name = morphological.match_city(tokens, cities_list)

            # サンタが日本に来ているか判別する
            if is_match_japan and city_name != None:
                # サンタが日本に来ている場合
                payload = {'city_name': city_name }
                self.send_custom_event(
                    'Custom.Santaguard', 'SkillHandler', payload)

            # レートリミットに当たるのを回避するために単純に2分間隔でリクエストする
            time.sleep(120)

    # セッションを永続化する処理 30秒おきにAlexaにSendする
    def session_persistence(self):
        while self.is_persistence_run:
            print('send')
            self.send_custom_event(
                'Custom.Santaguard', 'SkillHandler', {})
            time.sleep(30)


if __name__ == '__main__':
    try:
        SantaGuard().main()
    finally:
        logger.info("")
```

Alexaスキルが立ち上がった際に発火する``LaunchRequest``のタイミングで、Echoデバイスと接続されているRaspberry Piに対して``on_custom_santaguard_start``イベントをリクエストします。
Raspberry Pi側では、``on_custom_santaguard_start``イベントを受け取った後にTweetの取得とEchoデバイスとのセッション永続の処理を定期的に実行し始めます。
Tweetの取得は、本来であればレートリミットを取得してそれに合わせて実行すべきですがここでは簡単に2分おきに情報を取得するようにしています。

また、セッション永続化の処理も合わせて定期的に行っている理由としては、Alexaスキルは特定の時間ユーザーからの発話やガジェットのイベントを受け取らないとスキルがクローズしてしまい再びスキルを立ち上げなおす必要があります。
その立ち上げ直しを不要にし、サンタクロースが来日した情報が取得できたタイミングでAlexaに能動的に発話させるためにセッション永続化の処理を入れています。

### Alexaスキル側の実装
```skill-script.js
/**
 * gadgetからサンタクロースの追跡結果を発話させる
 *
 * @param {*} handlerInput
 */
function gadgetSantaResponse (handlerInput) {
    let sessionToken = gadgetUtil.sessionToken(handlerInput);
    // 永続化用のディレクティブ
    let persistenceDirective = gadgetDirective.sessionPersistence(sessionToken, null);

    let payload = gadgetUtil.getPayload(handlerInput);
    let cityName = payload.city_name;

    // ◎時間◎分のセッションを開始しますと読み上げます
    return handlerInput.responseBuilder
    .speak('サンタクロースが日本にいるようです！' + cityName + '付近に今サンタクロースがいるみたいです！')
    .addDirective(persistenceDirective)
    .getResponse();
}
```
スキル側のコードは一部抜粋です。
``function gadgetSantaResponse (handlerInput)``は、ガジェットからサンタクロースが検知されたときに発話する内容を生成しています。
ガジェット側でサンタクロースが日本にいることが分かったタイミングで実行され、Alexaが発話します。

# サンタクロースを追跡する
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Better get to bed if you&#39;re in Japan because <a href="https://twitter.com/hashtag/Santa?src=hash&amp;ref_src=twsrc%5Etfw">#Santa</a> is delivering presents in Tokyo right now. Wanna know where he&#39;s headed next? Call <a href="https://twitter.com/hashtag/NORAD?src=hash&amp;ref_src=twsrc%5Etfw">#NORAD</a> at 1-877-446-6723. <a href="https://twitter.com/hashtag/NORADTracksSanta?src=hash&amp;ref_src=twsrc%5Etfw">#NORADTracksSanta</a> <a href="https://t.co/HzbCGzomqG">pic.twitter.com/HzbCGzomqG</a></p>&mdash; NORAD Tracks Santa (@NoradSanta) <a href="https://twitter.com/NoradSanta/status/1077204820526809089?ref_src=twsrc%5Etfw">December 24, 2018</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
この記事を書いているときは、まだクリスマス・イブではないため2019年のサンタクロースを追跡することは出来ていません。
なので、今回は2018年の追跡時のTweetをデモアカウントでつぶやかせて取得しAlexaで発話できるか試してみましょう。
[![Youtube](http://img.youtube.com/vi/rfb08mlWVv4/0.jpg)](https://youtu.be/rfb08mlWVv4)

無事Tweetを元にサンタクロースの来日情報をAlexaで発話させることができました！

# おわりに
業務とは無縁の内容の記事となりましたが、これで今年も靴下を引っさげてスヤスヤ寝ててもサンタクロースを捕捉できそうです！
まだまだVUIはビジネス的な視点で見ると取り組むことが難しいのが現状ですが、新しいインターフェイスをいかに開拓していくか考えると非常に楽しくエンジニアリングできるので、ぜひこれを機にVUIの世界に飛び込んでみてください！
（社内でもAlexaおじさんとして草の根運動は進めていきます)

# 今回のコード
今整理中なので年末までにリポジトリ公開します。