AlexaスキルをGoogle Home対応アプリに移植した話

　過去に作成したAmazon Echo用のAlexaスキルをGoogle Home対応アプリに移植してみたので、その方法を備忘録がてら記録したいと思います。全部書いてると尋常じゃない量になりそうなので、参考にした他記事へのリンクを主に記しています。
![処理フロー](https://qiita-image-store.s3.amazonaws.com/0/230715/ffe89084-55c9-41ab-d619-1b32b5f67f93.jpeg)

# あらすじ
　元々のAlexaスキルはAmazon DeveloperとAWS Lambdaを利用して作成していました。LambdaはPythonで書いていたので、ほとんどnode.jsのサンプルしか転がっていないGoogle Homeアプリに移植するのはなかなか骨が折れそうでした。そこでAWS API Gatewayを使ってDialogflowのFulfillment処理をAlexaスキルで使用しているLambdaにそのまま投げることで、Alexa用のコードに少し追加するだけでGoogle Home用の処理も実現できました。APIの設定等に苦労はしましたが、それ以外はAlexaのLambda関数のコードをほぼ流用したのですんなりと移植できましたとさ。めでたしめでたし。

# Amazon EchoとGoogle Homeの（開発目線での）違い
|  | Amazon | Google |
|:--|:--|:--|
| 「リクエスト」と「インテント」  | 「ローンチリクエスト」「インテントリクエスト」などのリクエストがあって、インテントリクエストの中に「なんとかインテント」がある。  |  Dialogflowは起動時の呼び出しも全部「インテント」として扱う。 |
| プライバシーポリシー  | 特に個人情報を収集しないときはプライバシーポリシーを作成しなくてもいい。  |  Actions on Googleは公開申請するのにプライバシーポリシーが必須で、個人情報などを扱わないときは扱わない旨を記す。 |
| 会話中に何か情報を保持したいとき  | そのセッション中に情報をJSON形式で保持できる。 |  Dialogflowはcontext valueとしてそのcontext中は保持できるが、文字列しか保持できない。<br>詳しくは"[Dialogflowで会話中に値を保持する方法](https://qiita.com/running1017/items/15718bb7ee763613cda3)" |
| SSMLの仕様 | `<speak>20180228</speak>`というSSMLは勝手に`2018年2月28日`と解釈してくれる。<br>詳しくは"[Amazon Alexa(Amazon Echo)で使用できるSSMLタグのまとめ](https://kotodama.today/?p=251)" | `<speak><say-as interpret-as="date">20180228</say-as></speak>`としないと日付とは解釈してくれない。<br>詳しくは"[Actions on Google(Google Home)で使用できるSSMLタグのまとめ](https://console.actions.google.com/u/1/project/myassetsapp/overview)" |
| 発話内容とテキストアウトプット | スマートフォンのAlexaアプリ等でホームカードというスピーカーからの発話以外にもテキストや画像が発信できる。テキストは音声に対する補助的な内容でいい。 | スマートフォンのGoogle Assistantアプリから利用すると音声とテキストで返答できる。音声にはSSMLタグを含んでいたりテキストには改行を入れたりそれぞれ異なったものを出力できるが、ユーザーから見て同じ意味内容でないといけない。 |

# 移植したAlexaスキル

- 「俺の資産」
    - [Alexa版](https://smarthacks.jp/db/3238/)
    - [Google Assistant版](https://smarthacks.jp/db/8689/)
- 「フェルミ推定」
    - [Alexa版](https://smarthacks.jp/db/5831/)
    - [Google Assistant版](https://smarthacks.jp/db/8648/)(Actions on Googleのアプリ名のポリシーにより「フェルミ推定の例題」として公開しました)

# AlexaスキルをGoogle Home対応アプリに移植した手順
## 0. Alexaスキルの確認
　移植したいAlexaスキルのインテントやスロット、Lambdaの処理などをどのように設定していたか確認しておきましょう。

## 1. Actions on GoogleとDialogflowの設定
　"[Dialogflow入門](https://qiita.com/kenz_firespeed/items/0979ceb05e4e3299f313)"や"[Actions on Google と AWS Lambda で Google Home から Slack にポストする](http://tech.unifa-e.com/entry/2017/10/16/103458)"を参考にDialogflowのIntentやEntityをAlexaスキルと対応させるように設定します。

## 2. AWS API Gatewayの設定
　"[AWS API Gatewayの設定方法](https://qiita.com/running1017/items/e89c772d6ead6cca1c7f)"の手順でAPI Gatewayの設定をして、APIのURLをFulfillmentのWebhookのURLに貼り付けます。

## 3. Lambdaのコードを書き換える
　Lambda関数のハンドラの引数`event`の中身のJSONをCloud Watchのログで見ながら、Alexa用とGoogle Home用でレスポンスの形を分けて処理しました。`event`のJSONのキーがそれぞれで固有のものがあるので、それを見て場合分けしたサンプルコードが以下です。もっといい方法があるような気がします。

```python:gh_function.py
def gh_handler(event, context):
    # Google Home用の処理して返す

    return {'speech': 'Googleさん、こんにちは'}
```

```python:alexa_function.py
def alexa_handler(event, context):
    # Alexa用の処理して返す

    return {
        'responce': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': 'Amazonさん、こんにちは'
            },
            'shouldEndSession': True
        }
    }
```

```python:lambda_function.py
import gh_function
import alexa_function

def lambda_handler(event, context):
    # event.keys()に含まれているキーで場合分けする

    if 'result' in event.keys():
        return gh_function.gh_handler(event, context)
    elif 'session' in event.keys():
        return alexa_function.alexa_handler(event, context)
    else:
        raise ValueError("Unexpected Request Error")
```

## 4. 試行錯誤する
　Actions on GoogleのシミュレーターとLambdaとCloud Watchを行ったり来たりしてうまくいくようにがんばります。Lambdaのテストイベントも活用して動作するように仕上げます。元々動いていたAlexaのほうも動作がおかしくなっている可能性はあるので一応チェックしましょう。

## 5. 公開申請
　完成したらActions on Googleのアプリの説明やバナー画像などを設定してSUBMIT DRAFT FOR REVIEWからレビューの申請をします。1発で通ることはあまりないので、リジェクトされた理由を見ながら適宜修正して申請を繰り返します。今回「俺の資産」というアプリ名で申請したら、以下の理由でリジェクトされました。
>Actions on Googleのポリシーでは、製品、サービス、またはコンテンツのカテゴリーの単語や語句を含む普遍的な事柄をApp名としてご使用いただけません。そのため、大変申し訳ありませんが本アプリ名は以下の理由で認証できませんので、ご修正をお願い頂けますでしょうか。

しかし「[俺のドレッシング](https://smarthacks.jp/db/5897/)」というアプリが既に審査を通っていたのでこの理由では少々納得がいかず、アプリ名の基準についてサポートに問い合わせたところ以下のように返答されました。

>おっしゃる通りに”俺の資産”はアプリ名としてご使用できます。
レビュー結果に関してご迷惑をおかけして申し訳ございませんでした。
大変お手数ですが、もう一度ご申請していただけますようお願い申し上げます。
ただ、Policyは常に変更される可能性があり、同じ条件で必ず承認されるわけではないこと予めご了承ください。

このような事例もあったので、疑問に思ったことは問い合わせた方がいいでしょう。
　また、公開されてからGoogle Homeで動作確認のために「おれのしさんにつないで」と話しかけると、どうやら「俺の**子さん**につないで」と認識されてしまっていてうまく起動しませんでした。そこで"[Implicit/Explicit Invocation ~ Actions on Gogoleの呼び出し方を考える AoGを一発で呼び出すために ~](https://qiita.com/soundTricker/items/1d9d6bbe52caba5302af)"を参考に「俺の子さん」をImplicit Invocationとして呼び出せるようにしてみましたが、それだと
>ユーザー「おれのしさんにつないで」
スピーカー「俺の資産がおすすめです。利用しますか」
ユーザー「はい」

と不自然な会話になってしまいました。アプリ名を「俺の子さん」にするわけにもいかず、アプリ起動時の言語認識の部分はこちらからどうしようもないので、またGoogleのサポートにこれらの事象を問い合わせたところ以下のように返答されました。

>アプリ名に関する事象についてご報告いただき、ありがとうございます。
本件について確認を進めさせていただきますので、
回答まで今しばらくお待ちいただきますようお願い申し上げます。

おそらくなんとかしてくれると思うので、気長に待ちたいと思います。

# 躓いたことや時間がかかったこと、その解決方法
- AWS API Gatewayの設定がなかなかよくわからなくて躓いた
    - "[AWS API Gatewayの設定方法](https://qiita.com/running1017/items/e89c772d6ead6cca1c7f)"の記事を参照
- AlexaとやりとりするJSONには`'attributes'`というキーがあってそこにセッション中の情報が保存できるが、Dialogflowで同じようなことをする方法がわからず躓いた
    - "[Dialogflowで会話中に値を保持する方法](https://qiita.com/running1017/items/15718bb7ee763613cda3)"の記事を参照
- Actions on Googleでアプリの公開申請をしてリジェクトされた理由が納得いかなかった
    - 本記事の[公開申請](#公開申請)を参照

# まとめ
　HTTPリクエストやAPIについての知識がそれほどあるわけではなかったので設定に苦労しましたが、それ以外はAlexaのLambda関数のコードをほぼ流用したのですんなりと移植できました。それぞれの仕様やポリシーの違いで何度か足踏みしましたがうまくいったのでよしとします。

# 参考にしたページ
- [Dialogflow入門](https://qiita.com/kenz_firespeed/items/0979ceb05e4e3299f313)
- [Dialogflowで応答文の一部にwebhookの結果を設定する方法](https://qiita.com/makopo/items/79e8dfa1db2f5159ea75)
- [Actions on Google と AWS Lambda で Google Home から Slack にポストする](http://tech.unifa-e.com/entry/2017/10/16/103458)
- [Dialogflow公式ドキュメント](https://dialogflow.com/docs/getting-started/basics)
- [AWS Documentation - API Gateway](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/welcome.html)

# 補足
　今回は[株式会社SmartHacks](https://smarthacks.jp/)での就業形インターンとして、アプリならびにこの記事を作成しました。