kun432's blog

Alexaなどスマートスピーカーの話題中心に、Voiceflowの日本語情報を発信してます。たまにAWSやkubernetesなど。

〜スマートスピーカーやVoiceflowの記事は右メニューのカテゴリからどうぞ。〜

カスタムスキルでのマルチリンガルがどうなっているのかを調べてみた

f:id:kun432:20201218121344p:plain

スマートスピーカー Advent Calendar 2020 - Qiitaの18日目の記事です。

先日リリースされた、Alexaのマルチリンガルモードについては、スマートスピーカー Advent Calendar 2020 - Qiitaの初日の記事でご紹介していました。

あと、カスタムスキル自体はロケールが一つしか選択できません。「日本語スキルで英語の発話を受け取る」「英語スキルで日本語の発話を受け取る」といった多言語に対応したスキル開発というのは現時点ではできません、あしからず。

開発者としては、カスタムスキルで英語を受け取れるのか?というところが気になりますよね。ということで、かんたんに「スロットを英語で取れるか」のテストしてみたところ、どうしても日本語で認識しようとするように見えたので、「できない」と考えてました。

が、@YASCODEZさんのリリースされたこのスキルの説明を見ると、

f:id:kun432:20201218122852p:plain

どうやらできそう!@YASCODEZさんにも直接聞いてみたところ、

ということなので、少し突っ込んで調べてみました。

目次

前提条件

  • テストに使うAlexaデバイスの言語設定を「日本語/English」
  • カスタムスキルを「日本語」で作成。Alexa-hostedの「ハローワールド」をベースにする。

インテント

まずインテントが日本語・英語を見分けれるのかを調べてみます。

HelloWorldIntentを日本語用のHelloWorldJaIntentと英語用のHelloWorldJaIntentに分けます。サンプル発話はそれぞれ英語・日本語のみで構成しておきます。

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "マルチリンガルハローワールド",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "HelloWorldJaIntent",
                    "slots": [],
                    "samples": [
                        "こんにちは",
                        "こんちは",
                        "おげんきですか"
                    ]
                },
                {
                    "name": "HelloWorldEnIntent",
                    "slots": [],
                    "samples": [
                        "hello",
                        "how are you",
                        "what's up"
                    ]
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                }
            ],
            "types": []
        }
    }
}

バックエンドのコードです。対話モデルにあわせて、インテントハンドラもそれぞれ別にしてあります。英語で回答する場合はSSMLのVoice/Langタグをつけてます。

const Alexa = require('ask-sdk-core');

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speakOutput = 'マルチリンガルのテストです。こんにちは、と言ってみてください。<voice name="Joanna"><lang xml:lang="en-US">This is a test for multilingual. Say hello to me.</lang></voice>';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const HelloWorldJaIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldJaIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'こんにちは。';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};

const HelloWorldEnIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldEnIntent';
    },
    handle(handlerInput) {
        const speakOutput = '<voice name="Joanna"><lang xml:lang="en-US">Hello.</lang></voice>';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};

(snip)

// Reqest Interceptorを追加
const LogResponseInterceptor = {
  process(handlerInput) {
    console.log(`RESPONSE = ${JSON.stringify(handlerInput.responseBuilder.getResponse())}`);
  },
};

// Response Interceptorを追加
const LogRequestInterceptor = {
  process(handlerInput) {
    console.log(`REQUEST ENVELOPE = ${JSON.stringify(handlerInput.requestEnvelope)}`);
  },
};

exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        HelloWorldJaIntentHandler,
        HelloWorldEnIntentHandler,
    )
(snip)
    .addRequestInterceptors(LogRequestInterceptor)
    .addResponseInterceptors(LogResponseInterceptor)
    .withCustomUserAgent('sample/hello-world/v1.2')
    .lambda();

テストは実機で行う必要があります。動画で。

ちゃんとインテントで区別できていますね。リクエストの中身でもちゃんと別のインテントに流れてきていることがわかります。

日本語の場合。

f:id:kun432:20201218134522p:plain

英語の場合。

f:id:kun432:20201218134538p:plain

Alexaアプリのアクティビティから音声履歴を見ても、きちんと日本語・英語で認識されているのがわかります。

