見出し画像

NotebookLMで作成したスライドPDFをGoogleスライドで編集できるようにしたい

便利なNotebookLMだけど

スライドを作成してもPEFで出力されてしまう。
そして画像はハイレベルなんだが残念なことに、まだ日本語変換がおかしくて変な中国文字が出現してしまう。あと意図しないワードやテキストは削除したい。
ということで、テキストと画像を取り出すGASを作成しようということなんです。

GASを開く 

1.テキスト取り出し 

アップロードしたPDFからテキストのみ、スライドのページに合わせて再生成

デプロイまでのルート

このスクリプトを動かすには 「Drive API」の有効化 が必須です。

  1. GASプロジェクトの作成:

    • Google Apps Script を開き「新しいプロジェクト」を作成。

    • Code.gs に上記のコードを貼り付け。

    • 左側の「+(ファイルを追加)」から「HTML」を選択し、名前を Index にしてコードを貼り付け。

  2. Google Drive API の有効化:

    • 左メニューの「サービス(+)」をクリック。

    • Drive API を選択して「追加」をクリック。

  3. デプロイ:

    • 右上の「デプロイ」>「新しいデプロイ」をクリック。

    • 種類の選択で「ウェブアプリ」を選択。

    • 次の設定にします:

      • 説明: PDF変換アプリ

      • 実行ユーザー: 自分

      • アクセスできるユーザー: 自分のみ(または全員)

    • 「デプロイ」をクリックし、承認(アクセスを許可)を進める。

  4. 実行:

    • 発行された「ウェブアプリのURL」にアクセスし、PDFをアップロードします。

実践

1.コード.gs

