【JavaScript】VoskJsで音声ファイルを音声認識する。

音声認識の技術は、スマートスピーカーや音声入力のアプリケーション、さらには自動字幕生成などさまざまな分野で活躍しています。そんな中、Vosk.js は Web ブラウザ上で動作する軽量な音声認識ライブラリとして注目されています。この記事では、Vosk.js を使って音声ファイルから音声認識を行うサンプルコードを解説していきます。

部分結果:
最終結果:
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Vosk.js 音声ファイル認識</title>
  <style>
    body {
      padding: 20px;
    }
    #result {
      margin-top: 10px;
      border: 1px solid #ccc;
      padding: 10px;
      min-height: 50px;
    }
  </style>
</head>
<body>
  <h1>Vosk.js 音声ファイル認識</h1>
  <input type="file" id="audiofile" accept="audio/*">
  <button id="startFile" disabled>ファイル認識開始</button>
  <div>
    部分結果:<span id="partialResult"></span>
  </div>
  最終結果:
  <div id="result"></div>

  <!-- vosk-browser ライブラリ(v0.0.5)をCDNから読み込み -->
  <script src="https://cdn.jsdelivr.net/npm/vosk-browser@0.0.5/dist/vosk.js"></script>
  <script>
    let model = null;

    // モデルのロード
    async function initializeModel() {
      try {
        // ZIPファイルのパスは実際のホスト先に合わせてください
        model = await Vosk.createModel('vosk-model-small-ja-0.22.zip');
        document.getElementById('startFile').disabled = false;
        console.log("モデルのロード完了");
      } catch (error) {
        console.error("モデルのロードエラー:", error);
      }
    }

    // ファイル認識開始処理
    async function startFileRecognition() {
      const fileInput = document.getElementById("audiofile");
      if (!fileInput.files.length) {
        alert("ファイルを選択してください");
        return;
      }
      // 重複処理防止のためボタン無効化
      document.getElementById('startFile').disabled = true;

      const file = fileInput.files[0];
      const reader = new FileReader();
      
      reader.onload = async event => {
        try {
          // ArrayBufferをAudioContextでデコードしてAudioBufferに変換
          const audioContext = new AudioContext();
          const arrayBuffer = event.target.result;
          let audioBuffer = await audioContext.decodeAudioData(arrayBuffer);

          console.log("ファイルのサンプルレート:", audioBuffer.sampleRate);

          // (必要ならリサンプリング処理をここに追加できます)

          // 認識器の生成(AudioContext のサンプルレートを渡す)
          const recognizer = new model.KaldiRecognizer(audioContext.sampleRate);

          // 部分結果イベントの設定
          recognizer.on('partialresult', event => {
            document.getElementById('partialResult').textContent = event.result.partial;
          });
          // 最終結果イベント(最終的な読み取り結果が出たとき)
          recognizer.on('result', event => {
            const resDiv = document.getElementById('result');
            const p = document.createElement('p');
            p.textContent = event.result.text;
            resDiv.appendChild(p);
            document.getElementById('startFile').disabled = false;
          });

          // AudioBuffer のモノラルデータをチャンクごとに分割して投入
          const chunkSize = 4096;  // サンプル単位のチャンクサイズ
          const pcmData = audioBuffer.getChannelData(0);
          const conversionContext = new AudioContext({ sampleRate: audioBuffer.sampleRate });
          for (let offset = 0; offset < pcmData.length; offset += chunkSize) {
            const floatChunk = pcmData.subarray(offset, offset + chunkSize);
            const tempBuffer = conversionContext.createBuffer(1, floatChunk.length, audioBuffer.sampleRate);
            tempBuffer.copyToChannel(floatChunk, 0);
            recognizer.acceptWaveform(tempBuffer);
          }
          
          // --- 無音投入処理で入力終了を通知する ---
          // ここでは、2秒分の無音バッファを生成
          const silenceDurationSec = 2;
          const silenceLength = audioBuffer.sampleRate * silenceDurationSec;
          const silenceBuffer = conversionContext.createBuffer(1, silenceLength, audioBuffer.sampleRate);
          const silenceData = silenceBuffer.getChannelData(0);
          silenceData.fill(0);
          recognizer.acceptWaveform(silenceBuffer);

          // 無音投入後、さらに2秒待機して認識器が内部をフラッシュするのを待つ
          setTimeout(() => {
            console.log("無音投入後の待機終了。最終結果イベントが発火するはずです。");
          }, 2000);
          // --- ここまで ---
          
          // ※ マイク認識の場合はストリームの停止で最終結果が自動的に発火しますが、
          //     ファイル認識では上記のように無音投入と待機が必要です。

        } catch (err) {
          console.error("認識処理エラー:", err);
        }
      };

      reader.readAsArrayBuffer(file);
    }

    document.addEventListener("DOMContentLoaded", () => {
      initializeModel();
      document.getElementById('startFile').addEventListener('click', startFileRecognition);
    });
  </script>
