体調を記録するアプリを作っている。画面に質問が出て、声か文字で答えると、その日の記録が残る。声はブラウザの音声認識(Web Speech API)で文字にしている。
iPhone の Safari で、これが 2 問目から動かなくなった。1 問目は答えられる。2 問目でマイクを押すと、開始の音(ポロン)は鳴るのに、話しても何も入らない。画面には「聞き取れませんでした」とだけ出る。
見立てで直して、外し続けた
最初は「同じ認識器を使い回しているから、2 回目の start() を Safari が拒んでいる」と考えた。Playwright の WebKit で start() を 2 回呼ぶと、2 回目で InvalidStateError が出たからだ。押すたびに認識器を作り直す形に直した。
直らなかった。後で分かったが、この確かめ方が間違っていた。start() を同期で 2 回続けて呼んだだけで、1 回目がまだ開始中だった。開始中にもう一度呼べばエラーになるのは仕様どおりで、「終わった後にもう一度」は測っていなかった。測り直すと、WebKit は終わった認識器の再 start() を普通に受け付けた。
実機で測る前に入れた修正は、4 回とも外れた。PC の WebKit では、iPhone の音声まわりが再現できないからだ。
実機で、時刻つきで測った
見立てをやめて、使い方のページの中に自己診断を作った。マイクを押すたびに、何がいつ起きたかを 1 行ずつ並べる。
1 回目: start() 0.0秒 → 開いた 0.9秒 → 音を拾い始めた 1.4秒 → 聞こえた 2.4秒 → 終わった「はは」
2 回目: start() 0.0秒 → 開いた 0.0秒 → 音を拾い始めた 0.1秒 → 指を離した 2.5秒 → エラー aborted「No speech detected」
分かったことは 2 つあった。
- 失敗の理由は
No speech detected。話しているのに無音と判定されている。符号はabortedで、こちらが止めたときと同じ符号なので、アプリはこれを黙って捨てていた - 聞き取れる回は、開いてから音を拾い始めるまで 0.5〜1 秒かかる。失敗する回は、どちらもほぼ 0 秒で来る
マイクが実際には音を取り込まないまま、認識だけが動いているように見えた。
効かなかったこと
次の手はどれも効かなかった。
- 押すたびに
getUserMediaでマイクを開き直す → 1 回目から失敗するようになった - マイクを開いたままにする → 同じく 1 回目から失敗した
- 押すたびに新しい iframe を作り、その中で聞く → 変わらない
- 一度開いた認識を閉じずに使い続ける → 2 回目は入った。ただ、質問を読み上げる(
speechSynthesis)と、その後は音が届かなくなった
手で再読み込みすると 1 回目が動くので、「質問ごとにページを読み直す」形も入れた。これも実機では動かなかった。
効いていたのは時間だった
読み直さずに 1 分待って押すと、2 回目も普通に入った。読み直して動いていたのは、読み直したからではなく、その間に時間が経っていたからだった。
前の声が終わってから次に押すまでの秒数を記録して、並べた。
| 前の声から | 結果 |
|---|---|
| 32.4 秒 | 失敗 |
| 33.2 秒 | 失敗 |
| 34.0 秒 | 成功 |
| 35.8 秒 | 成功 |
境目は 34 秒前後だった。失敗した回も「終わった認識」として数え直しになる。1 台の iPhone で測った値なので、機種や iOS の版で違うかもしれない。
iPhone はキーボードの音声入力に任せた
40 秒待たせれば動いた。でも、答えるたびに 40 秒待たされるのは使いものにならない。録音してサーバで文字にすれば待ちは消えるが、費用がかかり、声が端末の外に出る。
iPhone では、アプリのマイクをやめて、キーボードのマイク(OS の音声入力)で話してもらうことにした。こちらには待ち時間が無い。答えの欄に触れるとキーボードが開き、最初の数回だけマイクの場所を案内する。PC と Android は、今までどおり押している間だけ聞く。
残したもの
- 声や画面を変えたら、iPhone の実機で練習用のチェックインを一通り通す。AI も記録も使わない偽物の応答で、何度でも試せる
- 利用者の端末で起きた失敗を数えて、自分宛てにメールで知らせる。今回の不具合は、おそらく iPhone で使っていた人の多くに起きていたが、報告が 1 件来るまで気づけなかった
いちばん高くついたのは、PC 上の確認で「直った」と判断したことだった。実機で測ってから直す、を順番どおりにやればよかった。