GAS実践 / Googleスライドの自動操作

第6回 GAS実践③ ── Googleスライドの自動操作

第4回・第5回で自動化したのは、データを動かす仕事でした。今回自動化するのは「人に見せるものを作る仕事」です。 資料づくりには、集計や転記と違って「見た目を整える」という工程がついてまわります。ここをどう機械に渡すかが今回のテーマです。 取り上げるのは5つのやり方。①GASを自分で書く → ②シートのデータから一括生成する → ③AIにGASごと設計させる → ④デザインを固定してAIには中身だけ作らせる(まじん式) → ⑤ChatGPTにPowerPointを直接作らせる。 どれか1つを覚えるのではなく、5つの選択肢を持ち帰って使い分けられる状態を目指します。 今回の題材とコードはすべてこのページだけで完結しています。第2〜5回の成果物は使いません。

🎯

到達目標

SlidesApp でスライドの作成・テキスト配置・図形追加ができる。デザインの規定をコードの先頭にまとめ、そこを書き換えるだけで全ページの見た目を変えられる構造を作れる。スプレッドシートのデータから報告資料を一括生成できる。さらに、デザインを固定して中身だけを生成AIに書かせる「テンプレート方式」を使え、ChatGPTにPowerPointを直接作らせ、プロジェクト機能で自社のトンマナを毎回保った状態で資料を出せる。

1今回のゴール ── 資料づくりを5段階で自動化する

「毎月おなじ形式の報告資料を作っている」── これは自動化の効果がいちばん出る仕事です。 形式が決まっているものは、機械に組み立てさせられるからです。 しかし資料づくりには、集計作業には無かった悩みがあります。「見た目をどうするか」です。

文字の大きさ、色、余白、どこに何を置くか。これを毎回考えていては自動化になりません。 かといって全部を生成AIに丸投げすると、実行するたびに違うデザインのものが出てきます。 今回学ぶ5つのやり方は、突き詰めると「デザインを誰が決めるか」と「中身を誰が書くか」の組み合わせの違いです。

左へ行くほど「自分で決める」/右へ行くほど「AIに任せる」 レベル1 自分でGASを書く デザイン=自分 中身=自分 仕組みが全部わかる レベル2 シートから一括生成 デザイン=自分 中身=表のデータ 毎月の定型に最強 レベル3 AIにGASを設計させる デザイン=AIと相談 中身=AIと相談 ゼロから作るとき レベル4 ★本命 まじん式 デザイン=固定 中身=AI 毎回おなじ品質 レベル5 ChatGPT × PowerPoint デザイン=ルール化 中身=AI Office環境向け Google スライドを作る(レベル1〜4) GASが動くのはGoogleの中だけ。社内で共有するなら スライドのURLを渡すだけで済む PowerPoint を作る(レベル5) 提出先が Office のときはこちら。ChatGPTが .pptx ファイルそのものを出力してくれる 選び方のめやす 毎月おなじ形式で作る → レベル2 / 中身が毎回ちがう → レベル4 一度きりの資料 → レベル3 / 提出先が PowerPoint → レベル5
図1:スライド自動化の5つのレベル。右へ行くほどAIの担当範囲が広がるが、そのぶんデザインを固定する工夫が必要になる
レベルやり方デザインを決めるのは中身を書くのは向いている場面
1GASを自分で書く自分(コードの定数)自分(コードに直書き)仕組みを理解する・型を作る
2シートのデータから生成自分(コードの定数)スプレッドシート毎月の定型報告
3AIにGASを設計させるAIと相談して決めるAIと相談して決めるゼロから作る・一度きりの資料
4まじん式(テンプレート方式)固定(触らせない)AI中身が毎回ちがう資料を量産する
5ChatGPT × PowerPointルールを渡して守らせるAI提出先がOffice環境
i

今回は「どれか1つを覚える回」ではありません。この5つは上位互換の関係ではなく、使い分けるものです。毎月同じ形式で出す報告書にレベル4を使うのは遠回りですし、一度きりの提案資料にレベル2の仕組みを作るのは無駄です。手元の仕事がどれに当たるかを判断できることが、今回いちばん持ち帰ってほしいものです。

2Googleスライドの構造と SlidesApp

第4回でスプレッドシートを操作したときは、SpreadsheetApp という窓口を使いました。 スライドの窓口は SlidesApp です。考え方は同じで、大きい入れ物から順に降りていくだけです。

階層スプレッドシート(第4回)スライド(今回)何にあたるか
いちばん外側SpreadsheetPresentationファイルそのもの
中の1単位Sheet(シート)Slide(1枚のページ)タブ/ページ
いちばん細かい単位Range(セル)PageElement(要素)置いてある物ひとつ

違いは、スライドには「セル」に相当する決まった枠が無いことです。 スプレッドシートは A1 と言えば場所が決まりますが、スライドは白紙の上に自分で座標を指定して物を置いていく世界です。 置ける物は、おおまかに次の4種類です。

1
テキストボックス

insertTextBox()。文字を置く。見出しも本文も、ほぼこれ

2
図形

insertShape()。四角・丸・矢印。罫線や色帯もこれで作る

3
表

insertTable()。行数・列数を決めてから、セルに書き込む

4
画像

insertImage()。公開されているURLか、Drive上のファイルを指定する

座標の考え方 ── 左上が原点、単位はポイント

物を置くときは 「左からの距離・上からの距離・幅・高さ」 の4つを指定します。 原点はスライドの左上で、単位はポイント(pt)です。 標準の16:9スライドは 横720 × 縦405ポイント。この数字は今回ずっと出てくるので覚えておいてください。

コード.gs ── いちばん短い例(イメージ)全19行
// スライドを1枚作り、文字と図形を1つずつ置くだけの例
function helloSlide() {
  const pres = SlidesApp.create('はじめてのスライド');   // 新しいスライドファイルを作る
  const slide = pres.getSlides()[0];                    // 最初の1枚を取り出す

  // 左から40pt・上から150ptの位置に、幅600pt・高さ60ptのテキストボックスを置く
  const box = slide.insertTextBox('はじめてのスライド', 40, 150, 600, 60);
  box.getText().getTextStyle()
    .setFontSize(32)
    .setBold(true)
    .setForegroundColor('#0d1b4c');

  // その下に、幅120pt・高さ4ptの青い帯を置く(見出しの下線のかわり)
  const bar = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, 40, 220, 120, 4);
  bar.getFill().setSolidFill('#2563eb');
  bar.getBorder().setTransparent();

  Logger.log(pres.getUrl());   // 完成したスライドのURLをログに出す
}
★

座標は「決め打ち」でよい、というのが今回の肝です。デザインが崩れる原因のほとんどは、毎回ちがう場所に物を置いてしまうことです。逆に言えば、置く場所さえ固定してしまえば、中身が変わってもデザインは崩れません。この後のコードは、すべて「座標を先に決めておく」という方針で書かれています。

Part 1 / レベル1〜3

まずはGASで、自分の手で作ってみる

最初に、コピペするだけで動くGASを1本お渡しします。これを動かしたうえで、 デザインの規定を自分の会社仕様に書き換える、スプレッドシートのデータから一括生成する、 AIにGASごと設計させる、と順に広げていきます。

3【レベル1】コピペで動かす ── デザイン規定つきGAS

まずは動かします。手順は4つだけです。まっさらなスプレッドシートから始めてください。

1
スプレッドシートを新規作成

名前は「スライド自動生成」など。中身は空のままでOK

2
拡張機能 → Apps Script

スクリプトエディタが開く。コード.gs の中身は全部消す

3
下のコードを全文貼り付け

コピーボタンを押して、そのまま貼り付けて保存(Ctrl/⌘ + S)

4
createDeck を実行

関数の選択欄で createDeck を選び、[実行]を押す

⚠

初回だけ「承認」を求められます。「このアプリはGoogleで確認されていません」と出たら、[詳細]→[(プロジェクト名)に移動(安全ではないページ)]と進んでください。自分で書いたコードを、自分のアカウントで動かすための確認です。第4回・第5回と同じ流れなので、迷ったらそのときの手順を思い出してください。

コード.gs ── レベル1 デザイン規定つきスライド自動生成(全文)全179行
/**
 * 第6回 レベル1 ── デザイン規定つきスライド自動生成
 *
 * 使い方:
 *   1. 新しいスプレッドシートを1つ作る(名前は「スライド自動生成」など)
 *   2. [拡張機能]→[Apps Script]を開く
 *   3. コード.gs の中身を消して、この全文を貼り付けて保存
 *   4. 関数の選択欄で createDeck を選び、[実行]を押す
 *   5. 実行ログに出てきたURLを開くと、スライドが完成している
 */

// ===== 1. デザイン規定(ここを変えると、全スライドの見た目が一度に変わる) =====
const DESIGN = {
  font: 'Arial',              // 日本語を明朝やゴシックにしたいときは 'Noto Sans JP' などに変更
  colorMain: '#0d1b4c',       // メインカラー(濃紺)
  colorAccent: '#2563eb',     // アクセントカラー(青)
  colorText: '#1a1a2e',       // 本文の文字色
  colorMuted: '#6b7280',      // 補足の文字色
  colorLine: '#c8ccd4',       // 罫線
  colorBg: '#ffffff',         // スライドの背景
  footerText: '株式会社 日本能率協会総合研究所'
};

// ===== 2. レイアウト(単位はポイント。16:9のスライドは 720 × 405) =====
const LAYOUT = {
  pageW: 720,
  pageH: 405,
  marginX: 42,
  titleTop: 150,
  dateTop: 232,
  headTop: 34,
  ruleTop: 74,
  subheadTop: 84,
  bodyTop: 118,
  lineHeight: 34,
  footerTop: 378
};

// ページ番号を数えるための変数
let pageCounter = 0;

// ===== 3. 実行する関数(ここに「どんなスライドを何枚作るか」を書く) =====
function createDeck() {
  const pres = SlidesApp.create('自動生成スライド_' + todayString_());
  removeFirstBlankSlide_(pres);
  pageCounter = 0;

  addTitleSlide_(pres, '調査レポート作成の自動化', todayString_());

  addSectionSlide_(pres, 1, '取り組みの背景');

  addContentSlide_(pres, '現在の課題', '毎月40時間が転記作業に消えている', [
    '集計結果をスライドへ転記する作業に、担当者1名で月40時間かかっている',
    '転記ミスが見つかるのが、発表の直前になりやすい',
    '担当者ごとに書式が違うため、レビューに時間がかかる'
  ]);

  addSectionSlide_(pres, 2, '自動化してわかったこと');

  addContentSlide_(pres, '導入の効果', '書式が決まっている資料は、機械に任せられる', [
    '転記作業は月40時間から3時間に短縮した',
    '書式が統一され、レビューでの差し戻しがほぼ無くなった',
    '人が考えるのは「中身」だけになった'
  ]);

  addClosingSlide_(pres, 'ご清聴ありがとうございました');

  Logger.log('スライドを作成しました:' + pres.getUrl());
}

// ===== 4. スライドの種類ごとの組み立て =====

/** 表紙 */
function addTitleSlide_(pres, title, dateText) {
  const slide = newSlide_(pres, DESIGN.colorBg);
  putBar_(slide, LAYOUT.marginX, LAYOUT.titleTop - 24, 60, 4, DESIGN.colorAccent);
  putText_(slide, title, LAYOUT.marginX, LAYOUT.titleTop, 620, 60,
    { size: 30, bold: true, color: DESIGN.colorMain });
  putText_(slide, dateText + ' / ' + DESIGN.footerText, LAYOUT.marginX, LAYOUT.dateTop, 560, 24,
    { size: 12, color: DESIGN.colorMuted });
  return slide;
}

/** 章扉(背景を濃紺にして、大きな章番号を置く) */
function addSectionSlide_(pres, no, title) {
  const slide = newSlide_(pres, DESIGN.colorMain);
  putText_(slide, ('0' + no).slice(-2), LAYOUT.marginX, 118, 220, 100,
    { size: 64, bold: true, color: '#3d4d7d' });
  putText_(slide, title, LAYOUT.marginX, 214, 600, 50,
    { size: 28, bold: true, color: '#ffffff' });
  return slide;
}

/** 中面(見出し+小見出し+箇条書き) */
function addContentSlide_(pres, title, subhead, points) {
  const slide = newSlide_(pres, DESIGN.colorBg);

  putText_(slide, title, LAYOUT.marginX, LAYOUT.headTop, 620, 32,
    { size: 22, bold: true, color: DESIGN.colorMain });
  putBar_(slide, LAYOUT.marginX, LAYOUT.ruleTop, 130, 3, DESIGN.colorAccent);

  if (subhead) {
    putText_(slide, subhead, LAYOUT.marginX, LAYOUT.subheadTop, 620, 22,
      { size: 12, color: DESIGN.colorMuted });
  }

  for (let i = 0; i < points.length; i++) {
    const top = LAYOUT.bodyTop + i * LAYOUT.lineHeight;
    putBar_(slide, LAYOUT.marginX, top + 9, 6, 6, DESIGN.colorAccent);
    putText_(slide, points[i], LAYOUT.marginX + 18, top, 600, 28,
      { size: 13, color: DESIGN.colorText });
  }

  putFooter_(slide);
  return slide;
}

/** 結び */
function addClosingSlide_(pres, message) {
  const slide = newSlide_(pres, DESIGN.colorMain);
  putText_(slide, message, 0, 180, LAYOUT.pageW, 40,
    { size: 24, bold: true, color: '#ffffff', align: SlidesApp.ParagraphAlignment.CENTER });
  return slide;
}

// ===== 5. 部品(ここから下は、慣れるまで触らなくてよい) =====

/** まっさらなスライドを1枚足して、背景色を塗る */
function newSlide_(pres, bgColor) {
  const slide = pres.appendSlide(SlidesApp.PredefinedLayout.BLANK);
  slide.getBackground().setSolidFill(bgColor);
  return slide;
}

/** テキストボックスを1つ置く */
function putText_(slide, text, left, top, width, height, options) {
  const opt = options || {};
  const box = slide.insertTextBox(String(text), left, top, width, height);
  const style = box.getText().getTextStyle();
  style.setFontFamily(DESIGN.font);
  style.setFontSize(opt.size || 13);
  style.setBold(opt.bold === true);
  style.setForegroundColor(opt.color || DESIGN.colorText);
  if (opt.align) {
    box.getText().getParagraphStyle().setParagraphAlignment(opt.align);
  }
  return box;
}

/** 塗りつぶした四角を1つ置く(罫線・アクセントバー・箇条書きの点に使う) */
function putBar_(slide, left, top, width, height, color) {
  const bar = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top, width, height);
  bar.getFill().setSolidFill(color);
  bar.getBorder().setTransparent();
  return bar;
}

/** 全ページ共通のフッター(会社名とページ番号) */
function putFooter_(slide) {
  pageCounter++;
  putBar_(slide, 0, LAYOUT.footerTop - 10, LAYOUT.pageW, 1, DESIGN.colorLine);
  putText_(slide, DESIGN.footerText, LAYOUT.marginX, LAYOUT.footerTop, 400, 16,
    { size: 8, color: DESIGN.colorMuted });
  putText_(slide, String(pageCounter), LAYOUT.pageW - LAYOUT.marginX - 40, LAYOUT.footerTop, 40, 16,
    { size: 8, color: DESIGN.colorMuted, align: SlidesApp.ParagraphAlignment.END });
}

/** SlidesApp.create で最初にできる空のスライドを消す */
function removeFirstBlankSlide_(pres) {
  const slides = pres.getSlides();
  for (let i = slides.length - 1; i >= 0; i--) {
    slides[i].remove();
  }
}

/** 今日の日付を 2026.09.01 の形にする */
function todayString_() {
  return Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy.MM.dd');
}

実行すると、下部の実行ログに スライドを作成しました:https://docs.google.com/presentation/d/... というURLが出ます。 クリックすると、表紙・章扉・本文・結びの6枚が出来上がっているはずです。

このコードは何をしているのか

長く見えますが、やっていることは3層に分かれているだけです。この構造が今回いちばん大事なところです。

ブロック役割変更するか
1. デザイン規定(DESIGN)
2. レイアウト(LAYOUT)
色・フォント・余白・座標を1か所にまとめた設定表ここだけ変える。会社の色にするならここ
3. 実行する関数(createDeck)「どんなスライドを、何枚、どの順で作るか」を書く台本資料の中身に応じて書き換える
4. スライドの種類ごとの組み立て
5. 部品
表紙・章扉・本文をどう描くかの実装基本は触らない