</body>
</html>

1. Vosk.js とはVosk 音声認識エンジンのブラウザ向け実装

Vosk.js は、Vosk 音声認識エンジンのブラウザ向け実装です。WebAssembly を活用しているため、クライアントサイドで高速かつ低リソースで音声認識を行うことが可能です。Web アプリケーションに組み込むことで、サーバーに音声データを送信する必要がなく、プライバシー保護や低遅延を実現できる点が大きなメリットとなっています。

Vosk.js の特徴としては、以下の点が挙げられます:

  • 軽量で高速:ブラウザ上でリアルタイムに動作するため、即時フィードバックが可能です。
  • 多言語対応:日本語をはじめとする複数の言語に対応しており、各種モデルが公開されています。
  • シンプルな実装:JavaScript の知識があれば簡単に導入でき、Web アプリケーションに組み込みやすい設計となっています。

2. Vosk.js ライブラリの読み込み

HTML 内部では、CDN 経由で Vosk.js ライブラリを読み込んでいます。

<script src="https://cdn.jsdelivr.net/npm/vosk-browser@0.0.5/dist/vosk.js"></script>

この記述により、外部ホストから最新の Vosk.js ライブラリが取得され、音声認識機能が利用可能となります。CDN を利用することで、ローカルにライブラリを配置する手間が省けるとともに、キャッシュなどの効果により読み込み速度が向上するメリットがあります。

3. モデルのロードと初期化

まず最初に行うのは、音声認識用のモデルを読み込む処理です。モデルは ZIP ファイルとして提供され、Vosk.createModel を使って非同期にロードされます。

let model = null;

async function initializeModel() {
  try {
    // ZIPファイルのパスは実際のホスト先に合わせてください
    model = await Vosk.createModel('vosk-model-small-ja-0.22.zip');
    document.getElementById('startFile').disabled = false;
    console.log("モデルのロード完了");
  } catch (error) {
    console.error("モデルのロードエラー:", error);
  }
}
  • 非同期処理
    async 関数と await を利用することで、モデルのロードが完了するまで次の処理に進まないようにしています。これにより、モデルが完全に読み込まれる前に認識処理を開始してしまうリスクを防止しています。
  • エラーハンドリング
    try...catch 文を利用して、モデルのロード時に何か問題が発生した場合にコンソールへエラー内容を出力し、デバッグしやすくしています。
  • ボタンの有効化
    モデルのロードが成功すると、認識開始ボタン(startFile)の disabled 属性が解除され、ユーザーがファイル認識を開始できるようになります。

