ai-lab.org
VoiceScribe

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: tinylang: japaneseinfer 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.tsdecodeToMono16k に閉じ込めていて、UI 側は ArrayBuffer を渡すだけ。 mic 録音側は MediaRecorder で webm の Blob を作って、同じ関数に通します。

3. モデルのキャッシュと初回 UX

@xenova/transformers は IndexedDB に ONNX を自動キャッシュしますが、初回はそれでも 75MB ほどのダウンロードが走ります。 これをユーザーに隠さずに見せるために、

  • progress_callback でモデルのファイル単位の進捗を kind: "download" として UI に流す
  • パーセンテージが取れない時はインジケーターをアニメーション
  • 初回の遅さの理由を画面上のラベルで言語化する

という方針にしました。「重い」と感じさせない一番の近道は なぜ重いかを画面に書く ことだと毎度思います。

4. マイク入力のレベルメーター

録音中は AnalyserNode.getByteTimeDomainDatarequestAnimationFrame で読み続け、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.js v3 の WebGPU バックエンドに切り替えれば、推論速度がさらに上がる

このサービスから言えること

WebAssembly と ONNX があれば、AI モデルそのものを ブラウザの中で完結させる ことは現実的になりました。サービスとしての差別化を、「サーバーに送らない」 という単純で強い一言にまとめられる時代です。 これを面白がってもらえる人に届けば、たぶんプライバシー軸のWebサービスの新しい型として広がります。

Next Action

読んだあとに、そのまま使って確かめる。

この開発ログは VoiceScribe をどう育てているかの記録です。読んだらそのままサービス本体へ戻って、価値を確かめられるようにしています。

Keep Reading, Keep Trying

読むだけで終わらせない主力サービス

改善ログで方向を理解したあとに、そのまま使って価値を確かめやすい主力を並べています。

hack-sim主力

HackSim

/hack-sim

1分で始められるSNS向けハッキング体験シミュレーター

· エンタメ· 公開中· 48 pv
prompt-stock主力

PromptStock

/prompt-stock

使うだけで終わらせず、改善して育てるためのプロンプト実用品ストック

· 改善· 改善して伸ばす· 20 pv

More Logs

次に読みたい改善ログ

記事一覧へ