NotebookLMは、Googleが提供する「自分専用のAIデータベース」です。通常の生成AIであるChatGPTやGeminiは、独自の巨大データセットやインターネットをもとに回答します。これは非常に便利なのですが、その反面、話が一般化しすぎたり、ときどきそれっぽい嘘(ハルシネーション)を堂々と語るという欠点もあります。
そこで登場するのが NotebookLM です。
NotebookLMは、自分が指定した情報源だけをもとに回答するAIです。つまり「世界の知識」ではなく、「自分の知識ベース」で動くAI。汎用AIの弱点をかなりうまく補ってくれます。
ただし、ここで一つ問題があります。
例えばWord1ページのメモでも、ファイルとしては「1ファイル」です。
もしメモを細かく分けて保存していると、あっという間にソース数の上限に到達してしまいます。逆に言えば、複数のメモを1つのファイルにまとめられれば、NotebookLMにより多くの情報を覚えさせることができるわけです。

私の場合、MBAの学習にNotebookLMを使っています。
参考文献のPDFはそのまま読み込ませ、レッスンのメモや参考文献の感想などは1つのファイルにまとめることを思いつきました。
そこで見つけたのが Obsidian というアプリです。
マークダウン形式でノートを書く無料ツールです。
マークダウンはテキストベースですが、 # 見出し [] リンク などの簡単な記号を使うことで、フォントサイズや構造が変わり、とても読みやすくなります。そして最大のメリットは、ファイルサイズが非常に小さいこと。ノート1つにつきファイルは1つですが、テキストなので容量はほとんど増えません。さらに面白いことに、ChatGPTなどのAIの出力も基本的にマークダウン形式です。
つまり、AIの回答をそのまま知識ノートとして保存できるわけです。

「これはNotebookLMと相性が良いのでは?」と思ったのですが……
ここで問題が発生しました。
NotebookLMをGoogle Drive経由で読み込ませようとしたところ、次の課題が判明しました。
- マークダウン形式(md)のファイルは、NotebookLMが読み込めない(GoogleDrive経由の場合)
- マークダウン形式のファイルを、手動でGoogle documentにコピペすることは、現実的ではない
- ファイルを手動でGoogle driveにアップロードすることも大変
AIに聞いても良い解決策を教えてくれませんでした。(生成AIの限界を垣間見れてうれしかったです)
そこで地道にGoogle検索を続けた結果、最終的に次の方法にたどり着きました。

(1) GASで処理しやすいように、Obsidian の中でNotebookLMに取り込みたいメモのタイトルを全て YYYYMMDD形式にします。
(2) Google Driveの純正同期アプリをPCにインストールし、Obsidianの保存フォルダをDriveと同期させます。
(3) NotebookLMへの更新頻度は、毎月程度なので、GASを使って「202602」のように年と月を指定します。 指定したノートを、指定したGoogle document(こちらは年ベース)にコピペします。 コードは後述。
(4) Google documentのタブ機能を使い、月ごとのシートを手動で分けます。
(5) NotebookLMには、このGoogle documentをソースとして取り込んであるので、使うときに手動で情報源を更新します。
この方法を使うと、Obsidianで書いた知識が自動的にNotebookLMの知識ベースに集約されるようになります。 つまり、NotebookLMが自分の第二の記憶装置として機能するわけです。 実際に動いたときは、感動しました。
唯一残念なのは、仕事の環境がGoogleではなくMicrosoft環境なことです。 そのため職場ではNotebookLMが使えません。 世の中なかなかうまくいきません。
GAS
Google spreadsheet を好きなフォルダに作成します。 B1セルにObsidian のノートが保存されているフォルダの固有IDを指定、B2セルに 読み込みたいファイルの件名に含まれる単語を入力、B3セルに張り付けるGoogle documentの固有IDを手動で指定

以下のコードは Gemini に教えてもらいました。
/* 指定フォルダおよびそのサブフォルダ内から、部分一致で.mdファイルを検索し統合する
*/
function mergeMarkdownFiles() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
// シートから設定値を取得
const folderId = sheet.getRange("B1").getValue(); // フォルダID
const searchTerm = sheet.getRange("B2").getValue(); // 検索ワード
const targetDocId = sheet.getRange("B3").getValue(); // 統合先ドキュメントID
if (!folderId || !targetDocId) {
Browser.msgBox("エラー: B1にフォルダID、B3にドキュメントIDを入力してください。");
return;
}
try {
const rootFolder = DriveApp.getFolderById(folderId);
const matchedFiles = [];
// 1. 再帰的にファイルを検索する関数を呼び出し
getAllFilesRecursively(rootFolder, searchTerm, matchedFiles);
if (matchedFiles.length === 0) {
Browser.msgBox("指定フォルダおよびサブフォルダ内に一致するファイルが見つかりませんでした。");
return;
}
// 2. スプレッドシートのA5セル以降にファイル名を書き出し
sheet.getRange("A5:A").clearContent();
const fileNamesForSheet = matchedFiles.map(file => [file.getName()]);
sheet.getRange(5, 1, fileNamesForSheet.length, 1).setValues(fileNamesForSheet);
// 3. Googleドキュメントに内容を統合
const doc = DocumentApp.openById(targetDocId);
const body = doc.getBody();
body.clear();
matchedFiles.forEach((file, index) => {
const content = file.getBlob().getDataAsString();
// ファイル名を見出しとして追加
body.appendParagraph(file.getName()).setHeading(DocumentApp.ParagraphHeading.HEADING2);
body.appendParagraph(content);
if (index < matchedFiles.length - 1) {
body.appendPageBreak();
}
});
Browser.msgBox("サブフォルダを含めた検索と統合が完了しました。");
} catch (e) {
Browser.msgBox("エラーが発生しました: " + e.message);
}
}
/**
* フォルダ内を再帰的にスキャンして条件に合うファイルをmatchedFiles配列に追加する
*/
function getAllFilesRecursively(folder, searchTerm, matchedFiles) {
// フォルダ直下のファイルをチェック
const files = folder.getFiles();
while (files.hasNext()) {
let file = files.next();
let fileName = file.getName();
if (fileName.toLowerCase().endsWith(".md") && fileName.includes(searchTerm)) {
matchedFiles.push(file);
}
}
// サブフォルダを順に辿る(再帰)
const subFolders = folder.getFolders();
while (subFolders.hasNext()) {
let subFolder = subFolders.next();
getAllFilesRecursively(subFolder, searchTerm, matchedFiles);
}
}
Google document は一番上のタブにコピペされるので、ブランクタブ を一番上にあげておきます。

NotebookLM のソースをクリックして、「Click to sync」でファイルを更新します。




コメント