タイプウェル国語R[基本常用語]で50秒切れた!

タイピングが遅いのが少しコンプレックスなので、タイピング速いと思われたくてちょくちょく練習してます。

ひたすらタイプウェルをやって、苦手なワードは TypeLighter で繰り返し練習です。

とは言うものの、ガチタイパーの方々と比べれば赤ちゃんみたいなもんなので、特に練習の戦略はありません。

ひたすら打ちまくるだけです。

ずっと成績が伸びなかったんですが、タイピングの姿勢を変えたところ 50 秒を切ることができました。

苦手語句を表示してくれるのはうれしいが、簡単な単語ばかりが並んでいて謎です。

Spaceキーのミスもカウントされるんですかね?

試しに寿司打をやってみたところ、今一歩 2 倍のスコアには届きませんでした。

タイプウェルとは異なり、ワードの先読みができないので秒あたりのタイプ数は少なめですね。

ワード見てから打ち始めるまでに速くて 0.6 秒もかかるくらい動体視力がよくないので(ネット見てると大体みんな意識しなくても 0.5 秒くらいはいけてるみたい)。

ワード慣れすれば 2 倍くらいはいけるかもしれません。

ガチタイパーになりたいわけではないので、50 秒をコンスタントに取れるようになれば一旦タイピング練習はやめようと思います。

いつになるやら。。。

Alexa に次のバスが何分後に来るかしゃべらせるスキルを作った

いつもナビタイムの「次のバス」スキルにお世話になっていました。

これは「最寄りのバス停を登録しておくと "次のバスのスキルを開いて" というだけで次のバスが何分後に来るか、さらにその次のバスは何時何分か」を教えてくれる便利なスキルです。

ところが、このスキルが今月末にサービス終了するようです。

これは非常に困るので自分で Alexa スキルを作ります。

Alexa を開発できるようにするための事前準備は前回の記事をご参照ください。

talkeyboid.com

コード書くぞ!

まずは結論から。以下のコードで実現できました。※時刻表の値はダミーです。

# -*- 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.dispatch_components import AbstractExceptionHandler
from ask_sdk_core.handler_input import HandlerInput

from ask_sdk_model import Response

# 日付時刻系インポート
from datetime import date, datetime, timezone, timedelta
import jpholiday
import math

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

# 時刻表マスタ ※祝日はsunday扱い
def get_today_table(weekday_en, year, month, day):
    master_timetable = {
        "weekday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 23, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 48, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ],
        "saturday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 45, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ],
        "sunday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 45, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ]
    }
    return master_timetable[weekday_en];

class LaunchRequestHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return ask_utils.is_request_type("LaunchRequest")(handler_input)

    def handle(self, handler_input):
        # 日時取得
        timezone_tokyo = timezone(timedelta(hours=9))
        datetime_now_jst = datetime.now(timezone_tokyo)
        
        year = datetime_now_jst.year
        month = datetime_now_jst.month
        day = datetime_now_jst.day
        
        # 曜日判定:月曜0-日曜6
        weekday = date(year, month, day).weekday()
        
        weekday_en = ""
        if weekday == 5:
            weekday_en = "saturday"
        elif weekday == 6:
            weekday_en = "sunday"
        else:
            weekday_en = "weekday"
        
        # 祝日判定
        if jpholiday.is_holiday(date(year, month, day)):
            weekday_en = "sunday"
        
        # 時刻表取得
        timetable = get_today_table(weekday_en, year, month, day)
        
        # 次とその次のバスを取得
        next_bus = []
        for item in timetable:
            if datetime_now_jst < item:
                next_bus.append(item)
        
        next_bus = next_bus[:2]
        
        # リストのサイズで終バス判断し返却値を出し分ける
        if len(next_bus) == 0:
            speak_output = "もう今日のバスはありません。"
        elif len(next_bus) == 1:
            diff = next_bus[0] - datetime_now_jst
            minute_after = math.floor(diff.total_seconds()/60)
            next_bus_hour = next_bus[0].hour
            next_bus_minute = next_bus[0].minute
            speak_output = f"次のバスは{minute_after}分後の{next_bus_hour}時{next_bus_minute}分です。このバスが最終です。"
        else:
            diff_1 = next_bus[0] - datetime_now_jst
            minute_after_1 = math.floor(diff_1.total_seconds()/60)
            diff_2 = next_bus[1] - datetime_now_jst
            minute_after_2 = math.floor(diff_2.total_seconds()/60)
            next_bus_hour = next_bus[1].hour
            next_bus_minute = next_bus[1].minute
            speak_output = f"次のバスは{minute_after_1}分後です。その次は{minute_after_2}分後の{next_bus_hour}時{next_bus_minute}分です。"
        
        return (
            handler_input.response_builder
                .speak(speak_output)
                # .ask(speak_output) # これがあるとセッションが維持される
                .set_should_end_session(True) # 1回しゃべったらセッション切る
                .response
        )