注目してほしいのは、createDeck の中身です。 addContentSlide_(pres, '現在の課題', '毎月40時間が…', ['…', '…']) のように、 「タイトル・小見出し・箇条書き」を渡すだけで1枚できています。 フォントの指定も、余白の計算も、フッターの配置も、呼び出す側には出てきません。 これが「デザインを固定した」状態です。

✓

末尾のアンダースコアには意味があります。addTitleSlide_ のように名前の最後に _ を付けた関数は、スクリプトエディタの実行メニューに出てこなくなります。「これは部品なので、直接実行するものではない」という印です。実行してよい関数(createDeck)だけがメニューに並ぶので、押し間違いが減ります。

4デザイン規定を自分の会社仕様に変える

いま出来上がったスライドは、濃紺と青の配色になっています。これを自分の部署の資料の色に変えてみましょう。 変えるのは DESIGN の中だけで、他の行は1文字も触りません。

コード.gs ── DESIGN を書き換える(例)全11行
// ===== 1. デザイン規定(ここだけ書き換える。他の行は触らない) =====
const DESIGN = {
  font: 'Noto Sans JP',              // ← 日本語ゴシックに変更
  colorMain: '#1f3864',              // ← メインカラーを自社の紺に
  colorAccent: '#c00000',            // ← アクセントを自社の赤に
  colorText: '#333333',
  colorMuted: '#767171',
  colorLine: '#d9d9d9',
  colorBg: '#ffffff',
  footerText: '調査事業本部 第1調査部'   // ← 部署名に変更
};

保存して createDeck をもう一度実行すると、全ページの配色が一度に入れ替わります。 この「1か所を変えれば全部変わる」構造が、資料を量産するときに効いてきます。

現在の課題 毎月40時間が転記作業に消えている 集計結果をスライドへ転記する作業に… 転記ミスが見つかるのが発表の直前に… 担当者ごとに書式が違い、レビューに… 株式会社 日本能率協会総合研究所 1 colorMain / LAYOUT.headTop colorAccent / LAYOUT.ruleTop colorMuted / LAYOUT.subheadTop colorText / LAYOUT.bodyTop footerText / LAYOUT.footerTop 左のスライドに見えているものは、すべて右の設定値のどれかで決まっている
図2:設定値とスライド上の場所の対応。数値を変えたときに何が動くかを、この図で確認できる
★

色は3系統までに抑えてください。メインカラー(濃い色)・アクセントカラー(1色だけ効かせる色)・グレー系(補足の文字と罫線)の3つです。色を増やすほど、資料は素人っぽく見えます。「強調したい箇所が3つあるから3色使う」ではなく、アクセント1色を、いちばん見てほしい1か所にだけ使うのがコツです。

変えたいこと触る場所補足
配色DESIGN.colorMain / colorAccentカラーコードは # から始まる6桁で書く
フォントDESIGN.font'Noto Sans JP' や 'M PLUS 1p' など、Googleスライドのフォント一覧にある名前を正確に
フッターの会社名DESIGN.footerText部署名を入れてもよい
本文の位置・行間LAYOUT.bodyTop / lineHeight行が下にはみ出すときは lineHeight を小さくする
左右の余白LAYOUT.marginX大きくすると余白が広がり、落ち着いた印象になる

5【レベル2】スプレッドシートのデータから一括生成

ここからが実務の本番です。レベル1では、スライドの中身をコードに直接書いていました。 これでは、来月また同じ作業をするときにコードを開いて文章を書き換えることになります。それでは自動化とは言えません。

そこで、中身をスプレッドシートに出します。表を更新してスクリプトを実行すれば、 行の数だけスライドが自動で増える。これがレベル2です。

手順1:シートを用意する

レベル1で使ったスプレッドシートに、「報告データ」という名前のシートを追加してください (下のタブを右クリック →[名前を変更])。 データは手で入力しなくて構いません。下の表の右上[TSVでコピー]を押して、 シートの A1 セルを選んでそのまま貼り付けると、見出し行を含めて一度に入ります。 1行目の見出しは必ずこの順番にしてください。コードが列の位置で読んでいます。

報告データ ── A1セルに貼り付けてください
A列:部署B列:対象月C列:主な実績D列:課題E列:次月の打ち手F列:達成率
調査1部2026年8月受託件数18件(前月比+3)報告書の作成が納期ぎりぎり集計テンプレートの共通化112
調査2部2026年8月受託件数12件(前月比-1)自由記述の分類に工数がかかる分類作業の一部を生成AIへ88
企画部2026年8月新規提案7件・受注2件提案書の作成に時間がかかる提案書の型を3種類に絞る95
営業推進部2026年8月商談化24件(前月比+5)商談後のフォローが属人化フォローの自動リマインド104
★

貼り付けたあとに1つだけ確認してください。F列の達成率が数値として入っているか(セルの右側に寄っていれば数値)。文字列として入ってしまった場合は、その列を選んで[表示形式]→[数値]に直してください。この列だけは、コードが計算に使っています。

手順2:関数を2つ追加する

スクリプトエディタを開き、レベル1のコードの一番下に、次の2つの関数を貼り足してください。 上のコードは消しません。DESIGN も addContentSlide_ も、そのまま部品として使い回します。

コード.gs ── レベル1のコードの末尾に貼り足す全78行
/**
 * 第6回 レベル2 ── スプレッドシートのデータからスライドを一括生成
 *
 * レベル1のコードの「一番下」に、この2つの関数を貼り足してください。
 * DESIGN / LAYOUT / addTitleSlide_ などの部品は、レベル1のものをそのまま使います。
 *
 * 実行前に、同じスプレッドシートに「報告データ」という名前のシートを作り、
 * 1行目に見出し、2行目から部署ごとのデータを入れておきます。
 */

/** シートを読んで、部署の数だけスライドを作る */
function createReportFromSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('報告データ');
  if (!sheet) {
    throw new Error('「報告データ」という名前のシートが見つかりません。シート名を確認してください。');
  }

  const values = sheet.getDataRange().getValues();
  const rows = values.slice(1);   // 1行目は見出しなので使わない

  const pres = SlidesApp.create('月次報告_' + todayString_());
  removeFirstBlankSlide_(pres);
  pageCounter = 0;

  addTitleSlide_(pres, '月次報告', todayString_());
  addSectionSlide_(pres, 1, '部署別の報告');

  rows.forEach(function (row) {
    const dept = row[0];
    if (!dept) return;            // 空行は飛ばす

    addContentSlide_(pres, dept + ' ' + row[1], '達成率 ' + row[5] + '%', [
      '実績:' + row[2],
      '課題:' + row[3],
      '打ち手:' + row[4]
    ]);
  });

  addSectionSlide_(pres, 2, '全体サマリ');
  addSummarySlide_(pres, rows);
  addClosingSlide_(pres, 'ご清聴ありがとうございました');

  Logger.log('スライドを作成しました:' + pres.getUrl());
}

/** 達成率を横棒グラフのように並べた1枚を作る */
function addSummarySlide_(pres, rows) {
  const slide = newSlide_(pres, DESIGN.colorBg);

  putText_(slide, '達成率の一覧', LAYOUT.marginX, LAYOUT.headTop, 620, 32,
    { size: 22, bold: true, color: DESIGN.colorMain });
  putBar_(slide, LAYOUT.marginX, LAYOUT.ruleTop, 130, 3, DESIGN.colorAccent);
  putText_(slide, '棒の長さは達成率。100%を満たすと右端まで伸びる', LAYOUT.marginX, LAYOUT.subheadTop, 620, 22,
    { size: 12, color: DESIGN.colorMuted });

  const barLeft = LAYOUT.marginX + 120;
  const barMaxW = 420;
  let top = LAYOUT.bodyTop;

  rows.forEach(function (row) {
    if (!row[0]) return;

    const rate = Number(row[5]) || 0;
    const filled = Math.max(2, barMaxW * Math.min(rate, 100) / 100);   // 幅0の図形は作れないので最低2

    putText_(slide, row[0], LAYOUT.marginX, top, 110, 22,
      { size: 12, color: DESIGN.colorText });
    putBar_(slide, barLeft, top + 6, barMaxW, 10, '#eef1f6');          // 目盛りの下地
    putBar_(slide, barLeft, top + 6, filled, 10, DESIGN.colorAccent);  // 実績ぶん
    putText_(slide, rate + '%', barLeft + barMaxW + 12, top, 60, 22,
      { size: 12, color: DESIGN.colorMuted });

    top += 30;
  });

  putFooter_(slide);
  return slide;
}

保存して createReportFromSheet を実行すると、 表紙 → 章扉 → 部署ごとに1枚(4枚)→ 章扉 → 達成率の一覧 → 結び の全8枚が出来上がります。 シートに5行目を足してもう一度実行すれば、9枚になります。コードは1文字も変えずに、です。

✓

ここが「自動化した」と言える状態です。来月やることは、シートの数字を新しくして、実行ボタンを1回押すだけ。転記も、書式合わせも、ページ番号の振り直しも発生しません。第4回で作った集計スクリプトの出力先をこのシートにすれば、集計から資料作成までが1本につながります。

グラフのように見せている部分

addSummarySlide_ では、達成率を横棒で表しています。 やっていることは単純で、灰色の四角を目盛りとして置き、その上に達成率のぶんだけ青い四角を重ねているだけです。 barMaxW * Math.min(rate, 100) / 100 が、その「ぶん」を計算している式です。

⚠

幅や高さに 0 を指定するとエラーになります。達成率が 0% の行があると、青い四角の幅が 0 になって GAS が止まります。コード中の Math.max(2, ...) はそれを防ぐための保険です。データ側に想定外の値が来ても止まらないようにする ── これは自動化を長く運用するうえで欠かせない考え方です。

6【レベル3】AIに「最適なGAS」を設計させる

ここまでは、私たちが用意した型の上で作ってきました。 では、まったく新しい種類の資料を作りたいときはどうするか。ここで生成AIの出番です。

ただし、「スライドを作るGASを書いて」と頼むだけでは、まず失敗します。 スライドのGASは、スプレッドシートのGASと比べて格段に難しいからです。 理由は3つあります。

  • 座標をすべて自分で決める必要がある ── セルのような「決まった置き場所」が無い
  • 正解が1つに決まらない ── 「見やすい」は数値で書かれていないので、AIが勝手に判断してしまう
  • メソッド名が独特で長い ── getText().getTextStyle().setForegroundColor() のような連結が多く、AIが途中で間違えやすい

そこで、いきなりコードを書かせないプロンプトを使います。 先に目的と聞き手を聞かせ、構成案とデザイン方針を出させ、こちらが承認してからコードを書かせる。 このやり取りを最初から指示に埋め込んでおくのがコツです。

🤖 ChatGPT へのプロンプト ── 設計から任せてGASを書かせる46行
あなたは、Google Apps Script(GAS)とプレゼンテーション設計の両方に精通した専門家です。 これから私と一緒に、Googleスライドを自動生成するスクリプトを作ります。 【進め方 ── この順番を必ず守ってください】 ステップ1:いきなりコードを書かないでください。まず次の5点を私に質問してください。       私が答えるまで、コードは1行も書かないこと。       ① この資料の目的(報告/提案/共有のどれか、相手に何をしてほしいか)       ② 聞き手は誰か(役職・前提知識・いちばんの関心事)       ③ 持ち時間と想定枚数       ④ いちばん伝えたい結論を1文で       ⑤ 使いたい色・フォント・入れたいロゴがあるか ステップ2:私の回答をもとに、次の2つを提示してください。ここでもコードは書きません。       ・構成案:スライド1枚ごとに「番号/タイトル/このスライドで伝えたいこと1文/        使う見せ方(箇条書き・比較・手順・表・カードのどれか)」を表にする       ・デザイン方針:背景色、メインカラー、アクセントカラー、フォント、文字サイズ、        余白の取り方を、具体的な数値とカラーコードで示す       そのうえで「この構成とデザインで進めてよいか」を私に確認してください。 ステップ3:私が承認したら、GASのコード全文を書いてください。 【コードの条件】 ・Google Apps Script の SlidesApp だけを使う(外部ライブラリ・外部API・有料サービスは使わない) ・スライドのサイズは 720 × 405 ポイント(16:9)。座標の単位はすべてポイント ・冒頭に DESIGN(色・フォント)と LAYOUT(余白・座標)の定数をまとめ、  ここを書き換えるだけで全スライドの見た目が変わる構造にする ・スライドの種類ごとに関数を分ける(表紙/章扉/本文/結び) ・実行する関数名は createDeck とし、SlidesApp.create() で新規スライドを作って、  最後に Logger.log で完成したスライドのURLを出力する ・日本語のコメントを、初心者が読んで意味がわかる粒度で入れる ・コードは1つのファイルにまとめ、そのまま貼り付けて動く状態にする 【デザインの条件】 ・1スライドで伝えるメッセージは1つだけ。要素が多いときはスライドを分ける ・使う色はメインカラー・アクセントカラー・グレー系の3系統まで ・すべてのスライドで、見出しの位置・本文の左端・フッターの位置を揃える ・本文の文末に句点「。」は付けない(体言止めを基本とする) ・図形の幅や高さに 0 を指定しない(GASでエラーになるため) 【最後に必ず行うこと】 コードを出力したあと、次の項目を1つずつ確認して結果を報告してください。  □ 貼り付けてすぐ実行できる状態か(未定義の変数・関数が残っていないか)  □ 座標がスライドの範囲(720 × 405)を超えていないか  □ すべてのスライドで見出しと本文の左端が揃っているか  □ DESIGN の値を変えるだけで配色を変更できる構造になっているか

このプロンプトの使い方

1
そのまま送る

ChatGPTに貼り付けて送信。AIから5つの質問が返ってくる

2
質問に答える

目的・聞き手・時間・結論・色を答える。ここが資料の質を決める

3
構成案を直す

出てきた構成表に注文を付ける。「3枚目と4枚目を入れ替えて」など

4
承認してコードを受け取る

「これで進めて」と伝えると、GAS全文が出てくる

⚠

一発で完璧なものを狙わないでください。スライドのGASは、初回はどこかが必ずずれます(文字がはみ出す、図形が重なる、色が想定と違う)。そこで諦めずに、「実際に実行したらこうなった」を具体的に伝えて直させるのが正しい進め方です。第3回で学んだ「段階的に改善させる」やり方が、そのまま効きます。

🔧 ChatGPT へのプロンプト ── 出来上がりを直させる15行
さきほど作ってもらったGASを実行しました。結果を報告しますので、修正版のコード全文をください。 【実行結果】 (例:スライドは6枚できたが、3枚目の箇条書きの3行目が枠の下にはみ出している) 【エラー文(出ていれば、そのまま貼り付ける)】 【直してほしいこと】 ・ ・ 【守ってほしいこと】 ・DESIGN と LAYOUT の構造は変えないこと(値の変更はしてよい) ・修正した箇所を、コードのあとに箇条書きで説明すること ・コードは省略せず、そのまま貼り付けて動く全文を出すこと
★

スクリーンショットを貼るのがいちばん速いです。出来上がったスライドの画面を撮って、そのまま ChatGPT に貼り付け、「この赤枠のところが重なっている。直して」と伝えてください。文章で「3枚目の左下の図形が」と説明するより、はるかに正確に伝わります。ChatGPTは画像を読めます。

Part 2 / レベル4

デザインを固定して、中身だけを生成AIに書かせる

ここからが今回の本命です。「AIにコードを書かせない」という、一見すると後ろ向きに見える発想が、 実はいちばん安定して、いちばん品質が揃うという話をします。 世の中で「まじん式」と呼ばれている手法と、その考え方をそのまま使った教材オリジナルの小さいテンプレートの両方を扱います。

7【レベル4】まじん式 ── コード固定 × データ可変

最近は、文章を渡すだけでスライドを作ってくれるサービスがいくつもあります。 しかし業務で使おうとすると、「そのサービスを社内で使ってよいか」という壁にぶつかります。 情報システム部門の承認が下りない、社外サービスに社内資料を渡せない ── よくある話です。

この壁を、すでに会社が使っているツール(GoogleとChatGPT/Gemini)だけで越えるために考えられたのが、 「まじん式」と呼ばれる手法です。2025年8月に公開され、広く使われるようになりました。

i

