メインコンテンツへスキップ
見出し画像

可変可能なGoogleスライドを、Genspark × GASでリッチに再現する悪魔の「けいたろう式プロンプト」


    AIでスライド生成に悩んでいませんか?

    AIでスライドを自動生成してみたけれど、こんな悩みはありませんか?

    • 思ったようなデザインが出てこない

    • 一度生成されたスライドを「ちょっと直したい」のに柔軟に修正できない

    • 仕上がりは早いけれど、チームで使うには不安定

    • 結局「自分で作ったほうが早いかも」と感じてしまう

    私自身、GensparkやManus、さらに「魔神式」と呼ばれるテンプレート型の生成方法まで、数々のアプローチを試してきました。その中で分かったのは、AIはとても便利だけれど、万能ではないということ。

    AIに丸投げすると、確かに「それっぽい」ものは出てきます。でも、いざ本番で使うには修正や調整が必要になる。その時に「どう修正可能な形にしておくか」が、実は一番のポイントでした。

    そこで生まれたのが、私が提案する 「けいたろう式プロンプト」 です。

    けいたろう式は、「AIが得意な部分」と「人間が最後に仕上げる部分」を切り分け、Googleスライド上で修正可能な形で下書きを自動生成する仕組みです。まずは、こちらをご覧ください。

    20秒だけください。編集なしです。 https://t.co/CID3RhLNKA pic.twitter.com/W76Zs0zYwO

    — けいたろう@Notion公式アンバサダー|satto公式エバンジェリスト (@keitaro_aigc) August 21, 2025

    けいたろう式の大まかな流れは以下の通りです。今回の記事ではプロンプトを含めて詳細に紹介します。

    • まずGensparkなどでリッチなスライドをHTMLとして出力

    • それをGoogle Apps Script(GAS)に変換

    • Googleスライドに下書きを再現し、あとは自由に編集できる

    この流れによって、スピード感は維持しながら、修正可能性と表現の豊かさを両立できるのが大きな特徴です。

    作成手順を動画に撮りましたので少しご覧ください。

    感動するから少し時間ください。 pic.twitter.com/CsxJo1HN6U

    — けいたろう@Notion公式アンバサダー|satto公式エバンジェリスト (@keitaro_aigc) August 23, 2025



    第1章 スライド作成の4つの考え方

    AIでスライドを作るときには、大きく分けて4つのパターンがあります。けいたろう式が考案されるまで以下の3パターンを提唱してきました。今回は4つ目のパターンでパターン1とパターン3を統合したものになります。

    スライド生成を発信してきた私の現在の整理。ブクマ必須。 pic.twitter.com/BPKD85y99u

    — けいたろう@Notion公式アンバサダー|satto公式エバンジェリスト (@keitaro_aigc) August 18, 2025

    パターン1:Genspark / Manusで一発生成

    最速で形を出す方法。 テキストを入力するだけで数十秒後には完成スライドが出てきます。

    • 強み

      • 圧倒的に速い

      • 個人利用やアイデア出しには十分

    • 弱み

      • 出てきたデザインの修正が難しい

      • チーム利用や業務用には安定性に欠ける

    画像
    Gensparkで一発生成されたスライドプレビュー

    パターン2:骨子(腰)だけAIに出力させる

    構成だけをAIに設計させ、人間がスライド化する方法。

    • 強み

      • 全体構成を早く固められる

      • 品質を人間の手で担保できる

    • 弱み

      • スライド化の手作業は残る



    パターン3:テンプレ流し込み型(まじん式)

    あらかじめ用意されたGASテンプレートに、AIが生成したデータを流し込む方法。

    • 強み

      • 再現性が非常に高い

      • 定型業務や毎月の報告に強い

    • 弱み

      • テンプレ依存で自由度が低い

    詳しくはこちらをご覧ください。

    これがGoogleスライドで一撃でできて感動している。 pic.twitter.com/mAxnt0xOJz

    — けいたろう@Notion公式アンバサダー|satto公式エバンジェリスト (@keitaro_aigc) August 16, 2025




    パターン4:ハイブリッド型(けいたろう式)

    今回の主役。
    GensparkでリッチなHTMLを生成し、それをGASでGoogleスライドに変換する方式です。

    • 強み

      • リッチな表現を持ち込める

      • Googleスライド上で自由に修正可能

    • 弱み

      • 安定性は若干落ちる

      • プロセスが二段階で少し手間

    「けいたろう式プロンプト」完成。Googleスライドポンだし。プロンプト需要ある? pic.twitter.com/hyOatkBYfk

    — けいたろう@Notion公式アンバサダー|satto公式エバンジェリスト (@keitaro_aigc) August 21, 2025



    第2章 けいたろう式プロンプトでスライドの作成方法

    ここからは、けいたろう式を実際に使うときの流れを整理します。


    1. アプローチの全体像

    • 違い:テンプレ依存ではなく、まずHTMLでリッチな表現を作り込み、その後スライドに変換

    • 特徴:リッチさと編集可能性の両立(安定性はやや犠牲)

    • 注意点:画像は持ち込まない。複雑なグラフは枠+説明テキストで代替

    📷(スクショ例:全体フロー図)


    2. スライド作成の4つの考え方(整理版)

    • パターン1:一発生成(速いが修正不可)

    • パターン2:骨子出力(構成安定)

    • パターン3:テンプレ流し込み(安定重視)

    • パターン4:けいたろう式(リッチ+編集自在)


    3. Genspark活用のプロセス

    敬太郎式の肝となるのが、この Genspark → HTML → GAS → Google Slides という流れです。
    「思った通りのスライドが出ない」「修正できない」というAIスライドの弱点を解消するために、まずは Gensparkでプロ仕様のHTMLスライド を作成するところから始めます。
    Gensparkはこちらから登録すると1000クレジットもらえます。

    ステップ1:GensparkでHTMLスライドを作成

    Gensparkは単なる自動生成ツールではありません。松上純一郎氏の『PowerPoint資料作成 プロフェッショナルの大原則』や、高田貴久氏の『ロジカル・プレゼンテーション』といったフレームワークを踏襲したプロンプトを使えば、背景→課題→解決策→効果 という流れを論理的かつ視覚的に「ポン」と出力してくれます。

    例えば以下のようなプロンプトを使うと、「PowerPoint資料作成 プロフェッショナルの大原則」をベースにした16:9スライドが数分で完成します。

    8月25日追記

    genspark_keitaro_style_slide_prompt:
      description: >
        プロフェッショナルスライドをGensparkで自動生成するためのテンプレート。
        松上純一郎著『PowerPoint資料作成 プロフェッショナルの大原則【生成AI対応版】』に基づき、
        13ステップ・177原則の論理構成と視覚設計を完全実装。
        以下の設計ルールを厳守する:
    
        - スライド比率は16:9固定
        - 各スライドは1メッセージのみ
        - 図解・グラフは単体配置し視認性重視
        - 全体の構成は背景→課題→解決策→効果
        - フォントはすべてNoto Sans JP
        - 配色はユーザ指定またはデフォルト(赤+白)
        - グラフはPre-attentive Attributesを活用し設計
        - 要素は全て静的で、インタラクティブな操作は不可
    
      version: "1.5 - Keitaro Roleplay Edition"
    
      roleplay_instructions:
        - 最初に必ず番号付きで質問を行い、ユーザが選択肢番号で答えられるようにすること
        - いきなりスライドを生成しないこと
        - 以下の順番で質問する:
    
        Q1: 「今日はどんなスライドを作りますか?」
          1: 新製品の営業提案資料
          2: 社内の業務改善提案
          3: 講演・セミナー用資料
          4: 研究発表用資料
          5: その他(自由入力)
    
        Q2: 「どのようなコーポレートカラーにしますか?」
          1: デフォルト(赤+白)
          2: お任せ(自動で最適カラーを選択)
          3: ユーザ指定(カラーコードまたはキーワード入力)
          4: その他(自由入力)
    
        Q3: 「聴衆や利用シーンを教えてください」
          1: 社内共有用
          2: 営業提案用
          3: 講演用
          4: 投資家向けプレゼン用
          5: その他(自由入力)
    
        Q4: 「補足コンテキストがあれば入力してください」
          - 例: 「AI活用事例を紹介するイベント用。参加者は経営層中心」など自由記述可
    
        - 全ての回答を受けた後に、初めてスライド生成フェーズに進むこと
    
      critical_rules:
        slide_size: "16:9"
        font_family: "Noto Sans JP"
        one_topic_per_slide: true
        use_visual_single_slide: true
        large_font_for_visibility: true
        dynamic_color_palette: true
        enforce_preattentive_attributes_in_graphs: true
        prohibit_scroll_or_dynamic_elements: true
    
      layout:
        font:
          family: "Noto Sans JP"
          title_size_pt: 32
          body_size_pt: 20
          note_size_pt: 14
        spacing:
          margin_top_px: 40
          margin_bottom_px: 40
          margin_left_px: 60
          margin_right_px: 60
        reservation:
          text_block_area: "全体余白内に限定配置"
          diagram_area: "上60%に収める"
          note_area: "下20%にスピーカーノートを予約"
    
      color_policy:
        base_palette: "user-choice"
        fallback_palette:
          primary: "#d50000"   # default red
          secondary: "#ffffff" # white
          text: "#000000"
    
      graph_design_guidelines:
        preattentive_attributes:
          enable: true
          usage_guidelines:
            - メッセージを先に定義し、それに対応する視覚属性を1〜2個選定
            - 色相のみで意味を伝えず、長さ・位置・面積などと併用すること
            - ラベル・凡例に依存せず、視覚属性で8割以上伝達できること
            - Tableau分類に基づく32種のグラフ対応表を参照し選定
            - カラーユニバーサルデザインを意識した設計
    
      slide_templates:
        cover:
          elements: ["Title", "Subtitle", "Date"]
          layout_note: "タイトルは上半分に配置。ロゴは不要"
        agenda:
          elements: ["Agenda Title", "項目リスト"]
        content:
          elements: ["Heading", "Visual", "Main Message"]
        graph_slide:
          elements: ["Graph Object", "Caption with preattentive emphasis"]
        summary:
          elements: ["Key Takeaways", "Next Steps"]
    
      generation_constraints:
        enforce_one_slide_one_message: true
        follow_logical_flow: ["背景", "課題", "解決策", "効果"]
        optimize_for_readability_and_clarity: true
        allow_visual_diagrams_only_if_static: true
    
      required_user_input:
        - theme
        - main_message
        - audience
        - color_preference
        - context
        - visual_elements (if applicable)
        - expected_slide_count (optional)
        - key_data_points (optional)
    
      output_instructions:
        - generate_genspark_or_GAS_code_only
        - reserve_layout_areas as specified
        - auto-apply best-fit preattentive chart attributes
        - do not include explanation outside code block
    
      post_generation_checklist:
        description: >
          Genspark実行後、以下の評価基準を満たしているかを人または自動スクリプトでチェックする。
    
        items:
          - id: layout-16x9
            label: "スライドサイズが16:9になっているか"
          - id: layout-safety-margin
            label: "全要素が安全余白内に収まっているか"
          - id: layout-static
            label: "スライドが全て静的(スクロール・動的要素なし)"
          - id: graph-pre-attentive
            label: "グラフにPre-attentive Attributesが使われているか"
          - id: font-consistency
            label: "すべてのフォントがNoto Sans JPで統一されているか"
          - id: color-theme-fit
            label: "配色がユーザ指定またはデフォルトポリシーに沿っているか"
          - id: chart-message-fit
            label: "グラフの種類がメッセージに論理的に適合しているか"
    
    # 必須ユーザ入力例
    theme: "あなたのテーマ"
    main_message: "伝えたいメッセージ"
    audience: "想定聴衆"
    color_preference: "1 / 2 / 3 / 4"
    context: "自由入力"
    
    画像
    Gensparkの入力画面と「完成したHTMLスライド」プレビュー


    ステップ2:出力されたHTMLをコピー

    完成したスライドはHTML形式で出力されます。ここで大事なのは、〜までをまるごとコピーすること。部分的にコピーすると後工程で崩れるので注意が必要です。

    画像
    表示をコードに切り替える
    画像
    Gensparkのエディタで生成されたHTML全選択

    ステップ3:GASプロンプト①でHTML整形

    コピーしたHTMLを、そのままGoogle Slidesで扱うことはできません。そこでまず、GASプロンプト① に投げてHTMLを「要素情報」に整形します。

    • TailwindやFontAwesomeといった外部CSSは無視

    • 図形やテキストの位置・大きさ・色・フォントを抽出

    • JSONやオブジェクト配列として出力

    user input == <ここにGensparkのエディタで生成されたHTMLにいれる>
    
    ====
    # HTML→Google Slides GAS変換プロンプト v3(けいたろう式プロンプト)
    
    ## 基本方針
    **エラーが発生しても処理を継続し、必ず最後まで完走する堅牢なコードを生成する。**
    
    ## 必須の実装要件
    
    ### 1. エラーハンドリング
    ```javascript
    // 全ての処理をtry-catchで包む
    try {
      // 処理
    } catch(e) {
      console.log('エラー発生: ' + e.toString());
      // エラーをログに記録して処理継続
    }
    ```
    
    ### 2. レイアウト設計
    ```javascript
    // 余白定義(定量的に管理)
    const MARGIN = {
      top: 40,
      bottom: 30,
      left: 40,
      right: 40,
      between: 20  // 要素間の間隔
    };
    
    // コンテンツエリアの計算
    const CONTENT_WIDTH = WIDTH - MARGIN.left - MARGIN.right;
    const CONTENT_HEIGHT = HEIGHT - MARGIN.top - MARGIN.bottom;
    ```
    
    ### 3. 複雑な要素の処理
    グラフ・チャート・複雑な図形は以下の方法で処理:
    1. 枠(プレースホルダー)を配置
    2. 枠内にグラフの説明テキストを挿入
    3. データプロパティを箇条書きで表示
    
    ```javascript
    // グラフプレースホルダーの例
    function createChartPlaceholder(slide, x, y, w, h, chartData) {
      // 枠を描画
      rect(slide, x, y, w, h, '#F3F4F6', {color: '#999999', weight: 1});
      
      // タイトル
      textbox(slide, x + 10, y + 10, w - 20, 20, 
        '[グラフ: ' + chartData.title + ']', {size: 12, bold: true});
      
      // データ説明
      const description = `
        種類: ${chartData.type}
        データ数: ${chartData.dataPoints}
        最大値: ${chartData.max}
        最小値: ${chartData.min}
      `;
      textbox(slide, x + 10, y + 35, w - 20, h - 45, 
        description, {size: 10, color: '#666666'});
    }
    ```
    
    ## 完全なコードテンプレート
    
    ```javascript
    function onOpen() {
      try {
        SlidesApp.getUi().createMenu('Build')
          .addItem('ビルド実行', 'buildSlides')
          .addToUi();
      } catch(e) {
        console.log('メニュー作成エラー: ' + e.toString());
      }
    }
    
    function buildSlides() {
      let completedSlides = 0;
      const errors = [];
      
      try {
        const pres = SlidesApp.getActivePresentation();
        const font = 'Noto Sans JP';
        const WIDTH = 720, HEIGHT = 405;
        const SCALE = WIDTH / 1280;
        const px = (n) => n * SCALE;
        const pt = (p) => p * 0.75;
        
        // 余白定義
        const MARGIN = {
          top: 40,
          bottom: 30,
          left: 40,
          right: 40,
          between: 20
        };
        
        // カラー定義
        const RED = '#d50000';
        const DARK = '#1a1a1a';
        const MID = '#555555';
        const GRAY100 = '#F3F4F6';
        const GRAY600 = '#4B5563';
        const WHITE = '#FFFFFF';
        
        // カラー変換(エラー耐性付き)
        function toHexColor(color) {
          try {
            if (!color) return WHITE;
            if (color.startsWith('#')) return color;
            
            // rgba/rgb処理
            if (color.includes('rgb')) {
              const values = color.match(/\d+/g);
              if (values && values.length >= 3) {
                const r = Math.min(255, parseInt(values[0])).toString(16).padStart(2, '0');
                const g = Math.min(255, parseInt(values[1])).toString(16).padStart(2, '0');
                const b = Math.min(255, parseInt(values[2])).toString(16).padStart(2, '0');
                return '#' + r + g + b;
              }
            }
            
            // 名前付きカラーの変換
            const namedColors = {
              'red': '#FF0000',
              'blue': '#0000FF',
              'green': '#008000',
              'transparent': WHITE
            };
            
            return namedColors[color.toLowerCase()] || WHITE;
          } catch(e) {
            console.log('カラー変換エラー: ' + color);
            return WHITE;
          }
        }
        
        // rect(エラー耐性付き)
        function rect(slide, x, y, w, h, fill, border) {
          try {
            const r = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, x, y, w, h);
            r.getFill().setSolidFill(toHexColor(fill) || WHITE);
            const bd = r.getBorder();
            
            if (border && border.weight === 0) {
              bd.setTransparent();
            } else if (border) {
              if (border.color) {
                bd.getLineFill().setSolidFill(toHexColor(border.color));
              }
              if (border.weight != null && border.weight > 0) {
                bd.setWeight(border.weight);
              }
            } else {
              bd.setTransparent();
            }
            return r;
          } catch(e) {
            console.log('rect作成エラー: ' + e.toString());
            return null;
          }
        }
        
        // textbox(エラー耐性付き)
        function textbox(slide, x, y, w, h, text, opt = {}) {
          try {
            const box = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, x, y, w, h);
            const t = box.getText();
            t.setText(text || '');
            
            const ts = t.getTextStyle();
            ts.setFontFamily(opt.font || font);
            if (opt.size) ts.setFontSize(opt.size);
            if (opt.color) ts.setForegroundColor(toHexColor(opt.color));
            if (opt.bold) ts.setBold(true);
            
            const ps = t.getParagraphStyle();
            if (opt.align === 'center') {
              ps.setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER);
            } else if (opt.align === 'right') {
              ps.setParagraphAlignment(SlidesApp.ParagraphAlignment.END);
            }
            
            // 背景と枠線を透明化
            try {
              box.getFill().setTransparent();
              box.getBorder().setTransparent();
            } catch(e) {
              // 透明化エラーは無視
            }
            
            return box;
          } catch(e) {
            console.log('textbox作成エラー: ' + e.toString());
            return null;
          }
        }
        
        // グラフプレースホルダー
        function createChartPlaceholder(slide, x, y, w, h, chartInfo) {
          try {
            // 背景枠
            rect(slide, x, y, w, h, GRAY100, {color: GRAY600, weight: 1});
            
            // ヘッダー
            rect(slide, x, y, w, 30, GRAY600);
            textbox(slide, x + 10, y + 5, w - 20, 20, 
              '[グラフ] ' + (chartInfo.title || 'データ表示'), 
              {size: 12, color: WHITE, bold: true});
            
            // データ情報
            let info = '';
            if (chartInfo.type) info += '種類: ' + chartInfo.type + '\n';
            if (chartInfo.dataPoints) info += 'データ数: ' + chartInfo.dataPoints + '\n';
            if (chartInfo.description) info += chartInfo.description;
            
            textbox(slide, x + 10, y + 40, w - 20, h - 50, 
              info, {size: 10, color: DARK});
          } catch(e) {
            console.log('グラフプレースホルダーエラー: ' + e.toString());
          }
        }
        
        // 箇条書き処理
        function bulletsPlain(text) {
          if (!text) return '';
          const lines = text.toString().split('\n');
          return lines.map(line => line.trim() ? '• ' + line.trim() : '').join('\n');
        }
        
        // 共通要素
        function topBottomBars(slide) {
          try {
            rect(slide, 0, 0, WIDTH, 24, RED);
            rect(slide, 0, HEIGHT - 16, WIDTH, 16, RED);
          } catch(e) {
            console.log('バー作成エラー: ' + e.toString());
          }
        }
        
        function pageNumber(slide, num) {
          try {
            textbox(slide, WIDTH - 40, HEIGHT - 35, 30, 20, 
              String(num), {size: 12, color: MID, align: 'right'});
          } catch(e) {
            console.log('ページ番号エラー: ' + e.toString());
          }
        }
        
        function newSlide() {
          try {
            return pres.appendSlide(SlidesApp.PredefinedLayout.BLANK);
          } catch(e) {
            console.log('スライド追加エラー: ' + e.toString());
            return null;
          }
        }
        
        // グリッドレイアウトヘルパー
        function createGrid(slide, startX, startY, cols, rows, cellWidth, cellHeight, gap) {
          const positions = [];
          for (let row = 0; row < rows; row++) {
            for (let col = 0; col < cols; col++) {
              positions.push({
                x: startX + col * (cellWidth + gap),
                y: startY + row * (cellHeight + gap),
                w: cellWidth,
                h: cellHeight
              });
            }
          }
          return positions;
        }
        
        // =================
        // スライド作成(エラー耐性付き)
        // =================
        
        // スライド1
        try {
          const slide1 = newSlide();
          if (slide1) {
            // タイトルスライドの内容
            rect(slide1, 0, 0, WIDTH, 40, RED);
            rect(slide1, 0, HEIGHT - 20, WIDTH, 20, RED);
            
            const titleY = MARGIN.top + 80;
            textbox(slide1, MARGIN.left, titleY, CONTENT_WIDTH, 45, 
              'プレゼンテーションタイトル', 
              {size: 32, bold: true, align: 'center'});
            
            completedSlides++;
          }
        } catch(e) {
          errors.push('スライド1: ' + e.toString());
        }
        
        // スライド2
        try {
          const slide2 = newSlide();
          if (slide2) {
            topBottomBars(slide2);
            
            // コンテンツエリアを2カラムに分割
            const colWidth = (CONTENT_WIDTH - MARGIN.between) / 2;
            
            // 左カラム
            const leftX = MARGIN.left;
            const leftY = MARGIN.top;
            rect(slide2, leftX, leftY, colWidth, 200, GRAY100);
            textbox(slide2, leftX + 10, leftY + 10, colWidth - 20, 30, 
              '左側コンテンツ', {size: 18, bold: true});
            
            // 右カラム
            const rightX = MARGIN.left + colWidth + MARGIN.between;
            rect(slide2, rightX, leftY, colWidth, 200, GRAY100);
            textbox(slide2, rightX + 10, leftY + 10, colWidth - 20, 30, 
              '右側コンテンツ', {size: 18, bold: true});
            
            pageNumber(slide2, 2);
            completedSlides++;
          }
        } catch(e) {
          errors.push('スライド2: ' + e.toString());
        }
        
        // [ここに追加のスライドを実装]
        
      } catch(e) {
        errors.push('全体エラー: ' + e.toString());
      }
      
      // 完了通知(エラーがあっても表示)
      let message = `処理完了\n作成スライド数: ${completedSlides}`;
      if (errors.length > 0) {
        message += '\n\nエラー:\n' + errors.join('\n');
        console.log('エラー詳細:', errors);
      }
      
      SlidesApp.getUi().alert(message);
    }
    ```
    
    ## プロンプトテンプレート
    
    ```
    以下のHTMLをGoogle SlidesのGASコードに変換してください。
    
    【絶対要件】
    1. **エラーが発生しても処理を止めずに最後まで実行すること**
    2. 全ての処理をtry-catchで包み、エラーログを記録
    3. 複雑なグラフは枠とテキスト説明で代替
    4. 余白を定量的に定義(MARGIN定数使用)
    5. グリッドレイアウトで整列
    
    【必須の対策】
    - setLineSpacing()は使用禁止
    - getBorder()のみ使用
    - rgba値はtoHexColor()で変換
    - 各スライドの処理を個別のtry-catchで包む
    
    【レイアウト設計】
    - 余白: top:40, bottom:30, left:40, right:40, between:20
    - コンテンツエリアを計算して配置
    - グリッドレイアウト関数を活用
    
    
    
    【出力要件】
    1. 完全なGASコード
    2. エラーハンドリング完備
    3. 処理完了後に結果レポート表示
    ```
    
    画像
    プロンプトの入力画面

    以下がこのコードで出力されたコードの例です。

    function onOpen() {
      SlidesApp.getUi().createMenu('Build').addItem('ビルド実行', 'buildSlides').addToUi();
    }
    
    function buildSlides() {
      const pres = SlidesApp.getActivePresentation();
      const font = 'Noto Sans JP';
      const WIDTH = 720, HEIGHT = 405; // 16:9
      
      // カラー定義
      const RED = '#d50000';
      const DARK = '#1a1a1a';
      const MID = '#555555';
      const GRAY600 = '#4B5563';
      const GRAY100 = '#F3F4F6';
      const GRAY50 = '#F9FAFB';
      const WHITE = '#FFFFFF';
    
      // ヘルパー関数
      function rect(slide, x, y, w, h, fill, border) {
        const r = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, x, y, w, h);
        r.getFill().setSolidFill(fill || WHITE);
        const bd = r.getBorder();
        if (border && border.weight === 0) {
          bd.setTransparent();
        } else if (border) {
          if (border.color) bd.getLineFill().setSolidFill(border.color);
          if (border.weight != null) bd.setWeight(border.weight);
        } else {
          bd.setTransparent();
        }
        return r;
      }
    
      function textbox(slide, x, y, w, h, text, opt = {}) {
        const box = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, x, y, w, h);
        const t = box.getText();
        t.setText(text || '');
        const ts = t.getTextStyle();
        ts.setFontFamily(opt.font || font);
        if (opt.size) ts.setFontSize(opt.size);
        if (opt.color) ts.setForegroundColor(opt.color);
        if (opt.bold) ts.setBold(true);
        
        const ps = t.getParagraphStyle();
        // lineSpacingは削除(Google Slidesでサポートされていない)
        if (opt.align === 'center') ps.setParagraphAlignment(SlidesApp.ParagraphAlignment.CENTER);
        if (opt.align === 'right') ps.setParagraphAlignment(SlidesApp.ParagraphAlignment.END);
        if (opt.align === 'justify') ps.setParagraphAlignment(SlidesApp.ParagraphAlignment.JUSTIFIED);
        
        // テキストボックスの背景と枠線を削除
        try {
          box.getFill().setTransparent();
          box.getBorder().setTransparent();
        } catch(e) {
          // エラーを無視
        }
        
        return box;
      }
    
      function topBottomBars(slide) {
        rect(slide, 0, 0, WIDTH, 24, RED);
        rect(slide, 0, HEIGHT - 16, WIDTH, 16, RED);
      }
    
      function pageNumber(slide, num) {
        textbox(slide, WIDTH - 40, HEIGHT - 35, 30, 20, String(num), {size: 12, color: MID, align: 'right'});
      }
    
      function newSlide() {
        return pres.appendSlide(SlidesApp.PredefinedLayout.BLANK);
      }
    
      // スライド1: タイトルスライド
      const slide1 = newSlide();
      rect(slide1, 0, 0, WIDTH, 40, RED);
      rect(slide1, 0, HEIGHT - 20, WIDTH, 20, RED);
      
      // NTT docomoロゴ部分
      textbox(slide1, WIDTH - 150, 60, 130, 25, 'NTT', {size: 16, color: DARK, align: 'right'});
      textbox(slide1, WIDTH - 150, 80, 130, 30, 'docomo', {size: 20, color: RED, bold: true, align: 'right'});
      
      // メインタイトル
      textbox(slide1, 60, 120, 600, 45, 'データ分析スキル向上研修', {size: 32, bold: true, align: 'center'});
      rect(slide1, 296, 170, 128, 3, RED);
      textbox(slide1, 60, 190, 600, 55, 'BYROW関数完全マスター', {size: 38, color: RED, bold: true, align: 'center'});
      
      // 説明文
      textbox(slide1, 80, 260, 560, 60, 
        '本研修は、NTT docomo社員を対象に、業務で即活用できる\nExcel/Google SheetsのBYROW関数の知識と実践力を\n習得するためのセッションです。', 
        {size: 14, color: MID, align: 'center', lineSpacing: 1.5});
      
      // 日付・講師情報
      textbox(slide1, 60, 340, 200, 20, '日時:[日付記載]', {size: 14});
      textbox(slide1, 60, 360, 200, 20, '講師:[講師名記載]', {size: 14});
      textbox(slide1, WIDTH - 160, 360, 140, 20, '社内研修資料', {size: 12, color: GRAY600, align: 'right'});
    
      // スライド2: 研修目的・対象者
      const slide2 = newSlide();
      topBottomBars(slide2);
      
      textbox(slide2, 40, 40, 300, 35, '研修目的・対象者', {size: 28, bold: true});
      rect(slide2, 40, 78, 80, 3, RED);
      
      // 目的ボックス
      rect(slide2, 40, 110, 320, 180, GRAY50);
      rect(slide2, 40, 110, 4, 180, RED);
      textbox(slide2, 55, 120, 150, 30, '【目的】', {size: 20, color: RED, bold: true});
      
      const objectives = [
        '業務効率向上に繋がるデータ分析スキルの習得',
        'BYROW関数の理解と実践スキル獲得',
        '日常業務での作業時間短縮と精度向上'
      ];
      
      let yPos = 155;
      objectives.forEach(obj => {
        textbox(slide2, 55, yPos, 290, 40, '• ' + obj, {size: 14, lineSpacing: 1.3});
        yPos += 40;
      });
      
      // 対象者ボックス
      rect(slide2, 380, 110, 320, 180, GRAY50);
      rect(slide2, 380, 110, 4, 180, RED);
      textbox(slide2, 395, 120, 150, 30, '【対象者】', {size: 20, color: RED, bold: true});
      
      const targets = [
        '営業/企画/管理部門のデータ担当者',
        '日常的にExcel/Google Sheetsを利用される全社員',
        'データ作業の効率化を図りたい方'
      ];
      
      yPos = 155;
      targets.forEach(target => {
        textbox(slide2, 395, yPos, 290, 40, '• ' + target, {size: 14, lineSpacing: 1.3});
        yPos += 40;
      });
      
      // 補足情報
      textbox(slide2, 40, 310, 660, 50, 
        '本研修では、特に営業データ分析・顧客データ集計・プロジェクト管理など、\n日常業務で頻繁に行うデータ処理の効率化に重点を置いています。', 
        {size: 12, color: GRAY600, lineSpacing: 1.5});
      
      pageNumber(slide2, 2);
    
      // スライド3: アジェンダ
      const slide3 = newSlide();
      topBottomBars(slide3);
      
      textbox(slide3, 40, 40, 200, 35, 'アジェンダ', {size: 28, bold: true});
      rect(slide3, 40, 78, 80, 3, RED);
      
      textbox(slide3, 40, 110, 250, 25, '本日の研修の流れ', {size: 18, bold: true});
      
      const agendaItems = [
        ['1', '研修の概要と進め方', '研修の目的と今日習得するスキルの確認'],
        ['2', 'BYROW関数の基礎', '定義・構文・特徴を理解する'],
        ['3', '従来手法との比較', '効率化効果とROIを検証する'],
        ['4', 'ビジネス現場での使い方実例', '実際の業務における活用事例']
      ];
      
      yPos = 145;
      agendaItems.forEach(item => {
        rect(slide3, 40, yPos, 32, 32, RED);
        textbox(slide3, 40, yPos, 32, 32, item[0], {size: 16, color: WHITE, bold: true, align: 'center'});
        textbox(slide3, 80, yPos, 250, 18, item[1], {size: 16, bold: true});
        textbox(slide3, 80, yPos + 18, 250, 14, item[2], {size: 12, color: GRAY600});
        yPos += 45;
      });
      
      const agendaItems2 = [
        ['5', '実践演習', '実際にBYROW関数を使ってみる'],
        ['6', '応用テクニック', 'さらに高度な使用方法'],
        ['7', '注意点・まとめ', '重要ポイントの復習と次回予告']
      ];
      
      yPos = 145;
      agendaItems2.forEach(item => {
        rect(slide3, 380, yPos, 32, 32, RED);
        textbox(slide3, 380, yPos, 32, 32, item[0], {size: 16, color: WHITE, bold: true, align: 'center'});
        textbox(slide3, 420, yPos, 250, 18, item[1], {size: 16, bold: true});
        textbox(slide3, 420, yPos + 18, 250, 14, item[2], {size: 12, color: GRAY600});
        yPos += 45;
      });
      
      // 研修資料情報
      rect(slide3, 380, 295, 320, 65, GRAY50);
      rect(slide3, 380, 295, 4, 65, RED);
      textbox(slide3, 395, 305, 200, 20, '研修資料について', {size: 14, bold: true});
      textbox(slide3, 395, 325, 290, 30, 
        '本日の資料はイントラネットからダウンロード可能です。\n実習用データも含まれています。', 
        {size: 11, color: GRAY600});
      
      pageNumber(slide3, 3);
    
      // スライド4: BYROW関数の基礎
      const slide4 = newSlide();
      topBottomBars(slide4);
      
      textbox(slide4, 40, 40, 300, 35, 'BYROW関数の基礎', {size: 28, bold: true});
      rect(slide4, 40, 78, 80, 3, RED);
      
      // 定義と構文
      rect(slide4, 40, 110, 320, 165, GRAY50);
      rect(slide4, 40, 110, 4, 165, RED);
      textbox(slide4, 55, 120, 200, 25, '定義と構文', {size: 20, color: RED, bold: true});
      textbox(slide4, 55, 150, 290, 35, 
        'BYROW関数とは、指定した範囲の各行に\n同じ処理を適用するための関数です。', 
        {size: 14, lineSpacing: 1.3});
      
      rect(slide4, 55, 195, 290, 65, WHITE, {color: GRAY600, weight: 1});
      textbox(slide4, 65, 200, 100, 20, '基本構文:', {size: 14, color: RED, bold: true});
      textbox(slide4, 65, 225, 270, 25, '=BYROW(配列, LAMBDA(行, 処理))', {size: 13, font: 'Courier New'});
      
      // 特徴とメリット
      rect(slide4, 380, 110, 320, 165, GRAY50);
      rect(slide4, 380, 110, 4, 165, RED);
      textbox(slide4, 395, 120, 200, 25, '主な特徴とメリット', {size: 20, color: RED, bold: true});
      
      const features = [
        '業務効率化:行ごとの計算を一括で自動処理',
        'エラー軽減:人的ミスを防止し、データ品質向上',
        '柔軟性:条件分岐や複雑な処理にも対応',
        'メンテナンス性:データ追加時も自動計算'
      ];
      
      yPos = 150;
      features.forEach(feature => {
        textbox(slide4, 395, yPos, 290, 25, '• ' + feature, {size: 13, lineSpacing: 1.2});
        yPos += 28;
      });
      
      // 実例
      rect(slide4, 40, 285, 660, 75, WHITE, {color: GRAY600, weight: 1});
      textbox(slide4, 55, 295, 200, 20, '実例:行ごとの合計を計算', {size: 16, color: RED, bold: true});
      textbox(slide4, 55, 320, 300, 30, 
        '入力データ:A1:C2(10,20,30 / 5,15,25)\n=BYROW(A1:C2, LAMBDA(row, SUM(row)))\n結果:60 / 45', 
        {size: 12, font: 'Courier New'});
      
      pageNumber(slide4, 4);
    
      // スライド5: 従来手法との比較
      const slide5 = newSlide();
      topBottomBars(slide5);
      
      textbox(slide5, 40, 40, 400, 35, '従来手法との比較・効率化効果', {size: 28, bold: true});
      rect(slide5, 40, 78, 80, 3, RED);
      
      // 従来手法
      rect(slide5, 40, 110, 320, 140, GRAY50);
      rect(slide5, 40, 110, 4, 140, RED);
      textbox(slide5, 55, 120, 200, 25, '従来手法', {size: 20, color: RED, bold: true});
      
      const oldMethods = [
        '手作業によるセルコピーと複雑な数式',
        '行ごとの処理を個別に作成',
        'データ更新時に手動で再計算が必要',
        '属人的なプロセスで引継ぎ困難'
      ];
      
      yPos = 150;
      oldMethods.forEach(method => {
        textbox(slide5, 55, yPos, 290, 20, '• ' + method, {size: 13});
        yPos += 22;
      });
      
      rect(slide5, 55, 220, 290, 25, '#FEE2E2', {color: RED, weight: 1});
      textbox(slide5, 65, 225, 270, 15, '課題:時間効率・正確性・継続性に問題', {size: 12, color: RED, bold: true});
      
      // BYROW関数
      rect(slide5, 380, 110, 320, 140, GRAY50);
      rect(slide5, 380, 110, 4, 140, RED);
      textbox(slide5, 395, 120, 200, 25, 'BYROW関数', {size: 20, color: RED, bold: true});
      
      const newMethods = [
        '1式で全行を動的自動処理',
        'データ追加時も自動展開・計算',
        '一貫したロジックで人的ミスを排除',
        '標準化されたプロセスで引継ぎ容易'
      ];
      
      yPos = 150;
      newMethods.forEach(method => {
        textbox(slide5, 395, yPos, 290, 20, '• ' + method, {size: 13});
        yPos += 22;
      });
      
      rect(slide5, 395, 220, 290, 25, '#DCFCE7', {color: '#16A34A', weight: 1});
      textbox(slide5, 405, 225, 270, 15, '利点:効率化・正確性向上・標準化を実現', {size: 12, color: '#16A34A', bold: true});
      
      // ROI効果
      rect(slide5, 40, 260, 660, 90, GRAY50, {color: RED, weight: 2});
      textbox(slide5, 230, 270, 280, 25, '【効果測定】業務効率化とROI', {size: 18, color: RED, bold: true, align: 'center'});
      
      const metrics = [
        {value: '70%', label: '作業時間削減'},
        {value: '99%', label: 'エラー削減'},
        {value: '85%', label: '再利用性向上'}
      ];
      
      let xPos = 110;
      metrics.forEach(metric => {
        rect(slide5, xPos, 300, 160, 40, WHITE, {color: GRAY600, weight: 1});
        textbox(slide5, xPos, 305, 160, 20, metric.value, {size: 24, color: RED, bold: true, align: 'center'});
        textbox(slide5, xPos, 325, 160, 12, metric.label, {size: 12, align: 'center'});
        xPos += 180;
      });
      
      pageNumber(slide5, 5);
    
      // スライド6: ビジネス実例① 営業データ分析
      const slide6 = newSlide();
      topBottomBars(slide6);
      
      textbox(slide6, 40, 40, 400, 35, 'ビジネス実例① 営業データ分析', {size: 28, bold: true});
      rect(slide6, 40, 78, 80, 3, RED);
      
      textbox(slide6, 40, 110, 350, 25, '実例:エリア×月別データの一括分析', {size: 18, color: RED, bold: true});
      
      rect(slide6, 40, 140, 400, 120, GRAY50);
      rect(slide6, 40, 140, 4, 120, RED);
      textbox(slide6, 55, 150, 370, 20, '営業データの月次報告作成で、以下のような分析が自動化できます:', {size: 13});
      
      const salesExamples = [
        'エリア別:関東/関西/九州などの地域ごとの実績比較',
        '契約数推移:5Gプラン/ギガホ/ahamo等の新規・解約数',
        'オプション分析:dカード/dポイントクラブ加入率推移'
      ];
      
      yPos = 175;
      salesExamples.forEach(example => {
        textbox(slide6, 55, yPos, 370, 20, '• ' + example, {size: 12});
        yPos += 22;
      });
      
      // BYROW関数の活用例
      rect(slide6, 460, 140, 240, 120, WHITE, {color: GRAY600, weight: 1});
      textbox(slide6, 470, 150, 220, 20, 'BYROW関数の活用例', {size: 14, color: RED, bold: true});
      textbox(slide6, 470, 175, 220, 70, 
        '=BYROW(B2:F42, LAMBDA(row,\n  IF(INDEX(row,1)="関東",\n    SUM(INDEX(row,2):INDEX(row,5)),\n    0)\n))', 
        {size: 11, font: 'Courier New'});
      textbox(slide6, 470, 235, 220, 20, '関東エリアの各行データのみ合計値を抽出', {size: 10, color: GRAY600});
      
      // サンプルテーブル
      rect(slide6, 40, 270, 660, 80, WHITE, {color: GRAY600, weight: 1});
      textbox(slide6, 50, 280, 100, 20, '拠点', {size: 12, bold: true, align: 'center'});
      textbox(slide6, 150, 280, 100, 20, '新規契約', {size: 12, bold: true, align: 'center'});
      textbox(slide6, 250, 280, 100, 20, '解約数', {size: 12, bold: true, align: 'center'});
      textbox(slide6, 350, 280, 100, 20, '純増減', {size: 12, color: RED, bold: true, align: 'center'});
      
      const sampleData = [
        ['新宿', '142', '87', '55'],
        ['渋谷', '118', '63', '55'],
        ['池袋', '96', '71', '25']
      ];
      
      yPos = 305;
      sampleData.forEach(row => {
        textbox(slide6, 50, yPos, 100, 15, row[0], {size: 11, align: 'center'});
        textbox(slide6, 150, yPos, 100, 15, row[1], {size: 11, align: 'center'});
        textbox(slide6, 250, yPos, 100, 15, row[2], {size: 11, align: 'center'});
        textbox(slide6, 350, yPos, 100, 15, row[3], {size: 11, color: RED, bold: true, align: 'center'});
        yPos += 18;
      });
      
      pageNumber(slide6, 6);
    
      // スライド7: ビジネス実例② 顧客データ集計
      const slide7 = newSlide();
      topBottomBars(slide7);
      
      textbox(slide7, 40, 40, 400, 35, 'ビジネス実例② 顧客データ集計', {size: 28, bold: true});
      rect(slide7, 40, 78, 80, 3, RED);
      
      textbox(slide7, 40, 110, 350, 25, '実例:顧客データ分析の効率化', {size: 18, color: RED, bold: true});
      
      // 年代×性別×エリア別
      rect(slide7, 40, 140, 320, 80, GRAY50);
      textbox(slide7, 50, 150, 300, 20, '年代×性別×エリア別の利用状況分析', {size: 14, bold: true});
      textbox(slide7, 50, 175, 300, 40, 
        '=BYROW(B2:F100, LAMBDA(row,\n  IF(AND(INDEX(row,4)="関東", INDEX(row,2)="20代"),\n    INDEX(row,5), 0)))', 
        {size: 11, font: 'Courier New'});
      
      // dポイント消化率
      rect(slide7, 380, 140, 320, 80, GRAY50);
      textbox(slide7, 390, 150, 300, 20, 'dポイント消化率の顧客別算出', {size: 14, bold: true});
      textbox(slide7, 390, 175, 300, 40, 
        '=BYROW(B2:G100, LAMBDA(row,\n  INDEX(row,5)/INDEX(row,6)*100))', 
        {size: 11, font: 'Courier New'});
      
      // 顧客満足度テーブル
      rect(slide7, 40, 235, 450, 115, WHITE, {color: GRAY600, weight: 1});
      textbox(slide7, 50, 245, 200, 20, '顧客満足度スコア自動計算', {size: 14, bold: true});
      
      textbox(slide7, 50, 270, 80, 15, '顧客ID', {size: 11, bold: true});
      textbox(slide7, 130, 270, 60, 15, '年代', {size: 11, bold: true});
      textbox(slide7, 190, 270, 80, 15, '料金満足', {size: 11, bold: true});
      textbox(slide7, 270, 270, 90, 15, 'サービス満足', {size: 11, bold: true});
      textbox(slide7, 360, 270, 80, 15, '総合スコア', {size: 11, color: RED, bold: true});
      
      const customerData = [
        ['D0012354', '30代', '4.2', '4.5', '4.35'],
        ['D0023651', '20代', '3.8', '4.1', '3.95'],
        ['D0034782', '50代', '4.0', '3.7', '3.85']
      ];
      
      yPos = 290;
      customerData.forEach(row => {
        textbox(slide7, 50, yPos, 80, 15, row[0], {size: 10});
        textbox(slide7, 130, yPos, 60, 15, row[1], {size: 10});
        textbox(slide7, 190, yPos, 80, 15, row[2], {size: 10});
        textbox(slide7, 270, yPos, 90, 15, row[3], {size: 10});
        textbox(slide7, 360, yPos, 80, 15, row[4], {size: 10, color: RED, bold: true});
        yPos += 18;
      });
      
      // 活用シーン
      rect(slide7, 510, 235, 190, 115, GRAY50);
      rect(slide7, 510, 235, 4, 115, RED);
      textbox(slide7, 520, 245, 170, 20, '主な活用シーン', {size: 14, bold: true});
      
      const scenes = [
        'マーケティング部署の\n週次KPI分析',
        'dポイントキャンペーン\n効果測定',
        '契約プラン変更による\n顧客満足度影響分析'
      ];
      
      yPos = 270;
      scenes.forEach(scene => {
        textbox(slide7, 520, yPos, 170, 25, '• ' + scene, {size: 10});
        yPos += 25;
      });
      
      pageNumber(slide7, 7);
    
      // スライド8: ビジネス実例③ プロジェクト管理
      const slide8 = newSlide();
      topBottomBars(slide8);
      
      textbox(slide8, 40, 40, 450, 35, 'ビジネス実例③ プロジェクト管理データ', {size: 28, bold: true});
      rect(slide8, 40, 78, 80, 3, RED);
      
      rect(slide8, 40, 110, 350, 110, GRAY50);
      rect(slide8, 40, 110, 4, 110, RED);
      textbox(slide8, 55, 120, 320, 25, '実例:プロジェクト管理の効率化', {size: 18, color: RED, bold: true});
      textbox(slide8, 55, 150, 320, 30, 
        'システム開発・5G基地局設置・ネットワーク増強などの\n各プロジェクトで、BYROW関数を活用して進捗状況を一元管理', 
        {size: 12, lineSpacing: 1.3});
      textbox(slide8, 55, 190, 320, 25, 
        '→ 各プロジェクトの進捗率を一括自動計算', 
        {size: 11, color: GRAY600});
      
      // BYROW関数例
      rect(slide8, 410, 110, 290, 110, WHITE, {color: GRAY600, weight: 1});
      textbox(slide8, 420, 120, 270, 20, 'BYROW関数の活用例:', {size: 14, bold: true});
      textbox(slide8, 420, 145, 270, 65, 
        '=BYROW(B4:F12, LAMBDA(row,\n  IF(INDEX(row,5)="完了", 100,\n    (INDEX(row,3)/INDEX(row,4))*100)\n))', 
        {size: 11, font: 'Courier New'});
      
      // プロジェクト進捗管理表
      rect(slide8, 40, 235, 660, 115, WHITE, {color: GRAY600, weight: 1});
      textbox(slide8, 50, 245, 200, 20, 'プロジェクト進捗管理表', {size: 14, color: RED, bold: true});
      
      textbox(slide8, 50, 270, 140, 15, 'プロジェクト名', {size: 11, bold: true});
      textbox(slide8, 190, 270, 100, 15, '担当部署', {size: 11, bold: true});
      textbox(slide8, 290, 270, 80, 15, '完了タスク', {size: 11, bold: true});
      textbox(slide8, 370, 270, 80, 15, '総タスク', {size: 11, bold: true});
      textbox(slide8, 450, 270, 80, 15, 'ステータス', {size: 11, bold: true});
      textbox(slide8, 530, 270, 80, 15, '進捗率', {size: 11, color: RED, bold: true});
      
      const projectData = [
        ['5G Tokyo East展開', 'ネットワーク部', '42', '50', '進行中', '84%'],
        ['dポイント連携強化', 'マーケティング部', '18', '18', '完了', '100%'],
        ['法人向けDX推進', '法人営業部', '12', '30', '進行中', '40%']
      ];
      
      yPos = 290;
      projectData.forEach(row => {
        textbox(slide8, 50, yPos, 140, 15, row[0], {size: 10});
        textbox(slide8, 190, yPos, 100, 15, row[1], {size: 10});
        textbox(slide8, 290, yPos, 80, 15, row[2], {size: 10, align: 'center'});
        textbox(slide8, 370, yPos, 80, 15, row[3], {size: 10, align: 'center'});
        textbox(slide8, 450, yPos, 80, 15, row[4], {size: 10});
        textbox(slide8, 530, yPos, 80, 15, row[5], {size: 10, color: RED, bold: true, align: 'center'});
        yPos += 18;
      });
      
      pageNumber(slide8, 8);
    
      // スライド9: 実践演習用サンプル
      const slide9 = newSlide();
      topBottomBars(slide9);
      
      textbox(slide9, 40, 40, 300, 35, '実践演習用サンプル', {size: 28, bold: true});
      rect(slide9, 40, 78, 80, 3, RED);
      
      // 演習1
      rect(slide9, 40, 110, 660, 75, GRAY50);
      rect(slide9, 40, 110, 4, 75, RED);
      textbox(slide9, 55, 120, 550, 20, '【演習1】契約数・解約数の行別純増減を全拠点一括算出', {size: 16, color: RED, bold: true});
      textbox(slide9, 55, 143, 400, 15, '各エリアの契約数・解約数から全拠点の純増減を自動計算', {size: 12});
      textbox(slide9, 55, 160, 450, 20, '=BYROW(B2:C8, LAMBDA(row, INDEX(row,1) - INDEX(row,2)))', {size: 12, font: 'Courier New', color: DARK});
      
      // 演習2
      rect(slide9, 40, 195, 660, 75, GRAY50);
      rect(slide9, 40, 195, 4, 75, RED);
      textbox(slide9, 55, 205, 550, 20, '【演習2】顧客リストから属性別"平均利用額"をBYROWで集計', {size: 16, color: RED, bold: true});
      textbox(slide9, 55, 228, 400, 15, '各顧客の年代・性別・エリア別のARPU(顧客単価)を分析', {size: 12});
      textbox(slide9, 55, 245, 450, 20, '=BYROW(C2:E20, LAMBDA(row, AVERAGE(row)))', {size: 12, font: 'Courier New', color: DARK});
      
      // 演習3
      rect(slide9, 40, 280, 660, 75, GRAY50);
      rect(slide9, 40, 280, 4, 75, RED);
      textbox(slide9, 55, 290, 550, 20, '【演習3】プロジェクト進捗から遅延行のみ特定するBYROW式を作成', {size: 16, color: RED, bold: true});
      textbox(slide9, 55, 313, 400, 15, '進捗率が計画を下回るプロジェクトを自動で抽出', {size: 12});
      textbox(slide9, 55, 330, 550, 20, '=FILTER(A2:E10, BYROW(D2:E10, LAMBDA(row, INDEX(row,1) < INDEX(row,2))))', {size: 12, font: 'Courier New', color: DARK});
      
      pageNumber(slide9, 9);
    
      // スライド10: 応用テクニック
      const slide10 = newSlide();
      topBottomBars(slide10);
      
      textbox(slide10, 40, 40, 300, 35, '応用テクニック', {size: 28, bold: true});
      rect(slide10, 40, 78, 80, 3, RED);
      
      rect(slide10, 40, 110, 320, 150, GRAY50);
      rect(slide10, 40, 110, 4, 150, RED);
      textbox(slide10, 55, 120, 250, 25, 'BYROW関数の高度な活用', {size: 18, color: RED, bold: true});
      
      const techniques = [
        ['IF関数との組み合わせ', '条件に応じた処理を行別に実行し、結果を一括表示'],
        ['INDEX・FILTERとの連携', '特定列の値を抽出・フィルタリングして高度な分析を実現'],
        ['スコア一括判定', '顧客スコア・営業成績などを自動判定してランク付け'],
        ['複雑なロジック再利用', 'LAMBDA関数でカスタム処理を定義し再利用性向上']
      ];
      
      yPos = 150;
      techniques.forEach(tech => {
        textbox(slide10, 55, yPos, 60, 15, '• ' + tech[0], {size: 12, bold: true});
        textbox(slide10, 115, yPos, 235, 28, tech[1], {size: 10, color: GRAY600});
        yPos += 28;
      });
      
      // コード例1
      rect(slide10, 380, 110, 320, 75, WHITE, {color: GRAY600, weight: 1});
      textbox(slide10, 390, 120, 300, 18, '条件分岐による成績評価', {size: 14, color: RED, bold: true});
      textbox(slide10, 390, 140, 300, 40, 
        '=BYROW(B2:D10, LAMBDA(row,\n  IF(SUM(row) > 1000, "S評価",\n  IF(SUM(row) > 700, "A評価", "B評価"))))', 
        {size: 11, font: 'Courier New'});
      
      // コード例2
      rect(slide10, 380, 195, 320, 65, WHITE, {color: GRAY600, weight: 1});
      textbox(slide10, 390, 205, 300, 18, 'FILTER連携による抽出', {size: 14, color: RED, bold: true});
      textbox(slide10, 390, 225, 300, 35, 
        '=FILTER(A2:E15, BYROW(B2:D15, LAMBDA(row,\n  AND(MAX(row) > 90, MIN(row) > 50))))', 
        {size: 11, font: 'Courier New'});
      
      // 応用アイデア
      rect(slide10, 40, 275, 660, 75, GRAY50, {color: RED, weight: 2});
      textbox(slide10, 55, 285, 150, 20, '応用アイデア:', {size: 14, color: RED, bold: true});
      textbox(slide10, 55, 305, 630, 35, 
        'BYCOL/MAP関数との使い分け、複合分析、多段階フィルタリングによる高度なデータ処理\nこれらの応用テクニックは、大量のデータを扱うdポイント分析、顧客セグメント分析、\nキャンペーン効果測定などで特に効果を発揮します。', 
        {size: 11, lineSpacing: 1.3});
      
      pageNumber(slide10, 10);
    
      // スライド11: 注意点・トラブルシューティング
      const slide11 = newSlide();
      topBottomBars(slide11);
      
      textbox(slide11, 40, 40, 450, 35, '注意点・トラブルシューティング', {size: 28, bold: true});
      rect(slide11, 40, 78, 80, 3, RED);
      
      rect(slide11, 40, 110, 320, 155, GRAY50);
      rect(slide11, 40, 110, 4, 155, RED);
      textbox(slide11, 55, 120, 250, 25, '【頻発エラーと対処法】', {size: 18, color: RED, bold: true});
      
      const errors = [
        ['LAMBDA構文エラー', 'カッコやカンマの欠落によるエラー', '関数構文を定型化し、小さな範囲でテスト'],
        ['データ範囲の誤指定', '処理対象の列数と実際のデータ不一致', 'INDEX関数での参照先を再確認'],
        ['空値(NULL)処理の不備', '空セルがエラーや意図しない結果を招く', 'ISBLANK/IFERROR関数で空値対策']
      ];
      
      yPos = 150;
      errors.forEach(error => {
        textbox(slide11, 55, yPos, 120, 15, '▲ ' + error[0], {size: 12, color: RED, bold: true});
        textbox(slide11, 55, yPos + 15, 200, 12, error[1], {size: 10, color: GRAY600});
        textbox(slide11, 55, yPos + 28, 290, 12, '解決策:' + error[2], {size: 10});
        yPos += 45;
      });
      
      rect(slide11, 380, 110, 320, 155, GRAY50);
      rect(slide11, 380, 110, 4, 155, RED);
      textbox(slide11, 395, 120, 250, 25, '【ベストプラクティス】', {size: 18, color: RED, bold: true});
      
      const practices = [
        ['標準化された構文テンプレート', '部署内で統一されたBYROW記述法を採用'],
        ['定期的なバックアップと段階的適用', '重要な業務ファイルは関数適用前にバックアップ'],
        ['名前付き範囲の活用', '頻繁に使う範囲は名前付き範囲として定義'],
        ['LAMBDA関数の段階的構築', '複雑な処理は単純な処理から徐々に構築']
      ];
      
      yPos = 150;
      let num = 1;
      practices.forEach(practice => {
        rect(slide11, 395, yPos, 20, 20, RED);
        textbox(slide11, 395, yPos, 20, 20, String(num), {size: 12, color: WHITE, bold: true, align: 'center'});
        textbox(slide11, 420, yPos, 140, 13, practice[0], {size: 11, bold: true});
        textbox(slide11, 420, yPos + 14, 270, 18, practice[1], {size: 9, color: GRAY600});
        yPos += 35;
        num++;
      });
      
      // エラー回避のためのコード例
      rect(slide11, 40, 275, 660, 75, '#1F2937', {color: GRAY600, weight: 1});
      textbox(slide11, 55, 285, 200, 15, '/* エラー回避のためのコード例 */', {size: 11, color: '#10B981'});
      textbox(slide11, 55, 300, 630, 45, 
        '=BYROW(A2:C10, LAMBDA(row,\n  IF(ISBLANK(INDEX(row,1)), "データなし",\n    IFERROR(SUM(row), 0))))', 
        {size: 12, font: 'Courier New', color: WHITE});
      
      pageNumber(slide11, 11);
    
      // スライド12: まとめ・次回研修予告
      const slide12 = newSlide();
      topBottomBars(slide12);
      
      textbox(slide12, 40, 40, 400, 35, 'まとめ・次回研修予告', {size: 28, bold: true});
      rect(slide12, 40, 78, 80, 3, RED);
      
      // 本日の研修内容
      rect(slide12, 40, 110, 320, 130, GRAY50);
      textbox(slide12, 50, 120, 200, 22, '本日の研修内容', {size: 18, color: RED, bold: true});
      rect(slide12, 50, 145, 300, 1, GRAY600);
      textbox(slide12, 50, 152, 100, 18, '習得したスキル', {size: 14, bold: true});
      
      const skills = [
        'BYROW関数の基礎構文と使い方',
        '営業データ・顧客データ・プロジェクト管理への応用',
        '従来手法と比較した業務効率化(工数70%削減)',
        '他関数(IF/FILTER/INDEX)との組み合わせ技術'
      ];
      
      yPos = 173;
      skills.forEach(skill => {
        textbox(slide12, 50, yPos, 300, 15, '• ' + skill, {size: 11});
        yPos += 16;
      });
      
      // 次回研修予告
      rect(slide12, 380, 110, 320, 130, GRAY50);
      rect(slide12, 380, 110, 4, 130, RED);
      textbox(slide12, 395, 120, 250, 22, '次回研修予告', {size: 18, color: RED, bold: true});
      textbox(slide12, 395, 145, 290, 18, 'データ分析スキル向上研修 応用編', {size: 14, bold: true});
      textbox(slide12, 395, 165, 290, 15, '日時:202X年XX月XX日 14:00-16:00', {size: 11});
      textbox(slide12, 395, 180, 290, 15, '場所:本社 研修室A(オンライン参加も可)', {size: 11});
      
      const nextTopics = [
        '複数配列処理(BYCOL/SCAN/REDUCE関数等)',
        '生成AI×Excel最新活用法',
        '業務プロセス自動化への応用テクニック'
      ];
      
      yPos = 200;
      nextTopics.forEach(topic => {
        textbox(slide12, 395, yPos, 290, 12, '• ' + topic, {size: 10});
        yPos += 14;
      });
      
      // アンケート案内
      rect(slide12, 40, 250, 660, 90, '#FEF2F2', {color: RED, weight: 1});
      textbox(slide12, 55, 260, 200, 20, 'アンケートご協力のお願い', {size: 16, bold: true});
      textbox(slide12, 55, 283, 630, 30, 
        '本日の研修内容の改善のため、アンケートへのご協力をお願いします。\n以下のQRコードまたはURLからアクセスできます。', 
        {size: 11});
      
      rect(slide12, 55, 315, 60, 20, WHITE, {color: GRAY600, weight: 1});
      textbox(slide12, 55, 318, 60, 14, 'QRコード', {size: 9, align: 'center', color: GRAY600});
      
      textbox(slide12, 130, 318, 300, 15, 'URL: https://docomo-training.jp/survey', {size: 11, color: '#2563EB'});
      
      // フッター情報
      textbox(slide12, 40, 350, 400, 15, 
        'ご質問やご相談は、データ分析部門(data-analysis@docomo.co.jp)までお寄せください。', 
        {size: 10, color: GRAY600});
      
      pageNumber(slide12, 12);
    
      SlidesApp.getUi().alert('スライド生成が完了しました');
    }

    かなりの長大なコードになります。

    ステップ4:GASプロンプト②でGoogle Slidesに変換

    整形されたデータを GASプロンプト② に渡し、Google Slides上に再現します。

    • onOpen()関数を実装 → メニューからワンクリックで実行

    • try/catchでエラーが出ても最後まで処理を継続

    • 画像は一切使用せず、テキストと図形で再現

    • グラフは「枠+説明テキスト」で代替

    画像
    Googleスライドを起動して「App Script」をクリック
    画像
    コードをペーストして保存する
    画像
    onOpen関数を実行する
    画像
    権限を確認する
    画像
    認証を行う
    画像
    ビルド実行をクリックする
    画像
    スライドが生成する
    画像
    完成したスライド

    このプロセスの意義

    この4ステップを通すことで、Gensparkのリッチな出力を「修正可能なGoogleスライド」に落とし込めるようになります。

    従来のAIスライドは「出力されたら終わり」で修正しづらいものでしたが、敬太郎式は「出力した後に編集できる」ことを前提に設計しているため、実務に耐える柔軟さ を手に入れることができます。

    4. 実装のポイント

    • エラーが出ても最後まで実行する(try/catch)

    • onOpen関数を設置してUIから実行可能にする

    • 下書き用途と割り切る

    • Gensparkで「気に入ったスライド」を作ってから移行する


    5. まとめ

    • けいたろう式:Genspark+GAS二段プロンプトでリッチな下書きを生成

    • テンプレ型:安定性重視の流し込み方式

    • 選び方は スピード/安定性/表現力 の優先度次第


    あとがき

    AIでスライドを作る方法はどんどん進化しています。
    けいたろう式はまだ発展途上ですが、「AIは下書き、人間が仕上げる」 という考え方を前提にすると非常に実用的です。

    これからは、案件や目的に応じてパターンを切り替えながら、最適なワークフローを作り込むことが重要になるでしょう。

    今後、プロンプトを修正してさらに磨いていきますのでこの記事に「スキ」をもらえると更新が通知されます。こちらから公式LINEに登録してもらえると幸いです。

    画像
    公式LINE

    noteの「スキ」を押すとQRコードがでてくるよ pic.twitter.com/qwXNaW5IPe

    — けいたろう@Notion公式アンバサダー|satto公式エバンジェリスト (@keitaro_aigc) August 22, 2025

    参考

    過去のプロンプト

    genspark_nttdocomo_slide_prompt:
      description: >
        NTT docomo向けのプロフェッショナルスライドをGensparkで自動生成するためのテンプレート。
        松上純一郎著『PowerPoint資料作成 プロフェッショナルの大原則【生成AI対応版】』に基づき、
        13ステップ・177原則の論理構成と視覚設計を完全実装。
        以下の設計ルールを厳守する:
    
        - スライド比率は16:9固定
        - 各スライドは1メッセージのみ
        - 図解・グラフは単体配置し視認性重視
        - 全体の構成は背景→課題→解決策→効果
        - フォントはすべてNoto Sans JP
        - 配色はテーマ文脈に応じて動的決定(docomo=赤白)
        - グラフはPre-attentive Attributesを活用し設計
        - 要素は全て静的で、インタラクティブな操作は不可
    
      version: "1.3 - docomo x Pre-attentive Attributes Edition"
    
      critical_rules:
        slide_size: "16:9"
        font_family: "Noto Sans JP"
        one_topic_per_slide: true
        use_visual_single_slide: true
        large_font_for_visibility: true
        dynamic_color_palette: true
        enforce_preattentive_attributes_in_graphs: true
        prohibit_scroll_or_dynamic_elements: true
    
      layout:
        font:
          family: "Noto Sans JP"
          title_size_pt: 32
          body_size_pt: 20
          note_size_pt: 14
        spacing:
          margin_top_px: 40
          margin_bottom_px: 40
          margin_left_px: 60
          margin_right_px: 60
        reservation:
          text_block_area: "全体余白内に限定配置"
          diagram_area: "上60%に収める"
          note_area: "下20%にスピーカーノートを予約"
    
      color_policy:
        base_palette: "context-dependent"
        fallback_palette:
          primary: "#d50000"   # docomo red
          secondary: "#ffffff" # white
          text: "#000000"
    
      graph_design_guidelines:
        preattentive_attributes:
          enable: true
          usage_guidelines:
            - メッセージを先に定義し、それに対応する視覚属性を1〜2個選定
            - 色相のみで意味を伝えず、長さ・位置・面積などと併用すること
            - ラベル・凡例に依存せず、視覚属性で8割以上伝達できること
            - Tableau分類に基づく32種のグラフ対応表を参照し選定
            - カラーユニバーサルデザインを意識した設計
    
      slide_templates:
        cover:
          elements: ["Title", "Subtitle", "Date"]
          layout_note: "タイトルは上半分に配置。ロゴは不要"
        agenda:
          elements: ["Agenda Title", "項目リスト"]
        content:
          elements: ["Heading", "Visual", "Main Message"]
        graph_slide:
          elements: ["Graph Object", "Caption with preattentive emphasis"]
        summary:
          elements: ["Key Takeaways", "Next Steps"]
    
      generation_constraints:
        enforce_one_slide_one_message: true
        follow_logical_flow: ["背景", "課題", "解決策", "効果"]
        optimize_for_readability_and_clarity: true
        allow_visual_diagrams_only_if_static: true
    
      required_user_input:
        - theme
        - main_message
        - audience
        - visual_elements (if applicable)
        - expected_slide_count (optional)
        - key_data_points (optional)
    
      output_instructions:
        - generate_genspark_or_GAS_code_only
        - reserve_layout_areas as specified
        - auto-apply best-fit preattentive chart attributes
        - do not include explanation outside code block
    
      post_generation_checklist:
        description: >
          Genspark実行後、以下の評価基準を満たしているかを人または自動スクリプトでチェックする。
    
        items:
          - id: layout-16x9
            label: "スライドサイズが16:9になっているか"
            method: "Slides設定から確認"
            tool: "Google Slides or Genspark出力解像度"
    
          - id: layout-safety-margin
            label: "全要素が安全余白内に収まっているか"
            method: "各要素のboundingBox確認"
            tool: "自動チェック or 目視確認"
    
          - id: layout-static
            label: "スライドが全て静的(スクロール・動的要素なし)"
            method: "インタラクティブDOM要素の有無確認"
            tool: "Genspark schema validation"
    
          - id: graph-pre-attentive
            label: "グラフにPre-attentive Attributesが使われているか"
            method: "色・長さ・位置・方向などの視覚属性で構成されているか"
            tool: "chart_type + visual_mapping 評価"
    
          - id: font-consistency
            label: "すべてのフォントがNoto Sans JPで統一されているか"
            method: "textStyle検証"
            tool: "Genspark出力style確認"
    
          - id: color-theme-fit
            label: "配色がコンテキストに即して動的に設定されているか"
            method: "テーマキーワードによるカラーパレットの適用確認"
            tool: "paletteResolver関数 or カラーコード確認"
    
          - id: chart-message-fit
            label: "グラフの種類がメッセージに論理的に適合しているか"
            method: "Tableau分類との一致、属性との整合性"
            tool: "chart_typeとmessage整合性マッチング"
    
    # ここに追記してください:
    theme: "あなたのテーマ"
    main_message: "伝えたいメッセージ"
    audience: "想定聴衆"
    
    
     
     
     
    イベント企画100回以上の経験を持つコミュニティマネージャー|AIと最小限のコードで業務を劇的に効率化中|PerplexityとGASで複雑な作業を簡単に自動化|非エンジニアでも実践できるAI活用術を日々発信|TableauNext2024/DATASaber/GCS認定コーチ/

    あなたへのおすすめ