sb = SkillBuilder()
sb.add_request_handler(LaunchRequestHandler())

lambda_handler = sb.lambda_handler()

めちゃシンプルなので特に解説することもないかと思いますが、どうせ忘れるので自分自身のメモとして残しておきます。

import

今回は祝日判定のために jpholiday を利用するので requirements.txt に追記します。

boto3==1.9.216
ask-sdk-core==1.11.0
jpholiday # 追記

github.com

時刻計算のために datetimetimedelta を分に換算するために math をインポートします。

from datetime import date, datetime, timezone, timedelta
import jpholiday
import math

時刻表マスタ

時刻表はハードコーディングします。自分だけで使うしバス停も1つなのでこっちの方が圧倒的保守性に優れてるためです。

DynamoDB 使うならパーティションキーに バス会社名-路線名-バス停名 、ソートキーに 時刻 がよいでしょうか。

それなら範囲検索できますしおすし(知らんけど)。

def get_today_table(weekday_en, year, month, day):
    master_timetable = {
        "weekday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 23, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 48, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ],
        "saturday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 45, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ],
        "sunday": [
            datetime(year, month, day, 6, 5, 00, tzinfo=timezone(timedelta(hours=9))),
            datetime(year, month, day, 6, 45, 00, tzinfo=timezone(timedelta(hours=9))),
            ... # 以下略
            ]
    }
    return master_timetable[weekday_en];

「あと何分後」を計算するために時刻差を取得する必要がありますが、バス時刻表って日付ごとではなく曜日ごとです。

そのため year, month, day を指定することで "その日" の時刻表を取得できるようにしています。

weekday_en によって曜日ごとの時刻表を出し分けています。

処理部分

LaunchRequestHandler の中身は普通にごり押しで実装してるだけです。

前回、エラーをキャッチしてしまう問題は .ask(speak_output) していたためでした。これをするとセッションが維持されるっぽいので外しています。

また、Alexa を放っておいてもスキルが勝手に終了せず2回しゃべってしまう問題はAlexa が一回しゃべったらすぐにセッションを切る設定(.set_should_end_session(True))をすることで解決しました。

あと、時刻表リストを全部ぶん回して最後に silce しててあんまりかっこよくはないんですが、時刻表なんか高々数十くらいのオーダーなんでこれでいいです。

動かすぞ!

動きました。※バス時刻はダミーです。

www.youtube.com

とりあえず動いてよかったです。

ベータ版テストでしか動かせてないので、次はちゃんとプライベートで公開できるようにしたいですね(Alexa for Business めんどくさそうなのでベータテストを3カ月に1回更新するでもいいかも???)。

とりあえず Alexa スキル動かすだけ

いつもナビタイムの「次のバス」スキルにお世話になっていました。

これは「最寄りのバス停を登録しておくと "次のバスのスキルを開いて" というだけで次のバスが何分後に来るか、さらにその次のバスは何時何分か」を教えてくれる便利なスキルです。

ところが、このスキルが今月末にサービス終了するようです。

これは非常に困るので自分で Alexa スキルを作ります。

