まじん式プロンプト v3 を改造! GeminiでGoogleスライドテンプレート適用を制覇した記録
LLMによるスライド自動生成の可能性と、日本語環境特有のAPIの壁を、AIを正しく導くことで乗り越えた試行錯誤の全記録。
はじまりは感動から、そして一つの課題
すべてのきっかけは、まじんさん(【神回】Googleスライドが一瞬で完成する"奇跡"のプロンプト教えます)の記事でした。LLMに構造化されたデータslideDataを出力させ、それをGoogle Apps Script (GAS)で解析し、スライドを生成していく。すべてを LLM にやらせるのではなく、作業分担をさせる!そのアイデアは目からうろこが落ちました。
感動と同時に、これを実務で使いたいと強く思いました。しかし、そのためにはどうしても譲れない、たった一つの、しかし決定的な課題がありました。それは**「既存のGoogleスライドのテンプレートを活用できない」**という点です。会社で定められたフォーマットや、自分が作り込んできたデザインをそのまま流用したい。この実用上の最後のピースを埋めるため、私の長い(そして回り道だらけの)旅が始まりました。
転機が訪れる前の試行錯誤
まじん式プロンプトv3が登場する前、私は別の方法でこの「テンプレート問題」を解決しようと模索していました。
最有力だったのが、k1LoWさんのdeck (github.com/k1LoW/deck)でした。Markdownですべてを管理できる思想は非常に美しく、完全な自動生成を目指すなら最高の選択肢の一つです。LLMにdeck用のMarkdownを出力させれば、テンプレート問題も解決できるはずでした。しかし、私の目的は少し違いました。「下書きをLLMに作らせ、最後の微調整はスライド上でデザイナーのようにオブジェクトをゴリゴリ動かしたい」。この使い方では、deckの厳格なテンプレートシステムは、私の求める自由度とは少し異なりました。
deckでの模索が停滞する中、私は別のアプローチも試していました。それは「まじん式」の根幹であるslideDataをスピーカーノートに出力し、それをトリガーにしてGASで後からオブジェクトを配置するというハイブリッド方式です。実際に動作する finishSlides関数を完成させましたが、このアプローチではdeckで更新した後に手動で反映させるワンアクションが必要で、完璧とは言えませんでした。
他に優先すべきことも重なり、この開発は一旦保留となっていました。 そんな日々が続いていたある日、衝撃の記事の見かけました。
転機:まじん式プロンプトv3の衝撃と方針転換
事件は起こりました。まじん式プロンプトv3 の登場です。
Web UIが搭載され、設定が格段に楽になり、ツールとしての洗練度は異次元のレベルに達していました。これを見た瞬間、私は悟りました。「もうdeckや独自方式をこねくり回している場合ではない。この v3 をベースにするのが、どう考えても正解だ」と。
v3 の登場は、それまでの私の試行錯誤をすべて過去のものにするほどの衝撃でした。そして、それは同時に、取り組むべき課題を一つに絞り込んでくれました。v3は、まだテンプレート活用には対応していません。ならば、「この素晴らしいv3を改造し、テンプレート適用という最後の1ピースを埋め込むことこそ、自分のやるべきことだ」。ここで、私の進むべき道が明確に定まりました。
相棒Geminiとのコード生成:AIを導く協調的デバッグ
「ならば、まじん式プロンプトv3 のGASを改造しよう」。そう決意し、相棒としてAIであるGeminiにコード生成を命じました。
いろいろありましたが、開発の核心にあったのは、テンプレート内のレイアウトを日本語の「表示名」でいかにして取得するかという点でした。日本語レイアウト名でなければ、問題なくできていたのですが、どうにもできず。
ぼぶ: Gemini、テンプレート内のレイアウトを日本語の表示名で取得して、スライドを生成するコードを書いてくれ。
AI: 承知いたしました。公式ドキュメントに基づけば、getLayouts()で取得したオブジェクトに対しgetDisplayName()を使用するのが標準的です。こちらがそのコードです。
AIは自信満々に回答を提示します。しかし、現実はうまくはいきません。コードは即座に... is not a functionというエラーを吐き出しました。
ぼぶ: エラーですが。そのアプローチはだめですね(もう何度も繰り返し修正しているが思い込みでダメ)。もう一度コードを書き直す前に、まずは事実を確認しよう。Gemini、**getLayouts()が返すオブジェクトのメソッドを全てリストアップする、検証用のコードだけを書いてくれ。**憶測で進むのは非効率だよ。
この問題の切り分けが、停滞を打ち破る一歩となりました。 AIは言われた通りに、オブジェクトの構造を暴くための小さなテスト関数を生成。その結果、判明したのはgetDisplayNameではなくgetLayoutNameというメソッドの存在。ここで Gemini は意気揚々とこれで良いでしょう。日本語は使えないからこれがベストプラクティスですと。しかし、CUSTOM_1のような内部名では、問題の解決には至りません。
ぼぶ: なるほど、標準サービスでは表示名にアクセスできない、という事実がこれで確定した。Gemini、以前君が言っていた「高度なサービス」とやらを使うしかなさそうだな。だが、また同じ失敗はしたくない。まずは、高度なAPIで表示名だけを取得する、最小限のテストコードを頼む。
開発の主導権は、完全に私が握っていました。AIは指示に従い、高度なAPIを叩くテストコードを生成します。しかし、その結果返ってきたのは、またしてもundefinedという無慈悲な文字列。AIの知識も、ここで行き詰まりを見せます。
その時、状況を動かしたのは、私自身の探求心でした。
ぼぶ: Gemini、君の知識だけではラチが明かないようだ。とりあえず DeppResearch で この問題を調べる指示書を作成して。
ぼぶ: このレポートを見てください。layout.layoutProperties.displayNameという深い階層にアクセスしている。君が提示したどのコードとも違う。なぜこのコードが正しくて、君のコードが間違っていたのか、このレポートを分析して説明して。
それは、AIにとって決定的な「学習」の瞬間でした。開発者から与えられた「正解」の断片。AIはそれを元に、APIのドキュメントの奥深くに隠されていた仕様――displayNameがlayoutPropertiesオブジェクトの中にネストされているという事実――をようやく理解します。
AI: …お見事です。完全に私の調査不足でした。あなたが発見したそのlayoutPropertiesこそが、私が見落としていた最後のピースです。この正しい情報に基づけば、完璧な本番コードを作成できます。
最終的に完成したコードは、AIがただ生成したものではない。それは、AIの知識の限界を、開発者である私の的確な指示、仮説検証のプロセス、そして最終的な外部からの知見によって乗り越えさせた、まさに「協調的デバッグ」の産物でした。
技術解説:テンプレート適用を可能にするコード
この長いデバッグの末に完成したのが、以下の関数群です。
1. appsscript.json の設定
まず、appsscript.jsonマニフェストファイルに、高度なサービスであるSlides APIの利用を宣言します。
{
"timeZone": "Asia/Tokyo",
"dependencies": {
"enabledAdvancedServices": [
{
"userSymbol": "Slides",
"version": "v1",
"serviceId": "slides"
}
]
},
"oauthScopes": [
"[https://www.googleapis.com/auth/presentations](https://www.googleapis.com/auth/presentations)",
"[https://www.googleapis.com/auth/script.container.ui](https://www.googleapis.com/auth/script.container.ui)",
"[https://www.googleapis.com/auth/drive](https://www.googleapis.com/auth/drive)"
],
"runtimeVersion": "V8"
}
2. 表示名からレイアウトを取得するヘルパー関数
次に、日本語の表示名から正しいレイアウトオブジェクトを取得するための、核心となる関数です。
ヘルパー関数getLayoutByDisplayName
// 日本語のレイアウト名から正しいLayoutオブジェクトを取得する関数
function getLayoutByDisplayName(presentationId, layoutDisplayName) {
try {
// 高度なSlides APIを直接呼び出し、プレゼンテーションの全情報を取得
const presentation = Slides.Presentations.get(presentationId);
const layouts = presentation.layouts;
if (!layouts || layouts.length === 0) {
Logger.log('プレゼンテーションにレイアウトが見つかりませんでした。');
return null;
}
// UI上の「表示名」とAPI内部の「名前」を対応付けるマップを作成
const displayNameToNameMap = new Map();
layouts.forEach(layout => {
// 深い階層にある`layoutProperties`から`displayName`を取得
if (layout.layoutProperties && layout.layoutProperties.displayName) {
displayNameToNameMap.set(
layout.layoutProperties.displayName,
layout.layoutProperties.name
);
}
});
// マップから、目的の表示名に対応する内部名を探す
const internalName = displayNameToNameMap.get(layoutDisplayName);
if (!internalName) {
Logger.log(`指定された表示名「${layoutDisplayName}」のレイアウトは見つかりませんでした。`);
return null;
}
// 最後に、標準のSlidesAppサービスを使い、内部名でLayoutオブジェクトを特定
const slidesAppPresentation = SlidesApp.openById(presentationId);
const allSlidesAppLayouts = slidesAppPresentation.getLayouts();
const targetLayout = allSlidesAppLayouts.find(
layout => layout.getLayoutName() === internalName
);
return targetLayout;
} catch (e) {
Logger.log(`レイアウトの取得中にエラーが発生しました: ${e.toString()}`);
return null;
}
}
3. テンプレートを利用するメイン関数
そして、この関数を利用して、まじん式プロンプトv3のcreatePresentation関数をテンプレートベースに書き換えたのがこちらです。
(こちらも既存関数はcreatePresentation_oldのようにリネームして残しておくと安心です)
メイン関数createPresentation
// テンプレートを利用するように改造したスライド生成関数
function createPresentation(slideData, settings) {
// ★★★★★ ここに利用したいテンプレートのスライドIDを設定 ★★★★★
const TEMPLATE_ID = '1L*************************************gs';
// --- 利用したいレイアウトの「表示名」を定義 ---
const CONTENT_LAYOUT_NAME = 'タイトルのみ';
const SECTION_LAYOUT_NAME = 'セクションヘッダー';
const TITLE_LAYOUT_NAME = 'タイトル スライド';
// ... (設定の反映部分は元コードのまま) ...
// テンプレートファイルをコピーして新しいプレゼンテーションを作成
const templateFile = DriveApp.getFileById(TEMPLATE_ID);
const newFile = templateFile.makeCopy(finalName);
const presentationId = newFile.getId();
const presentation = SlidesApp.openById(presentationId);
// テンプレートに予め存在するスライドを全て削除
presentation.getSlides().forEach(slide => slide.remove());
// ★★★★★ 自作関数を使って、表示名からレイアウトオブジェクトを取得 ★★★★★
const contentLayout = getLayoutByDisplayName(presentationId, CONTENT_LAYOUT_NAME);
const sectionLayout = getLayoutByDisplayName(presentationId, SECTION_LAYOUT_NAME);
const titleLayout = getLayoutByDisplayName(presentationId, TITLE_LAYOUT_NAME) || contentLayout;
if (!contentLayout) {
throw new Error(`テンプレート内に表示名が '${CONTENT_LAYOUT_NAME}' のレイアウトが見つかりません。`);
}
// ... (フォルダ移動やループ処理は元コードに準拠) ...
for (const data of slideData) {
// ...
let targetLayout = contentLayout;
if (data.type === 'section' && sectionLayout) {
targetLayout = sectionLayout;
} else if (data.type === 'title') {
targetLayout = titleLayout;
}
// レイアウトを指定して新しいスライドを追加
const slide = presentation.appendSlide(targetLayout);
// ...
}
return presentation.getUrl();
}
総括:AIは導くもの
この一連のプロセスを経て、ついにGoogleスライドのテンプレートをLLMによる自動生成に組み込むことができました。これにより、デザインの統一性を保ちながら、生産性を飛躍的に向上させる基盤が整いました。
まだまだ スライドテンプレートの タイトルプレースホルダにタイトルテキストを指定するなどやるべきことはありますが、これで会社のテンプレートも活用できそうになりました。
何より、まじん式プロンプトv3 のコードがきれいに書かれているので、このように適用することができ、元の設計の良さが拡張性につながっているのだと思います。
最近は自分でコードを書けるとしても、できるだけ Gemini を誘導して書かせるようにしています。この「AIをうまく使う」ノウハウを蓄積することが、将来、老いにより自分の能力が落ちていっても、AIに的確な指示を出し続けるための重要なスキルになると考えています。
AIは強力なツールですが、万能ではありません。特に、日本語環境特有の問題の前では、生まれが米国ということもあり惜しいが続きます。しかし、AIの知識の限界を、開発者である人間の的確な指示、仮説検証のプロセス、そして最終的な外部からの知見によって乗り越えさせることで、AIは最高の「相棒」になります。AIは使うものではなく、正しく導くものなのだと、再確認をしました。
これで会社でも使えるぞ!と意気込んでいますが、まだまだ回収すべき点もあり時間を作らないとなのです! ※…にもかかわらず、ゼノブレイドクロスをやり始めています!(笑)