アプリを開く →

OWworkout — アプリ解説資料

パワーリフティング特化のトレーニングログ(PWA) 版: v16 / 2026-08

この資料は2つの読者を想定しています。


1. これは何か

iPhone のホーム画面に置いて使う、パワーリフティング専用のトレーニング記録アプリです。 ブラウザで動く PWA(Progressive Web App)ですが、起動すると全画面になり、 通信が無くても記録・グラフ・計測がすべて動きます。

何を解決しようとしているか

一般的なトレーニングログアプリは「記録できる」で止まります。 このアプリが目指しているのは、その先の「次に何をするか」が楽になるところです。

価値を5段階に分けて考えています。

段階 内容 状態
① 記録 セット・重量・回数・RPE を残す 実装済み
② 効率 入力が速い。ルーチンから自動で埋まる 実装済み
③ 分析 推定1RM・ボリューム・バリエーション比・バー速度 実装済み
④ 判断 「次は何kgでいくか」を根拠つきで出す 実装済み(v15)
⑤ コーチング 期分け・ピーキング・試技選択 未実装

③の一部・④・⑤ が、汎用アプリとの差別化点です。 ①②は「あって当たり前」なので、速く・確実に動くことだけを目標にしています。

他アプリとの違い(要点)


2. iPhone で使い始める(初めての方へ)

App Store には出していません。Safari で一度開いて、ホーム画面にアイコンを置くだけで、 以降はふつうのアプリと同じように使えます。1分で終わります。

