即録くん(Sokuroku) -- リアルタイム文字起こしアプリの設計を詰めた日
Web会議中にリアルタイムで音声を文字起こしして、会議の最中にAIへテキストを渡せるアプリを作りたい。構想を計画書に落とし込み、Codexにレビューを投げたら致命的な指摘が2件返ってきて、設計を書き直すところまでが今日の作業だった。実装は翌日に持ち越し。
何を作るのか
Zoom/Google Meetで会議している最中に、マイク入力と相手の音声の両方をキャプチャして、Deepgramでリアルタイムに文字起こしする。文字起こし結果はデスクトップアプリの画面に流れ続け、そのテキストをそのままAIツールに渡して議事録や要約を生成できる。名前は「即録くん(Sokuroku)」。
Windows向けにゼロから設計する。macOS版の移植ではなく、Windowsの音声キャプチャAPIの特性に合わせた構成にした。
技術スタック選定 -- ReactからVueへ変更
最初の計画書ではReact 19で書いていた。しかし自分が普段触っているのはVue 3であり、Electronとの連携で余計なハマりを増やしたくないので、Vue 3 + TypeScriptに切り替えた。
最終的なスタックはこうなった:
- フレームワーク: Electron 33+
- フロントエンド: Vue 3 + TypeScript 5.x
- ビルドツール: electron-vite(Vite統合)
- 音声認識: Deepgram Nova-3(WebSocketストリーミング)
- 後処理: Gemini 2.0 Flash(誤字修正・要約)
electron-viteを選んだのは、Viteの開発体験をElectronにそのまま持ち込めるから。Vue + TSのテンプレートが公式に用意されている点も決め手だった。
音声処理パイプラインの設計
設計の核心は「マイクとシステム音声をどう混ぜてDeepgramに流すか」だった。
WindowsではElectronの desktopCapturer APIでシステム音声を取得できる。macOSだとBlackHoleのような仮想オーディオデバイスが必要になるが、Windowsでは追加ソフトなしで動く(はず)。
パイプラインの流れを図にすると:
マイク (48kHz) ──→ GainNode ──┐
├→ ChannelMerger → ダウンサンプル(16kHz) → WebSocket → Deepgram
システム音声 ──→ GainNode ──┘
↓
文字起こし結果
↓
Gemini Flash 誤字修正
↓
画面に表示
ダウンサンプリングにはAudioWorkletを使う。ScriptProcessorNodeは非推奨なので避けた。48kHzのFloat32を16kHzのInt16に変換して、WebSocket経由でDeepgramに送る。
Deepgramの責務はメインプロセスに
APIキーの管理とWebSocket接続はElectronのメインプロセスが担う。レンダラー(Vue側)が直接Deepgramと通信するとAPIキーがブラウザコンテキストに露出するので、IPC経由でやりとりする設計にした。
レンダラー(Vue) → IPC → メインプロセス → WebSocket → Deepgram
↑ ↓
文字起こし結果 ← ──── IPC ← ──── 結果受信
APIキーは electron-store で暗号化してローカル保存する。初回起動時にユーザーが自分のキーを入力する方式。開発時だけ .env を使う。
Codexレビューで致命的指摘が2件
計画書を書き終えてStep 1(スキャフォールド)に着手したところ、いきなり詰まった。pnpm create @electron-vite が対話型CLIで、スクリプトから自動実行できない。手動で入力を送り込もうとしたが失敗し、degit でテンプレートをクローンする方法も試みたがうまくいかない。
ここでCodexにレビューを依頼した。返ってきた指摘は2件、どちらも致命的だった。
指摘1: スキャフォールドが対話型CLIで動かない
計画書のStep 1に書いていた pnpm create @electron-vite は対話型のCLIツールで、CI環境やスクリプト内では動かない。まさに自分が直面していた問題そのものだった。
修正として、create-electron-vite の --template vue-ts オプションを明示的に指定する形に書き換えた:
pnpm create @electron-vite sokuroku --template vue-ts
これで対話プロンプトをスキップできる。
指摘2: Deepgramの責務がrendererとmainで矛盾
計画書のプロジェクト構成ではDeepgramクライアントが renderer/lib/deepgram-client.ts に配置されていたが、APIキー管理のセクションでは「メインプロセスがWebSocket接続を管理」と書いていた。責務がrendererとmainの間で矛盾している。
これを修正して、Deepgramクライアントをメインプロセス側(src/main/deepgram.ts)に移動し、レンダラーはIPC経由でのみやりとりする設計に統一した。Geminiの後処理も同様にメインプロセスで実行し、結果をIPCで返す形に揃えた。
PoC検証ステップの追加
Codexのレビューを受けて、もう一つ計画に追加したのがPoC(概念検証)ステップだった。
desktopCapturer でシステム音声を取得する方式は、Windows 11 + 現行Electronで本当に動くか保証がない。このPoCが通らないと後続の設計が全部崩れる。そこでStep 2(マイク文字起こし)の直後にStep 2.5としてPoCを挟んだ:
- 最小限のElectronアプリで
desktopCapturer.getSources()を呼ぶ - YouTubeを再生しながらシステム音声がAudioTrackとして取れるか確認
- 取得した音声に実際に波形データが入っているか確認
PoCが通らなければWASAPI Loopbackのネイティブモジュールに切り替える。この判定基準を計画書に明記した。
Gemini後処理の設計
Deepgramの文字起こし結果には誤認識がつきもので、特にIT用語や固有名詞がズレる。これをGemini 2.0 Flashで非同期に修正する。
設計はfire-and-forget方式を採った。finalテキストが確定した瞬間にUIへ即座に表示し、裏でGeminiに誤字修正リクエストを投げる。修正結果が返ってきたらUIのテキストを差し替える。UIはブロックされない。
Gemini 2.0 Flashは無料枠で1日1,500リクエスト処理できるので、個人利用ならコストは実質ゼロ。Deepgramも$200分の無料クレジットがあり、約775時間分の文字起こしに相当する。
今日やったこと・明日やること
計画書のVue.js化とCodexレビュー反映まで完了した。計画書は sokuroku/memo/2026-03-18/sokuroku-win-plan.md に保存してある。
明日はStep 1(create-electron-vite --template vue-ts でスキャフォールド)から着手して、Step 2(マイク音声のリアルタイム文字起こし)までを目指す。
Codexに指摘されるまで、対話型CLIの問題とDeepgramの責務矛盾に気づいていなかった。計画段階でレビューを挟んだおかげで、実装に入ってから手戻りするのを避けられた。設計書は書いた本人の盲点を突かれてこそ価値がある。