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:
115
generator/management/commands/analyze_workouts.py
Normal file
115
generator/management/commands/analyze_workouts.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
Django management command to analyze existing workouts and extract ML patterns.
|
||||
|
||||
Usage:
|
||||
python manage.py analyze_workouts
|
||||
python manage.py analyze_workouts --dry-run
|
||||
python manage.py analyze_workouts --verbosity 2
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from generator.services.workout_analyzer import WorkoutAnalyzer
|
||||
from generator.models import (
|
||||
MuscleGroupSplit,
|
||||
MovementPatternOrder,
|
||||
WeeklySplitPattern,
|
||||
WorkoutStructureRule,
|
||||
WorkoutType,
|
||||
)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
'Analyze existing workouts in the database and extract ML patterns '
|
||||
'into WorkoutType, MuscleGroupSplit, WeeklySplitPattern, '
|
||||
'WorkoutStructureRule, and MovementPatternOrder models.'
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Print what would be done without writing to the database.',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = options.get('dry_run', False)
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING(
|
||||
'DRY RUN mode - no changes will be written to the database.\n'
|
||||
'Remove --dry-run to actually run the analysis.\n'
|
||||
))
|
||||
self._print_current_state()
|
||||
return
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
analyzer = WorkoutAnalyzer()
|
||||
analyzer.analyze()
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
self.stdout.write('')
|
||||
self._print_current_state()
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\nAnalysis complete in {elapsed:.2f}s!'
|
||||
))
|
||||
|
||||
def _print_current_state(self):
|
||||
"""Print a summary of the current state of all ML pattern models."""
|
||||
self.stdout.write(self.style.MIGRATE_HEADING('\nCurrent ML Pattern Model State:'))
|
||||
self.stdout.write(f' WorkoutType: {WorkoutType.objects.count()} records')
|
||||
self.stdout.write(f' MuscleGroupSplit: {MuscleGroupSplit.objects.count()} records')
|
||||
self.stdout.write(f' WeeklySplitPattern: {WeeklySplitPattern.objects.count()} records')
|
||||
self.stdout.write(f' WorkoutStructureRule: {WorkoutStructureRule.objects.count()} records')
|
||||
self.stdout.write(f' MovementPatternOrder: {MovementPatternOrder.objects.count()} records')
|
||||
|
||||
# List WorkoutTypes
|
||||
wts = WorkoutType.objects.all().order_by('name')
|
||||
if wts.exists():
|
||||
self.stdout.write(self.style.MIGRATE_HEADING('\n WorkoutTypes:'))
|
||||
for wt in wts:
|
||||
self.stdout.write(
|
||||
f' - {wt.name}: reps {wt.rep_range_min}-{wt.rep_range_max}, '
|
||||
f'rounds {wt.round_range_min}-{wt.round_range_max}, '
|
||||
f'intensity={wt.typical_intensity}'
|
||||
)
|
||||
|
||||
# List MuscleGroupSplits
|
||||
splits = MuscleGroupSplit.objects.all().order_by('-frequency')
|
||||
if splits.exists():
|
||||
self.stdout.write(self.style.MIGRATE_HEADING('\n Top MuscleGroupSplits:'))
|
||||
for s in splits[:10]:
|
||||
muscles_str = ', '.join(s.muscle_names[:5])
|
||||
if len(s.muscle_names) > 5:
|
||||
muscles_str += f' (+{len(s.muscle_names) - 5} more)'
|
||||
self.stdout.write(
|
||||
f' - [{s.split_type}] {s.label} | '
|
||||
f'freq={s.frequency}, ex_count={s.typical_exercise_count} | '
|
||||
f'{muscles_str}'
|
||||
)
|
||||
|
||||
# List WeeklySplitPatterns
|
||||
patterns = WeeklySplitPattern.objects.all().order_by('-frequency')
|
||||
if patterns.exists():
|
||||
self.stdout.write(self.style.MIGRATE_HEADING('\n Top WeeklySplitPatterns:'))
|
||||
for p in patterns[:10]:
|
||||
self.stdout.write(
|
||||
f' - {p.days_per_week}-day: {p.pattern_labels} '
|
||||
f'(freq={p.frequency}, rest_days={p.rest_day_positions})'
|
||||
)
|
||||
|
||||
# List WorkoutStructureRule goal distribution
|
||||
rules = WorkoutStructureRule.objects.all()
|
||||
if rules.exists():
|
||||
from collections import Counter
|
||||
goal_counts = Counter(rules.values_list('goal_type', flat=True))
|
||||
self.stdout.write(self.style.MIGRATE_HEADING('\n WorkoutStructureRule by goal:'))
|
||||
for goal, count in sorted(goal_counts.items()):
|
||||
self.stdout.write(f' - {goal}: {count} rules')
|
||||
Reference in New Issue
Block a user