2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

市民ランナーがコードを書かずにGeminiとChatGPTで創り上げた「Strava連携AIランニングカルテ」

2
Posted at

1. はじめに:市民ランナーが直面する「データ死蔵」と「一般論AI」の限界

以前、Qiitaに「マラソン大会のGPXデータをGitHubで公開」という記事を投稿しました。

その後、改めて感じたことがあります。

市民ランナーにとって最も重要で、最も難しいテーマは、単に「速く走ること」ではありません。

本当に難しいのは、次の2つです。

  • 怪我を予防すること
  • 日々の疲労状態を見ながら、練習量を絶妙に調整すること

いわゆる「匙加減」です。

現在、多くのランナーがGarmin、COROS、Polar、Apple Watchなどの高性能GPSウォッチを使い、ランニングデータをGarmin ConnectやStravaに同期しています。

また、生成AIの普及により、ChatGPTやGeminiに、

目標達成のための3ヶ月練習プランを作ってください

と相談しているランナーも増えてきました。

しかし、実際に使ってみると、そこには大きな壁がありました。

課題1:せっかくの客観ログがLLMに渡っていない

手元には、距離、ペース、心拍数、時間、獲得標高などのリッチなデータが毎日蓄積されています。

しかし、LLMに相談するときは、

今日は10km走りました。少し疲れています。

と手入力するだけになりがちです。

これでは、せっかく蓄積されたランニングログの価値が活かされません。

課題2:過去の実績が文脈化されていない

AIにその日の相談をしても、過去数週間の走行距離、疲労の蓄積、ポイント練習の頻度、休養日の取り方などが文脈として渡っていなければ、返ってくるのは一般論になりがちです。

疲れているなら休みましょう
無理せずジョグにしましょう
週末にロング走を入れましょう

正しいけれど、自分専用ではない。

そこに限界を感じました。

課題3:リアルな専門家や生活制約が反映されない

市民ランナーの練習は、机上の理想プランだけでは回りません。

私たちの周りには、フォームを見てくれるクラブチームの監督やコーチ、身体をケアしてくれる整骨院の先生、練習仲間、ペーサーがいます。

さらに、仕事の繁忙期、出張、旅行、家族予定、怪我の違和感、天候といった現実の制約もあります。

AIがどれだけ美しいプランを出しても、それが現実の生活や身体の状態に合わなければ実行できません。

そこで私は、

Stravaの客観ログ × 今の主観コンディション × レースや生活制約 × AI相談

を統合し、AIと一緒に練習判断を育てる仕組みとして、

Strava連携AIランニングカルテ

を作りました。

コンセプトは、

自由に走って、強くなる。

です。


2. 私のバックグラウンド:コードは書けない。仕様を書き、テストして、走る。

少し、私自身のバックグラウンドをお話しします。

普段の私は、前職では地理空間データプラットフォームや、現職ではエンタープライズ向けのデータストレージ / データインフラ領域で、アライアンス構築や戦略的パートナーシップに関わる仕事をしています。

位置情報、API、データ活用、インフラの世界には長く関わってきました。

ただし、私は一般的なIT知識はあるものの、いわゆる「ガリガリとコードを書けるエンジニア」ではありません。

それでも、今回のシステムを作ることができました。

なぜか。

それは、AI時代の開発において重要なのは、必ずしも最初からコードを書けることだけではないからです。

重要なのは、

  • 何を解決したいのか
  • どんなデータを使うのか
  • どんな操作体験にしたいのか
  • どこで人間が判断すべきなのか
  • どこまでAIに任せ、どこから人間が引き取るのか

を具体的に設計することです。

私はその仕様を書き、実際に走ってテストし、エラーを見つけ、また直す、という流れを繰り返しました。

GeminiとChatGPTによるペアプロ開発

今回の開発では、2つのLLMを役割分担して使いました。

役割 主に使ったLLM 内容
仕様整理・初稿コード作成 Gemini in Canvas 全体構造、シート設計、GAS初稿作成
レビュー・バグ修正・リファクタリング ChatGPT GASの実行時エラー確認、列ズレ、スマホQOL、JSONサニタイズ、設計整理

