会議にAIを同席させたら、議論の質が変わった — Meeting Companion PoC の全貌
会議の音声をリアルタイムで文字起こしし、AIが論点・未解決事項・矛盾をマインドマップで可視化する伴走ツールのPoC。設計思想・プロンプト設計・CRDT状態管理・ダブルダイアモンドモードまで、技術的な仕組みを解説。
会議中、こんな経験はないだろうか。
- 「さっき誰かが言った数字、本当に合ってる?」
- 「話が発散しすぎてるけど、誰も止めない」
- 「結局何が決まって、何が決まってないんだっけ?」
Meeting Companion は、これらの問題をリアルタイムAI分析で解決しようとするPoCだ。会議の音声を文字起こししながら、AIが「論点」「未解決事項」「検証すべき主張」「気になる矛盾」をマインドマップ形式で可視化する。さらに、気になる項目をクリックすれば、その場でAIが裏取り調査まで行う。
この記事では、このツールの設計思想と技術的な仕組みを解説する。
全体アーキテクチャ
┌─────────────┐ Web Speech API ┌──────────────────┐
│ マイク入力 │ ───────────────────────→ │ ブラウザ │
└─────────────┘ │ (index.html) │
│ │
│ ┌─────────────┐ │
│ │ Yjs (CRDT) │ │
│ │ 状態管理 │ │
│ └──────┬──────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Markmap │ │
│ │ マインドマップ│ │
│ └─────────────┘ │
└────────┬──────────┘
│ fetch (10秒ごと)
┌────────▼──────────┐
│ Express サーバ │
│ (server.js) │
└────────┬──────────┘
│
┌────────▼──────────┐
│ Claude API │
│ (Sonnet 4.6) │
└───────────────────┘
技術スタックはシンプルだ。
| レイヤー | 技術 | 役割 |
|---|---|---|
| 音声認識 | Web Speech API | ブラウザ内で音声→テキスト変換 |
| フロントエンド | バニラJS + Markmap + Yjs | UI・マインドマップ描画・CRDT状態管理 |
| バックエンド | Node.js + Express | Claude APIプロキシ |
| AI分析 | Claude Sonnet 4.6 | リアルタイム議論分析・調査 |
フロントエンドはライブラリ管理のためにd3・Markmap・Yjsを使っているが、ReactやVueなどのフレームワークは使わず、単一HTMLファイルに全てを収めている。PoCとして「とりあえず動くものを最速で」という設計原則を貫いた結果だ。
音声認識:Web Speech APIの現実
音声認識にはブラウザ標準の Web Speech API を採用した。Whisper等の外部サービスを使わないため、サーバ負荷ゼロ・レイテンシゼロで文字起こしが始まる。
const recognition = new SpeechRecognition();
recognition.lang = "ja-JP";
recognition.continuous = true;
recognition.interimResults = true;
ただし、Web Speech APIには癖がある。
continuous = true でも止まる問題
長時間使うと、ブラウザが勝手に認識を止める。対策として onend イベントで自動再起動する仕組みを入れた。ユーザーが明示的に「停止」を押したときだけ再起動しないよう、フラグで制御している。
recognition.onend = () => {
if (shouldRestart) {
try { recognition.start(); } catch {}
} else {
setRecordingState(false);
}
};
確定結果と暫定結果の分離
Web Speech APIは認識途中の暫定結果(isFinal = false)と確定結果(isFinal = true)を返す。暫定結果はグレーで表示し、確定結果だけを分析対象のバッファに溜める。確定結果ごとに改行するので、発話の切れ目が自然に議事録として残る。
システム音声を拾う場合
Web Speech APIはマイク入力のみ対応なので、会議相手(Zoom等)の音声を拾うにはBlackHole(macOS)やVB-Audio Cable(Windows)といった仮想オーディオデバイスが必要になる。PoC段階ではここは割り切り、自分の声だけでも検証可能とした。
AI分析:3つのエンドポイント
サーバ側のAPIは3つ。全て Claude Sonnet 4.6 を使っている。
1. POST /api/analyze — リアルタイム分析
10秒ごとに呼ばれるメインの分析エンドポイント。直近の文字起こし全文と、過去の分析結果(直近5件)をコンテキストとして渡す。
Claude への指示(システムプロンプト)は以下のような構成だ。
あなたは会議の議論をリアルタイムで観察する伴走アナリストです。
直近の発話内容を読み、以下4項目を簡潔にJSONで返してください。
1. summary: 直近で話されている論点(最大2文)
2. openIssues: 未解決のまま流れそうな論点
3. claimsToVerify: データや事実確認が必要そうな主張
4. contradictions: 過去の発言や一般知識との矛盾点
重要な制約:
- 凡庸な指摘(議事録レベルの要約)は避ける
- 各項目は短く、参加者が3秒で読めること
- 該当なしなら空配列を返す(無理に埋めない)
「凡庸な指摘は避ける」「無理に埋めない」 という制約が重要だ。AIに「とにかく何か出して」と指示すると、議事録をなぞっただけの無意味な出力が並ぶ。本当に価値のある指摘だけに絞ることで、参加者が画面をチラ見したときに「おっ」と思える情報密度を目指した。
2. POST /api/analyze-diamond — ダブルダイアモンド分析
デザイン思考のフレームワーク「ダブルダイアモンド」に沿った分析モード。通常の4項目に加えて、以下を返す。
- phase: 現在の議論フェーズ(discover / define / develop / deliver)
- phaseAlert: フェーズ逸脱の警告(例:「まだ課題探索段階なのに解決策の議論が始まっています」)
- phaseAdvice: フェーズに適した具体的アドバイス
このモードの真価は フェーズ逸脱アラート にある。「それ今じゃなくない?」と人間が言いにくい場面で、AIが代わりに指摘してくれる。
3. POST /api/investigate — 深掘り調査
マインドマップ上の任意のノードをクリックすると呼ばれる。主張のファクトチェック、論点の妥当性評価、未解決事項への示唆などを、確信度(high / medium / low)付きで返す。
{
"verdict": "部分的に疑わしい。複合要因の可能性が高い。",
"confidence": "high",
"reason": "リテンション低下や競合流出など他の要因も考慮すべき"
}
「検証すべき主張」「気になる矛盾」のカテゴリに入った項目は、自動的に裏取り調査が走る。論点や未解決事項は、ユーザーが気になったときにクリックで調査を実行する設計にした。
マインドマップ:Markmapによる可視化
分析結果の表示には Markmap を採用した。
当初は自前のSVGレイアウトを書いていたが、ノード数が増えると重なりが発生する問題を解決できなかった。Markmapは木構造の自動レイアウトを持っており、ノードが増えても重ならない。
ツリー構造はこうなる。
会議(中心ノード)
├── 論点(青)
│ ├── "B2Cプロダクトの新機能の優先度"
│ └── "DAU 15%減少の原因分析"
├── 未解決(オレンジ)
│ └── "チュートリアル刷新 vs プッシュ通知改善の優先度"
├── 検証すべき主張(紫)
│ └── "DAU減少の主因がオンボーディング離脱率"
│ └── [HIGH] 部分的に疑わしい... ← 調査結果が子ノードに
└── 気になる矛盾(赤)
└── "田中さんの主張が前回と転換"
└── [HIGH] 明確な矛盾...
調査結果は子ノードとして展開される。Markmapが自動でレイアウトしてくれるので、調査結果が増えてもレイアウトが崩れない。
ノードの蓄積と重複排除
分析は10秒ごとに実行されるが、同じ話題が繰り返し検出されることがある。類似テキストの重複排除ロジックを入れて、既存ノードの更新と新規ノードの追加を判別している。
function isSimilar(a, b) {
if (a === b) return true;
const na = a.replace(/\s+/g, "");
const nb = b.replace(/\s+/g, "");
if (na === nb) return true;
const short = na.length < nb.length ? na : nb;
const long = na.length < nb.length ? nb : na;
const w = Math.floor(short.length * 0.4);
if (w < 4) return false;
const c1 = short.substring(0, w);
const mid = Math.floor((short.length - w) / 2);
const c2 = short.substring(mid, mid + w);
return long.includes(c1) && long.includes(c2);
}
テキストの先頭40%と中央40%の両方が長い方の文字列に含まれていれば「同じ話題」と判定する。完全一致ではなく部分一致にすることで、Claudeが微妙に表現を変えて返してきても同一トピックとして扱える。
CRDT:Yjsによる競合のない状態管理
ユーザーが手動でノードを追加する操作と、AIが分析結果を書き込む操作が同時に起きたらどうなるか。この競合を根本的に解決するために、Yjs(CRDTライブラリ)を導入した。
なぜCRDTなのか
通常のJSオブジェクトで状態管理する場合、以下の競合シナリオが起こりうる。
- ユーザーが「論点」に項目を手動追加
- 同時にAI分析が完了し、
updateMapState()が全項目のactiveフラグを書き換え - ユーザーの追加が上書きされる、またはactiveフラグが不整合になる
CRDTなら、これらの操作がそれぞれ独立した「操作ログ」として記録され、どの順序で適用しても最終状態が一致する。
実装パターン
状態は Y.Doc → Y.Map → Y.Array<Y.Map> の階層で管理している。
const ydoc = new Y.Doc();
const yMapState = ydoc.getMap("mapState");
// 4カテゴリそれぞれがY.Array
for (const cat of CATEGORIES) {
yMapState.set(cat, new Y.Array());
}
各ノードは Y.Map として表現され、フィールド単位で個別に更新できる。
function createYItem(item) {
const yItem = new Y.Map();
for (const [k, v] of Object.entries(item)) yItem.set(k, v);
return yItem;
}
AI分析による一括更新は ydoc.transact() でラップする。これにより、複数の書き込みがアトミックに実行され、observerの発火も1回で済む。
function updateMapState(analysis) {
ydoc.transact(() => {
// 全ノードをinactiveに
// → 新しい分析結果とマージ
// → 一致するものはactive更新、新規はpush
});
}
自動再描画
Yjsの observeDeep を使って、状態変更を検知したら自動的にマインドマップを再描画する。
let _renderTimer = null;
yMapState.observeDeep(() => {
if (_renderTimer) clearTimeout(_renderTimer);
_renderTimer = setTimeout(renderMindMap, 16); // 1フレーム分debounce
});
これにより、ユーザー操作(手動追加)もAI更新(分析・調査)も、書き込み側はYjsに書くだけで、描画は自動的に追従する。明示的な renderMindMap() 呼び出しを各所に書く必要がなくなった。
将来の拡張性
現在はブラウザ内のインメモリのみだが、Yjsの設計上、以下の拡張がそのまま可能だ。
- y-websocket: WebSocketプロバイダを追加するだけで、複数ブラウザ間のリアルタイム同期
- y-indexeddb: ローカルストレージに永続化。ページリロードしても状態が残る
- y-webrtc: サーバレスのP2P同期
コードの変更は「プロバイダを1行追加する」だけで、既存のロジックは一切変わらない。これがCRDTの強みだ。
ダブルダイアモンドモード
通常モードとは別に、デザイン思考の「ダブルダイアモンド」フレームワークに沿って会議を分析するモードを搭載した。
◇ Discover ◇ Define ◇ Develop ◇ Deliver
(発見) (定義) (展開) (実行)
↑ 発散 収束 ↓ ↑ 発散 収束 ↓
4つのフェーズ
| フェーズ | 目的 | AIが着目すること |
|---|---|---|
| Discover | 課題を広く探索 | まだ出ていない視点、深掘りすべき問い |
| Define | 解くべき問題を定義 | 問題の言語化の曖昧さ、合意できてない前提 |
| Develop | 解決策を発散 | アイデアの比較軸、見落としている選択肢 |
| Deliver | 実行計画に収束 | 決まっていないこと、ネクストアクションの抜け |
フェーズ逸脱アラート
最も価値が高いのはこの機能だ。例えば、Discoverフェーズ(課題探索中)なのに「じゃあチュートリアルを刷新しましょう」という解決策の議論が始まると:
⚠ まだ課題の探索段階です。具体的な解決策の議論は Define → Develop フェーズで行いましょう。
と警告が表示される。会議のファシリテーションで「それ今の議題じゃないですよね」と言うのは勇気がいるが、AIなら遠慮なく指摘できる。
エクスポート機能
「エクスポート」ボタンを押すと、以下を含むスタンドアロンHTMLファイルがダウンロードされる。
- マインドマップ — SVGがそのまま埋め込まれる
- 議事録 — 発話ごとに段落分けされたテキスト
- AI分析サマリー — カテゴリ別のリスト、調査結果(確信度バッジ付き)
ブラウザで開けばそのまま見れるし、印刷もできる。外部依存なしの1ファイル完結だ。
セッション保存:MCX フォーマット
HTMLエクスポートは「共有用」だが、途中の会議を中断して後で再開したいケースもある。そこで、独自のXMLベースのセッションファイル .mcx(Meeting Companion XML)を設計した。
フォーマット仕様
<?xml version="1.0" encoding="UTF-8"?>
<meeting-companion version="1" exported="2026-04-25T22:00:00Z" mode="normal">
<transcript>
<line>今日はDAU改善について議論します</line>
<line>オンボーディングの離脱率が高い</line>
</transcript>
<mindmap>
<category key="summary" label="論点">
<item id="1" active="true" added="1745600000000">
<text>DAU改善施策の議論</text>
</item>
</category>
<category key="claims" label="検証すべき主張">
<item id="2" active="true" added="1745600000000">
<text>離脱率が主因</text>
<investigation confidence="high">
<verdict>部分的に疑わしい</verdict>
<reason>複合要因の可能性</reason>
</investigation>
</item>
</category>
</mindmap>
<diamond phase="discover">
<entry phase="discover" time="1745600000000" />
<entry phase="define" time="1745601800000" />
</diamond>
</meeting-companion>
保存される情報:
- transcript — 発話テキスト(行単位)
- mindmap — 4カテゴリの全ノード(テキスト、active状態、調査結果込み)
- diamond — ダブルダイアモンドのフェーズ履歴(モード使用時のみ)
なぜ JSON ではなく XML か
JSONでも良かったが、あえてXMLにした理由がある。
- スキーマが自己文書化される: タグ名と属性名を見れば構造がわかる。
<investigation confidence="high">は{"investigation": {"confidence": "high"}}より意図が明確 - 将来的なスキーマバリデーション: XSDやRelaxNGで厳密な検証ができる
- 部分読み込みが容易: DOMParserで特定のセクションだけ読める
- 差分が見やすい: gitでの差分表示がJSONより読みやすい
使い方
ヘッダーの「保存(.mcx)」ボタンでダウンロード、「読込」ボタンで .mcx ファイルを選択すると全状態が復元される。議事録もマインドマップも調査結果もそのまま戻るので、録音を再開すれば中断した会議の続きをそのまま分析できる。
コスト感
Claude Sonnet 4.6 を使った場合の1会議(1時間)あたりの概算:
| インターバル | 分析回数 | 調査回数 | 概算コスト |
|---|---|---|---|
| 60秒 | ~8回 | ~15回 | ~$0.25 |
| 10秒 | ~50回 | ~100回 | ~$1.50 |
Haiku 4.5 に切り替えれば1/4程度になるが、分析の深さが落ちる。分析はSonnet、調査だけOpusという使い分けも可能。
ファイル構成
meeting-companion-poc/
├── package.json # 依存: express, anthropic-sdk, yjs, markmap-view, d3
├── server.js # Express + Claude API プロキシ (3エンドポイント)
├── .env # ANTHROPIC_API_KEY
├── public/
│ ├── index.html # フロントエンド全部入り (~900行)
│ └── vendor/
│ └── yjs-bundle.js # esbuildでバンドルしたYjs
└── README.md
npm run dev で起動。esbuildがYjsをブラウザ用にバンドルしてからExpressサーバが立ち上がる。
今後の課題
PoCとして動くものはできたが、本格運用には以下が必要になる。
- 音声認識の精度: Web Speech API は無料だが精度に限界がある。Whisper API や Deepgram への切り替えで大幅に改善できる
- 話者分離: 現状は全発言が1つのストリームに入る。話者分離(diarization)があれば「誰が何を言ったか」の追跡が可能に
- 分析プロンプトの調整: 会議の種類(ブレスト、定例、1on1)によって最適なプロンプトは異なる
- マルチクライアント同期: Yjsの基盤はあるので、y-websocketを追加すれば複数参加者が同じマインドマップをリアルタイムで見れる
- データソース連携: Slack、Notion、社内wikiの情報を参照して、より文脈のある調査結果を返す
まとめ
Meeting Companion は「AIを会議に同席させる」というコンセプトのPoCだ。
技術的には、Web Speech API → Claude API → Markmap → Yjs という比較的シンプルなパイプラインだが、プロンプト設計(凡庸な指摘を避ける、該当なしは空配列)とUX設計(3秒で読める、クリックで深掘り、邪魔しないローディング)の組み合わせで、実用的な体験になっている。
「AIに議事録を取らせる」ツールは既に多い。このPoCが目指したのは、その一歩先 — AIが議論の”質”に介入する世界だ。