体調を記録するアプリを作っている。画面から質問が出て、それに声か文字で答えると、その日の記録が残る。答えを文字に起こす音声入力と、質問を読み上げる音声合成の両方を持っている。
その両方を作り直した。
止めるまで聞く形をやめた
もとの音声入力は、一度マイクを開いたら利用者が止めるまで聞き続ける作りだった。
ただしブラウザの音声認識は、黙ると一区切りで終わってしまう。それを隠すために、裏で開き直しを繰り返していた。利用者からは、いま本当に聞いているのかが分からない。開き直しが空回りすることもあった。それを抑えるために「無音が 60 秒続いたら止める」「開き直して何も聞き取れないのが 3 回続いたら諦める」といったタイマーとカウンタを組んでいた。
直した形はこうなった。マイクは押している間だけ聞き、離した時点で答えを確定する。 読み上げが終わったあとの自動起動もやめた。
指の位置と「聞いているかどうか」が一致するので、開き直しを裏でやる必要も、それを抑えるためのタイマーやカウンタも要らなくなり、まとめて外した。
失敗の種類ごとに、やり直す導線も分けた。一文字も聞き取れなかったときは「もう一度押して話してください」と出す。聞き取りが違ったときは「消して言い直す」ボタンで答えを空にしてから話し直せる。マイク自体が使えなかったときはエラーを出しつつボタン自体は残す。権限を直せば、その場でもう一度試せるようにするためだ。
ここまでを実装してテストも通した。そのあと Chrome で動かしたら、三つ続けてハマった。
一つ目:最初の getVoices() は必ず空
Chrome の getVoices() は、最初に呼ぶと必ず空の配列を返す。
拡張音声を選ぶ処理がここに依存していたので、最初の質問だけ既定の声で読まれていた。二問目からは正しい声になるので、気づきにくい。
voiceschanged イベントを待ってから選び直すようにした。Chrome でいちばん自然に聞こえる「Google 日本語」も優先順位に加えた。
二つ目:許可ダイアログの裏で、指はもう離れている
Chrome はマイクを初めて使うとき、許可ダイアログを出す。
ダイアログが出ている間に指を離すと、その時点ではまだ認識が始まっていないので stop() が空振りする。そして利用者が許可した瞬間に認識が開き、指はもう離れているのに聞き続ける。
開いた時点で既に離されていたら、その場で閉じるようにした。
三つ目:8 秒黙ると勝手に切れる
Chrome は 8 秒ほど黙ると、勝手に区切って認識を終える。
押している間に言葉を探して黙ると、そこで切れてしまっていた。押している間に限って裏で開き直し、それまでの文をつなぐようにした。指を離せば必ず止まるので、以前のような際限のない空回りにはならない。
仕様書には書いていなかった
「押している間だけ」という単純なルールに変えたのに、実際に確かめるまでブラウザごとの違いは見えなかった。
三つとも、仕様書ではなく、指を置いて試したところに出てきた。