Geminiで大きな骨格を作り、ChatGPTで細かいバグや設計の粗さを潰していく。

この「2つのLLMを相手にしたペアプロ」によって、コードを書けない私でも、Google Apps Scriptを使った実用的なシステムを構築できました。


3. なぜアプリではなく「Googleスプレッドシート × 自由なLLM」なのか?

世の中には、AIを使ったランニングアプリや、トレーニング管理アプリがたくさんあります。

それらは便利です。

ただし、多くは固定されたクローズドな仕組みです。

  • どのAIモデルが使われているかわからない
  • 自分でプロンプトを調整できない
  • 自分のデータがどこに保存されているかわかりにくい
  • アプリの思想に合わせて、自分の練習管理を変える必要がある
  • コーチや自分の感覚を柔軟に差し込めない

私が作りたかったのは、そういう「閉じたAIアプリ」ではありません。

もっと自由で、もっと自分の手元にあり、もっと人間の判断を大切にする仕組みです。

LLMモデルを自由に選べる

このシステムでは、AIモデルを固定しません。

Googleスプレッドシート上でAI相談用プロンプトを生成し、それを自分の好きなLLMに投げます。

例えば、

  • ChatGPT
  • Gemini
  • Claude
  • その他のLLM

どれを使ってもよい。

さらに、納得がいかなければ、そのままチャット上で壁打ちできます。

この提案は少し攻めすぎでは?
週末は出張があるので調整してください
コーチに相談するなら、どこを論点にすべきですか?
もう少し守りのプランにしてください

このように、AIアプリの固定画面ではなく、LLMとの自然な対話をそのまま練習計画に接続できます。

データを自分で管理できる

ランニングログは、単なる運動データではありません。

心拍、疲労、体調、怪我の兆候、生活リズム、レース目標。

かなり個人的なライフログです。

だからこそ、自分のデータをどこに置くかは重要です。

このシステムでは、データは自分のGoogleドライブ上のスプレッドシートに保存されます。

つまり、

自分のデータを、自分の管理下に置く

という形にできます。

これは、いわばランニングデータにおけるデータ・ソブリンティです。

無料枠で運用できる

この仕組みは、基本的に以下で構成されています。

  • Googleスプレッドシート
  • Google Apps Script
  • Strava API
  • 任意のLLM

専用サーバーは不要です。

月額課金のバックエンドも不要です。

GoogleアカウントとStravaがあれば、かなりの部分を無料枠で運用できます。

もちろん本格的に大人数で運用するなら、API制限や権限管理、データ保護設計を考える必要があります。

しかし、個人やチーム内の検証であれば、十分に現実的な構成です。

AIは支配者ではなく、補助ヘルパー

このシステムで一番大事にしているのは、AIとの距離感です。

AIにカレンダーを支配させません。

AIが勝手にTraining_Planを書き換えることもありません。

AIは提案するだけです。

最終的には、人間がDashboardで確認し、必要ならAI案をコピーし、微修正してから、Training_Planへ反映します。

つまり、

AIは副コーチ。
正本を更新するのは人間。

という思想です。

これが、Strava連携AIランニングカルテの一番大事な設計思想です。


4. なぜGarmin直接接続ではなく、Stravaをデータハブにしたのか?

当初は、GarminユーザーなのだからGarmin Connectから直接データを取るのが自然だと考えていました。

しかし、調べていくと、個人開発で扱うにはハードルが高いことがわかりました。

Garminの公式APIは、一般個人が気軽にOAuth連携してアクティビティデータを取得する用途には向いていません。

一方で、周囲のランナーを見ると、使っているデバイスはバラバラです。

  • Garmin
  • COROS
  • Polar
  • Apple Watch
  • Suunto

など、さまざまです。

そこで、メーカーごとのAPIに依存するのではなく、Stravaをデータハブにする方針にしました。

多くのGPSウォッチはStravaに自動同期できます。

