LoginSignup
20
3

More than 5 years have passed since last update.

Retty音声検索をやってみた

Posted at

この記事は Retty Advent Calendar 2日目です。
昨日はすずたく(@takumi-suzuki)のMySQLのlogを可視化するでした。

さて、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。便利!!

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

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

    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を生成しましょう。

    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を叩いて結果を表示します。

         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ではユーザさんを呼び捨てにせず「さん付け」で呼ぶ文化があります。 

20
3
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
3