Alexa スキルは作ったことがなく、「Lambda とか DynamoDB くらいは使うんだろうなー」くらいに考えていましたがどうやらスキルを作るのに AWS は不要らしいです。

もちろん凝ったものを作ろうと思えば別でしょうが、今回は時刻表引いて返すだけのドシンプルなものです。

できれば簡単に作りたいですね。

さっそく作るぜ!

Alexa Developer Console とゆーのに EC サイト(NOT AWS)の Amazon アカウントでログインします。

スキルの作成 をクリック。

適当にスキルの名前を付けます。「次のバス」って名前を付けちゃうと既存ナビタイムアプリとウェイクワードが衝突します。

まあいいでしょう。どのみち後で変えることになります。

エクスペリエンスのタイプを「その他」に設定。

この項目自体にあまり意味はなくて、次の「モデルを選択する」で選べる選択肢のフィルタリングに使われてるっぽいです。

モデルは「カスタム」に設定。

カスタムにしとけば後で融通利くかなという感じ。変にプレビルド設定されてもよくわからんし。

「ヘルプドキュメント」のリンクが貼られてるが読みませ~ん。家電使うときに困ってから説明書読むタイプです。

ホスティングサービスは「Alexa-hosted(Python)」にしました。

下画像の内容を見る限りだと Alexa-hosted が Alexa 側で Lambda と DynamoDB の無料枠を使わせてくれるみたい。

Alexa-hosted でも無料枠を超えたら AWS アカウントに紐づける必要があるようです。

なぜ Node.js でなく Python なのか。わざわざ Python の選択肢あるのにサーバ側 JavaScript で書くの辛くない?ってだけです。

ホストリージョンは「オレゴンに」設定。そもそも東京リージョンという選択肢はない(独自のプロビジョニング を選択してたら使えるのかな?)。

うーん、バス時刻聞くだけなのに海越えていくなんてロマン溢れる~☆彡

ちな、バージニア北部よりオレゴンの方がちょっと速いみたいです。まあ普通に海底ケーブル太平洋通ってるから西海岸の方が速い。

各AWSリージョンとのネットワーク遅延を計測したい | DevelopersIO

テンプレートは「スクラッチで作成」を選択。これは悩んだが、まぁググればそれっぽいコードがゴロゴロ落ちてるだろという期待。

最終的な設定はこちら。

とりま動かすぜ!

さっそく意味わからんポータルが出てきた!右側の「スキルのビルド」の部分はビルド設定っぽいが全部チェックマークついてるので無視。

上タブで「テスト」に移動し、非公開 -> 開発中 に変更。

マイクアイコンを長押ししながら発話すると返答が返ってきます。すごい!

上タブで「公開」に移動し、適当に入力。

そうか、オレゴンリージョンにデプロイするからソフトウェア輸出規制に引っかかる恐れがあるのか。個人情報も企業機密や軍事機密もないのでOKでしょう。

ベータテストができないとのことで困る。が、とりあえず進めるところまで進んでみる。

エラーが出てきた!やったー!!これさえ解消すればベータテストできるはず。

サンプルフレーズを入力。これに伴い、スキル名を 次のバス から バスの時間 に変更。

スキルアイコンを適当に作る。アイコンビルダーないかなーと思ったらやはりあった。神。

dev.classmethod.jp

適当にバスのアイコンを作って設定。

さっきのベータテストの設定が記入できるようになったので適当に記載し有効化。

※メアド晒したくないので画像上はブランクです。

ベータテストは有効期限付きかー。まあそりゃそうか。

永続的にデプロイしたいがそのためにはやはり「Alexa for Business」が必要なんだろうか。

再度検証するとエラーは全て解消してました。

招待メールが届いているのでリンクをクリックし、スキルをデバイス側に登録する。

さっそく実行!※注意:再生するとあなたの家の Alexa が反応します

www.youtube.com

あれ。。。全然反応しない。。。

コンソールを見返す。ウェイクワードっぽい設定箇所は以下。

  • ビルド > 呼び出し > スキルの呼び出し名
  • 公開 > 公開名
  • 公開 > サンプルフレーズ

