- 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>
116 lines
4.4 KiB
Python
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')
|