手順

  1. iPhone の Safari で URL を開く(https://owworkout.minny0222.workers.dev/
    • 必ず Safari で開いてください。 Chrome など他のブラウザからではホーム画面アプリになりません
  2. 画面下の中央にある 共有ボタン(□ の中に ↑ の形)をタップ
  3. メニューを下にスクロールして「ホーム画面に追加」をタップ
  4. 名前を確認して「追加」をタップ
    • ここで表示されている名前が、そのままアイコン名になります。あとから変えられないので、変えたい場合はこの画面で直してください

ホーム画面にアイコンが出れば完了です。

以降はホーム画面のアイコンから開きます

Safari のタブからでも同じ画面は開けますが、必ずアイコンから起動してください。 理由は2つあります。

電波のあるところで一度だけ準備

ホーム画面に追加したあと、電波のある場所でアプリを開いて1〜2分そのままにしてください。 その間にアプリ本体が端末内に保存され、以降は電波が無くても起動します。 ジムが地下でも問題ありません。

心配な場合は、その場で機内モードにして起動できるか試しておくと確実です。

最初にやっておくと良いこと

  1. 設定 → 競技の設定 → 性別 を入れる(DOTS / IPF GL の点数計算に使います)
  2. 体重を記録 — ホーム画面の「今日の体重」から
  3. 1セット記録してみる — 種目を選んで重量・回数・RPE を入れる。これが毎回の基本操作です
  4. ルーチンを組む — 曜日ごとのメニューを入れておくと、次のセットの重量が自動で埋まります
  5. カメラ計測を1本試す — 明るい場所でプレートに円が合うか確認しておくと、本番で慌てません

やってはいけないこと

ホーム画面のアイコンを削除しないでください。記録データごと消えます。 アイコンは URL への近道ですが、iOS ではそこに記録が紐づいています。 「アイコンを消して入れ直せば新しくなる」は、このアプリでは通用しません。 アプリの更新は、アイコンを置き直さなくても届きます。

機種変更のときや、どうしてもアイコンを置き直したいときは、 設定 → エクスポート → JSON(完全バックアップ) を書き出してから行い、 新しい側で 設定 → インポート で読み込んでください。 この移行の経路は、実機から出したバックアップを使って毎回自動テストで確認しています。

更新の受け取り方

新しい版を置いたあとアプリを開き直すと、自動で切り替わります。 使っている最中に届いた場合だけ画面下に案内が出るので、区切りのいいところでタップしてください (入力の途中で勝手に入れ替わらないようにしています)。

届かないときは 設定 → アプリ本体を読み込み直す。記録・ルーチン・設定は消えません。


3. 何ができるか(画面ごと)

ホーム

ワークアウト(記録中)

トレーニング中に一番長く見る画面です。

履歴

日ごとの一覧。各日から 「この日を AI に渡す」 で、その日だけを書き出せます。

分析

ルーチン

設定

1RM推定式の選択 / 性別(DOTS計算用) / 種目の分類編集 / カメラ計測の補正係数・プレート直径 / 種目ごとのMVT / エクスポート(期間指定・AI用テキスト・CSV・JSON) / 過去7世代からの復元 / インポート / 保存状態の確認 / アプリの再読み込み


4. 設計の考え方(5つの原則)

(1) ジムで確実に動くこと 電波・ログイン・サーバーに依存しません。全機能が端末内で完結します。

(2) 入力の速さは機能である セット記録は「重量・回数・RPEを押す」だけ。予定値の事前入力、休憩時間の自動記録、 RPE をボタンで選べるようにしたことは、すべて「入力の手数を減らす」ためのものです。

(3) 根拠を必ず添える 「145kgを推奨」だけでは信用できません。「推定1RM 182.5kg → @7で5レップなら145kg」 のように計算過程を画面に出します。判断するのは本人です。

(4) 分からないときは黙る RPE が無ければ推奨を出しません。データが足りなければ「粗い推定です」と書きます。 それらしい数字を作らないことを優先しています。

(5) データは本人のもの サーバーに送りません。JSON で完全に持ち出せます。 アプリが無くなっても記録が読める形(CSV / Markdown)で出せます。


5. アーキテクチャ

全体像

iPhone(ホーム画面アプリ)
 └ index.html ─ 起動・全CSS・タブ骨組み
     ├ js/lib.js         純粋関数(1RM推定・DOTS・オートアジャスト・書き出し整形)
     ├ js/store.js       データ層(IndexedDB + localStorage、全ドメイン操作)
     ├ js/ui.js          画面生成とイベント(文字列でHTMLを組み、イベントは委譲)
     ├ js/chart.js       SVGの折れ線グラフ(自前)
     ├ js/velocity-cam.js カメラ計測の画面と解析
     ├ js/tracker.js     多点オプティカルフロー
     ├ js/detect.js      プレート(円)の検出
     └ sw.js             サービスワーカー(オフライン用キャッシュ)

数字

項目
アプリ本体 約 7,900 行(JS + HTML + SW)
テスト 約 1,900 行 / 10スイート / 347件
外部依存 0(npm パッケージ・CDN・フォント・解析ツールすべて無し)
ビルド 不要(素の ES Modules。ファイルを置けば動く)
通信 起動時のアプリ本体取得のみ。データ送信は一切なし

なぜ依存ゼロか

3つ理由があります。

  1. ジムで動かなくなる要因を減らす。 CDN が落ちればアプリが起動しません
  2. 10年後も動く。 ビルドツールの寿命はアプリの寿命より短い
  3. 1人が全部読める規模に収まる。 依存が無ければ、挙動の説明はすべてこのリポジトリ内で完結します

UI の方式

仮想DOMを使いません。状態 → HTML文字列 → innerHTML の単純な再描画です。 イベントは document へのデリゲーションで、data-act="..." を1箇所の switch で処理します。

// ui.js の中心構造
function render() {
  switch (route.name) {
    case 'home':     html = homeScreen(); break;
    case 'workout':  html = workoutScreen(route.workoutId); break;
    ...
  }
}
document.addEventListener('click', (e) => {
  const act = e.target.closest('[data-act]')?.dataset.act;
  switch (act) { case 'add-set': ... }
});

画面の規模(数百ノード)では再描画のコストが問題にならず、 「今の状態からこの画面になる」が1関数で読み切れる利点のほうが大きいためです。


6. データモデル

全データを 1つの JSON オブジェクトとしてメモリに載せ、配列操作で扱います。 1年分でも約1MBに収まるため、インデックスや部分読み込みは要りません。

{
  schema: 1,
  exercises: [],          // 種目マスタ(プリセット37 + 自作)
  workouts: [],           // 1日1件のトレーニング
  workoutExercises: [],   // その日にやった種目
  sets: [],               // セット1本ずつ
  bodyweights: [],        // 体重
  routines: [],           // ルーチン(複数可・1つがアクティブ)
  routineDays: [],        // 曜日別メニュー
  routineItems: [],       // メニューの中の種目と目標
  scheduleOverrides: [],  // 「今週だけこの日に移す」
  settings: {},
}

全レコードが id (UUID) / updated_at / deleted_at(論理削除)を持ちます。 将来サーバー同期を足せる形にしてあり、いまは使っていません。

セット1本が持つ情報

{
  id, workout_exercise_id, set_no,
  weight, reps, rpe,
  rep_velocities: [0.32, 0.29, 0.26, 0.23],  // レップごとの平均速度
  rep_traces: [...],                          // 軌跡(後の局面分析のため保存)
  planned_weight, planned_reps, planned_rpe,  // 記録時点のルーチン目標
  velocity, velocity_source,                  // 'camera' | 'manual'
  is_warmup, rest_sec,                        // 休憩は自動計算
  e1rm_formula, e1rm_rpe,                     // 記録時の式で確定して保存
  created_at, updated_at, deleted_at,
}

設計上の判断が2つあります。


7. 保存の仕組み

二重書き込み

実際に起きた事故が設計の理由です。 登録したルーチンが翌日に消えていました。 原因は、IndexedDB への書き込みが非同期であることです。 iOS はホーム画面に戻した瞬間にページを止めることがあり、書き込みが完了しません。

対策として、保存を二重にしています。

経路 性質 役割
localStorage 同期。その場で必ず完了する 取りこぼしゼロの保険(4MB上限)
IndexedDB 非同期・容量が大きい 本体(200msまとめ書き)

起動時に両方を読み、saved_at が新しい方を採用します。

日次スナップショット

その日の最初の保存時に、1日1世代・最大7世代のスナップショットを IndexedDB に残します。 誤操作の翌日でも、設定画面から前日以前の状態に戻せます。

消えないようにする

navigator.storage.persist() でブラウザの自動削除から保護を要求します。 ホーム画面に追加したアプリでは許可されやすい傾向があります。

重要な運用上の注意 iOS ではホーム画面のアイコンを削除すると記録データごと消えます。 アイコンは URL への近道にすぎず、置き直さなくてもアプリの更新は届きます。 アイコンを消して追加し直す操作は絶対に行わないでください。


8. カメラによるバー速度計測

外部センサー(加速度計・エンコーダ)を使わず、iPhoneのカメラだけでバー速度を測ります。

パイプライン

① プレートに円を合わせる(画面上でドラッグ/自動検出)
      └ 円の直径 = 実寸450mm。これが px→m の換算になる
   ↓
② 円の中から「追いやすい点」を数十個選ぶ
      └ コーナー性の高い点。tracker.js
   ↓
③ ピラミッド型 Lucas-Kanade オプティカルフローで毎フレーム追う
      └ 各点の移動量の【中央値】を採る
        1点が外れても全体は壊れない。模様の乏しいラバープレート、
        照明変化、手ブレに耐えるのはこのため
   ↓
④ 縦位置の時系列から挙上(コンセントリック)区間を切り出す
   ↓
⑤ レップごとに 変位 ÷ 所要時間 = 平均速度
   + 可動域を12分割した速度プロファイルを保存

平均速度の定義(ここが精度の要)

挙上の始点・終点を「速度がしきい値を超えた瞬間」で決めるとノイズに非常に弱い。 速度は位置の差分なので、位置のわずかな揺れが増幅されるためです。

そこで境界を 「位置が挙上距離の 2% / 98% を通過した時刻」 で決めています。

平均速度 = 挙上距離の96% ÷ (2%地点から98%地点までの時間)

位置そのものを見るのでノイズに強くなります。 両端の遅い部分を外すため、全区間平均より高めに出ます (理想的な半正弦なら1.172倍、最小ジャークなら1.283倍)。

ラックアップ・歩行をレップとして数えない

スクワットやベンチでは、実際のレップの前後に別の上下動が入ります。 ラックアップは上に挙がるのでレップに見え、1本混ざるとセット平均が丸ごと狂います。

見分ける手がかりは2つです。

  1. 本当のレップは、挙げる前に必ず同じくらい下ろしている。 直前4秒以内に挙上距離の50%以上の下降が無いものは除外
  2. 歩行は横に動く。 挙上距離に対して横の移動が60%を超えるものは除外

デッドリフトは床から挙げるので1レップ目の前に下降がありません。 その場合は条件1を外します(画面の「レップの型」で切り替え)。

実機での検証

同種の動画解析アプリ Metric と、同一のベンチプレス4レップで突き合わせました。

レップ1 レップ2 レップ3 レップ4
可動域(本アプリ) 17.1cm 19.1cm 21.5cm 20.5cm
可動域(Metric) 17cm 19cm 21cm 20cm
速度(本アプリ) 0.179 0.189 0.182 0.107
速度(Metric) 0.18 0.21 0.18 0.10

可動域は3%以内、セット平均速度は1.9%以内で一致しました。 この結果を受けて補正係数 calib1.0(補正なし)にしています。 当初は 0.816 を掛けていましたが、それは過補正でした。

なお、この検証に使った実機の軌跡データは tools/fixtures/bench-115kg.json として固定し、回帰テストに組み込んであります。

正直に書いておくこと

プレートの自動検出は、条件によっては失敗します。 ラバープレートは周囲との輝度差が5〜20階調しかないことがあり、 その場合はタオルや器具の縁を誤検出します。

古典的な画像処理の範囲では解決できないと判断し、 手動で合わせる操作を最良にする方向に舵を切りました。

自動検出は「うまくいけば手間が省ける補助」の位置づけです。


9. パワーリフティング特化のデータ設計

種目に3つの属性を持たせる

汎用アプリは種目を「名前とカテゴリ」でしか持ちません。 それでは「ポーズスクワットが弱い」を言えません。そこで各種目に3つ付けています。

属性 用途
lift SQ / BP / DL / なし どの競技種目に紐づくか。ボリューム集計・比の分母
role competition / variation / accessory 比を取る対象かの区別
emphasis bottom / mid / top / form / control 弱点がどこかを言い当てる鍵

プリセットは37種目。すべてに分類が付いています。設定画面から本人が変更できます。

{ name: 'ポーズスクワット', lift: 'SQ', role: 'variation',
  emphasis: 'bottom', ref_ratio: 0.885 }
{ name: 'ラックプル',       lift: 'DL', role: 'variation',
  emphasis: 'top',    ref_ratio: 1.15 }

競技種目は lift ごとに複数あってよい

デッドリフトのコンベンショナルとスモウのように、どちらも競技として成立する スタイルがあるためです。推定トータルには、その lift の競技種目のうち最も高い値を使います。

判定は「一般値」ではなく「自分の平常値」に対して行う

バリエーション比の一般的な目安は世に出回っていますが、個人差が非常に大きいので、 それを基準に「弱い/強い」と判定するのは危険です。

あなたのポーズSQ ÷ 競技SQ が、過去1年ずっと 0.88 前後で推移していた
   ↓
直近8週で 0.82 まで落ちた
   ↓
「ボトムの出力が相対的に落ちている」と根拠つきで言える

variationRatios()直近56日それより前の365日(重なりのない期間)を比較します。 表に載せている参考比は、データが貯まるまでの初期値にすぎません。

これは「大腿四頭筋が弱い」といった筋名の断定ではなく、動作局面の特定です。 計測で言える範囲はそこまで、という線を引いています。


10. オートアジャスト(次は何kgか)

v15 の中心機能です。LLM も通信も使いません。ただの計算です。

考え方

実施したセット(重量・回数・実際のRPE)から今日の推定1RMを出し、 そこから「予定していた回数を、予定していたRPEでこなせる重量」を逆算します。

予定 150kg × 5 @7
実際 150kg × 5 @8.5
  → 今日の推定1RM 182.5kg
  → @7 で 5レップなら 145kg
  → 次のセットは 145kg を推奨(-5kg)

逆に @6 で終われば 152.5kg を推奨します。

安全のための決まりごと

export function roundToward(v, base, step = 2.5) {
  if (v < base) return Math.ceil(v / step) * step;   // 下げるときは切り上げ
  if (v > base) return Math.floor(v / step) * step;  // 上げるときは切り捨て
  return v;
}

主観だけに頼らない

RPE は主観なので、その日の気分で上下します。 同じ重量で測ったバー速度は主観が入らないので、裏づけとして併記します。

同じ 150kg でのバー速度: 平常 0.34 → 今日 0.28 m/s(-18%・過去5セットとの比較)
  主観だけでなく速度も落ちています。

velocityReadiness() は、同じ重量±3%・過去180日・別の日の本番セット(ウォームアップ以外)から 速度の中央値を取って比較します。両方が同じ向きなら確度が上がり、食い違えばそれも分かります。

出るところ


11. 分析と指標

推定1RM

4つの式(Epley / Brzycki / Lombardi / O'Conner)から選べます。 加えて RPE換算RIR = 10 - RPE)でも計算し、両方をグラフに出します。 複数式を使う場合は中央値を採ります(外れ値に引きずられないため)。

競技力

SQ/BP/DL それぞれの競技種目の推定1RMからトータルを出し、 DOTSIPF GL ポイント、階級を表示します。係数は公式のものを実装しています。

const DOTS_COEF = {
  male:   [-307.75076, 24.0900756, -0.1918759221, 0.0007391293, -0.000001093],
  female: [ -57.96288, 13.6175032, -0.1126655495, 0.0005158568, -0.0000010706],
};

ボリューム

lift 単位で週次集計します(週の始まりは月曜)。 主働と補助を分けて表示するので、「SQ 12.4t(うち補助 2.1t)」のように読めます。 ピーキングやブロック設計は lift 単位で見るものなので、この集計単位が前提になります。

疲労の手がかり


12. エクスポートとAI連携

AI にトレーニング内容を渡してメニューを組んでもらう使い方を、主要な用途として設計しています

3つの形式

形式 用途
AI に渡すテキスト(Markdown) AIへの入力。読み方の凡例つき
CSV 表計算ソフトでの集計
JSON 完全バックアップ・復元

期間指定

「全期間」「直近90日」だけでは足りません。実際の使い方はこうです。

AI に組んでもらったメニューを実施 → その日の結果を RPE と速度つきで報告 → 次のルーチンをもらう

そのため 1日単位で出せます。履歴タブの各日から「この日を AI に渡す」、 または設定画面で開始日=終了日にします。 「最後の日/今日/7日/30日/90日/全期間」のショートカットもあります。

出力の例

## 読み方
145kg×4 RPE8 [予定@7] v0.28 (0.27/0.34/0.25/0.27) 休3分 W
  → 重量×回数 RPE 予定RPE 平均速度 (レップ毎速度) 休憩 W=ウォームアップ

### 2026-08-17
ポーズスクワット
  137.5kg×4 RPE7 v0.28 (0.32/0.29/0.26/0.23) 休4分
  ...
  次回案: 140kg(推定1RM 172.5kg → @7×4)

iOS での配信方法

書き出しは画面上のシート(コピー/共有/ダウンロード)で行います。

これは実機の不具合対応の結果です。iOS の Web Share API は text/csv を受け取れず、 共有するとファイル名1行だけのテキストが出来てしまいました。 中身を画面に出してコピーさせるのが最も確実なので、その経路を主にしています。

インポート

JSON を読み込むと現在のデータを完全に置き換えます。 BOM 付きのファイルも読めるようにしてあります(JSON.parse(json.replace(/^/, '').trim()))。

「書き出せた」だけでは検証になりません。 実機から出てきたバックアップそのものを tools/fixtures/backup-real.json として固定し、それで実際に復元できることを 自動テストで毎回確かめています。


13. 更新の仕組み

サービスワーカーは キャッシュ優先 + 裏で更新 です。 ジムで開いたときの起動が最速になり、電波があるときに新しい版を取り込みます。

skipWaiting()しません。使用中に中身が入れ替わると入力の途中で挙動が変わるため、 新しい版は待機させ、画面下の案内を本人が押したときに引き継ぎます。

デプロイ時は次の2箇所を必ず上げます(片方だけだと更新が届きません)。

更新が届かないときのために、「アプリ本体を読み込み直す」ボタンがあります。 キャッシュを捨てて取り直すだけで、記録・ルーチン・設定は消えません。


14. テスト

347件 / 10スイート。 純粋関数の単体テストと、実際のブラウザ(Playwright)での E2E を併用しています。

スイート 件数 内容
test-logic.js 96 1RM推定・DOTS/GL・オートアジャスト・書き出し整形
test-ui-e2e.js 70 画面操作の一通り
test-velocity.js 63 速度解析(合成波形+境界条件)
test-camera-e2e.js 38 カメラ画面(疑似映像を流し込む)
test-backup-e2e.js 31 実機バックアップからの復元・書き出しの中身
test-real-trace.js 12 実機の軌跡 vs Metric の実測値
test-update-e2e.js 12 サービスワーカーの更新
test-detect.js 11 円検出・半径の精密化
test-persistence-e2e.js 8 二重保存・スナップショット復元
test-force-refresh-e2e.js 6 強制再読み込み

方針


15. まだ実装していないこと

意図的に後回しにしているものです。

項目 理由
スティッキングポイントの検出 レップ内の速度プロファイル(12分割)の保存は v13 から開始済み。判定に足るデータが貯まるのを待っています。仕組みより先にデータを溜めるのが順序として正しいため
試合に向けたピーキング 試合日 + 試技選択(第1試技90% / 第2試技95% / 第3試技100%+)。設計はあるが未着手
減量・計量の計画 同上
クラウド同期・複数端末 データモデルは同期可能な形(UUID + updated_at + 論理削除)にしてある。必要になってから足す
AIコーチの自動連携 手動でのコピー&ペースト連携を採用。API キーの管理・通信・費用を持ち込まずに同じ価値が出せるため
完全な自動プレート検出 前述のとおり古典的手法の限界。手動操作の質を上げる方向を選択

16. 用語集

用語 意味
RPE 主観的運動強度。10 = これ以上挙げられない、8 = あと2回できた
RIR 余力レップ数。RIR = 10 - RPE
推定1RM (e1RM) 1回だけ挙げられる最大重量の推定値
MVT 最小挙上速度。その人がその種目で1RMを挙げるときのバー速度。1RMの速度推定の基準
VBT Velocity Based Training。バー速度を指標に負荷を決める方法
ROM 可動域(Range of Motion)
DOTS / IPF GL 体重で補正して選手の強さを比較する点数
lift 競技3種目(SQ = スクワット / BP = ベンチプレス / DL = デッドリフト)
role 種目の位置づけ(競技 / バリエーション / 補助)
emphasis その種目が効く動作局面(ボトム / 中間 / ロックアウト / フォーム / 制御)
PWA ブラウザ技術で作り、ホーム画面に置いてアプリのように使えるもの

OWworkout v16 — 依存ゼロ・オフライン動作・データは端末内のみ