f:id:kun432:20201218134612p:plain

サンプル発話を英語にしておくことで英語での発話をインテントに結びつけることができるということですね。

スロット

次にスロットです。サンプルとして「星座占い」的なもののスロットを受け取ってみます。

対話モデルはこんな感じ。最初のテストではインテントを分けましたが、今回は日本語・英語でインテントもスロットも共通のものにしました。カスタムスロットを使ってます。

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "ハローワールドのテスト",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "HelloWorldIntent",
                    "slots": [
                        {
                            "name": "zodiactype",
                            "type": "zodiactype"
                        }
                    ],
                    "samples": [
                        "I'm {zodiactype}",
                        "{zodiactype} です",
                        "my star sign is {zodiactype} ",
                        "星座は {zodiactype} です"
                    ]
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                }
            ],
            "types": [
                {
                    "name": "zodiactype",
                    "values": [
                        {
                            "name": {
                                "value": "魚座",
                                "synonyms": [
                                    "うお座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "水瓶座",
                                "synonyms": [
                                    "みずがめ座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "山羊座",
                                "synonyms": [
                                    "やぎ座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "射手座",
                                "synonyms": [
                                    "いて座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "獅子座",
                                "synonyms": [
                                    "しし座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "双子座",
                                "synonyms": [
                                    "ふたご座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "天秤座",
                                "synonyms": [
                                    "てんびん座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "蠍座",
                                "synonyms": [
                                    "さそり座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "乙女座",
                                "synonyms": [
                                    "おとめ座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "牡羊座",
                                "synonyms": [
                                    "おひつじ座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "蟹座",
                                "synonyms": [
                                    "かに座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "牡牛座",
                                "synonyms": [
                                    "おうし座"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "aquarius"
                            }
                        },
                        {
                            "name": {
                                "value": "pisces"
                            }
                        },
                        {
                            "name": {
                                "value": "capricornus"
                            }
                        },
                        {
                            "name": {
                                "value": "sagittarius"
                            }
                        },
                        {
                            "name": {
                                "value": "scorpius"
                            }
                        },
                        {
                            "name": {
                                "value": "libra"
                            }
                        },
                        {
                            "name": {
                                "value": "virgo"
                            }
                        },
                        {
                            "name": {
                                "value": "leo"
                            }
                        },
                        {
                            "name": {
                                "value": "cancer"
                            }
                        },
                        {
                            "name": {
                                "value": "aries"
                            }
                        },
                        {
                            "name": {
                                "value": "taurus"
                            }
                        },
                        {
                            "name": {
                                "value": "gemini"
                            }
                        }
                    ]
                }
            ]
        }
    }
}

バックエンドです。HelloWorldIntentHandlerの中でzodiactypeスロットを受け取って、スロットがアルファベットだけ=英語と判定して、応答を変えています。

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speakOutput = 'あなたの星座を教えてください。<voice name="Joanna"><lang xml:lang="en-US">What is your star sign?</lang></voice>';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const HelloWorldIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent';
    },
    handle(handlerInput) {
        const zodiactype = Alexa.getSlotValue(handlerInput.requestEnvelope, "zodiactype");
        const zodiactypeParent = Alexa.getSlot(handlerInput.requestEnvelope, "zodiactype")
        let speechText_jp = zodiactype + 'なんですね。今日の運勢は最高です!';
        let speechText_en = 'You are ' + zodiactype + ". Today is your lucky day!";

        let speakOutput;
        if (zodiactype.match(/^[a-zA-Z]+$/)) {
            speakOutput = '<voice name="Joanna"><lang xml:lang="en-US">' + speechText_en + '</lang></voice>';
        }else{
            speakOutput = speechText_jp;
        }
        
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};

実機でテスト。

ちゃんと想定通りに動いてますね。

Alexaアプリの音声履歴でもきちんと認識できています。

f:id:kun432:20201218145821p:plain

リスエストでもちゃんとスロットを英語と日本語で受け取れています!

f:id:kun432:20201218151047p:plain

f:id:kun432:20201218151057p:plain

SearchQueryでスロットを受け取る

開発者として一番やりたいのは多分これじゃないかなと思います。英語を自由に受け取ってゴニョゴニョできると夢が広がります。やってみましょう。

対話モデルはこんな感じです。searchQueryでキャリアフレーズなしにする場合、" {searchQuery}"(最初に半角スペース)がよく使われていましたが、最近はダメなのですね。以下を参考に、ダイアログモデルでオートデリゲートを使うようにしました。

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "マルチリンガルオウム返し",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "HelloWorldIntent",
                    "slots": [
                        {
                            "name": "UserSpeech",
                            "type": "AMAZON.SearchQuery",
                            "samples": [
                                "{UserSpeech}"
                            ]
                        }
                    ],
                    "samples": [
                        "begin",
                        "start",
                        "開始",
                        "スタート"
                    ]
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                }
            ],
            "types": []
        },
        "dialog": {
            "intents": [
                {
                    "name": "HelloWorldIntent",
                    "delegationStrategy": "ALWAYS",
                    "confirmationRequired": false,
                    "prompts": {},
                    "slots": [
                        {
                            "name": "UserSpeech",
                            "type": "AMAZON.SearchQuery",
                            "confirmationRequired": false,
                            "elicitationRequired": true,
                            "prompts": {
                                "elicitation": "Elicit.Slot.1313722150646.359517326789"
                            }
                        }
                    ]
                }
            ],
            "delegationStrategy": "ALWAYS"
        },
        "prompts": [
            {
                "id": "Elicit.Slot.1313722150646.359517326789",
                "variations": [
                    {
                        "type": "PlainText",
                        "value": "自由に話してみてください"
                    }
                ]
            }
        ]
    }
}

バックエンド側です。スロットを受け取ってそのまま返すだけですが、スロットの値がアルファベットや数字、記号のみの場合は英語と判断して英語で返すようにしています。

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speakOutput = 'マルチリンガルでオウム返しをするよ。スタートと言ってね。<voice name="Joanna"><lang xml:lang="en-US">This is multilingual parrot. Say start.</lang></voice>';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

const HelloWorldIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent';
    },
    handle(handlerInput) {
        const userSpeech = Alexa.getSlotValue(handlerInput.requestEnvelope, "UserSpeech");
        const userSpeechParent = Alexa.getSlot(handlerInput.requestEnvelope, "UserSpeech")

        let speakOutput;
        if (userSpeech.match(/^[0-9a-zA-Z .,]+$/)) {
            speakOutput = '<voice name="Joanna"><lang xml:lang="en-US">' + userSpeech + '</lang></voice>';
        }else{
            speakOutput = userSpeech;
        }
        
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .withShouldEndSession(true)
            .getResponse();
    }
};

実機でテストしてみた結果です。

英語で発話したものを認識できていません・・・

Alexaアプリでの音声履歴とリクエストはこうなっています。

f:id:kun432:20201218160955p:plain

f:id:kun432:20201218160858p:plain

他のフレーズでも試してみたのですが、スロット値は日本語として聞き取った内容で渡されるようです。例えば、"What's up, yo men"は「渡部オメン」とかw

AMAZON.Dateなど他のスロットタイプでも試してみたのですが、スロット値がない状態で渡されたりします。全部試してはいないのですが、ビルトインスロットタイプはマルチリンガルにはならなさそうです。

まとめ

カスタムスキルでのマルチリンガルの動きをまとめるとこんな感じになりました。

  • インテントのサンプル発話を日本語・英語で用意しておけば、マルチリンガルでインテントをルーティングしてくれる。
  • カスタムスロットのスロット値を日本語・英語で用意しておけば、マルチリンガルでスロット値を渡してくれる。
  • ビルトインスロットはマルチリンガルでは動作しない可能性が高い。

ビルトインスロットを全部確認したわけではないですが、リストタイプのものなどはもともと日本語のスロット値が登録されてそうですし、数値、日付、時刻、searchQueryあたりも日本語特有の言い回しに最適化されてそうな気がします。ビルトインスロットだけマルチリンガルとして認識しないのはこのあたりかなと勝手に推測しています。ぜひお試しいただいて、間違い等あれば指摘していただけると幸いです。

最後に、記事のきっかけになった@YASCODEZさんに感謝!