vosk-model-small-ja-0.22.zip

  • https://alphacephei.com/vosk/models からダウンロードできます。
  • Apache License 2.0(アパッチライセンス2.0)
    • Apacheソフトウェア財団(ASF)によって策定されたオープンソースソフトウェアライセンスです。
    • 非常に寛容なライセンスであり、ソフトウェアの利用、改変、配布、商用利用を広く許可しています。

    4. ファイル認識の開始処理

    ユーザーがファイル選択後に認識開始ボタンを押すと、startFileRecognition 関数が実行され、音声ファイルの処理が始まります。

    async function startFileRecognition() {
      const fileInput = document.getElementById("audiofile");
      if (!fileInput.files.length) {
        alert("ファイルを選択してください");
        return;
      }
      // 重複処理防止のためボタン無効化
      document.getElementById('startFile').disabled = true;
    
      const file = fileInput.files[0];
      const reader = new FileReader();
      
      reader.onload = async event => {
        try {
          // ArrayBufferをAudioContextでデコードしてAudioBufferに変換
          const audioContext = new AudioContext();
          const arrayBuffer = event.target.result;
          let audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
    
          console.log("ファイルのサンプルレート:", audioBuffer.sampleRate);
    
          // (必要ならリサンプリング処理をここに追加できます)
          
          // 認識器の生成(AudioContext のサンプルレートを渡す)
          const recognizer = new model.KaldiRecognizer(audioContext.sampleRate);
    
          // 部分結果イベントの設定
          recognizer.on('partialresult', event => {
            document.getElementById('partialResult').textContent = event.result.partial;
          });
          // 最終結果イベント(最終的な読み取り結果が出たとき)
          recognizer.on('result', event => {
            const resDiv = document.getElementById('result');
            const p = document.createElement('p');
            p.textContent = event.result.text;
            resDiv.appendChild(p);
          });
    
          // AudioBuffer のモノラルデータをチャンクごとに分割して投入
          const chunkSize = 4096;  // サンプル単位のチャンクサイズ
          const pcmData = audioBuffer.getChannelData(0);
          const conversionContext = new AudioContext({ sampleRate: audioBuffer.sampleRate });
          for (let offset = 0; offset < pcmData.length; offset += chunkSize) {
            const floatChunk = pcmData.subarray(offset, offset + chunkSize);
            const tempBuffer = conversionContext.createBuffer(1, floatChunk.length, audioBuffer.sampleRate);
            tempBuffer.copyToChannel(floatChunk, 0);
            recognizer.acceptWaveform(tempBuffer);
          }
          
          // --- 無音投入処理で入力終了を通知する ---
          // ここでは、2秒分の無音バッファを生成
          const silenceDurationSec = 2;
          const silenceLength = audioBuffer.sampleRate * silenceDurationSec;
          const silenceBuffer = conversionContext.createBuffer(1, silenceLength, audioBuffer.sampleRate);
          const silenceData = silenceBuffer.getChannelData(0);
          silenceData.fill(0);
          recognizer.acceptWaveform(silenceBuffer);
    
          // 無音投入後、さらに2秒待機して認識器が内部をフラッシュするのを待つ
          setTimeout(() => {
            console.log("無音投入後の待機終了。最終結果イベントが発火するはずです。");
          }, 2000);
          // --- ここまで ---
          
          // ※ マイク認識の場合はストリームの停止で最終結果が自動的に発火しますが、
          //     ファイル認識では上記のように無音投入と待機が必要です。
    
        } catch (err) {
          console.error("認識処理エラー:", err);
        }
      };
    
      reader.readAsArrayBuffer(file);
    }

    4.1. ファイル入力の確認

    • ファイルが選択されているかのチェック
      if (!fileInput.files.length) によって、ファイルが選択されていない場合にはアラートを出して処理を中断します。
    • ボタンの再度の無効化
      認識処理中にボタンを押して重複実行が起こらないよう、処理開始と同時にボタンを無効化しています。

    4.2. FileReader によるファイルの読み込み

    • FileReader の利用
      FileReader を使ってアップロードされた音声ファイルを読み込み、結果を ArrayBuffer として取得します。
      reader.onload イベントハンドラ内で、読み込み完了後に AudioContext を用いた音声データのデコード処理が行われます。

    4.3. AudioContext と AudioBuffer の利用

    • AudioContext の生成
      Web Audio API の中心となる AudioContext を生成しています。
    const audioContext = new AudioContext();

    これにより、ブラウザ内で音声データの再生、加工、解析が可能となります。

    • ArrayBuffer のデコード
      読み込んだ ArrayBuffer を audioContext.decodeAudioData でデコードし、AudioBuffer に変換します。
      AudioBuffer には、音声データのサンプルレートやチャネルごとのデータが格納されており、後続の処理で利用されます。
    • サンプルレートの確認
      audioBuffer.sampleRate により、音声ファイルのサンプルレートを確認し、認識器生成時に同じレートを渡すことで、認識精度を維持します。

    4.4. 認識器(Recognizer)の生成

    • KaldiRecognizer のインスタンス生成
      Vosk.js では、モデルから音声認識を行うために KaldiRecognizer を生成します。ここでは、AudioContext のサンプルレートを渡しています。
    const recognizer = new model.KaldiRecognizer(audioContext.sampleRate);
    

    サンプルレートを正しく設定することは、音声認識の精度に大きく影響するため、必須の処理となります。

    4.5. 部分結果と最終結果のイベント設定

    • 部分結果の取得
      認識中にリアルタイムで部分的な結果を表示するために、partialresult イベントをリッスンしています。
    recognizer.on('partialresult', event => {
      document.getElementById('partialResult').textContent = event.result.partial;
    });

    このイベントハンドラにより、音声認識の進行状況をユーザーにフィードバックできます。

    • 最終結果の取得
      認識が完了したときに result イベントが発火し、最終的な認識結果が表示されます。
    recognizer.on('result', event => {
      const resDiv = document.getElementById('result');
      const p = document.createElement('p');
      p.textContent = event.result.text;
      resDiv.appendChild(p);
    });

    認識結果は新しい <p> 要素に追加され、複数回の認識結果も順次表示できるようになっています。

    4.6. 音声データのチャンク投入

    • モノラルデータの抽出
      音声データが格納されている AudioBuffer から、最初のチャンネル(モノラルデータ)を取得します。
    const pcmData = audioBuffer.getChannelData(0);
    • チャンクサイズの設定
      認識処理を効率的に行うため、4096 サンプルずつに分割してデータを投入します。チャンクサイズは、システムのパフォーマンスや認識精度に応じて調整可能です。
    const chunkSize = 4096;
    • チャンクごとの処理
      ループで音声データを順次チャンクに分割し、各チャンクを一時的な AudioBuffer に変換してから認識器に投入します。
    for (let offset = 0; offset < pcmData.length; offset += chunkSize) {
      const floatChunk = pcmData.subarray(offset, offset + chunkSize);
      const tempBuffer = conversionContext.createBuffer(1, floatChunk.length, audioBuffer.sampleRate);
      tempBuffer.copyToChannel(floatChunk, 0);
      recognizer.acceptWaveform(tempBuffer);
    }

    この方法により、ストリーミングのように音声データが認識器に送信され、内部で処理が行われます。

    4.7. 無音バッファ投入による入力終了の通知

    ファイル認識の場合、マイク入力と異なり入力の終了を認識器に通知する必要があります。ここでは、一定期間の無音データを投入することで、認識器に「入力が終了した」というシグナルを送ります。

    • 無音バッファの生成
      2秒間の無音データを生成するために、サンプルレートに合わせた長さのバッファを作成し、中身を全て 0(無音)で埋めます。
    const silenceDurationSec = 2;
    const silenceLength = audioBuffer.sampleRate * silenceDurationSec;
    const silenceBuffer = conversionContext.createBuffer(1, silenceLength, audioBuffer.sampleRate);
    const silenceData = silenceBuffer.getChannelData(0);
    silenceData.fill(0);
    recognizer.acceptWaveform(silenceBuffer);
    • 待機処理
      無音バッファ投入後、認識器が内部で結果を確定するために、さらに2秒程度の待機時間を設けています。
    setTimeout(() => {
      console.log("無音投入後の待機終了。最終結果イベントが発火するはずです。");
    }, 2000);

    これにより、認識器はバッファ内の全データを処理し、最終的な結果を出力できるようになります。

    5. 付録

    5.1 マイク入力による音声認識

    <!DOCTYPE html>
    <html lang="ja">
    
    <head>
        <meta charset="UTF-8">
        <title>Vosk.js 音声認識</title>
        <style>
            body { padding: 20px; }
    
            #result {
                margin-top: 10px; 
                border: 1px solid #ccc;
                padding: 10px;
                min-height: 50px;
            }
        </style>
    </head>
    
    <body>
        <h1>Vosk.js 音声認識</h1>
        <select id="micSelect"></select>
        <button id="start" disabled>認識開始</button>
        <div>
            部分結果:
            <p id="partialResult"></p>
        </div>
        最終結果:
        <div id="result"></div>
    
        <!-- Vosk.jsライブラリをCDNから読み込み。これにより音声認識機能を使用可能にする -->
        <script src="https://cdn.jsdelivr.net/npm/vosk-browser@0.0.5/dist/vosk.js"></script>
        
        <script>
            // 音声認識モデルを格納するための変数。初期値はnull。
            let model = null;
    
            // ページの初期化処理を行う非同期関数
            async function initialize() {
                try {
                    // ユーザーにマイクの使用許可を求め、音声ストリームを取得
                    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    
                    // 取得後すぐにストリームのトラックを停止(実際の使用前のテスト目的)
                    stream.getTracks().forEach(track => track.stop());
    
                    // マイク選択用の<select>要素を取得
                    const micSelect = document.getElementById('micSelect');
    
                    // 利用可能なすべてのメディアデバイス(入力・出力デバイス)を列挙
                    const devices = await navigator.mediaDevices.enumerateDevices();
    
                    // オーディオ入力デバイス(マイク)のみをフィルタリングして処理
                    devices.filter(device => device.kind === "audioinput").forEach(device => {
                        // 各マイクデバイスに対応する<option>要素を生成
                        const option = document.createElement('option');
                        option.value = device.deviceId;               // デバイス固有のIDをoptionの値に設定
                        option.textContent = device.label || "マイク";  // ラベルがなければ「マイク」という名称を表示
                        // セレクトボックスに<option>要素を追加
                        micSelect.appendChild(option);
                    });
    
                    // 指定したZIPファイルから日本語用の小型音声認識モデルを非同期でロード
                    model = await Vosk.createModel('vosk-model-small-ja-0.22.zip');
                    document.getElementById('start').disabled = false;
                } catch (error) {
                    console.error("初期化エラー:", error);
                }
            }
    
            // 音声認識を開始するための非同期関数
            async function startRecognition() {
                try {
                    // 複数回のクリックによる重複処理を防ぐため、認識開始ボタンを無効化
                    document.getElementById('start').disabled = true;
    
                    // マイク選択用の<select>要素を取得
                    const micSelect = document.getElementById('micSelect');
    
                    // ユーザーが選択したマイクデバイスを指定して、オーディオストリームを取得
                    const stream = await navigator.mediaDevices.getUserMedia({
                        audio: {
                            deviceId: micSelect.value, // セレクトボックスで選択されたマイクのdeviceIdを指定
                            echoCancellation: true,    // エコーキャンセルの有効化(反響防止)
                            noiseSuppression: true,      // ノイズ抑制の有効化
                            channelCount: 1              // モノラル音声指定(一チャンネル)
                        }
                    });
    
                    // Web Audio APIのAudioContextを生成(音声の処理とグラフ構築に必要)
                    const audioContext = new AudioContext();
    
                    // モデル内に定義されたKaldiRecognizerを生成し、AudioContextのサンプルレートを渡す
                    const recognizer = new model.KaldiRecognizer(audioContext.sampleRate);
    
                    const resultDiv = document.getElementById('result');
                    const partialResultSpan = document.getElementById('partialResult');
    
                    // 認識の最終結果が出た際のイベントリスナーを設定
                    recognizer.on('result', event => {
                        const paragraph = document.createElement('p');
                        paragraph.textContent = event.result.text;
                        resultDiv.appendChild(paragraph);
                    });
    
                    // 部分的な認識結果(認識途中の結果)が出た際のイベントリスナーを設定
                    recognizer.on('partialresult', event => {
                        partialResultSpan.textContent = event.result.partial;
                    });
    
                    // ScriptProcessorNode を生成
                    // ※ バッファサイズ4096、入力チャンネル1、出力チャンネル1
                    // ※ ただし、ScriptProcessorNode は非推奨。AudioWorklet の使用が推奨されるが、ここでは簡易例として使用
                    const recognizerNode = audioContext.createScriptProcessor(4096, 1, 1);
                    recognizerNode.onaudioprocess = event => {
                        try {
                            // 入力された音声データ(入力バッファ)を認識器に渡して解析
                            recognizer.acceptWaveform(event.inputBuffer);
    
                            // 出力音声は不要なので、出力バッファのデータをゼロで埋める(無音化)
                            event.outputBuffer.getChannelData(0).fill(0);
                        } catch (error) {
                            console.error("音声処理エラー:", error);
                        }
                    };
    
                    // マイクからの音声ストリームをAudioContextの中に取り込むために、MediaStreamSourceノードを生成
                    const sourceNode = audioContext.createMediaStreamSource(stream);
    
                    // オーディオグラフ構築: マイク入力 → ScriptProcessorNode(解析処理) → AudioContext出力
                    sourceNode.connect(recognizerNode).connect(audioContext.destination);
                } catch (error) {
                    console.error("認識開始エラー:", error);
                    document.getElementById('start').disabled = false;
                }
            }
    
            // DOMが完全に読み込まれたタイミングで初期化処理を実行する
            document.addEventListener("DOMContentLoaded", () => {
                initialize();
                document.getElementById('start').addEventListener('click', startRecognition);
            });
        </script>
    </body>
    
    </html>

    5.2 マイク入力による音声認識(AudioWorklet版)

    <!DOCTYPE html>
    <html lang="ja">
    <head>
      <meta charset="UTF-8">
      <title>Vosk.js 音声認識 (AudioWorklet版)</title>
      <style>
        body { padding: 20px; }
        #result {
          margin-top: 10px; 
          border: 1px solid #ccc;
          padding: 10px;
          min-height: 50px;
        }
      </style>
    </head>
    <body>
      <h1>Vosk.js 音声認識 (AudioWorklet版)</h1>
      <select id="micSelect"></select>
      <button id="start" disabled>認識開始</button>
      <div>
        部分結果:
        <p id="partialResult"></p>
      </div>
      最終結果:
      <div id="result"></div>
    
      <!-- Vosk.jsライブラリをCDNから読み込み -->
      <script src="https://cdn.jsdelivr.net/npm/vosk-browser@0.0.5/dist/vosk.js"></script>
      <script>
        let model = null;
    
        // 初期化処理
        async function initialize() {
          try {
            // ユーザーにマイクの使用許可を求め、テスト的にストリーム取得後すぐ停止
            const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
            stream.getTracks().forEach(track => track.stop());
    
            // マイク選択用の<select>要素に利用可能なオーディオ入力デバイスを列挙
            const micSelect = document.getElementById('micSelect');
            const devices = await navigator.mediaDevices.enumerateDevices();
            devices.filter(device => device.kind === "audioinput").forEach(device => {
              const option = document.createElement('option');
              option.value = device.deviceId;
              option.textContent = device.label || "マイク";
              micSelect.appendChild(option);
            });
    
            // ZIPファイルから日本語用小型モデルをロード
            model = await Vosk.createModel('vosk-model-small-ja-0.22.zip');
            document.getElementById('start').disabled = false;
          } catch (error) {
            console.error("初期化エラー:", error);
          }
        }
    
        // 音声認識開始処理
        async function startRecognition() {
          try {
            document.getElementById('start').disabled = true;
            const micSelect = document.getElementById('micSelect');
            const stream = await navigator.mediaDevices.getUserMedia({
              audio: {
                deviceId: micSelect.value,
                echoCancellation: true,
                noiseSuppression: true,
                channelCount: 1
              }
            });
    
            // AudioContext の生成(サンプルレートは環境依存)
            const audioContext = new AudioContext();
            // モデル内の KaldiRecognizer を生成(AudioContext のサンプルレートを渡す)
            const recognizer = new model.KaldiRecognizer(audioContext.sampleRate);
    
            const resultDiv = document.getElementById('result');
            const partialResultP = document.getElementById('partialResult');
    
            // 最終認識結果のイベントリスナー
            recognizer.on('result', event => {
              const p = document.createElement('p');
              p.textContent = event.result.text;
              resultDiv.appendChild(p);
            });
    
            // 部分認識結果のイベントリスナー
            recognizer.on('partialresult', event => {
              partialResultP.textContent = event.result.partial;
            });
    
            // AudioWorklet モジュールの読み込み(同一ディレクトリに配置した recognizer-processor.js を指定)
            await audioContext.audioWorklet.addModule('recognizer-processor.js');
            // AudioWorkletNode の生成。第二引数は登録したプロセッサーの名前です。
            const recognizerNode = new AudioWorkletNode(audioContext, 'recognizer-processor');
    
            // AudioWorkletProcessor から送信される音声データを受け取る
            recognizerNode.port.onmessage = event => {
              // 受け取った Float32Array を AudioBuffer に変換
              const float32Data = event.data;
              const buffer = audioContext.createBuffer(1, float32Data.length, audioContext.sampleRate);
              buffer.copyToChannel(float32Data, 0);
              // 認識器にデータを渡す
              recognizer.acceptWaveform(buffer);
            };
    
            // マイク入力ストリームを AudioContext の MediaStreamSource に接続
            const sourceNode = audioContext.createMediaStreamSource(stream);
            // オーディオグラフ構築:マイク入力 → AudioWorkletNode → 出力(出力は無音でも問題ありません)
            sourceNode.connect(recognizerNode).connect(audioContext.destination);
          } catch (error) {
            console.error("認識開始エラー:", error);
            document.getElementById('start').disabled = false;
          }
        }
    
        document.addEventListener("DOMContentLoaded", () => {
          initialize();
          document.getElementById('start').addEventListener('click', startRecognition);
        });
      </script>
    </body>
    </html>