hero_picture
Cover Image for 非エンジニアがWeb会議の議事録作成を自動化してみた

非エンジニアがWeb会議の議事録作成を自動化してみた

いつもクリエイターズブログをご愛読いただいている皆様、初めまして。
業務管理部のkimと申します。

普段は デカメ―ル をはじめとした自社サービスのサポートや、総務・事務対応のほか
社内の情報セキュリティ管理も担当しております。
最近では、社内業務の効率化をDXで実現することにも取り組んでいます。

今回は、このDXの取り組みの一つとして、エンジニアではない私が生成AI技術を活用し
Web会議の議事録作成を自動化することができましたので、ご紹介させていただきます。

今回の目的

弊社では、毎朝Web会議(Google Meet)で朝礼を行い、議事録を作成しています。
しかし、議事録の作成と内容の要約作業に地味な手間がかかっており、課題となっていました。

Generated by ChatGPT
Generated by ChatGPT

そこで今回は、議事録の作成~要約~投稿まで全てAIにお任せしてしまおう!という目的になります。
必要な下準備や作業内容についても、ほぼ全てChatGPTに教えてもらいました。

使用ツール

1Google Meet:ふだんのWeb会議で使用。最近日本語対応したGeminiの「自動文字起こし」を活用します。
2Slack:ふだんの社内コミュニケーションで使用。議事録の投稿先となります。
3ChatGPT:GASスクリプトの作成、および議事録まとめ処理(API使用)を行います。

準備

Google Meet議事録データの仕様を確認

自動文字起こしをオンにした会議で、終了時に会議の主催者のGoogleドライブの「マイドライブ」に保存されます。

マイドライブ直下の「Meet Recordings」フォルダにGoogleドキュメント形式で自動保存されます。
マイドライブ直下の「Meet Recordings」フォルダにGoogleドキュメント形式で自動保存されます。
https://support.google.com/meet/answer/12849897?hl=ja

OpenAI APIでsecret keyを取得

取得した議事録データの内容を読み取り、ChatGPTにAPI経由で要約してもらいます。
※1回当たり 約 $0.05〜$0.10(7〜15円)程度のコストが発生します

💡
注意:ChatGPT Teamなど、有料プランを契約していてもAPIの利用は別途費用が掛かるので
実行回数や文字数が多い場合は、料金に注意しましょう。

OpenAI Platformにログインし、 sk- から始まるAPI用のSecret Keyを作成して控えておきます。
※はじめて利用する場合は、クレジットカードなどの支払い方法の設定が必要です。

https://platform.openai.com/login
Search から「key」と検索すると「API keys」が表示されるのでクリック
Search から「key」と検索すると「API keys」が表示されるのでクリック
「Create new secret key」をクリック。追加したキーは必ず控えておくようにしましょう
「Create new secret key」をクリック。追加したキーは必ず控えておくようにしましょう

Slack botの作成とBot User OAuth Tokenの取得

Slack API画面にログインし、「Create New App」よりSlack botを新規作成します。

https://api.slack.com/apps
「From scratch」を選択
「From scratch」を選択

botが作成できたら、ワークスペースへのインストールを行います。
※「
インストールするボットユーザーがありません」と表示される場合は以下のURLをご参照ください。
https://the-simple.jp/slack-nobotuser

「OAuth & Permissions」に移動し、「Bot Token Scopes」の「Add an OAuth Scope」に以下を設定します。

1channels:read
2chat:write.public
3chat:write

すると、OAuth Tokens に xoxb- から始まる「Bot User OAuth Token」が追加されるので、こちらを控えておきます。

Google Apps Scriptの実装

スクリプトファイルの作成

Googleドライブにログインし、右クリックメニューの「その他」から「Google Apps Script」を選択します。

使用するGoogle APIの設定

作成したGoogle Apps Scriptの編集画面の左横にある「サービス」の+ボタンから
Calendar、Drive、Slides のAPIをそれぞれ追加します。

スクリプトの内容(ChatGPT作成)

作成したGoogle Apps Scriptの「コード.gs」に以下を反映し、保存します。
ここで先ほど控えた OpenAI APIのsecret key(115行目)と、Slack botのBot User OAuth Token(179行目)を適用します。

