音声ファイルから、Google Assistantにリクエストして、実行されたインテントを確認するテスト用ライブラリを作った

# TL;DR / 結論から先に言うと
- 音声ファイル(wavファイル)から、Google Assistantにリクエストして、実行されたインテントを確認するテスト用ライブラリを作った
    - 以下で「google-assistant-test-from-wavefile」 として公開した
    - https://github.com/masachaco/google-assistant-test-from-wavefile
- テストも不十分で仮実装レベルなので実運用向きではない。α版未満。
- セットアップが大変
- できればどのEntityにマッチしたとかは、今後できるようにしていきたい

# 何ができるの

あなたにかわって、Google Assistantにテストライブラリが録音したあなたの音声で話しかけます。
話しかけた音声が、ちゃんと想定のインテントに落ちているかテストできます。
定期的に実行して、Google Assistantが発話に対する認識がかわってしまっていることがないか確認できます。

# すでにあるものと、やりたかったこと

「[actions-on-google-testing-nodejs](https://github.com/actions-on-google/actions-on-google-testing-nodejs)」 という公式のGoogle Assistant End to Endテストライブラリがすでに存在しています。

https://github.com/actions-on-google/actions-on-google-testing-nodejs

これは、以下のように、PC内で Google Assistantを実際に実行して、その結果を確認するというものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/c2f9e684-6b47-c697-a808-920c3238e1bf.png)


ですが、音声でのリクエストと、テストは現在サポートされていません。

そのため、以下の確認はできません。

- 音声でリクエストして、 今現在実際に Google Assistantに発話がどのように認識されているのか
- 音声でリクエストした結果、Dialogflowのどのインテントに落ちているのか

実際にテストするとなると、Assistantに話しかけるしかなく、テスト・確認に手間がかかっていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/dab8ca25-d5cc-44e8-1a7c-6424dc7ea0e1.png)

そこで、 `actions-on-google-testing-nodejs` と同じ仕組みで、wavファイルを指定して音声によるテストができるライブラリを作ってみました。
これで、「実行時点でGoogle Assistantはどういう風に認識するのか」その結果、「実行時点でDialogflowがどのIntentに落とし込むのか」を確認できるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/12b7cfee-f9d7-31c6-58f6-bdc934e31d3b.png)

# ということで

wavファイルから、Google Assistantへリクエストして、どのインテントに落ちたか確認できる

### google-assistant-test-from-wavefile(仮)

[https://github.com/masachaco/google-assistant-test-from-wavefile](https://github.com/masachaco/google-assistant-test-from-wavefile)

を作成しました。


※以下のプロジェクトを元に作成しました
googlesamples/assistant-sdk-nodejs
https://github.com/googlesamples/assistant-sdk-nodejs


# ライブラリのセットアップ手順

ライブラリをセットアップするために３つの認証情報を入手する必要があります。

- テスト対象の、Google Assistantの APIへの認証情報
- テスト対象の、Dialogflowの APIへの認証情報
- テストでPC上で立ち上げるGoogle Assistant(Device) の認証情報

その後、テストでPC上で立ち上げるGoogle Assistant(Device) の認証情報に、
Assistantを使用するユーザ(Googleアカウント)を指定して紐付けます。

# このライブラリを使う前に

だいぶセットアップの手順が煩雑なため、まずはこのライブラリの作成のために、

参考にしたプロジェクトである「[actions-on-google-testing-nodejs](https://github.com/actions-on-google/actions-on-google-testing-nodejs)」を先に使用して、使用イメージを掴むことをお勧めします。

よういちろうさん が作成してくださったブログに詳しい使い方がとてもわかりやすく記載されています。

### Googleアシスタント向けアプリのEnd to Endテストライブラリの使い方
[https://www.eisbahn.jp/yoichiro/2018/07/actions_on_google_testing_ja.html](https://www.eisbahn.jp/yoichiro/2018/07/actions_on_google_testing_ja.html)

(以下のセットアップ方法も上記の記事を参考に作成させていただきました。 :bow:  )

# ライブラリのインストールとセットアップ

Assistant アプリのプロジェクト配下に移動します

```bash
cd ~/Projects/your_assistnat_project
```

以下を実行します。

```bash
npm install --save-dev https://github.com/masachaco/google-assistant-test-from-wavefile
```

credentialsディレクトリを作成します。
コミットしないように `.gitignore` に追加します。
その他バージョン管理システムを使用している場合は適宜除外して、 `credentials` ディレクトリ以下は絶対にコミットしないようにしてください。

```bash
mkdir  ~/Projects/your_assistnat_project/credentials/
echo "\ncredentials/" >> ~/Projects/your_assistnat_project/credentials/.gitignore
```


# テスト用Assistantデバイスの作成

Actions on Googleにアクセス。　「GO TO ACTIONS CONSOLE」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/472c56f5-40e0-e00d-4764-312cbd09f439.png)

新規プロジェクトを作成します。特に指定がなければ、ロケールは日本・日本語にするのを忘れないようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/8056cba5-345a-73ed-a8f4-a6714d88c9b0.png)


いろいろ選択肢が出てきますが、「Device Registration」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/9ae213fe-81b0-62ef-b79b-72f1f18ec7b8.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/807b0e76-f907-7e3f-02b1-e3abdc8802b2.png)

