ammar101's picture
Deploy application code and models
0bb49b0
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.db.models import Q
from django.utils.translation import gettext as _
import json
import base64
import re
import numpy as np
import cv2
from io import BytesIO
from PIL import Image
import logging
logger = logging.getLogger(__name__)
from .models import Product, ProductVariant, Inventory, BodyScan, Recommendation, Size, Color
from .ai_modules.yolo_analyzer import get_yolo_analyzer
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def decode_base64_image(base64_string):
"""Decode base64 image to numpy array (BGR)."""
if not base64_string:
return None
if ',' in base64_string:
base64_string = base64_string.split(',')[1]
image_data = base64.b64decode(base64_string)
pil_image = Image.open(BytesIO(image_data))
image_array = np.array(pil_image)
if len(image_array.shape) == 3 and image_array.shape[2] == 3:
return cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR)
return image_array
SKIN_TONE_LABELS = {
'very_light': _("Very Light"),
'light': _("Light"),
'intermediate': _("Intermediate"),
'tan': _("Tan"),
'dark': _("Dark"),
}
UNDERTONE_LABELS = {
'warm': _("Warm"),
'cool': _("Cool"),
}
def _translate_dynamic_label(value: str) -> str:
"""
Translate dynamic labels from DB.
Handles both plain strings and mistakenly stored template tags
like "{% trans 'AVAILABLE COLOR' %}".
"""
if not value:
return value
cleaned = value.strip()
m = re.fullmatch(r"\{\%\s*trans\s+['\"](.+?)['\"]\s*\%\}", cleaned, flags=re.IGNORECASE)
if m:
cleaned = m.group(1).strip()
# Be tolerant of malformed template-like strings saved in DB
elif '{%' in cleaned and '%}' in cleaned:
q = re.search(r"['\"](.+?)['\"]", cleaned)
if q:
cleaned = q.group(1).strip()
return _(cleaned)
# ---------------------------------------------------------------------------
# Page views
# ---------------------------------------------------------------------------
def index(request):
"""Landing page"""
return render(request, 'index.html')
def scan(request):
"""Camera interface and scanning workflow"""
return render(request, 'scan.html')
# ---------------------------------------------------------------------------
# API: real-time frame analysis
# ---------------------------------------------------------------------------
@csrf_exempt
def analyze_frame(request):
"""
Analyse a single camera frame for real-time feedback.
mode='body' → YOLO pose detection (body step)
mode='face' → face detection (skin-tone selfie step)
"""
if request.method != 'POST':
return JsonResponse({'error': _('POST method required')}, status=400)
try:
data = json.loads(request.body)
image_data = data.get('image')
mode = data.get('mode', 'body')
if not image_data:
return JsonResponse({'error': _('Image is required')}, status=400)
image = decode_base64_image(image_data)
analyzer = get_yolo_analyzer()
if mode == 'face':
result = analyzer.analyze_face_frame(image)
else:
result = analyzer.analyze_pose_frame(image)
return JsonResponse(result)
except Exception as e:
return JsonResponse({'error': str(e), 'detected': False}, status=500)
# ---------------------------------------------------------------------------
# API: process full scan (body image + face image)
# ---------------------------------------------------------------------------
@csrf_exempt
def process_scan(request):
"""
Process two captured images:
front_image – full-body front view → YOLO pose + measurements
face_image – close-up selfie → skin-tone extraction
Then ask the LLM for a recommended size letter.
"""
if request.method != 'POST':
return JsonResponse({'error': _('POST method required')}, status=400)
try:
data = json.loads(request.body)
front_image_data = data.get('front_image')
face_image_data = data.get('face_image')
side_image_data = data.get('side_image') # kept for backward compat
user_height_cm = data.get('user_height_cm')
if not front_image_data:
return JsonResponse({'error': _('Front (body) image is required')}, status=400)
if not face_image_data:
return JsonResponse({'error': _('Face image is required for skin tone detection')}, status=400)
# Validate user height
if user_height_cm is None:
return JsonResponse({'error': _('Height is required. Please enter your height in cm.')}, status=400)
try:
user_height_cm = float(user_height_cm)
except (TypeError, ValueError):
return JsonResponse({'error': _('Height must be a valid number in cm.')}, status=400)
if user_height_cm < 100 or user_height_cm > 250:
return JsonResponse({'error': _('Height must be between 100 and 250 cm.')}, status=400)
front_image = decode_base64_image(front_image_data)
face_image = decode_base64_image(face_image_data)
analyzer = get_yolo_analyzer()
# Full pipeline: measurements + skin tone + LLM size
analysis = analyzer.full_analysis(
body_image_bgr=front_image,
face_image_bgr=face_image,
user_height_cm=user_height_cm,
)
measurements = analysis['measurements']
skin_tone = analysis['skin_tone']
undertone = analysis['undertone']
recommended_size = analysis['recommended_size']
confidence = analysis.get('confidence', 0.85)
# Persist to DB
body_scan = BodyScan.objects.create(
height = measurements.get('height', 170),
shoulder_width = measurements.get('shoulder_width', 42),
chest = measurements.get('chest', 92),
waist = measurements.get('waist', 78),
hip = measurements.get('hip'),
torso_length = measurements.get('torso_length'),
arm_length = measurements.get('arm_length'),
inseam = measurements.get('inseam'),
body_shape = 'rectangle', # not used in new flow
skin_tone = skin_tone,
undertone = undertone,
confidence_score= confidence,
frame_count = 1,
)
# Store the LLM-recommended size as a Recommendation record
# (we create one generic record so the recommendations view can read it)
try:
Recommendation.objects.create(
body_scan = body_scan,
product = Product.objects.first(), # placeholder
recommended_size = recommended_size,
recommended_fit = 'regular',
recommended_colors= '',
priority = 100,
)
except Exception:
pass # no products in DB yet – that's fine
return JsonResponse({
'success': True,
'session_id': str(body_scan.session_id),
'skin_tone': skin_tone,
'skin_tone_display': SKIN_TONE_LABELS.get(skin_tone, skin_tone.replace('_', ' ').title()),
'undertone': undertone,
'recommended_size': recommended_size,
'confidence': round(confidence, 2),
})
except ValueError as e:
logger.error(f"Validation error in process_scan: {e}")
return JsonResponse({'error': str(e)}, status=400)
except RuntimeError as e:
logger.error(f"Runtime error in process_scan: {e}")
return JsonResponse({'error': str(e)}, status=503)
except Exception as e:
logger.exception(f"Unexpected error in process_scan: {e}")
return JsonResponse({'error': _('Processing failed: %(error)s') % {'error': str(e)}}, status=500)
# ---------------------------------------------------------------------------
# API: process women scan (manual measurements + hand image)
# ---------------------------------------------------------------------------
@csrf_exempt
def process_scan_women(request):
"""
Process women's scan:
hand_image – photo of hand → skin-tone extraction
measurements – manually entered body measurements (dict)
Then ask the LLM for a recommended size letter.
"""
if request.method != 'POST':
return JsonResponse({'error': _('POST method required')}, status=400)
try:
data = json.loads(request.body)
hand_image_data = data.get('hand_image')
measurements = data.get('measurements', {})
if not hand_image_data:
return JsonResponse({'error': _('Hand image is required for skin tone detection')}, status=400)
# Validate required measurements
required_fields = ['height', 'chest', 'waist', 'hip']
field_labels = {
'height': _("Height"),
'chest': _("Bust / Chest"),
'waist': _("Waist"),
'hip': _("Hip"),
}
for field in required_fields:
val = measurements.get(field)
if val is None:
return JsonResponse({'error': _('%(field)s is required.') % {'field': field_labels.get(field, field.title())}}, status=400)
try:
measurements[field] = float(val)
except (TypeError, ValueError):
return JsonResponse({'error': _('%(field)s must be a valid number.') % {'field': field_labels.get(field, field.title())}}, status=400)
# Convert optional fields to float if present
optional_fields = ['shoulder_width', 'inseam', 'arm_length', 'torso_length']
for field in optional_fields:
val = measurements.get(field)
if val is not None and val != '':
try:
measurements[field] = float(val)
except (TypeError, ValueError):
measurements.pop(field, None)
else:
measurements.pop(field, None)
# Validate height range
height = measurements['height']
if height < 100 or height > 250:
return JsonResponse({'error': _('Height must be between 100 and 250 cm.')}, status=400)
hand_image = decode_base64_image(hand_image_data)
analyzer = get_yolo_analyzer()
# Women pipeline: manual measurements + hand skin tone + LLM size
analysis = analyzer.women_analysis(
measurements=measurements,
hand_image_bgr=hand_image,
)
skin_tone = analysis['skin_tone']
undertone = analysis['undertone']
recommended_size = analysis['recommended_size']
confidence = analysis.get('confidence', 0.90)
# Persist to DB
body_scan = BodyScan.objects.create(
height = measurements.get('height', 170),
shoulder_width = measurements.get('shoulder_width', 0),
chest = measurements.get('chest', 0),
waist = measurements.get('waist', 0),
hip = measurements.get('hip'),
torso_length = measurements.get('torso_length'),
arm_length = measurements.get('arm_length'),
inseam = measurements.get('inseam'),
body_shape = 'hourglass',
skin_tone = skin_tone,
undertone = undertone,
confidence_score= confidence,
frame_count = 0,
)
# Store as Recommendation record
try:
Recommendation.objects.create(
body_scan = body_scan,
product = Product.objects.first(),
recommended_size = recommended_size,
recommended_fit = 'regular',
recommended_colors= '',
priority = 100,
)
except Exception:
pass
return JsonResponse({
'success': True,
'session_id': str(body_scan.session_id),
'skin_tone': skin_tone,
'skin_tone_display': SKIN_TONE_LABELS.get(skin_tone, skin_tone.replace('_', ' ').title()),
'undertone': undertone,
'recommended_size': recommended_size,
'confidence': round(confidence, 2),
})
except ValueError as e:
logger.error(f"Validation error in process_scan_women: {e}")
return JsonResponse({'error': str(e)}, status=400)
except RuntimeError as e:
logger.error(f"Runtime error in process_scan_women: {e}")
return JsonResponse({'error': str(e)}, status=503)
except Exception as e:
logger.exception(f"Unexpected error in process_scan_women: {e}")
return JsonResponse({'error': _('Processing failed: %(error)s') % {'error': str(e)}}, status=500)
# ---------------------------------------------------------------------------
# Recommendations page
# ---------------------------------------------------------------------------
def recommendations(request, session_id):
"""Display AI results and matching store products."""
body_scan = get_object_or_404(BodyScan, session_id=session_id)
# Retrieve the LLM-recommended size from the first Recommendation record
first_rec = body_scan.recommendations.first()
recommended_size = first_rec.recommended_size if first_rec else 'M'
# Enforce scan-derived gender so recommendations are gender-specific.
# frame_count == 0 is used by this project for women/manual flow.
gender = 'women' if body_scan.frame_count == 0 else 'men'
# Get skin-tone-recommended color names so product cards sync with avatar
from .color_palettes import get_shirt_color_names, get_pants_color_names
preferred_colors = (
get_shirt_color_names(body_scan.skin_tone)
+ get_pants_color_names(body_scan.skin_tone)
)
# Match products whose variants have the recommended size in stock
matching_products = _get_matching_products(
recommended_size, gender, preferred_colors=preferred_colors, limit=12,
)
context = {
'body_scan': body_scan,
'recommended_size': recommended_size,
'skin_tone_display': SKIN_TONE_LABELS.get(
body_scan.skin_tone,
body_scan.skin_tone.replace('_', ' ').title(),
),
'undertone_display': UNDERTONE_LABELS.get(
body_scan.undertone,
body_scan.undertone.title(),
),
'matching_products': matching_products,
'selected_gender': gender,
}
return render(request, 'recommendations.html', context)
def _get_matching_products(recommended_size: str, gender=None, preferred_colors=None, limit=12):
"""
Return products that have the recommended size in stock.
Each item in the list is a dict with product + variant info.
When preferred_colors is provided, prefer variants whose color matches
the skin-tone palette so product cards sync with the avatar.
"""
category_labels = {
'shirt': _("Shirt"),
'pants': _("Pants"),
'jacket': _("Jacket"),
'dress': _("Dress"),
'skirt': _("Skirt"),
't-shirt': _("T-Shirt"),
'tshirt': _("T-Shirt"),
'jeans': _("Jeans"),
}
if gender and gender in ['men', 'women']:
products = Product.objects.filter(gender=gender)
else:
products = Product.objects.all()
preferred_set = set(preferred_colors) if preferred_colors else set()
results = []
for product in products:
base_qs = ProductVariant.objects.filter(
product=product,
size__name=recommended_size,
inventory__quantity__gt=0,
).select_related('size', 'color', 'product')
# Try to find a variant whose color is in the preferred palette first
variant = None
if preferred_set:
variant = base_qs.filter(color__name__in=preferred_set).first()
# Fall back to any available variant
if not variant:
variant = base_qs.first()
if variant:
results.append({
'product': product,
'product_name': _(product.name),
'category_label': category_labels.get(product.category, product.category.title()),
'variant': variant,
'recommended_size': recommended_size,
'color_name': _translate_dynamic_label(variant.color.name),
'color_hex': variant.color.hex_code,
})
return results[:limit]
# ---------------------------------------------------------------------------
# Avatar page (3D avatar viewer with skin tone & color recommendations)
# ---------------------------------------------------------------------------
# Map BodyScan skin_tone values → avatar swatch index & hex
SKIN_TONE_MAP = {
'very_light': {'index': 0, 'hex': 'fde8d0', 'label': 'Porcelain'},
'light': {'index': 1, 'hex': 'f5cba7', 'label': 'Ivory'},
'intermediate': {'index': 2, 'hex': 'e8a87c', 'label': 'Peach'},
'tan': {'index': 3, 'hex': 'c68642', 'label': 'Tan'},
'dark': {'index': 4, 'hex': '8d5524', 'label': 'Brown'},
}
def avatar(request, session_id):
"""3D avatar page with skin tone and color recommendations."""
body_scan = get_object_or_404(BodyScan, session_id=session_id)
# Retrieve recommended size
first_rec = body_scan.recommendations.first()
recommended_size = first_rec.recommended_size if first_rec else 'M'
# Determine gender
gender = request.GET.get('gender', None)
if not gender:
gender = 'women' if body_scan.frame_count == 0 else 'men'
# Map skin tone to avatar swatch
skin_info = SKIN_TONE_MAP.get(body_scan.skin_tone, SKIN_TONE_MAP['intermediate'])
# ── Unified colour palette for this skin tone ──
from .color_palettes import get_shirt_colors, get_pants_colors, SKIN_TONE_PALETTES
import json as _json
skin_tone_key = body_scan.skin_tone
shirt_palette = get_shirt_colors(skin_tone_key)
pants_palette = get_pants_colors(skin_tone_key)
# Try to get Gemini-recommended defaults
rec_shirt_hex = shirt_palette[0]['hex'] # fallback to first
rec_pants_hex = pants_palette[0]['hex'] # fallback to first
try:
from .ai_modules.gemini_client import get_gemini_client
gemini = get_gemini_client()
if gemini.available:
color_rec = gemini.get_color_recommendations(
skin_tone=skin_tone_key,
undertone=body_scan.undertone,
)
rec_shirt_name = color_rec.get('recommended_shirt', '')
rec_pants_name = color_rec.get('recommended_pants', '')
# Map names → hex codes
for c in shirt_palette:
if c['name'] == rec_shirt_name:
rec_shirt_hex = c['hex']
break
for c in pants_palette:
if c['name'] == rec_pants_name:
rec_pants_hex = c['hex']
break
except Exception as e:
logger.warning(f"Failed to get Gemini color recommendation for avatar: {e}")
# Build full palettes JSON for all 5 skin tones (for skin-swatch switching)
palettes_for_js = {}
for st_key, palette in SKIN_TONE_PALETTES.items():
palettes_for_js[st_key] = {
'shirts': [{'hex': c['hex'].lstrip('#'), 'tip': c['name']} for c in palette['shirts']],
'pants': [{'hex': c['hex'].lstrip('#'), 'tip': c['name']} for c in palette['pants']],
}
context = {
'body_scan': body_scan,
'session_id': str(session_id),
'recommended_size': recommended_size,
'gender': gender,
'skin_tone_index': skin_info['index'],
'skin_tone_hex': skin_info['hex'],
'skin_tone_label': skin_info['label'],
'skin_tone_display': SKIN_TONE_LABELS.get(
body_scan.skin_tone,
body_scan.skin_tone.replace('_', ' ').title(),
),
'undertone_display': UNDERTONE_LABELS.get(
body_scan.undertone,
body_scan.undertone.title(),
),
'skin_tone_key': skin_tone_key,
'rec_shirt_hex': rec_shirt_hex.lstrip('#'),
'rec_pants_hex': rec_pants_hex.lstrip('#'),
'palettes_json': _json.dumps(palettes_for_js),
}
return render(request, 'avatar.html', context)
# ---------------------------------------------------------------------------
# Store & inventory views (unchanged)
# ---------------------------------------------------------------------------
def inventory_dashboard(request):
"""Inventory management dashboard"""
variants = ProductVariant.objects.select_related(
'product', 'size', 'color', 'inventory'
).all()
in_stock, low_stock, out_of_stock = [], [], []
for variant in variants:
variant.localized_product_name = _(variant.product.name)
variant.localized_color_name = _translate_dynamic_label(variant.color.name)
try:
if variant.inventory.is_out_of_stock:
out_of_stock.append(variant)
elif variant.inventory.is_low_stock:
low_stock.append(variant)
else:
in_stock.append(variant)
except Inventory.DoesNotExist:
out_of_stock.append(variant)
context = {
'in_stock': in_stock,
'low_stock': low_stock,
'out_of_stock': out_of_stock,
'total_variants': variants.count(),
}
return render(request, 'inventory.html', context)
def store(request):
"""Online store product catalog"""
category = request.GET.get('category', '')
gender = request.GET.get('gender', '')
search = request.GET.get('search', '')
products = Product.objects.all()
if category:
products = products.filter(category=category)
if gender:
products = products.filter(Q(gender=gender) | Q(gender='unisex'))
if search:
products = products.filter(
Q(name__icontains=search) | Q(description__icontains=search)
)
category_labels = {
'shirt': _("Shirt"),
'pants': _("Pants"),
'jacket': _("Jacket"),
'dress': _("Dress"),
'skirt': _("Skirt"),
't-shirt': _("T-Shirt"),
'tshirt': _("T-Shirt"),
'jeans': _("Jeans"),
}
gender_labels = {
'men': _("Men"),
'women': _("Women"),
'unisex': _("Unisex"),
}
products = list(products)
for product in products:
product.localized_name = _(product.name)
product.localized_description = _(product.description)
product.localized_category = category_labels.get(product.category, product.category.title())
product.localized_gender = gender_labels.get(product.gender, product.gender.title())
categories = Product.objects.values_list('category', flat=True).distinct().order_by('category')
genders = Product.objects.values_list('gender', flat=True).distinct().order_by('gender')
category_options = [
{'value': c, 'label': category_labels.get(c, c.title())}
for c in categories
]
gender_options = [
{'value': g, 'label': gender_labels.get(g, g.title())}
for g in genders
]
context = {
'products': products,
'categories': categories,
'genders': genders,
'category_options': category_options,
'gender_options': gender_options,
'selected_category': category,
'selected_gender': gender,
'search_query': search,
}
return render(request, 'store.html', context)
def product_detail(request, product_id):
"""Product detail page"""
product = get_object_or_404(Product, id=product_id)
variants = product.variants.select_related('size', 'color', 'inventory').all()
available_sizes = set()
for variant in variants:
try:
if variant.inventory.is_available:
available_sizes.add(variant.size)
except Inventory.DoesNotExist:
pass
related_products = Product.objects.filter(
category=product.category
).exclude(id=product.id)[:4]
context = {
'product': product,
'variants': variants,
'available_sizes': sorted(available_sizes, key=lambda x: x.id),
'related_products': related_products,
}
return render(request, 'product_detail.html', context)
def api_inventory(request):
"""API endpoint for inventory data"""
variants = ProductVariant.objects.select_related(
'product', 'size', 'color', 'inventory'
).all()
data = []
for variant in variants:
try:
inv = variant.inventory
data.append({
'id': variant.id,
'product': variant.product.name,
'size': variant.size.name,
'color': variant.color.name,
'quantity': inv.quantity,
'is_low_stock': inv.is_low_stock,
'is_out_of_stock': inv.is_out_of_stock,
})
except Inventory.DoesNotExist:
data.append({
'id': variant.id,
'product': variant.product.name,
'size': variant.size.name,
'color': variant.color.name,
'quantity': 0,
'is_low_stock': False,
'is_out_of_stock': True,
})
return JsonResponse({'inventory': data})