つまり、

各種GPSウォッチ
  ↓
Strava
  ↓
Google Apps Script
  ↓
Googleスプレッドシート
  ↓
LLM相談

という流れにすれば、デバイス依存をかなり減らせます。

Strava API連携部分では、Qiita上の先行記事も参考にしました。(感謝)
-StravaのAPIにアクセスしてアカウント情報を取得する

OAuth2.0認証、リフレッシュトークン、アクティビティ取得などをGoogle Apps Script上に組み込み、スプレッドシートへRunアクティビティを同期する構成にしています。


5. 全体アーキテクチャ

Strava連携AIランニングカルテは、以下のような構成です。

[GPS Watch]
  Garmin / COROS / Apple Watch など
        │
        ▼
[Strava]
        │  Strava API
        ▼
[Google Apps Script]
        │
        ▼
[Google Sheets]
  - Dashboard
  - Training_Plan
  - Training_Master
  - Daily_Summary
  - Race_Calendar
  - Generated_Plan
        │
        ▼
[LLM]
  ChatGPT / Gemini / Claude など

重要なのは、Googleスプレッドシートを単なる表ではなく、軽量なデータベース兼UIとして使っている点です。


6. シート設計

主なシートの役割は以下です。

シート名 役割
Dashboard 日常操作の中心。AIプロンプト生成、JSON貼り戻し、AI提案レビュー、現行プラン更新を行う
Training_Plan 直近3ヶ月の正本カレンダー。予定と実績をすべて保持する
Training_Master Training_PlanとStrava実績の突合DB
Daily_Summary 日次集計。総距離、総時間、アクティビティ数、心拍、判定コメントなど
Race_Calendar レース、練習会、出張、旅行、休養など、長期の目標・制約を管理
Generated_Plan AI提案の一時保存。正本ではない
Strava_Activities Strava APIから取得したアクティビティログ

7. Human-in-the-Loop:AIに丸投げしない設計

このシステムの基本思想は、

AIに練習を丸投げしない

ことです。

AIはあくまでアドバイザーです。

正本であるTraining_Planを更新するのは、人間です。

Dashboardでは、AI提案と現行プランを横並びで見ます。

現行プラン
  ↓
AI提案を確認
  ↓
必要ならAIコピー
  ↓
人間が微修正
  ↓
現行プランを更新
  ↓
Training_Planへ反映

Dashboardのレビュー画面は、12列構成にしています。

内容
A 日付
B 現行 種別
C 現行 メニュー
D 現行 距離
E 現行 ペース
F AIコピー
G AI種別
H AIメニュー
I AI距離
J AIペース
K AI理由
L 更新対象

F列の「AIコピー」は、AI提案を現行欄へコピーするだけです。

Training_Planにはまだ反映されません。

人間がB〜E列を確認・微修正し、L列がONになった行だけを「現行プランを更新」でTraining_Planへ反映します。


8. Dashboardに全部出さない理由

Dashboardには、すべての日程を出しません。

表示対象は以下です。

1. 今日〜14日以内の予定は必ず表示
2. 15〜30日以内は、具体的なAI提案がある日だけ表示
3. 過去日は表示しない
4. 31日以降は表示しない

理由は、スマホで運用するためです。

30日分すべてを毎回表示すると、スクロールが長くなり、判断すべき日が埋もれます。

一方で、直近14日分は常に見たい。

そこで、

直近14日は必ず表示し、15〜30日はAIが触れた日だけ表示

という仕様にしました。

日付順は必ず維持します。

AI提案のある日を最上部に持ち上げると、カレンダーとしての視認性が悪くなるためです。

AI提案ありの日は、日付順の中で背景色によって強調します。


9. Training_Plan:3ヶ月固定の正本カレンダー

Training_Planは、直近3ヶ月の正本カレンダーです。

期間は、

先月1日〜翌月末

です。

例えば今日が2026年6月なら、

2026/05/01〜2026/07/31

を保持します。

Training_Planは1日1行を原則とします。

