VoiceScribe ができるまで — ブラウザだけでWhisperを動かして音声を文字に変える設計
transformers.js でWhisperをブラウザ内推論し、音声をサーバーに送らずに文字起こしするWebサービスの設計記録。モデルキャッシュ、長尺対応、字幕出力、UXの工夫を解説します。
VoiceScribe ができるまで — ブラウザだけで Whisper を動かして音声を文字に変える
VoiceScribe は、音声ファイルやマイク録音を 一切サーバーに送らず に文字起こしできる無料サービスです。Whisper モデルをブラウザ内で直接動かす、というそれだけの構造を、実用品のレベルまで成立させることを狙いました。
なぜこの形にしたか
「音声をアップロードして書き起こすサービス」は世の中にたくさんあります。でも、会議録、取材音声、家族の通話、医療相談、面接の予習音声など、他人のサーバーに送るのが本当は嫌な音声 はその何倍もあります。
そのギャップを、サーバーに送らずに文字起こしできる形で埋められるなら、それは説明不要の差別化になると考えました。
Whisper は OpenAI が公開している強力な音声認識モデルです。これを transformers.js (Hugging Face が公開している @xenova/transformers) 経由で ONNX Runtime Web に載せると、現代のブラウザは普通に推論まで完走します。 2026 年現在、これが普通に動くという事実そのものが、サービスのコンセプトになり得ます。
visual direction
「オーディオラボのコンソール」。
- 深い炭色 (
#07090b) に、ミントシアン (#3df5c1) のターミナルカーソル色を主アクセント - 録音中だけホットマゼンタ (
#ff3d6e) に切り替わる - 縦長の波形バーが録音音量に反応してリアルタイムに伸び縮みする
- モノスペースで
model: tiny、lang: japanese、infer 1280 msのような制御情報を冷たく見せる - 全部「サーバーに送らない」という主張を補強する方向に揃える
実装の見どころ
1. すべてが client component
page.tsx は Server Component のままにして、SEO 用のメタデータ・JSON-LD・How it works・FAQ を SSR で出します。本体のアプリは next/dynamic + ssr: false で client にだけロードします。 transformers.js は ESM サイズも大きく、ONNX や WebAssembly のロードを伴うので、サーバー側で触らせない設計が必須でした。
const VoiceScribeApp = dynamic(
() => import("@/components/services/voice-scribe/VoiceScribeApp").then((m) => m.VoiceScribeApp),
{ ssr: false, loading: () => null },
)
2. オーディオを 16kHz モノラルに変換するパイプライン
Whisper の入力は Float32Array @ 16kHz mono。 そこに合わせる前処理を全部ブラウザでやります。
File.arrayBuffer()で生バイトを取り出すAudioContext.decodeAudioData(buffer)でAudioBufferに変換 (mp3 / wav / m4a / webm / ogg, ブラウザが読める形式すべて対応)- 全チャンネルを単純平均してモノラル化
OfflineAudioContextで 16kHz にリサンプル
この前処理を whisper.ts の decodeToMono16k に閉じ込めていて、UI 側は ArrayBuffer を渡すだけ。 mic 録音側は MediaRecorder で webm の Blob を作って、同じ関数に通します。
3. モデルのキャッシュと初回 UX
@xenova/transformers は IndexedDB に ONNX を自動キャッシュしますが、初回はそれでも 75MB ほどのダウンロードが走ります。 これをユーザーに隠さずに見せるために、
progress_callbackでモデルのファイル単位の進捗をkind: "download"として UI に流す- パーセンテージが取れない時はインジケーターをアニメーション
- 初回の遅さの理由を画面上のラベルで言語化する
という方針にしました。「重い」と感じさせない一番の近道は なぜ重いかを画面に書く ことだと毎度思います。
4. マイク入力のレベルメーター
録音中は AnalyserNode.getByteTimeDomainData を requestAnimationFrame で読み続け、RMS から計算した level (0..1) を CSS の height に当てる構成にしました。 縦長のミントシアンのバーが中央に向かって膨らむ、典型的なラジオブースの可視化です。 「録音されているかどうかが顔色だけで分かる」 感覚を作りたかった。
5. タイムコード付きトランスクリプトと SRT 出力
Whisper は return_timestamps: true で chunk ごとに [start, end] を返してきます。 そのタイムコードを mm:ss で UI に並べつつ、SRT (字幕ファイル) としても書き出せるようにしました。 取材音声 → 字幕案、というワークフローまで一気に完結します。
chunks: TranscriptChunk[]
// → 表示用に mm:ss、ダウンロード用に 00:00:00,000
苦労したところ
- Next.js 16 + transformers.js は SSR を避けないと爆発する。最初に普通に import したら ONNX のバイナリ周りで死ぬ。
dynamic({ ssr: false })必須。 - decodeAudioData の format 互換性。M1 Mac の Safari で AAC + m4a が読めないケースがあるので、エラーを握りつぶさず素直にユーザーに見せる。
- モデルサイズの心理障壁。 70MB は速度的にはすぐだが、最初に何も言わずローダーだけ回すと不信感を生む。 「初回のみ ~75MB / 以降はオフライン」を最初から見せる。
今後の拡張
- 長尺音声の分割推論: 現状 30 分前後を上限としているのを、ファイル全体を 5 分単位に区切って progressive に走らせるモードに拡張する
- 多言語翻訳: Whisper の
task: "translate"を切り替えるだけで翻訳テキストが取れる。日本語 → 英語の議事録など、業務導線にちょうど良い - ヒント語彙: 固有名詞や専門用語を初期プロンプトとして与え、誤認識を減らす
- 長尺向けには WebGPU バックエンド:
transformers.jsv3 の WebGPU バックエンドに切り替えれば、推論速度がさらに上がる
このサービスから言えること
WebAssembly と ONNX があれば、AI モデルそのものを ブラウザの中で完結させる ことは現実的になりました。サービスとしての差別化を、「サーバーに送らない」 という単純で強い一言にまとめられる時代です。 これを面白がってもらえる人に届けば、たぶんプライバシー軸のWebサービスの新しい型として広がります。
Next Action
読んだあとに、そのまま使って確かめる。
この開発ログは VoiceScribe をどう育てているかの記録です。読んだらそのままサービス本体へ戻って、価値を確かめられるようにしています。