出典:まじん氏「【神回】Googleスライドが一瞬で完成する“奇跡”のプロンプト教えます」(note、2025年8月公開)。プロンプト全文とGASテンプレートは無料で公開されています。本ページでも、受講者がすぐ使えるよう第8章にマスタープロンプトの全文を引用して掲載しています(コピーボタンで全文を取得できます)。まじん式は改良が続いているため、最新版は必ず上記の記事でご確認ください。また、この後で扱う教材オリジナルの小さいテンプレートは、同じ設計思想をこの研修用に書き起こしたものです。

なぜ「AIにGASを書かせる」と失敗するのか

第6章でも触れましたが、これは実験でもはっきり出ています。 「この文字起こしをもとにGoogleスライドを作るGASを書いて」 という短い依頼を主要なAIに投げると、 多くの場合構文エラーで動かないか、動いても見るに堪えないデザインになります。

原因は、依頼の規模が大きすぎることです。この頼み方は、たとえるなら 「自動車工場をゼロから設計・建設して、ついでに新型車も1台つくってくれ」と言っているのと同じです。 設計図も部品の規格も無い状態では、まともなものが安定して出てくるはずがありません。

逆転の発想 ── 工場は人間が建てる

まじん式のやり方は、この順番をひっくり返します。

✕ うまくいかない頼み方 素材(議事録など) これをスライドにして AI がコードごと書く デザインも毎回ゼロから考える 構文エラー 動いても体裁がばらばら ○ まじん式 ── コード固定 × データ可変 ① 人間が「工場」を建てる GASテンプレート(約700行) 色・座標・フォントを全部固定 一度作ればずっと使う ② 作れる型を一覧化する 箇条書き/比較/手順/時系列 レーン図/カード/表/進捗バー = 車種カタログ ③ AI は「生産計画書」だけ 素材を読んで構成を考え どの型をどの順で使うか決め slideData(データの配列)を書く コードには一切触らせない ④ 貼り付けて実行 slideData を差し替えるだけ デザインは毎回まったく同じ 中身だけが入れ替わる スピーカーノートも自動
図3:AIに苦手なこと(構文どおりのコード生成)をやらせず、得意なこと(文脈の理解と構造化)だけをやらせる。これがまじん式の設計思想

この作りが生む3つのメリット

1
エラーが激減する

AIが触るのはデータだけ。コードの構文を壊しようがない

2
AIが構成に集中できる

コード生成という雑務から解放され「どう見せれば伝わるか」に力を使える

3
品質が毎回そろう

デザインが固定なので、誰がいつ作っても同じ体裁になる

✓

この考え方は、スライド以外にも効きます。「毎回ちがう結果になって困る」という自動化は、たいていAIに任せる範囲が広すぎることが原因です。変わらない部分をプログラムで固定し、変わる部分だけをAIに書かせる。この切り分けは、報告書・メール文面・議事録・分析レポートなど、あらゆる場面で使えます。第14回の統合演習でも、この発想が中心になります。

8まじん式を実際に動かす

原典のまじん式を使う手順です。ChatGPT でも Gemini でも動きますが、 推論能力の高いモデルを選んでください(考える時間が長いモードを使う、と考えれば十分です)。

1
プロンプトをコピーする

下のマスタープロンプトの[コピー]を押して、全文をクリップボードへ

2
AIに設定する

ChatGPTのプロジェクト(またはGeminiのGem)に貼り、常設の指示にする

3
素材を渡す

議事録・レポート・企画書などを、そのまま貼り付けるか添付する

4
出力を全文コピー

AIが .gs ファイルの中身をまるごと出してくる。それを全部コピーする

5
スライドに貼る

空のGoogleスライドで[拡張機能]→[Apps Script]を開き、貼り付けて保存

6
実行する

generatePresentation を実行。承認して、スライドに戻る

マスタープロンプト全文

約4万字あります。画面では枠内をスクロールして確認でき、[コピー]を押せば全文がクリップボードに入ります。 中身は①AIへの指示(1.0〜6.0)と②GASテンプレートの設計図(7.0)の2部構成です。 AIはこれを読んで、7.0のコードはそのままに、slideData の中身だけを差し替えたものを出力してきます。

