Zenn
📌

Web Speech APIを利用した音声認識Webアプリの作成

2024/12/02に公開

前書き

普段はPythonで開発しているAIエンジニアです。
音声認識関連のタスクに取り組むにあたりちょっとしたWebアプリを作る機会があったので、簡単にまとめておこうと思います。

取り組み時の私の状態ですが、

項目 状態
HTML 初めて利用。事前知識ゼロ。
CSS 初めて利用。事前知識ゼロ。
JavaScript 初めて利用。事前知識ゼロ。

といった感じです。
要するにただの素人がWebアプリを作ってみた!という感じの記事になります。

Web Speech API

Web Speech APIというAPIを利用します。
https://developer.mozilla.org/ja/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API

https://wicg.github.io/speech-api/


このAPIの音声認識に関する点を簡単にまとめます。
※調査には細心の注意を払って記載していますが、最終的な解釈・判断はご自身の責任でお願い致します。

項目 説明
概要 Web上で
・音声認識(音声からテキストに変換する技術)
・音声合成(テキストから音声に変換する技術)
をできるようにする仕様
仕様策定者 World Wide Web Consortium(W3C)
(Web技術の標準化を行う国際的な非営利団体)
音声認識モデル ブラウザごとに異なる
データの扱い ブラウザごとに異なる
ライセンス W3C Community Final Specification Agreementに基づく
料金 無料

Web Speech APIはあくまで仕様であって、Google Cloudの Speech-to-Text APIWhisper APIなどとは異なり音声認識を実施するサービスではない点に注意が必要です。

このAPIはあくまで仕様をまとめたものなので、
入力された音をどのように音声認識の処理にかけるのか?というバックエンド側の処理はブラウザごとに異なるらしいです。

同様の理由で入力した音データの扱いもブラウザごとに異なります。
例えばGoogle Chromeの場合、私の認識違いでなければこのページに行きつき、学習に利用するという明示的な同意がない場合も短期メモリ(RAM)に数分間だけ保持され、モデルの学習に利用されます
(この辺の扱いに敏感な方は注意した方が良いかもしれません。)

ちなみにFirefoxの場合は、私の認識違いでなければこのページに行きつき、音データがサービス提供以外の目的で利用されることはないらしいです。

ライセンス的に商用利用も可能で無料で利用できるのは大きな利点だと思います。

Webアプリの作成

機能のまとめ

まず簡単に機能をまとめます。

・startボタンを押して音声認識をスタート
・endボタンで音声認識を終了
・認識中か否かの状態を表示
・音声認識の候補結果を候補欄にボタン形式で表示
・候補の最後に"やり直し"ボタンを表示
・候補の中から"やり直し"でないボタンが選択された場合、最終的な結果欄に表示
・"やり直し"ボタンが選択された場合、音声認識を継続

デモ動画

イメージが掴めるように先にデモ動画を載せておきます。

ディレクトリ構成

今回はシンプルにindexファイルとmainのファイルを用意して実装します。

work_space/
    ├── index.html
    └── main.js

コード

まずはindex.htmlファイルを作成します。
それぞれの変数の上に何を表す変数なのかをコメントで記入しています。

index.html
<!doctype html>
<html class="no-js" lang="">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Web Speech APIによる音声認識のデモページ</title>
  
  <meta name="description" content="">
  <meta property="og:title" content="">
  <meta property="og:type" content="">
  <meta property="og:url" content="">
  <meta property="og:image" content="">
  <meta property="og:image:alt" content="">
  <meta name="theme-color" content="#fafafa">
</head>

<body>
  <header>音声認識のデモページ</header>
  <!-- div: なんでもOKの枠 -->
  <!-- p: 段落 -->
  <!-- ul: 箇条書きの枠 -->
  <!-- li: 箇条書きの要素 -->

  <p>
    <!-- 認識状態を表す -->
    <div id="status"></div>
  </p>

  <!-- 音声認識の操作をするボタン -->
  <button id="start-btn">start</button>
  <button id="stop-btn">stop</button>
  <br>
  <p>認識結果の候補</p>
  <!-- 音声認識の結果の候補を表示する欄 -->
  <div style="padding: 10px; margin-bottom: 10px; border: 1px solid #333333;">
    <div id="alternatives"></div>
  </div>
  <br>

  <p>認識結果</p>
  <!-- ユーザーが選択した認識結果を表示する欄 -->
  <div style="padding: 10px; margin-bottom: 10px; border: 5px double #333333;">
    <div id="result-div"></div>
  </div>

  <!-- 読み込むJavaScriptファイルの指定 -->
  <script src="main.js"></script>
</body>

</html>

次にmain.jsファイルです。
大きくなってしまったので要素ごとに分割して書いていきます。
(最後にまとめたコードも載せておきます。)

まずは画面表示に使用する変数を定義していきます。

