Electronを使って音声認識でプラレールを操作しよう

# やりたいこと
[Mabeee](http://mabeee.mobi)というスマホからBluetoothで操作できるIoT乾電池があります。標準で提供されるスマホアプリからは、声の大きさやスマホの加速度に乾電池を反応させることができます。また、正式サポート対象外ではありますが、MabeeeをMacから操作可能にするアプリ「[MaBeeeMacApp](https://github.com/novars-jp/MaBeeeMacApp)」も提供してくれています。せっかくなので、このアプリをつかって、標準のスマホアプリにはついていなかった「音声認識で乾電池を操作」というのをやってみたいと思います。これができれば、声でプラレールを操作することもできるようになるはず。
![BingSpeech.png](https://qiita-image-store.s3.amazonaws.com/0/160451/14bccc3e-1882-4185-a67e-d04df2ea4712.png)



# Bing Speechの利用
音声認識にはBing Speechを利用します。音声認識ならばWatsonがイイよ！と聞いたはずなのですが、うっかりBing Speechを使ってしまいました。WebSocketとか対応しておらず応用は限られるかと思いますが、短いセンテンスを認識するだけならば簡単にできたので、そう悪くはないと思います。
Bing Speechの登録方法は下記URLを参考にしました。

* [MicrosoftのCognitive Serviceの一つ、Bing Speech APIを使う手順をまとめました](http://magicbullet.hatenablog.jp/entry/CognitiveService_BingSpeechAPI)

上記に従ってKEYをゲットしたら、JavaScript向け[SDK](https://github.com/Microsoft/Cognitive-Speech-STT-JavaScript)をダウンロードします。この中に含まれる`speech.1.0.0.js`を読み込むことで、Bing SpeechのREST-APIを簡単に叩けるようになるようです。‥が、そのままではElectronから使えません。Electron経由で呼び出すとhttpを使うかどうかの判定箇所でhttpを使わないと判定してしまうことが原因みたいです。下記のように、とにかくtrueを返すようにuseHttp()を書き換えてしまいましょう。これで、Electronからもエラーを吐かずに使えるようになりました。

```js:speech.1.0.0.js
   function useHttp() {
        // return getValue("useHttp");
        return true;
    }
```

Bing Speechの[API Reference](https://www.microsoft.com/cognitive-services/en-us/Speech-api/documentation/GetStarted/GetStartedJS)ではJavaScriptライブラリの使い方は書いていないようなので、サンプルコード（先のGitリポジトリ内のindex.html）を参考にします。だいたい以下のような感じで使えそうです。

```js:app.js
function getMode() {
    // 認識モードを選択
    return Microsoft.CognitiveServices.SpeechRecognition.SpeechRecognitionMode.shortPhrase;
}

// 接続クライアント生成
var client = Microsoft.CognitiveServices.SpeechRecognition.SpeechRecognitionServiceFactory.createMicrophoneClient(getMode(), 'ja-jp', 'APIキー');

// 音声認識開始
client.startMicAndRecognition();

client.onFinalResponseReceived = function (response) {
    // 認識結果(response)に対する処理
}

// 音声認識終了(５秒後に実行）
setTimeout(client.endMicAndRecognition(), 5000);
```

クライアント生成は何度も呼ぶとエラーになったので、一度生成して使い回すのが良いようです。onFinalResponseReceived()に渡される引数responseのフォーマットは以下の通りです。

```json
[
    {
        "lexical": "こんにちは",
        "display": "こんにちは",
        "inverseNormalization": null,
        "maskedInverseNormalization": null,
        "transcript": "こんにちは",
        "confidence": 0.9468275
    }
]
```

# MaBeeeMacApp
## 起動
Githubで公開されているので、[ダウンロード](https://github.com/novars-jp/MaBeeeMacApp/archive/master.zip)してきます。
解凍、Mabeeeをダブルクリックするだけで簡単に起動できます。メニューバーに乾電池マークが表示され、`localhost:11111`にブラウザでアクセスして、`{"state":"Unknown","scan":false,"devices":[]}`とか返ってくれば成功です。

## 使い方
MaBeeeMacAppはREST-APIを提供しており、それを叩くとBluetoothを通じてMaBeee(乾電池)に命令をだしてくれます。APIの使い方はGitリポジトリのREADME/Wikiに丁寧な説明があります。また、[ツアー](https://github.com/novars-jp/MaBeeeMacApp/wiki/ツアー)もあるので、つまづくことはないでしょう。
ただ、私はMaBeeeに通電を忘れていてつまづきました。。MaBeeeに電池をセットしたら、プラレールなどに装着し、スイッチをONにして通電しましょう。
REST-APIでは、電池のON/OFFだけでなく、電源のON/OFFを小刻みに変えてモーターなどの回転数を制御するPWM(Pulese Width Modulation)を指定できます。

## Electronから使う
ツアーをみればわかりますがREST-APIを順に叩いて操作するので、RESTの応答を待ってから次の処理に移る同期処理をたくさん書く必要があります。async/awaitを使おうと思いましたが、エラーがでてうまくいかなかったのでおとなしくpromiesを使いました。
### 非同期処理のリトライ
MaBeeeとの接続が完了するまで待つ処理など「接続を試みて一定期間後に接続状況を確認し、未接続ならばまたしばらく待って確認する」という非同期処理のリトライを実装する必要があります。例えば、MaBeeeの存在をscanしてからデバイスが検出されるまで待つ、という処理は下記のように書きました。アンチパターンに出てきそうな書き方なので、良い方法があれば教えてください。

```js:app.js
axios.get('http://localhost:11111/scan/start')
.then((response) => {
    var retry_cntr = 0;
    return new Promise(function loop(resolve, reject) {
        return new Promise((_resolve, _reject) => {
            axios.get('http://localhost:11111/devices').then((_response)=> {
                console.log('devices :' + JSON.stringify(_response.data, null, '   '));
                if (!_response.data.device) {
                    // 未検出ならば
                    // リトライが３回失敗したら諦める
                    if (retry_cntr++ > 3) {
                        _reject(null);
                    }   
                    // 2秒後にリトライ
                    setTimeout(()=> { _resolve(true); }, 2000);
                } else {
                    // 検出されたならば
                    _reject(!_response.data.devices[0]);
                }   
            }); 
        }).then(loop.bind(null, resolve, reject),
            (isFound) => {
            if (isFound) {
                resolve();
            } else {
                reject();
            }   
        }); 
    }); 
}).then((device)=> {
    /* 検出後の処理 */
    console.log('Deviceが検出されました');
}).catch(()=> {
    /* 検出できなかった場合の処理 */
    console.log('Deviceは検出されませんでした');
}); 
```

# できあがり
MaBeeeMacAppのAPIを叩けるようになったら、Bing Speechの応答を[tiny-segmenter](https://www.npmjs.com/package/tiny-segmenter)で分かち書きし、含まれるキーワードに応じてMaBeeMacAppのAPIを叩きます。
今回は、すすめ(pwm:60)、はやく(pwm:100)、ゆっくり(pwm:40)、とまれ(pwm:0)を登録しておき、分かち書きの結果に応じてAPIを叩くようにしています。

<blockquote class="twitter-video" data-lang="en"><p lang="ja" dir="ltr">Qiita埋め込み用 <a href="https://t.co/gUehEifJz4">pic.twitter.com/gUehEifJz4</a></p>&mdash; saga (@vimyum) <a href="https://twitter.com/vimyum/status/841299468351754240">March 13, 2017</a></blockquote> <script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

ソースコードは[GitHub](https://github.com/vimyum/MaBeeeVoiceOperator)にあげてあります。MaBeeeMacAppを起動したのち、`electron .`で起動、Bing SpeechのKeyをフォーム入力すればOKです。