A〜M列が予定、N〜W列が実績です。

A〜M:予定
N〜W:実績

二部練や複数アクティビティは、Training_Plan上では1日1行に集約します。

実績側では、日次合算と代表Stravaを保持します。


10. Planned IDのライフサイクル管理

Training_Planでは、各予定にPlanned IDを持たせています。

空予定の初期行は、

CAL-yyyyMMdd-1

です。

人間が具体的な予定を書き込むと、

MAN-yyyyMMdd-xxxxxxxx

に自動昇格します。

逆に、予定を消して白紙に戻すと、再びCAL形式に戻します。

この仕組みにより、スプレッドシートを手で触っても、ゴミIDが増え続けることを防げます。

概念的には、以下のような状態遷移です。

空予定
  CAL-yyyyMMdd-1
        │ 予定を入力
        ▼
手動予定
  MAN-yyyyMMdd-xxxx
        │ 予定を削除
        ▼
空予定
  CAL-yyyyMMdd-1

イメージコードです。

function hasRealPlan_(type, menu, pace, dist) {
  return (
    type && type !== '未定' ||
    menu && menu !== '未定' ||
    pace && pace !== '-' ||
    Number(dist) > 0
  );
}

実際のコードでは、Training_Planが直接編集されたときに、予定の中身を見てIDとステータスを自動補正します。


11. Strava実績の突合

Stravaから取得したRunアクティビティは、Training_MasterでTraining_Planと突合します。

主な判定は以下です。

状態 突合ステータス 判定コメント
予定あり・実績あり 正常 計画通り / 走りすぎ注意 / 距離不足
空予定・実績あり 実績のみあり 予定外の実施
予定あり・実績なし 未実施 未実施
取消予定・実績あり 取消予定の実施 取消予定の実施

実績はTraining_PlanのN〜W列へ書き戻します。

内容
N 実績距離
O 実績時間
P 実績アクティビティ数
Q 実績ペース
R 平均心拍
S 最大心拍
T 突合ステータス
U 判定コメント
V 代表Strava URL
W 実績更新日時

12. 複数アクティビティから代表Stravaを選ぶ

1日に複数のStravaアクティビティがあることがあります。

例えば、

  • 朝ジョグ
  • 夜のポイント練習
  • アップジョグ
  • レース本体
  • ダウンジョグ

このとき、単純に距離が長いものを代表にすると、本当に負荷の高かった練習が埋もれることがあります。

そこで、代表Stravaは以下の簡易負荷で選びます。

平均心拍 × 実績時間(分)

イメージコードです。

function selectRepresentativeActivity_(activities) {
  if (!activities || activities.length === 0) return null;
  if (activities.length === 1) return activities[0];

  let representative = activities[0];
  let maxLoad = -1;

  activities.forEach(act => {
    const heartRate = act.average_heartrate || 120;
    const durationMin = (act.moving_time || 0) / 60;
    const load = heartRate * durationMin;

    if (load > maxLoad) {
      maxLoad = load;
      representative = act;
    }
  });

  return representative;
}

厳密なTRIMPではありませんが、日次レビュー用の代表選定としては十分実用的です。


13. 実績分類:Runを全部ジョグ扱いしない

Stravaのtype=Runを、そのまま全部「ジョグ」にしてしまうと、分析が弱くなります。

そこで、以下を組み合わせて実績分類します。

  • Stravaのworkout_type
  • タイトル
  • 距離
  • 平均心拍
  • 最大心拍
  • 予定種別

例えば、

条件 分類
workout_typeがRecovery リカバリー
workout_typeがWorkout ポイント練習候補
workout_typeがRace レース
距離15km以上 ポイント練習候補
平均心拍145以上かつ30分以上 ポイント練習候補
最大心拍170以上 ポイント練習候補

workout_typeは常に入るとは限らないため、補助判定として扱います。


14. Race_Calendarは「長期の地図」

Race_Calendarはレース専用ではありません。

レースだけでなく、練習計画に影響する制約も入れます。

