Spaces:
Sleeping
Sleeping
| """ | |
| Recommendation Engine | |
| Generates intelligent clothing recommendations using Gemini AI | |
| and database-backed product matching. | |
| Architecture: | |
| - Gemini AI: Intelligent size/fit/color recommendations | |
| - Database: Product matching, inventory checking, variant selection | |
| - color_palettes.py: Single source of truth for colour options | |
| """ | |
| import logging | |
| from typing import Dict, List, Tuple | |
| from django.db.models import Q | |
| logger = logging.getLogger(__name__) | |
| class RecommendationEngine: | |
| """ | |
| Generates clothing recommendations using Gemini AI for intelligence | |
| and the product database for matching. | |
| Gemini handles: | |
| - Size recommendation (considering body shape + garment type) | |
| - Fit type recommendation | |
| - Color recommendations (1 shirt + 1 pants from the unified palette) | |
| - Styling advice | |
| Database handles: | |
| - Finding products with matching size in stock | |
| - Color variant matching | |
| - Inventory availability | |
| """ | |
| SIZE_ORDER = ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL'] | |
| GARMENT_MEASUREMENTS = { | |
| 'shirt': {'fit_focus': 'chest'}, | |
| 'pants': {'fit_focus': 'waist'}, | |
| 'dress': {'fit_focus': 'waist'}, | |
| 'jacket': {'fit_focus': 'chest'}, | |
| 'skirt': {'fit_focus': 'waist'}, | |
| } | |
| def __init__(self): | |
| self._gemini = None | |
| def gemini(self): | |
| """Lazy-load Gemini client.""" | |
| if self._gemini is None: | |
| from .gemini_client import get_gemini_client | |
| self._gemini = get_gemini_client() | |
| return self._gemini | |
| # ββ Size ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def recommend_size(self, measurements: Dict[str, float], | |
| garment_type: str = 'shirt', | |
| body_shape: str = 'rectangle') -> str: | |
| if not self.gemini.available: | |
| raise RuntimeError("Gemini AI is not available for size recommendation") | |
| result = self.gemini.get_size_recommendation( | |
| measurements=measurements, | |
| garment_type=garment_type, | |
| body_shape=body_shape, | |
| ) | |
| size = result.get("recommended_size", "M") | |
| logger.info(f"Gemini size recommendation: {size} for {garment_type}") | |
| return size | |
| # ββ Fit βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def recommend_fit(self, measurements: Dict[str, float], | |
| garment_type: str = 'shirt', | |
| body_shape: str = 'rectangle') -> str: | |
| if not self.gemini.available: | |
| raise RuntimeError("Gemini AI is not available for fit recommendation") | |
| result = self.gemini.get_size_recommendation( | |
| measurements=measurements, | |
| garment_type=garment_type, | |
| body_shape=body_shape, | |
| ) | |
| return "regular" | |
| # ββ Colours (NEW: returns dict with shirt + pants) ββββββββββββ | |
| def recommend_colors(self, skin_tone: str, undertone: str = 'warm') -> Dict[str, str]: | |
| """ | |
| Returns dict: {'recommended_shirt': '<name>', 'recommended_pants': '<name>'} | |
| """ | |
| if not self.gemini.available: | |
| raise RuntimeError("Gemini AI is not available for color recommendation") | |
| result = self.gemini.get_color_recommendations( | |
| skin_tone=skin_tone, | |
| undertone=undertone, | |
| ) | |
| if not result or 'recommended_shirt' not in result: | |
| raise ValueError("Gemini returned empty color recommendations") | |
| return result | |
| # ββ Full product matching βββββββββββββββββββββββββββββββββββββ | |
| def get_matching_product_variants( | |
| self, | |
| body_scan, | |
| gender: str = None, | |
| limit: int = 6 | |
| ) -> List[Dict]: | |
| """ | |
| Get actual products from store with specific size and color recommendations. | |
| Uses Gemini AI for size/color/fit recommendations, then matches against inventory. | |
| """ | |
| from fitting_system.models import Product, ProductVariant, Color, Size | |
| measurements = { | |
| 'height': float(body_scan.height), | |
| 'chest': float(body_scan.chest), | |
| 'waist': float(body_scan.waist), | |
| 'shoulder_width': float(body_scan.shoulder_width), | |
| } | |
| if body_scan.hip: | |
| measurements['hip'] = float(body_scan.hip) | |
| if body_scan.inseam: | |
| measurements['inseam'] = float(body_scan.inseam) | |
| if body_scan.torso_length: | |
| measurements['torso_length'] = float(body_scan.torso_length) | |
| if body_scan.arm_length: | |
| measurements['arm_length'] = float(body_scan.arm_length) | |
| body_shape = getattr(body_scan, 'body_shape', 'rectangle') or 'rectangle' | |
| undertone = getattr(body_scan, 'undertone', 'warm') | |
| # Gemini-powered recommendations | |
| color_rec = self.recommend_colors(body_scan.skin_tone, undertone) | |
| rec_shirt_name = color_rec['recommended_shirt'] | |
| rec_pants_name = color_rec['recommended_pants'] | |
| recommended_fit = 'regular' # fit_type field removed | |
| # Map colour names β Color objects | |
| rec_shirt_color = Color.objects.filter(name=rec_shirt_name).first() | |
| rec_pants_color = Color.objects.filter(name=rec_pants_name).first() | |
| # Filter products | |
| if gender and gender in ['men', 'women']: | |
| products = Product.objects.filter( | |
| Q(gender=gender) | Q(gender='unisex') | |
| ) | |
| else: | |
| products = Product.objects.all() | |
| matching_products = [] | |
| for product in products: | |
| rec_size = self.recommend_size( | |
| measurements, | |
| garment_type=product.category, | |
| body_shape=body_shape, | |
| ) | |
| # Choose the right recommended colour for the product category | |
| is_top = product.category in ('shirt', 'jacket', 'dress') | |
| target_color = rec_shirt_color if is_top else rec_pants_color | |
| target_color_name = rec_shirt_name if is_top else rec_pants_name | |
| fit_matches = True # fit_type field removed | |
| # Priority 1: Exact size + recommended colour + in stock | |
| if target_color: | |
| variant = ProductVariant.objects.filter( | |
| product=product, | |
| size__name=rec_size, | |
| color=target_color, | |
| inventory__quantity__gt=0, | |
| ).select_related('size', 'color', 'product').first() | |
| if variant: | |
| matching_products.append({ | |
| 'product': product, | |
| 'variant': variant, | |
| 'recommended_size': rec_size, | |
| 'recommended_color': variant.color.name, | |
| 'color_hex': variant.color.hex_code, | |
| 'is_perfect_match': True, | |
| 'fit_matches_recommendation': fit_matches, | |
| 'recommended_fit': recommended_fit, | |
| 'fit_message': f"This {product.category} in size {rec_size} with {variant.color.name} will fit you perfectly!", | |
| }) | |
| continue | |
| # Priority 2: Exact size + any colour in stock | |
| fallback_variant = ProductVariant.objects.filter( | |
| product=product, | |
| size__name=rec_size, | |
| inventory__quantity__gt=0, | |
| ).select_related('size', 'color', 'product').first() | |
| if fallback_variant: | |
| matching_products.append({ | |
| 'product': product, | |
| 'variant': fallback_variant, | |
| 'recommended_size': rec_size, | |
| 'recommended_color': fallback_variant.color.name, | |
| 'color_hex': fallback_variant.color.hex_code, | |
| 'is_perfect_match': False, | |
| 'fit_matches_recommendation': fit_matches, | |
| 'recommended_fit': recommended_fit, | |
| 'fit_message': f"This {product.category} in size {rec_size} will fit you great!", | |
| }) | |
| matching_products.sort(key=lambda x: ( | |
| not x['fit_matches_recommendation'], | |
| not x['is_perfect_match'], | |
| x['product'].name, | |
| )) | |
| return matching_products[:limit] | |
| # ββ Generate & save Recommendation rows βββββββββββββββββββββββ | |
| def generate_recommendations_for_scan(self, body_scan) -> List[object]: | |
| from fitting_system.models import Recommendation | |
| measurements = { | |
| 'height': float(body_scan.height), | |
| 'chest': float(body_scan.chest), | |
| 'waist': float(body_scan.waist), | |
| 'shoulder_width': float(body_scan.shoulder_width), | |
| } | |
| if body_scan.hip: | |
| measurements['hip'] = float(body_scan.hip) | |
| if body_scan.torso_length: | |
| measurements['torso_length'] = float(body_scan.torso_length) | |
| if body_scan.arm_length: | |
| measurements['arm_length'] = float(body_scan.arm_length) | |
| if body_scan.inseam: | |
| measurements['inseam'] = float(body_scan.inseam) | |
| body_shape = getattr(body_scan, 'body_shape', 'rectangle') or 'rectangle' | |
| undertone = getattr(body_scan, 'undertone', 'warm') | |
| # Gemini | |
| base_recommended_size = self.recommend_size(measurements, body_shape=body_shape) | |
| recommended_fit = self.recommend_fit(measurements, body_shape=body_shape) | |
| color_rec = self.recommend_colors(body_scan.skin_tone, undertone) | |
| recommended_colors_str = f"{color_rec['recommended_shirt']}, {color_rec['recommended_pants']}" | |
| # Product recommendations across genders | |
| product_recommendations = [] | |
| for gender in ['men', 'women', 'unisex']: | |
| recs = self._recommend_products( | |
| measurements, body_scan.skin_tone, undertone, | |
| gender=gender, body_shape=body_shape, limit=10, | |
| ) | |
| product_recommendations.extend(recs) | |
| # Dedupe | |
| seen = set() | |
| unique = [] | |
| for product, priority in product_recommendations: | |
| if product.id not in seen: | |
| seen.add(product.id) | |
| unique.append((product, priority)) | |
| unique.sort(key=lambda x: x[1], reverse=True) | |
| # Create Recommendation objects | |
| recs_created = [] | |
| for product, priority in unique[:10]: | |
| rec_size = self.recommend_size( | |
| measurements, garment_type=product.category, body_shape=body_shape, | |
| ) | |
| rec = Recommendation.objects.create( | |
| body_scan=body_scan, | |
| product=product, | |
| recommended_size=rec_size, | |
| recommended_fit=recommended_fit, | |
| recommended_colors=recommended_colors_str, | |
| priority=priority, | |
| ) | |
| recs_created.append(rec) | |
| return recs_created | |
| def _recommend_products( | |
| self, measurements, skin_tone, undertone, | |
| gender='unisex', body_shape='rectangle', limit=10, | |
| ) -> List[Tuple[object, int]]: | |
| from fitting_system.models import Product, ProductVariant, Color | |
| recommended_size = self.recommend_size(measurements, body_shape=body_shape) | |
| recommended_fit = self.recommend_fit(measurements, body_shape=body_shape) | |
| color_rec = self.recommend_colors(skin_tone, undertone) | |
| rec_shirt_color = Color.objects.filter(name=color_rec['recommended_shirt']).first() | |
| rec_pants_color = Color.objects.filter(name=color_rec['recommended_pants']).first() | |
| products = Product.objects.filter( | |
| Q(gender=gender) | Q(gender='unisex') | |
| ) | |
| recommendations = [] | |
| for product in products: | |
| available = ProductVariant.objects.filter( | |
| product=product, inventory__quantity__gt=0, | |
| ) | |
| if not available.exists(): | |
| continue | |
| priority = 5 # base | |
| if available.filter(size__name=recommended_size).exists(): | |
| priority += 10 | |
| is_top = product.category in ('shirt', 'jacket', 'dress') | |
| target = rec_shirt_color if is_top else rec_pants_color | |
| if target and available.filter(color=target).exists(): | |
| priority += 10 | |
| recommendations.append((product, priority)) | |
| recommendations.sort(key=lambda x: x[1], reverse=True) | |
| return recommendations[:limit] | |
| # _fallback_recommend_size REMOVED β Gemini AI is the sole source. | |