- 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>
8.2 KiB
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 |