🧙 まじん式 マスタープロンプト全文(出典:まじん氏 note)約40,200字
## **1.0 PRIMARY_OBJECTIVE — 最終目標** あなたは、ユーザーから与えられた非構造テキスト情報を解析し、後述する **【GOOGLE_TEMPLATE_BLUEPRINT】** で定義された Google Apps Script(GAS)フレームワーク内で機能する、**slideData** という名の JavaScript オブジェクト配列を**生成**することだけに特化した、超高精度データサイエンティスト兼プレゼンテーション設計AIです。 あなたの**絶対的かつ唯一の使命**は、ユーザーの入力内容から論理的なプレゼンテーション構造を抽出し、各セクションに最適な「表現パターン(Pattern)」を選定し、さらに各スライドで話すべき発表原稿(スピーカーノート)のドラフトまで含んだ、ブループリント内の const slideData = [...] を完全に置き換えるための、完璧でエラーのない JavaScript オブジェクト配列を生成することです。 **slideData の生成以外のタスクを一切実行してはなりません。** ブループリントのロジック、デザイン設定、関数名、変数名など、1文字たりとも変更することは固く禁じられています。あなたの思考と出力のすべては、最高の slideData を生成するためだけに費やされます。 ## **2.0 GENERATION_WORKFLOW — 厳守すべき思考と生成のプロセス** 1. **【ステップ1: コンテキストの完全分解と正規化】** * **分解**: ユーザー提供のテキスト(議事録、記事、企画書、メモ等)を読み込み、**目的・意図・聞き手**を把握。内容を「**章(Chapter)→ 節(Section)→ 要点(Point)**」の階層に内部マッピング。 * **正規化**: 入力前処理を自動実行。(タブ→スペース、連続スペース→1つ、スマートクォート→ASCIIクォート、改行コード→LF、用語統一) 2. **【ステップ2: パターン選定と論理ストーリーの再構築】** * 章・節ごとに、後述の**サポート済み表現パターン**から最適なものを選定(例: 比較なら compare、時系列なら timeline)。 * 聞き手に最適な**説得ライン**(問題解決型、PREP法、時系列など)へ再配列。 3. **【ステップ3: スライドタイプへのマッピング】** * ストーリー要素を **Googleパターン・スキーマ**に**最適割当**。 * 表紙 → title / 章扉 → section(※背景に**半透明の大きな章番号**を描画) / 本文 → content, compare, process, timeline, diagram, cards, table, progress / 結び → closing 4. **【ステップ4: オブジェクトの厳密な生成】** * **3.0 スキーマ**と**4.0 ルール**に準拠し、文字列をエスケープ(' → \', \ → \\)して1件ずつ生成。 * **インライン強調記法**を使用可: * **太字** → 太字 * [[重要語]] → **太字+Googleブルー**(#4285F4) * **画像URLの抽出**: 入力テキスト内の ![](...png|.jpg|.jpeg|.gif|.webp) 形式、または裸URLで末尾が画像拡張子のものを抽出し、該当スライドの images 配列に格納(説明文がある場合は media の caption に入れる)。 * **スピーカーノート生成**: 各スライドの内容に基づき、発表者が話すべき内容の**ドラフトを生成**し、notesプロパティに格納する。 5. **【ステップ5: 自己検証と反復修正】** * **チェックリスト**: * 文字数・行数・要素数の上限遵守(各パターンの規定に従うこと) * 箇条書き要素に**改行(\n)を含めない** * テキスト内に**禁止記号**(■ / →)を含めない(※装飾・矢印はスクリプトが描画) * 箇条書き文末に **句点「。」を付けない**(体言止め推奨) * notesプロパティが各スライドに適切に設定されているか確認 * title.dateはYYYY.MM.DD形式 * **アジェンダ安全装置**: 「アジェンダ/Agenda/目次/本日お伝えすること」等のタイトルで points が空の場合、**章扉(section.title)から自動生成**するため、空配列を返さず **ダミー3点**以上を必ず生成 6. **【ステップ6: 最終出力】** * 検証済みオブジェクトを論理順に const slideData = [...] に格納。**【GOOGLE_TEMPLATE_BLUEPRINT】全文**をそのまま出力し、**サンプルの slideData ブロックだけ**をあなたが生成した slideData で**完全置換**した **単一 .gs ファイルの中身**のみを出力すること。**解説・前置き・後書き一切禁止**。 ## **3.0 slideDataスキーマ定義(GooglePatternVer.+SpeakerNotes)** **共通プロパティ** * **notes?: string**: すべてのスライドオブジェクトに任意で追加可能。スピーカーノートに設定する発表原稿のドラフト(プレーンテキスト)。 **スライドタイプ別定義** * **タイトル**: { type: 'title', title: '...', date: 'YYYY.MM.DD', notes?: '...' } * **章扉**: { type: 'section', title: '...', sectionNo?: number, notes?: '...' } ※sectionNo を指定しない場合は自動連番 * **クロージング**: { type: 'closing', notes?: '...' } **本文パターン(必要に応じて選択)** * **content(1カラム/2カラム+画像+小見出し)** { type: 'content', title: '...', subhead?: string, points?: string[], twoColumn?: boolean, columns?: [string[], string[]], images?: (string | { url: string, caption?: string })[], notes?: '...' } * **compare(対比)** { type: 'compare', title: '...', subhead?: string, leftTitle: '...', rightTitle: '...', leftItems: string[], rightItems: string[], images?: string[], notes?: '...' } * **process(手順・工程)** { type: 'process', title: '...', subhead?: string, steps: string[], images?: string[], notes?: '...' } * **timeline(時系列)** { type: 'timeline', title: '...', subhead?: string, milestones: { label: string, date: string, state?: 'done'|'next'|'todo' }[], images?: string[], notes?: '...' } * **diagram(レーン図)** { type: 'diagram', title: '...', subhead?: string, lanes: { title: string, items: string[] }[], images?: string[], notes?: '...' } * **cards(カードグリッド)** { type: 'cards', title: '...', subhead?: string, columns?: 2|3, items: (string | { title: string, desc?: string })[], images?: string[], notes?: '...' } * **table(表)** { type: 'table', title: '...', subhead?: string, headers: string[], rows: string[][], notes?: '...' } * **progress(進捗)** { type: 'progress', title: '...', subhead?: string, items: { label: string, percent: number }[], notes?: '...' } ## **4.0 COMPOSITION_RULES(GooglePatternVer.) — 美しさと論理性を最大化する絶対規則** * **全体構成**: 1. title(表紙) 2. content(アジェンダ、※章が2つ以上のときのみ) 3. section 4. 本文(content/compare/process/timeline/diagram/cards/table/progress から2〜5枚) 5. (3〜4を章の数だけ繰り返し) 6. closing(結び) * **テキスト表現・字数**(最大目安): * title.title: 全角35文字以内 * section.title: 全角30文字以内 * 各パターンの title: 全角40文字以内 * **subhead**: 全角50文字以内(フォント18) * 箇条書き等の要素テキスト: 各90文字以内・**改行禁止** * **notes(スピーカーノート)**: 発表内容を想定したドラフト。文字数制限は緩やかだが、要点を簡潔に。**プレーンテキスト**とし、強調記法は用いないこと。 * **禁止記号**: ■ / → を含めない(矢印や区切りはスクリプト側で描画) * 箇条書き文末の句点「。」**禁止**(体言止め推奨) * **インライン強調記法**: **太字** と [[重要語]](太字+Googleブルー)を必要箇所に使用可 ## **5.0 SAFETY_GUIDELINES — GASエラー回避とAPI負荷の配慮** * スライド上限: **最大50枚** * 画像制約: **50MB未満・25MP以下**の **PNG/JPEG/GIF/WebP** * 実行時間: Apps Script 全体で約 **6分** * テキストオーバーフロー回避: 本命令の**上限値厳守** * フォント: Arial が無い環境では標準サンセリフに自動フォールバック * 文字列リテラルの安全性: ' と \ を確実にエスケープ ## **6.0 OUTPUT_FORMAT — 最終出力形式** * 出力は **【GOOGLE_TEMPLATE_BLUEPRINT】の完全な全文**であり、唯一の差分が const slideData =... の中身になるように生成すること。 * **コード以外のテキスト(前置き/解説/謝罪/補足)は一切含めない。** ## **7.0 GOOGLE_TEMPLATE_BLUEPRINT — 【Universal Google Design Ver.】完成済み設計図** /** * @OnlyCurrentDoc * このスクリプトは、Google風デザインテンプレートに基づきGoogleスライドを自動生成します。 * Version: 12.0 (Universal Google Design - Final) * Author: Googleスライド自動生成マスター * Prompt Design: まじん式プロンプト * Description: 指定されたslideData配列を元に、Google風デザインに準拠したスライドを生成します。 */ // --- 1. 実行設定 --- const SETTINGS = { SHOULD_CLEAR_ALL_SLIDES: true, TARGET_PRESENTATION_ID: null }; // --- 2. マスターデザイン設定 (Google Design Ver.) --- const CONFIG = { BASE_PX: { W: 960, H: 540 }, // レイアウトの基準となる不変のpx値 POS_PX: { titleSlide: { logo: { left: 55, top: 105, width: 135 }, title: { left: 50, top: 230, width: 800, height: 90 }, date: { left: 50, top: 340, width: 250, height: 40 }, }, // 共通ヘッダーを持つ各スライド contentSlide: { headerLogo: { right: 20, top: 20, width: 75 }, title: { left: 25, top: 60, width: 830, height: 65 }, titleUnderline: { left: 25, top: 128, width: 260, height: 4 }, subhead: { left: 25, top: 140, width: 830, height: 30 }, body: { left: 25, top: 172, width: 910, height: 303 }, twoColLeft: { left: 25, top: 172, width: 440, height: 303 }, twoColRight: { left: 495, top: 172, width: 440, height: 303 } }, compareSlide: { headerLogo: { right: 20, top: 20, width: 75 }, title: { left: 25, top: 60, width: 830, height: 65 }, titleUnderline: { left: 25, top: 128, width: 260, height: 4 }, subhead: { left: 25, top: 140, width: 830, height: 30 }, leftBox: { left: 25, top: 172, width: 430, height: 303 }, rightBox: { left: 505, top: 172, width: 430, height: 303 } }, processSlide: { headerLogo: { right: 20, top: 20, width: 75 }, title: { left: 25, top: 60, width: 830, height: 65 }, titleUnderline: { left: 25, top: 128, width: 260, height: 4 }, subhead: { left: 25, top: 140, width: 830, height: 30 }, area: { left: 25, top: 172, width: 910, height: 303 } }, timelineSlide: { headerLogo: { right: 20, top: 20, width: 75 }, title: { left: 25, top: 60, width: 830, height: 65 }, titleUnderline: { left: 25, top: 128, width: 260, height: 4 }, subhead: { left: 25, top: 140, width: 830, height: 30 }, area: { left: 25, top: 172, width: 910, height: 303 } }, diagramSlide: { headerLogo: { right: 20, top: 20, width: 75 }, title: { left: 25, top: 60, width: 830, height: 65 }, titleUnderline: { left: 25, top: 128, width: 260, height: 4 }, subhead: { left: 25, top: 140, width: 830, height: 30 }, lanesArea: { left: 25, top: 172, width: 910, height: 303 } }, cardsSlide: { headerLogo: { right: 20, top: 20, width: 75 }, title: { left: 25, top: 60, width: 830, height: 65 }, titleUnderline: { left: 25, top: 128, width: 260, height: 4 }, subhead: { left: 25, top: 140, width: 830, height: 30 }, gridArea: { left: 25, top: 172, width: 910, height: 303 } }, tableSlide: { headerLogo: { right: 20, top: 20, width: 75 }, title: { left: 25, top: 60, width: 830, height: 65 }, titleUnderline: { left: 25, top: 128, width: 260, height: 4 }, subhead: { left: 25, top: 140, width: 830, height: 30 }, area: { left: 25, top: 172, width: 910, height: 303 } }, progressSlide: { headerLogo: { right: 20, top: 20, width: 75 }, title: { left: 25, top: 60, width: 830, height: 65 }, titleUnderline: { left: 25, top: 128, width: 260, height: 4 }, subhead: { left: 25, top: 140, width: 830, height: 30 }, area: { left: 25, top: 172, width: 910, height: 303 } }, // 章扉(背景に大きなゴースト番号) sectionSlide: { title: { left: 55, top: 230, width: 840, height: 80 }, ghostNum: { left: 35, top: 120, width: 300, height: 200 } }, footer: { leftText: { left: 15, top: 505, width: 250, height: 20 }, rightPage: { right: 15, top: 505, width: 50, height: 20 } }, bottomBar: { left: 0, top: 534, width: 960, height: 6 } }, FONTS: { family: 'Arial', sizes: { title: 45, date: 16, sectionTitle: 38, contentTitle: 28, subhead: 18, body: 14, footer: 9, chip: 11, laneTitle: 13, small: 10, processStep: 14, axis: 12, ghostNum: 180 } }, COLORS: { primary_blue: '#4285F4', google_red: '#EA4335', google_yellow: '#FBBC04', google_green: '#34A853', text_primary: '#333333', background_white: '#FFFFFF', background_gray: '#f8f9fa', faint_gray: '#e8eaed', lane_title_bg: '#f5f5f3', lane_border: '#dadce0', card_bg: '#ffffff', card_border: '#dadce0', neutral_gray: '#9e9e9e', ghost_gray: '#efefed' }, DIAGRAM: { laneGap_px: 24, lanePad_px: 10, laneTitle_h_px: 30, cardGap_px: 12, cardMin_h_px: 48, cardMax_h_px: 70, arrow_h_px: 10, arrowGap_px: 8 }, LOGOS: { header: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Google_2015_logo.svg/1024px-Google_2015_logo.svg.png', closing: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Google_2015_logo.svg/1024px-Google_2015_logo.svg.png' }, FOOTER_TEXT: `© ${new Date().getFullYear()} Your Organization` }; // --- 3. スライドデータ(サンプル:必ず置換してください) --- const slideData = [ { type: 'title', title: 'サンプルプレゼンテーション', date: '2025.08.12', notes: '本日はお集まりいただきありがとうございます。このプレゼンテーションは、Google風デザインテンプレートの機能と可能性についてご説明するものです。' }, { type: 'section', title: '1. はじめに', notes: '最初のセクションでは、このテンプレートが持つ主要な表現パターンについて概観します。' }, { type: 'cards', title: 'Google風デザインのテスト', subhead: 'モダンなデザインパターン', columns: 3, items: [ { title: 'パターン1', desc: '現状:[[重要機能]]実装済み\n課題:パフォーマンス**最適化**が必要' }, { title: 'パターン2', desc: '現状:デザイン更新完了\n課題:[[ユーザビリティ改善]]を検討' }, { title: 'パターン3', desc: '現状:テスト環境構築\n課題:**本番環境への移行準備**' } ], notes: 'こちらがカード形式のスライドです。3つの異なる項目を並べて比較検討する際に便利です。それぞれのカードにはタイトルと説明を設定できます。' }, { type: 'closing', notes: '以上で説明を終わります。ご清聴ありがとうございました。何かご質問はありますでしょうか。' } ]; // --- 4. メイン実行関数 --- let __SECTION_COUNTER = 0; // 章番号カウンタ(ゴースト数字用) function generatePresentation() { let presentation; try { presentation = SETTINGS.TARGET_PRESENTATION_ID ? SlidesApp.openById(SETTINGS.TARGET_PRESENTATION_ID) : SlidesApp.getActivePresentation(); if (!presentation) throw new Error('対象のプレゼンテーションが見つかりません。'); if (SETTINGS.SHOULD_CLEAR_ALL_SLIDES) { const slides = presentation.getSlides(); for (let i = slides.length - 1; i >= 0; i--) slides[i].remove(); } __SECTION_COUNTER = 0; const layout = createLayoutManager(presentation.getPageWidth(), presentation.getPageHeight()); let pageCounter = 0; for (const data of slideData) { const generator = slideGenerators[data.type]; if (data.type !== 'title' && data.type !== 'closing') pageCounter++; if (generator) { const slide = presentation.appendSlide(SlidesApp.PredefinedLayout.BLANK); generator(slide, data, layout, pageCounter); // スピーカーノートを設定する処理 if (data.notes) { try { const notesShape = slide.getNotesPage().getSpeakerNotesShape(); if (notesShape) { notesShape.getText().setText(data.notes); } } catch (e) { Logger.log(`スピーカーノートの設定に失敗しました: ${e.message}`); } } } } } catch (e) { Logger.log(`処理が中断されました: ${e.message}\nStack: ${e.stack}`); } } // --- 5. スライド生成ディスパッチャ --- const slideGenerators = { title: createTitleSlide, section: createSectionSlide, content: createContentSlide, compare: createCompareSlide, process: createProcessSlide, timeline: createTimelineSlide, diagram: createDiagramSlide, cards: createCardsSlide, table: createTableSlide, progress: createProgressSlide, closing: createClosingSlide }; // --- 6. スライド生成関数群 --- function createTitleSlide(slide, data, layout) { slide.getBackground().setSolidFill(CONFIG.COLORS.background_white); const logoRect = layout.getRect('titleSlide.logo'); const logo = slide.insertImage(CONFIG.LOGOS.header); const aspect = logo.getHeight() / logo.getWidth(); logo.setLeft(logoRect.left).setTop(logoRect.top).setWidth(logoRect.width).setHeight(logoRect.width * aspect); const titleRect = layout.getRect('titleSlide.title'); const titleShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, titleRect.left, titleRect.top, titleRect.width, titleRect.height); setStyledText(titleShape, data.title, { size: CONFIG.FONTS.sizes.title, bold: true }); const dateRect = layout.getRect('titleSlide.date'); const dateShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, dateRect.left, dateRect.top, dateRect.width, dateRect.height); dateShape.getText().setText(data.date || ''); applyTextStyle(dateShape.getText(), { size: CONFIG.FONTS.sizes.date }); drawBottomBar(slide, layout); } function createSectionSlide(slide, data, layout, pageNum) { slide.getBackground().setSolidFill(CONFIG.COLORS.background_gray); // 透かし番号:sectionNo > タイトル先頭の数字 > 自動連番 __SECTION_COUNTER++; const parsedNum = (() => { if (Number.isFinite(data.sectionNo)) return Number(data.sectionNo); const m = String(data.title || '').match(/^\s*(\d+)[\.\.]/); return m ? Number(m[1]) : __SECTION_COUNTER; })(); const num = String(parsedNum).padStart(2, '0'); const ghostRect = layout.getRect('sectionSlide.ghostNum'); const ghost = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, ghostRect.left, ghostRect.top, ghostRect.width, ghostRect.height); ghost.getText().setText(num); applyTextStyle(ghost.getText(), { size: CONFIG.FONTS.sizes.ghostNum, color: CONFIG.COLORS.ghost_gray, bold: true }); try { ghost.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e) {} const titleRect = layout.getRect('sectionSlide.title'); const titleShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, titleRect.left, titleRect.top, titleRect.width, titleRect.height); titleShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); setStyledText(titleShape, data.title, { size: CONFIG.FONTS.sizes.sectionTitle, bold: true, align: SlidesApp.ParagraphAlignment.CENTER }); addGoogleFooter(slide, layout, pageNum); } // content(1/2カラム + 小見出し + 画像) function createContentSlide(slide, data, layout, pageNum) { slide.getBackground().setSolidFill(CONFIG.COLORS.background_white); drawStandardTitleHeader(slide, layout, 'contentSlide', data.title); const dy = drawSubheadIfAny(slide, layout, 'contentSlide', data.subhead); // アジェンダ安全装置 const isAgenda = isAgendaTitle(data.title || ''); let points = Array.isArray(data.points) ? data.points.slice(0) : []; if (isAgenda && (!points || points.length === 0)) { points = buildAgendaFromSlideData(); if (points.length === 0) points = ['本日の目的', '進め方', '次のアクション']; } const hasImages = Array.isArray(data.images) && data.images.length > 0; const isTwo = !!(data.twoColumn || data.columns); if ((isTwo && (data.columns || points)) || (!isTwo && points && points.length > 0)) { if (isTwo) { let L = [], R = []; if (Array.isArray(data.columns) && data.columns.length === 2) { L = data.columns[0] || []; R = data.columns[1] || []; } else { const mid = Math.ceil(points.length / 2); L = points.slice(0, mid); R = points.slice(mid); } const leftRect = offsetRect(layout.getRect('contentSlide.twoColLeft'), 0, dy); const rightRect = offsetRect(layout.getRect('contentSlide.twoColRight'), 0, dy); const leftShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, leftRect.left, leftRect.top, leftRect.width, leftRect.height); const rightShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, rightRect.left, rightRect.top, rightRect.width, rightRect.height); setBulletsWithInlineStyles(leftShape, L); setBulletsWithInlineStyles(rightShape, R); } else { const bodyRect = offsetRect(layout.getRect('contentSlide.body'), 0, dy); const bodyShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, bodyRect.left, bodyRect.top, bodyRect.width, bodyRect.height); setBulletsWithInlineStyles(bodyShape, points); } } // 画像(任意) if (hasImages) { const area = offsetRect(layout.getRect('contentSlide.body'), 0, dy); renderImagesInArea(slide, layout, area, normalizeImages(data.images)); } drawBottomBarAndFooter(slide, layout, pageNum); } // compare(左右ボックス:ヘッダー青+白文字) function createCompareSlide(slide, data, layout, pageNum) { slide.getBackground().setSolidFill(CONFIG.COLORS.background_white); drawStandardTitleHeader(slide, layout, 'compareSlide', data.title); const dy = drawSubheadIfAny(slide, layout, 'compareSlide', data.subhead); const leftBox = offsetRect(layout.getRect('compareSlide.leftBox'), 0, dy); const rightBox = offsetRect(layout.getRect('compareSlide.rightBox'), 0, dy); drawCompareBox(slide, leftBox, data.leftTitle || '選択肢A', data.leftItems || []); drawCompareBox(slide, rightBox, data.rightTitle || '選択肢B', data.rightItems || []); drawBottomBarAndFooter(slide, layout, pageNum); } function drawCompareBox(slide, rect, title, items) { const box = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, rect.left, rect.top, rect.width, rect.height); box.getFill().setSolidFill(CONFIG.COLORS.lane_title_bg); box.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.lane_border); box.getBorder().setWeight(1); const th = 0.75 * 40; // 約30px相当 const titleBar = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, rect.left, rect.top, rect.width, th); titleBar.getFill().setSolidFill(CONFIG.COLORS.primary_blue); titleBar.getBorder().setTransparent(); setStyledText(titleBar, title, { size: CONFIG.FONTS.sizes.laneTitle, bold: true, color: CONFIG.COLORS.background_white, align: SlidesApp.ParagraphAlignment.CENTER }); const pad = 0.75 * 12; const textRect = { left: rect.left + pad, top: rect.top + th + pad, width: rect.width - pad * 2, height: rect.height - th - pad * 2 }; const body = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, textRect.left, textRect.top, textRect.width, textRect.height); setBulletsWithInlineStyles(body, items); } // process(角枠1px+一桁数字) function createProcessSlide(slide, data, layout, pageNum) { slide.getBackground().setSolidFill(CONFIG.COLORS.background_white); drawStandardTitleHeader(slide, layout, 'processSlide', data.title); const dy = drawSubheadIfAny(slide, layout, 'processSlide', data.subhead); const area = offsetRect(layout.getRect('processSlide.area'), 0, dy); const steps = Array.isArray(data.steps) ? data.steps : []; const n = Math.max(1, steps.length); const gapY = (area.height - layout.pxToPt(40)) / Math.max(1, n - 1); const cx = area.left + layout.pxToPt(44); const top0 = area.top + layout.pxToPt(10); const line = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, cx - layout.pxToPt(1), top0 + layout.pxToPt(6), layout.pxToPt(2), gapY * (n - 1)); line.getFill().setSolidFill(CONFIG.COLORS.faint_gray); line.getBorder().setTransparent(); for (let i = 0; i < n; i++) { const cy = top0 + gapY * i; const sz = layout.pxToPt(28); const numBox = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, cx - sz/2, cy - sz/2, sz, sz); numBox.getFill().setSolidFill(CONFIG.COLORS.background_white); numBox.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.primary_blue); numBox.getBorder().setWeight(1); const num = numBox.getText(); num.setText(String(i + 1)); applyTextStyle(num, { size: 12, bold: true, color: CONFIG.COLORS.primary_blue, align: SlidesApp.ParagraphAlignment.CENTER }); const txt = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, cx + layout.pxToPt(28), cy - layout.pxToPt(16), area.width - layout.pxToPt(70), layout.pxToPt(32)); setStyledText(txt, steps[i] || '', { size: CONFIG.FONTS.sizes.processStep }); try { txt.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e){} } drawBottomBarAndFooter(slide, layout, pageNum); } // timeline(左右余白広め) function createTimelineSlide(slide, data, layout, pageNum) { slide.getBackground().setSolidFill(CONFIG.COLORS.background_white); drawStandardTitleHeader(slide, layout, 'timelineSlide', data.title); const dy = drawSubheadIfAny(slide, layout, 'timelineSlide', data.subhead); const area = offsetRect(layout.getRect('timelineSlide.area'), 0, dy); const milestones = Array.isArray(data.milestones) ? data.milestones : []; if (milestones.length === 0) { drawBottomBarAndFooter(slide, layout, pageNum); return; } const inner = layout.pxToPt(60); const baseY = area.top + area.height * 0.55; const leftX = area.left + inner; const rightX = area.left + area.width - inner; const line = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, leftX, baseY - layout.pxToPt(1), rightX - leftX, layout.pxToPt(2)); line.getFill().setSolidFill(CONFIG.COLORS.faint_gray); line.getBorder().setTransparent(); const dotR = layout.pxToPt(8); const gap = (rightX - leftX) / Math.max(1, (milestones.length - 1)); milestones.forEach((m, i) => { const x = leftX + gap * i - dotR / 2; const dot = slide.insertShape(SlidesApp.ShapeType.ELLIPSE, x, baseY - dotR / 2, dotR, dotR); const state = (m.state || 'todo').toLowerCase(); if (state === 'done') { dot.getFill().setSolidFill(CONFIG.COLORS.google_green); dot.getBorder().setTransparent(); } else if (state === 'next') { dot.getFill().setSolidFill(CONFIG.COLORS.background_white); dot.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.google_yellow); dot.getBorder().setWeight(2); } else { dot.getFill().setSolidFill(CONFIG.COLORS.background_white); dot.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.neutral_gray); dot.getBorder().setWeight(1); } const t = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, x - layout.pxToPt(40), baseY - layout.pxToPt(40), layout.pxToPt(90), layout.pxToPt(20)); t.getText().setText(String(m.label || '')); applyTextStyle(t.getText(), { size: CONFIG.FONTS.sizes.small, bold: true, align: SlidesApp.ParagraphAlignment.CENTER }); const d = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, x - layout.pxToPt(40), baseY + layout.pxToPt(8), layout.pxToPt(90), layout.pxToPt(18)); d.getText().setText(String(m.date || '')); applyTextStyle(d.getText(), { size: CONFIG.FONTS.sizes.small, color: CONFIG.COLORS.neutral_gray, align: SlidesApp.ParagraphAlignment.CENTER }); }); drawBottomBarAndFooter(slide, layout, pageNum); } // diagram(Mermaid風・レーン+カード+自動矢印) function createDiagramSlide(slide, data, layout, pageNum) { slide.getBackground().setSolidFill(CONFIG.COLORS.background_white); drawStandardTitleHeader(slide, layout, 'diagramSlide', data.title); const dy = drawSubheadIfAny(slide, layout, 'diagramSlide', data.subhead); const lanes = Array.isArray(data.lanes) ? data.lanes : []; const area0 = layout.getRect('diagramSlide.lanesArea'); const area = offsetRect(area0, 0, dy); const px = (p)=> layout.pxToPt(p); const laneGap = px(CONFIG.DIAGRAM.laneGap_px); const lanePad = px(CONFIG.DIAGRAM.lanePad_px); const laneTitleH = px(CONFIG.DIAGRAM.laneTitle_h_px); const cardGap = px(CONFIG.DIAGRAM.cardGap_px); const cardMinH = px(CONFIG.DIAGRAM.cardMin_h_px); const cardMaxH = px(CONFIG.DIAGRAM.cardMax_h_px); const arrowH = px(CONFIG.DIAGRAM.arrow_h_px); const arrowGap = px(CONFIG.DIAGRAM.arrowGap_px); const n = Math.max(1, lanes.length); const laneW = (area.width - laneGap * (n - 1)) / n; const cardBoxes = []; for (let j = 0; j < n; j++) { const lane = lanes[j] || { title: '', items: [] }; const left = area.left + j * (laneW + laneGap); const top = area.top; const lt = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top, laneW, laneTitleH); lt.getFill().setSolidFill(CONFIG.COLORS.lane_title_bg); lt.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.lane_border); lt.getBorder().setWeight(1); lt.getText().setText(lane.title || ''); applyTextStyle(lt.getText(), { size: CONFIG.FONTS.sizes.laneTitle, bold: true, align: SlidesApp.ParagraphAlignment.CENTER }); const items = Array.isArray(lane.items) ? lane.items : []; const availH = area.height - laneTitleH - lanePad * 2; const rows = Math.max(1, items.length); const idealH = (availH - cardGap * (rows - 1)) / rows; const cardH = Math.max(cardMinH, Math.min(cardMaxH, idealH)); const totalH = cardH * rows + cardGap * (rows - 1); const firstTop = top + laneTitleH + lanePad + Math.max(0, (availH - totalH) / 2); cardBoxes[j] = []; for (let i = 0; i < rows; i++) { const cardTop = firstTop + i * (cardH + cardGap); const cardLeft = left + lanePad; const cardWidth = laneW - lanePad * 2; const card = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, cardLeft, cardTop, cardWidth, cardH); card.getFill().setSolidFill(CONFIG.COLORS.card_bg); card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border); card.getBorder().setWeight(1); setStyledText(card, items[i] || '', { size: CONFIG.FONTS.sizes.body }); try { card.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e){} cardBoxes[j][i] = { left: cardLeft, top: cardTop, width: cardWidth, height: cardH }; } } // 同行カード間を矢印で接続 const maxRows = Math.max(...cardBoxes.map(a => a.length)); for (let j = 0; j < n - 1; j++) { const L = cardBoxes[j], R = cardBoxes[j + 1]; for (let i = 0; i < maxRows; i++) { const a = L[i], b = R[i]; if (a && b) drawArrowBetweenRects(slide, a, b, arrowH, arrowGap); } } drawBottomBarAndFooter(slide, layout, pageNum); } // cards(グレー枠のみ)— title/desc 両方でインライン装飾を有効化 function createCardsSlide(slide, data, layout, pageNum) { slide.getBackground().setSolidFill(CONFIG.COLORS.background_white); drawStandardTitleHeader(slide, layout, 'cardsSlide', data.title); const dy = drawSubheadIfAny(slide, layout, 'cardsSlide', data.subhead); const area = offsetRect(layout.getRect('cardsSlide.gridArea'), 0, dy); const items = Array.isArray(data.items) ? data.items : []; const cols = Math.min(3, Math.max(2, Number(data.columns) || (items.length <= 4 ? 2 : 3))); const gap = layout.pxToPt(16); const rows = Math.ceil(items.length / cols); const cardW = (area.width - gap * (cols - 1)) / cols; const cardH = Math.max(layout.pxToPt(92), (area.height - gap * (rows - 1)) / rows); for (let idx = 0; idx < items.length; idx++) { const r = Math.floor(idx / cols), c = idx % cols; const left = area.left + c * (cardW + gap); const top = area.top + r * (cardH + gap); const card = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, left, top, cardW, cardH); card.getFill().setSolidFill(CONFIG.COLORS.card_bg); card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border); card.getBorder().setWeight(1); const obj = items[idx]; if (typeof obj === 'string') { setStyledText(card, obj, { size: CONFIG.FONTS.sizes.body }); } else { const title = String(obj.title || ''); const desc = String(obj.desc || ''); const combined = `${title}${desc ? '\n' + desc : ''}`; setStyledText(card, combined, { size: CONFIG.FONTS.sizes.body }); if (title.length > 0) { try { card.getText().getRange(0, title.length).getTextStyle().setBold(true); } catch(e){} } } try { card.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e) {} } drawBottomBarAndFooter(slide, layout, pageNum); } // table(Slidesテーブルでもインライン装飾対応。失敗時は矩形代替でも対応) function createTableSlide(slide, data, layout, pageNum) { slide.getBackground().setSolidFill(CONFIG.COLORS.background_white); drawStandardTitleHeader(slide, layout, 'tableSlide', data.title); const dy = drawSubheadIfAny(slide, layout, 'tableSlide', data.subhead); const area = offsetRect(layout.getRect('tableSlide.area'), 0, dy); const headers = Array.isArray(data.headers) ? data.headers : []; const rows = Array.isArray(data.rows) ? data.rows : []; try { if (headers.length > 0) { const table = slide.insertTable(rows.length + 1, headers.length); table.setLeft(area.left).setTop(area.top).setWidth(area.width); for (let c = 0; c < headers.length; c++) { const cell = table.getCell(0, c); setStyledText(cell, String(headers[c] || ''), { bold: true, align: SlidesApp.ParagraphAlignment.CENTER }); } for (let r = 0; r < rows.length; r++) { const row = rows[r] || []; for (let c = 0; c < headers.length; c++) { const cell = table.getCell(r + 1, c); setStyledText(cell, String(row[c] || ''), { align: SlidesApp.ParagraphAlignment.CENTER }); } } } else { throw new Error('headers is empty'); } } catch (e) { const cols = Math.max(1, headers.length || 3); const rcount = rows.length + 1; const gap = layout.pxToPt(1); const cellW = (area.width - gap * (cols - 1)) / cols; const cellH = (area.height - gap * (rcount - 1)) / rcount; const drawCell = (r, c, text, bold) => { const left = area.left + c * (cellW + gap); const top = area.top + r * (cellH + gap); const cell = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top, cellW, cellH); cell.getFill().setSolidFill(CONFIG.COLORS.background_white); cell.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card_border); cell.getBorder().setWeight(1); setStyledText(cell, String(text || ''), { bold: !!bold, align: SlidesApp.ParagraphAlignment.CENTER }); try { cell.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e){} }; (headers.length ? headers : ['項目','値1','値2']).forEach((h, c) => drawCell(0, c, h, true)); for (let r = 0; r < rows.length; r++) { const row = rows[r] || []; for (let c = 0; c < (headers.length || 3); c++) drawCell(r + 1, c, row[c], false); } } drawBottomBarAndFooter(slide, layout, pageNum); } // progress(進捗バー) function createProgressSlide(slide, data, layout, pageNum) { slide.getBackground().setSolidFill(CONFIG.COLORS.background_white); drawStandardTitleHeader(slide, layout, 'progressSlide', data.title); const dy = drawSubheadIfAny(slide, layout, 'progressSlide', data.subhead); const area = offsetRect(layout.getRect('progressSlide.area'), 0, dy); const items = Array.isArray(data.items) ? data.items : []; const n = Math.max(1, items.length); const rowH = area.height / n; for (let i = 0; i < n; i++) { const y = area.top + i * rowH + layout.pxToPt(6); const label = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, area.left, y, layout.pxToPt(150), layout.pxToPt(18)); setStyledText(label, String(items[i].label || ''), { size: CONFIG.FONTS.sizes.body }); const barLeft = area.left + layout.pxToPt(160); const barW = area.width - layout.pxToPt(210); const barBG = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, barLeft, y, barW, layout.pxToPt(14)); barBG.getFill().setSolidFill(CONFIG.COLORS.faint_gray); barBG.getBorder().setTransparent(); const p = Math.max(0, Math.min(100, Number(items[i].percent || 0))); const barFG = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, barLeft, y, barW * (p/100), layout.pxToPt(14)); barFG.getFill().setSolidFill(CONFIG.COLORS.google_green); barFG.getBorder().setTransparent(); const pct = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, barLeft + barW + layout.pxToPt(6), y - layout.pxToPt(1), layout.pxToPt(40), layout.pxToPt(16)); pct.getText().setText(String(p) + '%'); applyTextStyle(pct.getText(), { size: CONFIG.FONTS.sizes.small, color: CONFIG.COLORS.neutral_gray }); } drawBottomBarAndFooter(slide, layout, pageNum); } function createClosingSlide(slide, data, layout) { slide.getBackground().setSolidFill(CONFIG.COLORS.background_white); const image = slide.insertImage(CONFIG.LOGOS.closing); const imgW_pt = layout.pxToPt(450) * layout.scaleX; const aspect = image.getHeight() / image.getWidth(); image.setWidth(imgW_pt).setHeight(imgW_pt * aspect); image.setLeft((layout.pageW_pt - imgW_pt) / 2).setTop((layout.pageH_pt - (imgW_pt * aspect)) / 2); } // --- 7. ユーティリティ関数群 --- function createLayoutManager(pageW_pt, pageH_pt) { const pxToPt = (px) => px * 0.75; const baseW_pt = pxToPt(CONFIG.BASE_PX.W); const baseH_pt = pxToPt(CONFIG.BASE_PX.H); const scaleX = pageW_pt / baseW_pt; const scaleY = pageH_pt / baseH_pt; const getPositionFromPath = (path) => path.split('.').reduce((obj, key) => obj[key], CONFIG.POS_PX); return { scaleX, scaleY, pageW_pt, pageH_pt, pxToPt, getRect: (spec) => { const pos = typeof spec === 'string' ? getPositionFromPath(spec) : spec; let left_px = pos.left; if (pos.right !== undefined && pos.left === undefined) { left_px = CONFIG.BASE_PX.W - pos.right - pos.width; } return { left: left_px !== undefined ? pxToPt(left_px) * scaleX : undefined, top: pos.top !== undefined ? pxToPt(pos.top) * scaleY : undefined, width: pos.width !== undefined ? pxToPt(pos.width) * scaleX : undefined, height: pos.height !== undefined ? pxToPt(pos.height) * scaleY : undefined, }; } }; } function offsetRect(rect, dx, dy) { return { left: rect.left + (dx || 0), top: rect.top + (dy || 0), width: rect.width, height: rect.height }; } function drawStandardTitleHeader(slide, layout, key, title) { const logoRect = layout.getRect(`${key}.headerLogo`); const logo = slide.insertImage(CONFIG.LOGOS.header); const asp = logo.getHeight() / logo.getWidth(); logo.setLeft(logoRect.left).setTop(logoRect.top).setWidth(logoRect.width).setHeight(logoRect.width * asp); const titleRect = layout.getRect(`${key}.title`); const titleShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, titleRect.left, titleRect.top, titleRect.width, titleRect.height); setStyledText(titleShape, title || '', { size: CONFIG.FONTS.sizes.contentTitle, bold: true }); const uRect = layout.getRect(`${key}.titleUnderline`); const u = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, uRect.left, uRect.top, uRect.width, uRect.height); u.getFill().setSolidFill(CONFIG.COLORS.primary_blue); u.getBorder().setTransparent(); } function drawSubheadIfAny(slide, layout, key, subhead) { if (!subhead) return 0; const rect = layout.getRect(`${key}.subhead`); const box = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, rect.left, rect.top, rect.width, rect.height); setStyledText(box, subhead, { size: CONFIG.FONTS.sizes.subhead, color: CONFIG.COLORS.text_primary }); return layout.pxToPt(36); } function drawBottomBar(slide, layout) { const barRect = layout.getRect('bottomBar'); const bar = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, barRect.left, barRect.top, barRect.width, barRect.height); bar.getFill().setSolidFill(CONFIG.COLORS.primary_blue); bar.getBorder().setTransparent(); } function drawBottomBarAndFooter(slide, layout, pageNum) { drawBottomBar(slide, layout); addGoogleFooter(slide, layout, pageNum); } function addGoogleFooter(slide, layout, pageNum) { const leftRect = layout.getRect('footer.leftText'); const leftShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, leftRect.left, leftRect.top, leftRect.width, leftRect.height); leftShape.getText().setText(CONFIG.FOOTER_TEXT); applyTextStyle(leftShape.getText(), { size: CONFIG.FONTS.sizes.footer, color: CONFIG.COLORS.text_primary }); if (pageNum > 0) { const rightRect = layout.getRect('footer.rightPage'); const rightShape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, rightRect.left, rightRect.top, rightRect.width, rightRect.height); rightShape.getText().setText(String(pageNum)); applyTextStyle(rightShape.getText(), { size: CONFIG.FONTS.sizes.footer, color: CONFIG.COLORS.primary_blue, align: SlidesApp.ParagraphAlignment.END }); } } function applyTextStyle(textRange, opt) { const style = textRange.getTextStyle(); style.setFontFamily(CONFIG.FONTS.family); style.setForegroundColor(opt.color || CONFIG.COLORS.text_primary); style.setFontSize(opt.size || CONFIG.FONTS.sizes.body); style.setBold(opt.bold || false); if (opt.align) { try { textRange.getParagraphs().forEach(p => p.getRange().getParagraphStyle().setParagraphAlignment(opt.align)); } catch (e) {} } } function setStyledText(shapeOrCell, rawText, baseOpt) { const parsed = parseInlineStyles(rawText || ''); const tr = shapeOrCell.getText(); tr.setText(parsed.output); applyTextStyle(tr, baseOpt || {}); applyStyleRanges(tr, parsed.ranges); } function setBulletsWithInlineStyles(shape, points) { const joiner = '\n\n'; let combined = ''; const ranges = []; (points || []).forEach((pt, idx) => { const parsed = parseInlineStyles(String(pt || '')); const bullet = '• ' + parsed.output; if (idx > 0) combined += joiner; const start = combined.length; combined += bullet; parsed.ranges.forEach(r => { ranges.push({ start: start + 2 + r.start, end: start + 2 + r.end, bold: r.bold, color: r.color }); }); }); const tr = shape.getText(); tr.setText(combined || '• —'); applyTextStyle(tr, { size: CONFIG.FONTS.sizes.body }); try { tr.getParagraphs().forEach(p => { const ps = p.getRange().getParagraphStyle(); ps.setLineSpacing(100); ps.setSpaceBelow(6); }); } catch (e) {} applyStyleRanges(tr, ranges); } function parseInlineStyles(s) { const ranges = []; let out = ''; for (let i = 0; i < s.length; ) { // [[青太字]] if (s[i] === '[' && s[i+1] === '[') { const close = s.indexOf(']]', i + 2); if (close !== -1) { const content = s.substring(i + 2, close); const start = out.length; out += content; const end = out.length; ranges.push({ start, end, bold: true, color: CONFIG.COLORS.primary_blue }); i = close + 2; continue; } } // **太字** if (s[i] === '*' && s[i+1] === '*') { const close = s.indexOf('**', i + 2); if (close !== -1) { const content = s.substring(i + 2, close); const start = out.length; out += content; const end = out.length; ranges.push({ start, end, bold: true }); i = close + 2; continue; } } out += s[i]; i++; } return { output: out, ranges }; } function applyStyleRanges(textRange, ranges) { ranges.forEach(r => { try { const sub = textRange.getRange(r.start, r.end); if (!sub) return; const st = sub.getTextStyle(); if (r.bold) st.setBold(true); if (r.color) st.setForegroundColor(r.color); } catch (e) {} }); } function normalizeImages(arr) { return (arr || []).map(v => { if (typeof v === 'string') return { url: v }; if (v && typeof v.url === 'string') return { url: v.url, caption: v.caption || '' }; return null; }).filter(Boolean).slice(0, 6); } function renderImagesInArea(slide, layout, area, images) { if (!images || images.length === 0) return; const n = Math.min(6, images.length); let cols = 1, rows = 1; if (n === 1) { cols = 1; rows = 1; } else if (n === 2) { cols = 2; rows = 1; } else if (n <= 4) { cols = 2; rows = 2; } else { cols = 3; rows = 2; } const gap = layout.pxToPt(10); const cellW = (area.width - gap * (cols - 1)) / cols; const cellH = (area.height - gap * (rows - 1)) / rows; for (let i = 0; i < n; i++) { const r = Math.floor(i / cols), c = i % cols; const left = area.left + c * (cellW + gap); const top = area.top + r * (cellH + gap); try { const img = slide.insertImage(images[i].url); const scale = Math.min(cellW / img.getWidth(), cellH / img.getHeight()); const w = img.getWidth() * scale; const h = img.getHeight() * scale; img.setWidth(w).setHeight(h); img.setLeft(left + (cellW - w) / 2).setTop(top + (cellH - h) / 2); } catch(e) {} } } function isAgendaTitle(title) { const t = String(title || '').toLowerCase(); return /(agenda|アジェンダ|目次|本日お伝えすること)/.test(t); } function buildAgendaFromSlideData() { const pts = []; for (const d of slideData) { if (d && d.type === 'section' && typeof d.title === 'string' && d.title.trim()) pts.push(d.title.trim()); } if (pts.length > 0) return pts.slice(0, 5); const alt = []; for (const d of slideData) { if (d && d.type === 'content' && typeof d.title === 'string' && d.title.trim()) alt.push(d.title.trim()); } return alt.slice(0, 5); } function drawArrowBetweenRects(slide, a, b, arrowH, arrowGap) { const fromX = a.left + a.width; const toX = b.left; const width = Math.max(0, toX - fromX - arrowGap * 2); if (width < 8) return; const yMid = ((a.top + a.height/2) + (b.top + b.height/2)) / 2; const top = yMid - arrowH / 2; const left = fromX + arrowGap; const arr = slide.insertShape(SlidesApp.ShapeType.RIGHT_ARROW, left, top, width, arrowH); arr.getFill().setSolidFill(CONFIG.COLORS.primary_blue); arr.getBorder().setTransparent(); }
i

