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:
221
workout_generation.md
Normal file
221
workout_generation.md
Normal 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 |
|
||||
Reference in New Issue
Block a user