技術 約3分で読めます

iOSでWebSpeech APIを安定させる方法

AIと喋れる環境を作る(3)ついに喋れた編で作った音声チャットをウェブに移植しようとした。PC上では快適に動いていたが、iPhoneから話しかけると途切れ途切れに入力されたり、全く反応しなくなる問題が発生。同じApple製品でもmacOSのSafariでは問題ないのに、iOSだけおかしい。「勝手に止まる」「バッファが詰まる」「初回認識しない」など、iOSのWebSpeech APIは問題が多い。

Whisper API等の有料サービスを使えば解決するが、無料で対応できる現実的な対策をまとめた。

基本方針

  • インスタンスはシングルトン - 毎回newしない(ぴこんぴこん音対策)
  • Push-to-Talk方式 - continuous自動再開より安定
  • マイクを事前に温める - 初回認識失敗対策

実装例

// シングルトンで生成(ページ読み込み時に1回だけ)
const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
recognition.lang = 'ja-JP';
recognition.interimResults = true;
recognition.continuous = true;

const btn = document.getElementById('micBtn');

// Push-to-Talk
btn.addEventListener('touchstart', (e) => {
  e.preventDefault();
  recognition.start();
});

btn.addEventListener('touchend', () => {
  recognition.stop();
});

// ボタン外に指が出た時も止める
btn.addEventListener('touchcancel', () => recognition.stop());

// PC対応
btn.addEventListener('mousedown', () => recognition.start());
btn.addEventListener('mouseup', () => recognition.stop());
btn.addEventListener('mouseleave', () => recognition.stop());

// 結果処理
recognition.onresult = (event) => {
  for (let i = event.resultIndex; i < event.results.length; ++i) {
    if (event.results[i].isFinal) {
      const text = event.results[i][0].transcript;
      console.log('認識結果:', text);
      // ここでUIに反映
    }
  }
};

recognition.onerror = (event) => {
  console.warn('エラー:', event.error);
};

初回認識失敗対策

マイクを事前に温める

async function warmupMic() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    stream.getTracks().forEach(track => track.stop());
  } catch (e) {
    console.warn('マイク許可が必要です');
  }
}

// 初回ユーザージェスチャーで呼ぶ
document.body.addEventListener('click', () => {
  warmupMic();
}, { once: true });

AudioContextのunlock

function unlockAudio() {
  const ctx = new (window.AudioContext || window.webkitAudioContext)();
  const buf = ctx.createBuffer(1, 1, 22050);
  const src = ctx.createBufferSource();
  src.buffer = buf;
  src.connect(ctx.destination);
  src.start(0);
  ctx.resume();
}

空のrecognitionを先に回す

function preloadRecognition() {
  recognition.start();
  setTimeout(() => recognition.stop(), 100);
}

「話していいよ」の視覚フィードバック

マイク起動に時間がかかるため、ユーザーに待ちを伝える。

btn.addEventListener('touchstart', async (e) => {
  e.preventDefault();
  recognition.start();
  await new Promise(r => setTimeout(r, 300));
  btn.classList.add('ready'); // ここで「話していいよ」表示
});

btn.addEventListener('touchend', () => {
  recognition.stop();
  btn.classList.remove('ready');
});

continuous: true vs false

方式メリットデメリット
continuous: false + 自動再開iOS安定しやすい再開時に一瞬途切れる
continuous: true + シングルトン途切れにくい、音少ないiOSでバッファ詰まりリスク

Push-to-Talk方式なら continuous: true でOK。自動で聞き続けたい場合は以下のハイブリッド:

const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
recognition.continuous = !isIOS;

let shouldBeListening = false;

recognition.onend = () => {
  if (isIOS && shouldBeListening) {
    setTimeout(() => recognition.start(), 200);
  }
};

バックグラウンド対策

画面が裏に行くと死ぬので対策。

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    recognition.stop();
  }
});

window.addEventListener('focus', () => {
  // 必要なら再開処理
});

結論

  • 完璧は無理 - iOSのWebSpeech APIはそういうもの
  • Push-to-Talk + シングルトン + マイク事前warm-up が現実解
  • プロダクションで使うなら Whisper API 等の有料サービスを検討