Files
WerkoutAPI/generator/management/commands/normalize_muscle_names.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

117 lines
4.3 KiB
Python

"""
Normalize muscle names in the database and merge duplicates.
Uses the MUSCLE_NORMALIZATION_MAP from muscle_normalizer.py to:
1. Rename each Muscle record to its canonical lowercase form
2. Merge duplicates by updating ExerciseMuscle FKs to point to the canonical Muscle
3. Delete orphaned duplicate Muscle records
Usage:
python manage.py normalize_muscle_names --dry-run
python manage.py normalize_muscle_names
"""
from collections import defaultdict
from django.core.management.base import BaseCommand
from django.db import transaction
from muscle.models import Muscle, ExerciseMuscle
from generator.services.muscle_normalizer import normalize_muscle_name
class Command(BaseCommand):
help = 'Normalize muscle names and merge duplicates using MUSCLE_NORMALIZATION_MAP'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would change without writing to DB',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
all_muscles = Muscle.objects.all().order_by('id')
self.stdout.write(f'Found {all_muscles.count()} muscle records')
# Group muscles by their canonical name
canonical_groups = defaultdict(list)
for muscle in all_muscles:
canonical = normalize_muscle_name(muscle.name)
if canonical:
canonical_groups[canonical].append(muscle)
renamed = 0
merged = 0
deleted = 0
with transaction.atomic():
for canonical_name, muscles in canonical_groups.items():
# Pick the keeper: prefer the one with the lowest ID (oldest)
keeper = muscles[0]
# Rename keeper if needed
if keeper.name != canonical_name:
if dry_run:
self.stdout.write(f' Rename: "{keeper.name}" -> "{canonical_name}" (id={keeper.pk})')
else:
keeper.name = canonical_name
keeper.save(update_fields=['name'])
renamed += 1
# Merge duplicates into keeper
for dup in muscles[1:]:
# Count affected ExerciseMuscle rows
em_count = ExerciseMuscle.objects.filter(muscle=dup).count()
if dry_run:
self.stdout.write(
f' Merge: "{dup.name}" (id={dup.pk}) -> "{canonical_name}" '
f'(id={keeper.pk}), {em_count} ExerciseMuscle rows'
)
else:
# Update ExerciseMuscle FKs, handling unique_together conflicts
for em in ExerciseMuscle.objects.filter(muscle=dup):
# Check if keeper already has this exercise
existing = ExerciseMuscle.objects.filter(
exercise=em.exercise, muscle=keeper
).exists()
if existing:
em.delete()
else:
em.muscle = keeper
em.save(update_fields=['muscle'])
dup.delete()
merged += em_count
deleted += 1
if dry_run:
# Roll back the transaction for dry run
transaction.set_rollback(True)
prefix = '[DRY RUN] ' if dry_run else ''
self.stdout.write(self.style.SUCCESS(
f'\n{prefix}Results:'
f'\n Renamed: {renamed} muscles'
f'\n Merged: {merged} ExerciseMuscle references'
f'\n Deleted: {deleted} duplicate Muscle records'
))
# Verify
if not dry_run:
dupes = (
Muscle.objects.values('name')
.annotate(c=__import__('django.db.models', fromlist=['Count']).Count('id'))
.filter(c__gt=1)
)
if dupes.exists():
self.stdout.write(self.style.WARNING(
f' WARNING: {dupes.count()} duplicate names still exist!'
))
else:
self.stdout.write(self.style.SUCCESS(' No duplicate muscle names remain.'))