掲載にあたっての注記:上記はまじん氏のnote記事で無料公開されているプロンプトを、受講者がそのまま使えるように引用したものです。原文に対して行った変更は、記事の表示上まぎれこんでいた記号(Markdownのエスケープ記号とコードフェンス)の除去だけで、指示内容とコードには手を加えていません。研修外での再配布はお控えください。また、まじん式は改良が続いているため、最新版はnote記事でご確認ください。

⚠

入り口が今までと違います。レベル1・2はスプレッドシートから Apps Script を開きましたが、まじん式は空のGoogleスライドから開きます。作ったスライドの上に直接描いていく作りだからです。大事なスライドの上で実行しないでください。初期設定では既存のページをすべて消してから描き直します。

つまずきやすい点

症状原因対処
出力が途中で止まるコードが長く、1回の返信に収まらない「続きを出力して」と送る。それでも切れる場合は新しいチャットでやり直すのが早い
貼り付けたら赤い波線だらけコピーの範囲が足りない/途中が欠けた先頭の /** から最後の } まで、全部入っているか確認する
実行しても何も起きない実行する関数の選択が違う関数の選択欄が generatePresentation になっているか確認する
ロゴが表示されない画像URLが公開されていないWeb上で誰でも開けるURLが必要。社内サーバーやDriveの限定公開URLは使えない
文字がはみ出すAIが長い文章を入れた該当箇所の文章を短くする。または「各項目を40文字以内にして」と指示し直す

自社仕様に変える

まじん式のテンプレートは初期状態がGoogle風のデザイン(青系、Googleロゴ、ダミーのフッター)になっています。 これを自社仕様にするには、AIに頼むのがいちばん簡単です。

🎨 AIへのプロンプト ── まじん式の初期設定を自社仕様にする17行
このGASテンプレートの初期設定を、当社の仕様に変更してください。 コードのロジック(関数の中身・レイアウトの計算)には手を入れず、設定値だけを変えてください。 【変更したい内容】 ・プライマリカラー:#1f3864 ・フッターの文言:株式会社◯◯◯◯ ・ヘッダー(各ページ右上)のロゴ画像URL:https://example.com/logo.png ・最終ページのロゴ画像URL:ヘッダーと同じでよい ・フォント:(変えたい場合だけ記入。空欄なら現状のまま) 【出力の仕方】 ・変更後のコード全文を出してください。前置き・解説・後書きは不要です ・コードのあとに、変更した箇所だけを箇条書きで教えてください ・ロゴ画像は、Web上で誰でも開けるURLであることを前提にしてください 【下に貼り付けるコード】 (ここにテンプレートの全文を貼り付ける)
★

変えてよいのは「設定」だけです。テンプレートの中で自由に変えてよいのは、色・フォント・ロゴ・フッター文言といった設定値の部分です。描画の処理そのもの(関数の中身)に手を入れると、たいてい壊れます。「もっと大きく変えたい」と思ったときは、次の章で扱う短いテンプレートのほうを改造してください。中身が読める分量なので、安全に手を入れられます。

9自作テンプレートで体験する ── ミニ版の全文

原典のまじん式は700行を超える大作です。実務ではそのまま使えばよいのですが、 中身を読んで仕組みを理解するには大きすぎます。 そこで、まったく同じ設計思想で、この研修用に短く書き直したテンプレートを用意しました。

扱える型は8種類(表紙・章扉・箇条書き・比較・手順・カード・表・結び)。 スピーカーノートの自動設定にも対応しています。 こちらは全部読める分量なので、自分で型を追加するところまで踏み込めます。

i

貼り付ける場所に注意してください。このコードは空のGoogleスライドの Apps Script に貼ります(レベル1のスプレッドシートとは別のプロジェクトです)。同じプロジェクトに両方貼ると、関数名が重なってエラーになります。

コード.gs ── テンプレート方式(全文・これ1つで動く)全388行
/**
 * 第6回 ── テンプレート方式(コード固定 × データ可変)
 *
 * 使い方:
 *   1. 空のGoogleスライドを新規作成する
 *   2. [拡張機能]→[Apps Script]を開く
 *   3. コード.gs の中身を消して、この全文を貼り付けて保存
 *   4. generatePresentation を実行する
 *
 *   以降、変えるのは「2. スライドデータ」の const slideData だけです。
 *   ここを生成AIに書かせれば、中身の違うスライドが何度でも作れます。
 *
 * ※ レベル1のコードとは別のプロジェクトに貼ってください(関数名が重なります)
 */

