🎤

Web Speech API(ウェブ音声API)を使ってJavaScriptだけで音声検索を実装する

に公開

はじめに

Chrome139 から Web Speech API(ウェブ音声API) が使えるようになりました。
これにより、音声認識と読み上げがブラウザ上でできるようになりました。
今回は、これを利用して音声検索をJavaScriptだけで実装してみます。
申し訳ありませんが、Chrome以外での動作検証をしていないのでおそらくほかのブラウザでは動かないと思われます。

目標

マイクボタンを押すと音声入力モードになります。JavaScriptのbooksにある本の範囲で検索をします。
※CodePen上で動かないようです。悲しい。

実装

HTML

<h1>名作検索</h1>
<button type="button" id="searchButton">
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
    <path d="M5 3a3 3 0 0 1 6 0v5a3 3 0 0 1-6 0z" />
    <path d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5" />
  </svg>
</button>

<table>
  <thead>
    <tr>
      <th>タイトル</th>
      <th>著者</th>
    </tr>
  </thead>
  <tbody id="bookList"></tbody>
</table>

<div>
  <p id="output">
    <em>音声認識の結果</em>
  </p>
</div>

マイクのSVGはBootstrap Iconsを使わせてもらっています。

JavaScript

MDNのウェブ音声APIの説明を参考に作っていきます。

SpeechRecognitionのインスタンスを作成

SpeechRecognitionを使ってインスタンスを作成します。

const recognition = new SpeechRecognition();

音声認識に対する設定

次に、インスタンスに何個かの設定を入れます。

const recognition = new SpeechRecognition();

+ // 音声認識の設定
+ recognition.continuous = false;
+ recognition.lang = "ja-JP";
+ recognition.interimResults = false;
+ recognition.maxAlternatives = 1;

各設定項目は以下の通りです。

recognition.lang
言語の設定で、規定はHTMLのlang属性の値になります。念のため設定をしておくといいとよいでしょう。
日本語はja-JPです。

recognition.continuous
MDNでは「各認識の継続的な結果を返すか、単一の認識結果だけを返すかを制御」と書いてあります。
trueにすると文が切れても音声入力の受付が続くようになりました。
デフォルトはfalseで、一回、音声入力の受付が終了します。

recognition.interimResults
これをtrueにすると、「中間的な結果」を返します。
具体的に言うと、「夏目漱石」と音声入力をすると、「夏」「夏目」「夏目漱」「夏目漱石」と結果が返ってきます。
最終の結果だけで十分なので今回はfalseになっています。
デフォルトもfalseです。

recognition.maxAlternatives
候補を何個まで出すか設定できる値です。
この値を「2」にして試したところ、
「ごめん」という発音に対し、「ごめん」と「お面」が返ってきていました。

音声認識部分の実装

const recognition = new SpeechRecognition();

// 音声認識の設定
recognition.continuous = false;
recognition.lang = "ja-JP";
recognition.interimResults = false;
recognition.maxAlternatives = 1;

+ document.addEventListener('DOMContentLoaded', () => {
+   // Dom取得
+   const diagnostic = document.getElementById("output");
+   const searchButton = document.getElementById('searchButton');
+   const bookList = document.getElementById('bookList');
+ 
+   // ここに音声認識のコードを書く
+ });

DOMContentLoaded
本題ではないのでMDNの説明を参考にしてください。
簡単に言うと、JavaScriptやHTMLなどの準備が完了したら動き出します。

次に、音声認識部分を追加します。

const recognition = new SpeechRecognition();

// 音声認識の設定
recognition.continuous = false;
recognition.lang = "ja-JP";
recognition.interimResults = false;
recognition.maxAlternatives = 1;

document.addEventListener('DOMContentLoaded', () => {
// DOM取得
const diagnostic = document.getElementById("output");
const searchButton = document.getElementById('searchButton');
const bookList = document.getElementById('bookList');

+   // 音声認識を開始する
+   searchButton.addEventListener('click', () => {
+     recognition.start();
+   })
+ 
+   // 結果の受け取り
+   recognition.addEventListener('result', (event) => {
+     const result = event.results[0][0].transcript;
+     diagnostic.textContent = `結果: ${result}.`;
+     console.log(`結果: ${event.results[0][0].confidence}`);
+   });

+   // 音声認識サービスの実行を停止
+   recognition.addEventListener('speechend', () => {
+     recognition.stop();
+   })
});

recognition.start()
音声認識を始めます。
今回はsearchButtonを押すと音声の認識が始まります。

resultイベント
音声認識の結果が返ってきた場合に発動します。
event.resultsSpeechRecognitionResultListというオブジェクトを返します。
SpeechRecognitionResultがその中に入っていて、その中に具体的な結果が入っています。
結果が複数の場合はSpeechRecognitionResultの中に複数の結果が入っています。
今回は結果が1つなので0番目をとっています。
なお、どうやったらSpeechRecognitionResultListが複数のSpeechRecognitionResultを持つかがわかりませんでした。判明したら追記したいと思います。

speechendイベント
音声の文の区切りがついたときに発火します。正確には「認識された音声が検出されなくなったとき」に発火します。今回は区切りがついたときに音声認識を終了するようにしています。

recognition.end()
音声認識を終了します。

エラーのハンドリング

const recognition = new SpeechRecognition();