1// === メイン関数(カレンダー連携+ChatGPT要約付き) ===
2function runDailyMorningReportByCalendar() {
3  try {
4    const event = getTodaysMorningMeetingEvent();
5    if (!event) throw new Error("今日の朝礼イベントが見つかりません");
6
7    const file = getMeetingDocFromDrive();
8    if (!file) throw new Error("マイドライブ内に本日作成された議事録ファイルが見つかりません");
9
10    const plainText = extractPlainText(file.getId());
11    const summary = generateSummaryByChatGPT(plainText); // ChatGPTで要約生成
12    postToSlackAsText(summary); // Slackに投稿
13
14  } catch (e) {
15    Logger.log("❌ 処理中にエラーが発生しました: " + e.message);
16  } finally {
17    createTriggerForNext(); // ← 成功/失敗に関わらず必ず実行される
18  }
19}
20
21// === 明日9:30にこの関数を再実行するトリガーを作成 ===
22function createTriggerForNext() {
23  // 既存の同名トリガーを削除
24  const triggers = ScriptApp.getProjectTriggers();
25  for (let trigger of triggers) {
26    if (trigger.getHandlerFunction() === "runDailyMorningReportByCalendar") {
27      ScriptApp.deleteTrigger(trigger);
28      Logger.log("🗑️ 古いトリガーを削除しました: " + trigger.getUniqueId());
29    }
30  }
31
32  // 新規トリガーを作成
33  const tomorrow = new Date();
34  tomorrow.setDate(tomorrow.getDate() + 1);
35  tomorrow.setHours(9, 30, 0, 0);
36
37  ScriptApp.newTrigger("runDailyMorningReportByCalendar")
38    .timeBased()
39    .at(tomorrow)
40    .create();
41  Logger.log("✅ 次回の9:30実行トリガーを作成しました");
42}
43
44// === 今日に開始する「全体朝礼」イベントを取得 ===
45function getTodaysMorningMeetingEvent() {
46  const calendarId = 'primary';
47  const today = new Date();
48  const start = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 9, 0, 0);
49  const end = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 9, 30, 0);
50
51  const events = Calendar.Events.list(calendarId, {
52    timeMin: start.toISOString(),
53    timeMax: end.toISOString(),
54    singleEvents: true,
55    maxResults: 5
56  }).items;
57
58  for (let event of events) {
59    if (event.summary.includes('全体朝礼')) {
60      return event;
61    }
62  }
63  return null;
64}
65
66// === マイドライブから本日作成された「Gemini」かつ「全体朝礼」かつファイル名に本日の日付を含むドキュメントを取得 ===
67function getMeetingDocFromDrive() {
68  const folderName = "Meet Recordings";
69  const folders = DriveApp.getFoldersByName(folderName);
70  if (!folders.hasNext()) throw new Error(`${folderName}」という名前のフォルダが見つかりませんでした`);
71
72  const folder = folders.next();
73  const files = folder.getFilesByType(MimeType.GOOGLE_DOCS);
74  let latestFile = null;
75  const todayStrForDate = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy-MM-dd");
76  const todayStrForName = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd");
77
78  while (files.hasNext()) {
79    const file = files.next();
80    if (
81      file.getName().includes("Gemini") &&
82      file.getName().includes("全体朝礼") &&
83      file.getName().includes(todayStrForName)
84    ) {
85      const createdStr = Utilities.formatDate(file.getDateCreated(), "Asia/Tokyo", "yyyy-MM-dd");
86      if (createdStr === todayStrForDate) {
87        if (!latestFile || file.getLastUpdated().getTime() > latestFile.getLastUpdated().getTime()) {
88          latestFile = file;
89        }
90      }
91    }
92  }
93
94  if (!latestFile) throw new Error("本日作成された『Gemini』かつ『全体朝礼』が含まれる議事録が見つかりませんでした");
95
96  return latestFile;
97}
98
99// === 議事録本文を抽出 ===
100function extractPlainText(docId) {
101  const file = Drive.Files.get(docId, { fields: 'exportLinks' });
102  const exportUrl = file.exportLinks['text/plain'];
103
104  const response = UrlFetchApp.fetch(exportUrl, {
105    headers: {
106      Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
107    }
108  });
109
110  return response.getContentText();
111}
112
113// === ChatGPTで要約を生成(gpt-4-turbo使用) ===
114function generateSummaryByChatGPT(inputText) {
115  const apiKey = 'sk-***'; // ← OpenAI APIで作成したSecret Keyをここに設定
116  const url = "https://api.openai.com/v1/chat/completions";
117  const today = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd");
118
119  const prompt = `以下の文字起こしを元に、Slack投稿向けの「本日の朝礼要約(${today})」形式でカテゴリ別に要約してください。
120
121🔽 出力フォーマットは以下のように統一してください:
122・各カテゴリに「【カテゴリ名】」をタイトルとして明記すること
123・カテゴリは以下の順で並べてください:
124  1. 勤怠状況の確認
125  2. 全体共有
126  3. リリース報告
127  4. セキュリティニュースの共有
128  5. LT発表
129  6. その他
130
131🔽 注意点:
132・「Gemini」や「文字起こし」などの自動処理に関するメモや注意書きは含めないでください
133・実際の発表内容を基に、箇条書きでわかりやすく表現してください
134・「その他」の部分では、LTの感想を交えたユーモアある一言を添えてください
135・その他、任意の追加指示を記載
136
137--- 以下、文字起こし本文 ---
138
139${inputText}`;
140
141  const payload = {
142    model: "gpt-4-turbo",
143    messages: [
144      {
145        role: "system",
146        content: "あなたはSlack投稿形式の朝礼要約を作成するアシスタントです。"
147      },
148      {
149        role: "user",
150        content: prompt
151      }
152    ],
153    temperature: 0.3
154  };
155
156  const options = {
157    method: "post",
158    headers: {
159      Authorization: "Bearer " + apiKey,
160      "Content-Type": "application/json"
161    },
162    payload: JSON.stringify(payload),
163    muteHttpExceptions: true
164  };
165
166  const response = UrlFetchApp.fetch(url, options);
167  const result = JSON.parse(response.getContentText());
168
169  if (!result.choices || result.choices.length === 0) {
170    Logger.log(response.getContentText());
171    throw new Error("❌ ChatGPTから要約が得られませんでした");
172  }
173
174  return result.choices[0].message.content;
175}
176
177// === Slackにテキスト形式で投稿 ===
178function postToSlackAsText(summary) {
179  const slackToken = 'xoxb-***'; // Slack botの「Bot User OAuth Token」をここに設定
180  const channelId = 'C123456789'; // メッセージを投稿するSlackチャンネルのID
181
182  const today = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd");
183  const header = `📌 *本日の朝礼要約(${today})*`;
184
185  const payload = {
186    channel: channelId,
187    text: `${header}\n\n\`\`\`\n${summary}\n\`\`\``
188  };
189
190  const response = UrlFetchApp.fetch("https://slack.com/api/chat.postMessage", {
191    method: "post",
192    headers: {
193      Authorization: "Bearer " + slackToken,
194      "Content-Type": "application/json"
195    },
196    payload: JSON.stringify(payload),
197    muteHttpExceptions: false
198  });
199
200  Logger.log("📡 Slack postMessage response: " + response.getContentText());
201}
202