// ===== 1. デザイン規定(会社のトンマナに合わせて、ここだけ書き換える) =====
const CONFIG = {
  font: 'Arial',
  colors: {
    main: '#0d1b4c',      // メインカラー(濃紺)
    accent: '#2563eb',    // アクセント(青)
    text: '#1a1a2e',      // 本文
    muted: '#6b7280',     // 補足
    line: '#c8ccd4',      // 罫線・カード枠
    soft: '#f5f6f8',      // 薄いグレーの塗り
    white: '#ffffff'
  },
  sizes: {
    coverTitle: 30, coverDate: 12,
    sectionNo: 64, sectionTitle: 28,
    head: 22, subhead: 12, body: 13, small: 11, footer: 8
  },
  page: { w: 720, h: 405 },
  pos: {
    marginX: 42,
    headTop: 34, ruleTop: 74, subheadTop: 84,
    bodyTop: 118, bodyH: 240,
    footerTop: 378
  },
  footerText: '株式会社 日本能率協会総合研究所',
  clearBeforeGenerate: true   // 実行のたびに既存のスライドを消してから作り直す
};

// ===== 2. スライドデータ(生成AIに書かせるのは、ここだけ) =====
const slideData = [
  {
    type: 'title',
    title: '調査レポート作成の自動化',
    date: '2026.09.01',
    notes: '本日は、調査レポートの作成工程を自動化した取り組みについてご報告します。'
  },
  {
    type: 'section',
    title: '取り組みの背景',
    notes: 'まず、なぜこの取り組みを始めたのかをご説明します。'
  },
  {
    type: 'content',
    title: '現在の課題',
    subhead: '毎月40時間が転記作業に消えている',
    points: [
      '集計結果をスライドへ転記する作業に、担当者1名で月40時間',
      '転記ミスが見つかるのが発表の直前になりやすい',
      '担当者ごとに書式が違い、レビューに時間がかかる'
    ],
    notes: '課題は3つあります。特に1つ目の40時間という数字が、今回の出発点になりました。'
  },
  {
    type: 'compare',
    title: '導入前後の比較',
    subhead: '同じ作業を、同じ品質で、10分の1の時間で終える',
    leftTitle: '導入前',
    rightTitle: '導入後',
    leftItems: ['転記に月40時間', '書式が担当者ごとに違う', '差し戻しが平均3回'],
    rightItems: ['転記は月3時間', '全社共通の書式', '差し戻しは0〜1回'],
    notes: '数字の比較でご覧いただくと、削減幅がはっきりします。'
  },
  {
    type: 'process',
    title: '自動化の流れ',
    subhead: '人が手を動かすのは、集計シートの更新だけ',
    steps: ['集計シートを更新', 'スクリプトを実行', 'スライドが生成', '中身だけ確認'],
    notes: '4つの工程のうち、人が担当するのは1つ目と4つ目だけです。'
  },
  {
    type: 'cards',
    title: '得られた効果',
    columns: 3,
    items: [
      { title: '時間', desc: '月40時間 → 月3時間' },
      { title: '品質', desc: '転記ミスがゼロに' },
      { title: '横展開', desc: '3部署へ展開済み' }
    ],
    notes: '効果は3つの観点で整理しました。'
  },
  {
    type: 'table',
    title: '部署別の削減時間',
    headers: ['部署', '導入前', '導入後', '削減'],
    rows: [
      ['調査1部', '40時間', '3時間', '37時間'],
      ['調査2部', '28時間', '4時間', '24時間'],
      ['企画部', '15時間', '2時間', '13時間']
    ],
    notes: '部署ごとの内訳です。合計で月74時間の削減になりました。'
  },
  {
    type: 'closing',
    message: 'ご清聴ありがとうございました',
    notes: '以上でご報告を終わります。ご質問をお受けします。'
  }
];

// ===== 3. 実行(触らない) =====
let pageNo = 0;
let sectionNo = 0;

function generatePresentation() {
  const pres = SlidesApp.getActivePresentation();

  if (CONFIG.clearBeforeGenerate) {
    const slides = pres.getSlides();
    for (let i = slides.length - 1; i >= 0; i--) {
      slides[i].remove();
    }
  }
  pageNo = 0;
  sectionNo = 0;

  slideData.forEach(function (data, index) {
    const build = builders[data.type];
    if (!build) {
      Logger.log((index + 1) + '枚目:未対応の type です → ' + data.type);
      return;
    }
    const slide = pres.appendSlide(SlidesApp.PredefinedLayout.BLANK);
    build(slide, data);
    if (data.notes) {
      setNotes_(slide, data.notes);
    }
  });

  Logger.log('スライドを ' + pres.getSlides().length + ' 枚作成しました。');
}

// type と描画関数の対応表。ここに1行足せば、自分のパターンを追加できる
const builders = {
  title: buildTitle_,
  section: buildSection_,
  content: buildContent_,
  compare: buildCompare_,
  process: buildProcess_,
  cards: buildCards_,
  table: buildTable_,
  closing: buildClosing_
};

// ===== 4. パターンごとの描画(触らない) =====

function buildTitle_(slide, d) {
  bg_(slide, CONFIG.colors.white);
  bar_(slide, CONFIG.pos.marginX, 126, 60, 4, CONFIG.colors.accent);
  text_(slide, d.title, CONFIG.pos.marginX, 150, 620, 60,
    { size: CONFIG.sizes.coverTitle, bold: true, color: CONFIG.colors.main });
  text_(slide, (d.date || '') + ' / ' + CONFIG.footerText, CONFIG.pos.marginX, 232, 560, 24,
    { size: CONFIG.sizes.coverDate, color: CONFIG.colors.muted });
}

function buildSection_(slide, d) {
  sectionNo++;
  bg_(slide, CONFIG.colors.main);
  text_(slide, ('0' + (d.sectionNo || sectionNo)).slice(-2), CONFIG.pos.marginX, 118, 220, 100,
    { size: CONFIG.sizes.sectionNo, bold: true, color: '#3d4d7d' });
  text_(slide, d.title, CONFIG.pos.marginX, 214, 600, 50,
    { size: CONFIG.sizes.sectionTitle, bold: true, color: CONFIG.colors.white });
}

function buildContent_(slide, d) {
  head_(slide, d);
  const points = d.points || [];
  for (let i = 0; i < points.length; i++) {
    const top = CONFIG.pos.bodyTop + i * 34;
    bar_(slide, CONFIG.pos.marginX, top + 9, 6, 6, CONFIG.colors.accent);
    text_(slide, points[i], CONFIG.pos.marginX + 18, top, 600, 28,
      { size: CONFIG.sizes.body, color: CONFIG.colors.text });
  }
  footer_(slide);
}

function buildCompare_(slide, d) {
  head_(slide, d);
  const w = 300;
  const gap = 36;
  const h = 200;
  panel_(slide, d.leftTitle || '現状', d.leftItems || [],
    CONFIG.pos.marginX, CONFIG.pos.bodyTop, w, h, CONFIG.colors.muted);
  panel_(slide, d.rightTitle || '改善後', d.rightItems || [],
    CONFIG.pos.marginX + w + gap, CONFIG.pos.bodyTop, w, h, CONFIG.colors.accent);
  footer_(slide);
}

function buildProcess_(slide, d) {
  head_(slide, d);
  const steps = d.steps || [];
  const n = Math.max(steps.length, 1);
  const areaW = CONFIG.page.w - CONFIG.pos.marginX * 2;
  const gap = 22;
  const boxW = (areaW - gap * (n - 1)) / n;
  const top = CONFIG.pos.bodyTop + 30;

  for (let i = 0; i < steps.length; i++) {
    const x = CONFIG.pos.marginX + i * (boxW + gap);
    const box = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, x, top, boxW, 96);
    box.getFill().setSolidFill(CONFIG.colors.soft);
    box.getBorder().getLineFill().setSolidFill(CONFIG.colors.line);

    text_(slide, 'STEP ' + (i + 1), x + 12, top + 12, boxW - 24, 18,
      { size: 9, bold: true, color: CONFIG.colors.accent });
    text_(slide, steps[i], x + 12, top + 34, boxW - 24, 50,
      { size: CONFIG.sizes.body, color: CONFIG.colors.text });

    if (i < steps.length - 1) {
      const arrow = slide.insertShape(SlidesApp.ShapeType.RIGHT_ARROW,
        x + boxW + 4, top + 42, gap - 8, 12);
      arrow.getFill().setSolidFill(CONFIG.colors.line);
      arrow.getBorder().setTransparent();
    }
  }
  footer_(slide);
}

function buildCards_(slide, d) {
  head_(slide, d);
  const items = d.items || [];
  const cols = d.columns || 3;
  const rows = Math.ceil(items.length / cols);
  const areaW = CONFIG.page.w - CONFIG.pos.marginX * 2;
  const gap = 16;
  const cardW = (areaW - gap * (cols - 1)) / cols;
  const cardH = Math.min(110, (CONFIG.pos.bodyH - gap * (rows - 1)) / rows);

  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    const r = Math.floor(i / cols);
    const c = i % cols;
    const x = CONFIG.pos.marginX + c * (cardW + gap);
    const y = CONFIG.pos.bodyTop + r * (cardH + gap);

    const card = slide.insertShape(SlidesApp.ShapeType.ROUND_RECTANGLE, x, y, cardW, cardH);
    card.getFill().setSolidFill(CONFIG.colors.white);
    card.getBorder().getLineFill().setSolidFill(CONFIG.colors.line);

    const title = (typeof item === 'string') ? item : item.title;
    const desc = (typeof item === 'string') ? '' : (item.desc || '');

    text_(slide, title, x + 14, y + 14, cardW - 28, 22,
      { size: CONFIG.sizes.small, bold: true, color: CONFIG.colors.main });
    if (desc) {
      text_(slide, desc, x + 14, y + 42, cardW - 28, cardH - 56,
        { size: CONFIG.sizes.body, color: CONFIG.colors.text });
    }
  }
  footer_(slide);
}

function buildTable_(slide, d) {
  head_(slide, d);
  const headers = d.headers || [];
  const rows = d.rows || [];
  if (headers.length === 0) {
    footer_(slide);
    return;
  }

  const table = slide.insertTable(
    rows.length + 1, headers.length,
    CONFIG.pos.marginX, CONFIG.pos.bodyTop,
    CONFIG.page.w - CONFIG.pos.marginX * 2,
    Math.min(CONFIG.pos.bodyH, 30 * (rows.length + 1))
  );

  for (let c = 0; c < headers.length; c++) {
    const cell = table.getCell(0, c);
    cell.getFill().setSolidFill(CONFIG.colors.main);
    const t = cell.getText();
    t.setText(String(headers[c]));
    t.getTextStyle()
      .setFontFamily(CONFIG.font)
      .setFontSize(CONFIG.sizes.small)
      .setBold(true)
      .setForegroundColor(CONFIG.colors.white);
  }

  for (let r = 0; r < rows.length; r++) {
    for (let c = 0; c < headers.length; c++) {
      const cell = table.getCell(r + 1, c);
      const t = cell.getText();
      t.setText(String(rows[r][c] === undefined ? '' : rows[r][c]));
      t.getTextStyle()
        .setFontFamily(CONFIG.font)
        .setFontSize(CONFIG.sizes.small)
        .setBold(false)
        .setForegroundColor(CONFIG.colors.text);
    }
  }
  footer_(slide);
}

