腕時計に向かって「雨の音を流して」と話しかけても、何も起きませんでした。その静けさを私は長いあいだ、聞き取りの精度の問題だと思い込んでおりました。
手元のログを追い直したのは、9月4日という日付が近づいてからです。発話はきちんと届いており、アプリ側も呼ばれておりました。ただ、一致する音源が見つからなかったときの分岐が、ログを1行書いて何も返さずに終わっていたのです。
画面のある端末なら、これでも致命傷にはなりません。利用者は画面を見て、何も起きていないことを目で確認できます。手が塞がっている場所では、そうはいきませんでした。
9月4日に入れ替わるのは、聞き取る側です
9月4日から、Android のスマートフォンとタブレット、Wear OS のウォッチ、そして Android Auto の車載環境で、Google アシスタントの提供終了が始まります。後継は Gemini です。
押さえておきたいのは、これが段階的なロールアウトだという点です。全ての利用者に届くまでには数週間かかり、切り替わったあとでアシスタントへ戻す手段は用意されておりません。前後を比べたいなら、比較の材料は今のうちにしか取れないことになります。
そしてもう一点。アプリを作っている側から見ると、入れ替わるのは発話を解釈する側であって、アプリが受け取る入口そのものではありません。メディア再生を音声から頼まれる経路は、これまでどおりメディアセッションのコールバックに届きます。
変わるのは、そこへ渡ってくる文字列の姿です。同じ「雨の音」でも、言い回しの整え方が変われば、こちらの検索処理が空振りする確率も変わります。空振りしたときにどう振る舞うかを決めていないアプリほど、切り替わった直後に沈黙する回数が増えるのだと思います。
画面のない場所では、失敗の出口が1つしかありません
スマートフォンでの実装を振り返ると、私は失敗をトーストやスナックバーで伝えておりました。見つからないときに短い文言を1つ出す、という作りです。
Wear OS と Android Auto では、この出口が使えません。運転中の車載画面には任意のトーストを出せませんし、腕時計の小さな面に文字を出しても、そもそも見ていない前提の操作です。
代わりに用意されているのが、メディアセッションの再生状態そのものです。PlaybackStateCompat に STATE_ERROR とメッセージを載せて返すと、Android Auto と Wear OS のメディア UI がそれを受け取り、利用者に見える形・聞こえる形へ変換してくれます。
つまり、こちらが「表示する」のではありません。応答は画面に描くものではなく、セッションの状態として返すものです。 この線引きに気づくまで、私は失敗の分岐にログしか置いておりませんでした。
エラーを「見せる」から「返す」へ書き換える
書き換えたのは、音声からの検索を受けるコールバックです。もともとは一致する音源が無いときに return するだけでしたが、状態を返す形に変えました。
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
if (query.isNullOrBlank()) {
// 「何か流して」に相当します。詳細は次の節で扱います。
playFallback()
return
}
val track = library.findBest(query)
if (track == null) {
session.setPlaybackState(
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_ERROR, 0L, 0f)
.setErrorMessage(
PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED,
"「$query」に近い音源が見つかりませんでした"
)
.build()
)
return
}
play(track)
}ここで大事なのは、メッセージに利用者の言葉を戻していることです。「見つかりませんでした」だけでは、何が聞き取られたのかが分かりません。聞き取られた文字列を含めておくと、言い直すべきなのか、そもそも音源を持っていないのかを、利用者自身が判断できます。
先に確かめておきたいのが、そもそもこのコールバックへ届くかどうかです。再生状態の setActions に PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH を含めていないと、音声からの検索は自分のアプリへ渡ってきません。エラーの返し方を整えても、入口が閉じていれば1行も動かないままです。ここを見落としたまま「反応しない」と悩む時間は、私の場合いちばん長く続きました。
車も腕時計も手元に無いという場合でも、確認の手段はあります。Android Auto は Desktop Head Unit を使えば開発機の画面上で動かせますし、Wear OS はエミュレータをスマートフォンとペアリングして音声からの再生を試せます。実機と完全に同じにはなりませんが、失敗が無音で終わっていないかを見るには足ります。
エラーコードは用途で選び分けます。手元では、音源が無い場合に ERROR_CODE_NOT_SUPPORTED、通信に失敗した場合に ERROR_CODE_APP_ERROR を返す形に整えました。
もう一段あるのが、利用者に解決手段を渡す場合です。サインインの期限切れのように、こちらでは直せない失敗があります。この種類は、メッセージと一緒に解決用の操作を添えられます。
val resolution = Bundle().apply {
putString(
"android.media.extras.ERROR_RESOLUTION_ACTION_LABEL",
"サインインする"
)
putParcelable(
"android.media.extras.ERROR_RESOLUTION_ACTION_INTENT",
signInPendingIntent
)
}
session.setPlaybackState(
PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_ERROR, 0L, 0f)
.setErrorMessage(
PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED,
"サインインの有効期限が切れています"
)
.setExtras(resolution)
.build()
)キー名は androidx.media.utils.MediaConstants にも定数として用意されておりますので、直書きが気になる場合はそちらを参照してください。文字列そのものを載せたのは、どのキーが効いているのかを追いやすくするためです。
空のクエリは、失敗ではありません
書き換えの途中でいちばん考え込んだのが、クエリが空で届く場合でした。
これは聞き取りの失敗ではありません。「何か流して」に相当する依頼です。ここでエラーを返すと、利用者からは「頼んだのに断られた」という体験になります。
そこで、直前に再生していた音源、それも無ければ既定のプレイリストの先頭、という順で必ず何かを鳴らす形にしました。判断の材料を持っていないときは、選ばせるのではなく、こちらで選んで鳴らします——画面を見られない場面では、選択肢を提示すること自体が負担になるためです。
一方で、空でないのに一致しないときは、素直に返せない旨を返します。この2つを同じ分岐にまとめてしまうと、どちらの体験も中途半端になりました。
切り替わる前に決めておく3つ
残りの数週間で決めておくと楽になることを、3つに絞って表にしました。
| 決めること | 具体的な作業 | 後回しにしたときの症状 |
|---|---|---|
| 失敗の返し方 | 音声から呼ばれる分岐を洗い出し、ログのみで終わる箇所を STATE_ERROR に置き換える | 切り替え直後に「反応しない」という報告だけが増え、原因を絞り込めません |
| 空クエリの既定動作 | 直前の再生・既定プレイリストのどちらを返すかを決め、両方無い場合の音源も用意する | 依頼したのに無音で終わり、アプリ自体が壊れていると受け取られます |
| 比較のための記録 | 音声から開かれた再生の件数と、一致しなかった件数を、切り替わる前から数え始める | 戻せない移行のため、あとから前の状態を測り直せません |
3つ目だけは、実装というより準備の話です。切り替わったあとで「以前より悪くなった気がする」と感じても、比べる相手が残っていなければ判断できません。数え始めるのは、今週のうちが最後の機会になります。
音声から開かれた起動を見分けて記録する具体的な手順は、Google アシスタントの置き換えは戻せないので、基準線は今しか取れませんにまとめております。アプリ側にアシスタント前提の記述がどれだけ残っているかを洗い出す作業は、アシスタント前提の記述が、自分のアプリに何箇所残っているかが入口になります。
次の一手
まずは、音声から呼ばれるコールバックの中で return だけで終わっている箇所を1つ探して、そこに STATE_ERROR とメッセージを足してみてください。1箇所で構いません。
私もそこから始めました。腕時計に話しかけて、短い一言が返ってきた瞬間に、これまで何度も無音で終わらせていたのだと分かりました。お読みいただきありがとうございました。