// 必要な要素の取得
// 音声認識の状態を表示する要素の取得
let status_ = document.querySelector('#status');
// ボタン要素の取得
const startBtn = document.querySelector('#start-btn');
const stopBtn = document.querySelector('#stop-btn');
// 認識候補欄の取得
var alternatives = document.querySelector('#alternatives');
// 認識結果表示要素の取得
const resultDiv = document.querySelector('#result-div');
// 確定後の認識結果の表示
let finalTranscript = '';

次に音声認識を実施するオブジェクトを作成します。

// 音声認識を実施するオブジェクトの作成
// Chrome と Firefox 両方に対応するための記述
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();

次に音声認識に関する設定をします。
ここでは日本語の音声認識を実施し、認識結果の候補は最大で5個取得する仕様にします。

// 音声認識に関する設定
// 言語の指定
recognition.lang = 'ja-JP';
// 連続して認識するかどうか
recognition.continuous = true;
// 何個の候補を返すか
recognition.maxAlternatives = 5;

ここからは具体的な処理を書いていきます。
まず、音声認識の状態の変化に関する処理を書いていきます。
スタートボタンが押された場合、ストップボタンが押された場合、ストップボタン以外の条件(タイムアウトなど)で音声認識が終了した場合の処理を書いていきます。

// スタートボタンを押すと認識を開始
startBtn.onclick = () => {
    recognition.start();
    status_.innerHTML = "音声認識中...";
}
// ストップボタンを押すと認識を停止
stopBtn.onclick = () => {
    recognition.stop();
    status_.innerHTML = "音声認識停止中";
}
// 音声認識が停止された場合の処理
recognition.addEventListener("end", () => {
    console.log("音声認識が停止しました");
    status_.innerHTML = "音声認識停止中";
}
);

次に、音声認識を実施した時の処理を書いていきます。
APIで認識結果を取得できた場合、候補の結果をボタンとして出力します。
全ての候補の出力後に"やり直し"というボタンも出力します。

// 候補欄に出力する処理
function output_alternatives(event, event_index, num_of_alternatives) {
    // 認識候補ごとにボタンを出力
    for (let alternative_index = 0; alternative_index < num_of_alternatives; alternative_index++) {
        let alternative_text = event.results[event_index][alternative_index].transcript;
        console.log("認識候補: ", alternative_text);
        // 認識結果がない場合はスキップ
        if (alternative_text == ""){
            continue;
        }

        // ボタン要素を作成
        let button_ = document.createElement("button");
        // ボタンのテキストを設定
        button_.innerHTML = alternative_text;
        // ボタンを追加
        alternatives.appendChild(button_);
        // 改行を作成
        let br_ = document.createElement("br");
        // 改行をボタンの後に追加
        alternatives.appendChild(br_);
    }
}

// _やり直し_ボタンの追加
function add_restart_button() {
    let button_ = document.createElement("button");
    button_.innerHTML = "_やり直し_";
    alternatives.appendChild(button_);
    let br_ = document.createElement("br");
    alternatives.appendChild(br_);
}

// 認識結果が返ってきた時の処理
recognition.addEventListener("result", (event) => {
    console.log("新規イベント取得");
    console.log(event);
    
    alternatives.innerHTML = "";
    // イベントのindexごとに候補をボタンで表示
    for (let event_index = event.resultIndex; event_index < event.results.length; event_index++) {
        console.log("イベントのインデックス: ", event.resultIndex);
        console.log("for文内のイベントのインデックス: ", event_index);
        console.log("通算イベント回数: ", event.results.length);
        num_of_alternatives = event.results[event_index].length;
        console.log("認識候補数: ", num_of_alternatives);
        // 認識候補をボタンで表示
        output_alternatives(event, event_index, num_of_alternatives);
    }
    // やり直しボタンを追加
    add_restart_button();
}
);

最後に認識結果の候補ボタンがクリックされた場合の処理を書いていきます。
候補の中からクリックされた文字列を最終的な結果欄に表示します。
やり直しボタンがクリックされた場合は候補を削除し直して再度音声認識を実施します。

// 候補内のボタンがクリックされた場合、そのテキストを表示。やり直しの場合は候補を全削除
alternatives.addEventListener("click", function(event_) {
    // ボタンでない場合はスキップ
    if (event_.target.tagName != "BUTTON") {
        return;
    }
    console.log("認識結果決定のボタンが押されました");
    console.log("ボタンの内容: ", event_.target.innerHTML);
    if (event_.target.innerHTML == "_やり直し_") {
        console.log("やり直しボタンが押されました");
        alternatives.innerHTML = "";
        // SpeechRecognitionが停止している場合は再開
        if (recognition.ended) {
            recognition.start();
            status_.innerHTML = "音声認識中...";
        }
    } else {
        finalTranscript = finalTranscript + event_.target.innerHTML + '<br>';
        resultDiv.innerHTML = finalTranscript + '<br>';
    }
}
);