Register Modelを選択して、仮想スピーカーを登録します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/d21799fa-ab04-3d9a-2a1f-1be12ba0c469.png)

登録するデバイス名や、作成企業、作成デバイスは適当に選びます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/bce8ce19-a41f-7308-83af-93294d1c82da.png)

Download OAuth 2.0 credential を選択してダウンロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/8e1e4c33-6a75-67ba-f2f6-e50227c5290d.png)

ダウンロードしたCredentialファイルは、先ほどAssistantアプリ のディレクトリ配下に `device-credential.json` として配置します。
credentials ディレクトリ以下のファイルは `.gitignore` に追加して、絶対にコミットしないようにしてください。

```bash
mv ~/Downloads/(DownloadしたCredentialのJSON).json ~/Projects/your_assistnat_project/credentials/device-credentials.json
```

# Google Assistant APIの有効化

Google Assistantデバイスは、デフォルトではAPIへのアクセスが有効化されていません。
そのため、SDKからアクセスしようとしてもエラーになります。

以下のURLにアクセスして、Google Assistant APIを有効化してください。
https://console.cloud.google.com/apis/library/embeddedassistant.googleapis.com?q=Assistant

その際、GCP上部に表示されているプロジェクトに、先ほど作成したAssistant デバイスが選択されていることを確認してください。
選択されていない場合は、、先ほど作成したAssistant デバイスを選択し直して、再度上記のURLにアクセスしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/c3ada261-b917-a19a-c50f-73803dfffded.png)

Google Assistant APIを有効にするボタンを押下して、有効にします。

# テスト用Assistantデバイスとで使用するGoogleアカウントの認証ファイルの作成

以下のコマンドを実行して、Assistantに使用するGoogleアカウントを、先ほど作成したDeviceを紐付けます。
コマンドを実行すると、認証用のURLが表示されるので、ブラウザでアクセスします。

```bash
node ~/Projects/your_assistnat_project/node_modules/actions-on-google-testing/generate-credentials.js ~/Projects/your_assistnat_project/credentials/device-credentials.json
```

Assistantに使用するGoogleアカウントでログインします。
その後、Google Assistantが、使用するGoogleアカウントへのアクセスを求めてくるので、許可します。
最後に、認証用のコードが表示されるので、クリップボードにコピーします。

先ほどコマンドを実行したコンソールが、 `Authorization code` の入力を求めているので、さきほどクリップボードにコピーした内容を貼り付け、Enterを押します。

先ほどコマンドを実行したディレクトリに `test-credentials.json` が作成されます。
`credentials` ディレクトリ配下に移動します。
これも、gitなどに絶対コミットしないように `.gitignore` で除外するようにしてください。

```bash

mv ~/Projects/your_assistnat_project/test-credentials.json ~/Projects/your_assistnat_project/credentials/test-credentials.json
```

以上で、テスト用Assistantデバイスの作成と認証情報の設定は完了です。
お疲れ様でした。

# Dialogflow APIへの認証情報の作成

まだ認証情報を作成する必要があります。
テストライブラリでは、音声によって、想定したインテントが呼ばれているかインテント名を指定して確認します。


ですが、Assistant SDKは、デバッグモードでも、実行したインテントの「ID」しか返してくれません。
そのため、インテント名と、インテントIDを紐づけるために、Dialogflow APIへアクセスします。
そのための認証情報を作成します。

まずは、Google Assistant APIと同様に、以下のURLにアクセスしてDialogflow APIを有効にします。
今度、GCPのプロジェクトに、 テスト対象のDialogflowが選択されていなければならないことに注意してください。

https://console.cloud.google.com/flows/enableapi?apiid=dialogflow.googleapis.com

選択されていない場合は、テスト対象のActions アプリ / Dialogflowのプロジェクトを選択し直して、上記のURLに再度アクセスします。
その後、APIを有効にします。

