Raspberry PiにAlexaを召喚する3つの方法

# (2018/2/11追記)

　Alexaのインストール方法を探しにいらした方は、新しくこんなものを書いているので、こちらもどうぞ。

ReSpeakerと共に働くAlexaとGoogle Assistant
https://qiita.com/Dimeiza/items/2d9d17f94145bd6fa520

# はじめに

　スマートスピーカー隆盛の候、皆様いかがお過ごしでしょうか。

　AlexaとかGoogle Homeの話が至る所でされているのに釣られて、私もEcho Dotの招待申請をしてみたのですが、なかなかメールがやってきません。

　が、よくよく考えると、一応エンベデッド界隈の末席にいて、手元にRaspberry Piもあることだし、

* これは自分でAlexaを召喚してみたほうが面白いのでは？

　と思い立ち、純然たる好奇心ドリブンで、Raspberry PiにAlexaを召喚してみることにしました。

　英語版Alexaを召喚したり、日本語化したりと、あれこれと試行錯誤をする中で、Alexa召喚の方法を3つほど試し、最終的にAlexaが日本語をしゃべるようになったので、簡単に情報源と特徴、ハマったところとかを書き記しておこうかと思います。

　スマートスピーカー面白そうだな、と思っている方がお試しになる際にでも、ご参照いただければ幸いでございます。

## 環境

### ハードウェア

* Raspberry Pi3
    * Raspbian Stretch with Desktop
        * 後述するAVS Device SDKの公式対応バージョンです。jessieの場合は更新をお勧めします。
        * 最初はLiteでやったんですが、後でDesktopに切り替えました。
* USBマイク
    * 最初はこれを使っていましたが
        * https://www.amazon.co.jp/dp/B01KZPF1U8/
    * 後からこれに切り替え、
        * https://www.amazon.co.jp/gp/product/B01M7YIYZV/
    * さらにこれを使っています(マイク買いすぎぃ！)。
        * https://www.amazon.co.jp/gp/product/B06Y3ZL2V4/
        * https://www.amazon.co.jp/gp/product/B007WSKGIE/
    * 最後のUSBサウンドカードのマイク感度は良好でした。が、現時点でこれのオーディオ出力からAlexaの音声を出せておらず、今のところマイクのみの使用です。

### その他
 
　音声出力はステレオピンジャックから出力しました。

　ネットワーク接続はWifiで。

　基本的にシリアル通信モジュール経由でヘッドレス操作していましたが、後述するように、方法によってはAlexaを使用する際に一度Amazonにブラウザで接続する操作があったりします。
　グラフィカルログインかVNCを併用したほうが楽かもしれません。

　あと、最初に使ったマイク、lsusbで取得できるマイクの情報が、"Intel Corp."だったのでちょっと面喰いました。

>Bus 001 Device 006: ID 8086:0808 Intel Corp.
>Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
>Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
>Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

　他の方の事例だとC-Media等の文字列が出るようなので何かと思いましたが、arecordコマンド等で確認すると、きちんとオーディオデバイスとして認識されていました。これでも一応大丈夫そうです。

>arecord -l
>**** ハードウェアデバイス CAPTURE のリスト ****
>カード 1: Device [USB PnP Sound Device], デバイス 0: USB Audio [USB Audio]
>  サブデバイス: 1/1
>  サブデバイス #0: subdevice #0

## 最初に

 Amazon開発者コンソールから、Alexaのページにアクセスして、召喚先であるRaspberry Piのデバイス登録をしましょう。

https://developer.amazon.com/ja/

　この辺は多分至る所で説明されているはずなので、ここでは細かく触れませんが、

* どの方法でも、最終的に必要になる情報は3つ。
    * "製品情報"の製品ID(Device ID)
    * "セキュリティプロファイル"の
       * クライアントID(Client ID)
       * クライアントのシークレット(Client Secret)