function doGet() {
  return HtmlService.createTemplateFromFile('Index').evaluate()
    .setTitle('NotebookLM PDF to Slides - Final')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function processPdfToSlide(fileData, fileName) {
  try {
    const bytes = Utilities.base64Decode(fileData.split(",")[1]);
    const blob = Utilities.newBlob(bytes, "application/pdf", fileName);
    
    const presentation = SlidesApp.create(fileName.replace(".pdf", "") + "_ページ同期版");
    const slideW = presentation.getPageWidth();
    const slideH = presentation.getPageHeight();
    
    // Drive API v2 を使用
    const resource = { title: "[Temp] " + fileName, mimeType: "application/pdf" };
    // 【重要】OCR実行
    const tempDocFile = Drive.Files.insert(resource, blob, { ocr: true });
    
    const doc = DocumentApp.openById(tempDocFile.id);
    const body = doc.getBody();
    const paragraphs = body.getParagraphs();
    
    let currentSlide = presentation.appendSlide(SlidesApp.PredefinedLayout.BLANK);
    let pageText = ""; 

    paragraphs.forEach((p) => {
      const text = p.getText();
      const cleanText = text.trim();

      // 【改ページ検知のロジックをNotebookLMに最適化】
      // 1. 文書内の改ページ記号
      // 2. 「A NotebookLM」という文字(これが各ページの下部や上部にあるはずです)
      // 3. 「1 / 12」のようなページ番号形式
      const isPageBreak = p.getChild(0) && p.getChild(0).getType() === DocumentApp.ElementType.PAGE_BREAK;
      const isNotebookLogo = cleanText.includes("A NotebookLM");
      const isPageNumber = /^\d+\s*[\//]\s*\d+$/.test(cleanText); 

      if (isPageBreak || isNotebookLogo || isPageNumber) {
        // 現在蓄積されているテキストがあればスライドへ
        if (pageText.trim().length > 10) { // 10文字以下のノイズは無視
          writePageToSlide(currentSlide, pageText, slideW, slideH);
          // 新しいスライドを作成
          currentSlide = presentation.appendSlide(SlidesApp.PredefinedLayout.BLANK);
          pageText = "";
        }
        return; 
      }

      if (text.length > 0) {
        pageText += text + "\n"; // 改行を維持して積み上げ
      }
    });

    // 最後のページを書き出し
    if (pageText.trim().length > 0) {
      writePageToSlide(currentSlide, pageText, slideW, slideH);
    }

    // 最初の空スライドを削除
    if (presentation.getSlides().length > 1) presentation.getSlides()[0].remove();
    
    // 一時ドキュメントを削除
    Drive.Files.remove(tempDocFile.id);
    
    return { success: true, url: presentation.getUrl() };
  } catch (e) {
    return { success: false, error: "エラーが発生しました: " + e.toString() };
  }
}

/**
 * 1つのテキストボックスとして流し込む(重なり防止)
 */
function writePageToSlide(slide, text, w, h) {
  const shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 40, 40, w - 80, h - 80);
  const tr = shape.getText().setText(text);
  tr.getTextStyle()
    .setFontFamily("Noto Sans JP")
    .setFontSize(12) // 少し小さめに設定して溢れ防止
    .setForegroundColor("#333333");
}

2.Index.html

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body { font-family: 'Noto Sans JP', sans-serif; background: #f4f7f9; color: #333; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
    .container { background: white; padding: 40px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); text-align: center; width: 400px; }
    .drop-zone { border: 2px dashed #4285F4; padding: 30px; border-radius: 8px; margin-bottom: 20px; transition: 0.3s; cursor: pointer; }
    .drop-zone:hover { background: #e8f0fe; }
    button { background: #4285F4; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; width: 100%; }
    button:disabled { background: #ccc; }
    .loader { display: none; margin-top: 20px; color: #666; }
    #result { margin-top: 20px; font-weight: bold; }
  </style>
</head>
<body>
  <div class="container">
    <h2>PDF → スライド変換</h2>
    <p style="font-size: 12px; color: #666;">背景画像とテキストを分離して生成します</p>
    <div class="drop-zone" onclick="document.getElementById('fileInput').click()">
      PDFを選択
      <input type="file" id="fileInput" accept="application/pdf" style="display:none">
    </div>
    <button id="btn" onclick="startConvert()">スライドに変換</button>
    <div id="loader" class="loader">AI解析中...(最大1分)</div>
    <div id="result"></div>
  </div>

  <script>
    function startConvert() {
      const fileInput = document.getElementById('fileInput');
      const file = fileInput.files[0];
      if (!file) return alert('ファイルを選んでください');

      const btn = document.getElementById('btn');
      const loader = document.getElementById('loader');
      btn.disabled = true;
      loader.style.display = 'block';

      const reader = new FileReader();
      reader.onload = function(e) {
        google.script.run
          .withSuccessHandler(res => {
            btn.disabled = false;
            loader.style.display = 'none';
            if(res.success) {
              document.getElementById('result').innerHTML = `<a href="${res.url}" target="_blank">✅ スライドを開く</a>`;
            } else {
              alert(res.error);
            }
          })
          .processPdfToSlide(e.target.result, file.name);
      };
      reader.readAsDataURL(file);
    }
  </script>
</body>
</html>


2.画像の摘出

PDFをJPEG化しスライド化します。

1.テキスト生成とは別に画像生成のGASを作成します。

コード.gs

function doGet() {
  return HtmlService.createTemplateFromFile('Index').evaluate()
    .setTitle('Slide Background Uploader')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

/**
 * 画像をスライドの「最背面」に全画面で配置する(setPictureエラー回避策)
 */
function setBackgroundsFromImages(fileDataList) {
  try {
    if (!fileDataList || fileDataList.length === 0) throw "ファイルがありません";

    // ファイル名で並び替えて順序を維持
    fileDataList.sort((a, b) => a.name.localeCompare(b.name, undefined, {numeric: true}));

    const presentation = SlidesApp.create("NotebookLM_背景配置済み");
    const pageWidth = presentation.getPageWidth();
    const pageHeight = presentation.getPageHeight();
    
    fileDataList.forEach((file) => {
      const bytes = Utilities.base64Decode(file.data.split(",")[1]);
      const blob = Utilities.newBlob(bytes, file.contentType, file.name);
      
      // 新しいスライドを作成
      const slide = presentation.appendSlide(SlidesApp.PredefinedLayout.BLANK);
      
      // 画像をスライドに挿入し、サイズをスライド全体に合わせる
      const image = slide.insertImage(blob);
      image.setLeft(0)
           .setTop(0)
           .setWidth(pageWidth)
           .setHeight(pageHeight);
      
      // 最背面に移動(後から文字を乗せやすくするため)
      image.sendToBack();
    });

    // 最初の空スライドを削除
    if (presentation.getSlides().length > 1) {
      presentation.getSlides()[0].remove();
    }

    return { success: true, url: presentation.getUrl() };
  } catch (e) {
    return { success: false, error: "詳細エラー: " + e.toString() };
  }
}

Index.html

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body { font-family: sans-serif; padding: 30px; text-align: center; background: #f0f2f5; }
    .card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width: 500px; margin: auto; }
    #drop-zone { border: 2px dashed #4285F4; padding: 40px; border-radius: 5px; margin: 20px 0; background: #f8faff; cursor: pointer; }
    .btn { background: #4285F4; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 16px; }
    .btn:disabled { background: #ccc; }
    #status { margin-top: 20px; color: #555; }
  </style>
</head>
<body>
  <div class="card">
    <h2>スライド背景一括設定</h2>
    <p style="font-size: 13px; color: #666;">PDFから変換した画像をすべて選択してアップロードしてください</p>
    
    <div id="drop-zone" onclick="document.getElementById('fileInput').click()">
      画像ファイルをすべて選択<br>(1.jpg, 2.jpg...)
      <input type="file" id="fileInput" multiple accept="image/*" style="display:none">
    </div>
    
    <button id="uploadBtn" class="btn" onclick="uploadFiles()">スライドを生成</button>
    <div id="status"></div>
  </div>

  <script>
    async function uploadFiles() {
      const input = document.getElementById('fileInput');
      const files = input.files;
      if (files.length === 0) return alert("ファイルを選択してください");

      const btn = document.getElementById('uploadBtn');
      const status = document.getElementById('status');
      btn.disabled = true;
      status.innerText = "画像を処理中... (枚数が多いと時間がかかります)";

      const fileDataList = [];
      
      // ファイルを1つずつBase64に変換
      for (let i = 0; i < files.length; i++) {
        const file = files[i];
        const data = await new Promise(resolve => {
          const reader = new FileReader();
          reader.onload = e => resolve(e.target.result);
          reader.readAsDataURL(file);
        });
        fileDataList.push({
          data: data,
          name: file.name,
          contentType: file.type
        });
      }

      google.script.run
        .withSuccessHandler(res => {
          btn.disabled = false;
          if(res.success) {
            status.innerHTML = `<a href="${res.url}" target="_blank">✅ スライドが完成しました(クリックして開く)</a>`;
          } else {
            status.innerText = "エラー: " + res.error;
          }
        })
        .setBackgroundsFromImages(fileDataList);
    }
  </script>
</body>
</html>

🚀 使い方・ルート

  1. PDFを画像に変換する

    • Adobeの無料PDFをJPGに変換ツールなどを使って、NotebookLMのPDF(12ページ)を12枚の画像にします。

    • ファイル名が 1.jpg, 2.jpg... となっていることを確認します。

  2. GASの作成とデプロイ

    • 新しいGASプロジェクトを作り、上記コードを貼ります。

    • **Drive API(v2またはv3)**をサービスに追加します(IDは Drive)。

    • 「デプロイ」→「新しいデプロイ」→「ウェブアプリ」で公開します。

  3. 実行

    • 公開されたURLを開き、生成した画像をまとめて選択して「スライドを生成」を押します。

  4. 仕上げ

    • 完成したスライドを開きます。

    • 以前作った「テキスト抽出GAS」で取得したテキストを、この背景画像スライドの上にコピペして重ねれば、NotebookLMのデザインを維持したまま文字だけ編集できる完璧なスライドになります。

💡 なぜこの「別アプリ」方式が良いのか?

PDFを直接画像化する処理は、外部の有料APIなどを使わない限り非常に不安定です。この方式であれば、Google側の制限(OCRの回数制限など)に引っかからず、確実に高画質な背景を持つスライドが作成できます。

文字抽出GASとこの背景セットGASの2段構えで運用するのが、現在最もスマートな方法です!

まとめ


今回1日で(ほぼ半日)でNotebookLMのスライドからテキストと背景画像を摘出するGASでした。実際のところOCRの回数制限などが苦戦しました。
後半の画像変換だけでもGoogleスライドで編集できるので、これだけでもいいかと思います。
テキスト生成GASはかなり苦戦しまして、自身ではver.21までかかってしまったのが、大変な点でした。
今後、さらにバージョンアップを重ねて、一発で変換できるようにしたいと思いますので。ご期待ください!

いいなと思ったら応援しよう!