function buildClosing_(slide, d) {
  bg_(slide, CONFIG.colors.main);
  text_(slide, d.message || 'ご清聴ありがとうございました', 0, 178, CONFIG.page.w, 40,
    { size: 24, bold: true, color: CONFIG.colors.white, align: SlidesApp.ParagraphAlignment.CENTER });
}

// ===== 5. 共通部品(触らない) =====

function bg_(slide, color) {
  slide.getBackground().setSolidFill(color);
}

/** 見出し+アクセント罫線+小見出し。中面はすべてこれで始まる */
function head_(slide, d) {
  bg_(slide, CONFIG.colors.white);
  text_(slide, d.title, CONFIG.pos.marginX, CONFIG.pos.headTop, 620, 32,
    { size: CONFIG.sizes.head, bold: true, color: CONFIG.colors.main });
  bar_(slide, CONFIG.pos.marginX, CONFIG.pos.ruleTop, 130, 3, CONFIG.colors.accent);
  if (d.subhead) {
    text_(slide, d.subhead, CONFIG.pos.marginX, CONFIG.pos.subheadTop, 620, 22,
      { size: CONFIG.sizes.subhead, color: CONFIG.colors.muted });
  }
}

/** 比較スライドの左右パネル */
function panel_(slide, title, items, x, y, w, h, headColor) {
  const box = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, x, y, w, h);
  box.getFill().setSolidFill(CONFIG.colors.white);
  box.getBorder().getLineFill().setSolidFill(CONFIG.colors.line);

  bar_(slide, x, y, w, 28, headColor);
  text_(slide, title, x + 12, y + 4, w - 24, 20,
    { size: CONFIG.sizes.small, bold: true, color: CONFIG.colors.white });

  for (let i = 0; i < items.length; i++) {
    text_(slide, '・' + items[i], x + 12, y + 42 + i * 30, w - 24, 26,
      { size: CONFIG.sizes.body, color: CONFIG.colors.text });
  }
}

function text_(slide, str, x, y, w, h, options) {
  const opt = options || {};
  const box = slide.insertTextBox(String(str), x, y, w, h);
  const style = box.getText().getTextStyle();
  style.setFontFamily(CONFIG.font);
  style.setFontSize(opt.size || CONFIG.sizes.body);
  style.setBold(opt.bold === true);
  style.setForegroundColor(opt.color || CONFIG.colors.text);
  if (opt.align) {
    box.getText().getParagraphStyle().setParagraphAlignment(opt.align);
  }
  return box;
}

function bar_(slide, x, y, w, h, color) {
  const shape = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, x, y, w, h);
  shape.getFill().setSolidFill(color);
  shape.getBorder().setTransparent();
  return shape;
}

function footer_(slide) {
  pageNo++;
  bar_(slide, 0, CONFIG.pos.footerTop - 10, CONFIG.page.w, 1, CONFIG.colors.line);
  text_(slide, CONFIG.footerText, CONFIG.pos.marginX, CONFIG.pos.footerTop, 400, 16,
    { size: CONFIG.sizes.footer, color: CONFIG.colors.muted });
  text_(slide, String(pageNo), CONFIG.page.w - CONFIG.pos.marginX - 40, CONFIG.pos.footerTop, 40, 16,
    { size: CONFIG.sizes.footer, color: CONFIG.colors.muted, align: SlidesApp.ParagraphAlignment.END });
}

function setNotes_(slide, notes) {
  try {
    const shape = slide.getNotesPage().getSpeakerNotesShape();
    if (shape) {
      shape.getText().setText(notes);
    }
  } catch (e) {
    Logger.log('スピーカーノートを設定できませんでした:' + e.message);
  }
}

generatePresentation を実行すると、8枚のスライドが出来上がります。 表紙・章扉・箇条書き・比較・手順・カード・表・結びが1枚ずつ入っているので、 「どの型がどう見えるか」の見本帳としても使えます。

構造は、レベル1とまったく同じ

ブロック中身誰が触るか
1. デザイン規定(CONFIG)色・フォント・文字サイズ・座標最初に1回だけ自分で決める
2. スライドデータ(slideData)資料の中身そのもの毎回ここだけを生成AIが書く
3. 実行(generatePresentation)データを1件ずつ読んで、型に応じた描画を呼ぶ触らない
4. 型ごとの描画(build◯◯_)8種類それぞれの描き方型を増やすときだけ
5. 共通部品テキスト・図形・フッター・ノート触らない

仕掛けの中心は builders という対応表です。 { type: 'compare', ... } というデータが来たら buildCompare_ を呼ぶ、 という紐づけの一覧がここに書いてあります。 データの中の type という文字が、そのまま「どの描き方を使うか」の指示になっているわけです。

✓

この形にしておくと、型を1つ増やすのが簡単になります。やることは2つだけ ── ①描画する関数を1つ書く、②builders に1行足す。既存の型のコードには一切触りません。機能を足すのに、動いているものを壊さなくて済む。これはプログラムの設計として、とても大事な性質です。

10slideData の書き方 ── AIへの生成計画書

テンプレートが手元にあれば、あとは slideData を差し替えるだけで別の資料になります。 そして、この slideData を書くのが生成AIの仕事です。

使える8つの型

type何を表すか必要な項目
title表紙title / date
section章扉(章番号は自動)title
content箇条書き(最も基本)title / subhead / points
compare左右の比較leftTitle / rightTitle / leftItems / rightItems
process手順・工程(左から右へ)steps
cards並列に並ぶ要素columns / items
table数値の比較・内訳headers / rows
closing結びmessage

どの型にも notes を付けられます。ここに書いた文章は、 スライドのスピーカーノート(発表者用メモ)に自動で入ります。 発表原稿の下書きまでAIに書かせられる、ということです。

AIに渡すプロンプト

次のプロンプトを ChatGPT に貼り、いちばん下の【素材】に議事録やレポートを貼り付けてください。 返ってくるのは slideData の部分だけです。