main.jsファイル全体をまとめたものを載せておきます。

main.js
// 必要な変数の定義
// 音声認識の状態を表示する要素の取得
let status_ = document.querySelector('#status');
// ボタン要素の取得
const startBtn = document.querySelector('#start-btn');
const stopBtn = document.querySelector('#stop-btn');
// 認識候補欄の取得
var alternatives = document.querySelector('#alternatives');
// 認識結果表示要素の取得
const resultDiv = document.querySelector('#result-div');
// 確定後の認識結果の表示
let finalTranscript = '';


// 音声認識を実施するオブジェクトの作成
// Chrome と Firefox 両方に対応するための記述
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();

// 音声認識に関する設定
// 言語の指定
recognition.lang = 'ja-JP';
// 連続して認識するかどうか
recognition.continuous = true;
// 何個の候補を返すか
recognition.maxAlternatives = 5;

// スタートボタンを押すと認識を開始
startBtn.onclick = () => {
    recognition.start();
    status_.innerHTML = "音声認識中...";
}
// ストップボタンを押すと認識を停止
stopBtn.onclick = () => {
    recognition.stop();
    status_.innerHTML = "音声認識停止中";
}

// 音声認識が停止された場合の処理
recognition.addEventListener("end", () => {
    console.log("音声認識が停止しました");
    status_.innerHTML = "音声認識停止中";
}
);

// 候補欄に出力する処理
function output_alternatives(event, event_index, num_of_alternatives) {
    // 認識候補ごとにボタンを出力
    for (let alternative_index = 0; alternative_index < num_of_alternatives; alternative_index++) {
        let alternative_text = event.results[event_index][alternative_index].transcript;
        console.log("認識候補: ", alternative_text);
        // 認識結果がない場合はスキップ
        if (alternative_text == ""){
            continue;
        }

        // ボタン要素を作成
        let button_ = document.createElement("button");
        // ボタンのテキストを設定
        button_.innerHTML = alternative_text;
        // ボタンを追加
        alternatives.appendChild(button_);
        // 改行を作成
        let br_ = document.createElement("br");
        // 改行をボタンの後に追加
        alternatives.appendChild(br_);
    }
}

// _やり直し_ボタンの追加
function add_restart_button() {
    let button_ = document.createElement("button");
    button_.innerHTML = "_やり直し_";
    alternatives.appendChild(button_);
    let br_ = document.createElement("br");
    alternatives.appendChild(br_);
}

// 認識結果が返ってきた時の処理
recognition.addEventListener("result", (event) => {
    console.log("新規イベント取得");
    console.log(event);
    
    alternatives.innerHTML = "";
    // イベントのindexごとに候補をボタンで表示
    for (let event_index = event.resultIndex; event_index < event.results.length; event_index++) {
        console.log("イベントのインデックス: ", event.resultIndex);
        console.log("for文内のイベントのインデックス: ", event_index);
        console.log("通算イベント回数: ", event.results.length);
        num_of_alternatives = event.results[event_index].length;
        console.log("認識候補数: ", num_of_alternatives);
        // 認識候補をボタンで表示
        output_alternatives(event, event_index, num_of_alternatives);
    }
    // やり直しボタンを追加
    add_restart_button();
}
);

// 候補内のボタンがクリックされた場合、そのテキストを表示。やり直しの場合は候補を全削除
alternatives.addEventListener("click", function(event_) {
    // ボタンでない場合はスキップ
    if (event_.target.tagName != "BUTTON") {
        return;
    }
    console.log("認識結果決定のボタンが押されました");
    console.log("ボタンの内容: ", event_.target.innerHTML);
    if (event_.target.innerHTML == "_やり直し_") {
        console.log("やり直しボタンが押されました");
        alternatives.innerHTML = "";
        // SpeechRecognitionが停止している場合は再開
        if (recognition.ended) {
            recognition.start();
            status_.innerHTML = "音声認識中...";
        }
    } else {
        finalTranscript = finalTranscript + event_.target.innerHTML + '<br>';
        resultDiv.innerHTML = finalTranscript + '<br>';
    }
}
);

後書き

画面の伴う開発は初めてですが、やってみると面白いと感じました。
今後、Reactの勉強頑張ろうと思います。

参考

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

https://wicg.github.io/speech-api/

https://www.w3.org/community/speech-api/

・Googleの音声認識
https://moonlightdx999.hatenablog.com/entry/2019/02/03/120000

・World Wide Web Consortium(W3C)
https://www.w3.org/about/

https://www.internetacademy.jp/it/design/homepage/web-standardization-and-w3c-recommendation-process.html

・W3Cコミュニティ最終仕様契約
https://www.w3.org/community/about/process/final/

・Web Speech APIの利用例
https://qiita.com/hmmrjn/items/4b77a86030ed0071f548

Goals Tech Blog

Discussion

ログインするとコメントできます