で、さっき 次のバス から バスの時間 に設定しなおしたのは「公開設定」のみ。

公開設定は文字通り「公開するための設定」であって、Alexa スキルの設定ではないですね。ただのスキルの説明文です。

なので、「スキルの呼び出し名」を変更する必要がありました。

再度スキルをビルドし、テスト。ちゃんと答え返ってきてるっぽい!

ちょっとだけコード書くぞ!

コード書くといっても返答を日本語にするだけです。

上タブから「コードエディタ」へ移動。すでにサンプルコードが書かれてる。

以下の GitHub サンプルと同じコードだと思われ。

github.com

さっきのテストコードでの返答が "Welcome, you can say Hello or Help. Which would you like to try?" だったので、LaunchRequestHandler の中身の文字列だけ書き換えればよさそう。

書き換え前

書き換え後

ワードで分岐するような性質のスキルを作るわけじゃないので、handler_input は触らなくていいですね。

「保存」「デプロイ」し、テストタブに移動してテストする。

ちゃんと動いた!

既に Lambda へ反映されてるのでデバイス側のスキルは更新しなくてもいいかな?

とりま Alexa にしゃべりかける。

www.youtube.com

動いてはいるが、返答を2回繰り返したり、なんか英語話し出したりしている。

たぶん FallbackIntentHandler に反応してる。

ちょっとコードの意味を調べる。

developer.amazon.com

an_handle:can_handleメソッドは、SDKによって呼び出され、指定されたハンドラーが受け取ったリクエストを処理できるかどうかを判断します。この関数はハンドラー入力オブジェクトを受け付け、ブール型を返すように想定されています。メソッドがTrueを返せば、ハンドラーによってリクエストが正常に処理されたと考えられます。Falseを返す場合、ハンドラーが入力リクエストを処理できず、したがって実行されず完了もしなかったと考えられます。HandlerInputオブジェクトにはさまざまなアトリビュートがあるため、リクエストを正常に処理できるかどうかをSDKが判別するための任意の条件を作成できます。 handle:handleメソッドは、リクエストハンドラーを呼び出すときにSDKによって呼び出されます。この関数には、ハンドラーのリクエスト処理ロジックが含まれており、ハンドラー入力を受け取り、応答オブジェクトを返します。

blog.serverworks.co.jp

Alexaでの意味はユーザーの意図や目的を実現するためのアクション、といったところでしょうか。ユーザーが豆知識を教えてほしいという目的のときは「豆知識」というと「GetNewFactIntent」が実行され、ユーザーがヘルプを聞きたいときは「ヘルプ」と言うと「HelpIntent」が実行されるという感じですね。ユーザーの目的ごとにインテントが必要になる、というイメージです。  今回追加する「FallbackIntent」はユーザーが意図や目的をもってAlexaに話しかけても、Alexa側にそれに対応するためのインテントがない場合に実行されるインテントです。ちょっと特別なインテントですね。 とゆーことで、LaunchRequestHandler 以外のハンドラーを使わないようにする。

ふむふむ... そーゆーことか!わかったぞぉ!

今回作るスキルはインテントとか全く関係ないのでインテントハンドラーは必要ない。

SkillBuilder.add_request_handler をコメントアウトすれば無効化できる気がする。

ただし、最後のガードとしてまるっと例外キャッチする sb.add_exception_handler(CatchAllExceptionHandler()) は残しておく。

保存・デプロイして手元の Alexa で試した結果。

2回発話してしまうこと以外は想定通り(ちゃんと最後に例外キャッチした結果が返ってくる)。

www.youtube.com

今日は夜遅いのでここまで!

次はバス時刻表をしゃべらせるようにコーディングしていきます。

残課題

  • 1回しゃべったら自動的にスキルを終了する
  • 現在時刻から後ろで一番近いバス時刻と2番目に近いバス時刻をしゃべらせる ※最終便の場合、もうそれ以降バスがない場合の処理を忘れずに
  • ベータテストじゃなくちゃんとプライベートでホストする方法を探す(Skill for Business しかないのか。。。???)