[{"data":1,"prerenderedAt":710},["ShallowReactive",2],{"content-/realtime-meeting-summarizer":3,"all-pages-for-dir":708,"og-image-/realtime-meeting-summarizer":709},{"id":4,"title":5,"body":6,"category":689,"description":690,"extension":691,"meta":692,"navigation":269,"ogImage":693,"path":694,"project_name":695,"published":696,"publishedAt":697,"seo":698,"stem":699,"tags":700,"todo":706,"unpublished":696,"updatedAt":693,"__hash__":707},"pages/2026-03/2026-03-22/realtime-meeting-summarizer.md","Claude Codeでリアルタイム会議要約システムを構築した記録",{"type":7,"value":8,"toc":669},"minimark",[9,13,17,21,32,39,43,46,55,65,72,75,78,197,205,208,211,214,358,361,365,372,375,513,526,530,533,536,545,549,555,601,604,608,611,614,630,636,639,643,650,653,656,660,665],[10,11,5],"h1",{"id":12},"claude-codeでリアルタイム会議要約システムを構築した記録",[14,15,16],"p",{},"即録くん（リアルタイム文字起こしアプリ）が出力するテキストログを、Claude Codeが3分おきに読み取って要約し、Nuxtアプリに表示する仕組みを作った。保険営業のロールプレイ（約45分）を題材にテストし、会議の進行に追従して要約が更新されていく動作を確認できた。",[18,19,20],"h2",{"id":20},"全体構成",[22,23,28],"pre",{"className":24,"code":26,"language":27},[25],"language-text","即録くん(Windows) → テキストログ → Claude Code(devcontainer) → minutes.json → Nuxtアプリ(3秒ポーリング)\n","text",[29,30,26],"code",{"__ignoreMap":31},"",[14,33,34,35,38],{},"Claude Codeの ",[29,36,37],{},"/loop 3m /summarize-meeting"," コマンドでCronCreateを使い、3分間隔の自動実行を設定する。summarize-meetingスキルがログファイルを読み取り、要約をJSONに書き出す。NuxtアプリがそのJSONを3秒ごとにフェッチして画面に反映する。",[18,40,42],{"id":41},"devcontainerとwindowsのパス問題","devcontainerとWindowsのパス問題",[14,44,45],{},"最初に手が止まった。即録くんはWindows側で動いており、ログファイルはWindowsのファイルシステムに書き出される。一方、Claude Codeはdevcontainer（Linux）の中で動いている。そもそもログファイルを読めるのか。",[47,48,50,51,54],"h3",{"id":49},"発見-workspacelogs-からアクセスできた","発見: ",[29,52,53],{},"/workspace/logs/"," からアクセスできた",[14,56,57,58,61,62,64],{},"devcontainerのマウント設定を確認すると、Windowsのプロジェクトディレクトリが ",[29,59,60],{},"/workspace/"," にマウントされていた。即録くんのログ出力先をこのディレクトリ配下に設定すれば、devcontainer内から ",[29,63,53],{}," のパスで読み取れる。",[14,66,67,68,71],{},"実際に ",[29,69,70],{},"ls /workspace/logs/"," を実行し、即録くんが書き出したログファイルが見えることを確認した。",[47,73,74],{"id":74},"環境判定ロジックの追加",[14,76,77],{},"問題は、summarize-meetingスキルがWindows環境でもdevcontainer環境でも動く必要があること。ログファイルのパスが環境によって異なるため、実行環境を自動判定するロジックを組み込んだ。",[22,79,83],{"className":80,"code":81,"language":82,"meta":31,"style":31},"language-bash shiki shiki-themes vitesse-light vitesse-light","# Platform: linux かつ /workspace が存在 → devコンテナと判定\nif [[ \"$(uname)\" == \"Linux\" && -d \"/workspace\" ]]; then\n  LOG_DIR=\"/workspace/logs\"\nelse\n  LOG_DIR=\"C:/Users/numbe/path/to/logs\"\nfi\n","bash",[29,84,85,94,153,171,177,191],{"__ignoreMap":31},[86,87,90],"span",{"class":88,"line":89},"line",1,[86,91,93],{"class":92},"sxvE3","# Platform: linux かつ /workspace が存在 → devコンテナと判定\n",[86,95,97,101,105,109,112,116,119,122,126,128,132,134,137,140,142,145,147,150],{"class":88,"line":96},2,[86,98,100],{"class":99},"sHkkW","if",[86,102,104],{"class":103},"shFtX"," [[",[86,106,108],{"class":107},"sMJiu"," \"",[86,110,111],{"class":103},"$(",[86,113,115],{"class":114},"senZ8","uname",[86,117,118],{"class":103},")",[86,120,121],{"class":107},"\"",[86,123,125],{"class":124},"stQ0i"," ==",[86,127,108],{"class":107},[86,129,131],{"class":130},"sdGka","Linux",[86,133,121],{"class":107},[86,135,136],{"class":124}," &&",[86,138,139],{"class":124}," -d",[86,141,108],{"class":107},[86,143,144],{"class":130},"/workspace",[86,146,121],{"class":107},[86,148,149],{"class":103}," ]];",[86,151,152],{"class":99}," then\n",[86,154,156,160,163,165,168],{"class":88,"line":155},3,[86,157,159],{"class":158},"s4oTP","  LOG_DIR",[86,161,162],{"class":103},"=",[86,164,121],{"class":107},[86,166,167],{"class":130},"/workspace/logs",[86,169,170],{"class":107},"\"\n",[86,172,174],{"class":88,"line":173},4,[86,175,176],{"class":99},"else\n",[86,178,180,182,184,186,189],{"class":88,"line":179},5,[86,181,159],{"class":158},[86,183,162],{"class":103},[86,185,121],{"class":107},[86,187,188],{"class":130},"C:/Users/numbe/path/to/logs",[86,190,170],{"class":107},[86,192,194],{"class":88,"line":193},6,[86,195,196],{"class":99},"fi\n",[14,198,199,201,202,204],{},[29,200,115],{}," がLinuxを返し、かつ ",[29,203,144],{}," ディレクトリが存在する場合をdevコンテナと判定する。この2条件の組み合わせで、通常のLinux環境とdevcontainerを区別できた。",[18,206,207],{"id":207},"差分モードの実装",[14,209,210],{},"ログファイルは即録くんが逐次追記していく。3分ごとの実行で毎回ファイル全体を処理すると、同じ内容を繰り返し要約することになる。",[14,212,213],{},"そこで差分モードを実装した。処理済みの行数を記録しておき、次回実行時には新規追加分のみを読み取る。",[22,215,217],{"className":80,"code":216,"language":82,"meta":31,"style":31},"# 前回処理済みの行数を読み込む\nLAST_LINE=$(cat \"$STATE_FILE\" 2>/dev/null || echo \"0\")\n\n# 新規追加分のみ取得\nNEW_LINES=$(tail -n +\"$((LAST_LINE + 1))\" \"$LOG_FILE\")\n\n# 今回の総行数を記録\nwc -l \u003C \"$LOG_FILE\" > \"$STATE_FILE\"\n",[29,218,219,224,265,271,276,321,325,331],{"__ignoreMap":31},[86,220,221],{"class":88,"line":89},[86,222,223],{"class":92},"# 前回処理済みの行数を読み込む\n",[86,225,226,229,232,235,237,240,242,245,248,251,255,257,260,262],{"class":88,"line":96},[86,227,228],{"class":158},"LAST_LINE",[86,230,231],{"class":103},"=$(",[86,233,234],{"class":114},"cat",[86,236,108],{"class":107},[86,238,239],{"class":130},"$STATE_FILE",[86,241,121],{"class":107},[86,243,244],{"class":124}," 2>",[86,246,247],{"class":130},"/dev/null",[86,249,250],{"class":124}," ||",[86,252,254],{"class":253},"sz8Xr"," echo",[86,256,108],{"class":107},[86,258,259],{"class":130},"0",[86,261,121],{"class":107},[86,263,264],{"class":103},")\n",[86,266,267],{"class":88,"line":155},[86,268,270],{"emptyLinePlaceholder":269},true,"\n",[86,272,273],{"class":88,"line":173},[86,274,275],{"class":92},"# 新規追加分のみ取得\n",[86,277,278,281,283,286,290,293,295,298,300,303,307,310,312,314,317,319],{"class":88,"line":179},[86,279,280],{"class":158},"NEW_LINES",[86,282,231],{"class":103},[86,284,285],{"class":114},"tail",[86,287,289],{"class":288},"snbK4"," -n",[86,291,292],{"class":130}," +",[86,294,121],{"class":107},[86,296,297],{"class":103},"$((",[86,299,228],{"class":114},[86,301,302],{"class":130}," + ",[86,304,306],{"class":305},"sM54T","1",[86,308,309],{"class":103},"))",[86,311,121],{"class":107},[86,313,108],{"class":107},[86,315,316],{"class":130},"$LOG_FILE",[86,318,121],{"class":107},[86,320,264],{"class":103},[86,322,323],{"class":88,"line":193},[86,324,270],{"emptyLinePlaceholder":269},[86,326,328],{"class":88,"line":327},7,[86,329,330],{"class":92},"# 今回の総行数を記録\n",[86,332,334,337,340,343,345,347,349,352,354,356],{"class":88,"line":333},8,[86,335,336],{"class":114},"wc",[86,338,339],{"class":288}," -l",[86,341,342],{"class":124}," \u003C",[86,344,108],{"class":107},[86,346,316],{"class":130},[86,348,121],{"class":107},[86,350,351],{"class":124}," >",[86,353,108],{"class":107},[86,355,239],{"class":130},[86,357,170],{"class":107},[14,359,360],{},"新規分だけをLLMに渡し、前回までの要約と統合する。会議が長くなっても処理量が一定に保たれ、3分のインターバル内に収まる。",[18,362,364],{"id":363},"minutesjson-のリアルタイム更新","minutes.json のリアルタイム更新",[14,366,367,368,371],{},"Nuxtアプリは ",[29,369,370],{},"app/public/minutes.json"," を3秒ごとにポーリングして画面を更新する。summarize-meetingスキルの出力をこのJSONファイルに書き込めば、ブラウザにリアルタイムで要約が表示される。",[14,373,374],{},"JSONの構造は以下の通り:",[22,376,380],{"className":377,"code":378,"language":379,"meta":31,"style":31},"language-json shiki shiki-themes vitesse-light vitesse-light","{\n  \"status\": \"recording\",\n  \"lastUpdated\": \"2026-03-22T15:30:00+09:00\",\n  \"summary\": \"要約テキスト...\",\n  \"topics\": [\"トピック1\", \"トピック2\"],\n  \"actionItems\": [\"アクション1\"]\n}\n","json",[29,381,382,387,411,431,451,485,508],{"__ignoreMap":31},[86,383,384],{"class":88,"line":89},[86,385,386],{"class":103},"{\n",[86,388,389,393,396,398,401,403,406,408],{"class":88,"line":96},[86,390,392],{"class":391},"sqvqQ","  \"",[86,394,395],{"class":253},"status",[86,397,121],{"class":391},[86,399,400],{"class":103},":",[86,402,108],{"class":107},[86,404,405],{"class":130},"recording",[86,407,121],{"class":107},[86,409,410],{"class":103},",\n",[86,412,413,415,418,420,422,424,427,429],{"class":88,"line":155},[86,414,392],{"class":391},[86,416,417],{"class":253},"lastUpdated",[86,419,121],{"class":391},[86,421,400],{"class":103},[86,423,108],{"class":107},[86,425,426],{"class":130},"2026-03-22T15:30:00+09:00",[86,428,121],{"class":107},[86,430,410],{"class":103},[86,432,433,435,438,440,442,444,447,449],{"class":88,"line":173},[86,434,392],{"class":391},[86,436,437],{"class":253},"summary",[86,439,121],{"class":391},[86,441,400],{"class":103},[86,443,108],{"class":107},[86,445,446],{"class":130},"要約テキスト...",[86,448,121],{"class":107},[86,450,410],{"class":103},[86,452,453,455,458,460,462,465,467,470,472,475,477,480,482],{"class":88,"line":179},[86,454,392],{"class":391},[86,456,457],{"class":253},"topics",[86,459,121],{"class":391},[86,461,400],{"class":103},[86,463,464],{"class":103}," [",[86,466,121],{"class":107},[86,468,469],{"class":130},"トピック1",[86,471,121],{"class":107},[86,473,474],{"class":103},",",[86,476,108],{"class":107},[86,478,479],{"class":130},"トピック2",[86,481,121],{"class":107},[86,483,484],{"class":103},"],\n",[86,486,487,489,492,494,496,498,500,503,505],{"class":88,"line":193},[86,488,392],{"class":391},[86,490,491],{"class":253},"actionItems",[86,493,121],{"class":391},[86,495,400],{"class":103},[86,497,464],{"class":103},[86,499,121],{"class":107},[86,501,502],{"class":130},"アクション1",[86,504,121],{"class":107},[86,506,507],{"class":103},"]\n",[86,509,510],{"class":88,"line":327},[86,511,512],{"class":103},"}\n",[14,514,515,516,518,519,522,523,525],{},"ステータスは ",[29,517,405],{},"（会議中）と ",[29,520,521],{},"completed","（終了）の2値。会議中は3分ごとに要約が更新され、終了後にステータスを ",[29,524,521],{}," に切り替える。",[18,527,529],{"id":528},"worktree問題-ファイルを更新しても画面に反映されない","worktree問題: ファイルを更新しても画面に反映されない",[14,531,532],{},"差分モードも動き、JSONも生成される。ところがブラウザをリロードしても画面が変わらない。JSONのタイムスタンプは更新されているのに、Nuxtアプリ側の表示が古いまま止まっている。",[47,534,535],{"id":535},"原因",[14,537,538,539,541,542,544],{},"Claude Codeはgit worktree上で作業していた。worktree内の ",[29,540,370],{}," を更新しても、Nuxtの開発サーバーが参照しているのは元リポジトリ側の ",[29,543,370],{}," だった。worktreeと元リポジトリは別のディレクトリツリーを持つため、片方を書き換えてももう片方には反映されない。",[47,546,548],{"id":547},"修正-元リポジトリに直接書き込む","修正: 元リポジトリに直接書き込む",[14,550,551,552,554],{},"worktree内のパスではなく、元リポジトリの ",[29,553,370],{}," の絶対パスを指定して書き込むように修正した。",[22,556,558],{"className":80,"code":557,"language":82,"meta":31,"style":31},"# NG: worktree内のパス（Nuxtに反映されない）\nMINUTES_FILE=\"./app/public/minutes.json\"\n\n# OK: 元リポジトリの絶対パス\nMINUTES_FILE=\"/workspace/original-repo/app/public/minutes.json\"\n",[29,559,560,565,579,583,588],{"__ignoreMap":31},[86,561,562],{"class":88,"line":89},[86,563,564],{"class":92},"# NG: worktree内のパス（Nuxtに反映されない）\n",[86,566,567,570,572,574,577],{"class":88,"line":96},[86,568,569],{"class":158},"MINUTES_FILE",[86,571,162],{"class":103},[86,573,121],{"class":107},[86,575,576],{"class":130},"./app/public/minutes.json",[86,578,170],{"class":107},[86,580,581],{"class":88,"line":155},[86,582,270],{"emptyLinePlaceholder":269},[86,584,585],{"class":88,"line":173},[86,586,587],{"class":92},"# OK: 元リポジトリの絶対パス\n",[86,589,590,592,594,596,599],{"class":88,"line":179},[86,591,569],{"class":158},[86,593,162],{"class":103},[86,595,121],{"class":107},[86,597,598],{"class":130},"/workspace/original-repo/app/public/minutes.json",[86,600,170],{"class":107},[14,602,603],{},"パスを書き換えて再実行した直後、ブラウザの画面に要約テキストが流れ込んできた。worktreeは独立したワーキングディレクトリを持ち、元リポジトリとファイルシステムを共有しない。知っていたはずの性質だが、実際に「書き込んだのに映らない」という症状に出くわすまで結びつかなかった。",[18,605,607],{"id":606},"_45分間のテスト実行","45分間のテスト実行",[14,609,610],{},"保険営業のロールプレイ（約45分）を題材にテストを回した。即録くんがリアルタイムで文字起こしを行い、3分おきにClaude Codeが新しい発話を拾って要約を更新する。",[14,612,613],{},"テスト中に確認できた動作:",[615,616,617,621,624,627],"ul",{},[618,619,620],"li",{},"3分ごとに要約が自動更新される",[618,622,623],{},"トピックリストが会議の進行に合わせて増えていく",[618,625,626],{},"アクションアイテムが抽出される",[618,628,629],{},"差分モードにより、処理時間が安定している（毎回3分以内に完了）",[14,631,632,633,635],{},"会議終了後、ステータスを ",[29,634,521],{}," に更新し、最終版の要約をmarkdownファイルとして保存した。",[18,637,638],{"id":638},"振り返り",[47,640,642],{"id":641},"パス問題は動かして確認が速い","パス問題は「動かして確認」が速い",[14,644,645,646,649],{},"devcontainerからWindowsのファイルにアクセスできるかどうか、ドキュメントを読み込むより ",[29,647,648],{},"ls"," を1回叩くほうが速かった。マウント構成を頭の中で組み立てるよりも、実際にファイルが並ぶのを目で見たほうが確実に前に進む。",[47,651,652],{"id":652},"worktreeの罠は体感しないと分からない",[14,654,655],{},"git worktreeが別のディレクトリツリーだということは知識としては持っていた。だが、「Nuxtの開発サーバーが参照しているのはどちらのディレクトリか」という問いが浮かぶまでに時間がかかった。ファイルを書き込んでいるのに画面が変わらない、という症状からworktreeのパス問題に辿り着くまで、ログの出力先やポーリング間隔など別の箇所を疑って回り道をした。",[47,657,659],{"id":658},"croncreate-スキルの組み合わせ","CronCreate + スキルの組み合わせ",[14,661,662,664],{},[29,663,37],{}," という1行を打つだけで定期実行が回り始める。ちょっとしたバッチ処理をその場で試せる。スキル側にロジックを閉じ込めておけば、インターバルを変えたいときもスキルの中身だけ触ればいい。",[666,667,668],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .snbK4, html code.shiki .snbK4{--shiki-default:#A65E2B;--shiki-dark:#A65E2B}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}html pre.shiki code .sqvqQ, html code.shiki .sqvqQ{--shiki-default:#99841877;--shiki-dark:#99841877}",{"title":31,"searchDepth":96,"depth":96,"links":670},[671,672,677,678,679,683,684],{"id":20,"depth":96,"text":20},{"id":41,"depth":96,"text":42,"children":673},[674,676],{"id":49,"depth":155,"text":675},"発見: /workspace/logs/ からアクセスできた",{"id":74,"depth":155,"text":74},{"id":207,"depth":96,"text":207},{"id":363,"depth":96,"text":364},{"id":528,"depth":96,"text":529,"children":680},[681,682],{"id":535,"depth":155,"text":535},{"id":547,"depth":155,"text":548},{"id":606,"depth":96,"text":607},{"id":638,"depth":96,"text":638,"children":685},[686,687,688],{"id":641,"depth":155,"text":642},{"id":652,"depth":155,"text":652},{"id":658,"depth":155,"text":659},"dev","Claude Codeのsummarize-meetingスキルとCronCreateを使い、3分間隔で会議内容を自動要約するシステムを構築。devcontainerのパス問題、worktreeの反映問題、差分モードの実装まで。","md",{},null,"/realtime-meeting-summarizer","misc-dev",false,"2026-03-22T00:00:00.000Z",{"title":5,"description":690},"2026-03/2026-03-22/realtime-meeting-summarizer",[701,702,703,704,705],"claude-code","リアルタイム要約","devcontainer","worktree","CronCreate","memo","S6ZutU7eSCFlf3UMX8xTMyzdvQCx1lLppaIDLz06z8Q",[],"https://log.eurekapu.com/og/blog/realtime-meeting-summarizer.png?v=2026-03-22T00%3A00%3A00.000Z&title=Claude%20Code%E3%81%A7%E3%83%AA%E3%82%A2%E3%83%AB%E3%82%BF%E3%82%A4%E3%83%A0%E4%BC%9A%E8%AD%B0%E8%A6%81%E7%B4%84%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%82%92%E6%A7%8B%E7%AF%89%E3%81%97%E3%81%9F%E8%A8%98%E9%8C%B2&author=Kei%20Komatsu&sig=82255751c4506ce5",1782528819950]