* "許可された出荷地"と"許可された返品URL"には、各方法で指定された値を設定しておく
    * 後で"許可された出荷地"指定のURLにアクセスすることになります。Raspberry Pi側で入力したURLと、"許可された出荷地"指定のURLが一致している必要があります。

　…というぐらいが揃っていればよいかと思います。

　ところが、私はここで一つハマってしまいました。

### amazon.co.jpとamazon.comのアカウントについて

　何にハマったかというと、これです。

https://dev.classmethod.jp/voice-assistant/solution-of-a-problem-amazon-com-account-conflict/

　ここ半年ぐらい、Amazon.comでいろいろ買い物をしていて、アカウント設定を共通化していたのです。[^1]
　おかげで当初、本来Amazon.co.jpで入るはずだった開発者アカウントに(意図せずして)Amazon.comのアカウントで入ってしまい、alexa.amazon.com(アメリカのAlexaアプリ)でしか管理できていませんでした。

　私のような人のためにかいつまんで言うと、

* Amazon.comとAmazon.co.jpに、同一メールアドレスと同一パスワードでそれぞれアカウント登録している人は、パスワードだけ異なるものにしておく。
* Amazon.co.jpの領域内で作業する場合は、Amazon.co.jpのアカウントで開発者アカウントを作る必要がある。
* 開発者アカウントを作る際や、Alexaアプリにつなぐ際には、Amazon.co.jpのメールアドレスとパスワードを入力する(Amazon.comのパスワードを入れるとAmazon.com側に誘導される)。

　と、混乱なくAmazon.co.jpのアカウントで、以降の操作が可能になります。

　Amazon.co.jpだけしかアカウントないよ！　という人にはあまり関係のない話なので、そのまま先へ進みましょう。

　では3つの方法を順に。

## 1. AlexaPi

https://github.com/alexa-pi/AlexaPi

自分の環境で最初に試したのはこれでした。

手順そのものはいたって簡単で、フォルダ掘って必要アプリ入れて、gitからリポジトリコピーして、setupスクリプトを実行するだけです。

setupスクリプトでは上述の3つの情報を聞かれるので、都度入力していきます。

確かこれはヘッドレスでセットアップが完結して、サービススクリプトまで準備してくれるので、その気になれば起動時Alexa常駐とかも簡単です。

<img width="539" alt="AlexaPi.png" src="https://qiita-image-store.s3.amazonaws.com/0/54795/6d965a24-6445-b8f9-0f64-835745355d96.png">

インストール直後は起動スクリプトがenable化されなかったはずなので、こんな感じでstartしてやると、以降、"Alexa"に対して"Yes."と応答してくれるようになります。

ただ、このAlexaPiだとLocaleの変更ができないみたいなんですよね(APIのバージョンが古いという書き込みを見かけました)。

せっかくなので日本語しゃべらせてみたいなと思ったので、早々に次の手を探しました。

## 2. alexa-avs-sample-app

https://github.com/alexa/alexa-avs-sample-app

多分一番有名なサンプルアプリ。日本語で情報が一番そろっているのはこれかなと。

* サンプルアプリはjavaで作られたウィンドウアプリケーションになっていて、
* 起動時には2枚ほど端末を開いてコマンドを打つ必要があり、
* しかも起動時にブラウザ上からアクティベーションする必要があるので、

　グラフィカル環境で動かしたほうが無難です。

　これを動かした後でもう一つ方法を見つけたので、試してみることにしました。

## 3. avs-device-sdk

https://github.com/alexa/avs-device-sdk

https://developer.amazon.com/ja/alexa-voice-service/sdk

　察するに、Raspberry PiをはじめとするエンベデッドデバイスにAlexaを入れるなら、本命はこっちかもしれません。

　これもRaspberry Pi用のQuick start guideがあって、基本的にはその手順に従うのですが、上記2つよりちょっと手数が増えます。

　Device SDKのサンプルアプリのいいところは、一度『3.3 Obtain a refresh token』でActivationを済ませてしまうと、以降の起動時でもブラウザ起動なく動かせることです(ヘッドレス化が可能)。