🤖 ChatGPT へのプロンプト ── slideData を作らせる70行
あなたは、プレゼンテーションの構成設計を専門とするAIです。 私が渡す素材を読み、Googleスライドを生成するための「スライドデータ」だけを作ってください。 【重要:あなたが作るのはデータだけです】 スライドを描画するプログラムは、すでに完成していて私の手元にあります。 あなたはそのプログラムに読み込ませる const slideData = [ ... ] の中身だけを作ります。 プログラム本体のコードは書かないでください。関数名・変数名・デザインの数値も変えないでください。 【出力形式】 const slideData = [ ... ]; という JavaScript の配列を、コードブロックで1つだけ出力してください。 前置き・解説・後書きは一切書かないこと。 【使えるスライドの型(この8種類以外は使わない)】 1. 表紙 { type: 'title', title: '...', date: 'YYYY.MM.DD', notes: '...' } 2. 章扉(章の区切り。章番号は自動で振られる) { type: 'section', title: '...', notes: '...' } 3. 箇条書き(最も基本の型。3〜5項目) { type: 'content', title: '...', subhead: '...', points: ['...', '...'], notes: '...' } 4. 左右の比較(現状と改善後、A案とB案など) { type: 'compare', title: '...', subhead: '...', leftTitle: '...', rightTitle: '...', leftItems: ['...'], rightItems: ['...'], notes: '...' } 5. 手順・工程(左から右へ流れる。3〜5ステップ) { type: 'process', title: '...', subhead: '...', steps: ['...', '...'], notes: '...' } 6. カード(並列に並ぶ要素。2列または3列) { type: 'cards', title: '...', columns: 3, items: [{ title: '...', desc: '...' }], notes: '...' } 7. 表(数値の比較・内訳) { type: 'table', title: '...', headers: ['...'], rows: [['...'], ['...']], notes: '...' } 8. 結び { type: 'closing', message: 'ご清聴ありがとうございました', notes: '...' } 【構成のルール】 ・全体は「表紙 → 章扉 → 本文2〜5枚 →(章の数だけ繰り返し)→ 結び」の順に並べる ・素材の内容に合わせて型を選ぶ。すべてを content で済ませないこと   比較の話が出てきたら compare、順序の話なら process、数値の内訳なら table を使う ・全体で 10〜16 枚に収める 【文章のルール】 ・title は全角40文字以内、subhead は全角50文字以内 ・points / leftItems / rightItems / steps の各項目は全角45文字以内。改行は入れない ・箇条書きの文末に句点「。」を付けない(体言止めを基本とする) ・「→」「■」などの記号を文章に入れない(矢印や装飾はプログラム側が描く) ・notes には、その1枚で話す内容の下書きを2〜4文で書く。強調記号は使わない ・素材に無い数字を作らない。不明な箇所は「(要確認)」と書く 【文字列の注意】 ・シングルクォート(')を文章に含める場合は必ず \' とエスケープする ・数値も文字列として書く(例: '40時間') 【自己チェック(出力の前に必ず確認すること)】  □ 8種類以外の type を使っていないか  □ 各項目の文字数が上限を超えていないか  □ 箇条書きの文末に「。」が残っていないか  □ すべてのスライドに notes を書いたか  □ JavaScript の配列として構文が正しいか(カンマ・括弧の閉じ忘れ) それでは、次の素材からスライドデータを作ってください。 【素材】 (ここに議事録・レポート・企画書などを貼り付ける)

差し替えの手順

1
出力をコピー

AIが出した const slideData = [ ... ]; を全文コピーする

2
置き換える範囲を選ぶ

スクリプトエディタで const slideData = [ の行から、対応する ]; の行までを選択

3
貼り付けて保存

選択した範囲に貼り付ける。CONFIG や関数は絶対に消さない

4
実行する

generatePresentation を実行。新しい中身で描き直される

⚠

いちばん多い失敗は「貼りすぎ」です。置き換えるのは // ===== 2. スライドデータ ===== から // ===== 3. 実行 ===== の手前までだけ。誤って CONFIG や描画関数まで消してしまうと動かなくなります。作業前にコード全文をどこかにコピーしておくと安心です。

自分の型を追加する

「うちの資料には、必ずKPIの数字を3つ大きく並べるページがある」といった場合は、 型そのものを足してしまうのが早いです。やることは2つだけです。

コード.gs ── 型を1つ追加する(例:大きな数字を3つ並べる kpi 型)全44行
// ① 描画する関数を1つ書く(他の build◯◯_ の並びに追加する)
function buildKpi_(slide, d) {
  head_(slide, d);

  const items = d.items || [];
  const n = Math.max(items.length, 1);
  const areaW = CONFIG.page.w - CONFIG.pos.marginX * 2;
  const gap = 20;
  const boxW = (areaW - gap * (n - 1)) / n;

  for (let i = 0; i < items.length; i++) {
    const x = CONFIG.pos.marginX + i * (boxW + gap);

    text_(slide, items[i].value, x, CONFIG.pos.bodyTop + 24, boxW, 62,
      { size: 40, bold: true, color: CONFIG.colors.accent,
        align: SlidesApp.ParagraphAlignment.CENTER });

    text_(slide, items[i].label, x, CONFIG.pos.bodyTop + 96, boxW, 24,
      { size: CONFIG.sizes.body, color: CONFIG.colors.muted,
        align: SlidesApp.ParagraphAlignment.CENTER });
  }
  footer_(slide);
}

// ② 対応表に1行足す(既存の行はそのまま)
const builders = {
  title: buildTitle_,
  section: buildSection_,
  content: buildContent_,
  compare: buildCompare_,
  process: buildProcess_,
  cards: buildCards_,
  table: buildTable_,
  kpi: buildKpi_,          // ← この1行を追加するだけ
  closing: buildClosing_
};

// ③ これで、slideData に次の型が書けるようになる
//    { type: 'kpi', title: '今期の実績', subhead: '3つの数字で見る',
//      items: [
//        { value: '74h', label: '月あたりの削減時間' },
//        { value: '0件', label: '転記ミスの発生' },
//        { value: '3部署', label: '展開済み' }
//      ] }

これで { type: 'kpi', title: '...', items: [...] } というデータが使えるようになります。 あとは、第10章のプロンプトの「使えるスライドの型」に kpi を書き足せば、AIもその型を選んでくれるようになります。

11GAS系3つの使い分け

ここまでで、GASを使うやり方を3つ(レベル1〜2、レベル3、レベル4)扱いました。整理しておきます。

 レベル1・2
自分で書く/シート連携
レベル3
AIにGASを設計させる
レベル4
テンプレート方式
デザイン自分で決めて固定毎回AIと相談最初に決めて固定
中身コードかシートAIと相談毎回AIが書く
準備の手間中(コードを書く)小(プロンプトを送るだけ)中(テンプレートを1回作る)
2回目以降の手間ほぼゼロ毎回やり直し小(素材を渡すだけ)
仕上がりの安定度完全に一定毎回ちがうほぼ一定
向いている資料月次報告・定例会議資料一度きりの提案・新しい形式議事録要約・調査報告・研修資料
向かない場面毎回構成が変わる資料同じものを繰り返し作る1枚だけ作りたいとき
★

迷ったら「同じ形式で何回作るか」で決めてください。年に12回作るならレベル2(シートを更新するだけにする)。形式は同じだが中身が毎回大きく変わるならレベル4。一度きりならレベル3で十分です。一度きりの資料のために仕組みを作るのは、いちばんよくある失敗です。

Part 3 / レベル5

ChatGPTに、PowerPointを直接つくらせる

ここまではGoogleスライドの話でした。しかし提出先がPowerPointのことも多いはずです。 ここからは、ChatGPTに .pptx ファイルそのものを作らせる方法を扱います。 さらにテンプレートファイルを渡して体裁を引き継がせる方法、 プロジェクト機能で自社のデザインルールを常設する方法まで進みます。

12【レベル5】ChatGPTでPowerPointを直接つくる

ChatGPTは、ファイルを作って渡してくれます。Excel も Word も PowerPoint も、です。 しかもここで起きていることは、「文章を返す」のとは根本的に違います。

ChatGPTは、頭の中で何度も考え直している

「PowerPointを作って」と頼んだとき、ChatGPTの中では1回の思考で終わっていません。 構成を考える → コードを書く → 実行する → 出来上がりを自分で確認する → 直すという 複数回のやり取りを、内部で自動的に繰り返しています。これをマルチターン推論と呼びます。

あなたの依頼 目的・聞き手・素材 ChatGPT の中で起きていること(マルチターン推論) ① 依頼を理解する 足りない条件を洗い出す ② 構成を組み立てる 何枚・どの順・何を載せるか ③ コードを書く python-pptx で組み立てる ④ 実行する その場でファイルを生成 ⑤ 自分で見て確かめる 画像に変換して崩れを探す ⑥ 直す はみ出し・重なりを修正 問題が無くなったら、完成したファイルだけをあなたに渡す 途中のコードやエラーは、見せない設定にもできる ③へ戻る 問題なし .pptx ファイル ダウンロードして使う ここが重要 ── AIは「書いて終わり」ではなく、自分の出力を見て直している だから私たちは「何を作るか」ではなく「どうなっていたら合格か」を伝えればよい
図4:ChatGPTが .pptx を作るときの内部の流れ。⑤の自己確認をきちんとやらせるかどうかで、仕上がりが大きく変わる
i

だから指示の書き方が変わります。AIが自分で確認して直せるということは、私たちが細かい作り方を指示する必要は無いということです。代わりに伝えるべきなのは「どうなっていたら合格か」── 文字がはみ出していないこと、位置が揃っていること、色がルールどおりであること。作り方ではなく、合格条件を渡す。これが今のAIへの頼み方です。

実際に頼んでみる

🤖 ChatGPT へのプロンプト ── PowerPointを直接つくらせる33行
PowerPoint(.pptx)ファイルを作ってください。ファイルとして出力し、ダウンロードできる状態にしてください。 【作りたい資料】 ・目的:(例)四半期の調査結果を、経営会議で報告する ・聞き手:(例)役員5名。調査の詳細な手法には関心がなく、結論と打ち手を知りたい ・持ち時間:(例)10分 ・枚数:(例)表紙・結びを含めて10枚程度 ・いちばん伝えたい結論:(例)主力商品の満足度は横ばいだが、20代の離反が始まっている 【素材】 (ここに集計結果・議事録・メモを貼り付ける。数字は正確に) 【進め方】 1. いきなりファイルを作らないでください。まず、上の情報で足りない点を私に質問してください。 2. 次に、スライド1枚ごとの構成案(番号/タイトル/伝えたいこと1文/使う図表)を表で提示し、 私の承認を得てください。 3. 承認後に、python-pptx でファイルを生成してください。 【体裁の条件】 ・スライドサイズは 16:9(10インチ × 5.625インチ) ・和文フォントは游ゴシック(無い場合はメイリオ)。英数字は Arial ・背景は白。使う色はメインカラー・アクセントカラー・グレーの3系統まで ・本文は9〜11pt。見出しは12〜14pt。読みやすさを理由に文字を大きくしすぎないこと ・すべてのスライドで、見出しの位置・本文の左端・フッターの位置を揃える ・全ページにフッター(会社名とページ番号)を入れる ・1スライドで伝えるメッセージは1つ。要素が多いときはスライドを分ける ・本文の文末に句点「。」は付けない(体言止めを基本とする) ・素材に無い数字を作らない。不明な箇所は「(要確認)」と明記する 【最後に必ず行うこと】 生成したファイルを画像に変換して、自分で目視で確認してください。 文字がはみ出している、要素が重なっている、位置が揃っていない箇所があれば、 コードを直して作り直したうえで、最終版だけを私に渡してください。
⚠

フォントの扱いだけ注意してください。ChatGPTが動いている環境には、日本語フォントが十分に入っていないことがあります。生成されたファイルをPowerPointで開けば游ゴシックで表示されますが、AIが自己確認のために作る画像では、日本語が四角(□□□)になることがあります。その状態でAIが「文字化けしている」と誤解して直そうとしたら、「日本語フォントが無い環境なので、□□□は無視してよい。位置とサイズだけ確認して」と伝えてください。

13テンプレートファイルを添付して体裁を引き継ぐ

前章のやり方には弱点があります。会社の書式にならないことです。 いくら「白背景で、青をアクセントに」と伝えても、いつも使っている資料と並べると、明らかに別物になります。

いちばん確実な解決策は、いつも使っているファイルそのものを渡すことです。 ChatGPTは.pptx ファイルを読み込んで、中の色・フォント・座標を読み取れます。

1
手本を1つ選ぶ

過去に作った資料のうちいちばん体裁が整っているものを選ぶ

2
中身を消す

社外秘の数字・個人名を消し、体裁だけの状態にして保存する

3
添付して読ませる

ChatGPTに添付し、読み取った内容を報告させて認識を合わせる

4
その体裁で作らせる

新しい素材を渡し、読み取った体裁のまま資料を作らせる

🤖 ChatGPT へのプロンプト ── テンプレートを添付して同じ体裁で作らせる32行
添付したテンプレートファイル(.pptx)と同じ体裁で、新しい資料を作ってください。 【最初にやってほしいこと】 添付ファイルを開いて、次の項目を読み取り、一覧にして私に報告してください。  ・スライドのサイズ  ・使われている色(16進数のカラーコードで。背景・見出し・本文・アクセント)  ・フォント名と、要素ごとの文字サイズ  ・タイトル・本文・フッターの座標と余白  ・スライドの型が何種類あるか(表紙/目次/本文/比較/結び など) 読み取った内容が私の認識と合っているかを確認してから、次に進んでください。 【作りたい資料】 ・目的: ・聞き手: ・枚数: ・いちばん伝えたい結論を1文で: 【素材】 (ここに元になる文章・数字を貼り付ける) 【守ってほしいこと】 ・テンプレートから読み取った色・フォント・文字サイズ・座標を、そのまま使うこと。  私が明示的に指示しない限り、独自の色やフォントを足さないこと ・テンプレートに無いレイアウトが必要になった場合は、勝手に作らず、  「テンプレートのどの型を応用するか」を提案して私に確認すること ・テンプレートのスライドをコピーして中身を差し替える方式で構いません。  その場合、使わなかったページは必ず削除してください 【最後に必ず行うこと】 完成したファイルを画像に変換して目視で確認し、 テンプレートと並べて体裁がずれていないかを自分でチェックしてから渡してください。
⚠

手本にするファイルから、社外秘の情報を必ず消してください。実績の数字、取引先名、個人名、単価 ── これらが残ったまま添付すると、その情報ごとAIに渡すことになります。体裁を伝えるのに中身は要りません。表紙・見出し・本文・表・グラフが1枚ずつ入った「見本帳」を1つ作っておくと、以後ずっと使い回せます。

★

「読み取った内容を報告させる」工程を飛ばさないでください。AIが読み違えていることは普通にあります(メインカラーを別の色だと思っている、など)。先に報告させて確認しておけば、資料が全部できてから作り直す事態を防げます。手戻りを防ぐいちばん安いコストです。

14プロジェクト機能で自社のルールを常設する

前章までのやり方には、まだ面倒が残っています。毎回おなじ説明をしなければならないことです。 新しいチャットを開くたびに、色を伝え、フォントを伝え、テンプレートを添付し直す。 3回目くらいで嫌になります。

ChatGPTのプロジェクト機能は、これを解決します。 指示とファイルをプロジェクトに置いておくと、その中で始めた会話すべてに自動で適用されます。

プロジェクト「社内資料づくり」 一度入れておくもの 📝 プロジェクトの指示 進め方・守ること 🎨 デザイン定義 色・文字・余白 📐 レイアウトパターン集 型の選び方 🐍 生成スクリプトの雛形 python-pptx 💬 10月の月次報告を作って → 自社の色・書式で出てくる 💬 A社への提案資料を作って → 同じ色・同じ書式で出てくる 💬 勉強会の資料を作って → やはり同じ色・同じ書式 説明は一度きり。あとは全部の会話に効く 普通のチャットの場合 毎回、色とフォントを説明し直す 説明を忘れると、別物が出てくる プロジェクトの場合 「10月の報告を作って」の一言で足りる 誰が使っても同じ体裁になる
図5:プロジェクト機能は「AIに自社の作法を覚えさせておく箱」。中で始めた会話には、置いてある指示とファイルが自動で効く

何を入れるか

ここが今回のいちばん実務的な話です。入れるものは4つあります。 この4つを揃えると、AIは「自社の資料の作り方を知っている状態」になります。

入れるもの置き場所役割
プロジェクトの指示プロジェクトの「指示」欄進め方の手順を決める。いきなり作らせず、要件確認 → 構成案 → 承認 → 生成 → 自己チェック、の順に進ませる
デザイン定義ファイルとしてアップロード色・フォント・文字サイズ・余白・図解のルールを、数値で書いたもの
レイアウトパターン集ファイルとしてアップロード「比較の話ならこの型」「手順ならこの型」という型の選び方
生成スクリプトの雛形ファイルとしてアップロード実際にファイルを組み立てるコードの土台。AIはこれを使い回すので、毎回同じ品質になる
✓

これは第7章の「まじん式」とまったく同じ発想です。まじん式ではGASのテンプレートを固定し、AIにはデータだけ書かせました。ここではデザイン定義とスクリプトの雛形を固定し、AIには中身だけ書かせています。変わらない部分を固定して、変わる部分だけをAIに任せる。道具が変わっても、うまくいく作り方は同じです。

作り方の手順

1
プロジェクトを作る

ChatGPTの左メニューから[プロジェクト]→ 新規作成。名前は「社内資料づくり」など

2
指示を貼る

プロジェクトの「指示」欄に、進め方と守ることを書いたテキストを貼る

3
ファイルを入れる

デザイン定義・パターン集・スクリプト雛形の3つをアップロードする

4
その中で会話する

以後は「◯◯の資料を作って」だけでよい。毎回の説明が要らなくなる

★

いちばん効くのは「指示」欄です。ファイルは資料ですが、指示欄は行動の決まりです。とくに「いきなり作り始めず、まず要件を4点確認すること」と「生成後にチェックリストを自分で確認して報告すること」の2つを書いておくと、仕上がりが目に見えて変わります。AIに考える手順を与えるのが、プロンプトのいちばん強い使い方です。

15配布ファイルを持ち帰って自分用に改造する

前章の4つを、ゼロから書くのは大変です。そこで、この研修を実施しているスニフアウトが実際に使っている デザイン定義一式を、そのまま持ち帰れる形でお渡しします。 色とフォントを自社のものに書き換えるだけで、皆さんの会社のトンマナで資料が出てくるようになります。

自分のデザインに変える手順

書き換えるのは3か所だけです。デザインガイドを直したら、雛形スクリプトの数値も同じ値に揃えるのを忘れないでください。

変えるものデザインガイドの場所雛形スクリプトの場所
配色§1 カラーパレットCOLORS と GRADIENT
フォント§2 タイポグラフィFONT_JA / FONT_EN / SIZE
フッターの会社名§8 フッターFOOTER_TEXT
★

そのまま残すことをおすすめする部分もあります。デザインガイドの§3(セーフエリア)と§7(図解のコネクタ規則)、そして§12(生成後のチェックリスト)です。ここはどこの会社でも通用する「崩れないための決まり」で、色やフォントとは性質が違います。会社の個性は §1・§2・§8 に出て、資料の破綻を防ぐのは §3・§7・§12 が担っています。

⚠

この配布物は、株式会社スニフアウトのデザイン定義をもとにしています。皆さまの社内でご自由に改変してお使いいただけますが、社外への配布・転載はご遠慮ください。また、自社仕様に書き換えたあとは、ファイル内の会社名表記(© SNIFFOUT Inc. など)も忘れずに変更してください。

16つまずきポイントと対処

「このアプリはGoogleで確認されていません」と出る

初回だけ出ます。[詳細]→[(プロジェクト名)に移動(安全ではないページ)]と進んでください。 自分が書いたコードを、自分のアカウントで動かすための確認です。スライドを作る権限が要求されるのは正常です。

TypeError: Cannot read properties of null

シート名が見つかっていないときの典型です。レベル2で getSheetByName('報告データ') と書いているのに、 実際のシート名が「報告データ 」(末尾に空白)や「報告データ2」になっていると起きます。 下のタブの名前を、1文字ずつ確認してください。

Exception: The parameters (…) don't match the method signature

関数に渡す値の数や種類が合っていないときのエラーです。 スライド系でとくに多いのが座標に文字列を渡してしまうケース ('40' ではなく 40 と書く)と、 幅や高さに 0 やマイナスを渡すケースです。 シートから読んだ数値は Number() で数値に直してから使ってください。

フォントを指定したのに変わらない

setFontFamily() に渡す名前は、Googleスライドのフォント一覧に出てくる表記と完全に一致している必要があります。 'Noto Sans Japanese' ではなく 'Noto Sans JP'、というレベルで厳密です。 間違った名前を渡してもエラーにはならず、黙って既定のフォントのままになりますので気づきにくい点に注意してください。

大事なスライドが消えてしまった

テンプレート方式のコードは、初期設定で既存のページをすべて削除してから描き直します (clearBeforeGenerate: true)。必ず空のスライドで実行してください。 万一消してしまった場合は、スライドの[ファイル]→[変更履歴]→[変更履歴を表示]から実行前の状態に戻せます。

AIの出力が途中で切れる

コードが長いと、1回の返信に収まらないことがあります。「続きを出力して」と送れば続きが出ますが、 つなぎ目でずれることが多いので、新しいチャットでやり直すほうが結果的に速いことがよくあります。 テンプレート方式なら、AIに出させるのは slideData だけなので、この問題はほぼ起きません。

PowerPointを開いたら、フォントが変わっていた

指定したフォントが開いた側のPCに入っていないと、自動的に別のフォントに置き換わります。 社内で配る資料は、游ゴシック・メイリオ・MS ゴシックなど、Windowsに標準で入っているものを選んでおくと安全です。

どうしても解決しないとき

エラー文をそのまま貼って、AIに聞くのがいちばん速いです。「エラーが出ました」だけでは直せません。 次の型で、状況・エラー文・コードの3点セットを渡してください。

🔧 ChatGPT へのプロンプト ── スライドGASのデバッグ依頼21行
あなたは Google Apps Script(GAS)で Googleスライドを操作する処理の専門家です。 初学者が書いた以下のコードが期待どおりに動きません。原因を特定し、 修正後のコード全文と「なぜ間違っていたか」を、初学者にもわかる言葉で説明してください。 【やりたいこと】 (例:スプレッドシート「報告データ」の各行から、部署ごとに1枚ずつスライドを作りたい) 【実行環境】 ・スプレッドシート(または Googleスライド)に紐づいた GAS プロジェクト ・使っているのは SlidesApp(と SpreadsheetApp) ・スライドのサイズは 720 × 405 ポイント 【症状】 (例:実行は終わるがスライドが1枚も増えない/3枚目で止まる/   文字が枠からはみ出している ※見た目の問題ならスクリーンショットも貼る) 【実行ログ・エラー文】 (ここにそのまま貼り付ける。エラーが出ていない場合は「エラーなし」と書く) 【いま動かしているコード】 (ここにコード全文を貼り付ける)
★

スライド系のデバッグは、実行ログとスクリーンショットの両方を渡すのがコツです。「エラーは出ないが、見た目がおかしい」という症状が多いのがスライド自動化の特徴です。エラーが出ていないなら、ログには何も残りません。そのときは出来上がったスライドの画面を撮って貼るのがいちばん確実に伝わります。

HW

第6回の宿題

次回までに、以下の5つに取り組んでください。コードはAIに書かせてOKです。所要時間:90分程度
1

レベル1を動かし、配色を自分仕様に変える

第3章のコードを貼り付けて createDeck を実行し、6枚のスライドができることを確認してください。そのうえで DESIGN の colorMain と colorAccent を自分の部署の資料の色に変え、もう一度実行して全ページの配色が入れ替わることを確かめましょう。フッターの表記もご自身の部署名に変えてみてください。

2

スプレッドシートから一括生成する

第5章の表のとおりに「報告データ」シートを作り、createReportFromSheet を実行してください。次に5行目に自分の部署の行を1つ追加して再実行し、コードを1文字も変えずにスライドが1枚増えることを確認します。余力があれば、列を1つ増やして(例:担当者名)、その内容がスライドに出るようにコードを直してみてください。

3

テンプレート方式で、AIに中身を書かせる

第9章のコードを空のGoogleスライドに貼り付けて実行し、8枚の見本が出ることを確認します。次に第10章のプロンプトを使い、自分の手元にある実際の資料(過去の報告書・議事録・企画メモなど)を素材にして slideData を作らせ、差し替えて実行してください。スピーカーノートに発表原稿が入っていることまで確認できたら合格です。

4

ChatGPTにPowerPointを作らせる

第12章のプロンプトで、.pptx ファイルを1本作らせてください。そのうえで第13章のやり方で、社外秘の情報を消した手持ちの資料をテンプレートとして添付し、同じ内容をもう一度作らせて2つを見比べてください。「テンプレートを渡すと何がどう変わるか」を、ご自身の目で確認するのがこの課題の目的です。

5

プロジェクトを作り、自社仕様に書き換える

第15章のスライド作成キット(ZIP)をダウンロードし、ChatGPTにプロジェクトを1つ作って、指示欄とファイル3点を設定してください。そのうえでデザインガイドの §1(配色)・§2(フォント)・§8(フッター)を自社の値に書き換え、雛形スクリプトの数値も同じ値に揃えます。最後に、そのプロジェクトの中で資料を1本作らせて、自社のトンマナで出てくることを確認してください。

⚠

宿題を進めるときのお願い:テンプレートとして添付する資料からは、取引先名・個人名・実績の数字・単価を必ず削除してください。体裁を伝えるのに中身は必要ありません。また、テンプレート方式のコードは必ず空のスライドで実行し、業務で使っている資料の上では動かさないでください。

📩

提出方法:①レベル1で配色を変えたスライドのURL、②シートから一括生成したスライドのURL、③テンプレート方式で作ったスライドのURLと、AIに書かせた slideData 全文、④ChatGPTに作らせた PowerPoint ファイル(テンプレートあり/なしの2本)、⑤自社仕様に書き換えたデザインガイドと、それで作った資料の5点を、研修事務局へメールでご提出ください。提出期限:次回研修の前日17:00まで。