その後、以下の手順にそってサービスアカウントを作成、認証キーのJSONファイルをダウンロードします。
https://cloud.google.com/docs/authentication/getting-started

ダウンロードしたJSONファイルは、 Assistantアプリ プロジェクトの `credentials` ディレクトリ配下に `dialogflow-credential.json` として配置します。
これも、gitなどにコミットされないように `.gitignore` などで必ず除外されるようにしてください。

```bash
mv ~/Downloads/(ダウンロードしたJSON).json ~/Projects/your_assistnat_project/credentials/dialogflow-credential.json
```

大変、大変お疲れ様でした。
これで認証情報が全て揃いました。

# テストで使用する音声ファイルの準備

リクエストに使用するwavファイルは以下の形式である必要があります。

- WAV (Microsoft) 16bit PCM符号あり
- モノラル
- サンプリングレート 16000Hz

[Audacity](https://www.audacityteam.org/) などで、上記形式の音声ファイルは作成できます。１１

# テストの作成

[jest](https://jestjs.io/) を使用した場合のテスト例を以下に示します。


```javascript

/** テストするGoogle Assistaに統合されている、DialogflowのProject ID */
process.env.DIALOGFLOW_PROJECT_ID = '** Set your testing dialogflow project id. **';

/**
 * Dialogflow API にアクセスするための credential JSONのパス
 * @see https://github.com/googleapis/nodejs-dialogflow
 */
process.env.DIALOGFLOW_CREDENTIAL = './credentials/dialogflow-credential.json';

/**
 * Assistant Deviceとして登録されたデバイスの Credential JSONのパス
 */
process.env.DEVICE_CREDENTIAL = './credentials/test-credentials.json';

/**
 * リクエストに使用するwavファイルが保存されているパス
 */
process.env.WAVE_FILES_PATH = './waves/';

const GoogleAssistant = require('google-assistant-test-from-wavefile');
const assistant = new GoogleAssistant();

// テストに時間がかかるのでタイムアウトの時間を延長
jest.setTimeout(30000);

/** Google Assistantとの接続をセットアップ */
beforeAll(async () => {
    await assistant.setUp();
});

/** 「終了」を発話して、何らかのアシスタントアプリに入っているステータスの場合は抜け出す */
beforeEach(async () => {
    await assistant.initConversation();
});

/** 「終了」を発話して、アシスタントアプリに入っているステータスから抜け出す */
afterAll(async () => {
    await assistant.shutdownConversation();
});

test('Default Welcome Intentが呼ばれることをテストする', async () => {
    // ./wave/call_action.wav の音声でGoogle Assistantにリクエストすると、Default Welcome Intent が呼ばれることをテスト
    expect(await assistant.callIntentByAudio("call_action.wav")).toBe('Default Welcome Intent');

    // ./test-wave-files/hello.wav の音声でGoogle Assistantにリクエストすると、HelloIntent が呼ばれることをテスト
    expect(await assistant.callIntentByAudio("hello.wav")).toBe('HelloIntent');
});
```

# テストファイルの解説

## Google AssistantプロジェクトIDの指定
```javascript
process.env.DIALOGFLOW_PROJECT_ID
```
テスト対象のGoogle Assistant AppのIDを環境変数に指定します。
もちろんこの行を削除して、テスト実行前に指定しても問題ありません。

Dialogflowの![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/a352d691-060c-6d5d-7948-e63bc733c007.png)マークを押して、設定画面から確認することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96675/bd92c458-2957-198d-46ee-ae4147e97176.png)


## Dialogflow APIへアクセスするためのCredential JSONファイルの指定

```javascript
process.env.DIALOGFLOW_CREDENTIAL = './credentials/dialogflow-credential.json';
```

先ほどダウンロードした `dialogflow-credential.json` のパスを指定します。
nodejsプロジェクトのディレクトリ直下から見た形で指定します。

## テストで使用するwavを格納したディレクトリの指定

```javascript
process.env.WAVE_FILES_PATH = './waves/';
```

テストで使用するwavファイルが保存されているディレクトいのパスを指定します。
nodejsプロジェクトのディレクトリ直下から見た形で指定します。

先述したとおり、リクエストに使用するwavファイルは以下の形式である必要があります。

- WAV (Microsoft) 16bit PCM符号あり
- モノラル
- サンプリングレート 16000Hz

## Google Assistant APIへアクセスするためのCredential JSONファイルの指定

```javascript
process.env.DEVICE_CREDENTIAL = './credentials/test-credentials.json';
```

先ほどダウンロードした `test-credentials.json` のパスを指定します。
nodejsプロジェクトのディレクトリ直下から見た形で指定します。

## ライブラリのインポート

ライブラリをインポートし、インスタンスを作成します。

```javascript
const GoogleAssistant = require('google-assistant-test-from-wavefile');
const assistant = new GoogleAssistant();
```

## テストのセットアップ

テスト実行時の一番最初にDialogflowから、インテント一覧を取得します。

```javascript
/** Google Assistantとの接続をセットアップ */
beforeAll(async () => {
    await assistant.setUp();
});
```

## 会話の初期化

「終了」と発話して、何かしらのアプリに入っている場合は、テストの都度終了します。

```javascript
beforeEach(async () => {
    await assistant.initConversation();
});
```

引数にstringの配列を指定することで会話を初期化するための、発話を指定することができます。
配列の先頭から順番にテキストでAssistantへリクエストされます。

```javascript
beforeEach(async () => {
    await assistant.initConversation(['それじゃ','バイバイ']);
});
```

## 会話の終了

「終了」と発話して、会話をテストの都度終了します。

```javascript
afterAll(async () => {
    await assistant.shutdownConversation();
});
```

引数にstringの配列を指定することで会話を初期化するための、発話を指定することができます。
配列の先頭から順番にテキストでAssistantへリクエストされます。

```javascript
beforeEach(async () => {
    await assistant.shutdownConversation(['それじゃ','バイバイ']);
});
```


## テストの実行

```javascript
test('Default Welcome Intentが呼ばれることをテストする', async () => {
    // ./wave/call_action.wav の音声でGoogle Assistantにリクエストすると、Default Welcome Intent が呼ばれることをテスト
    expect(await assistant.callIntentByAudio("call_action.wav")).toBe('Default Welcome Intent');

    // ./test-wave-files/hello.wav の音声でGoogle Assistantにリクエストすると、HelloIntent が呼ばれることをテスト
    expect(await assistant.callIntentByAudio("hello.wav")).toBe('HelloIntent');
});
```

`assistant.callIntentByAudio('wavファイル名')` を指定することで、Google Assistantにリクエストを送ります。
非同期で実行されるので、必要に応じて `await` などしてください。

`assistant.callIntentByAudio` はwavファイルの音声によって実行されたインテント名を返します。

# 実行してみる

実行してみる

```bash

% npm test ./test/index.test.js

> assist-wave-test@1.0.0 test /Users/xxxx/your-assistant-app
> jest "./test/index.test.js"

(node:8102) DeprecationWarning: grpc.load: Use the @grpc/proto-loader module with grpc.loadPackageDefinition instead
  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:152
    query:終了

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:215
    User say: 終了

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:216
    Assistant say: わかりました

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:152
    query: call_action.wav

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:125
    Loading wave file... : call_action.wav

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:210
    User say: ○○○○○(アプリ名) につないで

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:211
    Assistant say: わかりました。○○○○○(アプリ名)のテストバージョンです。
    こんにちは、○○○○○(アプリ名)です

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:152
    query:hello.wav

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:125
    Loading wave file... : hello.wav

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:210
    User say: こんにちは

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:211
    Assistant say: はい、こんにちは

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:152
    query:終了

 PASS  test/index.test.js (24.826s)
  ✓ アプリに接続してあいさつできること (17438ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        27.603s
Ran all test suites matching /.\/test\/index.test.js/i.
  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:215
    User say: 終了

  console.log node_modules/google-assistant-test-from-wavefile/google-assistant-grpc/googleassistant.js:216
    Assistant say: undefined
```

# おまけ

一応、コードをいじると以下も取得できます。

- Assistantのレスポンステキスト
- Google Assistantによって、テキストに変換されたリクエスト
- マッチしたインテントのID

```javascript
    { text: 'はい、○○○○のテストバージョンです。\nこんにちは。',
      query: '○○○○　につないで',
      matchedIntentId: 'xxxx-xxxx-xxxx-xxxx' }
```

# まとめ

無事、音声でリクエストしてGoogle Assistantのテストをすることができました。
AIの認識は毎回かわるので、テストというよりも、一定の音声に対してGoogle AssistantとDialogflowがどんな認識をするのか、といった監視に近いのかもしれません。
とりあえず、実行できることを優先したので、今後は、コードを整理して綺麗に、使いやすくして、実用できるものにしていこうと思います。
`new GoogleAssistant()` で、毎回認証しているのでもう少し効率化できそうですね。
しばらくしたら、 Actions SDKを使用して、 node.jsで音声によるリクエストをするための詳細を記事にしてみようと思います。