// 音声認識の設定
recognition.continuous = false;
recognition.lang = "ja-JP";
recognition.interimResults = false;
recognition.maxAlternatives = 1;

document.addEventListener('DOMContentLoaded', () => {
  // Dom取得
  const diagnostic = document.getElementById("output");
  const searchButton = document.getElementById('searchButton');
  const bookList = document.getElementById('bookList');

  // 音声認識を開始する
  searchButton.addEventListener('click', () => {
    recognition.start();
  })

  // 結果の受け取り
  recognition.addEventListener('result', (event) => {
    const result = event.results[0][0].transcript;
    diagnostic.textContent = `結果: ${result}.`;
    console.log(`結果: ${event.results[0][0].confidence}`);
  });

  // 音声認識サービスの実行を停止
  recognition.addEventListener('speechend', () => {
    recognition.stop();
  })

+   // エラーが発生した場合
+   recognition.addEventListener('error', () => {
+     diagnostic.textContent = `音声認識でエラーが発生しました。: ${event.error}`;
+   })
});

errorイベント
音声認識でエラーが発生した場合という名前のままのイベント。
とりあえずつけていますが、発生したことはないです。

検索結果の表示

const recognition = new SpeechRecognition();

// 音声認識の設定
recognition.continuous = false;
recognition.lang = "ja-JP";
recognition.interimResults = false;
recognition.maxAlternatives = 1;

document.addEventListener('DOMContentLoaded', () => {
  // Dom取得
  const diagnostic = document.getElementById("output");
  const searchButton = document.getElementById('searchButton');
  const bookList = document.getElementById('bookList');

+   // 一覧の初期化
+   bookList.innerHTML = `${books.map(b => `<tr><td>${b.title}</td><td>${b.author}</td></tr>`).join('')}`

  // 音声認識を開始する
  searchButton.addEventListener('click', () => {
    recognition.start();
  })

  // 結果の受け取り
  recognition.addEventListener('result', (event) => {
    const result = event.results[0][0].transcript;
    diagnostic.textContent = `結果: ${result}.`;
    console.log(`結果: ${event.results[0][0].confidence}`);

+     // 絞り込んで結果を表示
+     bookList.innerHTML = books.filter(x => x.title.includes(result) || x.author.includes(result)).map(b => `<tr><td>${b.title}</td><td>${b.author}</td></tr>`).join('');
  });

  // 音声認識サービスの実行を停止
  recognition.addEventListener('speechend', () => {
    recognition.stop();
  })

  // エラーが発生した場合
  recognition.addEventListener('error', () => {
    diagnostic.textContent = `音声認識でエラーが発生しました。: ${event.error}`;
  })
});

一覧の初期化
最初にすべて表示しています。数が多い場合はページネーションをつけましょう。

検索結果の絞り込み

bookList.innerHTML = books.filter(x => x.title.includes(result) || x.author.includes(result)).map(b => `<tr><td>${b.title}</td><td>${b.author}</td></tr>`).join('');

分けると次のようになります。

// 検索結果の単語が入っているかどうかで絞り込む
// xは{title: '羅生門', author: '芥川龍之介'}のようになっている
// タイトル、もしくは作家に発音した文字が入っているかどうか判定
const filteredBooks = books.filter(x => x.title.includes(result) || x.author.includes(result));
// innerHTMLに渡すためにHTMLのテキストに変換
// XSS等の危険性があるので本番ではおすすめしません。
const bookHtmlTexts = filteredBooks.map(b => `<tr><td>${b.title}</td><td>${b.author}</td></tr>`);
// bookHtmlTextsはtr要素で書かれた文字列のarrayになっているのでつなげて一つの文字列にする
bookList.innerHTML = bookHtmlTexts.join('');

それぞれの細かい解説は範囲外なのでここではしません。

以上で音声検索が実装できました。

注意

ウェブ音声認識APIでは主に2つの問題があります。

認識エンジンが必要

MDNからの抜粋です。

Chrome などの一部のブラウザーでは、ウェブページで音声認識を使用するためにサーバーベースの認識エンジンが必要です。音声が認識処理のためにウェブサービスに送信されるため、オフラインでは機能しません。

つまり、音声をChrome管理のサーバーに送っていると思われます。
この場合、「会社のセキュリティ上に音声(商品名など)をChrome側の認識エンジンに送って大丈夫か」という問題と「インターネットに接続していないと動かない」という問題を抱えることになります。

固有名詞を認識してくれない

認識エンジンが知らない単語はうまく変換できません。
もともと、商品名の検索を作ろうとしていたのですが、「ぞんびーず」を聞き取って認識してくれませんでした。
「ぞんびーず」が「B's」になったり、「ゾンビーズ」になったりと絞り込むには厳しい状態でした。

まとめ

簡単に無料で音声認識が使えるようになったのでとてもありがたいと思う反面、実務では使えなさそうという感想でした。
ただ、音声認識の精度は思ったよりいいので、もう少し整理されたら使えるようになりそうです。

蛇足

この記事を書くためにウェブ音声API周りの調べものをしていた中で一番悲しかったのはJSpeech Grammar Formatを使えば固有名詞も認識してくれるんじゃないかと思って英文を半分くらいまで読んだところで、ウェブ音声APIでは非推奨になっていたことが分かったことです。

参考

https://developer.mozilla.org/ja/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API

Discussion