※2025/5/13 追記:createTriggerForNext 関数において、古いトリガー設定を削除する処理が不足していたため追記しました。(23~30行目)

スクリプトの実行:初回操作

実際の運用を始める前に、Google Apps Scriptの編集画面で
「runDailyMorningReportByCalendar」関数を「▶ 実行」で手動実行します。
これで明日以降、同じ時間に議事録の作成が行われるようになります。
(このスクリプトでは 09:20 を指定しています)

なお、Google Workspaceの設定によっては下記のようなエラーが表示される場合があります。

この場合、「エラーの詳細」をクリックすると表示される内容のうち
~.apps.googleusercontent.com (画像の黄色下線部分)を

以下手順の「クライアントID」に 置き換えて設定することで、利用可能となります。

https://support.itmc.i.moneyforward.com/l/ja/article/x6sms6m4ja-google-oauth#2197951945

結果

議事録をSlackに投稿できました!🎉

SlackアプリのアイコンもAI(ChatGPT)で作成!
SlackアプリのアイコンもAI(ChatGPT)で作成!

実際の会議の内容も適切に要約されており、数日使っていますが十分な実用性を感じています。
しかし、

今日のLTはまさに「誰得」の極みでしたね!

といった、失礼な言動もチラホラみられました。まだ調整の余地がありますね…。
ChatGPTへ指示する内容はGASスクリプトの
const prompt 部分(108行目)で変更できるので、ご参考ください!

注意:Google Meetの文字起こしについて

議事録を残したい会議では、必ず文字起こしをオンにしましょう(1敗)。
会議が始まる前に、Meetの画面右上にある鉛筆マークからオンにしておきます。

Meet画面の右上にある鉛筆マークを押し、「メモの作成を開始」をクリックします
Meet画面の右上にある鉛筆マークを押し、「メモの作成を開始」をクリックします
鉛筆マークの色が変わればOK!
鉛筆マークの色が変わればOK!


なお、定期的な会議では、文字起こしボタンの押し忘れを防止するために、あらかじめGoogleカレンダーで文字起こしを設定しておくことも可能です。
しかし、この機能を使うと
デフォルトで会議の使用言語が英語になってしまい、議事録データも英語になってしまうバグが起きるようです。※2025/4/22 現在

ここでオンにしておくことで押し忘れを解消できますが…
ここでオンにしておくことで押し忘れを解消できますが…
なぜか英語になってしまう。違うんですけど…
なぜか英語になってしまう。違うんですけど…

この場合も議事録データは残りますが、日本語を空耳で英語にしたような使い物にならない内容になってしまいます。

Googleのサポートの方に問い合わせたところ、この不具合は 2025/4/23 から2週間ほどかけて解決される見通しとのことです。
https://workspaceupdates.googleblog.com/2025/04/updates-for-configuring-your-preferred-language-take-notes-for-me-recorded-captions-meeting-transcripts-google-meet.html
これが完全自動化の最後のピースになるので、早めに解決してくれることを祈ります…。

※2025/5/8 追記:Googleカレンダーの設定画面で会議の使用言語が選択できるようになりました!

プルダウンで「日本語」を選択しましょう
プルダウンで「日本語」を選択しましょう

さいごに

いかがでしたでしょうか。
私自身はプログラミングの知識が豊富とは言えませんが、生成AIの力を借りることで議事録作成の自動化を実現できました。

最も苦労したのは、AIへの仕様や要望の伝え方、動作検証、そして修正のプロセスでした。
この経験を通じて、
AIへ適切な指示を出す力 = プロンプト力 の重要性を実感しました。

同じような課題をお持ちの方のお役に立てれば幸いです!


👉️「PHPシステム」のことならシーズにお任せください!
👉️「PHPシステム」のことならシーズにお任せください!