Alexaスキルで湯婆婆を実装してみる

# はじめに
何番煎じかわかりませんが、[流行り](https://qiita.com/Nemesis/items/c7192a7c510788d2cba2)に乗らせていただき、Alexaスキルで湯婆婆を実装してみました。

この湯婆婆シナリオは
・名前を聞く(ユーザの入力を受けつける)
　　↓
・名前を与える(情報を出力する)
という流れなので、ユーザとの対話で処理を進めていくAlexaスキルにぴったりなようにも思います。

# 作ったもの
以下で公開しています。
[名前泥棒](https://www.amazon.co.jp/dp/B08N495CQL)

Alexaスキルは自身が著作権を持たないものを題材にはできないため、湯婆婆を名乗ることは当然できません。
そのため、一般的な単語のみで構成されたスキル名にしています。

シミュレータでのスキルの流れは以下です。
![シミュレーション.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114715/fb762425-fb10-49da-f52c-b7b2125a4566.png)


# 実装
ソース全体は[こちら](https://github.com/uemuram/alexa-yubaba)。
javascriptで実装しています。

## スキル起動～名前を要求
スキルを起動し、名前の発話を要求するまでです。

``` index.js
const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speakOutput = `
            <speak>
                ようこそ。このスキルでは、契約で名前を奪われる従業員の気持ちを味わえます。
                経営者のセリフの後に、あなたの名前を教えてください。では、始めます。
                <break time="1200ms"/>
                <prosody pitch="low" rate="90%">契約書だよ。そこに名前を書きな。</prosody>
            </speak>
            `;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .withSimpleCard('経営者より', '契約書だよ。そこに名前を書きな。')
            .reprompt('<prosody pitch="low" rate="90%">契約書だよ。そこに名前を書きな。</prosody>')
            .getResponse();
    }
};
```
名前の発話を促す音声を定義し、返却しているだけです。

本来であれば、いきなり「契約書だよ。そこに名前を書きな。」とだけ返したいところですが、Alexaスキルにはユーザを迷わせるような応答を返してはいけない、という制約があります。

ユーザからの応答を受け付け始めて数秒(+reprompt後数秒)経過するとセッションが切れてスキル終了する、というのが標準仕様であり、何と返事をしてよいかわからない状態にしてしまうと、すぐにセッション切れになってしまうためです。
そのため、最初にスキルの趣旨を説明する一文を入れています。

※この辺はスキルの申請時に厳しめに審査されます。作る側としては、長々とスキルの説明をすると使い勝手が悪くなる、説明を省略しすぎると審査を通らない、というバランスが難しいように感じます。

説明の後は、prosodyタグを利用して声の高さ、速さを変え、湯婆婆のセリフっぽくしようとしています。

## 名前を受け付け～名前を奪う
ユーザからの名前の発話を受け付け、一文字だけ残して返す処理です。

まず、ユーザのアクション(インテント)を定義します。

``` ja-JP.json
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "名前泥棒",
            "intents": [
                //中略
                {
                    "name": "WriteNameIntent",
                    "slots": [
                        {
                            "name": "Name",
                            "type": "AMAZON.Person"
                        }
                    ],
                    "samples": [
                        " {Name} ",
                        " {Name} です",
                        "名前は {Name} ",
                        "名前は {Name} です"
                    ]
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                }
            ],
            "types": []
        }
    }
}
```
ユーザからは「山田太郎」のように名前だけ発話してもらえればいいのですが、人によっては「名前は山田太郎です」のように文を発話するかもしれません。
その場合でも名前部分だけを取り出せるように、samplesにユーザが言いそうなフレーズを一通り並べておきます。
「名前は山田太郎です」の場合は、"名前は {Name} です"に引っ掛かり、{Name}の部分に「山田太郎」が入る(後で取り出せる)ということです。

また、名前の部分には[AMAZON.Person](https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/slot-type-reference.html)スロットタイプを利用しています。
これは「実在の人物と架空の人物のフルネーム」を検知するようで、有名人の名前は引っ掛かりやすいですが、一般ユーザのフルネームの検知は不正確です。
そもそも、Alexaスキルで事前定義されていないフリーの単語や文章を取り出すのはなかなか難しいところがあり、各スキルで色々と工夫しながら実装しているようです。

続いて、一文字切り出す部分です。

``` index.js
const WriteNameIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'WriteNameIntent';
    },
    async handle(handlerInput) {
        // スロット値を取得
        const name = Alexa.getSlotValue(handlerInput.requestEnvelope, 'Name');

        //ランダムに一文字抽出する
        const idx = Math.floor(Math.random() * name.length);
        const newName = name.substring(idx, idx + 1);

        // 抽出した一文字の読みを統一するためにひらがなに変換する
        let newNameYomi;
        try {
            // 中略
            // ひらがな変換
            const res = await Axios.post(API_URL, {
                app_id: apiKey,
                output_type: 'hiragana',
                sentence: newName
            });
            newNameYomi = res.data.converted;

        } catch (error) {
            throw new Error(`http get error: ${error}`);
        }

        // 返事を組み立て
        const speakOutput = `
            <speak>
                <prosody pitch="low" rate="90%">
                    フン。${name}というのかい。贅沢な名だねぇ。
                    今からお前の名前は、${newNameYomi}だ。いいかい、${newNameYomi}だよ。分かったら返事をするんだ、${newNameYomi}!!
                </prosody>
                <break time="1200ms"/>
                あなたの名前は、${newNameYomi}になりました。もう一度試しますか?
            </speak>
        `;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .withSimpleCard('経営者より',
                `フン。${name}というのかい。贅沢な名だねぇ。`
                + `今からお前の名前は${newName}だ。いいかい、${newName}だよ。分かったら返事をするんだ、${newName}!!`)
            .reprompt('もう一度試しますか?')
            .getResponse();
    }
};
```
先ほど定義したNameから名前を取得し、その中からランダムに一文字取り出しています。

最初はまった点としては、漢字をそのまま読ませると、何と読むかはAlexa任せになります。文字によっては(恐らく前後の文字との兼ね合いで)同じ文字でも違う読み方になることがあります。
例えば切り出した一文字が「山」だとして、「今からお前の名前は山だ。いいかい、山だよ。分かったら返事をするんだ、山!!」としてしまうと、最初の山は「やま」次の山は「さん」と読んでしまう、ということが起こりえます。
それを避けるため、一度gooの[ひらがな化API](https://labs.goo.ne.jp/api/jp/hiragana-translation/)に一文字送り、ひらがなに変換してそれを読ませるようにしました。

# 公開に向けて
Alexaスキルを公開するには、公開用の説明文やアイコンなどを整備してAmazonの審査を通過する必要があります。

## アイコン作成
スキル用のアイコンとして、512x512、108x108の画像を用意します。画像は円形に切り取られるため、四隅が欠けても大丈夫なようにしておく必要があります。
フリー素材として公開されているジブリのシーンを使い、ペイントで地道に湯婆婆のシルエットっぽく加工しました。
![before_after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114715/6f2d1973-2ba6-a995-b7df-e9a4eda9a58e.png)
正直ここが一番大変でした。

## 審査
はたから見ると意味不明なスキルなので審査を通らない可能性もありそうだと思っていたのですが、意外にも指摘は1件だけでした。指摘の内容は、個人の情報である「名前」を受け付けているためプライバシーポリシーを定義せよ、というものでした。

名前はスキル内の応答にしか利用しない、保管もしない旨を記載した[プライバシーポリシー](https://uemuram.github.io/alexa-yubaba/privacy-policy.html)を作成し、申請時にURLを登録することにより審査通過しました。


# おわりに
この流行りは最初はネタだったと思いますが、各言語の基本を押さえることができ、作ってみると意外と発見もあるいいシナリオだと思いました。