はじめに
みなさんは コットン西村さん をご存知でしょうか。
お笑いコンビ・コットンの西村さんが毎朝Xに投稿する「衣装予報」が人気です。
【まだ家を出てない人へ】 今日は今13℃昼18℃夜14℃と
「肌寒いが昼はちょうどいい日」です。服はロンT/カットソー
トレンチコート/レザージャケット/ナイロンパーカーなどでOKです。
寒がりはカーディガンやベストを中に追加。折りたたみ傘を忘れずに。
僕を信じてください。
このテンション・この文体・この「僕を信じてください。」——これを毎朝LINEに自動で届けてくれるBotを作りました。
使った技術はこの4つだけ。全部無料(ほぼ)です。
- Open-Meteo:天気・気温データ取得(完全無料・APIキー不要)
- Gemini 2.5 Flash-Lite API:コットン西村風テキスト生成(無料枠内)
- Google Apps Script:全体の実行基盤(無料)
- LINE Messaging API:個人LINEへの通知送信(無料枠内)
完成イメージ
毎朝7時ごろ、こんな感じのメッセージがLINEに届きます。
【まだ家を出てない人へ】 今日は今8℃昼16℃夜11℃と
「朝だけ冬で昼以降は春っぽい日」です。服は長袖シャツ+カーディガン/
肌着+スウェット MA-1/チェスターコートなど
[昼以降]トレンチコート/レザージャケットなど。洗濯日和。
僕を信じてください。
システム構成
[Open-Meteo API] → [Google Apps Script] → [Gemini API] → [LINE]
天気・気温取得 毎朝7時に自動実行 文章生成 個人通知
GAS内だけで完結するので、サーバー不要・維持費ほぼゼロです。
実装のポイント
1. 天気データの取得
Open-MeteoはAPIキー不要で東京の気温・天気・降水確率を取得できます。hourly(時間別)データを使うことで、朝7時・昼13時・夜19時の気温を個別に取得しています。
function getWeather() {
const url = `https://api.open-meteo.com/v1/forecast`
+ `?latitude=35.6895&longitude=139.6917`
+ `&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,weathercode`
+ `&hourly=temperature_2m`
+ `&timezone=Asia%2FTokyo`
+ `&forecast_days=1`;
const res = UrlFetchApp.fetch(url);
const data = JSON.parse(res.getContentText());
const h = data.hourly;
return {
morningTemp: Math.round(h.temperature_2m[7]), // 朝7時
noonTemp: Math.round(h.temperature_2m[13]), // 昼13時
eveningTemp: Math.round(h.temperature_2m[19]), // 夜19時
// ...
};
}
1日フラットな日(寒暖差5℃未満)は「ずっと◯〜◯℃」、寒暖差がある日は「今◯℃昼◯℃夜◯℃」と自動で切り替えます。
2. 服の語彙リスト(温度帯別)
コットン西村さんの投稿を分析すると、気温帯ごとに登場するアイテムが決まっています。これをコードに落とし込みました。
function getWardrobeVocabulary(maxTemp) {
if (maxTemp >= 15 && maxTemp < 18) {
return {
level: '肌寒い',
inner: ['パーカー', 'スウェット', 'ロンT', '長袖シャツ', 'ニット'],
outer: ['ナイロンジャケット', 'ジップパーカー', 'トラックジャケット',
'トレンチコート', 'レザージャケット'],
bottoms: ['デニム', 'チノパン'],
note: '薄着厳禁。',
};
}
// ...(他の温度帯も同様)
}
Geminiはこのリストから自然な組み合わせを選んで投稿文を生成するので、毎日少しバリエーションが出ます。
3. プロンプト設計(few-shot)
文体の再現が一番の肝です。実際の投稿例を複数本プロンプトに突っ込んでいます。
const prompt = `
あなたは東京の天気・服装情報を毎朝投稿する人気アカウントです。
以下の実例を完全にコピーしたフォーマット・文体・テンションで、
今日の投稿文を1つだけ作ってください。
【実例集】
例A)【まだ家を出てない人へ】 今日はずっと14〜16℃と
「1日ずっとちょこっと肌寒いが続く日」です。...僕を信じてください。
// ...(季節ごとに複数例)
【絶対に守るルール】
1. 必ず「【まだ家を出てない人へ】 今日は」で始める
2. 気温は「${tempExpression}」の形式で入れる
3. 「〇〇な日」でキャッチコピーを付ける
// ...
8. 必ず「僕を信じてください。」で締める
`;
花粉シーズン(2〜4月)のみ花粉コメントを入れるという細かいルールも実装しています。
4. 季節フラグ管理
const month = new Date().getMonth() + 1;
const isPollenSeason = month >= 2 && month <= 4; // 花粉:2〜4月
const isTsuyu = month >= 6 && month <= 7; // 梅雨:6〜7月
季節に応じてプロンプトへのヒントを切り替えます。
ハマりどころ
実際に構築してみて詰まったポイントを記録しておきます。
① LINE DevelopersからMessaging APIを直接作成できなくなっていた
仕様変更により、LINE DevelopersコンソールからMessaging APIチャンネルを直接作成できなくなっています。上記の手順通り、LINE公式アカウントを先に作成してからMessaging APIを有効化してください。
② Geminiのモデル名は公式で確認する
コードに gemini-2.0-flash と書いたら404エラー。gemini-2.0-flash-lite に変えても同じく404でした。Geminiのモデルは追加・非推奨化が頻繁に起きるので、動作しない場合は以下の公式ページで現在利用可能なモデルを確認してください。
- モデル一覧:https://ai.google.dev/gemini-api/docs/models
- 料金・無料枠:https://ai.google.dev/gemini-api/docs/pricing
③ Open-MeteoはGASのIP共有問題で429が出ることがある
Open-Meteoは無料・APIキー不要で使えますが、GASのサーバーはIPが他ユーザーと共有されています。他のユーザーが大量リクエストを送ったタイミングと重なると、自分のリクエスト数に関係なく429エラーに巻き込まれることがあります。
対策としてOpenWeatherMapに切り替えました。APIキー認証なのでIP共有の影響を受けません。無料枠は月1,000,000コールで個人利用では十分です。
// スクリプトプロパティに追加
OWM_API_KEY: OpenWeatherMapで発行したAPIキー
④ OpenWeatherMapのAPIキーは発行後2時間ほど待つ必要がある
キーを発行してすぐ使うと401エラーが返ってきます。有効化に最大2時間かかるので、登録後はしばらく待ってから実行してください。
コード全文
// ============================================================
// 🧥 衣装予報士くん Bot (powered by Gemini + Open-Meteo)
// ============================================================
const CONFIG = {
LINE_CHANNEL_ACCESS_TOKEN: PropertiesService.getScriptProperties().getProperty('LINE_TOKEN'),
LINE_USER_ID: PropertiesService.getScriptProperties().getProperty('LINE_USER_ID'),
GEMINI_API_KEY: PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY'),
LATITUDE: 35.6895,
LONGITUDE: 139.6917,
};
function main() {
try {
const weather = getWeather();
const comment = generateComment(weather);
sendLine(comment);
Logger.log('✅ 送信完了:\n' + comment);
} catch (e) {
Logger.log('❌ エラー: ' + e.message);
}
}
function getWeather() {
const url = `https://api.open-meteo.com/v1/forecast`
+ `?latitude=${CONFIG.LATITUDE}&longitude=${CONFIG.LONGITUDE}`
+ `&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,weathercode`
+ `&hourly=temperature_2m`
+ `&timezone=Asia%2FTokyo`
+ `&forecast_days=1`;
const res = UrlFetchApp.fetch(url);
const data = JSON.parse(res.getContentText());
const d = data.daily;
const h = data.hourly;
const morningTemp = Math.round(h.temperature_2m[7]);
const noonTemp = Math.round(h.temperature_2m[13]);
const eveningTemp = Math.round(h.temperature_2m[19]);
const maxTemp = d.temperature_2m_max[0];
const minTemp = d.temperature_2m_min[0];
const tempRange = maxTemp - minTemp;
const isFlat = tempRange < 5;
const month = new Date().getMonth() + 1;
const isPollenSeason = month >= 2 && month <= 4;
const isTsuyu = month >= 6 && month <= 7;
const code = d.weathercode[0];
const isSunny = code <= 3;
return {
maxTemp, minTemp, morningTemp, noonTemp, eveningTemp,
tempRange, isFlat,
rainProb: d.precipitation_probability_max[0],
weatherCode: code,
weatherLabel: decodeWeatherCode(code),
isPollenSeason, isTsuyu, isSunny, month,
};
}
function decodeWeatherCode(code) {
if (code === 0) return '快晴';
if (code <= 2) return '晴れ';
if (code === 3) return '曇り';
if (code >= 45 && code <= 48) return '霧';
if (code >= 51 && code <= 57) return '霧雨';
if (code >= 61 && code <= 67) return '雨';
if (code >= 71 && code <= 77) return '雪';
if (code >= 80 && code <= 82) return 'にわか雨';
if (code >= 95) return '雷雨';
return '不明';
}
function getWardrobeVocabulary(maxTemp) {
if (maxTemp >= 30) {
return { level: '真夏日', inner: ['半袖', 'Tシャツ'], outer: ['長袖シャツ', '薄手のカーディガン', 'UVカットパーカー'], bottoms: ['ショートパンツ', '短パン', 'サンダル'], note: '真夏日。冷房対策として長袖シャツや薄手のカーディガンを鞄に。' };
} else if (maxTemp >= 27) {
return { level: '夏本番', inner: ['半袖', 'Tシャツ', '半袖シャツ'], outer: ['長袖シャツ', '薄手のカーディガン', 'UVカットパーカー', 'リネンシャツ'], bottoms: ['ショートパンツ', '短パン', 'チノパン', 'サンダルOK'], note: '朝or冷房対策に薄手の羽織りをカバンに。' };
} else if (maxTemp >= 23) {
return { level: '夏っぽい陽気', inner: ['半袖', 'Tシャツ', '半袖シャツ', 'ポロシャツ'], outer: ['デニムジャケット', 'トラックジャケット', '長袖シャツ', '薄手のカーディガン', 'リネンシャツ', 'ナイロンパーカー'], bottoms: ['チノパン', 'デニム', '暑がりは短パンも'], note: null };
} else if (maxTemp >= 20) {
return { level: '過ごしやすい', inner: ['半袖', 'Tシャツ', 'ロンT', '長袖シャツ'], outer: ['トラックジャケット', 'デニムジャケット', '長袖シャツ', '薄手のカーディガン', 'ジップパーカー', 'ナイロンパーカー'], bottoms: ['チノパン', 'デニム'], note: null };
} else if (maxTemp >= 18) {
return { level: '春らしい', inner: ['ロンT', '長袖シャツ', 'カットソー'], outer: ['トレンチコート', 'レザージャケット', 'トラックジャケット', 'ナイロンパーカー', 'デニムジャケット', 'ジップパーカー'], bottoms: ['デニム', 'チノパン'], note: null };
} else if (maxTemp >= 15) {
return { level: '肌寒い', inner: ['パーカー', 'スウェット', 'ロンT', '長袖シャツ', 'カットソー', 'ニット'], outer: ['ナイロンジャケット', 'ジップパーカー', 'トラックジャケット', 'トレンチコート', 'レザージャケット', 'ナイロンパーカー'], bottoms: ['デニム', 'チノパン'], note: '薄着厳禁。' };
} else if (maxTemp >= 12) {
return { level: 'しっかり肌寒い', inner: ['長袖シャツ', 'ロンT', 'カットソー', 'ベスト', 'カーディガン'], outer: ['トレンチコート', 'ナイロンパーカー', 'レザージャケット', 'マウンテンパーカー', 'ジップパーカー'], bottoms: ['デニム', 'チノパン'], note: 'アウター必要。寒がりは中にカーディガン/ベストを追加。' };
} else if (maxTemp >= 9) {
return { level: '寒い', inner: ['長袖シャツ+カーディガン', 'Tシャツ+スウェット', '肌着+ニット', 'パーカー', 'フリース', 'タートルネック'], outer: ['チェスターコート', 'マウンテンパーカー', 'フリースジャケット', 'MA-1', 'ライトダウン', 'トレンチコート(厚手)'], bottoms: ['デニム', '裏起毛パンツ'], note: null };
} else if (maxTemp >= 6) {
return { level: 'かなり寒い', inner: ['長袖シャツ+カーディガン', '肌着+スウェット', 'フリース', 'タートルネック', 'ニット'], outer: ['チェスターコート', 'MA-1', 'ダウンジャケット', 'ダウンベスト+コート', 'マウンテンパーカー'], bottoms: ['デニム', '裏起毛パンツ'], note: '朝晩マフラーや手袋を追加で防寒を。' };
} else {
return { level: '真冬レベル', inner: ['長袖シャツ+カーディガン', '肌着+スウェット', 'タートルネック', 'ニット+フリース', '重ね着'], outer: ['ダウンジャケット', 'チェスターコート', 'MA-1', 'ムートンコート', 'ウールコート'], bottoms: ['デニム(裏起毛推奨)', '裏起毛パンツ', 'ウールパンツ'], note: '朝晩マフラーや手袋を追加で防寒を。' };
}
}
function generateComment(weather) {
const vocab = getWardrobeVocabulary(weather.maxTemp);
const needUmbrella = weather.rainProb >= 40;
const rainAll = weather.rainProb >= 70;
const tempExpression = weather.isFlat
? `ずっと${weather.minTemp}〜${weather.maxTemp}℃`
: `今${weather.morningTemp}℃昼${weather.noonTemp}℃夜${weather.eveningTemp}℃`;
let extraHint = '';
if (weather.isPollenSeason) {
extraHint = '花粉シーズン(2〜4月)なので花粉量についてユーモアを交えた一言を必ず入れる(例:「鼻もげる花粉量」「花粉少なめバンザイ」)。';
} else if (weather.isTsuyu) {
extraHint = '梅雨シーズン(6〜7月)なので梅雨感のある一言を適宜入れてもよい。';
} else if (weather.isSunny && weather.rainProb < 30) {
extraHint = '晴天なので「洗濯日和」などを適宜入れてもよい(必須ではない)。';
} else {
extraHint = '特別なコメントは不要。傘情報だけ入れれば十分。';
}
const prompt = `
あなたは東京の天気・服装情報を毎朝投稿する人気アカウントです。
以下の実例を完全にコピーしたフォーマット・文体・テンションで、今日の投稿文を1つだけ作ってください。
【実例集】
例A)【まだ家を出てない人へ】 今日はずっと14〜16℃と「1日ずっとちょこっと肌寒いが続く日」です。服はロンT/長袖シャツ(寒がり+カーディガン/ベスト)トレンチコート/レザージャケット/ナイロンパーカーなどでOKです。折りたたみ傘はマストで。花粉はそこまでです。僕を信じてください。
例B)【まだ家を出てない人へ】 今日は今4℃昼12℃夜7℃と「朝晩べらぼうに寒く昼少し和らぐ日」です。服は長袖シャツ+カーディガン/Tシャツ+スウェット/肌着+ニットチェスターコート/マウンテンパーカー/フリースジャケットなどの冬アウターでOKです。洗濯日和。僕を信じてください。
例C)【まだ家を出てない人へ】 今日は今9℃昼20℃夜14℃と「寒暖差エグエグの日」です。服は[朝]スウェット/パーカー トレンチコート/レザージャケットなど [昼以降]ロンT/長袖シャツ デニムジャケット/ナイロンパーカーなどでOK。スーパーお花見日和。鼻もげる花粉量。僕を信じてください。
例D)【まだ家を出てない人へ】 今日はずっと16〜17℃です。春が姿をくらまして寒いので「薄着厳禁」です。服は長袖シャツ/ロンTナイロンジャケット/ジップパーカー/トラックジャケットなどのアウターが絶対に必要です。夕方前まで雨予報なので傘を。僕を信じてください。
例E)【まだ家を出てない人へ】 今日は今25℃昼30℃夜27℃です。今日は真夏日になります。服は半袖で大丈夫です。短パン・サンダルOKです。冷房対策として長袖シャツや薄手のカーディガンを鞄に。念のため折りたたみ傘を。僕を信じてください。
---
【今日の東京の天気データ】
- 気温: ${tempExpression}
- 天気: ${weather.weatherLabel}
- 降水確率: ${weather.rainProb}%
- 日の特徴: ${vocab.level}
- インナー候補: ${vocab.inner.join('/')}
- アウター候補: ${vocab.outer.join('/')}
- ボトムス候補: ${vocab.bottoms.join('/')}
- 補足メモ: ${vocab.note || 'なし'}
- 傘: ${rainAll ? '終日雨(傘必須)' : needUmbrella ? '折りたたみ傘マスト' : '不要'}
- 季節コメントヒント: ${extraHint}
【絶対に守るルール】
1. 必ず「【まだ家を出てない人へ】 今日は」で始める(スペース2つ)
2. 気温は「${tempExpression}」の形式で入れる
3. 「〇〇な日」でキャッチコピーを入れる(夏高温帯など省略することもある)
4. 寒暖差が大きい日(7℃以上)は「※〇〇注意」と「[朝帯]」「[昼以降]」を使う
5. 服の列挙は「/」区切り
6. 傘情報を適切に入れる(不要な日は言及しない)
7. 花粉シーズン(2〜4月)のみ花粉コメントを入れる。それ以外は絶対に花粉に触れない
8. 必ず「僕を信じてください。」で締める
9. 投稿文のみ出力する
10. 全体200〜290文字程度
`;
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent?key=${CONFIG.GEMINI_API_KEY}`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { temperature: 0.85, maxOutputTokens: 600 },
};
const res = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
});
const data = JSON.parse(res.getContentText());
return data.candidates[0].content.parts[0].text.trim();
}
function sendLine(message) {
const url = 'https://api.line.me/v2/bot/message/push';
const payload = {
to: CONFIG.LINE_USER_ID,
messages: [{ type: 'text', text: message }],
};
UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
headers: { 'Authorization': 'Bearer ' + CONFIG.LINE_CHANNEL_ACCESS_TOKEN },
payload: JSON.stringify(payload),
});
}
function testRun() {
main();
}
セットアップ手順
STEP 1:LINE公式アカウントの作成とMessaging API有効化
以前はLINE Developersコンソールから直接Messaging APIチャンネルを作成できましたが、現在はできなくなっています。
-
account.line.bizでLINE公式アカウントを作成 -
manager.line.biz→「設定」→「Messaging API」→「Messaging APIを利用する」 -
developers.line.biz→ 作成されたチャンネル →「チャンネルアクセストークン(長期)」を発行 -
developers.line.biz右上プロフィール →「Your user ID」(Uで始まる)をコピー
STEP 2:Gemini APIキーの取得
-
aistudio.google.comにアクセス - 左メニュー「Get API key」→「APIキーを作成」
STEP 3:GASにコードを貼る
-
script.google.com→「新しいプロジェクト」 - デフォルトのコードを削除して上記コードを貼り付け
-
Ctrl+Sで保存
STEP 4:スクリプトプロパティにAPIキーを登録
GASエディタ左サイドバー「⚙️ プロジェクトの設定」→「スクリプトプロパティ」→「プロパティを追加」
| プロパティ名 | 値 |
|---|---|
LINE_TOKEN |
LINEチャンネルアクセストークン |
LINE_USER_ID |
自分のLINEユーザーID(Uで始まる) |
GEMINI_API_KEY |
Gemini APIキー |
STEP 5:テスト実行
関数を testRun に切り替えて「▶ 実行」。LINEに届けば成功。
STEP 6:毎朝7時のトリガー設定
GASエディタ左サイドバー「⏰ トリガー」→「+ トリガーを追加」
| 項目 | 設定値 |
|---|---|
| 実行する関数 | main |
| イベントのソース | 時間主導型 |
| タイプ | 日タイマー |
| 時刻 | 午前7時〜8時 |
月額コスト
| サービス | 費用 |
|---|---|
| Open-Meteo | ¥0 |
| Gemini 2.5 Flash-Lite | ¥0(1,000リクエスト/日まで) |
| LINE Messaging API | ¥0(月200通まで) |
| Google Apps Script | ¥0 |
| 合計 | ¥0 |
まとめ
- Open-Meteo × Gemini × LINE × GASで月額¥0の服装通知Botが作れます
- few-shotプロンプトで特定の文体を再現するのは思ったより精度が出ます
- Geminiのモデル名は2026年4月時点で
gemini-2.5-flash-liteが正解です(2.0系は終了済み) - LINEのMessaging APIチャンネル作成フローが変わっているので注意
毎朝届く「僕を信じてください。」、なかなか癖になります。ぜひ試してみてください。