『4.Run the sample app』記載のコマンドを実行して、首尾よくいくとこんな画面になります。

<img width="539" alt="DeviceSDKSampleApp.png" src="https://qiita-image-store.s3.amazonaws.com/0/54795/97ff0fa5-0884-a72b-8a75-b95c48b71215.png">

この画面で"c"と入力してリターンすると、言語選択画面が出てきます。

<img width="539" alt="selectlanguage.png" src="https://qiita-image-store.s3.amazonaws.com/0/54795/75ca969b-09b2-112e-3a82-231b93e1b4be.png">

さらにここで"1"と入力してリターンすると、日本語を含む言語を選択可能に。

<img width="538" alt="ja-JP.png" src="https://qiita-image-store.s3.amazonaws.com/0/54795/2a1a07db-7a18-4839-a35e-2f1aeaf925e3.png">

ここで"6"を押してリターンすると、ロケールをja-JPにセットすることができ、以降Alexaは日本語で応答するようになります。

試しに彼女の創造主について聞いてみました(以下動画)。

[![Alexa device SDK in Raspberry Pi3 (locale ja-JP)](http://img.youtube.com/vi/E1HSjD7DMho/0.jpg)](http://www.youtube.com/watch?v=E1HSjD7DMho)

とりあえず日本語で受け答えできるようになりました。

### Alexaの設定

が、この状態でAlexaが返す情報は、なんだかちぐはぐです。

* 天気予報を聞くと、温度を華氏で回答するので、真冬のはずの日本の天気が灼熱地獄として報告されたり、
* 時間を聞いても正確な時刻を答えてくれなかったり

これはデバイスに紐づいている設定に依存していて、以下のサイト(Alexaアプリ)から変更することができるようです。

https://alexa.amazon.co.jp/

<img width="540" alt="端末の設定.png" src="https://qiita-image-store.s3.amazonaws.com/0/54795/2ff6e3e8-e840-44ca-3f03-5b0f5e3462cd.png">

タイムゾーン、温度の単位、デバイスの所在地を設定してやると、天気予報や時間も自然にアナウンスしてくれます。

### マイクの感度

　さて、AVS Device SDKのQuick start guideに従って構築してきた私は、ここで一つの不満にぶち当たりました。

　それは、

* Voice wake upの感度が悪すぎる！

　ということ。

　というのも、Wake up 以降の感度はすこぶる良いのです。部屋の端から質問しても、Alexaはきちんと質問に答えてくれます。
　公式に記載がある通り、Device SDKのVoice Wakeup Detectionは、以下2種類のサードパーティライブラリのうち、いずれかを使ってVoice Wakeupをサポートします。

* Sensory
* Snowboy

　デフォルトではSensoryを使用することになる(Quick start guideでもそうしている)のですが、SensoryではWake upの感度を調整できないらしいのです。

https://github.com/Sensory/alexa-rpi/issues/4

　じゃあもう片方を使ってみようか、ということで、ちょっと試してみました。

### Snowboyの組み込み

　Snowboyを組み込む際にはここにやり方が書いてあるのですが、

https://github.com/alexa/avs-device-sdk/wiki/Build-Options

　12/15時点でいくつかハマる箇所があったので、私は以下の方法を使って組み込みました。

　Sensory組み込み済みの環境を上書きしようとしたら、私の環境ではうまくいかなかったので、一度sdk-folderフォルダを削除したうえで、Quick start guideの手順をカスタマイズする形で、最初からやり直しています。

　あ、最初からやり直す際には、公式にもあるようにAlexaClientSDKConfig.jsonをバックアップしておくと、Activationしなおさなくて済むので楽です。

#### 1.『1.2 Install dependencies』まではそのまま実施する。

　パッケージはもう入っているはずですから、フォルダ掘りとPortAudioのビルドだけのはず。

#### 2.『1.3 Clone the AVS Device SDK and the Sensory wake word engine』で、Sensoryの代わりにSnowboyをダウンロードし、依存パッケージを追加。

```
cd /home/pi/sdk-folder/third-party && git clone git://github.com/Sensory/alexa-rpi.git
cd /home/pi/sdk-folder/third-party/alexa-rpi/bin/ && ./license.
```

　上記2コマンドの代わりに以下を実行して、snowboyのリポジトリ一式をthird-party以下にダウンロードします。

```
cd /home/pi/sdk-folder/third-party && git clone https://github.com/Kitt-AI/snowboy.git
```

公式記述に従って依存パッケージ(libatlas)を忘れずにインストール。

```
sudo apt-get install libatlas-base-dev
```

#### 3.ビルドエラー対策

　12/14時点で試したところ、以下のSnowboy向けcmakeを実行してSampleアプリをビルドしようとすると、ビルドエラーが発生してしまいます。

　2つあってですね。これと、

```
[ 51%] Building CXX object KWD/KittAi/src/CMakeFiles/KITTAI.dir/KittAiKeyWordDetector.cpp.o
/home/pi/sdk-folder/sdk-source/avs-device-sdk/KWD/KittAi/src/KittAiKeyWordDetector.cpp: In constructor ‘alexaClientSDK::kwd::KittAiKeyWordDetector::KittAiKeyWordDetector(std::shared_ptr<alexaClientSDK::avsCommon::utils::sds::SharedDataStream<alexaClientSDK::avsCommon::utils::sds::InProcessSDSTraits> >, alexaClientSDK::avsCommon::utils::AudioFormat, std::unordered_set<std::shared_ptr<alexaClientSDK::avsCommon::sdkInterfaces::KeyWordObserverInterface> >, std::unordered_set<std::shared_ptr<alexaClientSDK::avsCommon::sdkInterfaces::KeyWordDetectorStateObserverInterface> >, const string&, std::vector<alexaClientSDK::kwd::KittAiKeyWordDetector::KittAiConfiguration>, float, bool, std::chrono::milliseconds)’:
/home/pi/sdk-folder/sdk-source/avs-device-sdk/KWD/KittAi/src/KittAiKeyWordDetector.cpp:123:78: error: narrowing conversion of ‘(((std::chrono::duration<long long int, std::ratio<1ll, 1000ll> >::rep)(audioFormat.alexaClientSDK::avsCommon::utils::AudioFormat::sampleRateHz / 1000u)) * msToPushPerIteration.std::chrono::duration<_Rep, _Period>::count<long long int, std::ratio<1ll, 1000ll> >())’ from ‘std::chrono::duration<long long int, std::ratio<1ll, 1000ll> >::rep {aka long long int}’ to ‘size_t {aka unsigned int}’ inside { } [-Werror=narrowing]
 _maxSamplesPerPush{(audioFormat.sampleRateHz / HERTZ_PER_KILOHERTZ) * msToPushPerIteration.count()} {
```

　これ。

```
[ 62%] Linking CXX executable KittAiKeyWordDetectorTest
../src/libKITTAI.so: `snowboy::SnowboyDetect::SetSensitivity(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)' に対する定義されていない参照です
../src/libKITTAI.so: `snowboy::SnowboyDetect::SnowboyDetect(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)' に 対する定義されていない参照です
```

　変数の型変換とコンパイラのバージョンの問題っぽいです。とりあえずこんな感じで適当に変更しました。

```/home/pi/sdk-folder/sdk-source/avs-device-sdk/KWD/KittAi/src/KittAiKeyWordDetector.cpp
std::chrono::milliseconds msToPushPerIteration) :
        AbstractKeywordDetector(keyWordObservers, keyWordDetectorStateObservers),
        m_stream{stream},
        	m_maxSamplesPerPush{(audioFormat.sampleRateHz / HERTZ_PER_KILOHERTZ) * static_cast<unsigned int>(msToPushPerIteration.count())}{

```

　のキャスト" static_cast<unsigned int>"を追加。

```/home/pi/sdk-folder/sdk-source/avs-device-sdk/build/cmake/BuildOptions.cmake
add_definitions (-D_GLIBCXX_USE_CXX11_ABI=0)
```
　を、『# Set up the compiler flags.』の下あたりに追加。

　適切な変更かどうかは知りません。とりあえず動けばいいので、諸々無視して驀進します。

#### 4.オプションを変更して『2. Build the SDK』のcmakeを実行

```
cmake <absolute-path-to-source> -DKITTAI_KEY_WORD_DETECTOR=ON -DKITTAI_KEY_WORD_DETECTOR_LIB_PATH=<absolute-path>/snowboy-1.2.0/lib/libsnowboy-detect.a -DKITTAI_KEY_WORD_DETECTOR_INCLUDE_DIR=<absolute-path>/snowboy-1.2.0/include
```
　公式にはこう書いてあるので、先ほどのsnowboyの位置と合わせつつ、『2. Build the SDK』のcmakeを置き換える形で実行します。こんな感じ。

```
cd /home/pi/sdk-folder/sdk-build && cmake /home/pi/sdk-folder/sdk-source/avs-device-sdk -DKITTAI_KEY_WORD_DETECTOR=ON -DKITTAI_KEY_WORD_DETECTOR_LIB_PATH=/home/pi/sdk-folder/third-party/snowboy/lib/rpi/libsnowboy-detect.a -DKITTAI_KEY_WORD_DETECTOR_INCLUDE_DIR=/home/pi/sdk-folder/third-party/snowboy/include -DGSTREAMER_MEDIA_PLAYER=ON -DPORTAUDIO=ON -DPORTAUDIO_LIB_PATH=/home/pi/sdk-folder/third-party/portaudio/lib/.libs/libportaudio.a -DPORTAUDIO_INCLUDE_DIR=/home/pi/sdk-folder/third-party/portaudio/include
```
#### 5.サンプルアプリをビルド

　後はサンプルアプリをビルドするだけです。が、サンプルアプリ起動時、一つ変えなければならない場所があって。

#### 6.起動時のオプション

```
cd /home/pi/sdk-folder/sdk-build/SampleApp/src && TZ=UTC ./SampleApp /home/pi/sdk-folder/sdk-build/Integration/AlexaClientSDKConfig.json /home/pi/sdk-folder/third-party/alexa-rpi/models
```

　公式手順では起動時オプションがこう書いてありますが、最後の引数がどうやらVoice Wake upのオプションらしく、SensoryとSnowboyでは違うようなのです。
　なので、下記のように実行します。

```
cd /home/pi/sdk-folder/sdk-build/SampleApp/src && TZ=UTC ./SampleApp /home/pi/sdk-folder/sdk-build/Integration/AlexaClientSDKConfig.json /home/pi/sdk-folder/third-party/snowboy/resources/
```

　snowboyのresouucesというフォルダを参照する形になるようです。

　この手順で起動して、AlexaをVoice wakeupし、Alexaが応答してくれたら成功です。

#### 7.感度とボリュームの変更

　さて、肝心の感度とボリュームについてなんですが、以下のソースに値が決め打ちされています。

```/home/pi/sdk-folder/sdk-source/SampleApp/src/SampleApplication.cpp
/// The sensitivity of the Kitt.ai engine.
static const double KITT_AI_SENSITIVITY = 0.60;

/// The audio amplifier level of the Kitt.ai engine.
>static const float KITT_AI_AUDIO_GAIN = 2.0;
```
　KITT_AI_SENSITIVITYでWake upの感受性が、KITT_AI_AUDIO_GAINでボリュームの増幅を操作できるみたいです。

　ここを変更することで、そこそこ遠くからでも"Alexa"の呼びかけに答えてくれるようになりました。

　ただですね。感度の高さ、認識範囲の広さと誤認識率が背中合わせなのです。高感度で遠くから呼びかけられるようにすると、物音や"Alexa"以外の音声に反応してしまうことが増えてきます。スピーカーが近いと、Alexaがスピーカーから流した音声を拾ってしまうことも出てきます。[^2]

　ここは実際の環境に合わせて、バランスを取りつつ調整する必要が出てくるようです。

### ヘッドレス化最後の課題

　ここまで来たらシリアルコンソールさえなくせるはずだよね、と、シリアル接続を切りPCの電源をOFFにして、画面操作なしにAlexaを完全ハンズフリーで使おうとしたんですが、ここで一つ課題が。

* AlexaがVoice Wakeupされているか(質問を聞く状態になっているか)を画面以外から知る手段がない

　のです。

　Alexa Piやalexa-avs-sample-appは英語で話しているときに限って『Yes.』と言ってくれますが、日本語AlexaやDevice SDKのSample Appでは何の応答も返してくれず、画面の"Listening.."だけが頼りなのです。

　Wake upしてないのに質問を話すという徒労も出てきますが、それ以上に、先ほどの設定で感度が上がっていることもあり、誤検知でWake upして周囲の音声を拾ったリすると、Alexaが唐突に独り言をしゃべり始めてしまうのです。心臓に悪い。

　そこで、

* Wake upしたら音で反応する

　ようにしてみようかなと。

### コードの変更

　本来、Alexa自身がオーディオ制御しているはずなので、その方法を流用すれば音声流せるはずだよね、といろいろ調べたんですが、片手間だとちょっと難しそうなので、皆様があっと驚くローテクノロジーで実現することにしました。

```/home/pi/sdk-folder/sdk-source/avs-device-sk/SampleApp/src/UIManager.cpp
        switch (m_dialogState) {
            case DialogUXState::IDLE:
                ConsolePrinter::prettyPrint("Alexa is currently idle!");
                return;
            case DialogUXState::LISTENING:
        		system("aplay /home/pi/sdk-folder/third-party/snowboy/resources/dong.wav 1>/dev/null 2>/dev/null ");
                ConsolePrinter::prettyPrint("Listening...");
```
　
　system関数でaplayを叩いて音を流しているだけです。wavファイルはSnowboyのresourceフォルダにあったファイルですが、お好みのものをどうぞ。

　オーディオ周りで干渉とかしないかな、と心配したんですが、特に問題なく音を鳴らしてくれました。

　これで通常のEcho同様、コンソールなしでの操作が捗りそうです。

### ひとまず満足

　後は電源On時に自動起動するようにしておけば、Echoと同じように完全ハンズフリーでAlexaと対話することができそうです。

　ここからはスキルを拡大して、天気予報や時報以外にも、いろいろ操作できるようにしていきたいところですね。

# おわりに

　自然言語を解するAIアシスタントを手元に持ってくる、というのはなかなかワクワクする体験で、時間を忘れて『Alexa』を連呼していました。

　あと、日本語化した後で、『あえて英語のアシスタントも面白いかも』と思いました。

　個人的にここしばらく英語を訓練しているのですが、Alexaに命令を与えるのも、ちょっとした英作文になりますし、Alexaから流れてくる情報を聞き取るのもリスニングになりますからね。

　Raspberry Pi以外の(特に非Linux端末での)Alexa呼び出しとかについても、機会があれば調べてみたいものです。

[^1]: 最近、日本国内で流通していないNUCを買ったり、ブラックフライデーのセール品(Ryzen)を買ったりすることが多くて。海外から見た日本の市場価値というものを考えさせられてしまいました。

[^2]: 今から0xXX年前、在籍していた大学の研究室で『音声認識によって、離れたPCを遠隔操作する(CDをかける)』という、今から考えると発想が先進的だった研究をしていまして。この時の最大の課題は、『PCから流れる音楽が音声認識の精度を下げる』ということだったのですが、そんな昔の記憶を思い出しました。echoはスピーカーのすぐそばにマイクがある、という音声認識としては厳しい条件のはずで、この点を音響設計としてクリアしているということ自体が、あるいは音声認識そのものと同じぐらい価値のあることなのかもしれません。