ラノベみたいなタイトルですいません。
スマートスピーカー Advent Calendar 2020 に空きがあったので、クリスマスもう終わってますが後から追加です。11日目の記事です。
ちょっと前にUSのAlexa公式ブログのほうで、"PIN Confirmation"という新しい機能がなんの前触れもなく紹介されてました。
PIN Confirmationは、スキル内課金とか音声ショッピングで使う「4桁の暗証番号」による確認のことです。「パーソナライズ」を有効するとこの機能が使えるようになり、ユーザへの本人確認・同意の方法として提供されるみたいです。
ただし、現時点では「開発者プレビュー」ということだったので申請しないと使えないはず・・・なのですが、Alexa開発者コンソールのアクセス権のところを見ると設定ができるように見えます。
TwitterでもどうやらGAらしい、というような声があったので、設定を有効にしてみたのですがAlexaアプリのスキル設定には出てきません・・・どうやらやっぱりまだ開発者プレビューのようです。
ちょっとがっかりしたのですが、PIN Confirmationを試すのに良さげそうな「Person Profile API」の公式サンプルを触ってみたので、その話をします(転んでもただでは起きない関西人)
目次
事前準備
Person Profile APIの公式サンプルはこちらです。
これを日本語化して、Alexa-hostedでインポートできるようにしたものがこちらです。
オリジナルの方はそのままではちょっと不具合があったので、結構修正しています。こちらをAlexa−hostedスキルでインポートしてください。
アクセス権限
Person Profile APIでは以下の権限が必要です。
- スキルのパーソナライズ
- ユーザ名。姓名 or 名前のどちらか。
- ユーザの電話番号
「ビルド」タブの左のメニューから「ツール」→「アクセス権限」と進みます。
最初に述べた3つの権限を有効にしてください。ちなみに、ユーザ名とユーザの電話番号はCustomer Profile APIと共通になっています。
対話モデル
対話モデルはシンプルです。メインとなるインテントは以下の3つです。
- ProfileGivenNameIntent : 名前だけを聞くインテント
- ProfileFullNameIntent: フルネームを聞くインテント
- ProfileNumberIntent: 電話番号を聞くインテント
名前に関連するインテントは2つありますが権限設定上はどちらかしか選べません。したがって、インテントとしては機能しますが、バックエンド側ではどちらか片方だけにする必要があります。このあたりはCustomer Profile APIと同じ仕様です。
{ "interactionModel": { "languageModel": { "invocationName": "音声プロフィールのデモ", "intents": [ { "name": "AMAZON.CancelIntent", "samples": [] }, { "name": "AMAZON.HelpIntent", "samples": [] }, { "name": "AMAZON.StopIntent", "samples": [] }, { "name": "AMAZON.NavigateHomeIntent", "samples": [] }, { "name": "AMAZON.PauseIntent", "samples": [] }, { "name": "AMAZON.ResumeIntent", "samples": [] }, { "name": "ProfileGivenNameIntent", "slots": [], "samples": [ "名前を教えて", "名前", "私の名前は", "私の名前を教えて" ] }, { "name": "ProfileFullNameIntent", "slots": [], "samples": [ "フルネームを教えて", "フルネーム", "私のフルネームを教えて", "私のフルネームは" ] }, { "name": "ProfileNumberIntent", "slots": [], "samples": [ "携帯番号を教えて", "電話番号を教えて", "携帯番号", "電話番号", "私の電話番号を教えて", "私の電話番号は", "私の携帯番号を教えて", "私の携帯番号は" ] } ], "types": [] } }, "version": "1" }
コード
ではコードを見ていきましょう。
最初に「フルネーム」か「名前だけ」かを定義するためのモードを用意してあります。オリジナルではそのあたりの区別がなく、パーミッションもまるっと全部指定してあったのですが、権限がない場合にカードを送ると「フルネーム」と「名前」が両方あるのでエラーになったり、ハンドラの中でも権限設定していないほうを呼び出すとエラーになったりしていたので、ここでモードを指定して、各ハンドラでもこのモードを使って発話内容を切り替えたりしています。
繰り返しになりますが、パーミッションの指定の仕方もCustomer Prorfile APIと同じですね。
const MODE = "fullname"; // 姓名(fullname) or 姓名のみ(fullname以外)は排他。変更する場合は開発者コンソールのツール→権限も修正する。 let PERMISSIONS; if (MODE == "fullname") { PERMISSIONS = ['alexa::profile:mobile_number:read', 'alexa::profile:name:read'] } else { PERMISSIONS = ['alexa::profile:mobile_number:read', 'alexa::profile:given_name:read'] }
LaunchRequestを見るとわかりますが、上で述べたとおり、モードで発話を切り替えています。
const LaunchRequest = { canHandle(handlerInput) { return handlerInput.requestEnvelope.request.type === 'LaunchRequest'; }, handle(handlerInput) { let nameText = MODE == "fullname" ? 'フルネーム' : '名前'; let speechText = '音声プロフィールAPIのデモです。'; let repromptText = `あなたの、${nameText}、電話番号の、どちらを聞きたいですか?`; return handlerInput.responseBuilder.speak(speechText + repromptText) .reprompt(repromptText) .getResponse(); }, };
ではPerson Profile APIを使う部分です。ProfileGivenNameIntent、ProfileFullNameIntent、ProfileNumberIntentのどのインテントハンドラも中身はほぼ同じです。なので、ProfileFullNameIntentをピックアップしてみていきたいと思います。
canHandleは至って普通ですが、ここでもMODEを条件に含めています。
const ProfileFullNameIntent = { canHandle(handlerInput) { return ( handlerInput.requestEnvelope.request.type === 'IntentRequest' && handlerInput.requestEnvelope.request.intent.name === 'ProfileFullNameIntent' && MODE == "fullname" ); },
次にcanHandleをうけてのhandleのところです。
パーソナライズを有効にしていると、handlerInput.requestEnvelope.context.System.personで音声プロフィールに紐付いた個人の情報が渡されてきます。この中にPersonIdが含まれており、この時点ではユーザ名や電話番号などはわかりません。handlerInput.requestEnvelope.context.System.personが取れない場合は音声プロフィールが作成されていないということになりますので、アプリで音声プロフィールを作成することを促して、スキルを終了させます。
あわせてapiAccessTokenを取得していますが、実際には使っていませんね(今気づいた)。ただし、Person Profile APIへのアクセスにはhandlerInput.requestEnvelope.context.System.apiAccessTokenが必要です。
async handle(handlerInput) { const person = handlerInput.requestEnvelope.context.System.person; const consentToken = handlerInput.requestEnvelope.context.System.apiAccessToken; if (person) { const personId = person.personId; console.log("Received personId: ", personId); } else { let speechText = '音声プロフィールが認識されませんでした。アレクサアプリから、音声プロフィールを有効にして再度お試しください。'; return handlerInput.responseBuilder .speak(speechText) .getResponse(); }
Person Profile APIへのリクエストは、他のAPI利用と同じようにserviceClientFactoryを使って簡単にできるようになっています。ここで、getPersonsProfileNameを使うと、handlerInput.requestEnvelope.context.Systemのperson.personIdとapiAccessTokenがAPIに送られて、Person Profile APIからユーザの名前や電話番号を取得できるようです。
Alexaアプリで名前が設定されていない場合はgetPersonsProfileNameからの応答がnullになるため、再度ユーザにそれを設定するよう促します。
try { const client = handlerInput.serviceClientFactory.getUpsServiceClient(); const name = await client.getPersonsProfileName(); console.log('Name successfully retrieved, now responding to user.'); let response; let speechText; if (name == null) { speechText = '氏名が設定されていないようです。アレクサアプリで氏名を設定してください。'; response = handlerInput.responseBuilder.speak(speechText) .getResponse(); } else { speechText = `あなたのフルネームは、 ${name} です。`; response = handlerInput.responseBuilder.speak(speechText) .getResponse(); } return response;
で、このあたりの処理は、Customer Profile APIとほとんど同じです。Customer Profile APIのサンプルを少し抜粋します。
try { const client = serviceClientFactory.getUpsServiceClient(); const name = await client.getProfileName(); console.log('Name successfully retrieved, now responding to user.'); let response; if (name == null) { response = responseBuilder.speak(messages.NAME_MISSING).getResponse(); } else { response = responseBuilder.speak(messages.NAME_AVAILABLE + name).getResponse(); } return response;
getProfileNameがgetPersonsProfileNameになっただけということがわかりますね、
で、Person Profile APIへのアクセスでエラーが発生した場合は例外に流れます。エラーが起きるのは2パターンです。
- APIアクセスでなにかエラーが発生した(ネットワークエラーとかAPI側の問題とか)
- アクセス権が許可されていない場合、getPersonsProfileNameが例外を発生させる。
アクセス権が許可されていない場合はServiceErrorになりますので、それ以外のエラーをここで拾って、アクセス権が許可されていない場合はさらに例外をthrowします。
} catch (error) { if (error.name !== 'ServiceError') { const response = handlerInput.responseBuilder.speak("すいません、うまくいかないようです。").getResponse(); return response; } throw error; } } };
で、ここでの例外は、addErrorHandlersで指定されているProfileErrorが処理します。
exports.handler = skillBuilder .addRequestHandlers( LaunchRequest, ProfileGivenNameIntent, ProfileFullNameIntent, ProfileNumberIntent, SessionEndedRequest, HelpIntent, StopAndCancelIntent, UnhandledIntent, ) .addErrorHandlers(ProfileError) .withApiClient(new Alexa.DefaultApiClient()) .addRequestInterceptors(LogRequestInterceptor) .addResponseInterceptors(LogResponseInterceptor) .withCustomUserAgent('cookbook/customer-profile/v1') .lambda(); // The identifier of the recognized speaker.
ProfileErrorで403 ServiceError、つまりアクセス権がない場合の処理を行っています。ユーザに権限設定を促し、かつ、カードを送るといういつものやつです。オリジナルのコードだとここで「フルネーム」「名前だけ」の両方を送るようになっていたので、権限許可していない場合は100%エラーになっていました。addErrorHandlersの中でエラーが起きていたので、どこでエラーになっていたのかがなかなかわからずちょっとハマりました・・・
const ProfileError = { canHandle(handlerInput, error) { return error.name === 'ServiceError'; }, handle(handlerInput, error) { if (error.statusCode === 403) { let nameText = MODE == "fullname" ? '氏名' : '名前'; let speechText = `音声プロフィールのアクセス権が許可されていません。アレクサアプリでこのスキルの設定画面を開き、音声プロフィールから${nameText}と電話番号への権限を許可してください。`; return handlerInput.responseBuilder .speak(speechText) .withAskForPermissionsConsentCard(PERMISSIONS) .getResponse(); } let errorText = '音声プロフィールAPIへのアクセスでエラーが発生しました。もう一度やり直してください。'; return handlerInput.responseBuilder .speak(errorText) .reprompt(errorText) .getResponse(); }, };
ざっと見た感じ、ほんとうにCustomer Profile APIと似てますねー。音声プロファイルを設定してスキル起動して試してみてください。
Alexaアプリでの権限設定について
冒頭でお伝えしたとおり、アクセス権限の設定はCustomer Profile APIと共通のようです。つまり、ユーザへの権限要求はCustomer Profile APIのものもあわせて行われるのですが、ユーザ側でUser Profile APIのものとCustomer Profile APIのものを個別に設定ができるようになっています。ユーザ側が細かくコントロールできるのは良いと思うのですが、ちょっとわかりにくいかもしれませんね。
また、手元で複数の音声プロファイルを試してみようと思ったのですが、これみてちょっとゲンナリしてやめました・・・一般のユーザさんがこれを設定したりできるのかなぁ・・・
まとめ
音声プロファイルを使って音声認識できるのは非常に良いと思うのですが、ちょっと利用するまでのハードルが高い気がします。そうなるとなかなか開発者側としては実装するモチベーションがわきません。Person Profile APIの実装自体はとてもシンプルなので、このあたりが実機だけで完結できるようになるといいなと思うんですが、アカウントに紐付いているようなので難しいかもしれませんね。
あとパーソナライズと違って、Person Profile APIは個人情報を扱うことになると思うので、いろいろ取り扱いにはご注意ください。