内容
日付 イベント日
イベント名 レース名、出張名、練習会名など
イベント種別 レース / 練習会 / 制約 / 休養
目標/制約 目標タイム、走れない理由、注意点
優先度 A / B / C / 未定
メモ 補足

これにより、AIは単なる練習メニューだけでなく、生活上の制約も考慮できます。


15. JSON貼り戻しと自動サニタイズ

LLMの回答は、JSON形式でA13へ貼り戻します。

しかし、LLMの出力は揺らぎます。

  • 全角引用符が混じる
  • 末尾カンマが入る
  • 文字列内に生改行が入る
  • 軽微なカンマ抜けがある

そこで、GAS側でsanitizeJson_()を通し、できる範囲で自動修復してからJSON.parse()します。

A13に貼ると、以下が自動で走ります。

A13へJSON貼付
  ↓
onDashboardEdit
  ↓
sanitizeJson_
  ↓
JSON.parse
  ↓
Generated_Planへ保存
  ↓
Dashboardへ表示
  ↓
A13を自動クリア
  ↓
行高を再固定

16. スマホ運用へのこだわり

このシステムは、PCだけでなくほぼスマホで使う前提です。

そのため、以下をかなり重視しました。

  • 操作メニューを上部とレビュー直上の2箇所に置く
  • A11はコピー専用セルにする
  • A13はJSON貼り戻し専用セルにする
  • A13は自動クリアする
  • A11/A13の行高を固定する
  • Dashboardは12列に絞る
  • 表示対象を今日〜14日+AI提案あり30日以内に絞る

毎日使うものなので、UIの小さなストレスを潰すことが重要でした。

image.png


17. 実際の運用例

例えば、25kmの距離走を実施した直後に、左シンスプリント(ランニングを繰り返すことで、すねの内側に痛みが生じるスポーツ障害)気味の違和感があったとします。

Dashboardに主観情報として、

25km走は完遂。
大きなダメージはないが、左足のすね周りに少し違和感あり。
水曜のチーム練習をどうするか迷っている。

と入力します。

データ更新後、A11のプロンプトをLLMに貼ります。

AIは、Strava実績、直近14日間の負荷、Training_Plan、Race_Calendar、今の主観コンディションをもとに提案します。

例えば、

6/10の3SHINEチーム練習は、800mインターバルを回避し、
後半の5kmペース走のみ安全に合流する案を推奨。

と出てきます。

Dashboard上でそのAI提案を確認し、F列のAIコピーを押す。

すると、AI案が現行プラン編集欄にコピーされます。

そこで人間が、

5kmだけなら行けそう
ただしペースはBクラス設定に落とそう

と微修正します。

最後に「現行プランを更新」を実行すると、Training_Planへ反映されます。

この流れにより、AIの提案をそのまま鵜呑みにするのではなく、自分の身体感覚とコーチへの相談を前提に、現実的な予定へ落とし込めます。


18. 結論:AIを伴走者にする

Strava連携AIランニングカルテを作って感じたのは、AIは「答えを出す存在」というより、良い問いを立てるための伴走者だということです。

AIがいることで、

  • 休む勇気を持てる
  • 練習を減らす根拠を持てる
  • 監督やコーチに相談する論点が明確になる
  • 練習計画の意図が言語化される
  • 自分の疲労を客観視できる

ようになります。

そしてもう一つ伝えたいことがあります。

プログラムが書けないからといって、システム開発を諦める必要はありません。

自分の課題を深く理解し、仕様を考え、実際の現場でテストできるなら、GeminiやChatGPTはそれを動く形にしてくれます。

これからの個人開発では、ドメイン知識と仕様設計力がますます重要になるはずです。

まだ一般公開できる段階ではありませんが、チームメンバーで興味がある方々にも協力してもらいながら、今後もテストを続けていきます。

自分のスポーツログとLLMを組み合わせて、自分専用のAIコーチを召喚する。

そんな時代が、もう来ています。

自由に走って、強くなる。
私たちの限界は、まだまだ先にあるはずです。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?