workout generator audit: rules engine, structure rules, split patterns, injury UX, metadata cleanup

- Add rules_engine.py with quantitative rules for all 8 workout types
- Add quality gate retry loop in generate_single_workout()
- Expand calibrate_structure_rules to all 120 combinations (8 types × 5 goals × 3 sections)
- Wire WeeklySplitPattern DB records into _pick_weekly_split()
- Enforce movement patterns from WorkoutStructureRule in exercise selection
- Add straight-set strength support (single main lift, 4-6 rounds)
- Add modality consistency check for duration-dominant workout types
- Add InjuryStep component to onboarding and preferences
- Add sibling exercise exclusion in regenerate and preview_day endpoints
- Display generator warnings on dashboard
- Expand fix_rep_durations, fix_exercise_flags, fix_movement_pattern_typo
- Add audit_exercise_data and check_rules_drift management commands
- Add Next.js frontend with dashboard, onboarding, preferences, history pages
- Add generator app with ML-powered workout generation pipeline
- 96 new tests across 7 test modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-22 20:07:40 -06:00
parent 2a16b75c4b
commit 1c61b80731
111 changed files with 28108 additions and 30 deletions

221
workout_generation.md Normal file
View File

@@ -0,0 +1,221 @@
# Workout Generation
## Weekly Plan Generation Flow
```
User Preferences
|
v
[days_per_week] -----> Pick WeeklySplitPattern (from ML patterns or defaults)
e.g. 4 days -> push / pull / legs / full_body
|
v
[preferred_workout_types] --> Distribute workout types across training days
e.g. Day 1: cross_training, Day 2: functional_strength, ...
|
v
[preferred_days] ---------> Assign training days to weekday slots, fill rest days
|
v
For each training day:
|
+---> generate_single_workout()
|
v
_get_workout_type_params()
|
+-- Query WorkoutStructureRule where:
| workout_type = day's type (e.g. cross_training)
| section_type = 'working'
| goal_type = primary_goal (e.g. 'strength')
| Falls back to unfiltered if no match
|
+-- _apply_fitness_scaling()
| Beginner: +2 reps, -1 round, +15s rest
| Intermediate: baseline
| Advanced: -1 rep min, +1 round, -10s rest
| Elite: -2 rep min, +1 round, -15s rest
|
v
_build_warmup()
| Beginner: 5-7 exercises
| Intermediate: 4-6
| Advanced/Elite: 3-5
v
_build_working_supersets()
|
+-- Override duration_bias by primary_goal:
| strength=0.1, hypertrophy=0.15,
| endurance=0.7, weight_loss=0.6
|
+-- Scale superset counts by fitness_level:
| Beginner: max 3 supersets, max 3 exercises each
| Elite: +1 to both
|
+-- For each superset:
| Select exercises via ExerciseSelector
| Assign reps or duration per exercise
| Position-based movement patterns (compound early, isolation late)
|
v
_build_cooldown()
| Beginner: 4-5 exercises
| Intermediate: 3-4
| Advanced/Elite: 2-3
v
_adjust_to_time_target()
| preferred_workout_duration * active_time_ratio
| Beginner: 55%, Intermediate: 65%
| Advanced: 70%, Elite: 75%
| Trims supersets if over, pads if under
v
Final workout_spec
```
## Exercise Selection Flow
```
ExerciseSelector.select_exercises(muscle_groups, count, movement_pattern_preference)
|
v
_get_filtered_queryset()
|
+-- Start: All 1132 exercises
|
+-- REMOVE: user's excluded_exercises
|
+-- REMOVE: already used in this workout (no duplicates)
|
+-- FILTER by equipment:
| Exercises whose required equipment is in user's available_equipment
| + all bodyweight exercises (no WorkoutEquipment row)
| If no equipment set, skip this filter (all exercises available)
|
+-- FILTER by muscle groups:
| Match target muscles through ExerciseMuscle join table
| Uses normalized muscle names (e.g. "Quads" -> "quads")
|
+-- FILTER by duration flag:
| If duration-based superset, only is_duration=True
|
+-- FILTER by fitness level:
| Beginner: exclude movement_patterns containing "olympic" or "plyometric"
|
v
Apply movement pattern preference
|
+-- Split queryset into preferred_qs and other_qs
| based on MovementPatternOrder position (early/middle/late)
|
+-- Advanced/Elite: auto-boost "compound" and "multi-joint" if no
| explicit preference set
|
v
_weighted_pick(preferred_qs, other_qs, count)
|
+-- Build pool: preferred exercises appear 3x, others 1x
+-- Shuffle and randomly pick until count unique exercises selected
|
v
Fallbacks (if not enough exercises found):
|
+-- Widen to bodyweight-only exercises
+-- Widen to any muscle group (for warmup/cooldown)
+-- Try without muscle filter as last resort (for working sets)
|
v
_pair_sided_exercises()
|
+-- If exercise has side="Left", find matching "Right" version
+-- Insert partner immediately after the original
|
v
For each selected exercise, assign:
|
+-- Roll random() < duration_bias
| Yes + is_duration: assign random duration (duration_min to duration_max)
| No or is_reps: assign random reps (rep_min to rep_max)
| If is_weight: add weight=null placeholder
|
v
Final exercise list for superset
```
---
# Workout Generation Parameters
## Fitness Level Scaling
| Parameter | Beginner (1) | Intermediate (2) | Advanced (3) | Elite (4) |
|---|---|---|---|---|
| **Rep min adjustment** | +2 | baseline | -1 | -2 |
| **Rep max adjustment** | +2 | baseline | 0 | 0 |
| **Rounds adjustment** | -1 | baseline | +1 | +1 |
| **Rest between sets** | +15s | baseline | -10s | -15s |
| **Active time ratio** | 55% | 65% | 70% | 75% |
| **Max supersets** | capped at 3 | default | default | +1 allowed |
| **Max exercises/superset** | capped at 3 | default | default | +1 allowed |
| **Warmup exercises** | 5-7 | 4-6 | 3-5 | 3-5 |
| **Cooldown exercises** | 4-5 | 3-4 | 2-3 | 2-3 |
| **Exercise filtering** | excludes olympic, plyometric | none | boosts compound | boosts compound |
## Primary Goal Duration Bias Blending
The goal's duration bias is **blended** with the workout type's native bias (70% workout type / 30% goal) so the workout type's character stays dominant. A strength workout remains mostly rep-based even for endurance or weight-loss goals.
`final_bias = (workout_type_bias * 0.7) + (goal_bias * 0.3)`
| Goal | Goal Bias | Effect |
|---|---|---|
| **strength** | 0.1 | nudges toward rep-based |
| **hypertrophy** | 0.15 | slight nudge toward rep-based |
| **endurance** | 0.7 | nudges toward duration-based |
| **weight_loss** | 0.6 | slight nudge toward duration-based |
| **general_fitness** | (uses workout type default) | no blending applied |
Example: Traditional Strength (native 0.1) + weight_loss (0.6) + strength secondary (0.1):
- goal_bias = 0.6 * 0.7 + 0.1 * 0.3 = 0.45
- final = 0.1 * 0.7 + 0.45 * 0.3 = **0.21** (still ~80% rep-based)
## Goal-Based Structure Rule Adjustments
`analyze_workouts` generates 5 variants of each `WorkoutStructureRule` (one per goal), applying these adjustments to the baseline stats extracted from historical data:
| Parameter | strength | hypertrophy | endurance | weight_loss | general_fitness |
|---|---|---|---|---|---|
| **Rep min multiplier** | 0.6x | 0.9x | 1.3x | 1.2x | 1.0x |
| **Rep max multiplier** | 0.7x | 1.1x | 1.5x | 1.3x | 1.0x |
| **Rounds adjustment** | +1 | 0 | -1 | 0 | 0 |
| **Duration min adjustment** | 0s | 0s | +10s | +5s | 0s |
| **Duration max adjustment** | 0s | 0s | +15s | +10s | 0s |
Example: if baseline reps are 8-12, a strength variant gets 5-8, endurance gets 10-18.
## Goal-Based Structure Rule Matching
| Field | Behavior |
|---|---|
| **primary_goal** | `WorkoutStructureRule` queried with `goal_type=primary_goal` first |
| **secondary_goal** | falls back to `goal_type=secondary_goal` if no primary match; also blends into duration_bias (70% primary / 30% secondary) |
## Target Muscle Groups
When the user has `target_muscle_groups` set, those muscles are injected into every workout day's target muscle list (normalized, deduplicated). This ensures exercises for those muscles always get representation regardless of which split pattern was selected.
## Other Preferences
| Preference | Effect |
|---|---|
| **days_per_week** | determines split pattern and rest day distribution |
| **preferred_workout_duration** | time target in minutes (active time = duration * active_ratio) |
| **preferred_days** | which weekdays to schedule training |
| **preferred_workout_types** | distributed across training days |
| **available_equipment** | filters exercise pool to matching equipment + bodyweight |
| **excluded_exercises** | hard-excluded from all selection |
## Unused Fields
| Field | Status |
|---|---|
| **injuries_limitations** | removed from API serializers; field still exists on model but not exposed |