Files
WerkoutAPI/generator/management/commands/analyze_workouts.py
Trey t 1c61b80731 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>
2026-02-22 20:07:40 -06:00

116 lines
4.4 KiB
Python

"""
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')