打ち合わせの帰り道に手元のメモを開いて、決まったことの半分しか書き取れていないと気づく。この経験を何度か繰り返してから、私は対面の場ではメモを取ることを諦めて、終わってすぐ喫茶店で思い出しながら書き起こすようになりました。
その書き起こしが、いつも一番後回しになります。個人開発で動いていると、記録係を頼める相手がいません。この作業だけが手元に溜まっていきます。
2026年8月26日の更新で、Google Meet のメモ機能がウェブとモバイルのホーム画面から直接起動できるようになりました。会議に参加していなくてもメモセッションを始められるということは、目の前に相手がいる対面の打ち合わせでも使えるということです。
ただ、この機能を単体で使い始めると、生成されたドキュメントが Drive のあちこちに積み上がります。記録が残るようになった代わりに、探せなくなるのです。先に決めておくべきなのは、記録の作り方ではなく置き場所でした。
何が出力されるのかを、先に把握しておく
メモセッションを開始すると、Gemini が会話を聞き取り、終了後に Google ドキュメントを1つ作って Drive へ保存します。1つのドキュメントの中に、性質の違う3種類の情報が同居します。
| 出力 | 中身 | 後工程での扱い |
|---|---|---|
| 構造化された要約 | 話題ごとに見出しが立ち、経緯が数行にまとまる | そのまま読む。機械処理にも向く |
| アクションアイテム | 誰が何をするかの箇条書き | タスク管理側へ転記する対象 |
| 全文の書き起こし | 発言をそのまま起こしたテキスト | 言った言わないの確認用。普段は読まない |
この3つを同じ重さで扱うと破綻します。全文の書き起こしは対面30分でもかなりの分量になり、後から Gemini API に丸ごと渡すと、要約セクションに書いてある結論をもう一度探させることになります。
実務で毎回使うのは要約とアクションアイテムだけです。書き起こしは、争いになったときだけ開く保険として置いておく。この線引きを最初に引いておくと、後の自動化がずいぶん素直になります。
受け皿のフォルダを1つ作り、そこだけを見る
保存先を決めずに使い始めると、ドキュメントは自分の Drive の中に日付順で並びます。案件が2つ3つと並行し始めた時点で、どれがどの打ち合わせのものか、ファイル名だけでは判別できなくなります。
私はこの分類を、その場でやることを諦めました。採った形は単純です。受け皿のフォルダを1つだけ作り、打ち合わせが終わったらそこへ移す。それだけです。
Meetings/
_inbox/ ← 生成されたドキュメントを一旦ここへ
2026/
展示準備/
アプリ開発/
_inbox を経由させるのは、分類の判断を後回しにするためです。打ち合わせ直後は次の予定が詰まっていることが多く、その場でフォルダを選ばせると運用が続きません。移動だけなら数秒で終わります。
そして、この後に書く自動処理は _inbox だけを見ます。分類前のものを拾い、台帳へ落とし、処理済みの印をつける。分類は人が後からゆっくりやればよく、自動処理の側は分類結果に依存しません。
自動処理と人の判断を同じフォルダ構造に載せると、片方を変えたときにもう片方が壊れます。入口を1つに絞っておくと、その心配がなくなります。
決定事項だけを抜き出して台帳に落とす
ここからは Apps Script です。_inbox のドキュメントを順に開き、要約セクションだけを Gemini API に渡し、決定事項を構造化して返してもらい、スプレッドシートへ追記します。
まず設定と入口の処理です。
const CONFIG = {
// _inbox フォルダの ID(Drive の URL 末尾)
intakeFolderId: 'YOUR_FOLDER_ID',
// 決定事項を溜める台帳スプレッドシートの ID
ledgerSheetId: 'YOUR_SHEET_ID',
// モデル ID は公式のモデル一覧で現行のものを確認してください
model: 'gemini-3.7-flash',
};
function collectMeetingNotes() {
const folder = DriveApp.getFolderById(CONFIG.intakeFolderId);
const files = folder.getFilesByType(MimeType.GOOGLE_DOCS);
const store = PropertiesService.getScriptProperties();
while (files.hasNext()) {
const file = files.next();
const doneKey = 'done_' + file.getId();
if (store.getProperty(doneKey)) continue;
const section = extractSummarySection(file.getId());
if (!section) continue;
const result = askGemini(section);
appendToLedger(file, result);
store.setProperty(doneKey, String(Date.now()));
}
}処理済みの印をファイル側ではなくスクリプトプロパティに置いているのは、ドキュメントに手を加えたくないからです。打ち合わせの記録は原本のまま残しておきたく、自動処理の都合で本文にマーカーを書き込むのは避けました。フォルダを移動しても ID は変わらないため、分類後も二重取り込みは起きません。
次に、要約セクションだけを取り出す部分です。
function extractSummarySection(docId) {
const body = DocumentApp.openById(docId).getBody();
const total = body.getNumChildren();
const HEADINGS = ['要約', 'まとめ', 'Summary'];
const lines = [];
let started = false;
for (let i = 0; i < total; i++) {
const el = body.getChild(i);
const type = el.getType();
if (type !== DocumentApp.ElementType.PARAGRAPH &&
type !== DocumentApp.ElementType.LIST_ITEM) continue;
const item = (type === DocumentApp.ElementType.PARAGRAPH)
? el.asParagraph()
: el.asListItem();
const text = item.getText().trim();
const isHeading = item.getHeading() !== DocumentApp.ParagraphHeading.NORMAL;
if (isHeading) {
if (!started && HEADINGS.some(function (h) { return text.indexOf(h) === 0; })) {
started = true;
continue;
}
if (started) break; // 次の見出しに入ったら打ち切る
continue;
}
if (started && text) lines.push(text);
}
return lines.length ? lines.join('\n') : null;
}見出しの文字列で区間を切っているだけの、素朴な処理です。それでも全文を渡すのに比べると、投げるトークン量が桁で変わります。見出しの語は環境によって変わりますので、実際のドキュメントを一度開いて、先頭の見出しが何と書かれているかを確認してから HEADINGS を調整してください。
該当する見出しが見つからなければ null を返し、呼び出し側はそのファイルを飛ばします。想定外の形のドキュメントを無理に処理して、中途半端な行を台帳に混ぜるより、素通りさせて後で気づくほうが安全だと考えています。
API 呼び出しは、返ってくる形を先に決める
決定事項を後から機械で扱うなら、自由文で返してもらう理由がありません。responseSchema で形を固定します。
function askGemini(sectionText) {
const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
const endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' +
CONFIG.model + ':generateContent';
const payload = {
contents: [{
role: 'user',
parts: [{ text: buildPrompt(sectionText) }]
}],
generationConfig: {
responseMimeType: 'application/json',
responseSchema: {
type: 'object',
properties: {
decisions: { type: 'array', items: { type: 'string' } },
owners: { type: 'array', items: { type: 'string' } },
due: { type: 'string' }
},
required: ['decisions']
}
}
};
const res = UrlFetchApp.fetch(endpoint, {
method: 'post',
contentType: 'application/json',
headers: { 'x-goog-api-key': apiKey },
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const code = res.getResponseCode();
if (code !== 200) {
throw new Error('Gemini API ' + code + ': ' + res.getContentText());
}
const body = JSON.parse(res.getContentText());
return JSON.parse(body.candidates[0].content.parts[0].text);
}
function buildPrompt(sectionText) {
return [
'次は打ち合わせの要約です。決まったことだけを抜き出してください。',
'検討中の案、保留になった話題、感想は含めないでください。',
'owners には decisions と同じ順で担当者名を入れ、不明な場合は空文字にしてください。',
'---',
sectionText
].join('\n');
}generationConfig に temperature を書いていないのは、書き忘れではありません。2026年8月の時点で temperature・top_p・top_k は非推奨になりました。出力の揺れをこれらで抑えていた実装は、いずれ書き直しが必要になります。今から書くコードにわざわざ入れる理由はありませんでした。
揺れを抑える役目は、スキーマとプロンプトの制約側に移しています。「検討中の案を含めない」と明示的に書いておくのは、要約の中で結論と検討が隣り合って書かれているとき、両方を決定事項として拾ってしまうことがあったためです。
最後に台帳へ書き込みます。
function appendToLedger(file, result) {
const sheet = SpreadsheetApp.openById(CONFIG.ledgerSheetId).getSheets()[0];
const decisions = result.decisions || [];
const owners = result.owners || [];
if (!decisions.length) return;
const rows = decisions.map(function (text, i) {
return [
Utilities.formatDate(file.getDateCreated(), 'Asia/Tokyo', 'yyyy-MM-dd'),
file.getName(),
text,
owners[i] || '',
result.due || '',
file.getUrl()
];
});
sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length)
.setValues(rows);
}行ごとに元ドキュメントの URL を持たせているのが、地味ですが効きます。台帳の一行を見て「本当にそう決まったのか」と迷ったとき、その場から原本へ飛べます。要約は要約でしかなく、判断の根拠は常に原本側にあるという前提を、データ構造で表しておきたかったのです。
実行は時間主導型トリガーで1日1回にしています。打ち合わせの直後に即座に台帳へ載る必要はなく、翌朝までに揃っていれば十分でした。トリガーの本数が増えてきたら、Apps Script の時間主導型トリガーが静かに枯渇する で扱った一本化の考え方が役に立ちます。権限まわりは Apps Script × Gemini 自動化の権限を最小に保つ のスコープ設計に合わせて、appsscript.json に必要なものだけを明示してください。
対面で回す前に、人に伝えることを決めておく
技術的な準備よりも、こちらのほうが重要かもしれません。
オンライン会議であれば、記録が取られていることは画面上の表示で全員に伝わります。対面の打ち合わせでは、机の上に置かれた端末が何をしているのか、相手からは見えません。
私自身は、始める前にひとこと断ることを自分のルールにしました。「後で議事録を起こすのが追いつかないので、メモ機能を使わせてください」と伝えると、たいていは快く受け入れてもらえます。断られたときは素直に手で書きます。
あわせて、次の3つを自分の中で決めておくと迷いません。
- 共有範囲: 生成されたドキュメントを相手にも渡すのか、自分の手控えとして持つだけなのか
- 保持期間: 全文の書き起こしをいつまで残すか。決定事項が台帳に移った後は、原本を一定期間で整理する
- 除外する場面: 雑談混じりの会食や、まだ公にできない相談ごとでは使わない
記録が残ることは、相手にとって必ずしも歓迎される話ではありません。便利さを理由に、その感覚を飛び越えないようにしています。
次の一歩
まずは _inbox フォルダを1つ作って、次の打ち合わせでメモ機能を回してみてください。自動化はその後で構いません。生成されたドキュメントを一度自分の目で読み、要約の見出しがどう立つかを確認してからでないと、extractSummarySection の調整ができないためです。
なお、Workspace 側の新機能をどこまで自動化に取り込むかという線引きについては、Sheets canvas へ移せるのは入口までで、実行境界は Apps Script に残ります で、実行の責任をどちらに残すかという観点から詳しく整理しています。
対面の記録は、これまで一番手が回らない場所でした。そこに手が届くようになったこと自体は、素直にありがたく思っています。お読みいただきありがとうございました。