はじめに
AIを活用して私生活の管理をさせたいという思いがあったのと最近運動不足であったことも相まって、
GASやGeminiを活用して筋トレのメニュー作成・管理・メールでのリマインドを自動で回す仕組みを作成したので備忘も兼ねて簡単に残します。
仕組み
大まかに以下の要素で構成しています。基本、Googleの製品群で統一したので、要素間の連携は取りやすかったです。
- ナレッジ(Input)
- Google Docs : 筋トレの目標や、私生活のルールなど
- Google Sheets : 生成AIに作成させた筋トレメニューやApp Sheet経由で記載した筋トレのログ
- ロジック
- GAS : Googleドライブ上に保存されたナレッジの情報取得や、Gemini API の呼び出し処理・Gメールでの筋トレメニューや振り返りの送信処理など。トリガーにより自動で実行
- Gemini API : ナレッジの情報をもとに翌週の筋トレメニューや先週の実績からの振り返りをテキストで生成
- UI(OutPut)
- App Sheet : GoogleSheetに記載されている筋トレメニューの可視化と記録のためのUI
- Gmail : 翌週の筋トレメニューや先週の実績からの振り返りを週次で送付
アーキテクチャ
結果
GASをHubにすることでGoogleドライブに保存されたスプレッドシートやドキュメント、GeminiAPIの連携が比較的容易にできたので、本仕組みの実装はスムーズにできました。
スプレッドシートの情報をもとにGemini APIで振り返りやメニュー作成を自動化できたのも、仕組みとしては上手く動作していました。
ただ、個人の問題としてAIから送られてきたメールを無視してしまい、運動習慣としては続かない結果となりました。
ここは技術の問題ではなく、個人の問題なので別の方法で改善することとします。。
参考(各コンポーネントの例)
Brain(GAS)
GASスクリプト※Configファイルは省略
/**
* 日曜日から土曜日までのトレーニング計画を生成し、シートへ展開・メール通知する
* 実行トリガー:毎週土曜日の早朝などに設定
*/
function generateWeeklyTrainingPlanMain() {
const now = new Date();
// 実行日の「次の日曜日」を計算するロジック
// 今が日曜日なら今日、それ以外なら次の日曜日
const daysUntilSunday = (7 - now.getDay()) % 7;
const startSunday = new Date(now.getFullYear(), now.getMonth(), now.getDate() + daysUntilSunday);
console.log("--- AIエージェント稼働開始 ---");
console.log("対象期間: " + Utilities.formatDate(startSunday, "JST", "yyyy/MM/dd") + " (日) から1週間");
try {
generateWeeklyTrainingPlan(startSunday);
console.log("--- 全工程が正常に完了しました ---");
} catch (e) {
console.error("実行エラーが発生しました: " + e.message);
console.error("スタックトレース: " + e.stack);
}
}
/**
* コアロジック:日曜〜土曜のメニューを生成し、シート書き込み&メール送信
*/
function generateWeeklyTrainingPlan(startDate) {
const ss = SpreadsheetApp.openById(CONFIG.SHEET_ID);
const logSheet = ss.getSheetByName('Log');
const menuSheet = ss.getSheetByName('Menu');
const docBody = DocumentApp.openById(CONFIG.DOC_ID).getBody().getText();
// --- 1. 実績と「メモ(反省)」の取得 ---
const lastRow = logSheet.getLastRow();
let trainingLog = "実績なし";
let userNotes = "特になし";
if (lastRow > 1) {
// 直近30件の実績を取得
const logRange = logSheet.getRange(Math.max(2, lastRow - 29), 1, Math.min(30, lastRow - 1), 8);
const logValues = logRange.getValues();
trainingLog = JSON.stringify(logValues);
}
console.log("解析対象のメモ: " + trainingLog);
// --- 2. AIへのプロンプト(日曜〜土曜の7日間を指定) ---
const prompt = `あなたは私の体調と目標を深く理解する専属のAIエージェントです。
以下の情報を統合して、今週の【日曜日から土曜日まで】の7日間の筋トレメニューを作成してください。
■基本データ:
1. 長期目標と制約: ${docBody}
2. 直近のトレーニング実績: ${trainingLog}
■ミッション:
長期目標と制約・直近のトレーニング実績を考慮したメニューとすること
■出力形式(厳守):
JSON形式のみで回答。挨拶やMarkdownの枠(\`\`\`)は不要。
{
"weekly_advice": "今週の方針(長期目標・反省・過去の実績データを踏まえたフィードバック含む)",
"days": [
{
"date_offset": 0,
"advice": 本日の方針とアドバイス",
"items": [{"exercise": "種目名", "weight": "重量", "reps": "回数", "sets": "セット数"}]
}
]
}
※必ず日曜(0)から土曜(6)まで、合計7日分のデータを含めてください。`;
// --- 3. Gemini API リクエスト ---
const response = UrlFetchApp.fetch(`https://generativelanguage.googleapis.com/v1beta/models/${CONFIG.MODEL_NAME}:generateContent?key=${CONFIG.GEMINI_API_KEY}`, {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify({"contents": [{ "parts": [{ "text": prompt }] }]})
});
const rawJson = JSON.parse(response.getContentText());
let aiText = "";
try {
aiText = rawJson.candidates[0].content.parts[0].text;
} catch (e) {
throw new Error("APIレスポンスからテキストを抽出できませんでした。");
}
const jsonMatch = aiText.match(/\{[\s\S]*\}/s);
if (!jsonMatch) throw new Error("回答にJSONが含まれていませんでした。");
const data = JSON.parse(jsonMatch[0]);
const days = data.days || [];
const weeklyAdvice = data.weekly_advice || "なし";
console.log("解析成功。今週の方針: " + weeklyAdvice);
// --- メール本文の構築準備 ---
let emailBody = `■今週のフィードバック・方針\n${weeklyAdvice}\n\n`;
emailBody += `■トレーニングスケジュール\n----------------------------\n`;
// --- 4. シートへの展開(エラー防止ガード付き) ---
days.forEach((day, index) => {
// date_offsetが未定義ならindex(0-6)を自動割り当て
const offset = (day.date_offset !== undefined && day.date_offset !== null) ? day.date_offset : index;
const targetDate = new Date(startDate.getTime());
targetDate.setDate(startDate.getDate() + offset);
const dateStr = Utilities.formatDate(targetDate, "JST", "yyyy/MM/dd");
const items = day.items || day.exercises || [];
const dayMenuSummary = items.length > 0 ? items.map(i => `${i.exercise}(${i.weight})`).join(', ') : "休息日";
// Menuシート
menuSheet.appendRow([
Utilities.getUuid(),
dateStr,
dayMenuSummary,
day.advice || ""
]);
// Logシート(個別の種目)
if (items.length > 0) {
items.forEach(i => {
logSheet.appendRow([
Utilities.getUuid(),
dateStr,
i.exercise || "種目不明",
i.weight || "-",
i.reps || "-",
i.sets || "-",
"未実施",
""
]);
});
}
// メール本文に追加
emailBody += `【${dateStr}】 ${dayMenuSummary}\n`;
if (day.advice) {
emailBody += `└ アドバイス: ${day.advice}\n`;
}
emailBody += `\n`;
console.log(`${dateStr} (Day ${offset}) のメニューを書き込みました。`);
});
emailBody +=
`${CONFIG.APP_URL}`;
// --- 5. メール送信処理 ---
const recipient = Session.getActiveUser().getEmail(); // 実行者のメールアドレス
const subject = `【筋トレ計画】今週のメニューとフィードバック (${Utilities.formatDate(startDate, "JST", "MM/dd")}〜)`;
GmailApp.sendEmail(recipient, subject, emailBody);
console.log(`メールを送信しました: ${recipient}`);
}
UI(App Sheet※実施結果入力部分)
リマインダメール(Gmail)
筋トレ記録(Google Sheet)
