Webサイトにマイクボタンを追加するだけなら簡単そうに見えます。声を取り込み、音声を認識して、テキスト入力欄に挿入すればよいからです。しかし、デスクトップとモバイルの両方のブラウザーで安定して動かすのは、実際にはずっと難しい作業です。特に、マイクの音声をサーバーにアップロードせず、すべての音声認識を端末内で完結させる必要がある場合はなおさらです。
課題は音声認識モデルの選定だけではありません。実際に動くものにするには、マイクの権限、音声の取得、リサンプリング、ブラウザーのワーカー、WebAssembly、ONNXモデルの初期化、トークン化、キャッシュ、アプリケーションの入力欄との連携を正しく処理する必要があります。
この記事では、OpenAI Whisper、Transformers.js、ONNX Runtime Web、AudioWorklet、専用Web Workerを使い、プライバシーに配慮した実用的な音声テキスト入力を構築する方法を説明します。モデルファイルのダウンロードが正常に完了していても、マイクボタンが読み込み状態のまま動かなくなるような、実際の統合時に発生した問題も取り上げます。
目指す動作は単純です。訪問者がマイクボタンを押して自然に話すと、その内容が既存の入力欄に文字として表示されます。送信前に結果を確認し、必要なら編集できます。録音した音声自体をブラウザーの外に送る必要はありません。
完全にクライアント側で行う音声認識とは
Webサイトに音声入力を追加する方法は、大きく分けて2つあります。
1つ目は音声認識サービスを利用する方法です。ブラウザーが音声を取得して外部サービスに送り、認識されたテキストを受け取ります。実装は簡単になる可能性がありますが、ネットワークへの依存、プライバシー上の考慮、サービス提供状況、利用量に応じた費用といった問題が生じます。
2つ目はブラウザー内で直接推論を実行する方法です。Webサイトは音声認識モデルと実行に必要なランタイムをダウンロードします。ブラウザーがマイク音声を取得して数値サンプルに変換し、ローカルでモデルを実行して、音声認識サーバーを介さずに文字起こし結果を生成します。
この記事で扱うのは、2つ目の方法だけです。
通常のブラウザー音声認識と、ローカル処理が保証された音声認識を混同してはいけません。Web Speech APIはサーバー側の認識エンジンを利用する場合があります。新しい実装の一部にはprocessLocallyオプションがありますが、対応状況はブラウザーやOSによって異なります。厳密なローカル処理が必要なら、一般的なSpeechRecognitionを自動的な代替手段にしてはいけません。
その代わりに、アプリケーションが推論パイプライン全体を制御します。
Microphone
|
v
AudioWorklet
|
v
Float32 PCM audio
|
v
Dedicated Web Worker
|
v
16 kHz resampling
|
v
Whisper + ONNX Runtime Web
|
v
Tokenizer and text decoding
|
v
React input stateWebサイトは引き続きJavaScript、WebAssembly、設定ファイル、モデルの重みを配信します。これらは通常の静的ファイルのダウンロードです。重要なのは、認識のためにマイク音声をアップロードするのではなく、ローカルで処理するという点です。ユーザーが後から通常のフォームで認識結果のテキストを送信する場合、それは別のアプリケーション操作です。
実用的なモデルの選択:Whisper TinyとBase
Whisperは多言語対応の音声認識モデル群です。1つのモデルで多数の言語を認識できるため、多言語Webサイトに特に適しています。
短い音声メッセージでは、認識品質とブラウザーのリソース消費のバランスがよいモデルとして、Whisper TinyとWhisper Baseの2つが候補になります。
| モデル | 選択したQ8 ONNX重みの概算サイズ | 推奨用途 |
|---|---|---|
| Whisper Tiny | 41 MB | モバイルやリソースの少ない端末向けの慎重な選択肢 |
| Whisper Base | 77 MB | 動作検証済みの高性能端末向けの高品質な選択肢 |
このサイズは、調査したエクスポートに含まれるエンコーダーとデコーダーのONNXファイルのうち、使用するものを指します。すべての補助ファイルは含まれておらず、ブラウザーの最大メモリ使用量と混同してはいけません。
Whisper Tinyはダウンロードが速く、通常は計算資源の消費も少ないため、性能が不明なモバイル環境に向いています。一方、Whisper Baseは一般に認識品質が高く、とりわけ小さいモデルで誤認識が増える一部の言語で有利です。
モデルを物理RAMの容量だけで選んではいけません。ブラウザーが実際に使えるメモリは、端末の仕様から想像するよりかなり少ない場合があります。既存ページのメモリ使用量、WebAssemblyのメモリ確保、一時的なテンソル、OS側のメモリ圧迫も影響します。
慎重に進めるなら、性能が不明なモバイル端末ではTinyから始め、一定の条件を満たす端末にBaseを使用し、Baseの初期化に失敗したらTinyに切り替えます。
Baseの初期化に失敗した場合は、Tinyを読み込む前にそのインスタンスを解放する必要があります。2つのモデルをメモリに残したままでは、フォールバックの意味がありません。
WebAssemblyを本番環境の標準として選ぶ理由
ONNX Runtime Webはブラウザー内でニューラルネットワークを実行できます。今回の処理では、WebAssemblyが主要なデスクトップ・モバイルブラウザーで利用できる実用的なCPU推論手段になります。
WebGPUは対応するハードウェア上でサポート対象モデルを高速化できますが、利用可能であることと音声認識パイプラインが正常に動くことは別です。ブラウザーにnavigator.gpuがあっても、そのプラットフォームをONNX Runtimeがサポートするか、特定のWhisperグラフが正しく初期化されるか、推論を繰り返してもリソースの問題が起きないかは保証されません。
そのため、慎重な実装では本番バックエンドにWASMを採用し、WebGPUについては互換性、精度、メモリ使用量、連続セッションの検証後に別途評価できます。
初期実装をシングルスレッドWASMにすれば、Webサイト全体をクロスオリジン分離やSharedArrayBufferに依存させることも避けられます。
env.backends.onnx.wasm.numThreads = 1;
env.backends.onnx.wasm.proxy = false;推論を専用ワーカー内で実行すれば、負荷の高い計算をJavaScriptのメインスレッドから切り離せます。モデルが音声を処理している間も、入力欄、ソフトウェアキーボード、スクロール、アプリケーションのUIを応答可能な状態に保てます。
初回ページ読み込み時に音声認識を一切読み込まない
マイク機能を使わない訪問者に、大きなネットワーク通信やメモリ負荷を負担させるべきではありません。
初期アプリケーションバンドルにTransformers.jsを先行読み込みしないでください。ページ表示時にONNX Runtimeを初期化したり、アイドル中にモデルの重みを自動ダウンロードしたりすることも避けます。
認識処理に必要なものは、ユーザーが明示的にマイクボタンを押してから読み込みます。
初回利用時は、次の流れが有用です。
- マイクボタンのクリックを受け取る。
getUserMedia()でマイクの使用許可を要求する。- 許可されなければ、モデルをダウンロードせずにエラーを表示する。
- 許可されたら、モデルを準備する間は一時的なマイクストリームを解放する。
- 専用ワーカーを動的に初期化し、選択したローカルモデルを読み込む。
- ワーカーから認識パイプラインの準備完了通知を受けるまで待つ。
- ユーザーがキャンセルしていなければ自動的に録音を開始する。
- 録音中であることが分かる表示にする。
権限要求はユーザー操作に結び付いていなければなりません。また、録音を常に無言で再開できると決めつけず、キャンセルやブラウザー固有の音声取得制限にも対応する必要があります。
モデルの準備中は、音声入力を準備していますなどの状態を見えるようにします。意味のある状態変化がないまま回り続ける表示では、モデルのダウンロードが成功していても、マイクが故障しているように見えてしまいます。
初期化したワーカーは、次の音声入力に備えて短時間保持してもかまいません。アイドル状態が約1分続いたら終了する設定は、妥当な出発点の1つです。その後はワーカーを終了し、メモリ内のモデルを解放します。ブラウザーにキャッシュされたモデルファイルは、次回以降も利用できます。
AudioWorkletで生の音声を取得する
Whisperには数値化された音声サンプルが必要です。MediaRecorderは音声ファイルの録音には適していますが、ローカル音声認識に使うと、不要なエンコードとデコードの往復処理が発生する場合があります。
より単純な処理経路は次のとおりです。
getUserMedia()
|
v
AudioContext
|
v
AudioWorklet
|
v
Float32 PCM buffers
|
v
Dedicated WorkerAudioWorkletは、アプリケーションのJavaScriptメインスレッドとは別の場所で音声処理コードを実行します。PCMサンプルの取得、複数チャンネルからモノラルへの変換、簡単な音量測定、蓄積したバッファーのワーカーへの転送が可能です。
マイク初期化の簡略例を示します。
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: { ideal: 1 },
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
});
const context = new AudioContext();
await context.audioWorklet.addModule(
'/assets/speech/pcm-worklet.js'
);
const source = context.createMediaStreamSource(stream);
const processor = new AudioWorkletNode(
context,
'pcm-capture'
);
source.connect(processor);
// Connect the worklet through an inaudible output path
// if required to keep the audio processing graph active.マイクの制約条件は希望値であって、動作の保証ではありません。ブラウザーによって、エコーキャンセル、ノイズ抑制、自動ゲイン制御のアルゴリズムが異なる場合があります。
重要なのは、マイクのサンプルレートが必ずしも16 kHzではないことです。多くのAudioContextは44.1 kHzや48 kHzを使用します。Whisperは16 kHzの入力を想定しているため、取得したサンプルを適切にリサンプリングする必要があります。
2個または3個おきにサンプルを間引くだけでは、一般に正しい処理になりません。窓関数付きsinc方式などの適切なローパス・リサンプラーを使えば、サンプルレートを下げる際のエイリアシングを抑え、音声認識に必要な周波数成分を保てます。
リサンプリングは、リアルタイムのAudioWorkletコールバックではなく専用ワーカー内で行います。
ブラウザースレッド間で音声を効率よく転送する
AudioWorkletは多数の小さな音声チャンクを生成することがあります。音声レンダリングの各処理単位をすべて個別メッセージとして送ると、不要なオーバーヘッドが発生する可能性があります。
実用的なのは、PCMを短いバッチ単位にまとめ、各バッチをArrayBufferとして転送する方法です。
worker.postMessage(
{
type: 'audio-chunk',
buffer,
},
[buffer]
);転送リストが重要なのは、ArrayBuffer全体を余分にコピーせず、その所有権を移せるからです。
ワーカーは受信したチャンクを音声の蓄積領域に追加し、録音停止時に結合できます。
実際の録音サンプルレート、収集したサンプル数、録音時間を記録することも有用です。これらのメタデータがあれば、機密性の高い音声データをログに残さずに、無音の録音を診断できます。
別のニューラルネットワークを追加せずに無音区間を検出する
短いメッセージの音声入力には、ニューラルネットワークを使った音声区間検出器が必須とは限りません。
適応型RMS検出器を使えば、音声活動を軽量に推定できます。RMSは信号の振幅を測るため、話していた人が静かになったタイミングをおおよそ判定できます。
マイクボタンをもう一度押して手動停止できる機能は、必ず用意します。無音の自動検出は便利な追加機能であり、認識の成功に不可欠な条件ではありません。
短い音声入力の初期設定値としては、次の数値が考えられます。
| 項目 | 初期値 |
|---|---|
| 解析間隔 | 20 ms |
| ノイズの較正 | 300 ms |
| 発話の確認 | 140 ms |
| 自動停止までの無音時間 | 1.4秒 |
| 発話なしのタイムアウト | 8秒 |
| 録音時間の上限 | 30秒 |
これらは設定の出発点であり、あらゆる環境で最適な閾値ではありません。小声、背景音楽、マイクのノイズ処理はRMSの測定結果に影響します。
文末の小さな声を自動停止で切ってしまうより、ユーザーが手動で録音を止められることを優先してください。
一般的な短文入力では、途中結果を得るためにWhisperを繰り返し実行するより、録音を1回で文字起こしする方が通常は単純です。発話を録音し、停止後にモデルを1回実行して、最終的な認識結果を入力します。
Whisperの完全なパイプラインにはONNXモデル以外も必要
これは実装上、特に重要な点の1つです。
Whisperの推論は、ONNXエンコーダーを呼び出してからONNXデコーダーを呼ぶだけではありません。完全な自動音声認識パイプラインには、音声特徴量の抽出、トークン化、生成設定、予測トークンを読みやすいテキストに戻す処理も必要です。
処理全体の流れは次のとおりです。
Raw microphone PCM
|
v
Resampling to 16 kHz
|
v
Whisper feature extractor
|
v
ONNX encoder
|
v
ONNX decoder and token generation
|
v
Whisper tokenizer
|
v
Recognized textONNXセッションの初期化成功が証明するのは、モデルのグラフを読み込めることだけです。音声認識パイプライン全体の動作を証明するものではありません。
検証したTransformers.jsのある統合例では、モデルのセッションは初期化できたものの、ローカルで構築したパイプラインに必要なプロセッサーとトークナイザーが正しく設定されておらず、文字起こしに失敗しました。
互換性のある実装では、ローカルのモデルメタデータを使って、これらのコンポーネントを明示的に初期化しました。
以下はそのバージョン固有の方法を簡略化した例であり、単独で動作する完全なワーカーではありません。
import {
WhisperTokenizer,
WhisperFeatureExtractor,
WhisperProcessor,
} from '@huggingface/transformers';
// These values come from local model configuration files.
const tokenizer = new WhisperTokenizer(
tokenizerJson,
tokenizerConfig
);
const featureExtractor = new WhisperFeatureExtractor(
preprocessorConfig
);
const processor = new WhisperProcessor(
{},
{
tokenizer,
feature_extractor: featureExtractor,
},
''
);
// In the affected version-specific integration:
recognizer.tokenizer = tokenizer;
recognizer.processor = processor;コンストラクターの正確なシグネチャーと連携方法は、インストール済みのTransformers.jsのバージョンに照らして確認する必要があります。バージョンやモデルのエクスポートによっては自動的に構築できるため、手動生成が常に必要だと考えてはいけません。
完全なパイプラインが利用できれば、言語とタスクを明示して短文の文字起こしを呼び出せます。
const result = await recognizer(audio16k, {
language: 'en',
task: 'transcribe',
return_timestamps: false,
});ユーザーの言語がアプリケーション側ですでに分かっているなら、明示的に指定することで不要な言語判定を避け、短い録音の動作を予測しやすくできます。
互換性のあるTransformers.jsとONNX Runtimeのバージョンを固定する
もう1つの重要な障害要因は、Transformers.jsとONNX Runtime Webの組み合わせです。
Transformers.jsは上位レベルの認識パイプラインを提供し、ONNX Runtime Webはニューラルネットワークのグラフを実行します。両パッケージは互いに互換であるだけでなく、使用するONNXモデルのエクスポートとも適合していなければなりません。
調査した実装では、開発版のONNX Runtime WebはWhisperのエンコーダーを初期化できましたが、量子化されたデコーダーのセッション生成に失敗しました。エラーはDequantizeLinear演算で量子化スケールが欠けていることに関係していました。
これはマイク権限、ダウンロード、音声形式の問題ではありません。ブラウザーはモデルファイルを受信できていましたが、ランタイムが必要なモデルグラフの1つを初期化できなかったのです。
安定版としてバージョンを固定したONNX Runtime Webでエンコーダーとデコーダーの両方を直接テストしたところ、この特定の非互換性を解消できました。
この互換性チェックが特に有用だったのは、次の2つの主張を切り分けられたからです。
- モデルファイルのダウンロードに成功した。
- ランタイムがモデルファイルから利用可能な推論セッションを正常に生成した。
この2つは同じ意味ではありません。
検証したモデルのエクスポートでは、Transformers.js 4.2.0とONNX Runtime Web 1.24.3の組み合わせが互換でした。より新しい開発版ランタイムでは、量子化デコーダーのグラフで失敗しました。これは特定バージョンでの観察結果であり、古いランタイムの方が常に優れているという証拠ではありません。
間接依存するランタイムのバージョンを上書きするなら、パッケージのロックファイルに正確なバージョンを固定し、上書き理由を記録します。ランタイムを更新するときは、単なるパッケージインストールの成功ではなく、ブラウザーでの推論回帰テスト一式を必須にすべきです。
Whisperを調べる前にワーカーバンドルの形式を確認する
JavaScriptのWeb Workerは、異なるモジュール形式で作成できます。
実際の統合で起きた問題の1つは、アプリケーションがワーカーをESモジュールとして起動していたのに、生成されたワーカーバンドルが従来型ワーカー用のimportScripts()を使っていたことです。
この2つの実行方式に互換性はありません。モジュールワーカーでは、従来型ワーカー専用のコードをそのまま実行できません。
重要なのは、一般的なチュートリアルにあるコンストラクターのオプションをコピーするのではなく、実際に生成されたバンドルに合うWorkerコンストラクターを使うことです。
たとえば、次の選択肢は自動的に同等になるわけではありません。
// Module worker: requires a compatible module bundle.
new Worker(workerUrl, { type: 'module' });
// Classic worker: requires a compatible classic bundle.
new Worker(workerUrl);正しい選択は、バンドラーと生成されたJavaScriptによって決まります。
ワーカー初期化エラーを調べる際は、ブラウザーのConsole、生成されたワーカーファイル、Networkパネルを確認します。ONNX Runtimeの初期化が始まる前に失敗しているのかを切り分けてください。
バンドルされたブラウザー用ライブラリの中には、自身のモジュール位置を基準にURLを組み立てるものもあります。ワーカーに組み込む場合は、元のパッケージ相対URLがそのまま解決されると考えず、実際に生成されたランタイム資産のパスをテストしてください。
WASMとJavaScriptモジュールを正しいMIMEタイプで配信する
JavaScriptやONNXモデルのファイルに完全な互換性があっても、Webサーバーが誤ったレスポンスヘッダーを返すと失敗する場合があります。
具体的には、拡張子.mjsのJavaScriptモジュールがapplication/octet-streamとして配信された例がありました。JavaScriptモジュールを実行するのに適した応答タイプではなかったため、ブラウザーはそのモジュールを拒否しました。
一般的に想定されるレスポンスタイプは次のとおりです。
| ファイル拡張子 | Content-Type |
|---|---|
| .mjs | application/javascript |
| .wasm | application/wasm |
サーバーの設定ファイルだけでなく、本番環境が実際に返すレスポンスを確認してください。
curl -I https://example.com/runtime/speech/v2/runtime.mjs
curl -I https://example.com/runtime/speech/v2/runtime.wasm上のURLは説明用です。選択したランタイムビルドの、実際のバージョン付き資産ファイル名を使用してください。
HTTPステータスが正常なだけでは十分ではありません。MIMEタイプ、Content Security Policy、モジュールの実行形式、誤った資産URLが原因で、ブラウザーがファイルを拒否することがあります。
同じリクエストでも、ブラウザーのタブで直接開くと成功し、実行可能なモジュールやWebAssemblyの依存ファイルとして読み込むと失敗する場合があることを覚えておいてください。
壊れたランタイムをキャッシュに残さないための不変URL運用
Whisperのモデルファイルは容量が大きいため、ブラウザーキャッシュが重要です。
バージョンを含む不変の資産パスにすれば、利用者が音声入力するたびに同じモデルやランタイムをダウンロードする必要がなくなります。
一般的なキャッシュポリシーは次のとおりです。
Cache-Control: public, max-age=31536000, immutableただし、これにはデプロイ上の要件があります。一度URLを不変と宣言したら、その内容を黙って差し替えてはいけません。
ランタイムのバイナリを変更する必要がある場合は、新しいバージョン付きURLで公開します。
/models/speech/v1/
/runtime/speech/v1/
/runtime/speech/v2/ランタイムのパス変更には実際の意味があります。不変URLの内容を差し替えて同じURLを使い続けると、ブラウザーが以前キャッシュした非互換なWebAssemblyバイナリを使用し続ける可能性があります。
JavaScript側の補助コードとWebAssemblyファイルは、同じランタイム配布物から取得することも重要です。異なるバージョンのファイルを混在させると、原因の特定が難しい初期化失敗を招くことがあります。
ビルド時に必要なファイル、サイズ、暗号学的チェックサムを記録したマニフェストを用意すれば、デプロイ前にファイルの不足や不一致を検出できます。
認識ライブラリには自サイトでホストした資産を使用させ、外部からのモデル読み込みを無効にします。そうすれば、ローカルモデルが見つからない場合に、外部モデルホストから意図せずダウンロードするのではなく、明確なエラーになります。
何も動かないのにマイクだけが読み込み中になる理由
開発中によく見られるのは、マイクボタンが権限を要求し、読み込みアニメーションを表示した後、テキストが入力されないままエラー状態に戻るという症状です。
ブラウザー側には、マイクが使用中であると表示されることさえあります。
それでも音声認識が動作している証拠にはなりません。分かるのは、ある時点でマイクによる音声取得が許可されたか、実際に動作していたということだけです。
見た目がほぼ同じ症状でも、原因はそれぞれ独立していることがあります。
- JavaScriptモジュールのMIMEタイプが誤っている。 ブラウザーはランタイムモジュールをダウンロードできても、実行を拒否する。
- ワーカーの実行形式が一致していない。 ビルド済みワーカーが従来型ワーカーのAPIを使用しているのにモジュールワーカーとして起動する、またはその逆になっている。
- ONNXデコーダーに互換性がない。 ランタイムはエンコーダーのセッションを作成できるが、量子化デコーダーのグラフで失敗する。
- 認識パイプラインが不完全。 ONNXセッションは存在するが、トークナイザーや音声プロセッサーが正しく初期化されていない。
これらの問題は個別に診断する必要があります。1つを修正すると、次の問題が表面化することもあります。
デバッグで最も重要なのは、HTTP 200はモデルの初期化成功を意味せず、モデルの初期化成功も文字起こしの成功を意味しないという原則です。
文字起こしに成功しても、その結果がアプリケーションの実際の入力欄に届いたとは限りません。
それぞれの処理境界で、状態を判断できる診断情報が必要です。
音声のプライバシーを守りながら可観測性を確保する
役立つ診断ログを取るために、利用者の発話内容を記録する必要はありません。
たとえば、アプリケーションでは次の技術的状態を追跡できます。
permission-granted
worker-created
runtime-loading
runtime-ready
encoder-ready
decoder-ready
recording-started
pcm-received
recording-stopped
inference-started
inference-completed
result-insertedエラー時には、失敗した段階、安定した技術的エラーコード、機密情報を除去したエラーメタデータを記録します。
モデルの初期化失敗は、マイクの許可拒否や無音の録音と区別できるようにします。ワーカーの障害も、汎用エラーハンドラーに握りつぶされず、開発中に確認できるようにしてください。
マイクのサンプル、認識された文章、トークン列など、音声の内容に当たるものはログに残さないでください。性能テレメトリーには、所要時間の区間、選択したモデルの階層、初期化時間、推論時間、エラーの分類などを使用できます。
これにより、プライバシー要件を損なわずに、マイクが動かない原因をかなり調べやすくなります。
Reactの状態を通して認識結果を入力する
音声認識は、最終的な文字起こし結果が既存のフォーム入力欄に反映されて初めて役に立ちます。
Reactアプリケーションでは、その入力欄がコンポーネントの状態によって制御されていることがよくあります。input.valueを直接書き換えると、フレームワークの再描画処理と衝突する可能性があります。
そこで、認識されたテキストを通常のキー入力と同じ状態更新処理に通します。
連携処理では非同期動作も考慮する必要があります。認識が完了するまでに、ユーザーが文字を追加したり、カーソルを移動したりしている場合があります。
録音開始時に取得した古い入力値を使うと、その後の編集を誤って上書きしてしまう可能性があります。
連携の簡略例を示します。
const insertion = insertTranscript({
currentValue: latestValue,
transcript: recognizedText,
selection: currentSelection,
maxLength: inputLimit,
});
setMessage(insertion.value);
restoreCaret(insertion.caret);テキスト挿入用のヘルパーでは、現在の文章を保持し、必要に応じて選択範囲を置き換え、適切な空白を挿入し、入力欄の最大文字数を守る必要があります。
Unicodeにも注意が必要です。JavaScriptの文字列長はUTF-16のコードユニット数で計算されます。任意の位置で文字列を切ると、サロゲートペアの途中で分断してしまうことがあります。
最も重要なのは、音声認識の結果によってフォームを自動送信しないことです。ユーザーが送信前にテキストを確認して修正できなければなりません。
通常の文字入力を壊さずに障害へ対応する
音声入力機能は、任意で利用する補助機能であるべきです。
マイクの許可が拒否されたり、ブラウザーがモデル用メモリを確保できなかったり、ワーカーがクラッシュしたり、録音に発話が含まれていなかったりしても、通常のテキスト入力は使える状態を維持する必要があります。
マイクのライフサイクルは、次のような明示的な状態で表します。
idle
preparing
listening
recognizing
errorアクセシビリティに配慮したラベルと、分かりやすい状態表示を使ってください。読み込み後にマイクが赤やピンクになるだけでは、利用者にほとんど情報が伝わりません。
キャンセル時には、音声トラック、AudioContext、保留中のバッファー、進行中の録音セッションを適切に片付けます。また、以前キャンセルした録音の遅れて届いた結果が、新しい入力状態を上書きしないようにする必要があります。
大きいモデルがメモリ不足や初期化制限で失敗した場合は、小さいモデルを試す前に完全に解放します。実行できないモデルのダウンロードや初期化をブラウザーが繰り返さないよう、再試行回数も制限してください。
ローカル認識に失敗した際、クラウドの音声認識サービスへ黙って切り替えてはいけません。ローカル処理のみが厳密な要件なら、ローカル認識の失敗はそのままローカルで処理する必要があります。
ブラウザーのパイプライン全体をテストする
TypeScriptのチェックや本番ビルドが成功しても、音声入力が実際に機能する証拠にはなりません。
同様に、ブラウザーがONNXファイルをダウンロードできることを確認しても、両方のモデルセッションが正常に初期化されるとは限りません。
最も有用な回帰テストでは、本番環境と同じワーカー、ランタイムファイル、モデル、プロセッサー、トークナイザーを使って動作を検証します。
内容が分かっている短いPCM録音を実際のブラウザー推論経路に入力し、空でない文字起こし結果が返ることを確認してください。
次に、実際のブラウザーでマイク操作全体をテストします。
- 初回ページ読み込み: マイク使用前に音声認識モデルやランタイム資産がダウンロードされないことを確認する。
- コールド初期化: マイクの権限、モデルのリクエスト、JavaScriptとWASMのMIMEタイプ、ワーカーからの実際の準備完了応答を確認する。
- ONNX初期化: エンコーダーとデコーダーの両方のセッションを作成できるか確認する。
- 音声取得: AudioWorkletが空でないPCMバッファーを生成するか確認する。
- リサンプリング: ワーカーが実際のサンプルレートを受け取り、正しい16 kHzの音声を生成するか確認する。
- 手動停止: マイクボタンを2回目に押すと確実に録音が止まるか確認する。
- 自動停止: 自然な間を検出して、小さな声を切らずに録音を終えられるか確認する。
- 文字起こし: プロセッサー、モデル、トークナイザーを含むパイプライン全体から、認識可能なテキストが返るか確認する。
- 入力欄との連携: 認識された文字列が実際の制御された入力欄に入り、利用者の編集を上書きしないか確認する。
- 障害からの復旧: 権限拒否、ファイル不足、ワーカーエラー、モデル初期化失敗、キャンセル、小さいモデルへのフォールバックをテストする。
- プライバシー: ネットワークリクエストを確認し、マイク音声がサーバーや分析サービスへアップロードされないことを検証する。
- モバイル互換性: 実機でコールドスタート、ウォームスタート、繰り返しの音声入力をテストする。
録音済みPCMによるテストは推論処理の検証に非常に有効ですが、マイクによる音声取得の動作は証明できません。最終的な受け入れテストでは、実際のマイクと実際のUIを使う必要があります。
ブラウザーで安定した音声入力を実現する実用的な構成
短文・多言語・プライバシー重視の音声入力が必要なWebサイトでは、意図的に単純な構成が最も実用的です。
React microphone button
|
v
Permission request
|
v
Lazy model initialization
|
v
AudioWorklet PCM capture
|
v
Adaptive RMS / manual stop
|
v
Dedicated Worker
|
v
16 kHz audio resampling
|
v
Whisper Tiny or Base Q8
|
v
Compatible ONNX Runtime WASM
|
v
Whisper processor + tokenizer
|
v
Final transcript
|
v
Controlled React inputアプリケーションは、バージョン付きのモデルとランタイムファイルを自前でホストし、推論をメインスレッドの外で実行し、不要なモデルのダウンロードを避け、使用後にリソースを解放し、音声認識のためにマイク音声をアップロードしないようにします。
WebGPUによる高速化、ニューラルネットワークによる音声区間検出、途中結果を継続的に出す文字起こし、モデルの階層追加といった高度な機能は、測定結果から複雑化に見合うと判断できた段階で検討できます。
最も大切な教訓は、特定のライブラリのバージョンやモデルのサイズではありません。ブラウザー音声認識パイプラインのあらゆる処理境界を検証することです。
マイクの権限ダイアログが正常に動いても、音声が文字起こしされているとは限りません。モデルをダウンロードできても、デコーダーが初期化されたとは限りません。ONNXセッションが成功しても、トークナイザーが使えるとは限りません。そして文字起こしが成功しても、そのテキストがアプリケーションに正しく挿入されたとは限りません。
訪問者がマイクを押して自然に話し、録音を止め、認識された言葉が編集可能な入力欄に表示される。その一連の操作が、録音を音声認識サーバーに送らずに実現して初めて、この機能は完成したといえます。