Retty音声検索をやってみた

この記事は **Retty** Advent Calendar 2日目です。
昨日はすずたく(@takumi-suzuki)の[MySQLのlogを可視化する](http://qiita.com/takumi-suzuki/items/e88db63a1bfe4295d580)でした。


さて、Androidをお使いの皆さん、 "OK Google!" を使って音声検索してますか？
あれ、自分のアプリでやってみたいと思いますよね。というわけでやってみました٩( 'ω' )و

# その前に注意事項
AndroidのVoice Interact APIはAPI Level 23からしかサポートされていません。
RettyのAndroidアプリはもちろんもっと低いAPI Versionからサポートしています。
今回はTarget SDK versionを24に設定したサンプルアプリを作っています。
また、VoiceInteraction自体が割と不安定で、端末のGoogle Voice Typing設定が英語にしており、
かつ音声入力が英語の場合のみしかうまく動いてくれませんでした。。。

では、やっていきます٩( 'ω' )و

# やりたいこと

1. "OK Google"で発話された検索語を拾ってアプリを開く
    * （例）"OK Google, Rettyで検索"とか言うとRettyの音声検索画面を開く
2. 開いたアプリで音声入力を待ち受ける
3. 音声入力をキーワードにRettyの検索APIを叩いて結果表示

1は残念ながらAndroid API Lebel 24の時点でサポートされていません(´；ω；｀)
代わりに手動で音声検索画面を開くことにします。

# 実装方法

では2, 3について実装していきます


## 音声入力を待ち受ける

API Level 23までは、VoiceIntractionモードに入るには 
`android.intent.category.VOICE` のcategoryが設定されたIntentで起動される必要があったと記憶していますが、
APU Level 24で `Activtiy#startLocalVoiceInteraction()`が追加されたので、任意のタイミングでVoiceInteractionモードに設定することができます。
例えば画面を開いたタイミングでVoiceInteractionモードに入るのであればこの1行でOKです[^1]。便利！！

```kotlin
    override fun onResume() {
        super.onResume()
        this.startLocalVoiceInteraction(Bundle())
    }
```


では続いて音声入力を待ち受けます。
VoiceInteractionに入ったら`Activity#onLocalVoiceInteractionStarted()`が呼ばれるのでここで音声待ち受けを開始しましょう。

```kotlin
    override fun onLocalVoiceInteractionStarted() {
        super.onLocalVoiceInteractionStarted()
        val request = object: VoiceInteractor.PickOptionRequest(VoiceInteractor.Prompt("Say what you eat？"),
                                                                 this.getOptions(),
                                                                 Bundle()) {
            override fun onPickOptionResult(finished: Boolean,
                                            selections: Array<out Option>?,
                                            result: Bundle?) {
                super.onPickOptionResult(finished, selections, result)
                val voiceInputWord = selections?.let { it[0].label.toString() }
                this@VoiceIneractActivity.seachRestaurant(voiceInputWord)
            }
        }
        this.voiceInteractor.submitRequest(request)
    }
```

待ち受け状態に移行するためにVoiceInteractor#submitRequest()をコールする必要がありますが、API level 24で使えるRequestは下記の5種類。
残念ながらユーザさん[^2]が音声入力した任意のワードを取得するRequestはありません。

| Request    | Description|
|:-----------------|------------------|
| AbortVoiceRequest|Reports that the current interaction can not be complete with voice|
| CommandRequest   |Execute a vendor-specific command using the trusted system VoiceInteractionService|
| CompleteVoiceRequest|Reports that the current interaction was successfully completed with voice|
| ConfirmationRequest|Confirms an operation with the user via the trusted system VoiceInteractionService|
| PickOptionRequest|Select a single option from multiple potential options with the user via the trusted system VoiceInteractionService|

今回は一番下のPickOptionRequestを使います。
PickOptionRequestは選択可能なoptionをあらかじめ規定していて、その中のものと一致した場合のみコールバックが返ってくるRequestです。
（一致しない限りユーザさんの音声入力内容を取ることはできません。）
Rettyは料理のカテゴリーやお店のエリアなどの一覧を持っています。今回は料理のカテゴリーをoptionにもたせます。

PickOptionRequestのコンストラクタの引数は3つあります。

一つ目はPrompt. こちらはユーザさんに音声入力を促す際にシステムが読み上げてくれる文字列を指定することができます。

二つ目はOption. 料理カテゴリーのarrayからoptionを生成しましょう。

```kotlin
    private fun getOptions(): Array<VoiceInteractor.PickOptionRequest.Option> =
            this.getSearchCategoryWords().mapIndexed { index, string ->
                VoiceInteractor.PickOptionRequest.Option(string, index)
            }.toTypedArray()

    private fun getSearchCategoryWords(): Array<String> {
        // 内部からデータを引っ張ってきて["pizza", "steak"　, ...]のようなarrayを返すメソッド
    }

```

三つ目のbundleにはadditional informationを設定することができます。今回は特に必要ないので空のbundleを設定しています。

さて、これでユーザさんの音声を待ちうける状態ができました。

## 音声入力をキーワードにRettyの検索APIを叩いて結果表示

まず、ユーザさんの音声入力を取り出しましょう。
optionにもたせた文字列とユーザさんの音声入力が一致したら`PickOptionRequest#onPickOptionResult()`が呼ばれます。
ここの第2引数のselectionsに一致した文字列が入ってくるので取り出すことができます。
(nullableなのに注意してください)
その文字列を以って検索APIを叩いて結果を表示します。

```kotlin
         val request = object: VoiceInteractor.PickOptionRequest(VoiceInteractor.Prompt(""),
                                                                 this.getOptions(),
                                                                 Bundle()) {
            override fun onPickOptionResult(finished: Boolean,
                                            selections: Array<out Option>?,
                                            result: Bundle?) {
                super.onPickOptionResult(finished, selections, result)
                val voiceInputWord = selections?.let { it[0].label.toString() }
                this@VoiceIneractActivity.seachRestaurant(voiceInputWord)
            }
        }
```
```
    private fun seachRestaurant(keyWord: String?) {
        // do search and show result
    }
```

残念ながらRettyのAPIは公開していないので続きを書くことはできませんが、これで音声検索ができました！！ :tada: ٩( 'ω' )و :tada:

# まとめ

いかがでしたでしょうか？
音声入力あまり日本で流行ってない印象もありますが、新しい検索体験として可能性と夢が広がりますね！！
もっと使いやすいRequestクラスが増えると使用シーンが増えると思うので、Googleに期待したいところです。



[^1]: コード例のようにRettyでは部分的にKotlinを導入しています。もともとJavaで書かれたコードを生かしつつ、新しく追加するファイルについてはJavaでもKotlinでもどちらでもOKというゆるいルールにしています。今ではKotlinの比率がどんどん上がっています。Kotlinかわいい。
[^2]: Rettyではユーザさんを呼び捨てにせず「さん付け」で呼ぶ文化があります。