CONTENT
ここから
入力したテーマから、コンセプト・ターゲット・見せ方を自動生成する
この教材では、Google Apps ScriptのWebアプリで、企画書メーカーを作ります。
前回は、GASからGemini APIを呼び出して、AI文章を生成しました。
今回は、その仕組みを少し実用的にして、入力したテーマから短い企画書を自動で作れるようにします。
難しいことはしません。
今日作るのは、これです。
テーマを入れる
↓
ボタンを押す
↓
AIが企画書を作る
↓
画面に表示する
↓
スプレッドシートに保存する
企画書は、最初から完璧でなくて大丈夫です。
大切なのは、頭の中にある「なんとなく」を、まず言葉にすることです。
このページで完成するもの
今回は、次のようなアプリを作ります。
| 項目 | 内容 |
|---|---|
| 入力 | テーマ、ジャンル、届けたい相手、雰囲気 |
| AI生成 | 企画タイトル、ターゲット、コンセプト、見せ方 |
| 表示 | 生成結果をWeb画面に表示 |
| 保存 | スプレッドシートに履歴を保存 |
このアプリができると、次のような使い方ができます。
学校祭の紹介サイトを作りたい
↓
企画書メーカーに入力
↓
ターゲット・コンセプト・見せ方が出る
↓
画像生成やLP制作に進める
1. 今回の完成イメージ
たとえば、画面にこう入力します。
テーマ:
学校祭をもっと楽しむための案内サイト
ジャンル:
学校イベント
届けたい相手:
初めて学校祭に来る高校生や保護者
雰囲気:
明るく、親しみやすい
ボタンを押すと、AIが次のような企画書を作ります。
企画タイトル:
はじめての学校祭ナビ
ターゲット:
学校祭に初めて来る高校生や保護者
コンセプト:
初めて来た人でも、迷わず楽しく回れる学校祭案内サイト
見せ方:
明るく、地図や写真を使って分かりやすく見せる
これだけでも、企画はかなり前に進みます。
2. 作るファイル
今回も、使うファイルは2つだけです。
Code.gs
index.html
| ファイル | 役割 |
|---|---|
| Code.gs | AI APIを呼び出す、保存する |
| index.html | 入力画面と表示画面を作る |
3. 事前準備
まず、Googleスプレッドシートを作ります。
https://sheets.google.com/
新しいスプレッドシートを作成し、ファイル名を次にします。
AI企画書メーカー
次に、スプレッドシートの上部メニューからApps Scriptを開きます。
拡張機能
↓
Apps Script
Apps Scriptのプロジェクト名も、次にします。
AI企画書メーカー
4. APIキーを確認する
前回と同じように、Gemini APIキーを使います。
まだAPIキーを保存していない場合は、Apps Scriptの左側にある歯車アイコンを押します。
プロジェクトの設定
↓
スクリプト プロパティ
↓
スクリプト プロパティを追加
次のように設定します。
| 項目 | 入力 |
|---|---|
| プロパティ | GEMINI_API_KEY |
| 値 | 自分のGemini APIキー |
APIキーは人に見せないでください。
コードに直接書かず、スクリプトプロパティに保存します。
5. Code.gsを貼り付ける
Apps Scriptの Code.gs を開き、すべて消してから、次のコードを貼り付けます。
const SHEET_NAME = '企画書履歴';
const MODEL_NAME = 'gemini-2.5-flash';
function doGet() {
setupSheet_();
return HtmlService
.createHtmlOutputFromFile('index')
.setTitle('AI企画書メーカー')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function generatePlan(formData) {
validateFormData_(formData);
setupSheet_();
const prompt = buildPlanPrompt_(formData);
const result = callGemini_(prompt);
saveHistory_(formData, result);
return {
ok: true,
text: result,
message: '企画書を作成しました。'
};
}
function buildPlanPrompt_(formData) {
return `
あなたは、学生の企画制作を手伝うやさしい編集者です。
次の情報をもとに、短くて分かりやすい企画書を作ってください。
【テーマ】
${formData.theme}
【ジャンル】
${formData.genre}
【届けたい相手】
${formData.target}
【雰囲気】
${formData.tone}
【やってみたいこと】
${formData.memo || '未入力'}
以下の形式で出してください。
1. 企画タイトル
2. ターゲット
3. コンセプト
4. 伝えたいこと
5. 見せ方の雰囲気
6. 画像にするとよい場面
7. 動画にするとよい場面
8. Webサイトに載せる内容
9. 発表で使える一言
条件:
・難しい言葉を使わない
・1項目は短くする
・学生がそのまま発表に使える文章にする
・画像、動画、Webサイト制作に進めやすい内容にする
・少し前向きで、作ってみたくなる言葉にする
`;
}
function callGemini_(prompt) {
const apiKey = PropertiesService
.getScriptProperties()
.getProperty('GEMINI_API_KEY');
if (!apiKey) {
throw new Error('GEMINI_API_KEY が設定されていません。');
}
const url =
'https://generativelanguage.googleapis.com/v1beta/models/' +
MODEL_NAME +
':generateContent?key=' +
encodeURIComponent(apiKey);
const payload = {
contents: [
{
role: 'user',
parts: [{ text: prompt }]
}
],
generationConfig: {
temperature: 0.7,
maxOutputTokens: 1200
}
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, options);
const statusCode = response.getResponseCode();
const responseText = response.getContentText();
if (statusCode !== 200) {
throw new Error('Gemini APIエラー:' + responseText);
}
const json = JSON.parse(responseText);
const text = json.candidates?.[0]?.content?.parts?.[0]?.text;
if (!text) {
throw new Error('AIの回答を取得できませんでした。');
}
return text;
}
function saveHistory_(formData, result) {
const sheet = SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName(SHEET_NAME);
sheet.appendRow([
new Date(),
formData.theme,
formData.genre,
formData.target,
formData.tone,
formData.memo || '',
result
]);
}
function getRecentPlans() {
setupSheet_();
const sheet = SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName(SHEET_NAME);
const lastRow = sheet.getLastRow();
if (lastRow <= 1) {
return [];
}
const values = sheet.getRange(2, 1, lastRow - 1, 7).getValues();
return values
.map((row) => {
return {
createdAt: formatDate_(row[0]),
theme: row[1],
genre: row[2],
target: row[3],
tone: row[4],
result: row[6]
};
})
.reverse()
.slice(0, 5);
}
function setupSheet_() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
sheet = ss.insertSheet(SHEET_NAME);
}
if (sheet.getLastRow() === 0) {
sheet.appendRow([
'作成日時',
'テーマ',
'ジャンル',
'届けたい相手',
'雰囲気',
'メモ',
'企画書'
]);
}
}
function validateFormData_(formData) {
if (!formData) {
throw new Error('入力データがありません。');
}
const requiredFields = [
['theme', 'テーマ'],
['genre', 'ジャンル'],
['target', '届けたい相手'],
['tone', '雰囲気']
];
requiredFields.forEach(([key, label]) => {
if (!formData[key] || String(formData[key]).trim() === '') {
throw new Error(label + 'を入力してください。');
}
});
}
function formatDate_(date) {
if (!date) return '';
return Utilities.formatDate(
new Date(date),
Session.getScriptTimeZone(),
'yyyy/MM/dd HH:mm'
);
}
6. index.htmlを作る
Apps Scriptの左側にある「+」を押します。
+
↓
HTML
ファイル名は次にします。
index
作成された index.html の中身をすべて消して、次のコードを貼り付けます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<base target="_top" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI企画書メーカー</title>
<style>
:root {
--bg: #f6f7fb;
--card: #ffffff;
--text: #111827;
--muted: #6b7280;
--line: #dbe3ee;
--primary: #2563eb;
--primary-dark: #1d4ed8;
--soft: #eff6ff;
--success: #16a34a;
--danger: #dc2626;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: linear-gradient(180deg, #f9fbff 0%, #edf2f8 100%);
color: var(--text);
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
line-height: 1.7;
}
.page {
width: min(1100px, calc(100% - 32px));
margin: 0 auto;
padding: 32px 0 48px;
}
.hero {
padding: 28px;
margin-bottom: 20px;
border: 1px solid var(--line);
border-radius: 24px;
background: var(--card);
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
}
.badge {
display: inline-flex;
padding: 6px 12px;
border-radius: 999px;
background: var(--soft);
color: var(--primary);
font-size: 13px;
font-weight: 800;
}
h1 {
margin: 18px 0 8px;
font-size: clamp(28px, 5vw, 44px);
line-height: 1.15;
letter-spacing: -0.04em;
}
.lead {
margin: 0;
color: var(--muted);
}
.grid {
display: grid;
grid-template-columns: 0.85fr 1.15fr;
gap: 20px;
}
.card {
border: 1px solid var(--line);
border-radius: 24px;
background: var(--card);
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.06);
overflow: hidden;
}
.card-header {
padding: 20px 22px;
border-bottom: 1px solid var(--line);
background: #fbfdff;
}
.card-header h2 {
margin: 0;
font-size: 20px;
}
.card-header p {
margin: 4px 0 0;
color: var(--muted);
font-size: 14px;
}
form {
padding: 22px;
}
label {
display: block;
margin-bottom: 14px;
font-weight: 800;
}
.hint {
display: block;
margin: 2px 0 6px;
color: var(--muted);
font-size: 13px;
font-weight: 500;
}
input,
textarea,
select {
width: 100%;
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px 14px;
background: #ffffff;
color: var(--text);
font: inherit;
outline: none;
}
textarea {
min-height: 96px;
resize: vertical;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12);
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 20px;
}
button {
border: 0;
border-radius: 14px;
padding: 12px 18px;
cursor: pointer;
font: inherit;
font-weight: 900;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.primary {
background: var(--primary);
color: #ffffff;
}
.primary:hover {
background: var(--primary-dark);
}
.secondary {
background: #eef2f7;
color: #111827;
}
.message {
margin: 0 22px 22px;
padding: 12px 14px;
border-radius: 14px;
display: none;
font-weight: 800;
}
.message.success {
display: block;
background: #ecfdf5;
color: var(--success);
}
.message.error {
display: block;
background: #fef2f2;
color: var(--danger);
}
.result {
padding: 22px;
}
.result-box {
min-height: 430px;
padding: 20px;
border-radius: 18px;
background: #111827;
color: #f9fafb;
white-space: pre-wrap;
overflow-wrap: anywhere;
font-size: 15px;
}
.result-empty {
color: #9ca3af;
}
.loading {
display: none;
padding: 14px;
margin: 0 22px 22px;
border-radius: 14px;
background: #fffbeb;
color: #92400e;
font-weight: 800;
}
.loading.show {
display: block;
}
.recent {
padding: 0 22px 22px;
}
.recent-item {
padding: 14px 0;
border-top: 1px solid var(--line);
}
.recent-item:first-child {
border-top: 0;
}
.recent-title {
margin: 0;
font-weight: 900;
}
.recent-meta {
margin: 2px 0 0;
color: var(--muted);
font-size: 13px;
}
.small-button {
margin-top: 8px;
padding: 8px 12px;
border-radius: 10px;
background: #eef2f7;
color: #111827;
font-size: 13px;
}
.empty {
color: var(--muted);
font-size: 14px;
}
@media (max-width: 920px) {
.grid {
grid-template-columns: 1fr;
}
.hero {
padding: 22px;
}
}
</style>
</head>
<body>
<main class="page">
<section class="hero">
<span class="badge">6-4 企画書メーカー</span>
<h1>テーマから<br />企画書を作る</h1>
<p class="lead">
まだぼんやりしたアイデアを、AIと一緒に「伝わる企画」に整えます。
</p>
</section>
<section class="grid">
<div class="card">
<div class="card-header">
<h2>企画の材料を入力</h2>
<p>短くて大丈夫です。まずは仮で進めましょう。</p>
</div>
<form id="planForm">
<label>
テーマ
<span class="hint">例:学校祭をもっと楽しむための案内サイト</span>
<input
id="theme"
type="text"
placeholder="作りたい企画のテーマ"
required
/>
</label>
<label>
ジャンル
<span class="hint">近いものを選んでください</span>
<select id="genre" required>
<option value="">選択してください</option>
<option value="学校イベント">学校イベント</option>
<option value="架空のお店">架空のお店</option>
<option value="作品展示">作品展示</option>
<option value="地域イベント">地域イベント</option>
<option value="アプリ紹介">アプリ紹介</option>
<option value="その他">その他</option>
</select>
</label>
<label>
届けたい相手
<span class="hint">例:初めて学校祭に来る高校生や保護者</span>
<input
id="target"
type="text"
placeholder="誰に届けたいか"
required
/>
</label>
<label>
雰囲気
<span class="hint">迷ったら「明るい・親しみやすい」でOK</span>
<select id="tone" required>
<option value="">選択してください</option>
<option value="明るい・親しみやすい">明るい・親しみやすい</option>
<option value="おしゃれ・落ち着いた">おしゃれ・落ち着いた</option>
<option value="かわいい・やさしい">かわいい・やさしい</option>
<option value="かっこいい・シャープ">かっこいい・シャープ</option>
<option value="シンプル・分かりやすい">シンプル・分かりやすい</option>
</select>
</label>
<label>
やってみたいこと
<span class="hint">例:来た人が迷わず楽しめるようにしたい</span>
<textarea
id="memo"
placeholder="思いついたことを自由に書く"
></textarea>
</label>
<div class="actions">
<button class="primary" type="submit" id="generateButton">
企画書を作る
</button>
<button class="secondary" type="button" id="sampleButton">
サンプル入力
</button>
<button class="secondary" type="button" id="clearButton">
クリア
</button>
</div>
</form>
<div id="loadingBox" class="loading">
AIが企画書を作っています。少し待ってください。
</div>
<p id="messageBox" class="message"></p>
</div>
<div class="card">
<div class="card-header">
<h2>生成された企画書</h2>
<p>この文章をもとに、画像・動画・LP制作へ進みます。</p>
</div>
<div class="result">
<div id="resultBox" class="result-box">
<span class="result-empty">ここに企画書が表示されます。</span>
</div>
<div class="actions">
<button class="secondary" type="button" id="copyButton">
企画書をコピー
</button>
</div>
</div>
<div class="card-header">
<h2>最近作った企画書</h2>
<p>最新5件を表示します。</p>
</div>
<div class="recent" id="recentPlans">
<p class="empty">まだ履歴がありません。</p>
</div>
</div>
</section>
</main>
<script>
const form = document.getElementById('planForm');
const generateButton = document.getElementById('generateButton');
const sampleButton = document.getElementById('sampleButton');
const clearButton = document.getElementById('clearButton');
const copyButton = document.getElementById('copyButton');
const loadingBox = document.getElementById('loadingBox');
const messageBox = document.getElementById('messageBox');
const resultBox = document.getElementById('resultBox');
const fields = {
theme: document.getElementById('theme'),
genre: document.getElementById('genre'),
target: document.getElementById('target'),
tone: document.getElementById('tone'),
memo: document.getElementById('memo')
};
form.addEventListener('submit', (event) => {
event.preventDefault();
const formData = getFormData();
setLoading(true);
showMessage('', '');
google.script.run
.withSuccessHandler((result) => {
setLoading(false);
resultBox.textContent = result.text;
showMessage(result.message || '企画書を作成しました。', 'success');
loadRecentPlans();
})
.withFailureHandler((error) => {
setLoading(false);
showMessage(error.message || '企画書の作成に失敗しました。', 'error');
})
.generatePlan(formData);
});
sampleButton.addEventListener('click', () => {
fields.theme.value = '学校祭をもっと楽しむための案内サイト';
fields.genre.value = '学校イベント';
fields.target.value = '初めて学校祭に来る高校生や保護者';
fields.tone.value = '明るい・親しみやすい';
fields.memo.value =
'どこに行けばよいか迷わないように、見どころや回り方を分かりやすく紹介したい。';
showMessage('サンプルを入力しました。自由に書き換えてください。', 'success');
});
clearButton.addEventListener('click', () => {
form.reset();
resultBox.innerHTML =
'<span class="result-empty">ここに企画書が表示されます。</span>';
showMessage('', '');
});
copyButton.addEventListener('click', async () => {
const text = resultBox.textContent.trim();
if (!text || text === 'ここに企画書が表示されます。') {
showMessage('コピーする企画書がありません。', 'error');
return;
}
try {
await navigator.clipboard.writeText(text);
showMessage('企画書をコピーしました。', 'success');
} catch (error) {
showMessage('コピーできませんでした。手動で選択してコピーしてください。', 'error');
}
});
function getFormData() {
return {
theme: fields.theme.value.trim(),
genre: fields.genre.value.trim(),
target: fields.target.value.trim(),
tone: fields.tone.value.trim(),
memo: fields.memo.value.trim()
};
}
function setLoading(isLoading) {
generateButton.disabled = isLoading;
generateButton.textContent = isLoading ? '作成中...' : '企画書を作る';
loadingBox.classList.toggle('show', isLoading);
}
function showMessage(text, type) {
if (!text) {
messageBox.textContent = '';
messageBox.className = 'message';
return;
}
messageBox.textContent = text;
messageBox.className = 'message ' + type;
}
function loadRecentPlans() {
google.script.run
.withSuccessHandler(renderRecentPlans)
.withFailureHandler(() => {
document.getElementById('recentPlans').innerHTML =
'<p class="empty">履歴を読み込めませんでした。</p>';
})
.getRecentPlans();
}
function renderRecentPlans(plans) {
const container = document.getElementById('recentPlans');
if (!plans || plans.length === 0) {
container.innerHTML =
'<p class="empty">まだ履歴がありません。</p>';
return;
}
container.innerHTML = plans
.map((item) => {
return `
<div class="recent-item">
<p class="recent-title">${escapeHtml(item.theme)}</p>
<p class="recent-meta">
${escapeHtml(item.createdAt)}|${escapeHtml(item.genre)}|${escapeHtml(item.tone)}
</p>
<button class="small-button" type="button" onclick="showPlan('${encodeURIComponent(item.result || '')}')">
企画書を見る
</button>
</div>
`;
})
.join('');
}
function showPlan(encodedText) {
resultBox.textContent = decodeURIComponent(encodedText);
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
loadRecentPlans();
</script>
</body>
</html>
7. 保存する
コードを貼り付けたら保存します。
Command + S
Windowsの場合は、
Ctrl + S
です。
8. Webアプリとして公開する
右上の「デプロイ」を押します。
デプロイ
↓
新しいデプロイ
種類の選択で、歯車アイコンを押します。
種類を選択
↓
ウェブアプリ
設定は次のようにします。
| 項目 | 設定 |
|---|---|
| 説明 | 6-4 AI企画書メーカー |
| 次のユーザーとして実行 | 自分 |
| アクセスできるユーザー | 自分のみ |
まずは自分だけで動かします。
デプロイ後に表示されるURLを開くと、企画書メーカーが表示されます。
9. 動かしてみる
画面が開いたら、まず「サンプル入力」を押します。
そのあと、次のボタンを押します。
企画書を作る
少し待つと、右側に企画書が表示されます。
スプレッドシートには、自動で 企画書履歴 シートが作られ、生成結果が保存されます。
10. うまくいった時の見方
生成された企画書を見て、まずこの3つだけ確認します。
誰に届けるか
何を伝えるか
どんな雰囲気で見せるか
ここが分かれば成功です。
企画書は、長くなくて大丈夫です。
次の制作に進める「地図」になっていれば十分です。
11. うまく出ない時
文章が長すぎる
Code.gs のプロンプトに、次を追加します。
・各項目は2行以内にする
内容が普通すぎる
プロンプトに、次を追加します。
・学生が見て少しワクワクする言葉にする
画像や動画に展開しにくい
プロンプトに、次を追加します。
・画像にできる場面と、動画にできる場面を具体的にする
まずはコード全体ではなく、プロンプトだけを直してください。
12. 今日の提出物
この回の提出物は、次の3つです。
1. WebアプリURL
2. 生成された企画書のスクリーンショット
3. スプレッドシートの企画書履歴のスクリーンショット
最低限、次ができればOKです。
テーマを入力して、
AIが企画書を作れた
13. チェックリスト
| チェック | 内容 |
|---|---|
| スプレッドシートを作った | |
| Apps Scriptを開いた | |
| GEMINI_API_KEYを設定した | |
| Code.gsを貼り付けた | |
| index.htmlを作った | |
| index.htmlを貼り付けた | |
| Webアプリとしてデプロイした | |
| WebアプリURLを開いた | |
| サンプル入力を押した | |
| 企画書を作るを押した | |
| 企画書が画面に表示された | |
| スプレッドシートに履歴が保存された |
まとめ
今回は、テーマから企画書を自動生成する企画書メーカーを作りました。
やっていることは、とてもシンプルです。
入力する
↓
AIに渡す
↓
企画書になる
↓
保存する
企画は、最初からきれいな言葉になっていなくて大丈夫です。
少しぼんやりしたアイデアでも、AIに渡すことで、
ターゲット、コンセプト、見せ方が見えるようになります。
今日作った企画書メーカーは、次の制作の入口です。
企画書
↓
画像プロンプト
↓
動画台本
↓
LP構成
次の回では、この企画書をもとに、画像生成で使えるプロンプトを自動で作るアプリに進みます。