Spaces:
Sleeping
Sleeping
Deploy application code and models
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +18 -0
- .gemini/implementation_plan_store_recommendations.md +392 -0
- .gitignore +10 -0
- Dockerfile +41 -0
- HF_Deployment_Report.md +70 -0
- README.md +232 -0
- YOLOv8_Body_Measurement_and_Clothing_Recommendation_Updated (1).ipynb +0 -0
- avatar/FEMALE.glb +3 -0
- avatar/MALE.glb +3 -0
- avatar/index.html +699 -0
- compile_locale.py +31 -0
- config/__init__.py +0 -0
- config/asgi.py +16 -0
- config/settings.py +152 -0
- config/urls.py +38 -0
- config/wsgi.py +16 -0
- debug_gemini.py +37 -0
- fitting_system/__init__.py +0 -0
- fitting_system/admin.py +92 -0
- fitting_system/ai_modules/__init__.py +1 -0
- fitting_system/ai_modules/body_measurement.py +310 -0
- fitting_system/ai_modules/gemini_client.py +282 -0
- fitting_system/ai_modules/recommendation_engine.py +322 -0
- fitting_system/ai_modules/yolo_analyzer.py +564 -0
- fitting_system/apps.py +6 -0
- fitting_system/color_palettes.py +125 -0
- fitting_system/fixtures/initial_data.json +3296 -0
- fitting_system/management/__init__.py +1 -0
- fitting_system/management/commands/__init__.py +1 -0
- fitting_system/management/commands/populate_data.py +133 -0
- fitting_system/management/commands/populate_mvp.py +198 -0
- fitting_system/management/commands/reset_catalog.py +307 -0
- fitting_system/management/commands/set_password.py +18 -0
- fitting_system/migrations/0001_initial.py +123 -0
- fitting_system/migrations/0002_add_undertone_and_expand_skin_tone.py +23 -0
- fitting_system/migrations/0003_bodyscan_arm_length_bodyscan_body_shape_and_more.py +68 -0
- fitting_system/migrations/0004_bodyscan_error_message_bodyscan_is_fallback.py +23 -0
- fitting_system/migrations/0004_product_image.py +18 -0
- fitting_system/migrations/0005_remove_product_image_url.py +17 -0
- fitting_system/migrations/0006_merge_20260225_1734.py +14 -0
- fitting_system/migrations/0007_alter_product_image.py +18 -0
- fitting_system/migrations/0008_alter_product_image.py +19 -0
- fitting_system/migrations/0009_remove_product_fit_type.py +17 -0
- fitting_system/migrations/__init__.py +0 -0
- fitting_system/models.py +219 -0
- fitting_system/storage.py +39 -0
- fitting_system/templates/avatar.html +739 -0
- fitting_system/templates/base.html +411 -0
- fitting_system/templates/index.html +122 -0
- fitting_system/templates/inventory.html +179 -0
.dockerignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.gitignore
|
| 3 |
+
.vscode
|
| 4 |
+
__pycache__
|
| 5 |
+
*.pyc
|
| 6 |
+
*.pyo
|
| 7 |
+
*.pyd
|
| 8 |
+
*.sqlite3
|
| 9 |
+
db.sqlite3
|
| 10 |
+
.env
|
| 11 |
+
venv
|
| 12 |
+
media
|
| 13 |
+
staticfiles
|
| 14 |
+
pip_log.txt
|
| 15 |
+
conflicts.txt
|
| 16 |
+
*.ipynb
|
| 17 |
+
image testing
|
| 18 |
+
githup-projects
|
.gemini/implementation_plan_store_recommendations.md
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan: Store & Recommendation Integration (Updated)
|
| 2 |
+
|
| 3 |
+
## 📋 Project Analysis Summary
|
| 4 |
+
|
| 5 |
+
### Current State
|
| 6 |
+
- **Body Measurement Module**: Well-developed with MediaPipe integration
|
| 7 |
+
- **Skin Tone Analyzer**: Advanced ITA-based analysis with undertone detection
|
| 8 |
+
- **Recommendation Engine**: Generates recommendations but products display is basic
|
| 9 |
+
- **Store Page**: Has products but no connection to user's scan
|
| 10 |
+
- **Recommendations Page**: Shows measurements & colors (KEEP as-is for developer)
|
| 11 |
+
|
| 12 |
+
### Main Goal 🎯
|
| 13 |
+
**After scan → Show ACTUAL products from store with specific size & color recommendations**
|
| 14 |
+
|
| 15 |
+
Example Output:
|
| 16 |
+
> "This **Classic Cotton Shirt** in size **M** with **Olive Green** color will fit you perfectly!"
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 🛠️ Implementation Phases
|
| 21 |
+
|
| 22 |
+
### Phase 1: Enhanced Product Recommendations Display (PRIORITY: HIGH)
|
| 23 |
+
**Goal**: Show real products from store with exact size + color that fits the user
|
| 24 |
+
|
| 25 |
+
#### Current State (What we have):
|
| 26 |
+
```
|
| 27 |
+
Recommended Products
|
| 28 |
+
┌──────────────────────────────────────┐
|
| 29 |
+
│ [Icon] Product Name │
|
| 30 |
+
│ $XX.XX Size M │
|
| 31 |
+
│ [View Details] │
|
| 32 |
+
└──────────────────────────────────────┘
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
#### Target State (What we want):
|
| 36 |
+
```
|
| 37 |
+
Perfect Matches For You
|
| 38 |
+
┌──────────────────────────────────────┐
|
| 39 |
+
│ [PRODUCT IMAGE] │
|
| 40 |
+
│ │
|
| 41 |
+
│ Classic Cotton Shirt │
|
| 42 |
+
│ ✅ Your Size: M │
|
| 43 |
+
│ 🎨 Your Color: Olive Green │
|
| 44 |
+
│ $45.00 │
|
| 45 |
+
│ │
|
| 46 |
+
│ "This shirt will fit you perfectly!" │
|
| 47 |
+
│ │
|
| 48 |
+
│ [View Product] [Add to Cart] │
|
| 49 |
+
└──────────────────────────────────────┘
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
## 📁 Files to Modify
|
| 55 |
+
|
| 56 |
+
| File | Changes |
|
| 57 |
+
|------|---------|
|
| 58 |
+
| `recommendation_engine.py` | Enhance `generate_recommendations_for_scan()` to include matching colors |
|
| 59 |
+
| `views.py` | Update `recommendations` view to pass richer data |
|
| 60 |
+
| `recommendations.html` | Redesign product cards with size + color + fit message |
|
| 61 |
+
| `models.py` | (Optional) Add recommended_color field to Recommendation model |
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## 🔧 Detailed Implementation
|
| 66 |
+
|
| 67 |
+
### Step 1: Update Recommendation Engine
|
| 68 |
+
|
| 69 |
+
**File**: `fitting_system/ai_modules/recommendation_engine.py`
|
| 70 |
+
|
| 71 |
+
Add method to get actual matching product variants:
|
| 72 |
+
|
| 73 |
+
```python
|
| 74 |
+
def get_matching_product_variants(
|
| 75 |
+
self,
|
| 76 |
+
body_scan,
|
| 77 |
+
limit: int = 6
|
| 78 |
+
) -> List[Dict]:
|
| 79 |
+
"""
|
| 80 |
+
Get actual products with specific size and color recommendations.
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
List of dicts with product, size, color, and fit message
|
| 84 |
+
"""
|
| 85 |
+
from fitting_system.models import Product, ProductVariant, Color, Size
|
| 86 |
+
|
| 87 |
+
# Build measurements dict
|
| 88 |
+
measurements = {
|
| 89 |
+
'height': float(body_scan.height),
|
| 90 |
+
'chest': float(body_scan.chest),
|
| 91 |
+
'waist': float(body_scan.waist),
|
| 92 |
+
'shoulder_width': float(body_scan.shoulder_width)
|
| 93 |
+
}
|
| 94 |
+
if body_scan.hip:
|
| 95 |
+
measurements['hip'] = float(body_scan.hip)
|
| 96 |
+
if body_scan.inseam:
|
| 97 |
+
measurements['inseam'] = float(body_scan.inseam)
|
| 98 |
+
|
| 99 |
+
body_shape = getattr(body_scan, 'body_shape', 'rectangle') or 'rectangle'
|
| 100 |
+
undertone = getattr(body_scan, 'undertone', 'warm')
|
| 101 |
+
|
| 102 |
+
# Get recommended colors for user's skin tone
|
| 103 |
+
recommended_color_names = self.recommend_colors(body_scan.skin_tone, undertone)
|
| 104 |
+
recommended_fit = self.recommend_fit(measurements)
|
| 105 |
+
|
| 106 |
+
# Find matching products
|
| 107 |
+
matching_products = []
|
| 108 |
+
|
| 109 |
+
# Query products with matching fit
|
| 110 |
+
products = Product.objects.filter(fit_type=recommended_fit)
|
| 111 |
+
|
| 112 |
+
for product in products:
|
| 113 |
+
# Get garment-specific size
|
| 114 |
+
rec_size = self.recommend_size_for_garment(
|
| 115 |
+
measurements,
|
| 116 |
+
product.category,
|
| 117 |
+
body_shape
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
# Find variant with this size AND a recommended color
|
| 121 |
+
# Priority 1: Exact size + recommended color + in stock
|
| 122 |
+
matching_variant = ProductVariant.objects.filter(
|
| 123 |
+
product=product,
|
| 124 |
+
size__name=rec_size,
|
| 125 |
+
color__name__in=recommended_color_names,
|
| 126 |
+
inventory__quantity__gt=0
|
| 127 |
+
).select_related('size', 'color', 'product').first()
|
| 128 |
+
|
| 129 |
+
if matching_variant:
|
| 130 |
+
matching_products.append({
|
| 131 |
+
'product': product,
|
| 132 |
+
'variant': matching_variant,
|
| 133 |
+
'recommended_size': rec_size,
|
| 134 |
+
'recommended_color': matching_variant.color.name,
|
| 135 |
+
'color_hex': matching_variant.color.hex_code,
|
| 136 |
+
'fit_type': recommended_fit,
|
| 137 |
+
'is_perfect_match': True,
|
| 138 |
+
'fit_message': f"This {product.category} in size {rec_size} with {matching_variant.color.name} will fit you perfectly!"
|
| 139 |
+
})
|
| 140 |
+
continue
|
| 141 |
+
|
| 142 |
+
# Priority 2: Exact size + any color in stock
|
| 143 |
+
size_only_variant = ProductVariant.objects.filter(
|
| 144 |
+
product=product,
|
| 145 |
+
size__name=rec_size,
|
| 146 |
+
inventory__quantity__gt=0
|
| 147 |
+
).select_related('size', 'color', 'product').first()
|
| 148 |
+
|
| 149 |
+
if size_only_variant:
|
| 150 |
+
matching_products.append({
|
| 151 |
+
'product': product,
|
| 152 |
+
'variant': size_only_variant,
|
| 153 |
+
'recommended_size': rec_size,
|
| 154 |
+
'recommended_color': size_only_variant.color.name,
|
| 155 |
+
'color_hex': size_only_variant.color.hex_code,
|
| 156 |
+
'fit_type': recommended_fit,
|
| 157 |
+
'is_perfect_match': False,
|
| 158 |
+
'fit_message': f"This {product.category} in size {rec_size} will fit you great!"
|
| 159 |
+
})
|
| 160 |
+
|
| 161 |
+
# Sort: perfect matches first
|
| 162 |
+
matching_products.sort(key=lambda x: x['is_perfect_match'], reverse=True)
|
| 163 |
+
|
| 164 |
+
return matching_products[:limit]
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
---
|
| 168 |
+
|
| 169 |
+
### Step 2: Update Recommendations View
|
| 170 |
+
|
| 171 |
+
**File**: `fitting_system/views.py`
|
| 172 |
+
|
| 173 |
+
Update the `recommendations` view:
|
| 174 |
+
|
| 175 |
+
```python
|
| 176 |
+
def recommendations(request, session_id):
|
| 177 |
+
"""Display results and recommendations"""
|
| 178 |
+
body_scan = get_object_or_404(BodyScan, session_id=session_id)
|
| 179 |
+
|
| 180 |
+
rec_engine = RecommendationEngine()
|
| 181 |
+
undertone = getattr(body_scan, 'undertone', 'warm')
|
| 182 |
+
recommended_colors = rec_engine.recommend_colors(body_scan.skin_tone, undertone)
|
| 183 |
+
|
| 184 |
+
# Get actual matching products with size + color
|
| 185 |
+
matching_products = rec_engine.get_matching_product_variants(body_scan, limit=6)
|
| 186 |
+
|
| 187 |
+
# Get existing recommendations (for backward compatibility)
|
| 188 |
+
recommendations_list = body_scan.recommendations.all()
|
| 189 |
+
|
| 190 |
+
context = {
|
| 191 |
+
'body_scan': body_scan,
|
| 192 |
+
'recommendations': recommendations_list,
|
| 193 |
+
'matching_products': matching_products, # NEW: Actual products with size+color
|
| 194 |
+
'recommended_colors': recommended_colors[:5],
|
| 195 |
+
'recommended_size': recommendations_list.first().recommended_size if recommendations_list.exists() else 'N/A',
|
| 196 |
+
'recommended_fit': recommendations_list.first().recommended_fit if recommendations_list.exists() else 'N/A',
|
| 197 |
+
'skin_tone_display': body_scan.skin_tone.replace('_', ' ').title(),
|
| 198 |
+
'undertone_display': undertone.title(),
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
return render(request, 'recommendations.html', context)
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
---
|
| 205 |
+
|
| 206 |
+
### Step 3: Update Recommendations Template
|
| 207 |
+
|
| 208 |
+
**File**: `fitting_system/templates/recommendations.html`
|
| 209 |
+
|
| 210 |
+
Replace the "Recommended Products" section with enhanced cards:
|
| 211 |
+
|
| 212 |
+
```html
|
| 213 |
+
<!-- Actual Product Recommendations with Size + Color -->
|
| 214 |
+
<div class="bg-white rounded-2xl shadow-xl p-8 mb-8">
|
| 215 |
+
<h2 class="text-2xl font-bold text-gray-900 mb-2">🛍️ Perfect Matches For You</h2>
|
| 216 |
+
<p class="text-gray-600 mb-6">Products that fit your body and complement your skin tone</p>
|
| 217 |
+
|
| 218 |
+
{% if matching_products %}
|
| 219 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 220 |
+
{% for item in matching_products %}
|
| 221 |
+
<div class="border-2 {% if item.is_perfect_match %}border-green-300 bg-green-50{% else %}border-gray-200{% endif %} rounded-xl overflow-hidden hover:shadow-lg transition">
|
| 222 |
+
|
| 223 |
+
<!-- Perfect Match Badge -->
|
| 224 |
+
{% if item.is_perfect_match %}
|
| 225 |
+
<div class="bg-gradient-to-r from-green-500 to-emerald-500 text-white text-center py-2 text-sm font-bold">
|
| 226 |
+
✨ Perfect Match
|
| 227 |
+
</div>
|
| 228 |
+
{% endif %}
|
| 229 |
+
|
| 230 |
+
<!-- Product Image -->
|
| 231 |
+
<div class="aspect-square bg-gradient-to-br from-gray-50 to-gray-100 relative">
|
| 232 |
+
{% if item.product.category == 'shirt' %}
|
| 233 |
+
<img src="{% static 'images/products/mens_shirt.png' %}" alt="{{ item.product.name }}" class="w-full h-full object-cover">
|
| 234 |
+
{% elif item.product.category == 'pants' %}
|
| 235 |
+
<img src="{% static 'images/products/jeans.png' %}" alt="{{ item.product.name }}" class="w-full h-full object-cover">
|
| 236 |
+
{% elif item.product.category == 'dress' %}
|
| 237 |
+
<img src="{% static 'images/products/womens_dress.png' %}" alt="{{ item.product.name }}" class="w-full h-full object-cover">
|
| 238 |
+
{% elif item.product.category == 'jacket' %}
|
| 239 |
+
<img src="{% static 'images/products/jacket.png' %}" alt="{{ item.product.name }}" class="w-full h-full object-cover">
|
| 240 |
+
{% else %}
|
| 241 |
+
<img src="{% static 'images/products/mens_shirt.png' %}" alt="{{ item.product.name }}" class="w-full h-full object-cover">
|
| 242 |
+
{% endif %}
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
<!-- Product Info -->
|
| 246 |
+
<div class="p-5">
|
| 247 |
+
<h3 class="font-bold text-lg text-gray-900 mb-3">{{ item.product.name }}</h3>
|
| 248 |
+
|
| 249 |
+
<!-- Size Recommendation -->
|
| 250 |
+
<div class="flex items-center gap-2 mb-2">
|
| 251 |
+
<span class="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
| 252 |
+
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 253 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
| 254 |
+
</svg>
|
| 255 |
+
</span>
|
| 256 |
+
<span class="font-semibold text-gray-800">Your Size: <span class="text-indigo-600 text-lg">{{ item.recommended_size }}</span></span>
|
| 257 |
+
</div>
|
| 258 |
+
|
| 259 |
+
<!-- Color Recommendation -->
|
| 260 |
+
<div class="flex items-center gap-2 mb-3">
|
| 261 |
+
<span class="w-8 h-8 rounded-full border-2 border-white shadow" style="background-color: {{ item.color_hex }};"></span>
|
| 262 |
+
<span class="font-semibold text-gray-800">Your Color: <span class="text-purple-600">{{ item.recommended_color }}</span></span>
|
| 263 |
+
</div>
|
| 264 |
+
|
| 265 |
+
<!-- Price -->
|
| 266 |
+
<div class="text-2xl font-bold text-indigo-600 mb-3">${{ item.product.price }}</div>
|
| 267 |
+
|
| 268 |
+
<!-- Fit Message -->
|
| 269 |
+
<p class="text-sm text-green-700 bg-green-100 rounded-lg p-3 mb-4 italic">
|
| 270 |
+
"{{ item.fit_message }}"
|
| 271 |
+
</p>
|
| 272 |
+
|
| 273 |
+
<!-- Actions -->
|
| 274 |
+
<a href="{% url 'fitting_system:product_detail' item.product.id %}"
|
| 275 |
+
class="block w-full bg-gradient-to-r from-indigo-600 to-purple-600 text-white text-center py-3 rounded-lg font-medium hover:shadow-lg transition">
|
| 276 |
+
View Product Details
|
| 277 |
+
</a>
|
| 278 |
+
</div>
|
| 279 |
+
</div>
|
| 280 |
+
{% endfor %}
|
| 281 |
+
</div>
|
| 282 |
+
{% else %}
|
| 283 |
+
<div class="text-center py-12">
|
| 284 |
+
<svg class="w-16 h-16 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 285 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 286 |
+
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4">
|
| 287 |
+
</path>
|
| 288 |
+
</svg>
|
| 289 |
+
<p class="text-gray-600 text-lg">No matching products in stock right now.</p>
|
| 290 |
+
<p class="text-gray-500 mt-2">Check back later or browse our full catalog.</p>
|
| 291 |
+
</div>
|
| 292 |
+
{% endif %}
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
<!-- KEEP: Original measurements section for developer reference -->
|
| 296 |
+
<div class="bg-white rounded-2xl shadow-xl p-8 mb-8">
|
| 297 |
+
<h2 class="text-2xl font-bold text-gray-900 mb-6">Your Measurements</h2>
|
| 298 |
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
| 299 |
+
<div class="text-center p-4 bg-indigo-50 rounded-xl">
|
| 300 |
+
<p class="text-sm text-gray-600 mb-1">Height</p>
|
| 301 |
+
<p class="text-2xl font-bold text-indigo-600">{{ body_scan.height }} cm</p>
|
| 302 |
+
</div>
|
| 303 |
+
<div class="text-center p-4 bg-purple-50 rounded-xl">
|
| 304 |
+
<p class="text-sm text-gray-600 mb-1">Chest</p>
|
| 305 |
+
<p class="text-2xl font-bold text-purple-600">{{ body_scan.chest }} cm</p>
|
| 306 |
+
</div>
|
| 307 |
+
<div class="text-center p-4 bg-pink-50 rounded-xl">
|
| 308 |
+
<p class="text-sm text-gray-600 mb-1">Waist</p>
|
| 309 |
+
<p class="text-2xl font-bold text-pink-600">{{ body_scan.waist }} cm</p>
|
| 310 |
+
</div>
|
| 311 |
+
<div class="text-center p-4 bg-rose-50 rounded-xl">
|
| 312 |
+
<p class="text-sm text-gray-600 mb-1">Shoulders</p>
|
| 313 |
+
<p class="text-2xl font-bold text-rose-600">{{ body_scan.shoulder_width }} cm</p>
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
```
|
| 318 |
+
|
| 319 |
+
---
|
| 320 |
+
|
| 321 |
+
## 📅 Implementation Tasks
|
| 322 |
+
|
| 323 |
+
| # | Task | File | Time |
|
| 324 |
+
|---|------|------|------|
|
| 325 |
+
| 1 | Add `get_matching_product_variants()` method | `recommendation_engine.py` | 45 min |
|
| 326 |
+
| 2 | Update `recommendations` view | `views.py` | 15 min |
|
| 327 |
+
| 3 | Update recommendations template with new product cards | `recommendations.html` | 45 min |
|
| 328 |
+
| 4 | Test the complete flow | - | 30 min |
|
| 329 |
+
| **Total** | | | **~2 hours** |
|
| 330 |
+
|
| 331 |
+
---
|
| 332 |
+
|
| 333 |
+
## 🧪 Testing Flow
|
| 334 |
+
|
| 335 |
+
1. **Run the server**: `python manage.py runserver`
|
| 336 |
+
2. **Complete a body scan**: Go to `/scan/`
|
| 337 |
+
3. **View recommendations**: See actual products with:
|
| 338 |
+
- ✅ Product name
|
| 339 |
+
- ✅ Your recommended size (M, L, etc.)
|
| 340 |
+
- ✅ Your recommended color (with color swatch)
|
| 341 |
+
- ✅ Fit message: "This shirt in size M with Olive Green will fit you perfectly!"
|
| 342 |
+
4. **Click "View Product Details"** to see full product page
|
| 343 |
+
|
| 344 |
+
---
|
| 345 |
+
|
| 346 |
+
## 🎨 Visual Preview
|
| 347 |
+
|
| 348 |
+
### After Implementation:
|
| 349 |
+
|
| 350 |
+
```
|
| 351 |
+
┌─────────────────────────────────────────────────────────────────────┐
|
| 352 |
+
│ ��️ Perfect Matches For You │
|
| 353 |
+
│ Products that fit your body and complement your skin tone │
|
| 354 |
+
├─────────────────────┬─────────────────────┬─────────────────────────┤
|
| 355 |
+
│ ✨ Perfect Match │ ✨ Perfect Match │ │
|
| 356 |
+
│ ┌─────────────────┐ │ ┌─────────────────┐ │ ┌─────────────────┐ │
|
| 357 |
+
│ │ [SHIRT IMG] │ │ │ [PANTS IMG] │ │ │ [DRESS IMG] │ │
|
| 358 |
+
│ └─────────────────┘ │ └─────────────────┘ │ └─────────────────┘ │
|
| 359 |
+
│ │ │ │
|
| 360 |
+
│ Classic Cotton │ Slim Fit Chinos │ Elegant Midi │
|
| 361 |
+
│ │ │ │
|
| 362 |
+
│ ✅ Your Size: M │ ✅ Your Size: 32 │ ✅ Your Size: M │
|
| 363 |
+
│ 🎨 Color: Olive │ 🎨 Color: Navy │ 🎨 Color: Dusty Rose │
|
| 364 |
+
│ │ │ │
|
| 365 |
+
│ $45.00 │ $65.00 │ $85.00 │
|
| 366 |
+
│ │ │ │
|
| 367 |
+
│ "This shirt in │ "These pants in │ "This dress in size M │
|
| 368 |
+
│ size M with Olive │ size 32 with Navy │ will fit you great!" │
|
| 369 |
+
│ will fit you │ will fit you │ │
|
| 370 |
+
│ perfectly!" │ perfectly!" │ │
|
| 371 |
+
│ │ │ │
|
| 372 |
+
│ [View Product] │ [View Product] │ [View Product] │
|
| 373 |
+
└─────────────────────┴─────────────────────┴─────────────────────────┘
|
| 374 |
+
|
| 375 |
+
┌─────────────────────────────────────────────────────────────────────┐
|
| 376 |
+
│ Your Measurements (Dev View) │
|
| 377 |
+
│ Height: 175 cm │ Chest: 98 cm │ Waist: 82 cm │ Shoulders: 45 │
|
| 378 |
+
└─────────────────────────────────────────────────────────────────────┘
|
| 379 |
+
```
|
| 380 |
+
|
| 381 |
+
---
|
| 382 |
+
|
| 383 |
+
## 🚀 Ready to Implement?
|
| 384 |
+
|
| 385 |
+
This focused plan will:
|
| 386 |
+
1. ✅ Keep measurements visible for you (developer)
|
| 387 |
+
2. ✅ Show ACTUAL products from store
|
| 388 |
+
3. ✅ Display specific size recommendation per product
|
| 389 |
+
4. ✅ Show color that matches skin tone
|
| 390 |
+
5. ✅ Include friendly fit message
|
| 391 |
+
|
| 392 |
+
**Say "Start" and I'll begin implementing!**
|
.gitignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv/
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.sqlite3
|
| 5 |
+
db.sqlite3
|
| 6 |
+
media/
|
| 7 |
+
staticfiles/
|
| 8 |
+
.env
|
| 9 |
+
.DS_Store
|
| 10 |
+
.vscode/
|
Dockerfile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 4 |
+
PYTHONUNBUFFERED=1 \
|
| 5 |
+
PIP_NO_CACHE_DIR=1 \
|
| 6 |
+
PORT=7860
|
| 7 |
+
|
| 8 |
+
# Install required system packages for OpenCV
|
| 9 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 10 |
+
libgl1 \
|
| 11 |
+
libglib2.0-0 \
|
| 12 |
+
libsm6 \
|
| 13 |
+
libxext6 \
|
| 14 |
+
libxrender1 \
|
| 15 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
+
|
| 17 |
+
# Set up a new user called "user" with user ID 1000 (Required for Hugging Face Spaces)
|
| 18 |
+
RUN useradd -m -u 1000 user
|
| 19 |
+
USER user
|
| 20 |
+
ENV HOME=/home/user \
|
| 21 |
+
PATH=/home/user/.local/bin:$PATH
|
| 22 |
+
|
| 23 |
+
WORKDIR $HOME/app
|
| 24 |
+
|
| 25 |
+
# Copy requirements separately to cache the layer
|
| 26 |
+
COPY --chown=user requirements.txt ./
|
| 27 |
+
|
| 28 |
+
# Install python dependencies
|
| 29 |
+
RUN pip install --upgrade pip && \
|
| 30 |
+
pip install -r requirements.txt
|
| 31 |
+
|
| 32 |
+
# Copy project files with proper ownership
|
| 33 |
+
COPY --chown=user . .
|
| 34 |
+
|
| 35 |
+
# Create necessary directories with proper user permissions
|
| 36 |
+
RUN mkdir -p $HOME/app/staticfiles $HOME/app/media
|
| 37 |
+
|
| 38 |
+
EXPOSE 7860
|
| 39 |
+
|
| 40 |
+
# Run migrations, collect static files, and start Gunicorn (all as user 1000)
|
| 41 |
+
CMD ["sh", "-c", "python manage.py migrate && python manage.py collectstatic --noinput && gunicorn config.wsgi:application --bind 0.0.0.0:${PORT:-7860} --workers 2 --timeout 120"]
|
HF_Deployment_Report.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Deployment Report
|
| 2 |
+
|
| 3 |
+
## 1. Project Analysis & What Was Checked
|
| 4 |
+
I have comprehensively analyzed the project specifically targeting a deployment to **Hugging Face Spaces (Docker Plan)**. The free tier of Hugging Face Spaces provides up to 16GB of RAM, which is excellent to handle the YOLOv8 ML models `yolov8n-pose.pt` and `pose_landmarker.task` without crashing.
|
| 5 |
+
|
| 6 |
+
I reviewed the files you had already created:
|
| 7 |
+
* `Dockerfile`: The existing Dockerfile correctly used the `python:3.11-slim` base image and properly installed the OpenCV system dependencies (`libgl1`, `libglib2.0-0`), which was perfect!
|
| 8 |
+
* `.dockerignore`: Found that it correctly ignores virtual environments, caches, `.env` file, etc.
|
| 9 |
+
* `config/settings.py`: The core of the Django settings.
|
| 10 |
+
* `requirements.txt`: The python dependencies.
|
| 11 |
+
|
| 12 |
+
## 2. What I Updated
|
| 13 |
+
To make the deployment strictly compliant with Hugging Face's Docker Spaces architecture, I updated the following files:
|
| 14 |
+
|
| 15 |
+
**`Dockerfile`**
|
| 16 |
+
* **Added `useradd -m -u 1000 user`**: Hugging Face runs their containers natively as a non-root user (id 1000). To avoid "Permission Denied" errors when running `python manage.py collectstatic`, an exact user needed to be defined.
|
| 17 |
+
* **Added `--chown=user`**: Ensured that the newly created user specifically owns the copied files and installed directories.
|
| 18 |
+
|
| 19 |
+
**`requirements.txt`**
|
| 20 |
+
* **Added `gunicorn==21.2.0`**: Your production server framework.
|
| 21 |
+
* **Added `whitenoise==6.6.0`**: Required so your Django app can seamlessly serve static files without requiring an external Nginx server.
|
| 22 |
+
|
| 23 |
+
**`config/settings.py`**
|
| 24 |
+
* **Security & Hosts:** Replaced `ALLOWED_HOSTS = []` with `ALLOWED_HOSTS = ['*']` so Hugging Face infrastructure can correctly host your domain.
|
| 25 |
+
* **Form Submissions:** Added `CSRF_TRUSTED_ORIGINS = ['https://*.hf.space']` so POST requests (like uploading images inside the HF IFrame) are not rejected with 403 Forbidden errors.
|
| 26 |
+
* **Static Files:** Confirmed the integration of `whitenoise.middleware.WhiteNoiseMiddleware`.
|
| 27 |
+
|
| 28 |
+
## 3. Hugging Face Deployment Steps
|
| 29 |
+
|
| 30 |
+
You are now fully ready to deploy. Follow these precise steps:
|
| 31 |
+
|
| 32 |
+
1. **Create the Space:** Go to [Hugging Face Spaces](https://huggingface.co/spaces) and click **Create new Space**.
|
| 33 |
+
2. **Space Settings:**
|
| 34 |
+
* **Name:** `virtual-dressing` (or similar).
|
| 35 |
+
* **License:** *Optional* (e.g., MIT).
|
| 36 |
+
* **Select the Space SDK:** Choose **Docker**, then choose the **Blank** template.
|
| 37 |
+
* **Space Hardware:** Select standard Free tier (**16GB RAM, 2 vCPU**).
|
| 38 |
+
3. **Upload Files:** Connect your local repository to the Space using `git remote add huggingface <YOUR_HF_CLONE_URL>`, or manually upload all the project files into the Space's Files tab.
|
| 39 |
+
4. **Environment Secrets:** Once the files are uploaded, go to the Space's **Settings** tab. Scroll down to the **Variables and secrets** section and create New Secrets:
|
| 40 |
+
* `SECRET_KEY`: *Enter a random hard-to-guess string.*
|
| 41 |
+
* `DEBUG`: `False`
|
| 42 |
+
* `GEMINI_API_KEY`: *Your Google AI API key.*
|
| 43 |
+
5. **View Logs:** Wait for the container to say **"Building"**. You can click on the 'Logs' button on the top right to watch Gunicorn boot up on port 7860.
|
| 44 |
+
6. Once it says **"Running"**, your Virtual Dressing app is deployed and live!
|
| 45 |
+
|
| 46 |
+
*Note regarding ephemeral storage:* Any photos users upload or models saved to the SQLite DB will wipe when the Space goes to sleep (usually after 48h of user inactivity). To make the database persist, you'll want to configure *Hugging Face Persistent Storage* via the settings later.
|
| 47 |
+
|
| 48 |
+
## 4. Current Core Project Structure
|
| 49 |
+
Your project is tightly structured for Docker deployment:
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
virtual_dressing/
|
| 53 |
+
├── Dockerfile <-- Orchestrates the production setup
|
| 54 |
+
├── .dockerignore <-- Keeps Docker image small and secure
|
| 55 |
+
├── requirements.txt <-- Specifies needed dependencies
|
| 56 |
+
├── manage.py
|
| 57 |
+
├── db.sqlite3 <-- Database
|
| 58 |
+
├── yolov8n-pose.pt <-- Heavy YOLO Weights
|
| 59 |
+
├── pose_landmarker.task <-- MediaPipe Task weights
|
| 60 |
+
├── appDataDir/ <-- Optional AI context & configuration
|
| 61 |
+
├── config/ <-- Core Django Settings
|
| 62 |
+
│ └── settings.py <-- Configured for HF / Whitenoise
|
| 63 |
+
├── fitting_system/ <-- Main App Logic
|
| 64 |
+
│ ├── ai_modules/ <-- Integration with Gemini and YOLO
|
| 65 |
+
│ ├── templates/ <-- HTML views (scan.html, store.html)
|
| 66 |
+
│ ├── static/ <-- CSS / JS styles & scripts
|
| 67 |
+
│ ├── models.py
|
| 68 |
+
│ └── views.py
|
| 69 |
+
└── locale/ <-- Translations (Arabic/English)
|
| 70 |
+
```
|
README.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Virtual Dressing
|
| 3 |
+
emoji: shirt
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: cyan
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Intelligent In-Store Virtual Fitting System
|
| 11 |
+
|
| 12 |
+
An AI-powered virtual fitting system for men's and women's clothing stores. The system uses computer vision to estimate body measurements and skin tone, then provides intelligent clothing recommendations based on available inventory.
|
| 13 |
+
|
| 14 |
+
## Hugging Face Spaces (Docker)
|
| 15 |
+
|
| 16 |
+
This repository is configured to run on Hugging Face Spaces using Docker.
|
| 17 |
+
|
| 18 |
+
- Runtime port: `7860`
|
| 19 |
+
- Container entrypoint: `gunicorn config.wsgi:application`
|
| 20 |
+
- Startup tasks: `migrate` + `collectstatic` before server start
|
| 21 |
+
|
| 22 |
+
Required Space variables are documented in the deployment guide/checklist provided in this chat.
|
| 23 |
+
|
| 24 |
+
## Features
|
| 25 |
+
|
| 26 |
+
- **Real-Time Visual Guidance**: Interactive camera interface with body outline overlay and live feedback to ensure perfect pose.
|
| 27 |
+
- **AI Body Measurement**: Uses MediaPipe Pose (New Task API) to estimate height, shoulder width, chest, and waist measurements with high accuracy.
|
| 28 |
+
- **Skin Tone Analysis**: Analyzes skin tone using MediaPipe Face Detection to recommend suitable colors.
|
| 29 |
+
- **Intelligent Recommendations**: Suggests appropriate size, fit, and products based on measurements and inventory.
|
| 30 |
+
- **3D Avatar Visualization**: Displays a parametric 3D avatar using Three.js to visualize the fit.
|
| 31 |
+
- **Online Store**: Browse products with filtering and search capabilities.
|
| 32 |
+
- **Inventory Management**: Real-time stock tracking with low stock alerts.
|
| 33 |
+
- **Privacy-First**: Images are processed in memory and never stored.
|
| 34 |
+
|
| 35 |
+
## Technology Stack
|
| 36 |
+
|
| 37 |
+
- **Backend**: Django 4.2+ with SQLite database
|
| 38 |
+
- **Frontend**: Django Templates with Tailwind CSS
|
| 39 |
+
- **AI/CV**: MediaPipe (Pose + Face Detection), OpenCV, NumPy
|
| 40 |
+
- **3D Graphics**: Three.js
|
| 41 |
+
- **Python**: 3.9+
|
| 42 |
+
|
| 43 |
+
## Quick Start
|
| 44 |
+
|
| 45 |
+
Get the project running in 5 simple steps:
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
# 1. Create and activate virtual environment
|
| 49 |
+
python -m venv venv
|
| 50 |
+
.\venv\Scripts\Activate.ps1 # Windows PowerShell
|
| 51 |
+
# source venv/bin/activate # Linux/Mac
|
| 52 |
+
|
| 53 |
+
# 2. Install dependencies
|
| 54 |
+
pip install -r requirements.txt
|
| 55 |
+
|
| 56 |
+
# 3. Download MediaPipe model (required for AI features)
|
| 57 |
+
curl -L -o pose_landmarker.task https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/latest/pose_landmarker_heavy.task
|
| 58 |
+
|
| 59 |
+
# 4. Setup database and populate initial data
|
| 60 |
+
python manage.py migrate
|
| 61 |
+
python manage.py populate_data
|
| 62 |
+
|
| 63 |
+
# 5. Run the development server
|
| 64 |
+
python manage.py runserver
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
**Access the application at:** http://localhost:8000/
|
| 68 |
+
|
| 69 |
+
**Optional - Create admin account:**
|
| 70 |
+
```bash
|
| 71 |
+
python manage.py createsuperuser
|
| 72 |
+
```
|
| 73 |
+
Then access admin panel at: http://localhost:8000/admin/
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
## Installation
|
| 78 |
+
|
| 79 |
+
### Prerequisites
|
| 80 |
+
|
| 81 |
+
- Python 3.9 or higher
|
| 82 |
+
- Webcam (for body scanning)
|
| 83 |
+
- Modern web browser (Chrome, Firefox, or Edge)
|
| 84 |
+
- Internet connection (for initial model download)
|
| 85 |
+
|
| 86 |
+
### Setup Steps
|
| 87 |
+
|
| 88 |
+
1. **Clone the repository**
|
| 89 |
+
```bash
|
| 90 |
+
git clone https://github.com/AMMMMMMAR/AI-dressing.git
|
| 91 |
+
cd AI-dressing
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
2. **Create and activate virtual environment**
|
| 95 |
+
```bash
|
| 96 |
+
python -m venv venv
|
| 97 |
+
# Windows:
|
| 98 |
+
.\venv\Scripts\Activate.ps1
|
| 99 |
+
# Linux/Mac:
|
| 100 |
+
source venv/bin/activate
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
3. **Install dependencies**
|
| 104 |
+
```bash
|
| 105 |
+
pip install -r requirements.txt
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
4. **Download MediaPipe Model**
|
| 109 |
+
The system requires the `pose_landmarker.task` model file in the project root.
|
| 110 |
+
```bash
|
| 111 |
+
# Using curl (Windows/Linux/Mac):
|
| 112 |
+
curl -L -o pose_landmarker.task https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/latest/pose_landmarker_heavy.task
|
| 113 |
+
|
| 114 |
+
# Or using Python:
|
| 115 |
+
python -c "import urllib.request; urllib.request.urlretrieve('https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/latest/pose_landmarker_heavy.task', 'pose_landmarker.task')"
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
5. **Run database migrations**
|
| 119 |
+
```bash
|
| 120 |
+
python manage.py migrate
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
6. **Populate initial data**
|
| 124 |
+
```bash
|
| 125 |
+
python manage.py populate_data
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
7. **Create a superuser (for admin access)**
|
| 129 |
+
```bash
|
| 130 |
+
python manage.py createsuperuser
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
8. **Run the development server**
|
| 134 |
+
```bash
|
| 135 |
+
python manage.py runserver
|
| 136 |
+
```
|
| 137 |
+
|
| 138 |
+
9. **Access the application**
|
| 139 |
+
- Main application: http://localhost:8000/
|
| 140 |
+
- Admin panel: http://localhost:8000/admin/
|
| 141 |
+
|
| 142 |
+
## Usage Guide
|
| 143 |
+
|
| 144 |
+
### For Customers
|
| 145 |
+
|
| 146 |
+
1. **Virtual Fitting**:
|
| 147 |
+
- Click "Try Virtual Fitting" from the homepage or navigation.
|
| 148 |
+
- Allow camera access when prompted.
|
| 149 |
+
- **Real-Time Guidance**: Stand back until your body fits the outline.
|
| 150 |
+
- Wait for the status indicator to turn **Green** ("Perfect! Hold still...").
|
| 151 |
+
- Click "Start Scanning" or "Capture Front View".
|
| 152 |
+
- View your measurements, recommended size/fit, and product suggestions.
|
| 153 |
+
|
| 154 |
+
2. **Browse Store**:
|
| 155 |
+
- Navigate to "Store" to browse all products.
|
| 156 |
+
- Use filters to narrow down by category, gender, or search.
|
| 157 |
+
- Click "Try Virtual Fitting" on product pages to get personalized recommendations.
|
| 158 |
+
|
| 159 |
+
### For Store Employees
|
| 160 |
+
|
| 161 |
+
1. **Inventory Management**:
|
| 162 |
+
- Navigate to "Inventory" to view stock levels.
|
| 163 |
+
- Low stock items are highlighted in yellow.
|
| 164 |
+
- Out of stock items are highlighted in red.
|
| 165 |
+
- Click "Manage Inventory in Admin Panel" for detailed management.
|
| 166 |
+
|
| 167 |
+
2. **Admin Panel**:
|
| 168 |
+
- Access at http://localhost:8000/admin/
|
| 169 |
+
- Login with superuser credentials.
|
| 170 |
+
- Manage products, sizes, colors, variants, and inventory.
|
| 171 |
+
- View scan history and recommendations.
|
| 172 |
+
|
| 173 |
+
## System Architecture
|
| 174 |
+
|
| 175 |
+
### AI/CV Pipeline
|
| 176 |
+
|
| 177 |
+
1. **Real-Time Pose Analysis**:
|
| 178 |
+
- Analyzes video frames at 2FPS (`/analyze-frame/` endpoint).
|
| 179 |
+
- Checks for feet visibility, head position, and distance.
|
| 180 |
+
- Provides immediate feedback to the user.
|
| 181 |
+
|
| 182 |
+
2. **Body Measurement Estimation**:
|
| 183 |
+
- Uses MediaPipe Pose (Heavy model) to detect 33 body landmarks.
|
| 184 |
+
- Calculates measurements based on landmark distances and calibrated pixel-to-cm ratios.
|
| 185 |
+
- Estimates: Height, Shoulder Width, Chest, Waist.
|
| 186 |
+
|
| 187 |
+
3. **Skin Tone Analysis**:
|
| 188 |
+
- Detects face region and analyzes color in YCrCb space.
|
| 189 |
+
- Classifies skin tone (Light/Medium/Dark) for color coordination.
|
| 190 |
+
|
| 191 |
+
4. **Recommendation Engine**:
|
| 192 |
+
- Maps measurements to industry-standard size charts.
|
| 193 |
+
- Recommends "Slim", "Regular", or "Oversize" fit based on body proportions.
|
| 194 |
+
- Suggests complementary clothing colors based on skin tone.
|
| 195 |
+
|
| 196 |
+
### Database Models
|
| 197 |
+
|
| 198 |
+
- **Size**: Measurement ranges for sizes (S, M, L, etc.).
|
| 199 |
+
- **Color**: Color hex codes and metadata.
|
| 200 |
+
- **Product**: Catalog items with category and fit type.
|
| 201 |
+
- **ProductVariant**: Specific SKUs (Product + Size + Color).
|
| 202 |
+
- **Inventory**: Stock count for each variant.
|
| 203 |
+
- **BodyScan**: Session data (measurements only, no images stored).
|
| 204 |
+
|
| 205 |
+
## API Endpoints
|
| 206 |
+
|
| 207 |
+
- `POST /analyze-frame/`: Real-time pose analysis (JSON).
|
| 208 |
+
- `POST /process-scan/`: Final measurement processing.
|
| 209 |
+
- `GET /scan/`: Main camera interface.
|
| 210 |
+
- `GET /store/`: Product catalog.
|
| 211 |
+
- `GET /api/inventory/`: Inventory status API.
|
| 212 |
+
|
| 213 |
+
## Troubleshooting
|
| 214 |
+
|
| 215 |
+
### Camera Issues
|
| 216 |
+
- Ensure you have granted camera permissions in your browser.
|
| 217 |
+
- If the screen says "Camera access denied", check your browser settings (site settings) and allow camera access.
|
| 218 |
+
- Ensure no other app (Zoom, Teams) is using the camera.
|
| 219 |
+
|
| 220 |
+
### "No person detected"
|
| 221 |
+
- Make sure you are standing 2-3 meters away.
|
| 222 |
+
- Ensure there is sufficient lighting.
|
| 223 |
+
- The system requires your full body (head to feet) to be visible.
|
| 224 |
+
|
| 225 |
+
### 500 Server Error
|
| 226 |
+
- Check the terminal output for python errors.
|
| 227 |
+
- Ensure `pose_landmarker.task` is present in the project folder.
|
| 228 |
+
|
| 229 |
+
## License
|
| 230 |
+
|
| 231 |
+
This is an academic prototype for demonstration purposes.
|
| 232 |
+
|
YOLOv8_Body_Measurement_and_Clothing_Recommendation_Updated (1).ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
avatar/FEMALE.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e91638dc8dd79a59b38aabb89e04eb13911f86588f1c5ff4124e8c2c8bc6f714
|
| 3 |
+
size 706868
|
avatar/MALE.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2c3b59749e4b82204108f986cdd4c922301fd92744e59d16ad086e4b1b655a66
|
| 3 |
+
size 613636
|
avatar/index.html
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>Style Studio — 3D Avatar</title>
|
| 6 |
+
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
|
| 7 |
+
<style>
|
| 8 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
+
body { overflow: hidden; font-family: 'DM Sans', sans-serif; background: #1a1a2e; }
|
| 10 |
+
|
| 11 |
+
#app { display: flex; width: 100vw; height: 100vh; }
|
| 12 |
+
|
| 13 |
+
#ui {
|
| 14 |
+
flex: 0 0 270px; width: 270px; height: 100vh;
|
| 15 |
+
background: linear-gradient(160deg, #0f0f1a 0%, #1a1a2e 60%, #16213e 100%);
|
| 16 |
+
border-right: 1px solid rgba(255,255,255,0.07);
|
| 17 |
+
z-index: 10; overflow: hidden; position: relative;
|
| 18 |
+
}
|
| 19 |
+
#ui::before {
|
| 20 |
+
content: ''; position: absolute; inset: 0;
|
| 21 |
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
|
| 22 |
+
pointer-events: none; opacity: 0.5;
|
| 23 |
+
}
|
| 24 |
+
.ui-inner {
|
| 25 |
+
position: relative; z-index: 1; height: 100%;
|
| 26 |
+
padding: 22px 18px; overflow-y: auto;
|
| 27 |
+
scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.1) transparent;
|
| 28 |
+
}
|
| 29 |
+
.ui-inner::-webkit-scrollbar { width: 4px; }
|
| 30 |
+
.ui-inner::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }
|
| 31 |
+
|
| 32 |
+
#canvas-container { flex: 1 1 auto; position: relative; overflow: hidden; }
|
| 33 |
+
#canvas-container canvas { display: block; width: 100% !important; height: 100% !important; }
|
| 34 |
+
|
| 35 |
+
.logo { margin-bottom: 22px; }
|
| 36 |
+
.logo-label { font-size: 9px; letter-spacing: 4px; text-transform: uppercase; color: rgba(255,255,255,0.3); margin-bottom: 4px; }
|
| 37 |
+
.logo-title { font-family: 'Playfair Display', serif; font-size: 20px; font-weight: 600; color: #fff; }
|
| 38 |
+
.logo-title span { color: #c9a96e; }
|
| 39 |
+
|
| 40 |
+
/* ── Section ── */
|
| 41 |
+
.section { margin-bottom: 20px; animation: fadeUp 0.5s ease both; }
|
| 42 |
+
.section:nth-child(2) { animation-delay: 0.05s; }
|
| 43 |
+
.section:nth-child(3) { animation-delay: 0.10s; }
|
| 44 |
+
.section:nth-child(4) { animation-delay: 0.15s; }
|
| 45 |
+
|
| 46 |
+
.section-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
| 47 |
+
.section-icon { width: 26px; height: 26px; border-radius: 7px; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0; }
|
| 48 |
+
.section-icon.skin-icon { background: rgba(201,169,110,0.15); }
|
| 49 |
+
.section-icon.shirt-icon { background: rgba(99,179,237,0.12); }
|
| 50 |
+
.section-icon.pants-icon { background: rgba(154,117,255,0.12); }
|
| 51 |
+
.section-title { font-size: 10px; font-weight: 500; letter-spacing: 2.5px; text-transform: uppercase; color: rgba(255,255,255,0.45); }
|
| 52 |
+
|
| 53 |
+
/* ── Skin swatches — larger, always 6 ── */
|
| 54 |
+
.swatches-skin { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; }
|
| 55 |
+
|
| 56 |
+
/* ── Clothing swatches — 6 columns ── */
|
| 57 |
+
.swatches-clothes { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; }
|
| 58 |
+
|
| 59 |
+
.swatch {
|
| 60 |
+
aspect-ratio: 1; border-radius: 50%; cursor: pointer;
|
| 61 |
+
border: 2px solid transparent;
|
| 62 |
+
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
|
| 63 |
+
position: relative;
|
| 64 |
+
}
|
| 65 |
+
.swatch:hover { transform: scale(1.2); box-shadow: 0 4px 14px rgba(0,0,0,0.5); }
|
| 66 |
+
.swatch.active { border-color: #fff; box-shadow: 0 0 0 1px rgba(255,255,255,0.25), 0 4px 14px rgba(0,0,0,0.5); transform: scale(1.1); }
|
| 67 |
+
.swatch[data-tip]:hover::after {
|
| 68 |
+
content: attr(data-tip);
|
| 69 |
+
position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
|
| 70 |
+
background: rgba(0,0,0,0.9); color: #fff; font-size: 10px;
|
| 71 |
+
white-space: nowrap; padding: 3px 8px; border-radius: 4px;
|
| 72 |
+
pointer-events: none; z-index: 300;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/* ── Suggested palette badge ── */
|
| 76 |
+
.suggest-label {
|
| 77 |
+
font-size: 9px;
|
| 78 |
+
letter-spacing: 1.5px;
|
| 79 |
+
text-transform: uppercase;
|
| 80 |
+
color: #c9a96e;
|
| 81 |
+
margin-bottom: 7px;
|
| 82 |
+
display: flex;
|
| 83 |
+
align-items: center;
|
| 84 |
+
gap: 5px;
|
| 85 |
+
}
|
| 86 |
+
.suggest-label::before {
|
| 87 |
+
content: '';
|
| 88 |
+
display: inline-block;
|
| 89 |
+
width: 18px; height: 1px;
|
| 90 |
+
background: #c9a96e;
|
| 91 |
+
opacity: 0.5;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* Swatch animate-in when palette swaps */
|
| 95 |
+
@keyframes swatchPop {
|
| 96 |
+
from { opacity: 0; transform: scale(0.7); }
|
| 97 |
+
to { opacity: 1; transform: scale(1); }
|
| 98 |
+
}
|
| 99 |
+
.swatch.pop { animation: swatchPop 0.22s ease both; }
|
| 100 |
+
|
| 101 |
+
.divider { height: 1px; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.07), transparent); margin: 4px 0 20px; }
|
| 102 |
+
|
| 103 |
+
.footer { margin-top: 14px; padding-bottom: 8px; }
|
| 104 |
+
.hint { font-size: 10px; color: rgba(255,255,255,0.2); line-height: 1.7; text-align: center; }
|
| 105 |
+
.hint span { display: block; color: rgba(255,255,255,0.3); margin-bottom: 2px; }
|
| 106 |
+
|
| 107 |
+
#debug {
|
| 108 |
+
position: absolute; bottom: 12px; right: 12px;
|
| 109 |
+
background: rgba(0,0,0,0.75); color: #4eff91;
|
| 110 |
+
font-size: 10px; font-family: monospace;
|
| 111 |
+
padding: 10px 14px; border-radius: 6px;
|
| 112 |
+
z-index: 50; max-width: 460px; line-height: 1.8;
|
| 113 |
+
pointer-events: none;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
@keyframes fadeUp {
|
| 117 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 118 |
+
to { opacity: 1; transform: translateY(0); }
|
| 119 |
+
}
|
| 120 |
+
</style>
|
| 121 |
+
</head>
|
| 122 |
+
<body>
|
| 123 |
+
<div id="app">
|
| 124 |
+
|
| 125 |
+
<div id="ui">
|
| 126 |
+
<div class="ui-inner">
|
| 127 |
+
<div class="logo">
|
| 128 |
+
<div class="logo-label">Virtual Fitting</div>
|
| 129 |
+
<div class="logo-title">Style <span>Studio</span></div>
|
| 130 |
+
</div>
|
| 131 |
+
|
| 132 |
+
<!-- SKIN -->
|
| 133 |
+
<div class="section">
|
| 134 |
+
<div class="section-header">
|
| 135 |
+
<div class="section-icon skin-icon">🧬</div>
|
| 136 |
+
<div class="section-title">Skin Tone</div>
|
| 137 |
+
</div>
|
| 138 |
+
<div class="swatches-skin" id="skinSwatches">
|
| 139 |
+
<div class="swatch" style="background:#fde8d0" data-hex="fde8d0" data-skin="0" data-tip="Porcelain"></div>
|
| 140 |
+
<div class="swatch" style="background:#f5cba7" data-hex="f5cba7" data-skin="1" data-tip="Ivory"></div>
|
| 141 |
+
<div class="swatch" style="background:#e8a87c" data-hex="e8a87c" data-skin="2" data-tip="Peach"></div>
|
| 142 |
+
<div class="swatch" style="background:#c68642" data-hex="c68642" data-skin="3" data-tip="Tan"></div>
|
| 143 |
+
<div class="swatch" style="background:#8d5524" data-hex="8d5524" data-skin="4" data-tip="Brown"></div>
|
| 144 |
+
<div class="swatch" style="background:#3d2c23" data-hex="3d2c23" data-skin="5" data-tip="Deep"></div>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
<div class="divider"></div>
|
| 149 |
+
|
| 150 |
+
<!-- SHIRT -->
|
| 151 |
+
<div class="section">
|
| 152 |
+
<div class="section-header">
|
| 153 |
+
<div class="section-icon shirt-icon">👕</div>
|
| 154 |
+
<div class="section-title">T-Shirt Color</div>
|
| 155 |
+
</div>
|
| 156 |
+
<div class="suggest-label" id="shirtSuggestLabel">✦ Suggested for your skin tone</div>
|
| 157 |
+
<div class="swatches-clothes" id="shirtSwatches"></div>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<div class="divider"></div>
|
| 161 |
+
|
| 162 |
+
<!-- PANTS -->
|
| 163 |
+
<div class="section">
|
| 164 |
+
<div class="section-header">
|
| 165 |
+
<div class="section-icon pants-icon">👖</div>
|
| 166 |
+
<div class="section-title">Pants Color</div>
|
| 167 |
+
</div>
|
| 168 |
+
<div class="suggest-label" id="pantsSuggestLabel">✦ Suggested for your skin tone</div>
|
| 169 |
+
<div class="swatches-clothes" id="pantsSwatches"></div>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
<div class="footer">
|
| 173 |
+
<div class="hint">
|
| 174 |
+
<span>🖱 Drag to rotate · Scroll to zoom</span>
|
| 175 |
+
Colors update with your skin tone
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
|
| 181 |
+
<div id="canvas-container">
|
| 182 |
+
<div id="debug">⏳ Loading model...</div>
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
</div>
|
| 186 |
+
|
| 187 |
+
<script type="importmap">
|
| 188 |
+
{
|
| 189 |
+
"imports": {
|
| 190 |
+
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
| 191 |
+
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
</script>
|
| 195 |
+
|
| 196 |
+
<script type="module">
|
| 197 |
+
import * as THREE from 'three';
|
| 198 |
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
| 199 |
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
| 200 |
+
|
| 201 |
+
const dbg = document.getElementById('debug');
|
| 202 |
+
const container = document.getElementById('canvas-container');
|
| 203 |
+
|
| 204 |
+
// ────────────────────────────────────────────────────────────────
|
| 205 |
+
// CURATED PALETTES — one entry per skin tone (index 0-5)
|
| 206 |
+
// Each has 12 shirt colours + 12 pants colours
|
| 207 |
+
// Carefully chosen to complement each skin tone
|
| 208 |
+
// ────────────────────────────────────────────────────────────────
|
| 209 |
+
const SKIN_PALETTES = [
|
| 210 |
+
// 0 — Porcelain (very light)
|
| 211 |
+
{
|
| 212 |
+
shirts: [
|
| 213 |
+
{ hex:'e8d5f5', tip:'Lavender' },
|
| 214 |
+
{ hex:'b5d8f7', tip:'Baby Blue' },
|
| 215 |
+
{ hex:'f7c5d0', tip:'Blush Pink' },
|
| 216 |
+
{ hex:'a8d8a8', tip:'Sage Green' },
|
| 217 |
+
{ hex:'f5e6c8', tip:'Cream' },
|
| 218 |
+
{ hex:'8b9dc3', tip:'Periwinkle' },
|
| 219 |
+
{ hex:'d4a5c9', tip:'Mauve' },
|
| 220 |
+
{ hex:'7fb5b5', tip:'Teal Mist' },
|
| 221 |
+
{ hex:'c8e6c9', tip:'Mint' },
|
| 222 |
+
{ hex:'ffccbc', tip:'Peach Tint' },
|
| 223 |
+
{ hex:'546e7a', tip:'Blue Grey' },
|
| 224 |
+
{ hex:'37474f', tip:'Charcoal' },
|
| 225 |
+
],
|
| 226 |
+
pants: [
|
| 227 |
+
{ hex:'cfd8dc', tip:'Ash Grey' },
|
| 228 |
+
{ hex:'b0bec5', tip:'Silver' },
|
| 229 |
+
{ hex:'90a4ae', tip:'Steel' },
|
| 230 |
+
{ hex:'607d8b', tip:'Slate' },
|
| 231 |
+
{ hex:'455a64', tip:'Dark Slate' },
|
| 232 |
+
{ hex:'37474f', tip:'Graphite' },
|
| 233 |
+
{ hex:'d7ccc8', tip:'Warm Cream' },
|
| 234 |
+
{ hex:'bcaaa4', tip:'Latte' },
|
| 235 |
+
{ hex:'8d6e63', tip:'Mocha' },
|
| 236 |
+
{ hex:'5d4037', tip:'Espresso' },
|
| 237 |
+
{ hex:'1a237e', tip:'Indigo' },
|
| 238 |
+
{ hex:'111111', tip:'Black' },
|
| 239 |
+
],
|
| 240 |
+
},
|
| 241 |
+
// 1 — Ivory (light)
|
| 242 |
+
{
|
| 243 |
+
shirts: [
|
| 244 |
+
{ hex:'e3f2fd', tip:'Ice Blue' },
|
| 245 |
+
{ hex:'bbdefb', tip:'Sky Blue' },
|
| 246 |
+
{ hex:'90caf9', tip:'Cornflower' },
|
| 247 |
+
{ hex:'ce93d8', tip:'Lilac' },
|
| 248 |
+
{ hex:'f48fb1', tip:'Pink Bloom' },
|
| 249 |
+
{ hex:'a5d6a7', tip:'Mint Green' },
|
| 250 |
+
{ hex:'fff176', tip:'Pale Yellow' },
|
| 251 |
+
{ hex:'ffcc80', tip:'Peach' },
|
| 252 |
+
{ hex:'80cbc4', tip:'Seafoam' },
|
| 253 |
+
{ hex:'ef9a9a', tip:'Rose' },
|
| 254 |
+
{ hex:'546e7a', tip:'Blue Grey' },
|
| 255 |
+
{ hex:'ffffff', tip:'White' },
|
| 256 |
+
],
|
| 257 |
+
pants: [
|
| 258 |
+
{ hex:'e0e0e0', tip:'Pearl' },
|
| 259 |
+
{ hex:'bdbdbd', tip:'Light Grey' },
|
| 260 |
+
{ hex:'9e9e9e', tip:'Mid Grey' },
|
| 261 |
+
{ hex:'757575', tip:'Ash' },
|
| 262 |
+
{ hex:'c3b091', tip:'Khaki' },
|
| 263 |
+
{ hex:'a1887f', tip:'Rosy Brown' },
|
| 264 |
+
{ hex:'6d4c41', tip:'Walnut' },
|
| 265 |
+
{ hex:'4e342e', tip:'Dark Brown' },
|
| 266 |
+
{ hex:'1565c0', tip:'Royal Blue' },
|
| 267 |
+
{ hex:'0d47a1', tip:'Deep Navy' },
|
| 268 |
+
{ hex:'1b5e20', tip:'Forest' },
|
| 269 |
+
{ hex:'111111', tip:'Black' },
|
| 270 |
+
],
|
| 271 |
+
},
|
| 272 |
+
// 2 — Peach (light-medium)
|
| 273 |
+
{
|
| 274 |
+
shirts: [
|
| 275 |
+
{ hex:'ffffff', tip:'White' },
|
| 276 |
+
{ hex:'f5f5f5', tip:'Off-White' },
|
| 277 |
+
{ hex:'b3e5fc', tip:'Light Blue' },
|
| 278 |
+
{ hex:'0277bd', tip:'Ocean Blue' },
|
| 279 |
+
{ hex:'00695c', tip:'Deep Teal' },
|
| 280 |
+
{ hex:'558b2f', tip:'Olive Green' },
|
| 281 |
+
{ hex:'f9a825', tip:'Golden' },
|
| 282 |
+
{ hex:'e64a19', tip:'Burnt Orange'},
|
| 283 |
+
{ hex:'6a1b9a', tip:'Deep Purple' },
|
| 284 |
+
{ hex:'ad1457', tip:'Raspberry' },
|
| 285 |
+
{ hex:'37474f', tip:'Slate Grey' },
|
| 286 |
+
{ hex:'111111', tip:'Black' },
|
| 287 |
+
],
|
| 288 |
+
pants: [
|
| 289 |
+
{ hex:'e8f5e9', tip:'Pale Green' },
|
| 290 |
+
{ hex:'c3b091', tip:'Khaki' },
|
| 291 |
+
{ hex:'d4c5a9', tip:'Sand' },
|
| 292 |
+
{ hex:'795548', tip:'Brown' },
|
| 293 |
+
{ hex:'4e342e', tip:'Dark Brown' },
|
| 294 |
+
{ hex:'263238', tip:'Dark Slate' },
|
| 295 |
+
{ hex:'1565c0', tip:'Cobalt' },
|
| 296 |
+
{ hex:'283593', tip:'Indigo' },
|
| 297 |
+
{ hex:'4a148c', tip:'Plum' },
|
| 298 |
+
{ hex:'880e4f', tip:'Burgundy' },
|
| 299 |
+
{ hex:'1b5e20', tip:'Forest' },
|
| 300 |
+
{ hex:'111111', tip:'Black' },
|
| 301 |
+
],
|
| 302 |
+
},
|
| 303 |
+
// 3 — Tan (medium)
|
| 304 |
+
{
|
| 305 |
+
shirts: [
|
| 306 |
+
{ hex:'ffffff', tip:'White' },
|
| 307 |
+
{ hex:'fffde7', tip:'Cream' },
|
| 308 |
+
{ hex:'e3f2fd', tip:'Ice Blue' },
|
| 309 |
+
{ hex:'0288d1', tip:'Sky Blue' },
|
| 310 |
+
{ hex:'01579b', tip:'Navy' },
|
| 311 |
+
{ hex:'004d40', tip:'Deep Green' },
|
| 312 |
+
{ hex:'f57f17', tip:'Amber' },
|
| 313 |
+
{ hex:'bf360c', tip:'Rust Red' },
|
| 314 |
+
{ hex:'4a148c', tip:'Royal Purple'},
|
| 315 |
+
{ hex:'880e4f', tip:'Deep Rose' },
|
| 316 |
+
{ hex:'ffd600', tip:'Sun Yellow' },
|
| 317 |
+
{ hex:'111111', tip:'Black' },
|
| 318 |
+
],
|
| 319 |
+
pants: [
|
| 320 |
+
{ hex:'f5f5f5', tip:'Off-White' },
|
| 321 |
+
{ hex:'d4c5a9', tip:'Sand' },
|
| 322 |
+
{ hex:'c3b091', tip:'Khaki' },
|
| 323 |
+
{ hex:'8d6e63', tip:'Mocha' },
|
| 324 |
+
{ hex:'5d4037', tip:'Espresso' },
|
| 325 |
+
{ hex:'3e2723', tip:'Mahogany' },
|
| 326 |
+
{ hex:'1a237e', tip:'Deep Blue' },
|
| 327 |
+
{ hex:'0d47a1', tip:'Navy' },
|
| 328 |
+
{ hex:'006064', tip:'Dark Teal' },
|
| 329 |
+
{ hex:'1b5e20', tip:'Dark Green' },
|
| 330 |
+
{ hex:'37474f', tip:'Graphite' },
|
| 331 |
+
{ hex:'111111', tip:'Black' },
|
| 332 |
+
],
|
| 333 |
+
},
|
| 334 |
+
// 4 — Brown (medium-dark)
|
| 335 |
+
{
|
| 336 |
+
shirts: [
|
| 337 |
+
{ hex:'ffffff', tip:'White' },
|
| 338 |
+
{ hex:'fff9c4', tip:'Butter' },
|
| 339 |
+
{ hex:'ffd54f', tip:'Gold' },
|
| 340 |
+
{ hex:'ff8f00', tip:'Mango' },
|
| 341 |
+
{ hex:'e65100', tip:'Tangerine' },
|
| 342 |
+
{ hex:'00bcd4', tip:'Cyan' },
|
| 343 |
+
{ hex:'0097a7', tip:'Deep Cyan' },
|
| 344 |
+
{ hex:'7c4dff', tip:'Violet' },
|
| 345 |
+
{ hex:'d500f9', tip:'Magenta' },
|
| 346 |
+
{ hex:'76ff03', tip:'Lime' },
|
| 347 |
+
{ hex:'f5f5f5', tip:'Off-White' },
|
| 348 |
+
{ hex:'111111', tip:'Black' },
|
| 349 |
+
],
|
| 350 |
+
pants: [
|
| 351 |
+
{ hex:'fafafa', tip:'White' },
|
| 352 |
+
{ hex:'ffe082', tip:'Pale Gold' },
|
| 353 |
+
{ hex:'d4c5a9', tip:'Sand' },
|
| 354 |
+
{ hex:'a1887f', tip:'Rosy Brown' },
|
| 355 |
+
{ hex:'6d4c41', tip:'Walnut' },
|
| 356 |
+
{ hex:'3e2723', tip:'Mahogany' },
|
| 357 |
+
{ hex:'0d47a1', tip:'Navy' },
|
| 358 |
+
{ hex:'006064', tip:'Teal' },
|
| 359 |
+
{ hex:'1b5e20', tip:'Forest' },
|
| 360 |
+
{ hex:'880e4f', tip:'Burgundy' },
|
| 361 |
+
{ hex:'212121', tip:'Almost Black'},
|
| 362 |
+
{ hex:'111111', tip:'Black' },
|
| 363 |
+
],
|
| 364 |
+
},
|
| 365 |
+
// 5 — Deep (dark)
|
| 366 |
+
{
|
| 367 |
+
shirts: [
|
| 368 |
+
{ hex:'ffffff', tip:'White' },
|
| 369 |
+
{ hex:'fffde7', tip:'Ivory' },
|
| 370 |
+
{ hex:'fff176', tip:'Lemon' },
|
| 371 |
+
{ hex:'ffd740', tip:'Gold' },
|
| 372 |
+
{ hex:'ff6d00', tip:'Bright Orange'},
|
| 373 |
+
{ hex:'00e5ff', tip:'Electric Blue'},
|
| 374 |
+
{ hex:'00bfa5', tip:'Aqua' },
|
| 375 |
+
{ hex:'69f0ae', tip:'Neon Mint' },
|
| 376 |
+
{ hex:'ea80fc', tip:'Orchid' },
|
| 377 |
+
{ hex:'ff4081', tip:'Hot Pink' },
|
| 378 |
+
{ hex:'f5f5f5', tip:'Off-White' },
|
| 379 |
+
{ hex:'e0e0e0', tip:'Light Grey' },
|
| 380 |
+
],
|
| 381 |
+
pants: [
|
| 382 |
+
{ hex:'ffffff', tip:'White' },
|
| 383 |
+
{ hex:'f5f5f5', tip:'Off-White' },
|
| 384 |
+
{ hex:'e0e0e0', tip:'Pearl' },
|
| 385 |
+
{ hex:'bdbdbd', tip:'Silver' },
|
| 386 |
+
{ hex:'ffd740', tip:'Gold' },
|
| 387 |
+
{ hex:'ffe082', tip:'Pale Gold' },
|
| 388 |
+
{ hex:'0d47a1', tip:'Navy' },
|
| 389 |
+
{ hex:'006064', tip:'Teal' },
|
| 390 |
+
{ hex:'880e4f', tip:'Burgundy' },
|
| 391 |
+
{ hex:'4a148c', tip:'Plum' },
|
| 392 |
+
{ hex:'212121', tip:'Almost Black'},
|
| 393 |
+
{ hex:'111111', tip:'Black' },
|
| 394 |
+
],
|
| 395 |
+
},
|
| 396 |
+
];
|
| 397 |
+
|
| 398 |
+
// ────────────────────────────────────────────────────────────────
|
| 399 |
+
// THREE.JS SETUP
|
| 400 |
+
// ────────────────────────────────────────────────────────────────
|
| 401 |
+
const scene = new THREE.Scene();
|
| 402 |
+
scene.background = new THREE.Color(0x1a1a2e);
|
| 403 |
+
|
| 404 |
+
const camera = new THREE.PerspectiveCamera(
|
| 405 |
+
36,
|
| 406 |
+
Math.max(container.clientWidth, 1) / Math.max(container.clientHeight, 1),
|
| 407 |
+
0.1, 100
|
| 408 |
+
);
|
| 409 |
+
camera.position.set(0, 1.0, 6);
|
| 410 |
+
|
| 411 |
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 412 |
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| 413 |
+
renderer.setSize(Math.max(container.clientWidth, 1), Math.max(container.clientHeight, 1));
|
| 414 |
+
renderer.shadowMap.enabled = true;
|
| 415 |
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
| 416 |
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
| 417 |
+
renderer.toneMappingExposure = 1.1;
|
| 418 |
+
container.appendChild(renderer.domElement);
|
| 419 |
+
|
| 420 |
+
const controls = new OrbitControls(camera, renderer.domElement);
|
| 421 |
+
controls.target.set(0, 1.0, 0);
|
| 422 |
+
controls.enableDamping = true;
|
| 423 |
+
controls.dampingFactor = 0.07;
|
| 424 |
+
controls.minDistance = 1.0;
|
| 425 |
+
controls.maxDistance = 20;
|
| 426 |
+
controls.update();
|
| 427 |
+
|
| 428 |
+
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
| 429 |
+
const key = new THREE.DirectionalLight(0xfff5e6, 1.4);
|
| 430 |
+
key.position.set(3, 6, 4); key.castShadow = true;
|
| 431 |
+
key.shadow.mapSize.set(1024, 1024); scene.add(key);
|
| 432 |
+
const fill = new THREE.DirectionalLight(0xd0e8ff, 0.6);
|
| 433 |
+
fill.position.set(-3, 3, -2); scene.add(fill);
|
| 434 |
+
const rim = new THREE.DirectionalLight(0xc9a96e, 0.8);
|
| 435 |
+
rim.position.set(0, 4, -4); scene.add(rim);
|
| 436 |
+
|
| 437 |
+
const floor = new THREE.Mesh(
|
| 438 |
+
new THREE.CircleGeometry(2.5, 64),
|
| 439 |
+
new THREE.MeshStandardMaterial({ color: 0x16213e, roughness: 0.85, metalness: 0.05, transparent: true, opacity: 0.65 })
|
| 440 |
+
);
|
| 441 |
+
floor.rotation.x = -Math.PI / 2;
|
| 442 |
+
floor.receiveShadow = true;
|
| 443 |
+
scene.add(floor);
|
| 444 |
+
|
| 445 |
+
// ────────────────────────────────────────────────────────────────
|
| 446 |
+
// MESH BUCKETS
|
| 447 |
+
// ────────────────────────────────────────────────────────────────
|
| 448 |
+
const skinMeshes = [];
|
| 449 |
+
const shirtMeshes = [];
|
| 450 |
+
const pantsMeshes = [];
|
| 451 |
+
const lockedMeshes = []; // { mesh, hexColor }
|
| 452 |
+
|
| 453 |
+
// ── Helpers ──
|
| 454 |
+
function avgWorldY(mesh) {
|
| 455 |
+
const pos = mesh.geometry.attributes.position;
|
| 456 |
+
if (!pos || pos.count === 0) return 0;
|
| 457 |
+
let sum = 0;
|
| 458 |
+
const v = new THREE.Vector3();
|
| 459 |
+
for (let i = 0; i < pos.count; i++) {
|
| 460 |
+
v.fromBufferAttribute(pos, i);
|
| 461 |
+
v.applyMatrix4(mesh.matrixWorld);
|
| 462 |
+
sum += v.y;
|
| 463 |
+
}
|
| 464 |
+
return sum / pos.count;
|
| 465 |
+
}
|
| 466 |
+
function getMat(mesh) {
|
| 467 |
+
return Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
// Skin: keep texture, only tint colour
|
| 471 |
+
function applySkinColor(hexStr) {
|
| 472 |
+
const hex = parseInt(hexStr, 16);
|
| 473 |
+
const r = ((hex >> 16) & 0xff) / 255;
|
| 474 |
+
const g = ((hex >> 8) & 0xff) / 255;
|
| 475 |
+
const b = ( hex & 0xff) / 255;
|
| 476 |
+
skinMeshes.forEach(mesh => {
|
| 477 |
+
const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
| 478 |
+
mats.forEach(mat => { mat.color.setRGB(r, g, b); mat.needsUpdate = true; });
|
| 479 |
+
});
|
| 480 |
+
lockedMeshes.forEach(applyLocked);
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
// Clothing: clear texture, set pure colour
|
| 484 |
+
function applyClothingColor(meshList, hexStr) {
|
| 485 |
+
const hex = parseInt(hexStr, 16);
|
| 486 |
+
meshList.forEach(mesh => {
|
| 487 |
+
const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
| 488 |
+
mats.forEach(mat => { mat.color.setHex(hex); mat.map = null; mat.needsUpdate = true; });
|
| 489 |
+
});
|
| 490 |
+
lockedMeshes.forEach(applyLocked);
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
function applyLocked(entry) {
|
| 494 |
+
const mats = Array.isArray(entry.mesh.material) ? entry.mesh.material : [entry.mesh.material];
|
| 495 |
+
mats.forEach(mat => { mat.color.setHex(entry.hexColor); mat.map = null; mat.needsUpdate = true; });
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
// ────────────────────────────────────────────────────────────────
|
| 499 |
+
// PALETTE UI — rebuild swatches when skin tone changes
|
| 500 |
+
// ────────────────────────────────────────────────────────────────
|
| 501 |
+
let currentShirtHex = null;
|
| 502 |
+
let currentPantsHex = null;
|
| 503 |
+
|
| 504 |
+
function buildClothingSwatches(skinIndex) {
|
| 505 |
+
const palette = SKIN_PALETTES[skinIndex];
|
| 506 |
+
const shirtGrid = document.getElementById('shirtSwatches');
|
| 507 |
+
const pantsGrid = document.getElementById('pantsSwatches');
|
| 508 |
+
|
| 509 |
+
// ── Shirts ──
|
| 510 |
+
shirtGrid.innerHTML = '';
|
| 511 |
+
palette.shirts.forEach((item, i) => {
|
| 512 |
+
const sw = document.createElement('div');
|
| 513 |
+
sw.className = 'swatch pop';
|
| 514 |
+
sw.style.background = '#' + item.hex;
|
| 515 |
+
sw.style.animationDelay = (i * 0.025) + 's';
|
| 516 |
+
if (item.hex === 'ffffff' || item.hex === 'f5f5f5' || item.hex === 'fffde7' ||
|
| 517 |
+
item.hex === 'e3f2fd' || item.hex === 'e8f5e9' || item.hex === 'fff9c4' ||
|
| 518 |
+
item.hex === 'fffde7' || item.hex === 'fafafa')
|
| 519 |
+
sw.style.borderColor = 'rgba(255,255,255,0.3)';
|
| 520 |
+
sw.dataset.hex = item.hex;
|
| 521 |
+
sw.dataset.tip = item.tip;
|
| 522 |
+
sw.addEventListener('click', () => {
|
| 523 |
+
shirtGrid.querySelectorAll('.swatch').forEach(s => s.classList.remove('active'));
|
| 524 |
+
sw.classList.add('active');
|
| 525 |
+
currentShirtHex = item.hex;
|
| 526 |
+
applyClothingColor(shirtMeshes, item.hex);
|
| 527 |
+
});
|
| 528 |
+
shirtGrid.appendChild(sw);
|
| 529 |
+
});
|
| 530 |
+
|
| 531 |
+
// ── Pants ──
|
| 532 |
+
pantsGrid.innerHTML = '';
|
| 533 |
+
palette.pants.forEach((item, i) => {
|
| 534 |
+
const sw = document.createElement('div');
|
| 535 |
+
sw.className = 'swatch pop';
|
| 536 |
+
sw.style.background = '#' + item.hex;
|
| 537 |
+
sw.style.animationDelay = (i * 0.025) + 's';
|
| 538 |
+
if (item.hex === 'ffffff' || item.hex === 'f5f5f5' || item.hex === 'fafafa' ||
|
| 539 |
+
item.hex === 'e0e0e0' || item.hex === 'cfd8dc')
|
| 540 |
+
sw.style.borderColor = 'rgba(255,255,255,0.3)';
|
| 541 |
+
sw.dataset.hex = item.hex;
|
| 542 |
+
sw.dataset.tip = item.tip;
|
| 543 |
+
sw.addEventListener('click', () => {
|
| 544 |
+
pantsGrid.querySelectorAll('.swatch').forEach(s => s.classList.remove('active'));
|
| 545 |
+
sw.classList.add('active');
|
| 546 |
+
currentPantsHex = item.hex;
|
| 547 |
+
applyClothingColor(pantsMeshes, item.hex);
|
| 548 |
+
});
|
| 549 |
+
pantsGrid.appendChild(sw);
|
| 550 |
+
});
|
| 551 |
+
|
| 552 |
+
// Auto-apply the first shirt and first pants colour as a preview
|
| 553 |
+
currentShirtHex = palette.shirts[0].hex;
|
| 554 |
+
currentPantsHex = palette.pants[0].hex;
|
| 555 |
+
applyClothingColor(shirtMeshes, currentShirtHex);
|
| 556 |
+
applyClothingColor(pantsMeshes, currentPantsHex);
|
| 557 |
+
|
| 558 |
+
// Mark first swatch active
|
| 559 |
+
shirtGrid.querySelectorAll('.swatch')[0]?.classList.add('active');
|
| 560 |
+
pantsGrid.querySelectorAll('.swatch')[0]?.classList.add('active');
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
// ────────────────────────────────────────────────────────────────
|
| 564 |
+
// LOAD MODEL
|
| 565 |
+
// ──────────────────────────────────────��─────────────────────────
|
| 566 |
+
new GLTFLoader().load(
|
| 567 |
+
'MALE.glb',
|
| 568 |
+
function (gltf) {
|
| 569 |
+
const model = gltf.scene;
|
| 570 |
+
|
| 571 |
+
// Scale & place feet on y=0
|
| 572 |
+
{
|
| 573 |
+
const box = new THREE.Box3().setFromObject(model);
|
| 574 |
+
const size = box.getSize(new THREE.Vector3());
|
| 575 |
+
model.scale.setScalar(2.0 / Math.max(size.x, size.y, size.z));
|
| 576 |
+
model.updateMatrixWorld(true);
|
| 577 |
+
const box2 = new THREE.Box3().setFromObject(model);
|
| 578 |
+
const cen = box2.getCenter(new THREE.Vector3());
|
| 579 |
+
model.position.set(-cen.x, -box2.min.y, -cen.z);
|
| 580 |
+
}
|
| 581 |
+
scene.add(model);
|
| 582 |
+
model.updateMatrixWorld(true);
|
| 583 |
+
|
| 584 |
+
const wb = new THREE.Box3().setFromObject(model);
|
| 585 |
+
const wh = wb.max.y - wb.min.y;
|
| 586 |
+
const minY = wb.min.y;
|
| 587 |
+
|
| 588 |
+
const ankleY = minY + wh * 0.11;
|
| 589 |
+
const waistY = minY + wh * 0.47;
|
| 590 |
+
const neckY = minY + wh * 0.80;
|
| 591 |
+
const midY = minY + wh * 0.50;
|
| 592 |
+
|
| 593 |
+
const fovRad = THREE.MathUtils.degToRad(camera.fov);
|
| 594 |
+
const camDist = (wh / 2 / Math.tan(fovRad / 2)) * 1.65;
|
| 595 |
+
camera.position.set(0, midY, camDist);
|
| 596 |
+
controls.target.set(0, midY, 0);
|
| 597 |
+
controls.minDistance = camDist * 0.25;
|
| 598 |
+
controls.maxDistance = camDist * 5;
|
| 599 |
+
controls.update();
|
| 600 |
+
|
| 601 |
+
const allMeshes = [];
|
| 602 |
+
model.traverse(child => {
|
| 603 |
+
if (!child.isMesh) return;
|
| 604 |
+
child.material = Array.isArray(child.material)
|
| 605 |
+
? child.material.map(m => m.clone())
|
| 606 |
+
: child.material.clone();
|
| 607 |
+
child.castShadow = true;
|
| 608 |
+
child.receiveShadow = true;
|
| 609 |
+
allMeshes.push(child);
|
| 610 |
+
});
|
| 611 |
+
|
| 612 |
+
const lockBlack = ['eye','lash','iris','pupil','cornea','sclera','brow','eyebrow','hair','shoe','boot','sole','lace','sneaker','eyelid'];
|
| 613 |
+
const lockWhite = ['sock'];
|
| 614 |
+
const lockDefault = ['teeth','tooth','tongue','lip','mouth','gum','nose','nail'];
|
| 615 |
+
const pantsWords = ['pant','trouser','jean','denim','short','skirt','bottom','lower'];
|
| 616 |
+
const shirtWords = ['shirt','tee','jacket','hoodie','sweater','cloth','wear','blouse'];
|
| 617 |
+
|
| 618 |
+
const unclassified = [];
|
| 619 |
+
const log = [];
|
| 620 |
+
|
| 621 |
+
allMeshes.forEach(mesh => {
|
| 622 |
+
const n = mesh.name.toLowerCase();
|
| 623 |
+
if (lockBlack.some(w => n.includes(w))) { lockedMeshes.push({ mesh, hexColor: 0x0a0a0a }); log.push(`[LOCK-BLACK] ${mesh.name}`); return; }
|
| 624 |
+
if (lockWhite.some(w => n.includes(w))) { lockedMeshes.push({ mesh, hexColor: 0xfafafa }); log.push(`[LOCK-WHITE] ${mesh.name}`); return; }
|
| 625 |
+
if (lockDefault.some(w => n.includes(w))) { log.push(`[LOCK-DEFAULT] ${mesh.name}`); return; }
|
| 626 |
+
if (pantsWords.some(w => n.includes(w))) { pantsMeshes.push(mesh); log.push(`[PANTS-name] ${mesh.name}`); return; }
|
| 627 |
+
if (shirtWords.some(w => n.includes(w))) { shirtMeshes.push(mesh); log.push(`[SHIRT-name] ${mesh.name}`); return; }
|
| 628 |
+
|
| 629 |
+
const yc = avgWorldY(mesh);
|
| 630 |
+
const mat = getMat(mesh);
|
| 631 |
+
const { r, g, b } = mat ? mat.color : { r:0.5, g:0.5, b:0.5 };
|
| 632 |
+
const isDark = (r + g + b) < 0.8 || (r < 0.25 && g < 0.25);
|
| 633 |
+
|
| 634 |
+
if (yc < ankleY) { log.push(`[SKIP-feet] ${mesh.name}`); return; }
|
| 635 |
+
if (yc <= waistY) { isDark ? pantsMeshes.push(mesh) : unclassified.push(mesh); log.push(`[${isDark?'PANTS-pos':'UNCL-lower'}] ${mesh.name}`); return; }
|
| 636 |
+
if (yc <= neckY) { isDark ? shirtMeshes.push(mesh) : unclassified.push(mesh); log.push(`[${isDark?'SHIRT-pos':'UNCL-torso'}] ${mesh.name}`); return; }
|
| 637 |
+
unclassified.push(mesh);
|
| 638 |
+
log.push(`[UNCL-head] ${mesh.name}`);
|
| 639 |
+
});
|
| 640 |
+
|
| 641 |
+
unclassified.forEach(m => { skinMeshes.push(m); });
|
| 642 |
+
|
| 643 |
+
if (skinMeshes.length === 0) {
|
| 644 |
+
const used = new Set([...pantsMeshes, ...shirtMeshes, ...lockedMeshes.map(e => e.mesh)]);
|
| 645 |
+
allMeshes.forEach(m => { if (!used.has(m)) skinMeshes.push(m); });
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
lockedMeshes.forEach(applyLocked);
|
| 649 |
+
|
| 650 |
+
console.log('=== Mesh Classification ===');
|
| 651 |
+
log.forEach(l => console.log(l));
|
| 652 |
+
|
| 653 |
+
dbg.innerHTML =
|
| 654 |
+
`🧬 Skin: <b>${skinMeshes.length}</b> 👕 Shirt: <b>${shirtMeshes.length}</b> 👖 Pants: <b>${pantsMeshes.length}</b> 🔒 Locked: <b>${lockedMeshes.length}</b><br>` +
|
| 655 |
+
`<span style="opacity:.5;font-size:9px">F12 → Console for full mesh log</span>`;
|
| 656 |
+
|
| 657 |
+
// Build initial palette for skin index 0 (Porcelain)
|
| 658 |
+
buildClothingSwatches(0);
|
| 659 |
+
},
|
| 660 |
+
undefined,
|
| 661 |
+
err => { console.error(err); dbg.innerHTML = `<span style="color:#ff6b6b">❌ ${err.message}</span>`; }
|
| 662 |
+
);
|
| 663 |
+
|
| 664 |
+
// ────────────────────────────────────────────────────────────────
|
| 665 |
+
// SKIN SWATCHES
|
| 666 |
+
// ─────────────��──────────────────────────────────────────────────
|
| 667 |
+
document.getElementById('skinSwatches').querySelectorAll('.swatch').forEach(sw => {
|
| 668 |
+
sw.addEventListener('click', () => {
|
| 669 |
+
document.getElementById('skinSwatches').querySelectorAll('.swatch').forEach(s => s.classList.remove('active'));
|
| 670 |
+
sw.classList.add('active');
|
| 671 |
+
|
| 672 |
+
const skinIndex = parseInt(sw.dataset.skin);
|
| 673 |
+
|
| 674 |
+
// 1. Apply skin tone tint (keeps texture/face details)
|
| 675 |
+
applySkinColor(sw.dataset.hex);
|
| 676 |
+
|
| 677 |
+
// 2. Rebuild clothing colour palette for this skin tone
|
| 678 |
+
buildClothingSwatches(skinIndex);
|
| 679 |
+
});
|
| 680 |
+
});
|
| 681 |
+
|
| 682 |
+
// ── Render ──
|
| 683 |
+
(function animate() {
|
| 684 |
+
requestAnimationFrame(animate);
|
| 685 |
+
controls.update();
|
| 686 |
+
renderer.render(scene, camera);
|
| 687 |
+
})();
|
| 688 |
+
|
| 689 |
+
// ── Safe resize ──
|
| 690 |
+
new ResizeObserver(() => {
|
| 691 |
+
const w = Math.max(container.clientWidth, 1);
|
| 692 |
+
const h = Math.max(container.clientHeight, 1);
|
| 693 |
+
camera.aspect = w / h;
|
| 694 |
+
camera.updateProjectionMatrix();
|
| 695 |
+
renderer.setSize(w, h);
|
| 696 |
+
}).observe(container);
|
| 697 |
+
</script>
|
| 698 |
+
</body>
|
| 699 |
+
</html>
|
compile_locale.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""
|
| 3 |
+
Compile Django .po files to .mo when GNU gettext (msgfmt) is not installed (e.g. Windows).
|
| 4 |
+
Usage: python compile_locale.py
|
| 5 |
+
Requires: pip install babel
|
| 6 |
+
"""
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
try:
|
| 11 |
+
from babel.messages.pofile import read_po
|
| 12 |
+
from babel.messages.mofile import write_mo
|
| 13 |
+
except ImportError:
|
| 14 |
+
print("Install babel: pip install babel")
|
| 15 |
+
sys.exit(1)
|
| 16 |
+
|
| 17 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 18 |
+
LOCALE_DIR = os.path.join(BASE_DIR, "locale")
|
| 19 |
+
|
| 20 |
+
for lang in ("en", "ar"):
|
| 21 |
+
po_path = os.path.join(LOCALE_DIR, lang, "LC_MESSAGES", "django.po")
|
| 22 |
+
mo_path = os.path.join(LOCALE_DIR, lang, "LC_MESSAGES", "django.mo")
|
| 23 |
+
if not os.path.isfile(po_path):
|
| 24 |
+
continue
|
| 25 |
+
with open(po_path, "rb") as f:
|
| 26 |
+
catalog = read_po(f, locale=lang)
|
| 27 |
+
with open(mo_path, "wb") as f:
|
| 28 |
+
write_mo(f, catalog)
|
| 29 |
+
print(f"Compiled: {mo_path}")
|
| 30 |
+
|
| 31 |
+
print("Done.")
|
config/__init__.py
ADDED
|
File without changes
|
config/asgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ASGI config for config project.
|
| 3 |
+
|
| 4 |
+
It exposes the ASGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.asgi import get_asgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
| 15 |
+
|
| 16 |
+
application = get_asgi_application()
|
config/settings.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Django settings for config project.
|
| 3 |
+
|
| 4 |
+
Generated by 'django-admin startproject' using Django 5.2.5.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/5.2/topics/settings/
|
| 8 |
+
|
| 9 |
+
For the full list of settings and their values, see
|
| 10 |
+
https://docs.djangoproject.com/en/5.2/ref/settings/
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import os
|
| 15 |
+
from dotenv import load_dotenv
|
| 16 |
+
|
| 17 |
+
# Load environment variables from .env file
|
| 18 |
+
load_dotenv()
|
| 19 |
+
|
| 20 |
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
| 21 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# Quick-start development settings - unsuitable for production
|
| 25 |
+
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
| 26 |
+
|
| 27 |
+
# SECURITY WARNING: keep the secret key used in production secret!
|
| 28 |
+
SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-default-key-change-this')
|
| 29 |
+
|
| 30 |
+
# SECURITY WARNING: don't run with debug turned on in production!
|
| 31 |
+
DEBUG = os.environ.get('DEBUG', 'True') == 'True'
|
| 32 |
+
|
| 33 |
+
ALLOWED_HOSTS = ['*'] # Required for Hugging Face Spaces
|
| 34 |
+
CSRF_TRUSTED_ORIGINS = ['https://*.hf.space']
|
| 35 |
+
|
| 36 |
+
# Application definition
|
| 37 |
+
|
| 38 |
+
INSTALLED_APPS = [
|
| 39 |
+
'django.contrib.admin',
|
| 40 |
+
'django.contrib.auth',
|
| 41 |
+
'django.contrib.contenttypes',
|
| 42 |
+
'django.contrib.sessions',
|
| 43 |
+
'django.contrib.messages',
|
| 44 |
+
'django.contrib.staticfiles',
|
| 45 |
+
'fitting_system',
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
MIDDLEWARE = [
|
| 49 |
+
'django.middleware.security.SecurityMiddleware',
|
| 50 |
+
'whitenoise.middleware.WhiteNoiseMiddleware',
|
| 51 |
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
| 52 |
+
'django.middleware.locale.LocaleMiddleware',
|
| 53 |
+
'django.middleware.common.CommonMiddleware',
|
| 54 |
+
'django.middleware.csrf.CsrfViewMiddleware',
|
| 55 |
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
| 56 |
+
'django.contrib.messages.middleware.MessageMiddleware',
|
| 57 |
+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
ROOT_URLCONF = 'config.urls'
|
| 61 |
+
|
| 62 |
+
TEMPLATES = [
|
| 63 |
+
{
|
| 64 |
+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
| 65 |
+
'DIRS': [],
|
| 66 |
+
'APP_DIRS': True,
|
| 67 |
+
'OPTIONS': {
|
| 68 |
+
'context_processors': [
|
| 69 |
+
'django.template.context_processors.request',
|
| 70 |
+
'django.template.context_processors.i18n',
|
| 71 |
+
'django.contrib.auth.context_processors.auth',
|
| 72 |
+
'django.contrib.messages.context_processors.messages',
|
| 73 |
+
],
|
| 74 |
+
},
|
| 75 |
+
},
|
| 76 |
+
]
|
| 77 |
+
|
| 78 |
+
WSGI_APPLICATION = 'config.wsgi.application'
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# Database
|
| 82 |
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
| 83 |
+
|
| 84 |
+
DATABASES = {
|
| 85 |
+
'default': {
|
| 86 |
+
'ENGINE': 'django.db.backends.sqlite3',
|
| 87 |
+
'NAME': BASE_DIR / 'db.sqlite3',
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# Password validation
|
| 93 |
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
| 94 |
+
|
| 95 |
+
AUTH_PASSWORD_VALIDATORS = [
|
| 96 |
+
{
|
| 97 |
+
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
| 107 |
+
},
|
| 108 |
+
]
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# Internationalization
|
| 112 |
+
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
| 113 |
+
|
| 114 |
+
LANGUAGE_CODE = 'en'
|
| 115 |
+
|
| 116 |
+
TIME_ZONE = 'UTC'
|
| 117 |
+
|
| 118 |
+
USE_I18N = True
|
| 119 |
+
|
| 120 |
+
USE_TZ = True
|
| 121 |
+
|
| 122 |
+
# Supported languages: English (LTR), Arabic (RTL)
|
| 123 |
+
LANGUAGES = [
|
| 124 |
+
('en', 'English'),
|
| 125 |
+
('ar', 'العربية'),
|
| 126 |
+
]
|
| 127 |
+
|
| 128 |
+
LOCALE_PATHS = [BASE_DIR / 'locale']
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
# Static files (CSS, JavaScript, Images)
|
| 132 |
+
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
| 133 |
+
|
| 134 |
+
STATIC_URL = '/static/'
|
| 135 |
+
STATICFILES_DIRS = [BASE_DIR / 'static']
|
| 136 |
+
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
| 137 |
+
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
| 138 |
+
|
| 139 |
+
# Media files (User uploads)
|
| 140 |
+
MEDIA_URL = '/media/'
|
| 141 |
+
MEDIA_ROOT = Path(os.environ.get('MEDIA_ROOT', BASE_DIR / 'media'))
|
| 142 |
+
|
| 143 |
+
# Default primary key field type
|
| 144 |
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
| 145 |
+
|
| 146 |
+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
| 147 |
+
|
| 148 |
+
# Gemini AI Configuration
|
| 149 |
+
# Set your API key here or via environment variable GEMINI_API_KEY in .env
|
| 150 |
+
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY', '')
|
| 151 |
+
|
| 152 |
+
|
config/urls.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
URL configuration for config project.
|
| 3 |
+
|
| 4 |
+
The `urlpatterns` list routes URLs to views. For more information please see:
|
| 5 |
+
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
| 6 |
+
Examples:
|
| 7 |
+
Function views
|
| 8 |
+
1. Add an import: from my_app import views
|
| 9 |
+
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
| 10 |
+
Class-based views
|
| 11 |
+
1. Add an import: from other_app.views import Home
|
| 12 |
+
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
| 13 |
+
Including another URLconf
|
| 14 |
+
1. Import the include() function: from django.urls import include, path
|
| 15 |
+
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
| 16 |
+
"""
|
| 17 |
+
from django.contrib import admin
|
| 18 |
+
from django.urls import path, include
|
| 19 |
+
from django.conf import settings
|
| 20 |
+
from django.conf.urls.static import static
|
| 21 |
+
from django.conf.urls.i18n import i18n_patterns
|
| 22 |
+
|
| 23 |
+
urlpatterns = [
|
| 24 |
+
path('admin/', admin.site.urls),
|
| 25 |
+
path('i18n/', include('django.conf.urls.i18n')),
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
# App URLs with language prefix (e.g. /en/, /ar/)
|
| 29 |
+
urlpatterns += i18n_patterns(
|
| 30 |
+
path('', include('fitting_system.urls')),
|
| 31 |
+
prefix_default_language=False,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# WhiteNoise serves STATIC_URL in production.
|
| 35 |
+
# Keep media URL mapping available for container deployments where legacy media
|
| 36 |
+
# files may still be referenced.
|
| 37 |
+
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
| 38 |
+
|
config/wsgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WSGI config for config project.
|
| 3 |
+
|
| 4 |
+
It exposes the WSGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.wsgi import get_wsgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
| 15 |
+
|
| 16 |
+
application = get_wsgi_application()
|
debug_gemini.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
# Setup Django environment
|
| 6 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
| 7 |
+
import django
|
| 8 |
+
django.setup()
|
| 9 |
+
|
| 10 |
+
from django.conf import settings
|
| 11 |
+
from fitting_system.ai_modules.gemini_client import GeminiClient
|
| 12 |
+
import google.generativeai as genai
|
| 13 |
+
|
| 14 |
+
print(f"Python executable: {sys.executable}")
|
| 15 |
+
print(f"Django settings configured.")
|
| 16 |
+
print(f"API Key from settings: {getattr(settings, 'GEMINI_API_KEY', 'NOT_FOUND')[:10]}...")
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
import google.generativeai
|
| 20 |
+
print("google.generativeai imported successfully.")
|
| 21 |
+
print(f"Version: {google.generativeai.__version__}")
|
| 22 |
+
except ImportError:
|
| 23 |
+
print("ERROR: google.generativeai NOT installed.")
|
| 24 |
+
|
| 25 |
+
client = GeminiClient()
|
| 26 |
+
print(f"GeminiClient available: {client.available}")
|
| 27 |
+
|
| 28 |
+
if client.available:
|
| 29 |
+
print("Attempting simple generation...")
|
| 30 |
+
try:
|
| 31 |
+
model = genai.GenerativeModel('gemini-2.0-flash')
|
| 32 |
+
response = model.generate_content("Hello, do you work?")
|
| 33 |
+
print(f"Response: {response.text}")
|
| 34 |
+
except Exception as e:
|
| 35 |
+
print(f"Generation failed: {e}")
|
| 36 |
+
else:
|
| 37 |
+
print("Client not available, skipping generation test.")
|
fitting_system/__init__.py
ADDED
|
File without changes
|
fitting_system/admin.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
from .models import Size, Color, Product, ProductVariant, Inventory, BodyScan, Recommendation
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
@admin.register(Size)
|
| 6 |
+
class SizeAdmin(admin.ModelAdmin):
|
| 7 |
+
list_display = ['name', 'chest_min', 'chest_max', 'waist_min', 'waist_max', 'height_min', 'height_max']
|
| 8 |
+
search_fields = ['name']
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@admin.register(Color)
|
| 12 |
+
class ColorAdmin(admin.ModelAdmin):
|
| 13 |
+
list_display = ['name', 'hex_code', 'category']
|
| 14 |
+
list_filter = ['category']
|
| 15 |
+
search_fields = ['name']
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@admin.register(Product)
|
| 19 |
+
class ProductAdmin(admin.ModelAdmin):
|
| 20 |
+
list_display = ['name', 'category', 'gender', 'price', 'created_at']
|
| 21 |
+
list_filter = ['category', 'gender']
|
| 22 |
+
search_fields = ['name', 'description']
|
| 23 |
+
ordering = ['name']
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class InventoryInline(admin.TabularInline):
|
| 27 |
+
model = Inventory
|
| 28 |
+
extra = 0
|
| 29 |
+
fields = ['quantity', 'low_stock_threshold', 'last_updated']
|
| 30 |
+
readonly_fields = ['last_updated']
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@admin.register(ProductVariant)
|
| 34 |
+
class ProductVariantAdmin(admin.ModelAdmin):
|
| 35 |
+
list_display = ['product', 'size', 'color', 'sku', 'get_stock_quantity', 'get_stock_status']
|
| 36 |
+
list_filter = ['product__category', 'size', 'color']
|
| 37 |
+
search_fields = ['product__name', 'sku']
|
| 38 |
+
inlines = [InventoryInline]
|
| 39 |
+
|
| 40 |
+
def get_stock_quantity(self, obj):
|
| 41 |
+
try:
|
| 42 |
+
return obj.inventory.quantity
|
| 43 |
+
except Inventory.DoesNotExist:
|
| 44 |
+
return 'N/A'
|
| 45 |
+
get_stock_quantity.short_description = 'Stock'
|
| 46 |
+
|
| 47 |
+
def get_stock_status(self, obj):
|
| 48 |
+
try:
|
| 49 |
+
inv = obj.inventory
|
| 50 |
+
if inv.is_out_of_stock:
|
| 51 |
+
return '🔴 Out of Stock'
|
| 52 |
+
elif inv.is_low_stock:
|
| 53 |
+
return '🟡 Low Stock'
|
| 54 |
+
else:
|
| 55 |
+
return '🟢 In Stock'
|
| 56 |
+
except Inventory.DoesNotExist:
|
| 57 |
+
return 'N/A'
|
| 58 |
+
get_stock_status.short_description = 'Status'
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@admin.register(Inventory)
|
| 62 |
+
class InventoryAdmin(admin.ModelAdmin):
|
| 63 |
+
list_display = ['product_variant', 'quantity', 'low_stock_threshold', 'get_status', 'last_updated']
|
| 64 |
+
list_filter = ['last_updated']
|
| 65 |
+
search_fields = ['product_variant__product__name', 'product_variant__sku']
|
| 66 |
+
|
| 67 |
+
def get_status(self, obj):
|
| 68 |
+
if obj.is_out_of_stock:
|
| 69 |
+
return '🔴 Out of Stock'
|
| 70 |
+
elif obj.is_low_stock:
|
| 71 |
+
return '🟡 Low Stock'
|
| 72 |
+
else:
|
| 73 |
+
return '🟢 In Stock'
|
| 74 |
+
get_status.short_description = 'Status'
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@admin.register(BodyScan)
|
| 78 |
+
class BodyScanAdmin(admin.ModelAdmin):
|
| 79 |
+
list_display = ['session_id', 'height', 'chest', 'waist', 'shoulder_width', 'skin_tone', 'undertone', 'scanned_at']
|
| 80 |
+
list_filter = ['skin_tone', 'undertone', 'scanned_at']
|
| 81 |
+
search_fields = ['session_id']
|
| 82 |
+
readonly_fields = ['session_id', 'scanned_at']
|
| 83 |
+
ordering = ['-scanned_at']
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@admin.register(Recommendation)
|
| 87 |
+
class RecommendationAdmin(admin.ModelAdmin):
|
| 88 |
+
list_display = ['body_scan', 'product', 'recommended_size', 'recommended_fit', 'priority', 'created_at']
|
| 89 |
+
list_filter = ['recommended_size', 'recommended_fit', 'created_at']
|
| 90 |
+
search_fields = ['body_scan__session_id', 'product__name']
|
| 91 |
+
readonly_fields = ['created_at']
|
| 92 |
+
ordering = ['-created_at', '-priority']
|
fitting_system/ai_modules/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# AI modules package
|
fitting_system/ai_modules/body_measurement.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Body Measurement Estimation Module
|
| 3 |
+
|
| 4 |
+
Architecture:
|
| 5 |
+
- MediaPipe Pose: Used ONLY for real-time pose detection, landmark visualization,
|
| 6 |
+
and capturing well-framed body images (camera feedback).
|
| 7 |
+
- Gemini API: Used for actual measurement extraction from the captured images.
|
| 8 |
+
|
| 9 |
+
MediaPipe provides the visual feedback loop (pose overlay, framing guidance),
|
| 10 |
+
while Gemini provides the intelligence (measurement estimation).
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import cv2
|
| 14 |
+
import numpy as np
|
| 15 |
+
from typing import Dict, Optional, List
|
| 16 |
+
import os
|
| 17 |
+
import urllib.request
|
| 18 |
+
import logging
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
# New MediaPipe API (v0.10+)
|
| 24 |
+
import mediapipe as mp
|
| 25 |
+
from mediapipe.tasks import python
|
| 26 |
+
from mediapipe.tasks.python import vision
|
| 27 |
+
USE_NEW_API = True
|
| 28 |
+
except ImportError:
|
| 29 |
+
USE_NEW_API = False
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class BodyMeasurementEstimator:
|
| 33 |
+
"""
|
| 34 |
+
Body measurement estimator.
|
| 35 |
+
|
| 36 |
+
Uses MediaPipe for:
|
| 37 |
+
- Real-time pose detection (analyze_pose)
|
| 38 |
+
- Visual landmark feedback during camera capture
|
| 39 |
+
|
| 40 |
+
Uses Gemini API for:
|
| 41 |
+
- Actual body measurement extraction from images
|
| 42 |
+
- Body shape classification
|
| 43 |
+
- Skin tone analysis
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
# Model file URL for MediaPipe Pose Landmarker (used for pose visualization only)
|
| 47 |
+
MODEL_URL = "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_heavy/float16/latest/pose_landmarker_heavy.task"
|
| 48 |
+
MODEL_PATH = "pose_landmarker.task"
|
| 49 |
+
|
| 50 |
+
def __init__(self):
|
| 51 |
+
self.use_mediapipe = False
|
| 52 |
+
self.pose_landmarker = None
|
| 53 |
+
|
| 54 |
+
if USE_NEW_API:
|
| 55 |
+
# Download model if not exists
|
| 56 |
+
if not os.path.exists(self.MODEL_PATH):
|
| 57 |
+
print(f"Downloading MediaPipe Pose model...")
|
| 58 |
+
try:
|
| 59 |
+
urllib.request.urlretrieve(self.MODEL_URL, self.MODEL_PATH)
|
| 60 |
+
print("Model downloaded successfully!")
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"Failed to download model: {e}")
|
| 63 |
+
print("Pose visualization will be unavailable")
|
| 64 |
+
return
|
| 65 |
+
|
| 66 |
+
# Initialize PoseLandmarker (for visualization/feedback only)
|
| 67 |
+
try:
|
| 68 |
+
base_options = python.BaseOptions(model_asset_path=self.MODEL_PATH)
|
| 69 |
+
options = vision.PoseLandmarkerOptions(
|
| 70 |
+
base_options=base_options,
|
| 71 |
+
output_segmentation_masks=False, # Not needed - Gemini handles analysis
|
| 72 |
+
num_poses=1
|
| 73 |
+
)
|
| 74 |
+
self.pose_landmarker = vision.PoseLandmarker.create_from_options(options)
|
| 75 |
+
self.use_mediapipe = True
|
| 76 |
+
print("MediaPipe Pose initialized (for camera feedback only)")
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"Failed to initialize MediaPipe: {e}")
|
| 79 |
+
print("Pose visualization will be unavailable")
|
| 80 |
+
|
| 81 |
+
def analyze_pose(self, image_data: np.ndarray) -> Dict:
|
| 82 |
+
"""
|
| 83 |
+
Analyze pose for real-time camera feedback.
|
| 84 |
+
|
| 85 |
+
This is the ONLY function that uses MediaPipe directly.
|
| 86 |
+
It provides visual guidance to the user during image capture:
|
| 87 |
+
- Is the full body visible?
|
| 88 |
+
- Is the person centered?
|
| 89 |
+
- Are they too close/far?
|
| 90 |
+
|
| 91 |
+
The actual measurement extraction happens via Gemini in estimate_from_image().
|
| 92 |
+
"""
|
| 93 |
+
if not self.use_mediapipe or self.pose_landmarker is None:
|
| 94 |
+
return {
|
| 95 |
+
"detected": True,
|
| 96 |
+
"message": "System ready",
|
| 97 |
+
"status": "ready",
|
| 98 |
+
"quality": 1.0,
|
| 99 |
+
"landmarks": []
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
try:
|
| 103 |
+
image_rgb = cv2.cvtColor(image_data, cv2.COLOR_BGR2RGB)
|
| 104 |
+
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=image_rgb)
|
| 105 |
+
detection_result = self.pose_landmarker.detect(mp_image)
|
| 106 |
+
|
| 107 |
+
if not detection_result.pose_landmarks or len(detection_result.pose_landmarks) == 0:
|
| 108 |
+
return {
|
| 109 |
+
"detected": False,
|
| 110 |
+
"message": "No person detected",
|
| 111 |
+
"status": "bad",
|
| 112 |
+
"quality": 0.0,
|
| 113 |
+
"landmarks": []
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
landmarks = detection_result.pose_landmarks[0]
|
| 117 |
+
|
| 118 |
+
# Key landmarks for framing feedback
|
| 119 |
+
nose = landmarks[0]
|
| 120 |
+
l_ankle = landmarks[27]
|
| 121 |
+
r_ankle = landmarks[28]
|
| 122 |
+
|
| 123 |
+
# Check framing conditions
|
| 124 |
+
message = "Perfect! Hold still..."
|
| 125 |
+
status = "good"
|
| 126 |
+
quality = 0.95
|
| 127 |
+
|
| 128 |
+
# Check Feet visibility
|
| 129 |
+
if l_ankle.y > 0.95 or r_ankle.y > 0.95:
|
| 130 |
+
message = "Feet not visible - Step Back"
|
| 131 |
+
status = "warning"
|
| 132 |
+
quality = 0.5
|
| 133 |
+
# Check Head visibility
|
| 134 |
+
elif nose.y < 0.05:
|
| 135 |
+
message = "Head cut off - Adjust Camera"
|
| 136 |
+
status = "warning"
|
| 137 |
+
quality = 0.5
|
| 138 |
+
else:
|
| 139 |
+
# Check if too far
|
| 140 |
+
person_h = ((l_ankle.y + r_ankle.y) / 2) - nose.y
|
| 141 |
+
if person_h < 0.4:
|
| 142 |
+
message = "Too far - Come Closer"
|
| 143 |
+
status = "warning"
|
| 144 |
+
quality = 0.6
|
| 145 |
+
|
| 146 |
+
landmarks_data = [{'x': lm.x, 'y': lm.y} for lm in landmarks]
|
| 147 |
+
|
| 148 |
+
return {
|
| 149 |
+
"detected": True,
|
| 150 |
+
"message": message,
|
| 151 |
+
"status": status,
|
| 152 |
+
"quality": quality,
|
| 153 |
+
"landmarks": landmarks_data
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
except Exception as e:
|
| 157 |
+
logger.error(f"Pose analysis error: {e}")
|
| 158 |
+
return {"detected": False, "message": "Analysis failed", "status": "error", "quality": 0.0}
|
| 159 |
+
|
| 160 |
+
def estimate_from_image(
|
| 161 |
+
self,
|
| 162 |
+
image_data: np.ndarray,
|
| 163 |
+
reference_height_cm: Optional[float] = None
|
| 164 |
+
) -> Dict[str, float]:
|
| 165 |
+
"""
|
| 166 |
+
Estimate body measurements from a single image using Gemini API.
|
| 167 |
+
|
| 168 |
+
This method converts the image to JPEG bytes and sends it to Gemini
|
| 169 |
+
for intelligent measurement extraction.
|
| 170 |
+
|
| 171 |
+
Args:
|
| 172 |
+
image_data: Image as numpy array (BGR format from OpenCV)
|
| 173 |
+
reference_height_cm: Optional known height for calibration
|
| 174 |
+
|
| 175 |
+
Returns:
|
| 176 |
+
Dictionary with measurements in centimeters
|
| 177 |
+
"""
|
| 178 |
+
from .gemini_client import get_gemini_client
|
| 179 |
+
|
| 180 |
+
if True:
|
| 181 |
+
# Convert OpenCV image to JPEG bytes for Gemini
|
| 182 |
+
image_bytes = self._image_to_bytes(image_data)
|
| 183 |
+
|
| 184 |
+
# Use Gemini for measurement extraction
|
| 185 |
+
gemini = get_gemini_client()
|
| 186 |
+
measurements = gemini.extract_measurements(
|
| 187 |
+
front_image_bytes=image_bytes,
|
| 188 |
+
reference_height_cm=reference_height_cm
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
logger.info(f"Gemini measurements: {measurements}")
|
| 192 |
+
return measurements
|
| 193 |
+
|
| 194 |
+
def estimate_from_front_and_side(
|
| 195 |
+
self,
|
| 196 |
+
front_image: np.ndarray,
|
| 197 |
+
side_image: Optional[np.ndarray] = None,
|
| 198 |
+
reference_height_cm: Optional[float] = None
|
| 199 |
+
) -> Dict[str, float]:
|
| 200 |
+
"""
|
| 201 |
+
Estimate measurements from front and optional side images using Gemini.
|
| 202 |
+
|
| 203 |
+
Both images are sent to Gemini for more accurate analysis.
|
| 204 |
+
"""
|
| 205 |
+
from .gemini_client import get_gemini_client
|
| 206 |
+
|
| 207 |
+
if True:
|
| 208 |
+
front_bytes = self._image_to_bytes(front_image)
|
| 209 |
+
side_bytes = self._image_to_bytes(side_image) if side_image is not None else None
|
| 210 |
+
|
| 211 |
+
gemini = get_gemini_client()
|
| 212 |
+
measurements = gemini.extract_measurements(
|
| 213 |
+
front_image_bytes=front_bytes,
|
| 214 |
+
side_image_bytes=side_bytes,
|
| 215 |
+
reference_height_cm=reference_height_cm
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
return measurements
|
| 219 |
+
|
| 220 |
+
def analyze_body_complete(
|
| 221 |
+
self,
|
| 222 |
+
front_image: np.ndarray,
|
| 223 |
+
side_image: Optional[np.ndarray] = None,
|
| 224 |
+
reference_height_cm: Optional[float] = None
|
| 225 |
+
) -> Dict:
|
| 226 |
+
"""
|
| 227 |
+
Complete body analysis using Gemini: measurements + body shape + skin tone.
|
| 228 |
+
|
| 229 |
+
This is the preferred method - it makes a single Gemini call to get
|
| 230 |
+
everything at once, which is faster and more consistent.
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
front_image: Front view body image (BGR numpy array)
|
| 234 |
+
side_image: Optional side view image
|
| 235 |
+
reference_height_cm: Optional known height for calibration
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
Dict with:
|
| 239 |
+
measurements: Dict[str, float]
|
| 240 |
+
body_shape: str
|
| 241 |
+
skin_tone: str
|
| 242 |
+
undertone: str
|
| 243 |
+
confidence: float
|
| 244 |
+
"""
|
| 245 |
+
from .gemini_client import get_gemini_client
|
| 246 |
+
|
| 247 |
+
if True:
|
| 248 |
+
front_bytes = self._image_to_bytes(front_image)
|
| 249 |
+
side_bytes = self._image_to_bytes(side_image) if side_image is not None else None
|
| 250 |
+
|
| 251 |
+
gemini = get_gemini_client()
|
| 252 |
+
result = gemini.analyze_body(
|
| 253 |
+
front_image_bytes=front_bytes,
|
| 254 |
+
side_image_bytes=side_bytes,
|
| 255 |
+
reference_height_cm=reference_height_cm
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
return result
|
| 259 |
+
|
| 260 |
+
def estimate_with_stability(
|
| 261 |
+
self,
|
| 262 |
+
frames: List[np.ndarray],
|
| 263 |
+
reference_height_cm: Optional[float] = None
|
| 264 |
+
) -> Dict[str, float]:
|
| 265 |
+
"""
|
| 266 |
+
Estimate measurements from multiple frames for stability.
|
| 267 |
+
|
| 268 |
+
For Gemini-based analysis, we select the best frame rather than
|
| 269 |
+
averaging multiple results (which would waste API calls).
|
| 270 |
+
|
| 271 |
+
Args:
|
| 272 |
+
frames: List of image frames (BGR format)
|
| 273 |
+
reference_height_cm: Optional known height for calibration
|
| 274 |
+
|
| 275 |
+
Returns:
|
| 276 |
+
Dictionary with measurements
|
| 277 |
+
"""
|
| 278 |
+
if not frames:
|
| 279 |
+
raise ValueError("No frames provided for measurement estimation")
|
| 280 |
+
|
| 281 |
+
# Select the middle frame (usually best quality/pose)
|
| 282 |
+
best_frame_idx = len(frames) // 2
|
| 283 |
+
best_frame = frames[best_frame_idx]
|
| 284 |
+
|
| 285 |
+
return self.estimate_from_image(best_frame, reference_height_cm)
|
| 286 |
+
|
| 287 |
+
# --- Helper Methods ---
|
| 288 |
+
|
| 289 |
+
@staticmethod
|
| 290 |
+
def _image_to_bytes(image_data: np.ndarray) -> bytes:
|
| 291 |
+
"""Convert OpenCV BGR image to JPEG bytes for Gemini API."""
|
| 292 |
+
success, buffer = cv2.imencode('.jpg', image_data, [cv2.IMWRITE_JPEG_QUALITY, 85])
|
| 293 |
+
if not success:
|
| 294 |
+
raise ValueError("Failed to encode image to JPEG")
|
| 295 |
+
return buffer.tobytes()
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
# _fallback_measurements REMOVED to force error propagation
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
@staticmethod
|
| 302 |
+
def normalize_measurement(value: float, round_to: float = 0.5, min_val: float = 0, max_val: float = 300) -> float:
|
| 303 |
+
"""Fashion-grade normalization: round to nearest increment and clamp."""
|
| 304 |
+
clamped = max(min_val, min(max_val, value))
|
| 305 |
+
return round(clamped / round_to) * round_to
|
| 306 |
+
|
| 307 |
+
def __del__(self):
|
| 308 |
+
"""Cleanup MediaPipe resources."""
|
| 309 |
+
if hasattr(self, 'pose_landmarker') and self.pose_landmarker:
|
| 310 |
+
self.pose_landmarker.close()
|
fitting_system/ai_modules/gemini_client.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gemini AI Client Module
|
| 3 |
+
Central AI engine for body measurements and recommendations.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import base64
|
| 8 |
+
import logging
|
| 9 |
+
import re
|
| 10 |
+
from typing import Dict, List, Optional, Tuple
|
| 11 |
+
from django.conf import settings
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
import google.generativeai as genai
|
| 17 |
+
GEMINI_AVAILABLE = True
|
| 18 |
+
except ImportError:
|
| 19 |
+
GEMINI_AVAILABLE = False
|
| 20 |
+
logger.warning("google-generativeai package not installed. Run: pip install google-generativeai")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class GeminiClient:
|
| 24 |
+
"""
|
| 25 |
+
Wrapper around Google Gemini API for fashion AI tasks.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
# gemini-2.5-flash: fastest valid model as of Feb 2026
|
| 29 |
+
MODEL_NAME = "gemini-2.5-flash"
|
| 30 |
+
|
| 31 |
+
def __init__(self, api_key: str = None):
|
| 32 |
+
self.api_key = api_key or getattr(settings, 'GEMINI_API_KEY', None)
|
| 33 |
+
self.model = None
|
| 34 |
+
self.available = False
|
| 35 |
+
|
| 36 |
+
if not GEMINI_AVAILABLE:
|
| 37 |
+
return
|
| 38 |
+
|
| 39 |
+
if not self.api_key:
|
| 40 |
+
logger.error("No Gemini API key configured.")
|
| 41 |
+
return
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
genai.configure(api_key=self.api_key)
|
| 45 |
+
# Using system_instruction to set the "persona" permanently for the model
|
| 46 |
+
self.model = genai.GenerativeModel(
|
| 47 |
+
model_name=self.MODEL_NAME,
|
| 48 |
+
system_instruction="You are an expert anthropometric AI. Your goal is to visually analyze human body proportions and provide highly accurate clothing size and measurement data."
|
| 49 |
+
)
|
| 50 |
+
self.available = True
|
| 51 |
+
logger.info(f"Gemini AI client ({self.MODEL_NAME}) initialized successfully")
|
| 52 |
+
except Exception as e:
|
| 53 |
+
logger.error(f"Failed to initialize Gemini: {e}")
|
| 54 |
+
|
| 55 |
+
def _encode_image_for_gemini(self, image_bytes: bytes, mime_type: str = "image/jpeg") -> dict:
|
| 56 |
+
return {
|
| 57 |
+
"mime_type": mime_type,
|
| 58 |
+
"data": base64.b64encode(image_bytes).decode("utf-8")
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
def _parse_json_response(self, text: str) -> dict:
|
| 62 |
+
json_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?\s*```', text, re.DOTALL)
|
| 63 |
+
if json_match:
|
| 64 |
+
text = json_match.group(1)
|
| 65 |
+
|
| 66 |
+
json_obj_match = re.search(r'(\{.*\})', text, re.DOTALL)
|
| 67 |
+
if json_obj_match:
|
| 68 |
+
text = json_obj_match.group(1)
|
| 69 |
+
|
| 70 |
+
try:
|
| 71 |
+
return json.loads(text)
|
| 72 |
+
except json.JSONDecodeError as e:
|
| 73 |
+
logger.error(f"Failed to parse JSON: {e}")
|
| 74 |
+
return {}
|
| 75 |
+
|
| 76 |
+
def analyze_body(
|
| 77 |
+
self,
|
| 78 |
+
front_image_bytes: bytes,
|
| 79 |
+
side_image_bytes: bytes = None,
|
| 80 |
+
reference_height_cm: float = None
|
| 81 |
+
) -> Dict:
|
| 82 |
+
if not self.available:
|
| 83 |
+
raise RuntimeError("Gemini AI service is not available")
|
| 84 |
+
|
| 85 |
+
height_info = f"The person's actual height is {reference_height_cm} cm." if reference_height_cm else "Estimate height based on surroundings."
|
| 86 |
+
|
| 87 |
+
prompt = f"""Analyze the person in the image(s) for a virtual fitting room.
|
| 88 |
+
{height_info}
|
| 89 |
+
|
| 90 |
+
1. Extract precise body measurements in cm.
|
| 91 |
+
2. Identify body shape (hourglass, rectangle, triangle, inverted_triangle, oval).
|
| 92 |
+
3. Identify skin tone (light, medium, dark) and undertone (warm, cool).
|
| 93 |
+
|
| 94 |
+
Return ONLY a JSON object:
|
| 95 |
+
{{
|
| 96 |
+
"measurements": {{
|
| 97 |
+
"height": <cm>,
|
| 98 |
+
"shoulder_width": <cm>,
|
| 99 |
+
"chest": <cm>,
|
| 100 |
+
"waist": <cm>,
|
| 101 |
+
"hip": <cm>,
|
| 102 |
+
"torso_length": <cm>,
|
| 103 |
+
"arm_length": <cm>,
|
| 104 |
+
"inseam": <cm>
|
| 105 |
+
}},
|
| 106 |
+
"body_shape": "...",
|
| 107 |
+
"skin_tone": "...",
|
| 108 |
+
"undertone": "...",
|
| 109 |
+
"confidence": <0.0-1.0>
|
| 110 |
+
}}"""
|
| 111 |
+
|
| 112 |
+
content_parts = [prompt, self._encode_image_for_gemini(front_image_bytes)]
|
| 113 |
+
if side_image_bytes:
|
| 114 |
+
content_parts.append(self._encode_image_for_gemini(side_image_bytes))
|
| 115 |
+
|
| 116 |
+
response = self.model.generate_content(content_parts)
|
| 117 |
+
result = self._parse_json_response(response.text)
|
| 118 |
+
|
| 119 |
+
if not result or "measurements" not in result:
|
| 120 |
+
raise ValueError("Incomplete data from Gemini")
|
| 121 |
+
|
| 122 |
+
result["measurements"] = self._validate_measurements(result["measurements"], reference_height_cm)
|
| 123 |
+
return result
|
| 124 |
+
|
| 125 |
+
def _validate_measurements(self, raw: Dict, reference_height: float = None) -> Dict[str, float]:
|
| 126 |
+
"""Validates measurements without 'flattening' the person's unique shape."""
|
| 127 |
+
validated = {}
|
| 128 |
+
# Get height first as it is our primary scale
|
| 129 |
+
height = float(raw.get("height", reference_height or 175))
|
| 130 |
+
if reference_height: height = reference_height
|
| 131 |
+
|
| 132 |
+
# Dynamic ratios: We allow a range rather than a fixed multiplier
|
| 133 |
+
# This prevents an athletic person from being 'clamped' into a rectangle shape
|
| 134 |
+
proportions = {
|
| 135 |
+
"shoulder_width": (0.20, 0.35), # 20% to 35% of height
|
| 136 |
+
"chest": (0.45, 0.75),
|
| 137 |
+
"waist": (0.35, 0.70),
|
| 138 |
+
"hip": (0.45, 0.75),
|
| 139 |
+
"torso_length": (0.25, 0.35),
|
| 140 |
+
"arm_length": (0.30, 0.40),
|
| 141 |
+
"inseam": (0.40, 0.50),
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
validated["height"] = height
|
| 145 |
+
for key, (min_ratio, max_ratio) in proportions.items():
|
| 146 |
+
val = float(raw.get(key, height * ((min_ratio + max_ratio) / 2)))
|
| 147 |
+
|
| 148 |
+
# Check if value is within a 'realistic' human ratio
|
| 149 |
+
lower_bound = height * min_ratio
|
| 150 |
+
upper_bound = height * max_ratio
|
| 151 |
+
|
| 152 |
+
# Clamp to the bounds to avoid 'impossible' measurements
|
| 153 |
+
final_val = max(lower_bound, min(upper_bound, val))
|
| 154 |
+
validated[key] = round(final_val * 2) / 2 # Round to nearest 0.5cm
|
| 155 |
+
|
| 156 |
+
return validated
|
| 157 |
+
|
| 158 |
+
def get_size_recommendation(self, measurements: Dict, garment_type: str, body_shape: str = "rectangle", available_sizes: List[str] = None) -> Dict:
|
| 159 |
+
if not self.available:
|
| 160 |
+
raise RuntimeError("Gemini AI is not available for size recommendation")
|
| 161 |
+
|
| 162 |
+
if available_sizes is None:
|
| 163 |
+
available_sizes = ["XS", "S", "M", "L", "XL", "XXL"]
|
| 164 |
+
|
| 165 |
+
prompt = f"""You are an expert fashion sizing specialist.
|
| 166 |
+
|
| 167 |
+
Analyze the following body measurements and recommend the single best clothing size.
|
| 168 |
+
|
| 169 |
+
Body Measurements (all values in centimetres):
|
| 170 |
+
{json.dumps(measurements, indent=2)}
|
| 171 |
+
|
| 172 |
+
Garment Type: {garment_type}
|
| 173 |
+
Body Shape: {body_shape}
|
| 174 |
+
Available Sizes: {', '.join(available_sizes)}
|
| 175 |
+
|
| 176 |
+
Use your expert knowledge of international sizing standards.
|
| 177 |
+
Do NOT apply fixed thresholds — reason holistically from ALL measurements (chest, waist, hips, shoulder width, height, etc.).
|
| 178 |
+
Consider the garment type and body shape when deciding between borderline sizes.
|
| 179 |
+
|
| 180 |
+
Return ONLY a valid JSON object:
|
| 181 |
+
{{
|
| 182 |
+
"recommended_size": "<one of the available sizes>",
|
| 183 |
+
"fit_type": "<slim, regular, or oversize>",
|
| 184 |
+
"reasoning": "<one sentence explaining why this size fits best>"
|
| 185 |
+
}}"""
|
| 186 |
+
|
| 187 |
+
response = self.model.generate_content(prompt)
|
| 188 |
+
result = self._parse_json_response(response.text)
|
| 189 |
+
|
| 190 |
+
if not result or "recommended_size" not in result:
|
| 191 |
+
raise ValueError("Gemini failed to return a valid size recommendation")
|
| 192 |
+
|
| 193 |
+
recommended = result["recommended_size"]
|
| 194 |
+
if recommended not in available_sizes:
|
| 195 |
+
raise ValueError(
|
| 196 |
+
f"Gemini returned size '{recommended}' which is not in available sizes {available_sizes}"
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
if result.get("fit_type") not in ["slim", "regular", "oversize"]:
|
| 200 |
+
result["fit_type"] = "regular"
|
| 201 |
+
|
| 202 |
+
logger.info(f"Gemini size recommendation: {recommended} ({result.get('reasoning', '')})")
|
| 203 |
+
return result
|
| 204 |
+
|
| 205 |
+
def get_color_recommendations(self, skin_tone: str, undertone: str = "warm", body_shape: str = "rectangle", garment_type: str = None) -> Dict:
|
| 206 |
+
"""
|
| 207 |
+
Ask Gemini to pick exactly ONE shirt colour and ONE pants colour
|
| 208 |
+
from the pre-approved palette for the given skin tone.
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
dict with keys 'recommended_shirt' and 'recommended_pants'
|
| 212 |
+
(both are exact colour name strings from color_palettes.py).
|
| 213 |
+
"""
|
| 214 |
+
if not self.available:
|
| 215 |
+
raise RuntimeError("Gemini AI is not available for color recommendations")
|
| 216 |
+
|
| 217 |
+
from fitting_system.color_palettes import get_shirt_color_names, get_pants_color_names
|
| 218 |
+
|
| 219 |
+
shirt_options = get_shirt_color_names(skin_tone)
|
| 220 |
+
pants_options = get_pants_color_names(skin_tone)
|
| 221 |
+
|
| 222 |
+
prompt = f"""You are an expert fashion color consultant.
|
| 223 |
+
|
| 224 |
+
Person's profile:
|
| 225 |
+
- Skin tone: {skin_tone}
|
| 226 |
+
- Undertone: {undertone}
|
| 227 |
+
- Body shape: {body_shape}
|
| 228 |
+
|
| 229 |
+
Available shirt colors (pick exactly ONE): {', '.join(shirt_options)}
|
| 230 |
+
Available pants colors (pick exactly ONE): {', '.join(pants_options)}
|
| 231 |
+
|
| 232 |
+
Choose the single best shirt color and single best pants color from the lists above.
|
| 233 |
+
You MUST only use color names from the provided lists. Do NOT invent new colors.
|
| 234 |
+
|
| 235 |
+
Return ONLY a JSON object:
|
| 236 |
+
{{"recommended_shirt": "<one of the shirt colors>", "recommended_pants": "<one of the pants colors>"}}"""
|
| 237 |
+
|
| 238 |
+
response = self.model.generate_content(prompt)
|
| 239 |
+
result = self._parse_json_response(response.text)
|
| 240 |
+
|
| 241 |
+
rec_shirt = result.get("recommended_shirt", "")
|
| 242 |
+
rec_pants = result.get("recommended_pants", "")
|
| 243 |
+
|
| 244 |
+
# Validate – fall back to first option if Gemini hallucinated
|
| 245 |
+
if rec_shirt not in shirt_options:
|
| 246 |
+
logger.warning(f"Gemini returned invalid shirt color '{rec_shirt}', falling back to '{shirt_options[0]}'")
|
| 247 |
+
rec_shirt = shirt_options[0]
|
| 248 |
+
if rec_pants not in pants_options:
|
| 249 |
+
logger.warning(f"Gemini returned invalid pants color '{rec_pants}', falling back to '{pants_options[0]}'")
|
| 250 |
+
rec_pants = pants_options[0]
|
| 251 |
+
|
| 252 |
+
return {"recommended_shirt": rec_shirt, "recommended_pants": rec_pants}
|
| 253 |
+
|
| 254 |
+
def get_styling_advice(self, measurements: Dict, body_shape: str, skin_tone: str, undertone: str = "warm") -> str:
|
| 255 |
+
if not self.available:
|
| 256 |
+
raise RuntimeError("Gemini AI is not available for styling advice")
|
| 257 |
+
|
| 258 |
+
prompt = f"""You are a personal fashion stylist. Give brief, practical styling advice.
|
| 259 |
+
|
| 260 |
+
Person's profile:
|
| 261 |
+
- Body shape: {body_shape}
|
| 262 |
+
- Skin tone: {skin_tone} with {undertone} undertone
|
| 263 |
+
- Measurements: {json.dumps(measurements)}
|
| 264 |
+
|
| 265 |
+
Give 3-4 concise styling tips specific to their body type and coloring. Return plain text."""
|
| 266 |
+
|
| 267 |
+
response = self.model.generate_content(prompt)
|
| 268 |
+
return response.text.strip()
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
# ---------------------------------------------------------------------------
|
| 272 |
+
# Singleton accessor
|
| 273 |
+
# ---------------------------------------------------------------------------
|
| 274 |
+
_gemini_client_instance = None
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def get_gemini_client() -> GeminiClient:
|
| 278 |
+
"""Return the global GeminiClient singleton."""
|
| 279 |
+
global _gemini_client_instance
|
| 280 |
+
if _gemini_client_instance is None:
|
| 281 |
+
_gemini_client_instance = GeminiClient()
|
| 282 |
+
return _gemini_client_instance
|
fitting_system/ai_modules/recommendation_engine.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Recommendation Engine
|
| 3 |
+
Generates intelligent clothing recommendations using Gemini AI
|
| 4 |
+
and database-backed product matching.
|
| 5 |
+
|
| 6 |
+
Architecture:
|
| 7 |
+
- Gemini AI: Intelligent size/fit/color recommendations
|
| 8 |
+
- Database: Product matching, inventory checking, variant selection
|
| 9 |
+
- color_palettes.py: Single source of truth for colour options
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import logging
|
| 13 |
+
from typing import Dict, List, Tuple
|
| 14 |
+
from django.db.models import Q
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class RecommendationEngine:
|
| 20 |
+
"""
|
| 21 |
+
Generates clothing recommendations using Gemini AI for intelligence
|
| 22 |
+
and the product database for matching.
|
| 23 |
+
|
| 24 |
+
Gemini handles:
|
| 25 |
+
- Size recommendation (considering body shape + garment type)
|
| 26 |
+
- Fit type recommendation
|
| 27 |
+
- Color recommendations (1 shirt + 1 pants from the unified palette)
|
| 28 |
+
- Styling advice
|
| 29 |
+
|
| 30 |
+
Database handles:
|
| 31 |
+
- Finding products with matching size in stock
|
| 32 |
+
- Color variant matching
|
| 33 |
+
- Inventory availability
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
SIZE_ORDER = ['XS', 'S', 'M', 'L', 'XL', 'XXL', 'XXXL']
|
| 37 |
+
|
| 38 |
+
GARMENT_MEASUREMENTS = {
|
| 39 |
+
'shirt': {'fit_focus': 'chest'},
|
| 40 |
+
'pants': {'fit_focus': 'waist'},
|
| 41 |
+
'dress': {'fit_focus': 'waist'},
|
| 42 |
+
'jacket': {'fit_focus': 'chest'},
|
| 43 |
+
'skirt': {'fit_focus': 'waist'},
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
def __init__(self):
|
| 47 |
+
self._gemini = None
|
| 48 |
+
|
| 49 |
+
@property
|
| 50 |
+
def gemini(self):
|
| 51 |
+
"""Lazy-load Gemini client."""
|
| 52 |
+
if self._gemini is None:
|
| 53 |
+
from .gemini_client import get_gemini_client
|
| 54 |
+
self._gemini = get_gemini_client()
|
| 55 |
+
return self._gemini
|
| 56 |
+
|
| 57 |
+
# ── Size ──────────────────────────────────────────────────────
|
| 58 |
+
def recommend_size(self, measurements: Dict[str, float],
|
| 59 |
+
garment_type: str = 'shirt',
|
| 60 |
+
body_shape: str = 'rectangle') -> str:
|
| 61 |
+
if not self.gemini.available:
|
| 62 |
+
raise RuntimeError("Gemini AI is not available for size recommendation")
|
| 63 |
+
result = self.gemini.get_size_recommendation(
|
| 64 |
+
measurements=measurements,
|
| 65 |
+
garment_type=garment_type,
|
| 66 |
+
body_shape=body_shape,
|
| 67 |
+
)
|
| 68 |
+
size = result.get("recommended_size", "M")
|
| 69 |
+
logger.info(f"Gemini size recommendation: {size} for {garment_type}")
|
| 70 |
+
return size
|
| 71 |
+
|
| 72 |
+
# ── Fit ───────────────────────────────────────────────────────
|
| 73 |
+
def recommend_fit(self, measurements: Dict[str, float],
|
| 74 |
+
garment_type: str = 'shirt',
|
| 75 |
+
body_shape: str = 'rectangle') -> str:
|
| 76 |
+
if not self.gemini.available:
|
| 77 |
+
raise RuntimeError("Gemini AI is not available for fit recommendation")
|
| 78 |
+
result = self.gemini.get_size_recommendation(
|
| 79 |
+
measurements=measurements,
|
| 80 |
+
garment_type=garment_type,
|
| 81 |
+
body_shape=body_shape,
|
| 82 |
+
)
|
| 83 |
+
return "regular"
|
| 84 |
+
|
| 85 |
+
# ── Colours (NEW: returns dict with shirt + pants) ────────────
|
| 86 |
+
def recommend_colors(self, skin_tone: str, undertone: str = 'warm') -> Dict[str, str]:
|
| 87 |
+
"""
|
| 88 |
+
Returns dict: {'recommended_shirt': '<name>', 'recommended_pants': '<name>'}
|
| 89 |
+
"""
|
| 90 |
+
if not self.gemini.available:
|
| 91 |
+
raise RuntimeError("Gemini AI is not available for color recommendation")
|
| 92 |
+
result = self.gemini.get_color_recommendations(
|
| 93 |
+
skin_tone=skin_tone,
|
| 94 |
+
undertone=undertone,
|
| 95 |
+
)
|
| 96 |
+
if not result or 'recommended_shirt' not in result:
|
| 97 |
+
raise ValueError("Gemini returned empty color recommendations")
|
| 98 |
+
return result
|
| 99 |
+
|
| 100 |
+
# ── Full product matching ─────────────────────────────────────
|
| 101 |
+
def get_matching_product_variants(
|
| 102 |
+
self,
|
| 103 |
+
body_scan,
|
| 104 |
+
gender: str = None,
|
| 105 |
+
limit: int = 6
|
| 106 |
+
) -> List[Dict]:
|
| 107 |
+
"""
|
| 108 |
+
Get actual products from store with specific size and color recommendations.
|
| 109 |
+
Uses Gemini AI for size/color/fit recommendations, then matches against inventory.
|
| 110 |
+
"""
|
| 111 |
+
from fitting_system.models import Product, ProductVariant, Color, Size
|
| 112 |
+
|
| 113 |
+
measurements = {
|
| 114 |
+
'height': float(body_scan.height),
|
| 115 |
+
'chest': float(body_scan.chest),
|
| 116 |
+
'waist': float(body_scan.waist),
|
| 117 |
+
'shoulder_width': float(body_scan.shoulder_width),
|
| 118 |
+
}
|
| 119 |
+
if body_scan.hip:
|
| 120 |
+
measurements['hip'] = float(body_scan.hip)
|
| 121 |
+
if body_scan.inseam:
|
| 122 |
+
measurements['inseam'] = float(body_scan.inseam)
|
| 123 |
+
if body_scan.torso_length:
|
| 124 |
+
measurements['torso_length'] = float(body_scan.torso_length)
|
| 125 |
+
if body_scan.arm_length:
|
| 126 |
+
measurements['arm_length'] = float(body_scan.arm_length)
|
| 127 |
+
|
| 128 |
+
body_shape = getattr(body_scan, 'body_shape', 'rectangle') or 'rectangle'
|
| 129 |
+
undertone = getattr(body_scan, 'undertone', 'warm')
|
| 130 |
+
|
| 131 |
+
# Gemini-powered recommendations
|
| 132 |
+
color_rec = self.recommend_colors(body_scan.skin_tone, undertone)
|
| 133 |
+
rec_shirt_name = color_rec['recommended_shirt']
|
| 134 |
+
rec_pants_name = color_rec['recommended_pants']
|
| 135 |
+
recommended_fit = 'regular' # fit_type field removed
|
| 136 |
+
|
| 137 |
+
# Map colour names → Color objects
|
| 138 |
+
rec_shirt_color = Color.objects.filter(name=rec_shirt_name).first()
|
| 139 |
+
rec_pants_color = Color.objects.filter(name=rec_pants_name).first()
|
| 140 |
+
|
| 141 |
+
# Filter products
|
| 142 |
+
if gender and gender in ['men', 'women']:
|
| 143 |
+
products = Product.objects.filter(
|
| 144 |
+
Q(gender=gender) | Q(gender='unisex')
|
| 145 |
+
)
|
| 146 |
+
else:
|
| 147 |
+
products = Product.objects.all()
|
| 148 |
+
|
| 149 |
+
matching_products = []
|
| 150 |
+
|
| 151 |
+
for product in products:
|
| 152 |
+
rec_size = self.recommend_size(
|
| 153 |
+
measurements,
|
| 154 |
+
garment_type=product.category,
|
| 155 |
+
body_shape=body_shape,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
# Choose the right recommended colour for the product category
|
| 159 |
+
is_top = product.category in ('shirt', 'jacket', 'dress')
|
| 160 |
+
target_color = rec_shirt_color if is_top else rec_pants_color
|
| 161 |
+
target_color_name = rec_shirt_name if is_top else rec_pants_name
|
| 162 |
+
|
| 163 |
+
fit_matches = True # fit_type field removed
|
| 164 |
+
|
| 165 |
+
# Priority 1: Exact size + recommended colour + in stock
|
| 166 |
+
if target_color:
|
| 167 |
+
variant = ProductVariant.objects.filter(
|
| 168 |
+
product=product,
|
| 169 |
+
size__name=rec_size,
|
| 170 |
+
color=target_color,
|
| 171 |
+
inventory__quantity__gt=0,
|
| 172 |
+
).select_related('size', 'color', 'product').first()
|
| 173 |
+
|
| 174 |
+
if variant:
|
| 175 |
+
matching_products.append({
|
| 176 |
+
'product': product,
|
| 177 |
+
'variant': variant,
|
| 178 |
+
'recommended_size': rec_size,
|
| 179 |
+
'recommended_color': variant.color.name,
|
| 180 |
+
'color_hex': variant.color.hex_code,
|
| 181 |
+
|
| 182 |
+
'is_perfect_match': True,
|
| 183 |
+
'fit_matches_recommendation': fit_matches,
|
| 184 |
+
'recommended_fit': recommended_fit,
|
| 185 |
+
'fit_message': f"This {product.category} in size {rec_size} with {variant.color.name} will fit you perfectly!",
|
| 186 |
+
})
|
| 187 |
+
continue
|
| 188 |
+
|
| 189 |
+
# Priority 2: Exact size + any colour in stock
|
| 190 |
+
fallback_variant = ProductVariant.objects.filter(
|
| 191 |
+
product=product,
|
| 192 |
+
size__name=rec_size,
|
| 193 |
+
inventory__quantity__gt=0,
|
| 194 |
+
).select_related('size', 'color', 'product').first()
|
| 195 |
+
|
| 196 |
+
if fallback_variant:
|
| 197 |
+
matching_products.append({
|
| 198 |
+
'product': product,
|
| 199 |
+
'variant': fallback_variant,
|
| 200 |
+
'recommended_size': rec_size,
|
| 201 |
+
'recommended_color': fallback_variant.color.name,
|
| 202 |
+
'color_hex': fallback_variant.color.hex_code,
|
| 203 |
+
|
| 204 |
+
'is_perfect_match': False,
|
| 205 |
+
'fit_matches_recommendation': fit_matches,
|
| 206 |
+
'recommended_fit': recommended_fit,
|
| 207 |
+
'fit_message': f"This {product.category} in size {rec_size} will fit you great!",
|
| 208 |
+
})
|
| 209 |
+
|
| 210 |
+
matching_products.sort(key=lambda x: (
|
| 211 |
+
not x['fit_matches_recommendation'],
|
| 212 |
+
not x['is_perfect_match'],
|
| 213 |
+
x['product'].name,
|
| 214 |
+
))
|
| 215 |
+
return matching_products[:limit]
|
| 216 |
+
|
| 217 |
+
# ── Generate & save Recommendation rows ───────────────────────
|
| 218 |
+
def generate_recommendations_for_scan(self, body_scan) -> List[object]:
|
| 219 |
+
from fitting_system.models import Recommendation
|
| 220 |
+
|
| 221 |
+
measurements = {
|
| 222 |
+
'height': float(body_scan.height),
|
| 223 |
+
'chest': float(body_scan.chest),
|
| 224 |
+
'waist': float(body_scan.waist),
|
| 225 |
+
'shoulder_width': float(body_scan.shoulder_width),
|
| 226 |
+
}
|
| 227 |
+
if body_scan.hip:
|
| 228 |
+
measurements['hip'] = float(body_scan.hip)
|
| 229 |
+
if body_scan.torso_length:
|
| 230 |
+
measurements['torso_length'] = float(body_scan.torso_length)
|
| 231 |
+
if body_scan.arm_length:
|
| 232 |
+
measurements['arm_length'] = float(body_scan.arm_length)
|
| 233 |
+
if body_scan.inseam:
|
| 234 |
+
measurements['inseam'] = float(body_scan.inseam)
|
| 235 |
+
|
| 236 |
+
body_shape = getattr(body_scan, 'body_shape', 'rectangle') or 'rectangle'
|
| 237 |
+
undertone = getattr(body_scan, 'undertone', 'warm')
|
| 238 |
+
|
| 239 |
+
# Gemini
|
| 240 |
+
base_recommended_size = self.recommend_size(measurements, body_shape=body_shape)
|
| 241 |
+
recommended_fit = self.recommend_fit(measurements, body_shape=body_shape)
|
| 242 |
+
color_rec = self.recommend_colors(body_scan.skin_tone, undertone)
|
| 243 |
+
recommended_colors_str = f"{color_rec['recommended_shirt']}, {color_rec['recommended_pants']}"
|
| 244 |
+
|
| 245 |
+
# Product recommendations across genders
|
| 246 |
+
product_recommendations = []
|
| 247 |
+
for gender in ['men', 'women', 'unisex']:
|
| 248 |
+
recs = self._recommend_products(
|
| 249 |
+
measurements, body_scan.skin_tone, undertone,
|
| 250 |
+
gender=gender, body_shape=body_shape, limit=10,
|
| 251 |
+
)
|
| 252 |
+
product_recommendations.extend(recs)
|
| 253 |
+
|
| 254 |
+
# Dedupe
|
| 255 |
+
seen = set()
|
| 256 |
+
unique = []
|
| 257 |
+
for product, priority in product_recommendations:
|
| 258 |
+
if product.id not in seen:
|
| 259 |
+
seen.add(product.id)
|
| 260 |
+
unique.append((product, priority))
|
| 261 |
+
unique.sort(key=lambda x: x[1], reverse=True)
|
| 262 |
+
|
| 263 |
+
# Create Recommendation objects
|
| 264 |
+
recs_created = []
|
| 265 |
+
for product, priority in unique[:10]:
|
| 266 |
+
rec_size = self.recommend_size(
|
| 267 |
+
measurements, garment_type=product.category, body_shape=body_shape,
|
| 268 |
+
)
|
| 269 |
+
rec = Recommendation.objects.create(
|
| 270 |
+
body_scan=body_scan,
|
| 271 |
+
product=product,
|
| 272 |
+
recommended_size=rec_size,
|
| 273 |
+
recommended_fit=recommended_fit,
|
| 274 |
+
recommended_colors=recommended_colors_str,
|
| 275 |
+
priority=priority,
|
| 276 |
+
)
|
| 277 |
+
recs_created.append(rec)
|
| 278 |
+
return recs_created
|
| 279 |
+
|
| 280 |
+
def _recommend_products(
|
| 281 |
+
self, measurements, skin_tone, undertone,
|
| 282 |
+
gender='unisex', body_shape='rectangle', limit=10,
|
| 283 |
+
) -> List[Tuple[object, int]]:
|
| 284 |
+
from fitting_system.models import Product, ProductVariant, Color
|
| 285 |
+
|
| 286 |
+
recommended_size = self.recommend_size(measurements, body_shape=body_shape)
|
| 287 |
+
recommended_fit = self.recommend_fit(measurements, body_shape=body_shape)
|
| 288 |
+
|
| 289 |
+
color_rec = self.recommend_colors(skin_tone, undertone)
|
| 290 |
+
rec_shirt_color = Color.objects.filter(name=color_rec['recommended_shirt']).first()
|
| 291 |
+
rec_pants_color = Color.objects.filter(name=color_rec['recommended_pants']).first()
|
| 292 |
+
|
| 293 |
+
products = Product.objects.filter(
|
| 294 |
+
Q(gender=gender) | Q(gender='unisex')
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
recommendations = []
|
| 298 |
+
for product in products:
|
| 299 |
+
available = ProductVariant.objects.filter(
|
| 300 |
+
product=product, inventory__quantity__gt=0,
|
| 301 |
+
)
|
| 302 |
+
if not available.exists():
|
| 303 |
+
continue
|
| 304 |
+
|
| 305 |
+
priority = 5 # base
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
if available.filter(size__name=recommended_size).exists():
|
| 310 |
+
priority += 10
|
| 311 |
+
|
| 312 |
+
is_top = product.category in ('shirt', 'jacket', 'dress')
|
| 313 |
+
target = rec_shirt_color if is_top else rec_pants_color
|
| 314 |
+
if target and available.filter(color=target).exists():
|
| 315 |
+
priority += 10
|
| 316 |
+
|
| 317 |
+
recommendations.append((product, priority))
|
| 318 |
+
|
| 319 |
+
recommendations.sort(key=lambda x: x[1], reverse=True)
|
| 320 |
+
return recommendations[:limit]
|
| 321 |
+
|
| 322 |
+
# _fallback_recommend_size REMOVED – Gemini AI is the sole source.
|
fitting_system/ai_modules/yolo_analyzer.py
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
YOLO-based Body Analyzer Module
|
| 3 |
+
|
| 4 |
+
Architecture:
|
| 5 |
+
- YOLOv8 Pose: Real-time pose detection + body keypoint extraction for
|
| 6 |
+
body measurement estimation (replaces MediaPipe).
|
| 7 |
+
- YOLOv8 Pose (face image): Used for face detection; skin tone extracted
|
| 8 |
+
directly from the face region using color analysis.
|
| 9 |
+
- Gemini LLM: Receives the estimated measurements and returns a recommended
|
| 10 |
+
clothing size letter (S, M, L, XL, XXL, XXXL).
|
| 11 |
+
|
| 12 |
+
Two images are required:
|
| 13 |
+
1. Body image – full-body front view for pose + measurements.
|
| 14 |
+
2. Face image – close-up selfie for accurate skin-tone detection.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import cv2
|
| 18 |
+
import numpy as np
|
| 19 |
+
import logging
|
| 20 |
+
from typing import Dict, Optional, Tuple
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
# ---------------------------------------------------------------------------
|
| 25 |
+
# Try to import ultralytics (YOLOv8)
|
| 26 |
+
# ---------------------------------------------------------------------------
|
| 27 |
+
try:
|
| 28 |
+
from ultralytics import YOLO
|
| 29 |
+
YOLO_AVAILABLE = True
|
| 30 |
+
except ImportError:
|
| 31 |
+
YOLO_AVAILABLE = False
|
| 32 |
+
logger.warning("ultralytics not installed. Run: pip install ultralytics")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
# YOLO model singleton helpers
|
| 37 |
+
# ---------------------------------------------------------------------------
|
| 38 |
+
_pose_model = None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _get_pose_model():
|
| 42 |
+
"""Lazy-load the YOLOv8 pose model (downloads on first use)."""
|
| 43 |
+
global _pose_model
|
| 44 |
+
if _pose_model is None:
|
| 45 |
+
if not YOLO_AVAILABLE:
|
| 46 |
+
raise RuntimeError("ultralytics package is not installed.")
|
| 47 |
+
_pose_model = YOLO("yolov8n-pose.pt") # nano – fast, good enough
|
| 48 |
+
logger.info("YOLOv8 pose model loaded.")
|
| 49 |
+
return _pose_model
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ---------------------------------------------------------------------------
|
| 53 |
+
# Keypoint indices (COCO 17-keypoint layout used by YOLOv8)
|
| 54 |
+
# ---------------------------------------------------------------------------
|
| 55 |
+
KP = {
|
| 56 |
+
"nose": 0,
|
| 57 |
+
"left_eye": 1,
|
| 58 |
+
"right_eye": 2,
|
| 59 |
+
"left_ear": 3,
|
| 60 |
+
"right_ear": 4,
|
| 61 |
+
"left_shoulder": 5,
|
| 62 |
+
"right_shoulder":6,
|
| 63 |
+
"left_elbow": 7,
|
| 64 |
+
"right_elbow": 8,
|
| 65 |
+
"left_wrist": 9,
|
| 66 |
+
"right_wrist": 10,
|
| 67 |
+
"left_hip": 11,
|
| 68 |
+
"right_hip": 12,
|
| 69 |
+
"left_knee": 13,
|
| 70 |
+
"right_knee": 14,
|
| 71 |
+
"left_ankle": 15,
|
| 72 |
+
"right_ankle": 16,
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
SKELETON_CONNECTIONS = [
|
| 76 |
+
(KP["left_shoulder"], KP["right_shoulder"]),
|
| 77 |
+
(KP["left_shoulder"], KP["left_elbow"]),
|
| 78 |
+
(KP["left_elbow"], KP["left_wrist"]),
|
| 79 |
+
(KP["right_shoulder"], KP["right_elbow"]),
|
| 80 |
+
(KP["right_elbow"], KP["right_wrist"]),
|
| 81 |
+
(KP["left_shoulder"], KP["left_hip"]),
|
| 82 |
+
(KP["right_shoulder"], KP["right_hip"]),
|
| 83 |
+
(KP["left_hip"], KP["right_hip"]),
|
| 84 |
+
(KP["left_hip"], KP["left_knee"]),
|
| 85 |
+
(KP["left_knee"], KP["left_ankle"]),
|
| 86 |
+
(KP["right_hip"], KP["right_knee"]),
|
| 87 |
+
(KP["right_knee"], KP["right_ankle"]),
|
| 88 |
+
(KP["nose"], KP["left_shoulder"]),
|
| 89 |
+
(KP["nose"], KP["right_shoulder"]),
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# ---------------------------------------------------------------------------
|
| 94 |
+
# Core analyzer class
|
| 95 |
+
# ---------------------------------------------------------------------------
|
| 96 |
+
|
| 97 |
+
class YOLOBodyAnalyzer:
|
| 98 |
+
"""
|
| 99 |
+
Analyzes body images using YOLOv8 pose estimation and skin-tone color
|
| 100 |
+
analysis, then uses an LLM to recommend a clothing size.
|
| 101 |
+
"""
|
| 102 |
+
|
| 103 |
+
# ---- Size chart (chest circumference in cm) used as LLM context ----
|
| 104 |
+
SIZE_CHART = {
|
| 105 |
+
"XS": (76, 84),
|
| 106 |
+
"S": (84, 92),
|
| 107 |
+
"M": (92, 100),
|
| 108 |
+
"L": (100, 108),
|
| 109 |
+
"XL": (108, 116),
|
| 110 |
+
"XXL": (116, 124),
|
| 111 |
+
"XXXL": (124, 140),
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
# ---- Fitzpatrick-inspired skin-tone buckets (HSV hue + saturation) ----
|
| 115 |
+
SKIN_TONE_BUCKETS = [
|
| 116 |
+
("very_light", (200, 170, 130)), # approximate RGB centres
|
| 117 |
+
("light", (195, 155, 110)),
|
| 118 |
+
("intermediate",(180, 130, 85)),
|
| 119 |
+
("tan", (160, 110, 65)),
|
| 120 |
+
("dark", (120, 80, 45)),
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
+
def __init__(self):
|
| 124 |
+
pass # GeminiClient is used directly via get_gemini_client()
|
| 125 |
+
|
| 126 |
+
# ------------------------------------------------------------------
|
| 127 |
+
# Public API
|
| 128 |
+
# ------------------------------------------------------------------
|
| 129 |
+
|
| 130 |
+
def analyze_pose_frame(self, image_bgr: np.ndarray) -> Dict:
|
| 131 |
+
"""
|
| 132 |
+
Real-time pose feedback for the camera overlay (body image).
|
| 133 |
+
Returns landmark positions + guidance message.
|
| 134 |
+
"""
|
| 135 |
+
try:
|
| 136 |
+
model = _get_pose_model()
|
| 137 |
+
except RuntimeError as e:
|
| 138 |
+
return {"detected": True, "message": "System ready", "status": "ready",
|
| 139 |
+
"quality": 1.0, "landmarks": []}
|
| 140 |
+
|
| 141 |
+
results = model(image_bgr, verbose=False)
|
| 142 |
+
|
| 143 |
+
if not results or len(results) == 0:
|
| 144 |
+
return {"detected": False, "message": "No person detected", "status": "bad",
|
| 145 |
+
"quality": 0.0, "landmarks": []}
|
| 146 |
+
|
| 147 |
+
result = results[0]
|
| 148 |
+
if result.keypoints is None or len(result.keypoints.xy) == 0:
|
| 149 |
+
return {"detected": False, "message": "No person detected", "status": "bad",
|
| 150 |
+
"quality": 0.0, "landmarks": []}
|
| 151 |
+
|
| 152 |
+
kps = result.keypoints.xy[0].cpu().numpy() # shape (17, 2)
|
| 153 |
+
h, w = image_bgr.shape[:2]
|
| 154 |
+
|
| 155 |
+
# Normalise to [0,1]
|
| 156 |
+
landmarks = [{"x": float(kp[0]) / w, "y": float(kp[1]) / h} for kp in kps]
|
| 157 |
+
|
| 158 |
+
# Framing checks
|
| 159 |
+
nose = landmarks[KP["nose"]]
|
| 160 |
+
l_ankle = landmarks[KP["left_ankle"]]
|
| 161 |
+
r_ankle = landmarks[KP["right_ankle"]]
|
| 162 |
+
|
| 163 |
+
message = "Perfect! Hold still..."
|
| 164 |
+
status = "good"
|
| 165 |
+
quality = 0.95
|
| 166 |
+
|
| 167 |
+
avg_ankle_y = (l_ankle["y"] + r_ankle["y"]) / 2
|
| 168 |
+
|
| 169 |
+
if avg_ankle_y > 0.95:
|
| 170 |
+
message, status, quality = "Feet not visible – Step Back", "warning", 0.5
|
| 171 |
+
elif nose["y"] < 0.05:
|
| 172 |
+
message, status, quality = "Head cut off – Adjust Camera", "warning", 0.5
|
| 173 |
+
else:
|
| 174 |
+
person_h = avg_ankle_y - nose["y"]
|
| 175 |
+
if person_h < 0.4:
|
| 176 |
+
message, status, quality = "Too far – Come Closer", "warning", 0.6
|
| 177 |
+
|
| 178 |
+
return {"detected": True, "message": message, "status": status,
|
| 179 |
+
"quality": quality, "landmarks": landmarks}
|
| 180 |
+
|
| 181 |
+
def analyze_face_frame(self, image_bgr: np.ndarray) -> Dict:
|
| 182 |
+
"""
|
| 183 |
+
Real-time face feedback for the selfie step (face image).
|
| 184 |
+
Uses OpenCV Haar cascade for speed.
|
| 185 |
+
"""
|
| 186 |
+
h, w = image_bgr.shape[:2]
|
| 187 |
+
face_cascade = cv2.CascadeClassifier(
|
| 188 |
+
cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
|
| 189 |
+
)
|
| 190 |
+
gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
|
| 191 |
+
faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(50, 50))
|
| 192 |
+
|
| 193 |
+
if len(faces) == 0:
|
| 194 |
+
return {"detected": False, "message": "No face detected – look at the camera",
|
| 195 |
+
"status": "error", "quality": 0.0, "landmarks": []}
|
| 196 |
+
|
| 197 |
+
x, y, fw, fh = max(faces, key=lambda f: f[2] * f[3])
|
| 198 |
+
face_area_ratio = (fw * fh) / (w * h)
|
| 199 |
+
center_x_ratio = abs((x + fw / 2) - w / 2) / w
|
| 200 |
+
center_y_ratio = abs((y + fh / 2) - h / 2) / h
|
| 201 |
+
|
| 202 |
+
if face_area_ratio < 0.08:
|
| 203 |
+
return {"detected": True, "message": "Move CLOSER to the camera",
|
| 204 |
+
"status": "warning", "quality": 0.4, "landmarks": []}
|
| 205 |
+
elif face_area_ratio < 0.15:
|
| 206 |
+
return {"detected": True, "message": "A bit closer for better accuracy",
|
| 207 |
+
"status": "warning", "quality": 0.6, "landmarks": []}
|
| 208 |
+
elif center_x_ratio > 0.25 or center_y_ratio > 0.25:
|
| 209 |
+
return {"detected": True, "message": "Center your face in the circle",
|
| 210 |
+
"status": "warning", "quality": 0.7, "landmarks": []}
|
| 211 |
+
else:
|
| 212 |
+
return {"detected": True, "message": "Perfect! Hold still...",
|
| 213 |
+
"status": "good", "quality": 0.95, "landmarks": []}
|
| 214 |
+
|
| 215 |
+
def extract_measurements_from_body_image(
|
| 216 |
+
self, body_image_bgr: np.ndarray, user_height_cm: float = None
|
| 217 |
+
) -> Dict[str, float]:
|
| 218 |
+
"""
|
| 219 |
+
Use YOLO keypoints to estimate body measurements (cm).
|
| 220 |
+
Requires user_height_cm for accurate pixel-to-cm calibration.
|
| 221 |
+
Returns a dict with: height, shoulder_width, chest, waist,
|
| 222 |
+
hip, torso_length, arm_length, inseam.
|
| 223 |
+
|
| 224 |
+
Raises RuntimeError if YOLO detection fails.
|
| 225 |
+
Raises ValueError if user_height_cm is not provided.
|
| 226 |
+
"""
|
| 227 |
+
if not user_height_cm or user_height_cm <= 0:
|
| 228 |
+
raise ValueError("User height is required for accurate measurements. Please provide your height in cm.")
|
| 229 |
+
|
| 230 |
+
try:
|
| 231 |
+
model = _get_pose_model()
|
| 232 |
+
except RuntimeError as e:
|
| 233 |
+
raise RuntimeError(f"YOLO model is not available: {e}")
|
| 234 |
+
|
| 235 |
+
results = model(body_image_bgr, verbose=False)
|
| 236 |
+
if not results or results[0].keypoints is None:
|
| 237 |
+
raise RuntimeError("YOLO could not detect any person in the body image.")
|
| 238 |
+
|
| 239 |
+
result = results[0]
|
| 240 |
+
if len(result.keypoints.xy) == 0:
|
| 241 |
+
raise RuntimeError("YOLO could not detect keypoints in the body image.")
|
| 242 |
+
|
| 243 |
+
kps = result.keypoints.xy[0].cpu().numpy() # (17, 2) in pixels
|
| 244 |
+
h_img, w_img = body_image_bgr.shape[:2]
|
| 245 |
+
|
| 246 |
+
def kp(name):
|
| 247 |
+
return kps[KP[name]]
|
| 248 |
+
|
| 249 |
+
# ---- pixel distances ----
|
| 250 |
+
l_shoulder = kp("left_shoulder")
|
| 251 |
+
r_shoulder = kp("right_shoulder")
|
| 252 |
+
l_hip = kp("left_hip")
|
| 253 |
+
r_hip = kp("right_hip")
|
| 254 |
+
l_ankle = kp("left_ankle")
|
| 255 |
+
r_ankle = kp("right_ankle")
|
| 256 |
+
nose_pt = kp("nose")
|
| 257 |
+
l_wrist = kp("left_wrist")
|
| 258 |
+
l_elbow = kp("left_elbow")
|
| 259 |
+
r_wrist = kp("right_wrist")
|
| 260 |
+
r_elbow = kp("right_elbow")
|
| 261 |
+
|
| 262 |
+
def dist(a, b):
|
| 263 |
+
return float(np.linalg.norm(a - b))
|
| 264 |
+
|
| 265 |
+
# ---------------------------------------------------------------
|
| 266 |
+
# FIX 1: Estimate top-of-head instead of using the nose directly.
|
| 267 |
+
# The nose sits ~55-60% of the way down the head. We approximate
|
| 268 |
+
# the head top by projecting upward by 0.6× the nose-to-shoulder
|
| 269 |
+
# distance (which is roughly one head-length).
|
| 270 |
+
# This prevents px_height from being ~10% too short, which was
|
| 271 |
+
# inflating every cm measurement by ~10%.
|
| 272 |
+
# ---------------------------------------------------------------
|
| 273 |
+
mid_shoulder = (l_shoulder + r_shoulder) / 2
|
| 274 |
+
nose_to_shoulder_dist = dist(nose_pt, mid_shoulder)
|
| 275 |
+
head_top_pt = nose_pt - np.array([0, nose_to_shoulder_dist * 0.6])
|
| 276 |
+
mid_ankle = (l_ankle + r_ankle) / 2
|
| 277 |
+
px_height = dist(head_top_pt, mid_ankle)
|
| 278 |
+
|
| 279 |
+
px_shoulder_w = dist(l_shoulder, r_shoulder)
|
| 280 |
+
px_torso = dist(mid_shoulder, (l_hip + r_hip) / 2)
|
| 281 |
+
px_hip_w = dist(l_hip, r_hip)
|
| 282 |
+
px_arm_l = (dist(l_shoulder, l_elbow) + dist(l_elbow, l_wrist) +
|
| 283 |
+
dist(r_shoulder, r_elbow) + dist(r_elbow, r_wrist)) / 2
|
| 284 |
+
px_inseam = (dist(l_hip, l_ankle) + dist(r_hip, r_ankle)) / 2
|
| 285 |
+
|
| 286 |
+
# ---- calibration: use user-provided height ----
|
| 287 |
+
if px_height < 1:
|
| 288 |
+
px_height = h_img * 0.85 # approximate from image height
|
| 289 |
+
|
| 290 |
+
px_per_cm = px_height / user_height_cm
|
| 291 |
+
|
| 292 |
+
def to_cm(px):
|
| 293 |
+
return round(px / px_per_cm, 1) if px_per_cm > 0 else 0.0
|
| 294 |
+
|
| 295 |
+
shoulder_w_cm = to_cm(px_shoulder_w)
|
| 296 |
+
torso_cm = to_cm(px_torso)
|
| 297 |
+
hip_w_cm = to_cm(px_hip_w)
|
| 298 |
+
arm_cm = to_cm(px_arm_l)
|
| 299 |
+
inseam_cm = to_cm(px_inseam)
|
| 300 |
+
|
| 301 |
+
# ---------------------------------------------------------------
|
| 302 |
+
# FIX 2: Derive waist width from the correct anatomical landmark.
|
| 303 |
+
# Previously waist_cm used px_hip_w (hip keypoint width), which
|
| 304 |
+
# made waist ≈ hip and erased body shape entirely.
|
| 305 |
+
# The waist sits roughly 65% down the torso from shoulders.
|
| 306 |
+
# We interpolate a waist point and use its estimated pixel width,
|
| 307 |
+
# approximated as 70% of the hip-keypoint width (a common
|
| 308 |
+
# anthropometric ratio for average adults).
|
| 309 |
+
# ---------------------------------------------------------------
|
| 310 |
+
# Estimate circumferences from skeletal widths using anthropometric ratios.
|
| 311 |
+
# YOLO keypoints measure joint-to-joint (bideltoid / bi-iliac), NOT full width.
|
| 312 |
+
# Ratios derived from anthropometric studies:
|
| 313 |
+
# bideltoid width → chest circumference ≈ ×2.7
|
| 314 |
+
# waist width → waist circumference ≈ ×2.5 (waist width ≈ 70% of hip width)
|
| 315 |
+
# bi-iliac width → hip circumference ≈ ×2.7
|
| 316 |
+
px_waist_w = px_hip_w * 0.70 # waist is narrower than hips
|
| 317 |
+
chest_cm = shoulder_w_cm * 2.7
|
| 318 |
+
waist_cm = to_cm(px_waist_w) * 2.5 # now correctly uses waist-estimated width
|
| 319 |
+
hip_cm = hip_w_cm * 2.7
|
| 320 |
+
|
| 321 |
+
# Clamp to realistic ranges
|
| 322 |
+
def clamp(v, lo, hi):
|
| 323 |
+
return max(lo, min(hi, v))
|
| 324 |
+
|
| 325 |
+
measurements = {
|
| 326 |
+
"height": user_height_cm,
|
| 327 |
+
"shoulder_width": clamp(shoulder_w_cm, 30, 60),
|
| 328 |
+
"chest": clamp(chest_cm, 65, 150),
|
| 329 |
+
"waist": clamp(waist_cm, 50, 140),
|
| 330 |
+
"hip": clamp(hip_cm, 70, 150),
|
| 331 |
+
"torso_length": clamp(torso_cm, 35, 65),
|
| 332 |
+
"arm_length": clamp(arm_cm, 45, 80),
|
| 333 |
+
"inseam": clamp(inseam_cm, 55, 100),
|
| 334 |
+
}
|
| 335 |
+
logger.info(f"YOLO measurements (height={user_height_cm}cm): {measurements}")
|
| 336 |
+
return measurements
|
| 337 |
+
|
| 338 |
+
def extract_skin_tone_from_face_image(
|
| 339 |
+
self, face_image_bgr: np.ndarray
|
| 340 |
+
) -> Tuple[str, str]:
|
| 341 |
+
"""
|
| 342 |
+
Detect skin tone from a face close-up image.
|
| 343 |
+
Returns (skin_tone, undertone) where:
|
| 344 |
+
skin_tone ∈ {very_light, light, intermediate, tan, dark}
|
| 345 |
+
undertone ∈ {warm, cool}
|
| 346 |
+
"""
|
| 347 |
+
h, w = face_image_bgr.shape[:2]
|
| 348 |
+
|
| 349 |
+
# Try to isolate the face region with Haar cascade
|
| 350 |
+
face_cascade = cv2.CascadeClassifier(
|
| 351 |
+
cv2.data.haarcascades + "haarcascade_frontalface_default.xml"
|
| 352 |
+
)
|
| 353 |
+
gray = cv2.cvtColor(face_image_bgr, cv2.COLOR_BGR2GRAY)
|
| 354 |
+
faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(50, 50))
|
| 355 |
+
|
| 356 |
+
if len(faces) > 0:
|
| 357 |
+
x, y, fw, fh = max(faces, key=lambda f: f[2] * f[3])
|
| 358 |
+
# Use the central 60% of the face to avoid hair/background
|
| 359 |
+
cx, cy = x + fw // 2, y + fh // 2
|
| 360 |
+
roi_w, roi_h = int(fw * 0.6), int(fh * 0.6)
|
| 361 |
+
x1 = max(0, cx - roi_w // 2)
|
| 362 |
+
y1 = max(0, cy - roi_h // 2)
|
| 363 |
+
x2 = min(w, cx + roi_w // 2)
|
| 364 |
+
y2 = min(h, cy + roi_h // 2)
|
| 365 |
+
roi = face_image_bgr[y1:y2, x1:x2]
|
| 366 |
+
else:
|
| 367 |
+
# Fallback: use the central 40% of the image
|
| 368 |
+
y1, y2 = int(h * 0.3), int(h * 0.7)
|
| 369 |
+
x1, x2 = int(w * 0.3), int(w * 0.7)
|
| 370 |
+
roi = face_image_bgr[y1:y2, x1:x2]
|
| 371 |
+
|
| 372 |
+
if roi.size == 0:
|
| 373 |
+
return "intermediate", "warm"
|
| 374 |
+
|
| 375 |
+
# Convert to RGB and compute mean colour
|
| 376 |
+
roi_rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)
|
| 377 |
+
mean_rgb = roi_rgb.reshape(-1, 3).mean(axis=0) # [R, G, B]
|
| 378 |
+
|
| 379 |
+
skin_tone = self._classify_skin_tone(mean_rgb)
|
| 380 |
+
undertone = self._classify_undertone(mean_rgb)
|
| 381 |
+
|
| 382 |
+
logger.info(f"Skin tone: {skin_tone}, undertone: {undertone}, mean_rgb: {mean_rgb}")
|
| 383 |
+
return skin_tone, undertone
|
| 384 |
+
|
| 385 |
+
def get_size_recommendation_from_llm(
|
| 386 |
+
self,
|
| 387 |
+
measurements: Dict[str, float],
|
| 388 |
+
body_shape: str = "rectangle", # FIX 3: accept body_shape so Gemini can use it
|
| 389 |
+
) -> str:
|
| 390 |
+
"""
|
| 391 |
+
Ask Gemini to recommend a clothing size letter based on measurements.
|
| 392 |
+
|
| 393 |
+
Uses the canonical GeminiClient (gemini-2.5-flash) – no rule-based
|
| 394 |
+
fallback. Raises RuntimeError if Gemini is unavailable or fails.
|
| 395 |
+
"""
|
| 396 |
+
from .gemini_client import get_gemini_client
|
| 397 |
+
client = get_gemini_client()
|
| 398 |
+
if not client.available:
|
| 399 |
+
raise RuntimeError("Gemini AI is not available for size recommendation")
|
| 400 |
+
result = client.get_size_recommendation(
|
| 401 |
+
measurements=measurements,
|
| 402 |
+
garment_type="general",
|
| 403 |
+
body_shape=body_shape, # FIX 3: pass body_shape so Gemini reasons correctly
|
| 404 |
+
)
|
| 405 |
+
size = result.get("recommended_size", "").upper().strip()
|
| 406 |
+
valid = {"XS", "S", "M", "L", "XL", "XXL", "XXXL"}
|
| 407 |
+
if size not in valid:
|
| 408 |
+
raise ValueError(f"Gemini returned invalid size: '{size}'. Response: {result}")
|
| 409 |
+
logger.info(f"Gemini recommended size: {size} (reason: {result.get('reasoning', 'N/A')})")
|
| 410 |
+
return size
|
| 411 |
+
|
| 412 |
+
def full_analysis(
|
| 413 |
+
self,
|
| 414 |
+
body_image_bgr: np.ndarray,
|
| 415 |
+
face_image_bgr: np.ndarray,
|
| 416 |
+
user_height_cm: float = None,
|
| 417 |
+
body_shape: str = "rectangle", # FIX 3: accept and forward body_shape
|
| 418 |
+
) -> Dict:
|
| 419 |
+
"""
|
| 420 |
+
Complete analysis pipeline:
|
| 421 |
+
1. Extract body measurements from body image (YOLO) using user height.
|
| 422 |
+
2. Extract skin tone from face image (color analysis).
|
| 423 |
+
3. Get recommended size from LLM.
|
| 424 |
+
|
| 425 |
+
Args:
|
| 426 |
+
body_image_bgr: Full-body front-view image (BGR numpy array).
|
| 427 |
+
face_image_bgr: Face close-up image (BGR numpy array).
|
| 428 |
+
user_height_cm: User's actual height in centimetres (required).
|
| 429 |
+
body_shape: Body shape string forwarded to Gemini for better
|
| 430 |
+
borderline-size decisions (e.g. "hourglass").
|
| 431 |
+
|
| 432 |
+
Returns:
|
| 433 |
+
{
|
| 434 |
+
"measurements": {...},
|
| 435 |
+
"skin_tone": "intermediate",
|
| 436 |
+
"undertone": "warm",
|
| 437 |
+
"recommended_size": "L",
|
| 438 |
+
"confidence": 0.85,
|
| 439 |
+
}
|
| 440 |
+
"""
|
| 441 |
+
measurements = self.extract_measurements_from_body_image(body_image_bgr, user_height_cm)
|
| 442 |
+
skin_tone, undertone = self.extract_skin_tone_from_face_image(face_image_bgr)
|
| 443 |
+
recommended_size = self.get_size_recommendation_from_llm(measurements, body_shape=body_shape)
|
| 444 |
+
|
| 445 |
+
return {
|
| 446 |
+
"measurements": measurements,
|
| 447 |
+
"skin_tone": skin_tone,
|
| 448 |
+
"undertone": undertone,
|
| 449 |
+
"recommended_size": recommended_size,
|
| 450 |
+
"confidence": 0.85,
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
# ------------------------------------------------------------------
|
| 454 |
+
# Women flow helpers
|
| 455 |
+
# ------------------------------------------------------------------
|
| 456 |
+
|
| 457 |
+
def extract_skin_tone_from_hand_image(
|
| 458 |
+
self, hand_image_bgr: np.ndarray
|
| 459 |
+
) -> Tuple[str, str]:
|
| 460 |
+
"""
|
| 461 |
+
Detect skin tone from a hand image.
|
| 462 |
+
Uses the central region of the image (no face detection needed).
|
| 463 |
+
Returns (skin_tone, undertone).
|
| 464 |
+
"""
|
| 465 |
+
h, w = hand_image_bgr.shape[:2]
|
| 466 |
+
|
| 467 |
+
# Sample the central 40% of the image (palm area)
|
| 468 |
+
y1, y2 = int(h * 0.3), int(h * 0.7)
|
| 469 |
+
x1, x2 = int(w * 0.3), int(w * 0.7)
|
| 470 |
+
roi = hand_image_bgr[y1:y2, x1:x2]
|
| 471 |
+
|
| 472 |
+
if roi.size == 0:
|
| 473 |
+
return "intermediate", "warm"
|
| 474 |
+
|
| 475 |
+
roi_rgb = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB)
|
| 476 |
+
mean_rgb = roi_rgb.reshape(-1, 3).mean(axis=0)
|
| 477 |
+
|
| 478 |
+
skin_tone = self._classify_skin_tone(mean_rgb)
|
| 479 |
+
undertone = self._classify_undertone(mean_rgb)
|
| 480 |
+
|
| 481 |
+
logger.info(f"Hand skin tone: {skin_tone}, undertone: {undertone}, mean_rgb: {mean_rgb}")
|
| 482 |
+
return skin_tone, undertone
|
| 483 |
+
|
| 484 |
+
def women_analysis(
|
| 485 |
+
self,
|
| 486 |
+
measurements: Dict[str, float],
|
| 487 |
+
hand_image_bgr: np.ndarray,
|
| 488 |
+
body_shape: str = "hourglass",
|
| 489 |
+
) -> Dict:
|
| 490 |
+
"""
|
| 491 |
+
Women's analysis pipeline (no YOLO needed):
|
| 492 |
+
1. Use manually entered measurements directly.
|
| 493 |
+
2. Extract skin tone from hand image.
|
| 494 |
+
3. Get recommended size from LLM.
|
| 495 |
+
|
| 496 |
+
Args:
|
| 497 |
+
measurements: Dict with keys like height, chest, waist, hip, etc.
|
| 498 |
+
hand_image_bgr: Hand photo (BGR numpy array) for skin tone.
|
| 499 |
+
body_shape: Body shape string for Gemini.
|
| 500 |
+
|
| 501 |
+
Returns same structure as full_analysis.
|
| 502 |
+
"""
|
| 503 |
+
skin_tone, undertone = self.extract_skin_tone_from_hand_image(hand_image_bgr)
|
| 504 |
+
recommended_size = self.get_size_recommendation_from_llm(measurements, body_shape=body_shape)
|
| 505 |
+
|
| 506 |
+
return {
|
| 507 |
+
"measurements": measurements,
|
| 508 |
+
"skin_tone": skin_tone,
|
| 509 |
+
"undertone": undertone,
|
| 510 |
+
"recommended_size": recommended_size,
|
| 511 |
+
"confidence": 0.90,
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
# ------------------------------------------------------------------
|
| 515 |
+
# Private helpers
|
| 516 |
+
# ------------------------------------------------------------------
|
| 517 |
+
|
| 518 |
+
@staticmethod
|
| 519 |
+
def _classify_skin_tone(mean_rgb: np.ndarray) -> str:
|
| 520 |
+
"""Map mean RGB to a Fitzpatrick-inspired skin tone label."""
|
| 521 |
+
# Use luminance as primary signal
|
| 522 |
+
r, g, b = mean_rgb
|
| 523 |
+
luminance = 0.299 * r + 0.587 * g + 0.114 * b
|
| 524 |
+
|
| 525 |
+
if luminance > 200:
|
| 526 |
+
return "very_light"
|
| 527 |
+
elif luminance > 170:
|
| 528 |
+
return "light"
|
| 529 |
+
elif luminance > 135:
|
| 530 |
+
return "intermediate"
|
| 531 |
+
elif luminance > 100:
|
| 532 |
+
return "tan"
|
| 533 |
+
else:
|
| 534 |
+
return "dark"
|
| 535 |
+
|
| 536 |
+
@staticmethod
|
| 537 |
+
def _classify_undertone(mean_rgb: np.ndarray) -> str:
|
| 538 |
+
"""
|
| 539 |
+
Warm undertone: red/yellow dominant (R > B).
|
| 540 |
+
Cool undertone: blue/pink dominant (B >= R).
|
| 541 |
+
"""
|
| 542 |
+
r, _, b = mean_rgb
|
| 543 |
+
return "warm" if r > b else "cool"
|
| 544 |
+
|
| 545 |
+
# _rule_based_size REMOVED – Gemini LLM is the sole source for size recommendations.
|
| 546 |
+
# _default_measurements REMOVED – user must provide their height; YOLO must detect keypoints.
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
# _SizeLLM REMOVED – size recommendations now go directly through GeminiClient
|
| 550 |
+
# (gemini-2.5-flash) in get_size_recommendation_from_llm().
|
| 551 |
+
|
| 552 |
+
|
| 553 |
+
# ---------------------------------------------------------------------------
|
| 554 |
+
# Module-level singleton
|
| 555 |
+
# ---------------------------------------------------------------------------
|
| 556 |
+
_analyzer_instance: Optional[YOLOBodyAnalyzer] = None
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
def get_yolo_analyzer() -> YOLOBodyAnalyzer:
|
| 560 |
+
"""Return the global YOLOBodyAnalyzer singleton."""
|
| 561 |
+
global _analyzer_instance
|
| 562 |
+
if _analyzer_instance is None:
|
| 563 |
+
_analyzer_instance = YOLOBodyAnalyzer()
|
| 564 |
+
return _analyzer_instance
|
fitting_system/apps.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.apps import AppConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class FittingSystemConfig(AppConfig):
|
| 5 |
+
default_auto_field = 'django.db.models.BigAutoField'
|
| 6 |
+
name = 'fitting_system'
|
fitting_system/color_palettes.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Unified Color Palettes
|
| 3 |
+
Single source of truth for every colour used in the store, avatar, and AI prompts.
|
| 4 |
+
|
| 5 |
+
Structure:
|
| 6 |
+
SKIN_TONE_PALETTES[skin_tone]['shirts'] → list of 4 dicts {name, hex}
|
| 7 |
+
SKIN_TONE_PALETTES[skin_tone]['pants'] → list of 4 dicts {name, hex}
|
| 8 |
+
|
| 9 |
+
Rules:
|
| 10 |
+
• Gemini MUST pick from the 4 shirt colours + 4 pants colours for the
|
| 11 |
+
detected skin tone — nothing else.
|
| 12 |
+
• The database seeder creates Color + ProductVariant rows for every
|
| 13 |
+
colour listed here.
|
| 14 |
+
• The 3D avatar renders exactly these swatches.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
SKIN_TONE_PALETTES = {
|
| 18 |
+
# ───────────────────────────────────────────────
|
| 19 |
+
'very_light': {
|
| 20 |
+
'shirts': [
|
| 21 |
+
{'name': 'Lavender', 'hex': '#e8d5f5'},
|
| 22 |
+
{'name': 'Baby Blue', 'hex': '#b5d8f7'},
|
| 23 |
+
{'name': 'Blush Pink', 'hex': '#f7c5d0'},
|
| 24 |
+
{'name': 'Charcoal', 'hex': '#37474f'},
|
| 25 |
+
],
|
| 26 |
+
'pants': [
|
| 27 |
+
{'name': 'Ash Grey', 'hex': '#cfd8dc'},
|
| 28 |
+
{'name': 'Slate', 'hex': '#607d8b'},
|
| 29 |
+
{'name': 'Indigo', 'hex': '#1a237e'},
|
| 30 |
+
{'name': 'Black', 'hex': '#111111'},
|
| 31 |
+
],
|
| 32 |
+
},
|
| 33 |
+
# ───────────────────────────────────────────────
|
| 34 |
+
'light': {
|
| 35 |
+
'shirts': [
|
| 36 |
+
{'name': 'Sky Blue', 'hex': '#bbdefb'},
|
| 37 |
+
{'name': 'Mint Green', 'hex': '#a5d6a7'},
|
| 38 |
+
{'name': 'Rose', 'hex': '#ef9a9a'},
|
| 39 |
+
{'name': 'White', 'hex': '#ffffff'},
|
| 40 |
+
],
|
| 41 |
+
'pants': [
|
| 42 |
+
{'name': 'Khaki', 'hex': '#c3b091'},
|
| 43 |
+
{'name': 'Walnut', 'hex': '#6d4c41'},
|
| 44 |
+
{'name': 'Deep Navy', 'hex': '#0d47a1'},
|
| 45 |
+
{'name': 'Black', 'hex': '#111111'},
|
| 46 |
+
],
|
| 47 |
+
},
|
| 48 |
+
# ───────────────────────────────────────────────
|
| 49 |
+
'intermediate': {
|
| 50 |
+
'shirts': [
|
| 51 |
+
{'name': 'Off-White', 'hex': '#f5f5f5'},
|
| 52 |
+
{'name': 'Ocean Blue', 'hex': '#0277bd'},
|
| 53 |
+
{'name': 'Olive Green', 'hex': '#558b2f'},
|
| 54 |
+
{'name': 'Burnt Orange', 'hex': '#e64a19'},
|
| 55 |
+
],
|
| 56 |
+
'pants': [
|
| 57 |
+
{'name': 'Sand', 'hex': '#d4c5a9'},
|
| 58 |
+
{'name': 'Dark Slate', 'hex': '#263238'},
|
| 59 |
+
{'name': 'Cobalt', 'hex': '#1565c0'},
|
| 60 |
+
{'name': 'Burgundy', 'hex': '#880e4f'},
|
| 61 |
+
],
|
| 62 |
+
},
|
| 63 |
+
# ───────────────────────────────────────────────
|
| 64 |
+
'tan': {
|
| 65 |
+
'shirts': [
|
| 66 |
+
{'name': 'Cream', 'hex': '#fffde7'},
|
| 67 |
+
{'name': 'Navy', 'hex': '#01579b'},
|
| 68 |
+
{'name': 'Deep Green', 'hex': '#004d40'},
|
| 69 |
+
{'name': 'Amber', 'hex': '#f57f17'},
|
| 70 |
+
],
|
| 71 |
+
'pants': [
|
| 72 |
+
{'name': 'Mocha', 'hex': '#8d6e63'},
|
| 73 |
+
{'name': 'Mahogany', 'hex': '#3e2723'},
|
| 74 |
+
{'name': 'Dark Teal', 'hex': '#006064'},
|
| 75 |
+
{'name': 'Graphite', 'hex': '#37474f'},
|
| 76 |
+
],
|
| 77 |
+
},
|
| 78 |
+
# ───────────────────────────────────────────────
|
| 79 |
+
'dark': {
|
| 80 |
+
'shirts': [
|
| 81 |
+
{'name': 'Lemon', 'hex': '#fff176'},
|
| 82 |
+
{'name': 'Bright Orange', 'hex': '#ff6d00'},
|
| 83 |
+
{'name': 'Electric Blue', 'hex': '#00e5ff'},
|
| 84 |
+
{'name': 'Hot Pink', 'hex': '#ff4081'},
|
| 85 |
+
],
|
| 86 |
+
'pants': [
|
| 87 |
+
{'name': 'Pearl', 'hex': '#e0e0e0'},
|
| 88 |
+
{'name': 'Silver', 'hex': '#bdbdbd'},
|
| 89 |
+
{'name': 'Plum', 'hex': '#4a148c'},
|
| 90 |
+
{'name': 'Almost Black', 'hex': '#212121'},
|
| 91 |
+
],
|
| 92 |
+
},
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def get_all_unique_colors():
|
| 97 |
+
"""Return a deduplicated list of all colour dicts across every skin tone."""
|
| 98 |
+
seen = set()
|
| 99 |
+
unique = []
|
| 100 |
+
for palette in SKIN_TONE_PALETTES.values():
|
| 101 |
+
for c in palette['shirts'] + palette['pants']:
|
| 102 |
+
if c['name'] not in seen:
|
| 103 |
+
seen.add(c['name'])
|
| 104 |
+
unique.append(c)
|
| 105 |
+
return unique
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def get_shirt_colors(skin_tone: str):
|
| 109 |
+
"""Return the 4 shirt colour dicts for a given skin tone."""
|
| 110 |
+
return SKIN_TONE_PALETTES.get(skin_tone, SKIN_TONE_PALETTES['intermediate'])['shirts']
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def get_pants_colors(skin_tone: str):
|
| 114 |
+
"""Return the 4 pants colour dicts for a given skin tone."""
|
| 115 |
+
return SKIN_TONE_PALETTES.get(skin_tone, SKIN_TONE_PALETTES['intermediate'])['pants']
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def get_shirt_color_names(skin_tone: str):
|
| 119 |
+
"""Return a plain list of shirt colour names for a given skin tone."""
|
| 120 |
+
return [c['name'] for c in get_shirt_colors(skin_tone)]
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def get_pants_color_names(skin_tone: str):
|
| 124 |
+
"""Return a plain list of pants colour names for a given skin tone."""
|
| 125 |
+
return [c['name'] for c in get_pants_colors(skin_tone)]
|
fitting_system/fixtures/initial_data.json
ADDED
|
@@ -0,0 +1,3296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"model": "fitting_system.size",
|
| 4 |
+
"pk": 1,
|
| 5 |
+
"fields": {
|
| 6 |
+
"name": "S",
|
| 7 |
+
"chest_min": "85.00",
|
| 8 |
+
"chest_max": "92.00",
|
| 9 |
+
"waist_min": "70.00",
|
| 10 |
+
"waist_max": "77.00",
|
| 11 |
+
"shoulder_min": "40.00",
|
| 12 |
+
"shoulder_max": "43.00",
|
| 13 |
+
"height_min": "160.00",
|
| 14 |
+
"height_max": "170.00"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"model": "fitting_system.size",
|
| 19 |
+
"pk": 2,
|
| 20 |
+
"fields": {
|
| 21 |
+
"name": "M",
|
| 22 |
+
"chest_min": "93.00",
|
| 23 |
+
"chest_max": "100.00",
|
| 24 |
+
"waist_min": "78.00",
|
| 25 |
+
"waist_max": "85.00",
|
| 26 |
+
"shoulder_min": "44.00",
|
| 27 |
+
"shoulder_max": "47.00",
|
| 28 |
+
"height_min": "168.00",
|
| 29 |
+
"height_max": "178.00"
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"model": "fitting_system.size",
|
| 34 |
+
"pk": 3,
|
| 35 |
+
"fields": {
|
| 36 |
+
"name": "L",
|
| 37 |
+
"chest_min": "101.00",
|
| 38 |
+
"chest_max": "108.00",
|
| 39 |
+
"waist_min": "86.00",
|
| 40 |
+
"waist_max": "93.00",
|
| 41 |
+
"shoulder_min": "48.00",
|
| 42 |
+
"shoulder_max": "51.00",
|
| 43 |
+
"height_min": "175.00",
|
| 44 |
+
"height_max": "185.00"
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"model": "fitting_system.size",
|
| 49 |
+
"pk": 4,
|
| 50 |
+
"fields": {
|
| 51 |
+
"name": "XL",
|
| 52 |
+
"chest_min": "109.00",
|
| 53 |
+
"chest_max": "116.00",
|
| 54 |
+
"waist_min": "94.00",
|
| 55 |
+
"waist_max": "101.00",
|
| 56 |
+
"shoulder_min": "52.00",
|
| 57 |
+
"shoulder_max": "55.00",
|
| 58 |
+
"height_min": "180.00",
|
| 59 |
+
"height_max": "190.00"
|
| 60 |
+
}
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"model": "fitting_system.color",
|
| 64 |
+
"pk": 1,
|
| 65 |
+
"fields": {
|
| 66 |
+
"name": "Black",
|
| 67 |
+
"hex_code": "#000000",
|
| 68 |
+
"category": "neutral"
|
| 69 |
+
}
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"model": "fitting_system.color",
|
| 73 |
+
"pk": 2,
|
| 74 |
+
"fields": {
|
| 75 |
+
"name": "White",
|
| 76 |
+
"hex_code": "#FFFFFF",
|
| 77 |
+
"category": "neutral"
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"model": "fitting_system.color",
|
| 82 |
+
"pk": 3,
|
| 83 |
+
"fields": {
|
| 84 |
+
"name": "Navy Blue",
|
| 85 |
+
"hex_code": "#000080",
|
| 86 |
+
"category": "neutral"
|
| 87 |
+
}
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"model": "fitting_system.color",
|
| 91 |
+
"pk": 4,
|
| 92 |
+
"fields": {
|
| 93 |
+
"name": "Gray",
|
| 94 |
+
"hex_code": "#808080",
|
| 95 |
+
"category": "neutral"
|
| 96 |
+
}
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"model": "fitting_system.color",
|
| 100 |
+
"pk": 5,
|
| 101 |
+
"fields": {
|
| 102 |
+
"name": "Beige",
|
| 103 |
+
"hex_code": "#F5F5DC",
|
| 104 |
+
"category": "neutral"
|
| 105 |
+
}
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"model": "fitting_system.color",
|
| 109 |
+
"pk": 6,
|
| 110 |
+
"fields": {
|
| 111 |
+
"name": "Burgundy",
|
| 112 |
+
"hex_code": "#800020",
|
| 113 |
+
"category": "medium"
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"model": "fitting_system.color",
|
| 118 |
+
"pk": 7,
|
| 119 |
+
"fields": {
|
| 120 |
+
"name": "Light Blue",
|
| 121 |
+
"hex_code": "#ADD8E6",
|
| 122 |
+
"category": "light"
|
| 123 |
+
}
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"model": "fitting_system.color",
|
| 127 |
+
"pk": 8,
|
| 128 |
+
"fields": {
|
| 129 |
+
"name": "Pastel Pink",
|
| 130 |
+
"hex_code": "#FFD1DC",
|
| 131 |
+
"category": "light"
|
| 132 |
+
}
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"model": "fitting_system.product",
|
| 136 |
+
"pk": 1,
|
| 137 |
+
"fields": {
|
| 138 |
+
"name": "Classic Cotton Shirt",
|
| 139 |
+
"category": "shirt",
|
| 140 |
+
"fit_type": "regular",
|
| 141 |
+
"gender": "men",
|
| 142 |
+
"price": "49.99",
|
| 143 |
+
"description": "A timeless classic cotton shirt perfect for any occasion. Made from 100% premium cotton for maximum comfort and breathability.",
|
| 144 |
+
"image": "",
|
| 145 |
+
"created_at": "2026-02-06T08:10:47.701Z"
|
| 146 |
+
}
|
| 147 |
+
},
|
| 148 |
+
{
|
| 149 |
+
"model": "fitting_system.product",
|
| 150 |
+
"pk": 2,
|
| 151 |
+
"fields": {
|
| 152 |
+
"name": "Casual Denim Jeans",
|
| 153 |
+
"category": "pants",
|
| 154 |
+
"fit_type": "regular",
|
| 155 |
+
"gender": "men",
|
| 156 |
+
"price": "79.99",
|
| 157 |
+
"description": "Comfortable denim jeans with a classic fit. Durable and stylish for everyday wear with premium denim fabric.",
|
| 158 |
+
"image": "",
|
| 159 |
+
"created_at": "2026-02-06T08:10:47.718Z"
|
| 160 |
+
}
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"model": "fitting_system.product",
|
| 164 |
+
"pk": 3,
|
| 165 |
+
"fields": {
|
| 166 |
+
"name": "Leather Jacket",
|
| 167 |
+
"category": "jacket",
|
| 168 |
+
"fit_type": "regular",
|
| 169 |
+
"gender": "men",
|
| 170 |
+
"price": "199.99",
|
| 171 |
+
"description": "Premium leather jacket with a classic design. Timeless piece that never goes out of style, crafted from genuine leather.",
|
| 172 |
+
"image": "",
|
| 173 |
+
"created_at": "2026-02-06T08:10:47.736Z"
|
| 174 |
+
}
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"model": "fitting_system.product",
|
| 178 |
+
"pk": 4,
|
| 179 |
+
"fields": {
|
| 180 |
+
"name": "Elegant Blouse",
|
| 181 |
+
"category": "shirt",
|
| 182 |
+
"fit_type": "regular",
|
| 183 |
+
"gender": "women",
|
| 184 |
+
"price": "54.99",
|
| 185 |
+
"description": "Sophisticated blouse with delicate details. Perfect for both office and evening wear with premium silk-like fabric.",
|
| 186 |
+
"image": "",
|
| 187 |
+
"created_at": "2026-02-06T08:10:47.755Z"
|
| 188 |
+
}
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"model": "fitting_system.product",
|
| 192 |
+
"pk": 5,
|
| 193 |
+
"fields": {
|
| 194 |
+
"name": "Summer Dress",
|
| 195 |
+
"category": "dress",
|
| 196 |
+
"fit_type": "regular",
|
| 197 |
+
"gender": "women",
|
| 198 |
+
"price": "89.99",
|
| 199 |
+
"description": "Light and breezy summer dress perfect for warm weather. Comfortable and stylish with a flattering silhouette.",
|
| 200 |
+
"image": "",
|
| 201 |
+
"created_at": "2026-02-06T08:10:47.770Z"
|
| 202 |
+
}
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
"model": "fitting_system.product",
|
| 206 |
+
"pk": 6,
|
| 207 |
+
"fields": {
|
| 208 |
+
"name": "High-Waist Trousers",
|
| 209 |
+
"category": "pants",
|
| 210 |
+
"fit_type": "regular",
|
| 211 |
+
"gender": "women",
|
| 212 |
+
"price": "74.99",
|
| 213 |
+
"description": "Flattering high-waist trousers with a comfortable fit. Versatile for any occasion from office to casual outings.",
|
| 214 |
+
"image": "",
|
| 215 |
+
"created_at": "2026-02-06T08:10:47.788Z"
|
| 216 |
+
}
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"model": "fitting_system.product",
|
| 220 |
+
"pk": 7,
|
| 221 |
+
"fields": {
|
| 222 |
+
"name": "Modern T shirt",
|
| 223 |
+
"category": "shirt",
|
| 224 |
+
"fit_type": "slim",
|
| 225 |
+
"gender": "men",
|
| 226 |
+
"price": "10.00",
|
| 227 |
+
"description": "The best shirt ever",
|
| 228 |
+
"image": "",
|
| 229 |
+
"created_at": "2026-02-24T01:10:42.582Z"
|
| 230 |
+
}
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"model": "fitting_system.product",
|
| 234 |
+
"pk": 8,
|
| 235 |
+
"fields": {
|
| 236 |
+
"name": "grenn shirt",
|
| 237 |
+
"category": "shirt",
|
| 238 |
+
"fit_type": "slim",
|
| 239 |
+
"gender": "men",
|
| 240 |
+
"price": "10.00",
|
| 241 |
+
"description": "the best ever",
|
| 242 |
+
"image": "",
|
| 243 |
+
"created_at": "2026-02-24T01:18:39.741Z"
|
| 244 |
+
}
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
"model": "fitting_system.product",
|
| 248 |
+
"pk": 9,
|
| 249 |
+
"fields": {
|
| 250 |
+
"name": "Green Tshirt",
|
| 251 |
+
"category": "shirt",
|
| 252 |
+
"fit_type": "regular",
|
| 253 |
+
"gender": "men",
|
| 254 |
+
"price": "20.00",
|
| 255 |
+
"description": "The best T shirt ever",
|
| 256 |
+
"image": "products/Gemini_Generated_Image_u7btrfu7btrfu7bt.png",
|
| 257 |
+
"created_at": "2026-02-24T01:27:49.388Z"
|
| 258 |
+
}
|
| 259 |
+
},
|
| 260 |
+
{
|
| 261 |
+
"model": "fitting_system.product",
|
| 262 |
+
"pk": 10,
|
| 263 |
+
"fields": {
|
| 264 |
+
"name": "Green Tshirt(2)",
|
| 265 |
+
"category": "shirt",
|
| 266 |
+
"fit_type": "oversize",
|
| 267 |
+
"gender": "men",
|
| 268 |
+
"price": "90.00",
|
| 269 |
+
"description": "oooooo",
|
| 270 |
+
"image": "products/Gemini_Generated_Image_u7btrfu7btrfu7bt_DmLFSAa.png",
|
| 271 |
+
"created_at": "2026-02-24T21:18:53.644Z"
|
| 272 |
+
}
|
| 273 |
+
},
|
| 274 |
+
{
|
| 275 |
+
"model": "fitting_system.product",
|
| 276 |
+
"pk": 11,
|
| 277 |
+
"fields": {
|
| 278 |
+
"name": "white T shirt",
|
| 279 |
+
"category": "shirt",
|
| 280 |
+
"fit_type": "slim",
|
| 281 |
+
"gender": "men",
|
| 282 |
+
"price": "90.00",
|
| 283 |
+
"description": "nothhing",
|
| 284 |
+
"image": "products/Gemini_Generated_Image_u7btrfu7btrfu7bt_h0fZdOE.png",
|
| 285 |
+
"created_at": "2026-02-25T13:09:02.864Z"
|
| 286 |
+
}
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
"model": "fitting_system.productvariant",
|
| 290 |
+
"pk": 1,
|
| 291 |
+
"fields": {
|
| 292 |
+
"product": 1,
|
| 293 |
+
"size": 1,
|
| 294 |
+
"color": 5,
|
| 295 |
+
"sku": "1-S-5-1",
|
| 296 |
+
"created_at": "2026-02-06T08:10:47.817Z"
|
| 297 |
+
}
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"model": "fitting_system.productvariant",
|
| 301 |
+
"pk": 2,
|
| 302 |
+
"fields": {
|
| 303 |
+
"product": 1,
|
| 304 |
+
"size": 1,
|
| 305 |
+
"color": 1,
|
| 306 |
+
"sku": "1-S-1-2",
|
| 307 |
+
"created_at": "2026-02-06T08:10:47.849Z"
|
| 308 |
+
}
|
| 309 |
+
},
|
| 310 |
+
{
|
| 311 |
+
"model": "fitting_system.productvariant",
|
| 312 |
+
"pk": 3,
|
| 313 |
+
"fields": {
|
| 314 |
+
"product": 1,
|
| 315 |
+
"size": 1,
|
| 316 |
+
"color": 6,
|
| 317 |
+
"sku": "1-S-6-3",
|
| 318 |
+
"created_at": "2026-02-06T08:10:47.886Z"
|
| 319 |
+
}
|
| 320 |
+
},
|
| 321 |
+
{
|
| 322 |
+
"model": "fitting_system.productvariant",
|
| 323 |
+
"pk": 4,
|
| 324 |
+
"fields": {
|
| 325 |
+
"product": 1,
|
| 326 |
+
"size": 2,
|
| 327 |
+
"color": 5,
|
| 328 |
+
"sku": "1-M-5-4",
|
| 329 |
+
"created_at": "2026-02-06T08:10:47.925Z"
|
| 330 |
+
}
|
| 331 |
+
},
|
| 332 |
+
{
|
| 333 |
+
"model": "fitting_system.productvariant",
|
| 334 |
+
"pk": 5,
|
| 335 |
+
"fields": {
|
| 336 |
+
"product": 1,
|
| 337 |
+
"size": 2,
|
| 338 |
+
"color": 1,
|
| 339 |
+
"sku": "1-M-1-5",
|
| 340 |
+
"created_at": "2026-02-06T08:10:47.971Z"
|
| 341 |
+
}
|
| 342 |
+
},
|
| 343 |
+
{
|
| 344 |
+
"model": "fitting_system.productvariant",
|
| 345 |
+
"pk": 6,
|
| 346 |
+
"fields": {
|
| 347 |
+
"product": 1,
|
| 348 |
+
"size": 2,
|
| 349 |
+
"color": 6,
|
| 350 |
+
"sku": "1-M-6-6",
|
| 351 |
+
"created_at": "2026-02-06T08:10:48.013Z"
|
| 352 |
+
}
|
| 353 |
+
},
|
| 354 |
+
{
|
| 355 |
+
"model": "fitting_system.productvariant",
|
| 356 |
+
"pk": 7,
|
| 357 |
+
"fields": {
|
| 358 |
+
"product": 1,
|
| 359 |
+
"size": 3,
|
| 360 |
+
"color": 5,
|
| 361 |
+
"sku": "1-L-5-7",
|
| 362 |
+
"created_at": "2026-02-06T08:10:48.092Z"
|
| 363 |
+
}
|
| 364 |
+
},
|
| 365 |
+
{
|
| 366 |
+
"model": "fitting_system.productvariant",
|
| 367 |
+
"pk": 8,
|
| 368 |
+
"fields": {
|
| 369 |
+
"product": 1,
|
| 370 |
+
"size": 3,
|
| 371 |
+
"color": 1,
|
| 372 |
+
"sku": "1-L-1-8",
|
| 373 |
+
"created_at": "2026-02-06T08:10:48.136Z"
|
| 374 |
+
}
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"model": "fitting_system.productvariant",
|
| 378 |
+
"pk": 9,
|
| 379 |
+
"fields": {
|
| 380 |
+
"product": 1,
|
| 381 |
+
"size": 3,
|
| 382 |
+
"color": 6,
|
| 383 |
+
"sku": "1-L-6-9",
|
| 384 |
+
"created_at": "2026-02-06T08:10:48.168Z"
|
| 385 |
+
}
|
| 386 |
+
},
|
| 387 |
+
{
|
| 388 |
+
"model": "fitting_system.productvariant",
|
| 389 |
+
"pk": 10,
|
| 390 |
+
"fields": {
|
| 391 |
+
"product": 2,
|
| 392 |
+
"size": 1,
|
| 393 |
+
"color": 5,
|
| 394 |
+
"sku": "2-S-5-1",
|
| 395 |
+
"created_at": "2026-02-06T08:10:48.202Z"
|
| 396 |
+
}
|
| 397 |
+
},
|
| 398 |
+
{
|
| 399 |
+
"model": "fitting_system.productvariant",
|
| 400 |
+
"pk": 11,
|
| 401 |
+
"fields": {
|
| 402 |
+
"product": 2,
|
| 403 |
+
"size": 1,
|
| 404 |
+
"color": 1,
|
| 405 |
+
"sku": "2-S-1-2",
|
| 406 |
+
"created_at": "2026-02-06T08:10:48.238Z"
|
| 407 |
+
}
|
| 408 |
+
},
|
| 409 |
+
{
|
| 410 |
+
"model": "fitting_system.productvariant",
|
| 411 |
+
"pk": 12,
|
| 412 |
+
"fields": {
|
| 413 |
+
"product": 2,
|
| 414 |
+
"size": 1,
|
| 415 |
+
"color": 6,
|
| 416 |
+
"sku": "2-S-6-3",
|
| 417 |
+
"created_at": "2026-02-06T08:10:48.268Z"
|
| 418 |
+
}
|
| 419 |
+
},
|
| 420 |
+
{
|
| 421 |
+
"model": "fitting_system.productvariant",
|
| 422 |
+
"pk": 13,
|
| 423 |
+
"fields": {
|
| 424 |
+
"product": 2,
|
| 425 |
+
"size": 2,
|
| 426 |
+
"color": 5,
|
| 427 |
+
"sku": "2-M-5-4",
|
| 428 |
+
"created_at": "2026-02-06T08:10:48.299Z"
|
| 429 |
+
}
|
| 430 |
+
},
|
| 431 |
+
{
|
| 432 |
+
"model": "fitting_system.productvariant",
|
| 433 |
+
"pk": 14,
|
| 434 |
+
"fields": {
|
| 435 |
+
"product": 2,
|
| 436 |
+
"size": 2,
|
| 437 |
+
"color": 1,
|
| 438 |
+
"sku": "2-M-1-5",
|
| 439 |
+
"created_at": "2026-02-06T08:10:48.329Z"
|
| 440 |
+
}
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"model": "fitting_system.productvariant",
|
| 444 |
+
"pk": 15,
|
| 445 |
+
"fields": {
|
| 446 |
+
"product": 2,
|
| 447 |
+
"size": 2,
|
| 448 |
+
"color": 6,
|
| 449 |
+
"sku": "2-M-6-6",
|
| 450 |
+
"created_at": "2026-02-06T08:10:48.374Z"
|
| 451 |
+
}
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"model": "fitting_system.productvariant",
|
| 455 |
+
"pk": 16,
|
| 456 |
+
"fields": {
|
| 457 |
+
"product": 2,
|
| 458 |
+
"size": 3,
|
| 459 |
+
"color": 5,
|
| 460 |
+
"sku": "2-L-5-7",
|
| 461 |
+
"created_at": "2026-02-06T08:10:48.405Z"
|
| 462 |
+
}
|
| 463 |
+
},
|
| 464 |
+
{
|
| 465 |
+
"model": "fitting_system.productvariant",
|
| 466 |
+
"pk": 17,
|
| 467 |
+
"fields": {
|
| 468 |
+
"product": 2,
|
| 469 |
+
"size": 3,
|
| 470 |
+
"color": 1,
|
| 471 |
+
"sku": "2-L-1-8",
|
| 472 |
+
"created_at": "2026-02-06T08:10:48.436Z"
|
| 473 |
+
}
|
| 474 |
+
},
|
| 475 |
+
{
|
| 476 |
+
"model": "fitting_system.productvariant",
|
| 477 |
+
"pk": 18,
|
| 478 |
+
"fields": {
|
| 479 |
+
"product": 2,
|
| 480 |
+
"size": 3,
|
| 481 |
+
"color": 6,
|
| 482 |
+
"sku": "2-L-6-9",
|
| 483 |
+
"created_at": "2026-02-06T08:10:48.471Z"
|
| 484 |
+
}
|
| 485 |
+
},
|
| 486 |
+
{
|
| 487 |
+
"model": "fitting_system.productvariant",
|
| 488 |
+
"pk": 19,
|
| 489 |
+
"fields": {
|
| 490 |
+
"product": 3,
|
| 491 |
+
"size": 1,
|
| 492 |
+
"color": 5,
|
| 493 |
+
"sku": "3-S-5-1",
|
| 494 |
+
"created_at": "2026-02-06T08:10:48.504Z"
|
| 495 |
+
}
|
| 496 |
+
},
|
| 497 |
+
{
|
| 498 |
+
"model": "fitting_system.productvariant",
|
| 499 |
+
"pk": 20,
|
| 500 |
+
"fields": {
|
| 501 |
+
"product": 3,
|
| 502 |
+
"size": 1,
|
| 503 |
+
"color": 1,
|
| 504 |
+
"sku": "3-S-1-2",
|
| 505 |
+
"created_at": "2026-02-06T08:10:48.536Z"
|
| 506 |
+
}
|
| 507 |
+
},
|
| 508 |
+
{
|
| 509 |
+
"model": "fitting_system.productvariant",
|
| 510 |
+
"pk": 21,
|
| 511 |
+
"fields": {
|
| 512 |
+
"product": 3,
|
| 513 |
+
"size": 1,
|
| 514 |
+
"color": 6,
|
| 515 |
+
"sku": "3-S-6-3",
|
| 516 |
+
"created_at": "2026-02-06T08:10:48.568Z"
|
| 517 |
+
}
|
| 518 |
+
},
|
| 519 |
+
{
|
| 520 |
+
"model": "fitting_system.productvariant",
|
| 521 |
+
"pk": 22,
|
| 522 |
+
"fields": {
|
| 523 |
+
"product": 3,
|
| 524 |
+
"size": 2,
|
| 525 |
+
"color": 5,
|
| 526 |
+
"sku": "3-M-5-4",
|
| 527 |
+
"created_at": "2026-02-06T08:10:48.598Z"
|
| 528 |
+
}
|
| 529 |
+
},
|
| 530 |
+
{
|
| 531 |
+
"model": "fitting_system.productvariant",
|
| 532 |
+
"pk": 23,
|
| 533 |
+
"fields": {
|
| 534 |
+
"product": 3,
|
| 535 |
+
"size": 2,
|
| 536 |
+
"color": 1,
|
| 537 |
+
"sku": "3-M-1-5",
|
| 538 |
+
"created_at": "2026-02-06T08:10:48.638Z"
|
| 539 |
+
}
|
| 540 |
+
},
|
| 541 |
+
{
|
| 542 |
+
"model": "fitting_system.productvariant",
|
| 543 |
+
"pk": 24,
|
| 544 |
+
"fields": {
|
| 545 |
+
"product": 3,
|
| 546 |
+
"size": 2,
|
| 547 |
+
"color": 6,
|
| 548 |
+
"sku": "3-M-6-6",
|
| 549 |
+
"created_at": "2026-02-06T08:10:48.669Z"
|
| 550 |
+
}
|
| 551 |
+
},
|
| 552 |
+
{
|
| 553 |
+
"model": "fitting_system.productvariant",
|
| 554 |
+
"pk": 25,
|
| 555 |
+
"fields": {
|
| 556 |
+
"product": 3,
|
| 557 |
+
"size": 3,
|
| 558 |
+
"color": 5,
|
| 559 |
+
"sku": "3-L-5-7",
|
| 560 |
+
"created_at": "2026-02-06T08:10:48.700Z"
|
| 561 |
+
}
|
| 562 |
+
},
|
| 563 |
+
{
|
| 564 |
+
"model": "fitting_system.productvariant",
|
| 565 |
+
"pk": 26,
|
| 566 |
+
"fields": {
|
| 567 |
+
"product": 3,
|
| 568 |
+
"size": 3,
|
| 569 |
+
"color": 1,
|
| 570 |
+
"sku": "3-L-1-8",
|
| 571 |
+
"created_at": "2026-02-06T08:10:48.730Z"
|
| 572 |
+
}
|
| 573 |
+
},
|
| 574 |
+
{
|
| 575 |
+
"model": "fitting_system.productvariant",
|
| 576 |
+
"pk": 27,
|
| 577 |
+
"fields": {
|
| 578 |
+
"product": 3,
|
| 579 |
+
"size": 3,
|
| 580 |
+
"color": 6,
|
| 581 |
+
"sku": "3-L-6-9",
|
| 582 |
+
"created_at": "2026-02-06T08:10:48.760Z"
|
| 583 |
+
}
|
| 584 |
+
},
|
| 585 |
+
{
|
| 586 |
+
"model": "fitting_system.productvariant",
|
| 587 |
+
"pk": 28,
|
| 588 |
+
"fields": {
|
| 589 |
+
"product": 4,
|
| 590 |
+
"size": 1,
|
| 591 |
+
"color": 5,
|
| 592 |
+
"sku": "4-S-5-1",
|
| 593 |
+
"created_at": "2026-02-06T08:10:48.791Z"
|
| 594 |
+
}
|
| 595 |
+
},
|
| 596 |
+
{
|
| 597 |
+
"model": "fitting_system.productvariant",
|
| 598 |
+
"pk": 29,
|
| 599 |
+
"fields": {
|
| 600 |
+
"product": 4,
|
| 601 |
+
"size": 1,
|
| 602 |
+
"color": 1,
|
| 603 |
+
"sku": "4-S-1-2",
|
| 604 |
+
"created_at": "2026-02-06T08:10:48.820Z"
|
| 605 |
+
}
|
| 606 |
+
},
|
| 607 |
+
{
|
| 608 |
+
"model": "fitting_system.productvariant",
|
| 609 |
+
"pk": 30,
|
| 610 |
+
"fields": {
|
| 611 |
+
"product": 4,
|
| 612 |
+
"size": 1,
|
| 613 |
+
"color": 6,
|
| 614 |
+
"sku": "4-S-6-3",
|
| 615 |
+
"created_at": "2026-02-06T08:10:48.849Z"
|
| 616 |
+
}
|
| 617 |
+
},
|
| 618 |
+
{
|
| 619 |
+
"model": "fitting_system.productvariant",
|
| 620 |
+
"pk": 31,
|
| 621 |
+
"fields": {
|
| 622 |
+
"product": 4,
|
| 623 |
+
"size": 2,
|
| 624 |
+
"color": 5,
|
| 625 |
+
"sku": "4-M-5-4",
|
| 626 |
+
"created_at": "2026-02-06T08:10:48.908Z"
|
| 627 |
+
}
|
| 628 |
+
},
|
| 629 |
+
{
|
| 630 |
+
"model": "fitting_system.productvariant",
|
| 631 |
+
"pk": 32,
|
| 632 |
+
"fields": {
|
| 633 |
+
"product": 4,
|
| 634 |
+
"size": 2,
|
| 635 |
+
"color": 1,
|
| 636 |
+
"sku": "4-M-1-5",
|
| 637 |
+
"created_at": "2026-02-06T08:10:48.946Z"
|
| 638 |
+
}
|
| 639 |
+
},
|
| 640 |
+
{
|
| 641 |
+
"model": "fitting_system.productvariant",
|
| 642 |
+
"pk": 33,
|
| 643 |
+
"fields": {
|
| 644 |
+
"product": 4,
|
| 645 |
+
"size": 2,
|
| 646 |
+
"color": 6,
|
| 647 |
+
"sku": "4-M-6-6",
|
| 648 |
+
"created_at": "2026-02-06T08:10:48.981Z"
|
| 649 |
+
}
|
| 650 |
+
},
|
| 651 |
+
{
|
| 652 |
+
"model": "fitting_system.productvariant",
|
| 653 |
+
"pk": 34,
|
| 654 |
+
"fields": {
|
| 655 |
+
"product": 4,
|
| 656 |
+
"size": 3,
|
| 657 |
+
"color": 5,
|
| 658 |
+
"sku": "4-L-5-7",
|
| 659 |
+
"created_at": "2026-02-06T08:10:49.011Z"
|
| 660 |
+
}
|
| 661 |
+
},
|
| 662 |
+
{
|
| 663 |
+
"model": "fitting_system.productvariant",
|
| 664 |
+
"pk": 35,
|
| 665 |
+
"fields": {
|
| 666 |
+
"product": 4,
|
| 667 |
+
"size": 3,
|
| 668 |
+
"color": 1,
|
| 669 |
+
"sku": "4-L-1-8",
|
| 670 |
+
"created_at": "2026-02-06T08:10:49.052Z"
|
| 671 |
+
}
|
| 672 |
+
},
|
| 673 |
+
{
|
| 674 |
+
"model": "fitting_system.productvariant",
|
| 675 |
+
"pk": 36,
|
| 676 |
+
"fields": {
|
| 677 |
+
"product": 4,
|
| 678 |
+
"size": 3,
|
| 679 |
+
"color": 6,
|
| 680 |
+
"sku": "4-L-6-9",
|
| 681 |
+
"created_at": "2026-02-06T08:10:49.092Z"
|
| 682 |
+
}
|
| 683 |
+
},
|
| 684 |
+
{
|
| 685 |
+
"model": "fitting_system.productvariant",
|
| 686 |
+
"pk": 37,
|
| 687 |
+
"fields": {
|
| 688 |
+
"product": 5,
|
| 689 |
+
"size": 1,
|
| 690 |
+
"color": 5,
|
| 691 |
+
"sku": "5-S-5-1",
|
| 692 |
+
"created_at": "2026-02-06T08:10:49.128Z"
|
| 693 |
+
}
|
| 694 |
+
},
|
| 695 |
+
{
|
| 696 |
+
"model": "fitting_system.productvariant",
|
| 697 |
+
"pk": 38,
|
| 698 |
+
"fields": {
|
| 699 |
+
"product": 5,
|
| 700 |
+
"size": 1,
|
| 701 |
+
"color": 1,
|
| 702 |
+
"sku": "5-S-1-2",
|
| 703 |
+
"created_at": "2026-02-06T08:10:49.168Z"
|
| 704 |
+
}
|
| 705 |
+
},
|
| 706 |
+
{
|
| 707 |
+
"model": "fitting_system.productvariant",
|
| 708 |
+
"pk": 39,
|
| 709 |
+
"fields": {
|
| 710 |
+
"product": 5,
|
| 711 |
+
"size": 1,
|
| 712 |
+
"color": 6,
|
| 713 |
+
"sku": "5-S-6-3",
|
| 714 |
+
"created_at": "2026-02-06T08:10:49.205Z"
|
| 715 |
+
}
|
| 716 |
+
},
|
| 717 |
+
{
|
| 718 |
+
"model": "fitting_system.productvariant",
|
| 719 |
+
"pk": 40,
|
| 720 |
+
"fields": {
|
| 721 |
+
"product": 5,
|
| 722 |
+
"size": 2,
|
| 723 |
+
"color": 5,
|
| 724 |
+
"sku": "5-M-5-4",
|
| 725 |
+
"created_at": "2026-02-06T08:10:49.237Z"
|
| 726 |
+
}
|
| 727 |
+
},
|
| 728 |
+
{
|
| 729 |
+
"model": "fitting_system.productvariant",
|
| 730 |
+
"pk": 41,
|
| 731 |
+
"fields": {
|
| 732 |
+
"product": 5,
|
| 733 |
+
"size": 2,
|
| 734 |
+
"color": 1,
|
| 735 |
+
"sku": "5-M-1-5",
|
| 736 |
+
"created_at": "2026-02-06T08:10:49.297Z"
|
| 737 |
+
}
|
| 738 |
+
},
|
| 739 |
+
{
|
| 740 |
+
"model": "fitting_system.productvariant",
|
| 741 |
+
"pk": 42,
|
| 742 |
+
"fields": {
|
| 743 |
+
"product": 5,
|
| 744 |
+
"size": 2,
|
| 745 |
+
"color": 6,
|
| 746 |
+
"sku": "5-M-6-6",
|
| 747 |
+
"created_at": "2026-02-06T08:10:49.332Z"
|
| 748 |
+
}
|
| 749 |
+
},
|
| 750 |
+
{
|
| 751 |
+
"model": "fitting_system.productvariant",
|
| 752 |
+
"pk": 43,
|
| 753 |
+
"fields": {
|
| 754 |
+
"product": 5,
|
| 755 |
+
"size": 3,
|
| 756 |
+
"color": 5,
|
| 757 |
+
"sku": "5-L-5-7",
|
| 758 |
+
"created_at": "2026-02-06T08:10:49.373Z"
|
| 759 |
+
}
|
| 760 |
+
},
|
| 761 |
+
{
|
| 762 |
+
"model": "fitting_system.productvariant",
|
| 763 |
+
"pk": 44,
|
| 764 |
+
"fields": {
|
| 765 |
+
"product": 5,
|
| 766 |
+
"size": 3,
|
| 767 |
+
"color": 1,
|
| 768 |
+
"sku": "5-L-1-8",
|
| 769 |
+
"created_at": "2026-02-06T08:10:49.406Z"
|
| 770 |
+
}
|
| 771 |
+
},
|
| 772 |
+
{
|
| 773 |
+
"model": "fitting_system.productvariant",
|
| 774 |
+
"pk": 45,
|
| 775 |
+
"fields": {
|
| 776 |
+
"product": 5,
|
| 777 |
+
"size": 3,
|
| 778 |
+
"color": 6,
|
| 779 |
+
"sku": "5-L-6-9",
|
| 780 |
+
"created_at": "2026-02-06T08:10:49.452Z"
|
| 781 |
+
}
|
| 782 |
+
},
|
| 783 |
+
{
|
| 784 |
+
"model": "fitting_system.productvariant",
|
| 785 |
+
"pk": 46,
|
| 786 |
+
"fields": {
|
| 787 |
+
"product": 6,
|
| 788 |
+
"size": 1,
|
| 789 |
+
"color": 5,
|
| 790 |
+
"sku": "6-S-5-1",
|
| 791 |
+
"created_at": "2026-02-06T08:10:49.497Z"
|
| 792 |
+
}
|
| 793 |
+
},
|
| 794 |
+
{
|
| 795 |
+
"model": "fitting_system.productvariant",
|
| 796 |
+
"pk": 47,
|
| 797 |
+
"fields": {
|
| 798 |
+
"product": 6,
|
| 799 |
+
"size": 1,
|
| 800 |
+
"color": 1,
|
| 801 |
+
"sku": "6-S-1-2",
|
| 802 |
+
"created_at": "2026-02-06T08:10:49.538Z"
|
| 803 |
+
}
|
| 804 |
+
},
|
| 805 |
+
{
|
| 806 |
+
"model": "fitting_system.productvariant",
|
| 807 |
+
"pk": 48,
|
| 808 |
+
"fields": {
|
| 809 |
+
"product": 6,
|
| 810 |
+
"size": 1,
|
| 811 |
+
"color": 6,
|
| 812 |
+
"sku": "6-S-6-3",
|
| 813 |
+
"created_at": "2026-02-06T08:10:49.582Z"
|
| 814 |
+
}
|
| 815 |
+
},
|
| 816 |
+
{
|
| 817 |
+
"model": "fitting_system.productvariant",
|
| 818 |
+
"pk": 49,
|
| 819 |
+
"fields": {
|
| 820 |
+
"product": 6,
|
| 821 |
+
"size": 2,
|
| 822 |
+
"color": 5,
|
| 823 |
+
"sku": "6-M-5-4",
|
| 824 |
+
"created_at": "2026-02-06T08:10:49.629Z"
|
| 825 |
+
}
|
| 826 |
+
},
|
| 827 |
+
{
|
| 828 |
+
"model": "fitting_system.productvariant",
|
| 829 |
+
"pk": 50,
|
| 830 |
+
"fields": {
|
| 831 |
+
"product": 6,
|
| 832 |
+
"size": 2,
|
| 833 |
+
"color": 1,
|
| 834 |
+
"sku": "6-M-1-5",
|
| 835 |
+
"created_at": "2026-02-06T08:10:49.672Z"
|
| 836 |
+
}
|
| 837 |
+
},
|
| 838 |
+
{
|
| 839 |
+
"model": "fitting_system.productvariant",
|
| 840 |
+
"pk": 51,
|
| 841 |
+
"fields": {
|
| 842 |
+
"product": 6,
|
| 843 |
+
"size": 2,
|
| 844 |
+
"color": 6,
|
| 845 |
+
"sku": "6-M-6-6",
|
| 846 |
+
"created_at": "2026-02-06T08:10:49.717Z"
|
| 847 |
+
}
|
| 848 |
+
},
|
| 849 |
+
{
|
| 850 |
+
"model": "fitting_system.productvariant",
|
| 851 |
+
"pk": 52,
|
| 852 |
+
"fields": {
|
| 853 |
+
"product": 6,
|
| 854 |
+
"size": 3,
|
| 855 |
+
"color": 5,
|
| 856 |
+
"sku": "6-L-5-7",
|
| 857 |
+
"created_at": "2026-02-06T08:10:49.758Z"
|
| 858 |
+
}
|
| 859 |
+
},
|
| 860 |
+
{
|
| 861 |
+
"model": "fitting_system.productvariant",
|
| 862 |
+
"pk": 53,
|
| 863 |
+
"fields": {
|
| 864 |
+
"product": 6,
|
| 865 |
+
"size": 3,
|
| 866 |
+
"color": 1,
|
| 867 |
+
"sku": "6-L-1-8",
|
| 868 |
+
"created_at": "2026-02-06T08:10:49.793Z"
|
| 869 |
+
}
|
| 870 |
+
},
|
| 871 |
+
{
|
| 872 |
+
"model": "fitting_system.productvariant",
|
| 873 |
+
"pk": 54,
|
| 874 |
+
"fields": {
|
| 875 |
+
"product": 6,
|
| 876 |
+
"size": 3,
|
| 877 |
+
"color": 6,
|
| 878 |
+
"sku": "6-L-6-9",
|
| 879 |
+
"created_at": "2026-02-06T08:10:49.831Z"
|
| 880 |
+
}
|
| 881 |
+
},
|
| 882 |
+
{
|
| 883 |
+
"model": "fitting_system.inventory",
|
| 884 |
+
"pk": 1,
|
| 885 |
+
"fields": {
|
| 886 |
+
"product_variant": 1,
|
| 887 |
+
"quantity": 0,
|
| 888 |
+
"low_stock_threshold": 5,
|
| 889 |
+
"last_updated": "2026-02-06T08:10:47.833Z"
|
| 890 |
+
}
|
| 891 |
+
},
|
| 892 |
+
{
|
| 893 |
+
"model": "fitting_system.inventory",
|
| 894 |
+
"pk": 2,
|
| 895 |
+
"fields": {
|
| 896 |
+
"product_variant": 2,
|
| 897 |
+
"quantity": 0,
|
| 898 |
+
"low_stock_threshold": 5,
|
| 899 |
+
"last_updated": "2026-02-06T08:10:47.867Z"
|
| 900 |
+
}
|
| 901 |
+
},
|
| 902 |
+
{
|
| 903 |
+
"model": "fitting_system.inventory",
|
| 904 |
+
"pk": 3,
|
| 905 |
+
"fields": {
|
| 906 |
+
"product_variant": 3,
|
| 907 |
+
"quantity": 0,
|
| 908 |
+
"low_stock_threshold": 5,
|
| 909 |
+
"last_updated": "2026-02-06T08:10:47.909Z"
|
| 910 |
+
}
|
| 911 |
+
},
|
| 912 |
+
{
|
| 913 |
+
"model": "fitting_system.inventory",
|
| 914 |
+
"pk": 4,
|
| 915 |
+
"fields": {
|
| 916 |
+
"product_variant": 4,
|
| 917 |
+
"quantity": 0,
|
| 918 |
+
"low_stock_threshold": 5,
|
| 919 |
+
"last_updated": "2026-02-06T08:10:47.956Z"
|
| 920 |
+
}
|
| 921 |
+
},
|
| 922 |
+
{
|
| 923 |
+
"model": "fitting_system.inventory",
|
| 924 |
+
"pk": 5,
|
| 925 |
+
"fields": {
|
| 926 |
+
"product_variant": 5,
|
| 927 |
+
"quantity": 0,
|
| 928 |
+
"low_stock_threshold": 5,
|
| 929 |
+
"last_updated": "2026-02-06T08:10:47.993Z"
|
| 930 |
+
}
|
| 931 |
+
},
|
| 932 |
+
{
|
| 933 |
+
"model": "fitting_system.inventory",
|
| 934 |
+
"pk": 6,
|
| 935 |
+
"fields": {
|
| 936 |
+
"product_variant": 6,
|
| 937 |
+
"quantity": 0,
|
| 938 |
+
"low_stock_threshold": 5,
|
| 939 |
+
"last_updated": "2026-02-06T08:10:48.061Z"
|
| 940 |
+
}
|
| 941 |
+
},
|
| 942 |
+
{
|
| 943 |
+
"model": "fitting_system.inventory",
|
| 944 |
+
"pk": 7,
|
| 945 |
+
"fields": {
|
| 946 |
+
"product_variant": 7,
|
| 947 |
+
"quantity": 0,
|
| 948 |
+
"low_stock_threshold": 5,
|
| 949 |
+
"last_updated": "2026-02-06T08:10:48.112Z"
|
| 950 |
+
}
|
| 951 |
+
},
|
| 952 |
+
{
|
| 953 |
+
"model": "fitting_system.inventory",
|
| 954 |
+
"pk": 8,
|
| 955 |
+
"fields": {
|
| 956 |
+
"product_variant": 8,
|
| 957 |
+
"quantity": 0,
|
| 958 |
+
"low_stock_threshold": 5,
|
| 959 |
+
"last_updated": "2026-02-06T08:10:48.152Z"
|
| 960 |
+
}
|
| 961 |
+
},
|
| 962 |
+
{
|
| 963 |
+
"model": "fitting_system.inventory",
|
| 964 |
+
"pk": 9,
|
| 965 |
+
"fields": {
|
| 966 |
+
"product_variant": 9,
|
| 967 |
+
"quantity": 0,
|
| 968 |
+
"low_stock_threshold": 5,
|
| 969 |
+
"last_updated": "2026-02-06T08:10:48.186Z"
|
| 970 |
+
}
|
| 971 |
+
},
|
| 972 |
+
{
|
| 973 |
+
"model": "fitting_system.inventory",
|
| 974 |
+
"pk": 10,
|
| 975 |
+
"fields": {
|
| 976 |
+
"product_variant": 10,
|
| 977 |
+
"quantity": 0,
|
| 978 |
+
"low_stock_threshold": 5,
|
| 979 |
+
"last_updated": "2026-02-06T08:10:48.222Z"
|
| 980 |
+
}
|
| 981 |
+
},
|
| 982 |
+
{
|
| 983 |
+
"model": "fitting_system.inventory",
|
| 984 |
+
"pk": 11,
|
| 985 |
+
"fields": {
|
| 986 |
+
"product_variant": 11,
|
| 987 |
+
"quantity": 4,
|
| 988 |
+
"low_stock_threshold": 5,
|
| 989 |
+
"last_updated": "2026-02-06T08:10:48.254Z"
|
| 990 |
+
}
|
| 991 |
+
},
|
| 992 |
+
{
|
| 993 |
+
"model": "fitting_system.inventory",
|
| 994 |
+
"pk": 12,
|
| 995 |
+
"fields": {
|
| 996 |
+
"product_variant": 12,
|
| 997 |
+
"quantity": 4,
|
| 998 |
+
"low_stock_threshold": 5,
|
| 999 |
+
"last_updated": "2026-02-06T08:10:48.283Z"
|
| 1000 |
+
}
|
| 1001 |
+
},
|
| 1002 |
+
{
|
| 1003 |
+
"model": "fitting_system.inventory",
|
| 1004 |
+
"pk": 13,
|
| 1005 |
+
"fields": {
|
| 1006 |
+
"product_variant": 13,
|
| 1007 |
+
"quantity": 1,
|
| 1008 |
+
"low_stock_threshold": 5,
|
| 1009 |
+
"last_updated": "2026-02-06T08:10:48.314Z"
|
| 1010 |
+
}
|
| 1011 |
+
},
|
| 1012 |
+
{
|
| 1013 |
+
"model": "fitting_system.inventory",
|
| 1014 |
+
"pk": 14,
|
| 1015 |
+
"fields": {
|
| 1016 |
+
"product_variant": 14,
|
| 1017 |
+
"quantity": 1,
|
| 1018 |
+
"low_stock_threshold": 5,
|
| 1019 |
+
"last_updated": "2026-02-06T08:10:48.344Z"
|
| 1020 |
+
}
|
| 1021 |
+
},
|
| 1022 |
+
{
|
| 1023 |
+
"model": "fitting_system.inventory",
|
| 1024 |
+
"pk": 15,
|
| 1025 |
+
"fields": {
|
| 1026 |
+
"product_variant": 15,
|
| 1027 |
+
"quantity": 2,
|
| 1028 |
+
"low_stock_threshold": 5,
|
| 1029 |
+
"last_updated": "2026-02-06T08:10:48.389Z"
|
| 1030 |
+
}
|
| 1031 |
+
},
|
| 1032 |
+
{
|
| 1033 |
+
"model": "fitting_system.inventory",
|
| 1034 |
+
"pk": 16,
|
| 1035 |
+
"fields": {
|
| 1036 |
+
"product_variant": 16,
|
| 1037 |
+
"quantity": 2,
|
| 1038 |
+
"low_stock_threshold": 5,
|
| 1039 |
+
"last_updated": "2026-02-06T08:10:48.421Z"
|
| 1040 |
+
}
|
| 1041 |
+
},
|
| 1042 |
+
{
|
| 1043 |
+
"model": "fitting_system.inventory",
|
| 1044 |
+
"pk": 17,
|
| 1045 |
+
"fields": {
|
| 1046 |
+
"product_variant": 17,
|
| 1047 |
+
"quantity": 4,
|
| 1048 |
+
"low_stock_threshold": 5,
|
| 1049 |
+
"last_updated": "2026-02-06T08:10:48.451Z"
|
| 1050 |
+
}
|
| 1051 |
+
},
|
| 1052 |
+
{
|
| 1053 |
+
"model": "fitting_system.inventory",
|
| 1054 |
+
"pk": 18,
|
| 1055 |
+
"fields": {
|
| 1056 |
+
"product_variant": 18,
|
| 1057 |
+
"quantity": 1,
|
| 1058 |
+
"low_stock_threshold": 5,
|
| 1059 |
+
"last_updated": "2026-02-06T08:10:48.488Z"
|
| 1060 |
+
}
|
| 1061 |
+
},
|
| 1062 |
+
{
|
| 1063 |
+
"model": "fitting_system.inventory",
|
| 1064 |
+
"pk": 19,
|
| 1065 |
+
"fields": {
|
| 1066 |
+
"product_variant": 19,
|
| 1067 |
+
"quantity": 2,
|
| 1068 |
+
"low_stock_threshold": 5,
|
| 1069 |
+
"last_updated": "2026-02-06T08:10:48.521Z"
|
| 1070 |
+
}
|
| 1071 |
+
},
|
| 1072 |
+
{
|
| 1073 |
+
"model": "fitting_system.inventory",
|
| 1074 |
+
"pk": 20,
|
| 1075 |
+
"fields": {
|
| 1076 |
+
"product_variant": 20,
|
| 1077 |
+
"quantity": 2,
|
| 1078 |
+
"low_stock_threshold": 5,
|
| 1079 |
+
"last_updated": "2026-02-06T08:10:48.553Z"
|
| 1080 |
+
}
|
| 1081 |
+
},
|
| 1082 |
+
{
|
| 1083 |
+
"model": "fitting_system.inventory",
|
| 1084 |
+
"pk": 21,
|
| 1085 |
+
"fields": {
|
| 1086 |
+
"product_variant": 21,
|
| 1087 |
+
"quantity": 23,
|
| 1088 |
+
"low_stock_threshold": 5,
|
| 1089 |
+
"last_updated": "2026-02-06T08:10:48.583Z"
|
| 1090 |
+
}
|
| 1091 |
+
},
|
| 1092 |
+
{
|
| 1093 |
+
"model": "fitting_system.inventory",
|
| 1094 |
+
"pk": 22,
|
| 1095 |
+
"fields": {
|
| 1096 |
+
"product_variant": 22,
|
| 1097 |
+
"quantity": 20,
|
| 1098 |
+
"low_stock_threshold": 5,
|
| 1099 |
+
"last_updated": "2026-02-06T08:10:48.622Z"
|
| 1100 |
+
}
|
| 1101 |
+
},
|
| 1102 |
+
{
|
| 1103 |
+
"model": "fitting_system.inventory",
|
| 1104 |
+
"pk": 23,
|
| 1105 |
+
"fields": {
|
| 1106 |
+
"product_variant": 23,
|
| 1107 |
+
"quantity": 12,
|
| 1108 |
+
"low_stock_threshold": 5,
|
| 1109 |
+
"last_updated": "2026-02-06T08:10:48.654Z"
|
| 1110 |
+
}
|
| 1111 |
+
},
|
| 1112 |
+
{
|
| 1113 |
+
"model": "fitting_system.inventory",
|
| 1114 |
+
"pk": 24,
|
| 1115 |
+
"fields": {
|
| 1116 |
+
"product_variant": 24,
|
| 1117 |
+
"quantity": 21,
|
| 1118 |
+
"low_stock_threshold": 5,
|
| 1119 |
+
"last_updated": "2026-02-06T08:10:48.683Z"
|
| 1120 |
+
}
|
| 1121 |
+
},
|
| 1122 |
+
{
|
| 1123 |
+
"model": "fitting_system.inventory",
|
| 1124 |
+
"pk": 25,
|
| 1125 |
+
"fields": {
|
| 1126 |
+
"product_variant": 25,
|
| 1127 |
+
"quantity": 18,
|
| 1128 |
+
"low_stock_threshold": 5,
|
| 1129 |
+
"last_updated": "2026-02-06T08:10:48.715Z"
|
| 1130 |
+
}
|
| 1131 |
+
},
|
| 1132 |
+
{
|
| 1133 |
+
"model": "fitting_system.inventory",
|
| 1134 |
+
"pk": 26,
|
| 1135 |
+
"fields": {
|
| 1136 |
+
"product_variant": 26,
|
| 1137 |
+
"quantity": 14,
|
| 1138 |
+
"low_stock_threshold": 5,
|
| 1139 |
+
"last_updated": "2026-02-06T08:10:48.745Z"
|
| 1140 |
+
}
|
| 1141 |
+
},
|
| 1142 |
+
{
|
| 1143 |
+
"model": "fitting_system.inventory",
|
| 1144 |
+
"pk": 27,
|
| 1145 |
+
"fields": {
|
| 1146 |
+
"product_variant": 27,
|
| 1147 |
+
"quantity": 11,
|
| 1148 |
+
"low_stock_threshold": 5,
|
| 1149 |
+
"last_updated": "2026-02-06T08:10:48.775Z"
|
| 1150 |
+
}
|
| 1151 |
+
},
|
| 1152 |
+
{
|
| 1153 |
+
"model": "fitting_system.inventory",
|
| 1154 |
+
"pk": 28,
|
| 1155 |
+
"fields": {
|
| 1156 |
+
"product_variant": 28,
|
| 1157 |
+
"quantity": 18,
|
| 1158 |
+
"low_stock_threshold": 5,
|
| 1159 |
+
"last_updated": "2026-02-06T08:10:48.805Z"
|
| 1160 |
+
}
|
| 1161 |
+
},
|
| 1162 |
+
{
|
| 1163 |
+
"model": "fitting_system.inventory",
|
| 1164 |
+
"pk": 29,
|
| 1165 |
+
"fields": {
|
| 1166 |
+
"product_variant": 29,
|
| 1167 |
+
"quantity": 22,
|
| 1168 |
+
"low_stock_threshold": 5,
|
| 1169 |
+
"last_updated": "2026-02-06T08:10:48.835Z"
|
| 1170 |
+
}
|
| 1171 |
+
},
|
| 1172 |
+
{
|
| 1173 |
+
"model": "fitting_system.inventory",
|
| 1174 |
+
"pk": 30,
|
| 1175 |
+
"fields": {
|
| 1176 |
+
"product_variant": 30,
|
| 1177 |
+
"quantity": 10,
|
| 1178 |
+
"low_stock_threshold": 5,
|
| 1179 |
+
"last_updated": "2026-02-06T08:10:48.865Z"
|
| 1180 |
+
}
|
| 1181 |
+
},
|
| 1182 |
+
{
|
| 1183 |
+
"model": "fitting_system.inventory",
|
| 1184 |
+
"pk": 31,
|
| 1185 |
+
"fields": {
|
| 1186 |
+
"product_variant": 31,
|
| 1187 |
+
"quantity": 17,
|
| 1188 |
+
"low_stock_threshold": 5,
|
| 1189 |
+
"last_updated": "2026-02-06T08:10:48.929Z"
|
| 1190 |
+
}
|
| 1191 |
+
},
|
| 1192 |
+
{
|
| 1193 |
+
"model": "fitting_system.inventory",
|
| 1194 |
+
"pk": 32,
|
| 1195 |
+
"fields": {
|
| 1196 |
+
"product_variant": 32,
|
| 1197 |
+
"quantity": 11,
|
| 1198 |
+
"low_stock_threshold": 5,
|
| 1199 |
+
"last_updated": "2026-02-06T08:10:48.964Z"
|
| 1200 |
+
}
|
| 1201 |
+
},
|
| 1202 |
+
{
|
| 1203 |
+
"model": "fitting_system.inventory",
|
| 1204 |
+
"pk": 33,
|
| 1205 |
+
"fields": {
|
| 1206 |
+
"product_variant": 33,
|
| 1207 |
+
"quantity": 19,
|
| 1208 |
+
"low_stock_threshold": 5,
|
| 1209 |
+
"last_updated": "2026-02-06T08:10:48.997Z"
|
| 1210 |
+
}
|
| 1211 |
+
},
|
| 1212 |
+
{
|
| 1213 |
+
"model": "fitting_system.inventory",
|
| 1214 |
+
"pk": 34,
|
| 1215 |
+
"fields": {
|
| 1216 |
+
"product_variant": 34,
|
| 1217 |
+
"quantity": 15,
|
| 1218 |
+
"low_stock_threshold": 5,
|
| 1219 |
+
"last_updated": "2026-02-06T08:10:49.030Z"
|
| 1220 |
+
}
|
| 1221 |
+
},
|
| 1222 |
+
{
|
| 1223 |
+
"model": "fitting_system.inventory",
|
| 1224 |
+
"pk": 35,
|
| 1225 |
+
"fields": {
|
| 1226 |
+
"product_variant": 35,
|
| 1227 |
+
"quantity": 16,
|
| 1228 |
+
"low_stock_threshold": 5,
|
| 1229 |
+
"last_updated": "2026-02-06T08:10:49.070Z"
|
| 1230 |
+
}
|
| 1231 |
+
},
|
| 1232 |
+
{
|
| 1233 |
+
"model": "fitting_system.inventory",
|
| 1234 |
+
"pk": 36,
|
| 1235 |
+
"fields": {
|
| 1236 |
+
"product_variant": 36,
|
| 1237 |
+
"quantity": 12,
|
| 1238 |
+
"low_stock_threshold": 5,
|
| 1239 |
+
"last_updated": "2026-02-06T08:10:49.108Z"
|
| 1240 |
+
}
|
| 1241 |
+
},
|
| 1242 |
+
{
|
| 1243 |
+
"model": "fitting_system.inventory",
|
| 1244 |
+
"pk": 37,
|
| 1245 |
+
"fields": {
|
| 1246 |
+
"product_variant": 37,
|
| 1247 |
+
"quantity": 21,
|
| 1248 |
+
"low_stock_threshold": 5,
|
| 1249 |
+
"last_updated": "2026-02-06T08:10:49.149Z"
|
| 1250 |
+
}
|
| 1251 |
+
},
|
| 1252 |
+
{
|
| 1253 |
+
"model": "fitting_system.inventory",
|
| 1254 |
+
"pk": 38,
|
| 1255 |
+
"fields": {
|
| 1256 |
+
"product_variant": 38,
|
| 1257 |
+
"quantity": 13,
|
| 1258 |
+
"low_stock_threshold": 5,
|
| 1259 |
+
"last_updated": "2026-02-06T08:10:49.189Z"
|
| 1260 |
+
}
|
| 1261 |
+
},
|
| 1262 |
+
{
|
| 1263 |
+
"model": "fitting_system.inventory",
|
| 1264 |
+
"pk": 39,
|
| 1265 |
+
"fields": {
|
| 1266 |
+
"product_variant": 39,
|
| 1267 |
+
"quantity": 23,
|
| 1268 |
+
"low_stock_threshold": 5,
|
| 1269 |
+
"last_updated": "2026-02-06T08:10:49.221Z"
|
| 1270 |
+
}
|
| 1271 |
+
},
|
| 1272 |
+
{
|
| 1273 |
+
"model": "fitting_system.inventory",
|
| 1274 |
+
"pk": 40,
|
| 1275 |
+
"fields": {
|
| 1276 |
+
"product_variant": 40,
|
| 1277 |
+
"quantity": 15,
|
| 1278 |
+
"low_stock_threshold": 5,
|
| 1279 |
+
"last_updated": "2026-02-06T08:10:49.271Z"
|
| 1280 |
+
}
|
| 1281 |
+
},
|
| 1282 |
+
{
|
| 1283 |
+
"model": "fitting_system.inventory",
|
| 1284 |
+
"pk": 41,
|
| 1285 |
+
"fields": {
|
| 1286 |
+
"product_variant": 41,
|
| 1287 |
+
"quantity": 17,
|
| 1288 |
+
"low_stock_threshold": 5,
|
| 1289 |
+
"last_updated": "2026-02-06T08:10:49.316Z"
|
| 1290 |
+
}
|
| 1291 |
+
},
|
| 1292 |
+
{
|
| 1293 |
+
"model": "fitting_system.inventory",
|
| 1294 |
+
"pk": 42,
|
| 1295 |
+
"fields": {
|
| 1296 |
+
"product_variant": 42,
|
| 1297 |
+
"quantity": 24,
|
| 1298 |
+
"low_stock_threshold": 5,
|
| 1299 |
+
"last_updated": "2026-02-06T08:10:49.354Z"
|
| 1300 |
+
}
|
| 1301 |
+
},
|
| 1302 |
+
{
|
| 1303 |
+
"model": "fitting_system.inventory",
|
| 1304 |
+
"pk": 43,
|
| 1305 |
+
"fields": {
|
| 1306 |
+
"product_variant": 43,
|
| 1307 |
+
"quantity": 25,
|
| 1308 |
+
"low_stock_threshold": 5,
|
| 1309 |
+
"last_updated": "2026-02-06T08:10:49.389Z"
|
| 1310 |
+
}
|
| 1311 |
+
},
|
| 1312 |
+
{
|
| 1313 |
+
"model": "fitting_system.inventory",
|
| 1314 |
+
"pk": 44,
|
| 1315 |
+
"fields": {
|
| 1316 |
+
"product_variant": 44,
|
| 1317 |
+
"quantity": 11,
|
| 1318 |
+
"low_stock_threshold": 5,
|
| 1319 |
+
"last_updated": "2026-02-06T08:10:49.430Z"
|
| 1320 |
+
}
|
| 1321 |
+
},
|
| 1322 |
+
{
|
| 1323 |
+
"model": "fitting_system.inventory",
|
| 1324 |
+
"pk": 45,
|
| 1325 |
+
"fields": {
|
| 1326 |
+
"product_variant": 45,
|
| 1327 |
+
"quantity": 25,
|
| 1328 |
+
"low_stock_threshold": 5,
|
| 1329 |
+
"last_updated": "2026-02-06T08:10:49.477Z"
|
| 1330 |
+
}
|
| 1331 |
+
},
|
| 1332 |
+
{
|
| 1333 |
+
"model": "fitting_system.inventory",
|
| 1334 |
+
"pk": 46,
|
| 1335 |
+
"fields": {
|
| 1336 |
+
"product_variant": 46,
|
| 1337 |
+
"quantity": 24,
|
| 1338 |
+
"low_stock_threshold": 5,
|
| 1339 |
+
"last_updated": "2026-02-06T08:10:49.518Z"
|
| 1340 |
+
}
|
| 1341 |
+
},
|
| 1342 |
+
{
|
| 1343 |
+
"model": "fitting_system.inventory",
|
| 1344 |
+
"pk": 47,
|
| 1345 |
+
"fields": {
|
| 1346 |
+
"product_variant": 47,
|
| 1347 |
+
"quantity": 21,
|
| 1348 |
+
"low_stock_threshold": 5,
|
| 1349 |
+
"last_updated": "2026-02-06T08:10:49.559Z"
|
| 1350 |
+
}
|
| 1351 |
+
},
|
| 1352 |
+
{
|
| 1353 |
+
"model": "fitting_system.inventory",
|
| 1354 |
+
"pk": 48,
|
| 1355 |
+
"fields": {
|
| 1356 |
+
"product_variant": 48,
|
| 1357 |
+
"quantity": 11,
|
| 1358 |
+
"low_stock_threshold": 5,
|
| 1359 |
+
"last_updated": "2026-02-06T08:10:49.607Z"
|
| 1360 |
+
}
|
| 1361 |
+
},
|
| 1362 |
+
{
|
| 1363 |
+
"model": "fitting_system.inventory",
|
| 1364 |
+
"pk": 49,
|
| 1365 |
+
"fields": {
|
| 1366 |
+
"product_variant": 49,
|
| 1367 |
+
"quantity": 17,
|
| 1368 |
+
"low_stock_threshold": 5,
|
| 1369 |
+
"last_updated": "2026-02-06T08:10:49.652Z"
|
| 1370 |
+
}
|
| 1371 |
+
},
|
| 1372 |
+
{
|
| 1373 |
+
"model": "fitting_system.inventory",
|
| 1374 |
+
"pk": 50,
|
| 1375 |
+
"fields": {
|
| 1376 |
+
"product_variant": 50,
|
| 1377 |
+
"quantity": 19,
|
| 1378 |
+
"low_stock_threshold": 5,
|
| 1379 |
+
"last_updated": "2026-02-06T08:10:49.694Z"
|
| 1380 |
+
}
|
| 1381 |
+
},
|
| 1382 |
+
{
|
| 1383 |
+
"model": "fitting_system.inventory",
|
| 1384 |
+
"pk": 51,
|
| 1385 |
+
"fields": {
|
| 1386 |
+
"product_variant": 51,
|
| 1387 |
+
"quantity": 25,
|
| 1388 |
+
"low_stock_threshold": 5,
|
| 1389 |
+
"last_updated": "2026-02-06T08:10:49.739Z"
|
| 1390 |
+
}
|
| 1391 |
+
},
|
| 1392 |
+
{
|
| 1393 |
+
"model": "fitting_system.inventory",
|
| 1394 |
+
"pk": 52,
|
| 1395 |
+
"fields": {
|
| 1396 |
+
"product_variant": 52,
|
| 1397 |
+
"quantity": 10,
|
| 1398 |
+
"low_stock_threshold": 5,
|
| 1399 |
+
"last_updated": "2026-02-06T08:10:49.777Z"
|
| 1400 |
+
}
|
| 1401 |
+
},
|
| 1402 |
+
{
|
| 1403 |
+
"model": "fitting_system.inventory",
|
| 1404 |
+
"pk": 53,
|
| 1405 |
+
"fields": {
|
| 1406 |
+
"product_variant": 53,
|
| 1407 |
+
"quantity": 10,
|
| 1408 |
+
"low_stock_threshold": 5,
|
| 1409 |
+
"last_updated": "2026-02-06T08:10:49.815Z"
|
| 1410 |
+
}
|
| 1411 |
+
},
|
| 1412 |
+
{
|
| 1413 |
+
"model": "fitting_system.inventory",
|
| 1414 |
+
"pk": 54,
|
| 1415 |
+
"fields": {
|
| 1416 |
+
"product_variant": 54,
|
| 1417 |
+
"quantity": 17,
|
| 1418 |
+
"low_stock_threshold": 5,
|
| 1419 |
+
"last_updated": "2026-02-06T08:10:49.849Z"
|
| 1420 |
+
}
|
| 1421 |
+
},
|
| 1422 |
+
{
|
| 1423 |
+
"model": "fitting_system.bodyscan",
|
| 1424 |
+
"pk": 1,
|
| 1425 |
+
"fields": {
|
| 1426 |
+
"session_id": "33e8d83d-1535-450f-8085-b6b071cb2f67",
|
| 1427 |
+
"height": "170.0",
|
| 1428 |
+
"shoulder_width": "42.5",
|
| 1429 |
+
"chest": "93.5",
|
| 1430 |
+
"waist": "79.9",
|
| 1431 |
+
"hip": "93.5",
|
| 1432 |
+
"torso_length": "51.0",
|
| 1433 |
+
"arm_length": "56.1",
|
| 1434 |
+
"inseam": "76.5",
|
| 1435 |
+
"body_shape": "rectangle",
|
| 1436 |
+
"skin_tone": "medium",
|
| 1437 |
+
"undertone": "warm",
|
| 1438 |
+
"confidence_score": "0.30",
|
| 1439 |
+
"frame_count": 1,
|
| 1440 |
+
"scanned_at": "2026-02-14T12:37:34.007Z"
|
| 1441 |
+
}
|
| 1442 |
+
},
|
| 1443 |
+
{
|
| 1444 |
+
"model": "fitting_system.bodyscan",
|
| 1445 |
+
"pk": 2,
|
| 1446 |
+
"fields": {
|
| 1447 |
+
"session_id": "0bb9006d-65c6-4601-a107-ef063301913f",
|
| 1448 |
+
"height": "170.0",
|
| 1449 |
+
"shoulder_width": "42.5",
|
| 1450 |
+
"chest": "93.5",
|
| 1451 |
+
"waist": "79.9",
|
| 1452 |
+
"hip": "93.5",
|
| 1453 |
+
"torso_length": "51.0",
|
| 1454 |
+
"arm_length": "56.1",
|
| 1455 |
+
"inseam": "76.5",
|
| 1456 |
+
"body_shape": "rectangle",
|
| 1457 |
+
"skin_tone": "medium",
|
| 1458 |
+
"undertone": "warm",
|
| 1459 |
+
"confidence_score": "0.30",
|
| 1460 |
+
"frame_count": 1,
|
| 1461 |
+
"scanned_at": "2026-02-14T12:44:17.023Z"
|
| 1462 |
+
}
|
| 1463 |
+
},
|
| 1464 |
+
{
|
| 1465 |
+
"model": "fitting_system.bodyscan",
|
| 1466 |
+
"pk": 3,
|
| 1467 |
+
"fields": {
|
| 1468 |
+
"session_id": "c335ba12-aeb0-4f69-92f1-5170f2b7dd80",
|
| 1469 |
+
"height": "170.0",
|
| 1470 |
+
"shoulder_width": "42.5",
|
| 1471 |
+
"chest": "93.5",
|
| 1472 |
+
"waist": "79.9",
|
| 1473 |
+
"hip": "93.5",
|
| 1474 |
+
"torso_length": "51.0",
|
| 1475 |
+
"arm_length": "56.1",
|
| 1476 |
+
"inseam": "76.5",
|
| 1477 |
+
"body_shape": "rectangle",
|
| 1478 |
+
"skin_tone": "medium",
|
| 1479 |
+
"undertone": "warm",
|
| 1480 |
+
"confidence_score": "0.30",
|
| 1481 |
+
"frame_count": 1,
|
| 1482 |
+
"scanned_at": "2026-02-16T09:42:40.334Z"
|
| 1483 |
+
}
|
| 1484 |
+
},
|
| 1485 |
+
{
|
| 1486 |
+
"model": "fitting_system.bodyscan",
|
| 1487 |
+
"pk": 4,
|
| 1488 |
+
"fields": {
|
| 1489 |
+
"session_id": "b6cb4016-cdce-4899-b6e2-d62664055df8",
|
| 1490 |
+
"height": "175.0",
|
| 1491 |
+
"shoulder_width": "44.0",
|
| 1492 |
+
"chest": "95.0",
|
| 1493 |
+
"waist": "80.0",
|
| 1494 |
+
"hip": "98.0",
|
| 1495 |
+
"torso_length": "48.0",
|
| 1496 |
+
"arm_length": "62.0",
|
| 1497 |
+
"inseam": "78.0",
|
| 1498 |
+
"body_shape": "rectangle",
|
| 1499 |
+
"skin_tone": "medium",
|
| 1500 |
+
"undertone": "warm",
|
| 1501 |
+
"confidence_score": "0.85",
|
| 1502 |
+
"frame_count": 1,
|
| 1503 |
+
"scanned_at": "2026-02-18T13:06:30.569Z"
|
| 1504 |
+
}
|
| 1505 |
+
},
|
| 1506 |
+
{
|
| 1507 |
+
"model": "fitting_system.bodyscan",
|
| 1508 |
+
"pk": 5,
|
| 1509 |
+
"fields": {
|
| 1510 |
+
"session_id": "74b0ffcf-bdb1-40f2-90bc-537f23bfa19f",
|
| 1511 |
+
"height": "170.0",
|
| 1512 |
+
"shoulder_width": "42.0",
|
| 1513 |
+
"chest": "92.0",
|
| 1514 |
+
"waist": "78.0",
|
| 1515 |
+
"hip": "96.0",
|
| 1516 |
+
"torso_length": "50.0",
|
| 1517 |
+
"arm_length": "60.0",
|
| 1518 |
+
"inseam": "78.0",
|
| 1519 |
+
"body_shape": "rectangle",
|
| 1520 |
+
"skin_tone": "intermediate",
|
| 1521 |
+
"undertone": "warm",
|
| 1522 |
+
"confidence_score": "0.85",
|
| 1523 |
+
"frame_count": 1,
|
| 1524 |
+
"scanned_at": "2026-02-18T15:00:02.342Z"
|
| 1525 |
+
}
|
| 1526 |
+
},
|
| 1527 |
+
{
|
| 1528 |
+
"model": "fitting_system.bodyscan",
|
| 1529 |
+
"pk": 6,
|
| 1530 |
+
"fields": {
|
| 1531 |
+
"session_id": "fd274e84-f630-494e-8b80-d717e006b2eb",
|
| 1532 |
+
"height": "170.0",
|
| 1533 |
+
"shoulder_width": "42.0",
|
| 1534 |
+
"chest": "92.0",
|
| 1535 |
+
"waist": "78.0",
|
| 1536 |
+
"hip": "96.0",
|
| 1537 |
+
"torso_length": "50.0",
|
| 1538 |
+
"arm_length": "60.0",
|
| 1539 |
+
"inseam": "78.0",
|
| 1540 |
+
"body_shape": "rectangle",
|
| 1541 |
+
"skin_tone": "tan",
|
| 1542 |
+
"undertone": "warm",
|
| 1543 |
+
"confidence_score": "0.85",
|
| 1544 |
+
"frame_count": 1,
|
| 1545 |
+
"scanned_at": "2026-02-18T15:00:46.545Z"
|
| 1546 |
+
}
|
| 1547 |
+
},
|
| 1548 |
+
{
|
| 1549 |
+
"model": "fitting_system.bodyscan",
|
| 1550 |
+
"pk": 7,
|
| 1551 |
+
"fields": {
|
| 1552 |
+
"session_id": "025a7e85-025a-4f15-96ce-729a6c713c8b",
|
| 1553 |
+
"height": "170.0",
|
| 1554 |
+
"shoulder_width": "35.3",
|
| 1555 |
+
"chest": "70.6",
|
| 1556 |
+
"waist": "50.0",
|
| 1557 |
+
"hip": "70.0",
|
| 1558 |
+
"torso_length": "57.1",
|
| 1559 |
+
"arm_length": "58.5",
|
| 1560 |
+
"inseam": "87.4",
|
| 1561 |
+
"body_shape": "rectangle",
|
| 1562 |
+
"skin_tone": "dark",
|
| 1563 |
+
"undertone": "warm",
|
| 1564 |
+
"confidence_score": "0.85",
|
| 1565 |
+
"frame_count": 1,
|
| 1566 |
+
"scanned_at": "2026-02-19T20:23:48.828Z"
|
| 1567 |
+
}
|
| 1568 |
+
},
|
| 1569 |
+
{
|
| 1570 |
+
"model": "fitting_system.bodyscan",
|
| 1571 |
+
"pk": 8,
|
| 1572 |
+
"fields": {
|
| 1573 |
+
"session_id": "5aab062a-729f-43dd-b4b2-86b8b8b6db19",
|
| 1574 |
+
"height": "170.0",
|
| 1575 |
+
"shoulder_width": "39.4",
|
| 1576 |
+
"chest": "78.8",
|
| 1577 |
+
"waist": "50.0",
|
| 1578 |
+
"hip": "70.0",
|
| 1579 |
+
"torso_length": "65.0",
|
| 1580 |
+
"arm_length": "80.0",
|
| 1581 |
+
"inseam": "75.1",
|
| 1582 |
+
"body_shape": "rectangle",
|
| 1583 |
+
"skin_tone": "tan",
|
| 1584 |
+
"undertone": "warm",
|
| 1585 |
+
"confidence_score": "0.85",
|
| 1586 |
+
"frame_count": 1,
|
| 1587 |
+
"scanned_at": "2026-02-19T20:24:38.194Z"
|
| 1588 |
+
}
|
| 1589 |
+
},
|
| 1590 |
+
{
|
| 1591 |
+
"model": "fitting_system.bodyscan",
|
| 1592 |
+
"pk": 9,
|
| 1593 |
+
"fields": {
|
| 1594 |
+
"session_id": "57a37a4d-4c6e-4b16-95e5-f82baba03961",
|
| 1595 |
+
"height": "170.0",
|
| 1596 |
+
"shoulder_width": "35.3",
|
| 1597 |
+
"chest": "70.6",
|
| 1598 |
+
"waist": "50.0",
|
| 1599 |
+
"hip": "70.0",
|
| 1600 |
+
"torso_length": "57.1",
|
| 1601 |
+
"arm_length": "58.5",
|
| 1602 |
+
"inseam": "87.4",
|
| 1603 |
+
"body_shape": "rectangle",
|
| 1604 |
+
"skin_tone": "intermediate",
|
| 1605 |
+
"undertone": "warm",
|
| 1606 |
+
"confidence_score": "0.85",
|
| 1607 |
+
"frame_count": 1,
|
| 1608 |
+
"scanned_at": "2026-02-19T20:25:11.384Z"
|
| 1609 |
+
}
|
| 1610 |
+
},
|
| 1611 |
+
{
|
| 1612 |
+
"model": "fitting_system.bodyscan",
|
| 1613 |
+
"pk": 10,
|
| 1614 |
+
"fields": {
|
| 1615 |
+
"session_id": "ee7edd5a-8eea-4e5d-a6b7-2354d98fe3e8",
|
| 1616 |
+
"height": "168.0",
|
| 1617 |
+
"shoulder_width": "56.3",
|
| 1618 |
+
"chest": "112.6",
|
| 1619 |
+
"waist": "67.7",
|
| 1620 |
+
"hip": "82.7",
|
| 1621 |
+
"torso_length": "65.0",
|
| 1622 |
+
"arm_length": "80.0",
|
| 1623 |
+
"inseam": "55.0",
|
| 1624 |
+
"body_shape": "rectangle",
|
| 1625 |
+
"skin_tone": "dark",
|
| 1626 |
+
"undertone": "warm",
|
| 1627 |
+
"confidence_score": "0.85",
|
| 1628 |
+
"frame_count": 1,
|
| 1629 |
+
"scanned_at": "2026-02-19T21:14:29.613Z"
|
| 1630 |
+
}
|
| 1631 |
+
},
|
| 1632 |
+
{
|
| 1633 |
+
"model": "fitting_system.bodyscan",
|
| 1634 |
+
"pk": 11,
|
| 1635 |
+
"fields": {
|
| 1636 |
+
"session_id": "e875a744-3dcc-4130-bb5a-c49c490e17ec",
|
| 1637 |
+
"height": "175.0",
|
| 1638 |
+
"shoulder_width": "36.3",
|
| 1639 |
+
"chest": "72.6",
|
| 1640 |
+
"waist": "50.0",
|
| 1641 |
+
"hip": "70.0",
|
| 1642 |
+
"torso_length": "58.7",
|
| 1643 |
+
"arm_length": "60.2",
|
| 1644 |
+
"inseam": "90.0",
|
| 1645 |
+
"body_shape": "rectangle",
|
| 1646 |
+
"skin_tone": "intermediate",
|
| 1647 |
+
"undertone": "warm",
|
| 1648 |
+
"confidence_score": "0.85",
|
| 1649 |
+
"frame_count": 1,
|
| 1650 |
+
"scanned_at": "2026-02-19T21:15:48.703Z"
|
| 1651 |
+
}
|
| 1652 |
+
},
|
| 1653 |
+
{
|
| 1654 |
+
"model": "fitting_system.bodyscan",
|
| 1655 |
+
"pk": 12,
|
| 1656 |
+
"fields": {
|
| 1657 |
+
"session_id": "f51a33e2-d6ec-440c-b9d5-ff628ef16810",
|
| 1658 |
+
"height": "170.0",
|
| 1659 |
+
"shoulder_width": "37.9",
|
| 1660 |
+
"chest": "75.8",
|
| 1661 |
+
"waist": "50.0",
|
| 1662 |
+
"hip": "70.0",
|
| 1663 |
+
"torso_length": "65.0",
|
| 1664 |
+
"arm_length": "80.0",
|
| 1665 |
+
"inseam": "75.8",
|
| 1666 |
+
"body_shape": "rectangle",
|
| 1667 |
+
"skin_tone": "dark",
|
| 1668 |
+
"undertone": "warm",
|
| 1669 |
+
"confidence_score": "0.85",
|
| 1670 |
+
"frame_count": 1,
|
| 1671 |
+
"scanned_at": "2026-02-19T21:39:35.108Z"
|
| 1672 |
+
}
|
| 1673 |
+
},
|
| 1674 |
+
{
|
| 1675 |
+
"model": "fitting_system.bodyscan",
|
| 1676 |
+
"pk": 13,
|
| 1677 |
+
"fields": {
|
| 1678 |
+
"session_id": "5ac3048a-115b-4131-9556-9012f066a825",
|
| 1679 |
+
"height": "170.0",
|
| 1680 |
+
"shoulder_width": "37.9",
|
| 1681 |
+
"chest": "75.8",
|
| 1682 |
+
"waist": "50.0",
|
| 1683 |
+
"hip": "70.0",
|
| 1684 |
+
"torso_length": "65.0",
|
| 1685 |
+
"arm_length": "80.0",
|
| 1686 |
+
"inseam": "75.8",
|
| 1687 |
+
"body_shape": "rectangle",
|
| 1688 |
+
"skin_tone": "dark",
|
| 1689 |
+
"undertone": "warm",
|
| 1690 |
+
"confidence_score": "0.85",
|
| 1691 |
+
"frame_count": 1,
|
| 1692 |
+
"scanned_at": "2026-02-19T21:50:02.138Z"
|
| 1693 |
+
}
|
| 1694 |
+
},
|
| 1695 |
+
{
|
| 1696 |
+
"model": "fitting_system.bodyscan",
|
| 1697 |
+
"pk": 14,
|
| 1698 |
+
"fields": {
|
| 1699 |
+
"session_id": "ae54c1ea-fe54-4546-9603-7179129609ff",
|
| 1700 |
+
"height": "168.0",
|
| 1701 |
+
"shoulder_width": "56.3",
|
| 1702 |
+
"chest": "112.6",
|
| 1703 |
+
"waist": "67.7",
|
| 1704 |
+
"hip": "82.7",
|
| 1705 |
+
"torso_length": "65.0",
|
| 1706 |
+
"arm_length": "80.0",
|
| 1707 |
+
"inseam": "55.0",
|
| 1708 |
+
"body_shape": "rectangle",
|
| 1709 |
+
"skin_tone": "dark",
|
| 1710 |
+
"undertone": "warm",
|
| 1711 |
+
"confidence_score": "0.85",
|
| 1712 |
+
"frame_count": 1,
|
| 1713 |
+
"scanned_at": "2026-02-19T21:50:51.604Z"
|
| 1714 |
+
}
|
| 1715 |
+
},
|
| 1716 |
+
{
|
| 1717 |
+
"model": "fitting_system.bodyscan",
|
| 1718 |
+
"pk": 15,
|
| 1719 |
+
"fields": {
|
| 1720 |
+
"session_id": "cc5de619-da33-41cc-9220-b8e4a6920dd8",
|
| 1721 |
+
"height": "168.0",
|
| 1722 |
+
"shoulder_width": "56.3",
|
| 1723 |
+
"chest": "112.6",
|
| 1724 |
+
"waist": "67.7",
|
| 1725 |
+
"hip": "82.7",
|
| 1726 |
+
"torso_length": "65.0",
|
| 1727 |
+
"arm_length": "80.0",
|
| 1728 |
+
"inseam": "55.0",
|
| 1729 |
+
"body_shape": "rectangle",
|
| 1730 |
+
"skin_tone": "dark",
|
| 1731 |
+
"undertone": "warm",
|
| 1732 |
+
"confidence_score": "0.85",
|
| 1733 |
+
"frame_count": 1,
|
| 1734 |
+
"scanned_at": "2026-02-19T21:52:22.884Z"
|
| 1735 |
+
}
|
| 1736 |
+
},
|
| 1737 |
+
{
|
| 1738 |
+
"model": "fitting_system.bodyscan",
|
| 1739 |
+
"pk": 16,
|
| 1740 |
+
"fields": {
|
| 1741 |
+
"session_id": "a3ef5c2a-0c90-402f-97cf-3d101d10f0de",
|
| 1742 |
+
"height": "170.0",
|
| 1743 |
+
"shoulder_width": "38.4",
|
| 1744 |
+
"chest": "76.8",
|
| 1745 |
+
"waist": "50.0",
|
| 1746 |
+
"hip": "70.0",
|
| 1747 |
+
"torso_length": "58.6",
|
| 1748 |
+
"arm_length": "62.4",
|
| 1749 |
+
"inseam": "84.6",
|
| 1750 |
+
"body_shape": "rectangle",
|
| 1751 |
+
"skin_tone": "tan",
|
| 1752 |
+
"undertone": "warm",
|
| 1753 |
+
"confidence_score": "0.85",
|
| 1754 |
+
"frame_count": 1,
|
| 1755 |
+
"scanned_at": "2026-02-19T21:57:02.682Z"
|
| 1756 |
+
}
|
| 1757 |
+
},
|
| 1758 |
+
{
|
| 1759 |
+
"model": "fitting_system.bodyscan",
|
| 1760 |
+
"pk": 17,
|
| 1761 |
+
"fields": {
|
| 1762 |
+
"session_id": "9fb44691-2913-47a0-be48-6f855b3ba9db",
|
| 1763 |
+
"height": "170.0",
|
| 1764 |
+
"shoulder_width": "38.4",
|
| 1765 |
+
"chest": "76.8",
|
| 1766 |
+
"waist": "50.0",
|
| 1767 |
+
"hip": "70.0",
|
| 1768 |
+
"torso_length": "58.6",
|
| 1769 |
+
"arm_length": "62.4",
|
| 1770 |
+
"inseam": "84.6",
|
| 1771 |
+
"body_shape": "rectangle",
|
| 1772 |
+
"skin_tone": "tan",
|
| 1773 |
+
"undertone": "warm",
|
| 1774 |
+
"confidence_score": "0.85",
|
| 1775 |
+
"frame_count": 1,
|
| 1776 |
+
"scanned_at": "2026-02-19T22:06:30.850Z"
|
| 1777 |
+
}
|
| 1778 |
+
},
|
| 1779 |
+
{
|
| 1780 |
+
"model": "fitting_system.bodyscan",
|
| 1781 |
+
"pk": 18,
|
| 1782 |
+
"fields": {
|
| 1783 |
+
"session_id": "72fdca5e-4758-4509-b2ac-0c1cb9fc0fc6",
|
| 1784 |
+
"height": "170.0",
|
| 1785 |
+
"shoulder_width": "38.4",
|
| 1786 |
+
"chest": "76.8",
|
| 1787 |
+
"waist": "50.0",
|
| 1788 |
+
"hip": "70.0",
|
| 1789 |
+
"torso_length": "58.6",
|
| 1790 |
+
"arm_length": "62.4",
|
| 1791 |
+
"inseam": "84.6",
|
| 1792 |
+
"body_shape": "rectangle",
|
| 1793 |
+
"skin_tone": "tan",
|
| 1794 |
+
"undertone": "warm",
|
| 1795 |
+
"confidence_score": "0.85",
|
| 1796 |
+
"frame_count": 1,
|
| 1797 |
+
"scanned_at": "2026-02-19T22:19:11.204Z"
|
| 1798 |
+
}
|
| 1799 |
+
},
|
| 1800 |
+
{
|
| 1801 |
+
"model": "fitting_system.bodyscan",
|
| 1802 |
+
"pk": 19,
|
| 1803 |
+
"fields": {
|
| 1804 |
+
"session_id": "12ebedb7-7c1d-4205-a53a-3c0c06decb12",
|
| 1805 |
+
"height": "175.0",
|
| 1806 |
+
"shoulder_width": "36.3",
|
| 1807 |
+
"chest": "72.6",
|
| 1808 |
+
"waist": "50.0",
|
| 1809 |
+
"hip": "70.0",
|
| 1810 |
+
"torso_length": "58.7",
|
| 1811 |
+
"arm_length": "60.2",
|
| 1812 |
+
"inseam": "90.0",
|
| 1813 |
+
"body_shape": "rectangle",
|
| 1814 |
+
"skin_tone": "intermediate",
|
| 1815 |
+
"undertone": "warm",
|
| 1816 |
+
"confidence_score": "0.85",
|
| 1817 |
+
"frame_count": 1,
|
| 1818 |
+
"scanned_at": "2026-02-19T22:31:34.874Z"
|
| 1819 |
+
}
|
| 1820 |
+
},
|
| 1821 |
+
{
|
| 1822 |
+
"model": "fitting_system.bodyscan",
|
| 1823 |
+
"pk": 20,
|
| 1824 |
+
"fields": {
|
| 1825 |
+
"session_id": "94fb4ad1-566e-445e-bdd0-8684a4e464ca",
|
| 1826 |
+
"height": "170.0",
|
| 1827 |
+
"shoulder_width": "38.4",
|
| 1828 |
+
"chest": "103.7",
|
| 1829 |
+
"waist": "59.2",
|
| 1830 |
+
"hip": "70.0",
|
| 1831 |
+
"torso_length": "58.6",
|
| 1832 |
+
"arm_length": "62.4",
|
| 1833 |
+
"inseam": "84.6",
|
| 1834 |
+
"body_shape": "rectangle",
|
| 1835 |
+
"skin_tone": "tan",
|
| 1836 |
+
"undertone": "warm",
|
| 1837 |
+
"confidence_score": "0.85",
|
| 1838 |
+
"frame_count": 1,
|
| 1839 |
+
"scanned_at": "2026-02-19T22:45:57.026Z"
|
| 1840 |
+
}
|
| 1841 |
+
},
|
| 1842 |
+
{
|
| 1843 |
+
"model": "fitting_system.bodyscan",
|
| 1844 |
+
"pk": 21,
|
| 1845 |
+
"fields": {
|
| 1846 |
+
"session_id": "5617575a-5378-4b5e-bc13-da87ddb76629",
|
| 1847 |
+
"height": "175.0",
|
| 1848 |
+
"shoulder_width": "36.3",
|
| 1849 |
+
"chest": "98.0",
|
| 1850 |
+
"waist": "59.2",
|
| 1851 |
+
"hip": "70.0",
|
| 1852 |
+
"torso_length": "58.7",
|
| 1853 |
+
"arm_length": "60.2",
|
| 1854 |
+
"inseam": "90.0",
|
| 1855 |
+
"body_shape": "rectangle",
|
| 1856 |
+
"skin_tone": "intermediate",
|
| 1857 |
+
"undertone": "warm",
|
| 1858 |
+
"confidence_score": "0.85",
|
| 1859 |
+
"frame_count": 1,
|
| 1860 |
+
"scanned_at": "2026-02-19T22:49:00.229Z"
|
| 1861 |
+
}
|
| 1862 |
+
},
|
| 1863 |
+
{
|
| 1864 |
+
"model": "fitting_system.bodyscan",
|
| 1865 |
+
"pk": 22,
|
| 1866 |
+
"fields": {
|
| 1867 |
+
"session_id": "2e06df7c-c856-46cd-a0f8-5ccf1bae6487",
|
| 1868 |
+
"height": "170.0",
|
| 1869 |
+
"shoulder_width": "35.1",
|
| 1870 |
+
"chest": "94.8",
|
| 1871 |
+
"waist": "50.0",
|
| 1872 |
+
"hip": "70.0",
|
| 1873 |
+
"torso_length": "53.5",
|
| 1874 |
+
"arm_length": "57.0",
|
| 1875 |
+
"inseam": "77.3",
|
| 1876 |
+
"body_shape": "rectangle",
|
| 1877 |
+
"skin_tone": "tan",
|
| 1878 |
+
"undertone": "warm",
|
| 1879 |
+
"confidence_score": "0.85",
|
| 1880 |
+
"frame_count": 1,
|
| 1881 |
+
"scanned_at": "2026-02-19T23:02:03.079Z"
|
| 1882 |
+
}
|
| 1883 |
+
},
|
| 1884 |
+
{
|
| 1885 |
+
"model": "fitting_system.bodyscan",
|
| 1886 |
+
"pk": 23,
|
| 1887 |
+
"fields": {
|
| 1888 |
+
"session_id": "dc63f8d0-8a8d-4b94-bf0c-2bd7be121ed8",
|
| 1889 |
+
"height": "170.0",
|
| 1890 |
+
"shoulder_width": "35.1",
|
| 1891 |
+
"chest": "94.8",
|
| 1892 |
+
"waist": "50.0",
|
| 1893 |
+
"hip": "70.0",
|
| 1894 |
+
"torso_length": "53.5",
|
| 1895 |
+
"arm_length": "57.0",
|
| 1896 |
+
"inseam": "77.3",
|
| 1897 |
+
"body_shape": "rectangle",
|
| 1898 |
+
"skin_tone": "tan",
|
| 1899 |
+
"undertone": "warm",
|
| 1900 |
+
"confidence_score": "0.85",
|
| 1901 |
+
"frame_count": 1,
|
| 1902 |
+
"scanned_at": "2026-02-19T23:04:18.419Z"
|
| 1903 |
+
}
|
| 1904 |
+
},
|
| 1905 |
+
{
|
| 1906 |
+
"model": "fitting_system.bodyscan",
|
| 1907 |
+
"pk": 24,
|
| 1908 |
+
"fields": {
|
| 1909 |
+
"session_id": "87de428d-97cb-4ba9-87a4-bbaec8bd01af",
|
| 1910 |
+
"height": "170.0",
|
| 1911 |
+
"shoulder_width": "35.1",
|
| 1912 |
+
"chest": "94.8",
|
| 1913 |
+
"waist": "50.0",
|
| 1914 |
+
"hip": "70.0",
|
| 1915 |
+
"torso_length": "53.5",
|
| 1916 |
+
"arm_length": "57.0",
|
| 1917 |
+
"inseam": "77.3",
|
| 1918 |
+
"body_shape": "rectangle",
|
| 1919 |
+
"skin_tone": "tan",
|
| 1920 |
+
"undertone": "warm",
|
| 1921 |
+
"confidence_score": "0.85",
|
| 1922 |
+
"frame_count": 1,
|
| 1923 |
+
"scanned_at": "2026-02-19T23:06:24.972Z"
|
| 1924 |
+
}
|
| 1925 |
+
},
|
| 1926 |
+
{
|
| 1927 |
+
"model": "fitting_system.bodyscan",
|
| 1928 |
+
"pk": 25,
|
| 1929 |
+
"fields": {
|
| 1930 |
+
"session_id": "af5e6d11-0498-4a37-a2b3-a5f8c4326ff1",
|
| 1931 |
+
"height": "168.0",
|
| 1932 |
+
"shoulder_width": "34.7",
|
| 1933 |
+
"chest": "93.7",
|
| 1934 |
+
"waist": "50.0",
|
| 1935 |
+
"hip": "70.0",
|
| 1936 |
+
"torso_length": "52.9",
|
| 1937 |
+
"arm_length": "56.3",
|
| 1938 |
+
"inseam": "76.4",
|
| 1939 |
+
"body_shape": "rectangle",
|
| 1940 |
+
"skin_tone": "tan",
|
| 1941 |
+
"undertone": "warm",
|
| 1942 |
+
"confidence_score": "0.85",
|
| 1943 |
+
"frame_count": 1,
|
| 1944 |
+
"scanned_at": "2026-02-19T23:10:22.691Z"
|
| 1945 |
+
}
|
| 1946 |
+
},
|
| 1947 |
+
{
|
| 1948 |
+
"model": "fitting_system.bodyscan",
|
| 1949 |
+
"pk": 26,
|
| 1950 |
+
"fields": {
|
| 1951 |
+
"session_id": "0f54fb44-704c-44ce-a393-3099158bec13",
|
| 1952 |
+
"height": "168.0",
|
| 1953 |
+
"shoulder_width": "49.8",
|
| 1954 |
+
"chest": "134.5",
|
| 1955 |
+
"waist": "58.2",
|
| 1956 |
+
"hip": "89.6",
|
| 1957 |
+
"torso_length": "65.0",
|
| 1958 |
+
"arm_length": "73.5",
|
| 1959 |
+
"inseam": "55.0",
|
| 1960 |
+
"body_shape": "rectangle",
|
| 1961 |
+
"skin_tone": "dark",
|
| 1962 |
+
"undertone": "warm",
|
| 1963 |
+
"confidence_score": "0.85",
|
| 1964 |
+
"frame_count": 1,
|
| 1965 |
+
"scanned_at": "2026-02-19T23:12:04.827Z"
|
| 1966 |
+
}
|
| 1967 |
+
},
|
| 1968 |
+
{
|
| 1969 |
+
"model": "fitting_system.bodyscan",
|
| 1970 |
+
"pk": 27,
|
| 1971 |
+
"fields": {
|
| 1972 |
+
"session_id": "98fc9354-a31e-49ad-851a-30672ab249fe",
|
| 1973 |
+
"height": "168.0",
|
| 1974 |
+
"shoulder_width": "34.7",
|
| 1975 |
+
"chest": "93.7",
|
| 1976 |
+
"waist": "50.0",
|
| 1977 |
+
"hip": "70.0",
|
| 1978 |
+
"torso_length": "52.9",
|
| 1979 |
+
"arm_length": "56.3",
|
| 1980 |
+
"inseam": "76.4",
|
| 1981 |
+
"body_shape": "rectangle",
|
| 1982 |
+
"skin_tone": "tan",
|
| 1983 |
+
"undertone": "warm",
|
| 1984 |
+
"confidence_score": "0.85",
|
| 1985 |
+
"frame_count": 1,
|
| 1986 |
+
"scanned_at": "2026-02-19T23:16:36.664Z"
|
| 1987 |
+
}
|
| 1988 |
+
},
|
| 1989 |
+
{
|
| 1990 |
+
"model": "fitting_system.bodyscan",
|
| 1991 |
+
"pk": 28,
|
| 1992 |
+
"fields": {
|
| 1993 |
+
"session_id": "7904a6c6-b054-4ea5-8d2b-0bef4c591e1f",
|
| 1994 |
+
"height": "168.0",
|
| 1995 |
+
"shoulder_width": "34.7",
|
| 1996 |
+
"chest": "93.7",
|
| 1997 |
+
"waist": "50.0",
|
| 1998 |
+
"hip": "70.0",
|
| 1999 |
+
"torso_length": "52.9",
|
| 2000 |
+
"arm_length": "56.3",
|
| 2001 |
+
"inseam": "76.4",
|
| 2002 |
+
"body_shape": "rectangle",
|
| 2003 |
+
"skin_tone": "tan",
|
| 2004 |
+
"undertone": "warm",
|
| 2005 |
+
"confidence_score": "0.85",
|
| 2006 |
+
"frame_count": 1,
|
| 2007 |
+
"scanned_at": "2026-02-20T02:59:00.821Z"
|
| 2008 |
+
}
|
| 2009 |
+
},
|
| 2010 |
+
{
|
| 2011 |
+
"model": "fitting_system.bodyscan",
|
| 2012 |
+
"pk": 29,
|
| 2013 |
+
"fields": {
|
| 2014 |
+
"session_id": "30bcd50d-5a90-475a-bc96-df77fcf5bc7d",
|
| 2015 |
+
"height": "168.0",
|
| 2016 |
+
"shoulder_width": "49.8",
|
| 2017 |
+
"chest": "134.5",
|
| 2018 |
+
"waist": "58.2",
|
| 2019 |
+
"hip": "89.6",
|
| 2020 |
+
"torso_length": "65.0",
|
| 2021 |
+
"arm_length": "73.5",
|
| 2022 |
+
"inseam": "55.0",
|
| 2023 |
+
"body_shape": "rectangle",
|
| 2024 |
+
"skin_tone": "dark",
|
| 2025 |
+
"undertone": "warm",
|
| 2026 |
+
"confidence_score": "0.85",
|
| 2027 |
+
"frame_count": 1,
|
| 2028 |
+
"scanned_at": "2026-02-20T03:00:02.208Z"
|
| 2029 |
+
}
|
| 2030 |
+
},
|
| 2031 |
+
{
|
| 2032 |
+
"model": "fitting_system.bodyscan",
|
| 2033 |
+
"pk": 30,
|
| 2034 |
+
"fields": {
|
| 2035 |
+
"session_id": "e393cf5c-d399-42e5-86de-17876dbfe1c0",
|
| 2036 |
+
"height": "175.0",
|
| 2037 |
+
"shoulder_width": "33.3",
|
| 2038 |
+
"chest": "89.9",
|
| 2039 |
+
"waist": "50.0",
|
| 2040 |
+
"hip": "70.0",
|
| 2041 |
+
"torso_length": "53.9",
|
| 2042 |
+
"arm_length": "55.2",
|
| 2043 |
+
"inseam": "82.5",
|
| 2044 |
+
"body_shape": "rectangle",
|
| 2045 |
+
"skin_tone": "intermediate",
|
| 2046 |
+
"undertone": "warm",
|
| 2047 |
+
"confidence_score": "0.85",
|
| 2048 |
+
"frame_count": 1,
|
| 2049 |
+
"scanned_at": "2026-02-20T03:01:13.813Z"
|
| 2050 |
+
}
|
| 2051 |
+
},
|
| 2052 |
+
{
|
| 2053 |
+
"model": "fitting_system.bodyscan",
|
| 2054 |
+
"pk": 31,
|
| 2055 |
+
"fields": {
|
| 2056 |
+
"session_id": "7e268f77-cd7e-4607-abed-1cee4e8b3de9",
|
| 2057 |
+
"height": "168.0",
|
| 2058 |
+
"shoulder_width": "34.7",
|
| 2059 |
+
"chest": "93.7",
|
| 2060 |
+
"waist": "50.0",
|
| 2061 |
+
"hip": "70.0",
|
| 2062 |
+
"torso_length": "52.9",
|
| 2063 |
+
"arm_length": "56.3",
|
| 2064 |
+
"inseam": "76.4",
|
| 2065 |
+
"body_shape": "rectangle",
|
| 2066 |
+
"skin_tone": "tan",
|
| 2067 |
+
"undertone": "warm",
|
| 2068 |
+
"confidence_score": "0.85",
|
| 2069 |
+
"frame_count": 1,
|
| 2070 |
+
"scanned_at": "2026-02-20T11:12:41.178Z"
|
| 2071 |
+
}
|
| 2072 |
+
},
|
| 2073 |
+
{
|
| 2074 |
+
"model": "fitting_system.bodyscan",
|
| 2075 |
+
"pk": 32,
|
| 2076 |
+
"fields": {
|
| 2077 |
+
"session_id": "225d422d-2a38-4216-8310-4ce6b19b016d",
|
| 2078 |
+
"height": "168.0",
|
| 2079 |
+
"shoulder_width": "34.7",
|
| 2080 |
+
"chest": "93.7",
|
| 2081 |
+
"waist": "50.0",
|
| 2082 |
+
"hip": "70.0",
|
| 2083 |
+
"torso_length": "52.9",
|
| 2084 |
+
"arm_length": "56.3",
|
| 2085 |
+
"inseam": "76.4",
|
| 2086 |
+
"body_shape": "rectangle",
|
| 2087 |
+
"skin_tone": "tan",
|
| 2088 |
+
"undertone": "warm",
|
| 2089 |
+
"confidence_score": "0.85",
|
| 2090 |
+
"frame_count": 1,
|
| 2091 |
+
"scanned_at": "2026-02-21T11:53:24.689Z"
|
| 2092 |
+
}
|
| 2093 |
+
},
|
| 2094 |
+
{
|
| 2095 |
+
"model": "fitting_system.bodyscan",
|
| 2096 |
+
"pk": 33,
|
| 2097 |
+
"fields": {
|
| 2098 |
+
"session_id": "d0419c48-5d9a-40c1-b428-80eed8d6a946",
|
| 2099 |
+
"height": "168.0",
|
| 2100 |
+
"shoulder_width": "34.7",
|
| 2101 |
+
"chest": "93.7",
|
| 2102 |
+
"waist": "50.0",
|
| 2103 |
+
"hip": "70.0",
|
| 2104 |
+
"torso_length": "52.9",
|
| 2105 |
+
"arm_length": "56.3",
|
| 2106 |
+
"inseam": "76.4",
|
| 2107 |
+
"body_shape": "rectangle",
|
| 2108 |
+
"skin_tone": "tan",
|
| 2109 |
+
"undertone": "warm",
|
| 2110 |
+
"confidence_score": "0.85",
|
| 2111 |
+
"frame_count": 1,
|
| 2112 |
+
"scanned_at": "2026-02-22T00:54:22.521Z"
|
| 2113 |
+
}
|
| 2114 |
+
},
|
| 2115 |
+
{
|
| 2116 |
+
"model": "fitting_system.bodyscan",
|
| 2117 |
+
"pk": 34,
|
| 2118 |
+
"fields": {
|
| 2119 |
+
"session_id": "18b1062b-f8ab-47f3-95ba-d67c830d308a",
|
| 2120 |
+
"height": "168.0",
|
| 2121 |
+
"shoulder_width": "37.0",
|
| 2122 |
+
"chest": "99.9",
|
| 2123 |
+
"waist": "50.0",
|
| 2124 |
+
"hip": "70.0",
|
| 2125 |
+
"torso_length": "65.0",
|
| 2126 |
+
"arm_length": "80.0",
|
| 2127 |
+
"inseam": "74.0",
|
| 2128 |
+
"body_shape": "rectangle",
|
| 2129 |
+
"skin_tone": "dark",
|
| 2130 |
+
"undertone": "warm",
|
| 2131 |
+
"confidence_score": "0.85",
|
| 2132 |
+
"frame_count": 1,
|
| 2133 |
+
"scanned_at": "2026-02-22T02:34:25.193Z"
|
| 2134 |
+
}
|
| 2135 |
+
},
|
| 2136 |
+
{
|
| 2137 |
+
"model": "fitting_system.bodyscan",
|
| 2138 |
+
"pk": 35,
|
| 2139 |
+
"fields": {
|
| 2140 |
+
"session_id": "e61d5372-5c1c-4897-920a-e6bc505872f7",
|
| 2141 |
+
"height": "168.0",
|
| 2142 |
+
"shoulder_width": "34.7",
|
| 2143 |
+
"chest": "93.7",
|
| 2144 |
+
"waist": "50.0",
|
| 2145 |
+
"hip": "70.0",
|
| 2146 |
+
"torso_length": "52.9",
|
| 2147 |
+
"arm_length": "56.3",
|
| 2148 |
+
"inseam": "76.4",
|
| 2149 |
+
"body_shape": "rectangle",
|
| 2150 |
+
"skin_tone": "tan",
|
| 2151 |
+
"undertone": "warm",
|
| 2152 |
+
"confidence_score": "0.85",
|
| 2153 |
+
"frame_count": 1,
|
| 2154 |
+
"scanned_at": "2026-02-22T02:35:31.856Z"
|
| 2155 |
+
}
|
| 2156 |
+
},
|
| 2157 |
+
{
|
| 2158 |
+
"model": "fitting_system.bodyscan",
|
| 2159 |
+
"pk": 36,
|
| 2160 |
+
"fields": {
|
| 2161 |
+
"session_id": "7fd040ef-fa6c-48b7-aeea-7564e765bc3b",
|
| 2162 |
+
"height": "168.0",
|
| 2163 |
+
"shoulder_width": "34.7",
|
| 2164 |
+
"chest": "93.7",
|
| 2165 |
+
"waist": "50.0",
|
| 2166 |
+
"hip": "70.0",
|
| 2167 |
+
"torso_length": "52.9",
|
| 2168 |
+
"arm_length": "56.3",
|
| 2169 |
+
"inseam": "76.4",
|
| 2170 |
+
"body_shape": "rectangle",
|
| 2171 |
+
"skin_tone": "tan",
|
| 2172 |
+
"undertone": "warm",
|
| 2173 |
+
"confidence_score": "0.85",
|
| 2174 |
+
"frame_count": 1,
|
| 2175 |
+
"scanned_at": "2026-02-22T02:58:39.041Z"
|
| 2176 |
+
}
|
| 2177 |
+
},
|
| 2178 |
+
{
|
| 2179 |
+
"model": "fitting_system.bodyscan",
|
| 2180 |
+
"pk": 37,
|
| 2181 |
+
"fields": {
|
| 2182 |
+
"session_id": "1850417a-e56e-4ac2-95d3-8d4eed56fb80",
|
| 2183 |
+
"height": "168.0",
|
| 2184 |
+
"shoulder_width": "34.7",
|
| 2185 |
+
"chest": "93.7",
|
| 2186 |
+
"waist": "50.0",
|
| 2187 |
+
"hip": "70.0",
|
| 2188 |
+
"torso_length": "52.9",
|
| 2189 |
+
"arm_length": "56.3",
|
| 2190 |
+
"inseam": "76.4",
|
| 2191 |
+
"body_shape": "rectangle",
|
| 2192 |
+
"skin_tone": "tan",
|
| 2193 |
+
"undertone": "warm",
|
| 2194 |
+
"confidence_score": "0.85",
|
| 2195 |
+
"frame_count": 1,
|
| 2196 |
+
"scanned_at": "2026-02-22T03:46:04.991Z"
|
| 2197 |
+
}
|
| 2198 |
+
},
|
| 2199 |
+
{
|
| 2200 |
+
"model": "fitting_system.bodyscan",
|
| 2201 |
+
"pk": 38,
|
| 2202 |
+
"fields": {
|
| 2203 |
+
"session_id": "764f7019-2831-4581-811a-145493ca35d0",
|
| 2204 |
+
"height": "168.0",
|
| 2205 |
+
"shoulder_width": "34.7",
|
| 2206 |
+
"chest": "93.7",
|
| 2207 |
+
"waist": "50.0",
|
| 2208 |
+
"hip": "70.0",
|
| 2209 |
+
"torso_length": "52.9",
|
| 2210 |
+
"arm_length": "56.3",
|
| 2211 |
+
"inseam": "76.4",
|
| 2212 |
+
"body_shape": "rectangle",
|
| 2213 |
+
"skin_tone": "tan",
|
| 2214 |
+
"undertone": "warm",
|
| 2215 |
+
"confidence_score": "0.85",
|
| 2216 |
+
"frame_count": 1,
|
| 2217 |
+
"scanned_at": "2026-02-22T22:17:11.244Z"
|
| 2218 |
+
}
|
| 2219 |
+
},
|
| 2220 |
+
{
|
| 2221 |
+
"model": "fitting_system.bodyscan",
|
| 2222 |
+
"pk": 39,
|
| 2223 |
+
"fields": {
|
| 2224 |
+
"session_id": "7a00d475-6812-44ae-aba6-0f237d1b2595",
|
| 2225 |
+
"height": "168.0",
|
| 2226 |
+
"shoulder_width": "34.7",
|
| 2227 |
+
"chest": "93.7",
|
| 2228 |
+
"waist": "50.0",
|
| 2229 |
+
"hip": "70.0",
|
| 2230 |
+
"torso_length": "52.9",
|
| 2231 |
+
"arm_length": "56.3",
|
| 2232 |
+
"inseam": "76.4",
|
| 2233 |
+
"body_shape": "rectangle",
|
| 2234 |
+
"skin_tone": "tan",
|
| 2235 |
+
"undertone": "warm",
|
| 2236 |
+
"confidence_score": "0.85",
|
| 2237 |
+
"frame_count": 1,
|
| 2238 |
+
"scanned_at": "2026-02-22T22:26:46.257Z"
|
| 2239 |
+
}
|
| 2240 |
+
},
|
| 2241 |
+
{
|
| 2242 |
+
"model": "fitting_system.bodyscan",
|
| 2243 |
+
"pk": 40,
|
| 2244 |
+
"fields": {
|
| 2245 |
+
"session_id": "2858afe3-d307-435a-a515-e4d3aa81e3aa",
|
| 2246 |
+
"height": "168.0",
|
| 2247 |
+
"shoulder_width": "34.7",
|
| 2248 |
+
"chest": "93.7",
|
| 2249 |
+
"waist": "50.0",
|
| 2250 |
+
"hip": "70.0",
|
| 2251 |
+
"torso_length": "52.9",
|
| 2252 |
+
"arm_length": "56.3",
|
| 2253 |
+
"inseam": "76.4",
|
| 2254 |
+
"body_shape": "rectangle",
|
| 2255 |
+
"skin_tone": "tan",
|
| 2256 |
+
"undertone": "warm",
|
| 2257 |
+
"confidence_score": "0.85",
|
| 2258 |
+
"frame_count": 1,
|
| 2259 |
+
"scanned_at": "2026-02-22T22:29:23.652Z"
|
| 2260 |
+
}
|
| 2261 |
+
},
|
| 2262 |
+
{
|
| 2263 |
+
"model": "fitting_system.bodyscan",
|
| 2264 |
+
"pk": 41,
|
| 2265 |
+
"fields": {
|
| 2266 |
+
"session_id": "b7324911-f11e-4570-b5b3-b17e25e4274b",
|
| 2267 |
+
"height": "168.0",
|
| 2268 |
+
"shoulder_width": "34.7",
|
| 2269 |
+
"chest": "93.7",
|
| 2270 |
+
"waist": "50.0",
|
| 2271 |
+
"hip": "70.0",
|
| 2272 |
+
"torso_length": "52.9",
|
| 2273 |
+
"arm_length": "56.3",
|
| 2274 |
+
"inseam": "76.4",
|
| 2275 |
+
"body_shape": "rectangle",
|
| 2276 |
+
"skin_tone": "tan",
|
| 2277 |
+
"undertone": "warm",
|
| 2278 |
+
"confidence_score": "0.85",
|
| 2279 |
+
"frame_count": 1,
|
| 2280 |
+
"scanned_at": "2026-02-22T22:29:35.149Z"
|
| 2281 |
+
}
|
| 2282 |
+
},
|
| 2283 |
+
{
|
| 2284 |
+
"model": "fitting_system.bodyscan",
|
| 2285 |
+
"pk": 42,
|
| 2286 |
+
"fields": {
|
| 2287 |
+
"session_id": "1afb5bd6-4b7c-4c20-9bde-72a12da6a86e",
|
| 2288 |
+
"height": "166.0",
|
| 2289 |
+
"shoulder_width": "34.3",
|
| 2290 |
+
"chest": "92.6",
|
| 2291 |
+
"waist": "50.0",
|
| 2292 |
+
"hip": "70.0",
|
| 2293 |
+
"torso_length": "52.3",
|
| 2294 |
+
"arm_length": "55.6",
|
| 2295 |
+
"inseam": "75.5",
|
| 2296 |
+
"body_shape": "rectangle",
|
| 2297 |
+
"skin_tone": "tan",
|
| 2298 |
+
"undertone": "warm",
|
| 2299 |
+
"confidence_score": "0.85",
|
| 2300 |
+
"frame_count": 1,
|
| 2301 |
+
"scanned_at": "2026-02-22T22:44:10.600Z"
|
| 2302 |
+
}
|
| 2303 |
+
},
|
| 2304 |
+
{
|
| 2305 |
+
"model": "fitting_system.bodyscan",
|
| 2306 |
+
"pk": 43,
|
| 2307 |
+
"fields": {
|
| 2308 |
+
"session_id": "a0d239b6-d244-444c-8f93-fc7fa1924e0f",
|
| 2309 |
+
"height": "168.0",
|
| 2310 |
+
"shoulder_width": "49.8",
|
| 2311 |
+
"chest": "134.5",
|
| 2312 |
+
"waist": "58.2",
|
| 2313 |
+
"hip": "89.6",
|
| 2314 |
+
"torso_length": "65.0",
|
| 2315 |
+
"arm_length": "73.5",
|
| 2316 |
+
"inseam": "55.0",
|
| 2317 |
+
"body_shape": "rectangle",
|
| 2318 |
+
"skin_tone": "dark",
|
| 2319 |
+
"undertone": "warm",
|
| 2320 |
+
"confidence_score": "0.85",
|
| 2321 |
+
"frame_count": 1,
|
| 2322 |
+
"scanned_at": "2026-02-24T01:31:55.964Z"
|
| 2323 |
+
}
|
| 2324 |
+
},
|
| 2325 |
+
{
|
| 2326 |
+
"model": "fitting_system.bodyscan",
|
| 2327 |
+
"pk": 44,
|
| 2328 |
+
"fields": {
|
| 2329 |
+
"session_id": "375e767a-6a50-4c02-a17f-f75f562a9afa",
|
| 2330 |
+
"height": "168.0",
|
| 2331 |
+
"shoulder_width": "34.7",
|
| 2332 |
+
"chest": "93.7",
|
| 2333 |
+
"waist": "50.0",
|
| 2334 |
+
"hip": "70.0",
|
| 2335 |
+
"torso_length": "52.9",
|
| 2336 |
+
"arm_length": "56.3",
|
| 2337 |
+
"inseam": "76.4",
|
| 2338 |
+
"body_shape": "rectangle",
|
| 2339 |
+
"skin_tone": "dark",
|
| 2340 |
+
"undertone": "warm",
|
| 2341 |
+
"confidence_score": "0.85",
|
| 2342 |
+
"frame_count": 1,
|
| 2343 |
+
"scanned_at": "2026-02-24T01:40:21.540Z"
|
| 2344 |
+
}
|
| 2345 |
+
},
|
| 2346 |
+
{
|
| 2347 |
+
"model": "fitting_system.bodyscan",
|
| 2348 |
+
"pk": 45,
|
| 2349 |
+
"fields": {
|
| 2350 |
+
"session_id": "aa09f159-e1d3-4979-abb7-483e225a1c7d",
|
| 2351 |
+
"height": "168.0",
|
| 2352 |
+
"shoulder_width": "34.7",
|
| 2353 |
+
"chest": "93.7",
|
| 2354 |
+
"waist": "50.0",
|
| 2355 |
+
"hip": "70.0",
|
| 2356 |
+
"torso_length": "52.9",
|
| 2357 |
+
"arm_length": "56.3",
|
| 2358 |
+
"inseam": "76.4",
|
| 2359 |
+
"body_shape": "rectangle",
|
| 2360 |
+
"skin_tone": "tan",
|
| 2361 |
+
"undertone": "warm",
|
| 2362 |
+
"confidence_score": "0.85",
|
| 2363 |
+
"frame_count": 1,
|
| 2364 |
+
"scanned_at": "2026-02-24T01:46:04.824Z"
|
| 2365 |
+
}
|
| 2366 |
+
},
|
| 2367 |
+
{
|
| 2368 |
+
"model": "fitting_system.bodyscan",
|
| 2369 |
+
"pk": 46,
|
| 2370 |
+
"fields": {
|
| 2371 |
+
"session_id": "b693fff5-80be-4334-9107-98871a9aa60d",
|
| 2372 |
+
"height": "168.0",
|
| 2373 |
+
"shoulder_width": "49.8",
|
| 2374 |
+
"chest": "134.5",
|
| 2375 |
+
"waist": "58.2",
|
| 2376 |
+
"hip": "89.6",
|
| 2377 |
+
"torso_length": "65.0",
|
| 2378 |
+
"arm_length": "73.5",
|
| 2379 |
+
"inseam": "55.0",
|
| 2380 |
+
"body_shape": "rectangle",
|
| 2381 |
+
"skin_tone": "dark",
|
| 2382 |
+
"undertone": "warm",
|
| 2383 |
+
"confidence_score": "0.85",
|
| 2384 |
+
"frame_count": 1,
|
| 2385 |
+
"scanned_at": "2026-02-24T01:50:58.709Z"
|
| 2386 |
+
}
|
| 2387 |
+
},
|
| 2388 |
+
{
|
| 2389 |
+
"model": "fitting_system.bodyscan",
|
| 2390 |
+
"pk": 47,
|
| 2391 |
+
"fields": {
|
| 2392 |
+
"session_id": "38bcb0c4-9d5c-465c-a7e3-075c5c3d4336",
|
| 2393 |
+
"height": "168.0",
|
| 2394 |
+
"shoulder_width": "34.7",
|
| 2395 |
+
"chest": "93.7",
|
| 2396 |
+
"waist": "50.0",
|
| 2397 |
+
"hip": "70.0",
|
| 2398 |
+
"torso_length": "52.9",
|
| 2399 |
+
"arm_length": "56.3",
|
| 2400 |
+
"inseam": "76.4",
|
| 2401 |
+
"body_shape": "rectangle",
|
| 2402 |
+
"skin_tone": "tan",
|
| 2403 |
+
"undertone": "warm",
|
| 2404 |
+
"confidence_score": "0.85",
|
| 2405 |
+
"frame_count": 1,
|
| 2406 |
+
"scanned_at": "2026-02-24T02:07:30.646Z"
|
| 2407 |
+
}
|
| 2408 |
+
},
|
| 2409 |
+
{
|
| 2410 |
+
"model": "fitting_system.bodyscan",
|
| 2411 |
+
"pk": 48,
|
| 2412 |
+
"fields": {
|
| 2413 |
+
"session_id": "5b1d9af9-b417-4d94-ba83-43221f50d3dd",
|
| 2414 |
+
"height": "168.0",
|
| 2415 |
+
"shoulder_width": "34.7",
|
| 2416 |
+
"chest": "93.7",
|
| 2417 |
+
"waist": "50.0",
|
| 2418 |
+
"hip": "70.0",
|
| 2419 |
+
"torso_length": "52.9",
|
| 2420 |
+
"arm_length": "56.3",
|
| 2421 |
+
"inseam": "76.4",
|
| 2422 |
+
"body_shape": "rectangle",
|
| 2423 |
+
"skin_tone": "dark",
|
| 2424 |
+
"undertone": "warm",
|
| 2425 |
+
"confidence_score": "0.85",
|
| 2426 |
+
"frame_count": 1,
|
| 2427 |
+
"scanned_at": "2026-02-24T02:15:21.355Z"
|
| 2428 |
+
}
|
| 2429 |
+
},
|
| 2430 |
+
{
|
| 2431 |
+
"model": "fitting_system.bodyscan",
|
| 2432 |
+
"pk": 49,
|
| 2433 |
+
"fields": {
|
| 2434 |
+
"session_id": "c58a1c13-5b46-4ad2-8f83-14bbf075f04f",
|
| 2435 |
+
"height": "168.0",
|
| 2436 |
+
"shoulder_width": "34.7",
|
| 2437 |
+
"chest": "93.7",
|
| 2438 |
+
"waist": "50.0",
|
| 2439 |
+
"hip": "70.0",
|
| 2440 |
+
"torso_length": "52.9",
|
| 2441 |
+
"arm_length": "56.3",
|
| 2442 |
+
"inseam": "76.4",
|
| 2443 |
+
"body_shape": "rectangle",
|
| 2444 |
+
"skin_tone": "tan",
|
| 2445 |
+
"undertone": "warm",
|
| 2446 |
+
"confidence_score": "0.85",
|
| 2447 |
+
"frame_count": 1,
|
| 2448 |
+
"scanned_at": "2026-02-24T20:52:09.926Z"
|
| 2449 |
+
}
|
| 2450 |
+
},
|
| 2451 |
+
{
|
| 2452 |
+
"model": "fitting_system.recommendation",
|
| 2453 |
+
"pk": 1,
|
| 2454 |
+
"fields": {
|
| 2455 |
+
"body_scan": 1,
|
| 2456 |
+
"product": 2,
|
| 2457 |
+
"recommended_size": "M",
|
| 2458 |
+
"recommended_fit": "regular",
|
| 2459 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2460 |
+
"priority": 40,
|
| 2461 |
+
"created_at": "2026-02-14T12:37:36.057Z"
|
| 2462 |
+
}
|
| 2463 |
+
},
|
| 2464 |
+
{
|
| 2465 |
+
"model": "fitting_system.recommendation",
|
| 2466 |
+
"pk": 2,
|
| 2467 |
+
"fields": {
|
| 2468 |
+
"body_scan": 1,
|
| 2469 |
+
"product": 3,
|
| 2470 |
+
"recommended_size": "M",
|
| 2471 |
+
"recommended_fit": "regular",
|
| 2472 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2473 |
+
"priority": 40,
|
| 2474 |
+
"created_at": "2026-02-14T12:37:36.160Z"
|
| 2475 |
+
}
|
| 2476 |
+
},
|
| 2477 |
+
{
|
| 2478 |
+
"model": "fitting_system.recommendation",
|
| 2479 |
+
"pk": 3,
|
| 2480 |
+
"fields": {
|
| 2481 |
+
"body_scan": 1,
|
| 2482 |
+
"product": 4,
|
| 2483 |
+
"recommended_size": "M",
|
| 2484 |
+
"recommended_fit": "regular",
|
| 2485 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2486 |
+
"priority": 40,
|
| 2487 |
+
"created_at": "2026-02-14T12:37:36.332Z"
|
| 2488 |
+
}
|
| 2489 |
+
},
|
| 2490 |
+
{
|
| 2491 |
+
"model": "fitting_system.recommendation",
|
| 2492 |
+
"pk": 4,
|
| 2493 |
+
"fields": {
|
| 2494 |
+
"body_scan": 1,
|
| 2495 |
+
"product": 6,
|
| 2496 |
+
"recommended_size": "M",
|
| 2497 |
+
"recommended_fit": "regular",
|
| 2498 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2499 |
+
"priority": 40,
|
| 2500 |
+
"created_at": "2026-02-14T12:37:36.435Z"
|
| 2501 |
+
}
|
| 2502 |
+
},
|
| 2503 |
+
{
|
| 2504 |
+
"model": "fitting_system.recommendation",
|
| 2505 |
+
"pk": 5,
|
| 2506 |
+
"fields": {
|
| 2507 |
+
"body_scan": 1,
|
| 2508 |
+
"product": 5,
|
| 2509 |
+
"recommended_size": "M",
|
| 2510 |
+
"recommended_fit": "regular",
|
| 2511 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2512 |
+
"priority": 40,
|
| 2513 |
+
"created_at": "2026-02-14T12:37:36.594Z"
|
| 2514 |
+
}
|
| 2515 |
+
},
|
| 2516 |
+
{
|
| 2517 |
+
"model": "fitting_system.recommendation",
|
| 2518 |
+
"pk": 6,
|
| 2519 |
+
"fields": {
|
| 2520 |
+
"body_scan": 2,
|
| 2521 |
+
"product": 2,
|
| 2522 |
+
"recommended_size": "M",
|
| 2523 |
+
"recommended_fit": "regular",
|
| 2524 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2525 |
+
"priority": 40,
|
| 2526 |
+
"created_at": "2026-02-14T12:44:18.766Z"
|
| 2527 |
+
}
|
| 2528 |
+
},
|
| 2529 |
+
{
|
| 2530 |
+
"model": "fitting_system.recommendation",
|
| 2531 |
+
"pk": 7,
|
| 2532 |
+
"fields": {
|
| 2533 |
+
"body_scan": 2,
|
| 2534 |
+
"product": 3,
|
| 2535 |
+
"recommended_size": "M",
|
| 2536 |
+
"recommended_fit": "regular",
|
| 2537 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2538 |
+
"priority": 40,
|
| 2539 |
+
"created_at": "2026-02-14T12:44:18.923Z"
|
| 2540 |
+
}
|
| 2541 |
+
},
|
| 2542 |
+
{
|
| 2543 |
+
"model": "fitting_system.recommendation",
|
| 2544 |
+
"pk": 8,
|
| 2545 |
+
"fields": {
|
| 2546 |
+
"body_scan": 2,
|
| 2547 |
+
"product": 4,
|
| 2548 |
+
"recommended_size": "M",
|
| 2549 |
+
"recommended_fit": "regular",
|
| 2550 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2551 |
+
"priority": 40,
|
| 2552 |
+
"created_at": "2026-02-14T12:44:19.030Z"
|
| 2553 |
+
}
|
| 2554 |
+
},
|
| 2555 |
+
{
|
| 2556 |
+
"model": "fitting_system.recommendation",
|
| 2557 |
+
"pk": 9,
|
| 2558 |
+
"fields": {
|
| 2559 |
+
"body_scan": 2,
|
| 2560 |
+
"product": 6,
|
| 2561 |
+
"recommended_size": "M",
|
| 2562 |
+
"recommended_fit": "regular",
|
| 2563 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2564 |
+
"priority": 40,
|
| 2565 |
+
"created_at": "2026-02-14T12:44:19.205Z"
|
| 2566 |
+
}
|
| 2567 |
+
},
|
| 2568 |
+
{
|
| 2569 |
+
"model": "fitting_system.recommendation",
|
| 2570 |
+
"pk": 10,
|
| 2571 |
+
"fields": {
|
| 2572 |
+
"body_scan": 2,
|
| 2573 |
+
"product": 5,
|
| 2574 |
+
"recommended_size": "M",
|
| 2575 |
+
"recommended_fit": "regular",
|
| 2576 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2577 |
+
"priority": 40,
|
| 2578 |
+
"created_at": "2026-02-14T12:44:19.310Z"
|
| 2579 |
+
}
|
| 2580 |
+
},
|
| 2581 |
+
{
|
| 2582 |
+
"model": "fitting_system.recommendation",
|
| 2583 |
+
"pk": 11,
|
| 2584 |
+
"fields": {
|
| 2585 |
+
"body_scan": 3,
|
| 2586 |
+
"product": 2,
|
| 2587 |
+
"recommended_size": "M",
|
| 2588 |
+
"recommended_fit": "regular",
|
| 2589 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2590 |
+
"priority": 40,
|
| 2591 |
+
"created_at": "2026-02-16T09:42:42.495Z"
|
| 2592 |
+
}
|
| 2593 |
+
},
|
| 2594 |
+
{
|
| 2595 |
+
"model": "fitting_system.recommendation",
|
| 2596 |
+
"pk": 12,
|
| 2597 |
+
"fields": {
|
| 2598 |
+
"body_scan": 3,
|
| 2599 |
+
"product": 3,
|
| 2600 |
+
"recommended_size": "M",
|
| 2601 |
+
"recommended_fit": "regular",
|
| 2602 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2603 |
+
"priority": 40,
|
| 2604 |
+
"created_at": "2026-02-16T09:42:42.640Z"
|
| 2605 |
+
}
|
| 2606 |
+
},
|
| 2607 |
+
{
|
| 2608 |
+
"model": "fitting_system.recommendation",
|
| 2609 |
+
"pk": 13,
|
| 2610 |
+
"fields": {
|
| 2611 |
+
"body_scan": 3,
|
| 2612 |
+
"product": 4,
|
| 2613 |
+
"recommended_size": "M",
|
| 2614 |
+
"recommended_fit": "regular",
|
| 2615 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2616 |
+
"priority": 40,
|
| 2617 |
+
"created_at": "2026-02-16T09:42:43.085Z"
|
| 2618 |
+
}
|
| 2619 |
+
},
|
| 2620 |
+
{
|
| 2621 |
+
"model": "fitting_system.recommendation",
|
| 2622 |
+
"pk": 14,
|
| 2623 |
+
"fields": {
|
| 2624 |
+
"body_scan": 3,
|
| 2625 |
+
"product": 6,
|
| 2626 |
+
"recommended_size": "M",
|
| 2627 |
+
"recommended_fit": "regular",
|
| 2628 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2629 |
+
"priority": 40,
|
| 2630 |
+
"created_at": "2026-02-16T09:42:43.200Z"
|
| 2631 |
+
}
|
| 2632 |
+
},
|
| 2633 |
+
{
|
| 2634 |
+
"model": "fitting_system.recommendation",
|
| 2635 |
+
"pk": 15,
|
| 2636 |
+
"fields": {
|
| 2637 |
+
"body_scan": 3,
|
| 2638 |
+
"product": 5,
|
| 2639 |
+
"recommended_size": "M",
|
| 2640 |
+
"recommended_fit": "regular",
|
| 2641 |
+
"recommended_colors": "Earth Brown, Olive Green, Burgundy, Mustard Yellow, Terracotta",
|
| 2642 |
+
"priority": 40,
|
| 2643 |
+
"created_at": "2026-02-16T09:42:43.325Z"
|
| 2644 |
+
}
|
| 2645 |
+
},
|
| 2646 |
+
{
|
| 2647 |
+
"model": "fitting_system.recommendation",
|
| 2648 |
+
"pk": 16,
|
| 2649 |
+
"fields": {
|
| 2650 |
+
"body_scan": 4,
|
| 2651 |
+
"product": 2,
|
| 2652 |
+
"recommended_size": "M",
|
| 2653 |
+
"recommended_fit": "regular",
|
| 2654 |
+
"recommended_colors": "Olive Green, Mustard Yellow, Terracotta, Rust Orange, Warm Beige",
|
| 2655 |
+
"priority": 30,
|
| 2656 |
+
"created_at": "2026-02-18T13:06:43.031Z"
|
| 2657 |
+
}
|
| 2658 |
+
},
|
| 2659 |
+
{
|
| 2660 |
+
"model": "fitting_system.recommendation",
|
| 2661 |
+
"pk": 17,
|
| 2662 |
+
"fields": {
|
| 2663 |
+
"body_scan": 4,
|
| 2664 |
+
"product": 3,
|
| 2665 |
+
"recommended_size": "M",
|
| 2666 |
+
"recommended_fit": "regular",
|
| 2667 |
+
"recommended_colors": "Olive Green, Mustard Yellow, Terracotta, Rust Orange, Warm Beige",
|
| 2668 |
+
"priority": 30,
|
| 2669 |
+
"created_at": "2026-02-18T13:06:44.707Z"
|
| 2670 |
+
}
|
| 2671 |
+
},
|
| 2672 |
+
{
|
| 2673 |
+
"model": "fitting_system.recommendation",
|
| 2674 |
+
"pk": 18,
|
| 2675 |
+
"fields": {
|
| 2676 |
+
"body_scan": 4,
|
| 2677 |
+
"product": 4,
|
| 2678 |
+
"recommended_size": "M",
|
| 2679 |
+
"recommended_fit": "regular",
|
| 2680 |
+
"recommended_colors": "Olive Green, Mustard Yellow, Terracotta, Rust Orange, Warm Beige",
|
| 2681 |
+
"priority": 30,
|
| 2682 |
+
"created_at": "2026-02-18T13:06:45.609Z"
|
| 2683 |
+
}
|
| 2684 |
+
},
|
| 2685 |
+
{
|
| 2686 |
+
"model": "fitting_system.recommendation",
|
| 2687 |
+
"pk": 19,
|
| 2688 |
+
"fields": {
|
| 2689 |
+
"body_scan": 4,
|
| 2690 |
+
"product": 6,
|
| 2691 |
+
"recommended_size": "M",
|
| 2692 |
+
"recommended_fit": "regular",
|
| 2693 |
+
"recommended_colors": "Olive Green, Mustard Yellow, Terracotta, Rust Orange, Warm Beige",
|
| 2694 |
+
"priority": 30,
|
| 2695 |
+
"created_at": "2026-02-18T13:06:47.230Z"
|
| 2696 |
+
}
|
| 2697 |
+
},
|
| 2698 |
+
{
|
| 2699 |
+
"model": "fitting_system.recommendation",
|
| 2700 |
+
"pk": 20,
|
| 2701 |
+
"fields": {
|
| 2702 |
+
"body_scan": 4,
|
| 2703 |
+
"product": 5,
|
| 2704 |
+
"recommended_size": "M",
|
| 2705 |
+
"recommended_fit": "regular",
|
| 2706 |
+
"recommended_colors": "Olive Green, Mustard Yellow, Terracotta, Rust Orange, Warm Beige",
|
| 2707 |
+
"priority": 30,
|
| 2708 |
+
"created_at": "2026-02-18T13:06:48.207Z"
|
| 2709 |
+
}
|
| 2710 |
+
},
|
| 2711 |
+
{
|
| 2712 |
+
"model": "fitting_system.recommendation",
|
| 2713 |
+
"pk": 21,
|
| 2714 |
+
"fields": {
|
| 2715 |
+
"body_scan": 5,
|
| 2716 |
+
"product": 2,
|
| 2717 |
+
"recommended_size": "M",
|
| 2718 |
+
"recommended_fit": "regular",
|
| 2719 |
+
"recommended_colors": "",
|
| 2720 |
+
"priority": 100,
|
| 2721 |
+
"created_at": "2026-02-18T15:00:02.365Z"
|
| 2722 |
+
}
|
| 2723 |
+
},
|
| 2724 |
+
{
|
| 2725 |
+
"model": "fitting_system.recommendation",
|
| 2726 |
+
"pk": 22,
|
| 2727 |
+
"fields": {
|
| 2728 |
+
"body_scan": 6,
|
| 2729 |
+
"product": 2,
|
| 2730 |
+
"recommended_size": "M",
|
| 2731 |
+
"recommended_fit": "regular",
|
| 2732 |
+
"recommended_colors": "",
|
| 2733 |
+
"priority": 100,
|
| 2734 |
+
"created_at": "2026-02-18T15:00:46.565Z"
|
| 2735 |
+
}
|
| 2736 |
+
},
|
| 2737 |
+
{
|
| 2738 |
+
"model": "fitting_system.recommendation",
|
| 2739 |
+
"pk": 23,
|
| 2740 |
+
"fields": {
|
| 2741 |
+
"body_scan": 7,
|
| 2742 |
+
"product": 2,
|
| 2743 |
+
"recommended_size": "XS",
|
| 2744 |
+
"recommended_fit": "regular",
|
| 2745 |
+
"recommended_colors": "",
|
| 2746 |
+
"priority": 100,
|
| 2747 |
+
"created_at": "2026-02-19T20:23:48.852Z"
|
| 2748 |
+
}
|
| 2749 |
+
},
|
| 2750 |
+
{
|
| 2751 |
+
"model": "fitting_system.recommendation",
|
| 2752 |
+
"pk": 24,
|
| 2753 |
+
"fields": {
|
| 2754 |
+
"body_scan": 8,
|
| 2755 |
+
"product": 2,
|
| 2756 |
+
"recommended_size": "XS",
|
| 2757 |
+
"recommended_fit": "regular",
|
| 2758 |
+
"recommended_colors": "",
|
| 2759 |
+
"priority": 100,
|
| 2760 |
+
"created_at": "2026-02-19T20:24:38.215Z"
|
| 2761 |
+
}
|
| 2762 |
+
},
|
| 2763 |
+
{
|
| 2764 |
+
"model": "fitting_system.recommendation",
|
| 2765 |
+
"pk": 25,
|
| 2766 |
+
"fields": {
|
| 2767 |
+
"body_scan": 9,
|
| 2768 |
+
"product": 2,
|
| 2769 |
+
"recommended_size": "XS",
|
| 2770 |
+
"recommended_fit": "regular",
|
| 2771 |
+
"recommended_colors": "",
|
| 2772 |
+
"priority": 100,
|
| 2773 |
+
"created_at": "2026-02-19T20:25:11.410Z"
|
| 2774 |
+
}
|
| 2775 |
+
},
|
| 2776 |
+
{
|
| 2777 |
+
"model": "fitting_system.recommendation",
|
| 2778 |
+
"pk": 26,
|
| 2779 |
+
"fields": {
|
| 2780 |
+
"body_scan": 10,
|
| 2781 |
+
"product": 2,
|
| 2782 |
+
"recommended_size": "XL",
|
| 2783 |
+
"recommended_fit": "regular",
|
| 2784 |
+
"recommended_colors": "",
|
| 2785 |
+
"priority": 100,
|
| 2786 |
+
"created_at": "2026-02-19T21:14:29.635Z"
|
| 2787 |
+
}
|
| 2788 |
+
},
|
| 2789 |
+
{
|
| 2790 |
+
"model": "fitting_system.recommendation",
|
| 2791 |
+
"pk": 27,
|
| 2792 |
+
"fields": {
|
| 2793 |
+
"body_scan": 11,
|
| 2794 |
+
"product": 2,
|
| 2795 |
+
"recommended_size": "XS",
|
| 2796 |
+
"recommended_fit": "regular",
|
| 2797 |
+
"recommended_colors": "",
|
| 2798 |
+
"priority": 100,
|
| 2799 |
+
"created_at": "2026-02-19T21:15:48.723Z"
|
| 2800 |
+
}
|
| 2801 |
+
},
|
| 2802 |
+
{
|
| 2803 |
+
"model": "fitting_system.recommendation",
|
| 2804 |
+
"pk": 28,
|
| 2805 |
+
"fields": {
|
| 2806 |
+
"body_scan": 12,
|
| 2807 |
+
"product": 2,
|
| 2808 |
+
"recommended_size": "XS",
|
| 2809 |
+
"recommended_fit": "regular",
|
| 2810 |
+
"recommended_colors": "",
|
| 2811 |
+
"priority": 100,
|
| 2812 |
+
"created_at": "2026-02-19T21:39:35.131Z"
|
| 2813 |
+
}
|
| 2814 |
+
},
|
| 2815 |
+
{
|
| 2816 |
+
"model": "fitting_system.recommendation",
|
| 2817 |
+
"pk": 29,
|
| 2818 |
+
"fields": {
|
| 2819 |
+
"body_scan": 13,
|
| 2820 |
+
"product": 2,
|
| 2821 |
+
"recommended_size": "XS",
|
| 2822 |
+
"recommended_fit": "regular",
|
| 2823 |
+
"recommended_colors": "",
|
| 2824 |
+
"priority": 100,
|
| 2825 |
+
"created_at": "2026-02-19T21:50:02.162Z"
|
| 2826 |
+
}
|
| 2827 |
+
},
|
| 2828 |
+
{
|
| 2829 |
+
"model": "fitting_system.recommendation",
|
| 2830 |
+
"pk": 30,
|
| 2831 |
+
"fields": {
|
| 2832 |
+
"body_scan": 14,
|
| 2833 |
+
"product": 2,
|
| 2834 |
+
"recommended_size": "XL",
|
| 2835 |
+
"recommended_fit": "regular",
|
| 2836 |
+
"recommended_colors": "",
|
| 2837 |
+
"priority": 100,
|
| 2838 |
+
"created_at": "2026-02-19T21:50:51.624Z"
|
| 2839 |
+
}
|
| 2840 |
+
},
|
| 2841 |
+
{
|
| 2842 |
+
"model": "fitting_system.recommendation",
|
| 2843 |
+
"pk": 31,
|
| 2844 |
+
"fields": {
|
| 2845 |
+
"body_scan": 15,
|
| 2846 |
+
"product": 2,
|
| 2847 |
+
"recommended_size": "XL",
|
| 2848 |
+
"recommended_fit": "regular",
|
| 2849 |
+
"recommended_colors": "",
|
| 2850 |
+
"priority": 100,
|
| 2851 |
+
"created_at": "2026-02-19T21:52:22.901Z"
|
| 2852 |
+
}
|
| 2853 |
+
},
|
| 2854 |
+
{
|
| 2855 |
+
"model": "fitting_system.recommendation",
|
| 2856 |
+
"pk": 32,
|
| 2857 |
+
"fields": {
|
| 2858 |
+
"body_scan": 16,
|
| 2859 |
+
"product": 2,
|
| 2860 |
+
"recommended_size": "XS",
|
| 2861 |
+
"recommended_fit": "regular",
|
| 2862 |
+
"recommended_colors": "",
|
| 2863 |
+
"priority": 100,
|
| 2864 |
+
"created_at": "2026-02-19T21:57:02.696Z"
|
| 2865 |
+
}
|
| 2866 |
+
},
|
| 2867 |
+
{
|
| 2868 |
+
"model": "fitting_system.recommendation",
|
| 2869 |
+
"pk": 33,
|
| 2870 |
+
"fields": {
|
| 2871 |
+
"body_scan": 17,
|
| 2872 |
+
"product": 2,
|
| 2873 |
+
"recommended_size": "XS",
|
| 2874 |
+
"recommended_fit": "regular",
|
| 2875 |
+
"recommended_colors": "",
|
| 2876 |
+
"priority": 100,
|
| 2877 |
+
"created_at": "2026-02-19T22:06:30.869Z"
|
| 2878 |
+
}
|
| 2879 |
+
},
|
| 2880 |
+
{
|
| 2881 |
+
"model": "fitting_system.recommendation",
|
| 2882 |
+
"pk": 34,
|
| 2883 |
+
"fields": {
|
| 2884 |
+
"body_scan": 18,
|
| 2885 |
+
"product": 2,
|
| 2886 |
+
"recommended_size": "XS",
|
| 2887 |
+
"recommended_fit": "regular",
|
| 2888 |
+
"recommended_colors": "",
|
| 2889 |
+
"priority": 100,
|
| 2890 |
+
"created_at": "2026-02-19T22:19:11.226Z"
|
| 2891 |
+
}
|
| 2892 |
+
},
|
| 2893 |
+
{
|
| 2894 |
+
"model": "fitting_system.recommendation",
|
| 2895 |
+
"pk": 35,
|
| 2896 |
+
"fields": {
|
| 2897 |
+
"body_scan": 19,
|
| 2898 |
+
"product": 2,
|
| 2899 |
+
"recommended_size": "XS",
|
| 2900 |
+
"recommended_fit": "regular",
|
| 2901 |
+
"recommended_colors": "",
|
| 2902 |
+
"priority": 100,
|
| 2903 |
+
"created_at": "2026-02-19T22:31:34.899Z"
|
| 2904 |
+
}
|
| 2905 |
+
},
|
| 2906 |
+
{
|
| 2907 |
+
"model": "fitting_system.recommendation",
|
| 2908 |
+
"pk": 36,
|
| 2909 |
+
"fields": {
|
| 2910 |
+
"body_scan": 20,
|
| 2911 |
+
"product": 2,
|
| 2912 |
+
"recommended_size": "XL",
|
| 2913 |
+
"recommended_fit": "regular",
|
| 2914 |
+
"recommended_colors": "",
|
| 2915 |
+
"priority": 100,
|
| 2916 |
+
"created_at": "2026-02-19T22:45:57.043Z"
|
| 2917 |
+
}
|
| 2918 |
+
},
|
| 2919 |
+
{
|
| 2920 |
+
"model": "fitting_system.recommendation",
|
| 2921 |
+
"pk": 37,
|
| 2922 |
+
"fields": {
|
| 2923 |
+
"body_scan": 21,
|
| 2924 |
+
"product": 2,
|
| 2925 |
+
"recommended_size": "M",
|
| 2926 |
+
"recommended_fit": "regular",
|
| 2927 |
+
"recommended_colors": "",
|
| 2928 |
+
"priority": 100,
|
| 2929 |
+
"created_at": "2026-02-19T22:49:00.246Z"
|
| 2930 |
+
}
|
| 2931 |
+
},
|
| 2932 |
+
{
|
| 2933 |
+
"model": "fitting_system.recommendation",
|
| 2934 |
+
"pk": 38,
|
| 2935 |
+
"fields": {
|
| 2936 |
+
"body_scan": 22,
|
| 2937 |
+
"product": 2,
|
| 2938 |
+
"recommended_size": "M",
|
| 2939 |
+
"recommended_fit": "regular",
|
| 2940 |
+
"recommended_colors": "",
|
| 2941 |
+
"priority": 100,
|
| 2942 |
+
"created_at": "2026-02-19T23:02:03.104Z"
|
| 2943 |
+
}
|
| 2944 |
+
},
|
| 2945 |
+
{
|
| 2946 |
+
"model": "fitting_system.recommendation",
|
| 2947 |
+
"pk": 39,
|
| 2948 |
+
"fields": {
|
| 2949 |
+
"body_scan": 23,
|
| 2950 |
+
"product": 2,
|
| 2951 |
+
"recommended_size": "L",
|
| 2952 |
+
"recommended_fit": "regular",
|
| 2953 |
+
"recommended_colors": "",
|
| 2954 |
+
"priority": 100,
|
| 2955 |
+
"created_at": "2026-02-19T23:04:18.435Z"
|
| 2956 |
+
}
|
| 2957 |
+
},
|
| 2958 |
+
{
|
| 2959 |
+
"model": "fitting_system.recommendation",
|
| 2960 |
+
"pk": 40,
|
| 2961 |
+
"fields": {
|
| 2962 |
+
"body_scan": 24,
|
| 2963 |
+
"product": 2,
|
| 2964 |
+
"recommended_size": "L",
|
| 2965 |
+
"recommended_fit": "regular",
|
| 2966 |
+
"recommended_colors": "",
|
| 2967 |
+
"priority": 100,
|
| 2968 |
+
"created_at": "2026-02-19T23:06:24.989Z"
|
| 2969 |
+
}
|
| 2970 |
+
},
|
| 2971 |
+
{
|
| 2972 |
+
"model": "fitting_system.recommendation",
|
| 2973 |
+
"pk": 41,
|
| 2974 |
+
"fields": {
|
| 2975 |
+
"body_scan": 25,
|
| 2976 |
+
"product": 2,
|
| 2977 |
+
"recommended_size": "S",
|
| 2978 |
+
"recommended_fit": "regular",
|
| 2979 |
+
"recommended_colors": "",
|
| 2980 |
+
"priority": 100,
|
| 2981 |
+
"created_at": "2026-02-19T23:10:22.708Z"
|
| 2982 |
+
}
|
| 2983 |
+
},
|
| 2984 |
+
{
|
| 2985 |
+
"model": "fitting_system.recommendation",
|
| 2986 |
+
"pk": 42,
|
| 2987 |
+
"fields": {
|
| 2988 |
+
"body_scan": 26,
|
| 2989 |
+
"product": 2,
|
| 2990 |
+
"recommended_size": "XXL",
|
| 2991 |
+
"recommended_fit": "regular",
|
| 2992 |
+
"recommended_colors": "",
|
| 2993 |
+
"priority": 100,
|
| 2994 |
+
"created_at": "2026-02-19T23:12:04.842Z"
|
| 2995 |
+
}
|
| 2996 |
+
},
|
| 2997 |
+
{
|
| 2998 |
+
"model": "fitting_system.recommendation",
|
| 2999 |
+
"pk": 43,
|
| 3000 |
+
"fields": {
|
| 3001 |
+
"body_scan": 27,
|
| 3002 |
+
"product": 2,
|
| 3003 |
+
"recommended_size": "M",
|
| 3004 |
+
"recommended_fit": "regular",
|
| 3005 |
+
"recommended_colors": "",
|
| 3006 |
+
"priority": 100,
|
| 3007 |
+
"created_at": "2026-02-19T23:16:36.681Z"
|
| 3008 |
+
}
|
| 3009 |
+
},
|
| 3010 |
+
{
|
| 3011 |
+
"model": "fitting_system.recommendation",
|
| 3012 |
+
"pk": 44,
|
| 3013 |
+
"fields": {
|
| 3014 |
+
"body_scan": 28,
|
| 3015 |
+
"product": 2,
|
| 3016 |
+
"recommended_size": "L",
|
| 3017 |
+
"recommended_fit": "regular",
|
| 3018 |
+
"recommended_colors": "",
|
| 3019 |
+
"priority": 100,
|
| 3020 |
+
"created_at": "2026-02-20T02:59:00.841Z"
|
| 3021 |
+
}
|
| 3022 |
+
},
|
| 3023 |
+
{
|
| 3024 |
+
"model": "fitting_system.recommendation",
|
| 3025 |
+
"pk": 45,
|
| 3026 |
+
"fields": {
|
| 3027 |
+
"body_scan": 29,
|
| 3028 |
+
"product": 2,
|
| 3029 |
+
"recommended_size": "XXL",
|
| 3030 |
+
"recommended_fit": "regular",
|
| 3031 |
+
"recommended_colors": "",
|
| 3032 |
+
"priority": 100,
|
| 3033 |
+
"created_at": "2026-02-20T03:00:02.230Z"
|
| 3034 |
+
}
|
| 3035 |
+
},
|
| 3036 |
+
{
|
| 3037 |
+
"model": "fitting_system.recommendation",
|
| 3038 |
+
"pk": 46,
|
| 3039 |
+
"fields": {
|
| 3040 |
+
"body_scan": 30,
|
| 3041 |
+
"product": 2,
|
| 3042 |
+
"recommended_size": "M",
|
| 3043 |
+
"recommended_fit": "regular",
|
| 3044 |
+
"recommended_colors": "",
|
| 3045 |
+
"priority": 100,
|
| 3046 |
+
"created_at": "2026-02-20T03:01:13.830Z"
|
| 3047 |
+
}
|
| 3048 |
+
},
|
| 3049 |
+
{
|
| 3050 |
+
"model": "fitting_system.recommendation",
|
| 3051 |
+
"pk": 47,
|
| 3052 |
+
"fields": {
|
| 3053 |
+
"body_scan": 31,
|
| 3054 |
+
"product": 2,
|
| 3055 |
+
"recommended_size": "L",
|
| 3056 |
+
"recommended_fit": "regular",
|
| 3057 |
+
"recommended_colors": "",
|
| 3058 |
+
"priority": 100,
|
| 3059 |
+
"created_at": "2026-02-20T11:12:41.196Z"
|
| 3060 |
+
}
|
| 3061 |
+
},
|
| 3062 |
+
{
|
| 3063 |
+
"model": "fitting_system.recommendation",
|
| 3064 |
+
"pk": 48,
|
| 3065 |
+
"fields": {
|
| 3066 |
+
"body_scan": 32,
|
| 3067 |
+
"product": 2,
|
| 3068 |
+
"recommended_size": "M",
|
| 3069 |
+
"recommended_fit": "regular",
|
| 3070 |
+
"recommended_colors": "",
|
| 3071 |
+
"priority": 100,
|
| 3072 |
+
"created_at": "2026-02-21T11:53:24.712Z"
|
| 3073 |
+
}
|
| 3074 |
+
},
|
| 3075 |
+
{
|
| 3076 |
+
"model": "fitting_system.recommendation",
|
| 3077 |
+
"pk": 49,
|
| 3078 |
+
"fields": {
|
| 3079 |
+
"body_scan": 33,
|
| 3080 |
+
"product": 2,
|
| 3081 |
+
"recommended_size": "M",
|
| 3082 |
+
"recommended_fit": "regular",
|
| 3083 |
+
"recommended_colors": "",
|
| 3084 |
+
"priority": 100,
|
| 3085 |
+
"created_at": "2026-02-22T00:54:22.542Z"
|
| 3086 |
+
}
|
| 3087 |
+
},
|
| 3088 |
+
{
|
| 3089 |
+
"model": "fitting_system.recommendation",
|
| 3090 |
+
"pk": 50,
|
| 3091 |
+
"fields": {
|
| 3092 |
+
"body_scan": 34,
|
| 3093 |
+
"product": 2,
|
| 3094 |
+
"recommended_size": "L",
|
| 3095 |
+
"recommended_fit": "regular",
|
| 3096 |
+
"recommended_colors": "",
|
| 3097 |
+
"priority": 100,
|
| 3098 |
+
"created_at": "2026-02-22T02:34:25.223Z"
|
| 3099 |
+
}
|
| 3100 |
+
},
|
| 3101 |
+
{
|
| 3102 |
+
"model": "fitting_system.recommendation",
|
| 3103 |
+
"pk": 51,
|
| 3104 |
+
"fields": {
|
| 3105 |
+
"body_scan": 35,
|
| 3106 |
+
"product": 2,
|
| 3107 |
+
"recommended_size": "M",
|
| 3108 |
+
"recommended_fit": "regular",
|
| 3109 |
+
"recommended_colors": "",
|
| 3110 |
+
"priority": 100,
|
| 3111 |
+
"created_at": "2026-02-22T02:35:31.873Z"
|
| 3112 |
+
}
|
| 3113 |
+
},
|
| 3114 |
+
{
|
| 3115 |
+
"model": "fitting_system.recommendation",
|
| 3116 |
+
"pk": 52,
|
| 3117 |
+
"fields": {
|
| 3118 |
+
"body_scan": 36,
|
| 3119 |
+
"product": 2,
|
| 3120 |
+
"recommended_size": "L",
|
| 3121 |
+
"recommended_fit": "regular",
|
| 3122 |
+
"recommended_colors": "",
|
| 3123 |
+
"priority": 100,
|
| 3124 |
+
"created_at": "2026-02-22T02:58:39.060Z"
|
| 3125 |
+
}
|
| 3126 |
+
},
|
| 3127 |
+
{
|
| 3128 |
+
"model": "fitting_system.recommendation",
|
| 3129 |
+
"pk": 53,
|
| 3130 |
+
"fields": {
|
| 3131 |
+
"body_scan": 37,
|
| 3132 |
+
"product": 2,
|
| 3133 |
+
"recommended_size": "L",
|
| 3134 |
+
"recommended_fit": "regular",
|
| 3135 |
+
"recommended_colors": "",
|
| 3136 |
+
"priority": 100,
|
| 3137 |
+
"created_at": "2026-02-22T03:46:05.015Z"
|
| 3138 |
+
}
|
| 3139 |
+
},
|
| 3140 |
+
{
|
| 3141 |
+
"model": "fitting_system.recommendation",
|
| 3142 |
+
"pk": 54,
|
| 3143 |
+
"fields": {
|
| 3144 |
+
"body_scan": 38,
|
| 3145 |
+
"product": 2,
|
| 3146 |
+
"recommended_size": "L",
|
| 3147 |
+
"recommended_fit": "regular",
|
| 3148 |
+
"recommended_colors": "",
|
| 3149 |
+
"priority": 100,
|
| 3150 |
+
"created_at": "2026-02-22T22:17:11.275Z"
|
| 3151 |
+
}
|
| 3152 |
+
},
|
| 3153 |
+
{
|
| 3154 |
+
"model": "fitting_system.recommendation",
|
| 3155 |
+
"pk": 55,
|
| 3156 |
+
"fields": {
|
| 3157 |
+
"body_scan": 39,
|
| 3158 |
+
"product": 2,
|
| 3159 |
+
"recommended_size": "M",
|
| 3160 |
+
"recommended_fit": "regular",
|
| 3161 |
+
"recommended_colors": "",
|
| 3162 |
+
"priority": 100,
|
| 3163 |
+
"created_at": "2026-02-22T22:26:46.290Z"
|
| 3164 |
+
}
|
| 3165 |
+
},
|
| 3166 |
+
{
|
| 3167 |
+
"model": "fitting_system.recommendation",
|
| 3168 |
+
"pk": 56,
|
| 3169 |
+
"fields": {
|
| 3170 |
+
"body_scan": 40,
|
| 3171 |
+
"product": 2,
|
| 3172 |
+
"recommended_size": "L",
|
| 3173 |
+
"recommended_fit": "regular",
|
| 3174 |
+
"recommended_colors": "",
|
| 3175 |
+
"priority": 100,
|
| 3176 |
+
"created_at": "2026-02-22T22:29:23.683Z"
|
| 3177 |
+
}
|
| 3178 |
+
},
|
| 3179 |
+
{
|
| 3180 |
+
"model": "fitting_system.recommendation",
|
| 3181 |
+
"pk": 57,
|
| 3182 |
+
"fields": {
|
| 3183 |
+
"body_scan": 41,
|
| 3184 |
+
"product": 2,
|
| 3185 |
+
"recommended_size": "L",
|
| 3186 |
+
"recommended_fit": "regular",
|
| 3187 |
+
"recommended_colors": "",
|
| 3188 |
+
"priority": 100,
|
| 3189 |
+
"created_at": "2026-02-22T22:29:35.174Z"
|
| 3190 |
+
}
|
| 3191 |
+
},
|
| 3192 |
+
{
|
| 3193 |
+
"model": "fitting_system.recommendation",
|
| 3194 |
+
"pk": 58,
|
| 3195 |
+
"fields": {
|
| 3196 |
+
"body_scan": 42,
|
| 3197 |
+
"product": 2,
|
| 3198 |
+
"recommended_size": "M",
|
| 3199 |
+
"recommended_fit": "regular",
|
| 3200 |
+
"recommended_colors": "",
|
| 3201 |
+
"priority": 100,
|
| 3202 |
+
"created_at": "2026-02-22T22:44:10.617Z"
|
| 3203 |
+
}
|
| 3204 |
+
},
|
| 3205 |
+
{
|
| 3206 |
+
"model": "fitting_system.recommendation",
|
| 3207 |
+
"pk": 59,
|
| 3208 |
+
"fields": {
|
| 3209 |
+
"body_scan": 43,
|
| 3210 |
+
"product": 2,
|
| 3211 |
+
"recommended_size": "XXL",
|
| 3212 |
+
"recommended_fit": "regular",
|
| 3213 |
+
"recommended_colors": "",
|
| 3214 |
+
"priority": 100,
|
| 3215 |
+
"created_at": "2026-02-24T01:31:55.981Z"
|
| 3216 |
+
}
|
| 3217 |
+
},
|
| 3218 |
+
{
|
| 3219 |
+
"model": "fitting_system.recommendation",
|
| 3220 |
+
"pk": 60,
|
| 3221 |
+
"fields": {
|
| 3222 |
+
"body_scan": 44,
|
| 3223 |
+
"product": 2,
|
| 3224 |
+
"recommended_size": "M",
|
| 3225 |
+
"recommended_fit": "regular",
|
| 3226 |
+
"recommended_colors": "",
|
| 3227 |
+
"priority": 100,
|
| 3228 |
+
"created_at": "2026-02-24T01:40:21.557Z"
|
| 3229 |
+
}
|
| 3230 |
+
},
|
| 3231 |
+
{
|
| 3232 |
+
"model": "fitting_system.recommendation",
|
| 3233 |
+
"pk": 61,
|
| 3234 |
+
"fields": {
|
| 3235 |
+
"body_scan": 45,
|
| 3236 |
+
"product": 2,
|
| 3237 |
+
"recommended_size": "S",
|
| 3238 |
+
"recommended_fit": "regular",
|
| 3239 |
+
"recommended_colors": "",
|
| 3240 |
+
"priority": 100,
|
| 3241 |
+
"created_at": "2026-02-24T01:46:04.842Z"
|
| 3242 |
+
}
|
| 3243 |
+
},
|
| 3244 |
+
{
|
| 3245 |
+
"model": "fitting_system.recommendation",
|
| 3246 |
+
"pk": 62,
|
| 3247 |
+
"fields": {
|
| 3248 |
+
"body_scan": 46,
|
| 3249 |
+
"product": 2,
|
| 3250 |
+
"recommended_size": "XXL",
|
| 3251 |
+
"recommended_fit": "regular",
|
| 3252 |
+
"recommended_colors": "",
|
| 3253 |
+
"priority": 100,
|
| 3254 |
+
"created_at": "2026-02-24T01:50:58.725Z"
|
| 3255 |
+
}
|
| 3256 |
+
},
|
| 3257 |
+
{
|
| 3258 |
+
"model": "fitting_system.recommendation",
|
| 3259 |
+
"pk": 63,
|
| 3260 |
+
"fields": {
|
| 3261 |
+
"body_scan": 47,
|
| 3262 |
+
"product": 2,
|
| 3263 |
+
"recommended_size": "M",
|
| 3264 |
+
"recommended_fit": "regular",
|
| 3265 |
+
"recommended_colors": "",
|
| 3266 |
+
"priority": 100,
|
| 3267 |
+
"created_at": "2026-02-24T02:07:30.668Z"
|
| 3268 |
+
}
|
| 3269 |
+
},
|
| 3270 |
+
{
|
| 3271 |
+
"model": "fitting_system.recommendation",
|
| 3272 |
+
"pk": 64,
|
| 3273 |
+
"fields": {
|
| 3274 |
+
"body_scan": 48,
|
| 3275 |
+
"product": 2,
|
| 3276 |
+
"recommended_size": "L",
|
| 3277 |
+
"recommended_fit": "regular",
|
| 3278 |
+
"recommended_colors": "",
|
| 3279 |
+
"priority": 100,
|
| 3280 |
+
"created_at": "2026-02-24T02:15:21.380Z"
|
| 3281 |
+
}
|
| 3282 |
+
},
|
| 3283 |
+
{
|
| 3284 |
+
"model": "fitting_system.recommendation",
|
| 3285 |
+
"pk": 65,
|
| 3286 |
+
"fields": {
|
| 3287 |
+
"body_scan": 49,
|
| 3288 |
+
"product": 2,
|
| 3289 |
+
"recommended_size": "L",
|
| 3290 |
+
"recommended_fit": "regular",
|
| 3291 |
+
"recommended_colors": "",
|
| 3292 |
+
"priority": 100,
|
| 3293 |
+
"created_at": "2026-02-24T20:52:09.952Z"
|
| 3294 |
+
}
|
| 3295 |
+
}
|
| 3296 |
+
]
|
fitting_system/management/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Management commands package
|
fitting_system/management/commands/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Management commands package
|
fitting_system/management/commands/populate_data.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.core.management.base import BaseCommand
|
| 2 |
+
from fitting_system.models import Size, Color, Product, ProductVariant, Inventory
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Command(BaseCommand):
|
| 6 |
+
help = 'Populate database with initial data for testing'
|
| 7 |
+
|
| 8 |
+
def handle(self, *args, **kwargs):
|
| 9 |
+
self.stdout.write('Populating database with initial data...')
|
| 10 |
+
|
| 11 |
+
# Create Sizes
|
| 12 |
+
self.stdout.write('Creating sizes...')
|
| 13 |
+
sizes_data = [
|
| 14 |
+
{'name': 'S', 'chest_min': 85, 'chest_max': 92, 'waist_min': 70, 'waist_max': 77,
|
| 15 |
+
'shoulder_min': 40, 'shoulder_max': 43, 'height_min': 160, 'height_max': 170},
|
| 16 |
+
{'name': 'M', 'chest_min': 93, 'chest_max': 100, 'waist_min': 78, 'waist_max': 85,
|
| 17 |
+
'shoulder_min': 44, 'shoulder_max': 47, 'height_min': 168, 'height_max': 178},
|
| 18 |
+
{'name': 'L', 'chest_min': 101, 'chest_max': 108, 'waist_min': 86, 'waist_max': 93,
|
| 19 |
+
'shoulder_min': 48, 'shoulder_max': 51, 'height_min': 175, 'height_max': 185},
|
| 20 |
+
{'name': 'XL', 'chest_min': 109, 'chest_max': 116, 'waist_min': 94, 'waist_max': 101,
|
| 21 |
+
'shoulder_min': 52, 'shoulder_max': 55, 'height_min': 180, 'height_max': 190},
|
| 22 |
+
{'name': 'XXL', 'chest_min': 117, 'chest_max': 125, 'waist_min': 102, 'waist_max': 110,
|
| 23 |
+
'shoulder_min': 56, 'shoulder_max': 60, 'height_min': 185, 'height_max': 195},
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
for size_data in sizes_data:
|
| 27 |
+
Size.objects.get_or_create(name=size_data['name'], defaults=size_data)
|
| 28 |
+
|
| 29 |
+
# Create Colors
|
| 30 |
+
self.stdout.write('Creating colors...')
|
| 31 |
+
colors_data = [
|
| 32 |
+
# Light skin tone colors
|
| 33 |
+
{'name': 'Pastel Pink', 'hex_code': '#FFD1DC', 'category': 'light'},
|
| 34 |
+
{'name': 'Light Blue', 'hex_code': '#ADD8E6', 'category': 'light'},
|
| 35 |
+
{'name': 'Lavender', 'hex_code': '#E6E6FA', 'category': 'light'},
|
| 36 |
+
{'name': 'Mint Green', 'hex_code': '#98FF98', 'category': 'light'},
|
| 37 |
+
|
| 38 |
+
# Medium skin tone colors
|
| 39 |
+
{'name': 'Earth Brown', 'hex_code': '#8B4513', 'category': 'medium'},
|
| 40 |
+
{'name': 'Olive Green', 'hex_code': '#808000', 'category': 'medium'},
|
| 41 |
+
{'name': 'Burgundy', 'hex_code': '#800020', 'category': 'medium'},
|
| 42 |
+
{'name': 'Mustard Yellow', 'hex_code': '#FFDB58', 'category': 'medium'},
|
| 43 |
+
|
| 44 |
+
# Dark skin tone colors
|
| 45 |
+
{'name': 'Bright White', 'hex_code': '#FFFFFF', 'category': 'dark'},
|
| 46 |
+
{'name': 'Vibrant Red', 'hex_code': '#FF0000', 'category': 'dark'},
|
| 47 |
+
{'name': 'Electric Blue', 'hex_code': '#7DF9FF', 'category': 'dark'},
|
| 48 |
+
{'name': 'Sunny Yellow', 'hex_code': '#FFD700', 'category': 'dark'},
|
| 49 |
+
|
| 50 |
+
# Neutral colors
|
| 51 |
+
{'name': 'Black', 'hex_code': '#000000', 'category': 'neutral'},
|
| 52 |
+
{'name': 'White', 'hex_code': '#FFFFFF', 'category': 'neutral'},
|
| 53 |
+
{'name': 'Gray', 'hex_code': '#808080', 'category': 'neutral'},
|
| 54 |
+
{'name': 'Navy Blue', 'hex_code': '#000080', 'category': 'neutral'},
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
for color_data in colors_data:
|
| 58 |
+
Color.objects.get_or_create(name=color_data['name'], defaults=color_data)
|
| 59 |
+
|
| 60 |
+
# Create Products
|
| 61 |
+
self.stdout.write('Creating products...')
|
| 62 |
+
products_data = [
|
| 63 |
+
# Men's products
|
| 64 |
+
{'name': 'Classic Cotton Shirt', 'category': 'shirt', 'fit_type': 'regular', 'gender': 'men',
|
| 65 |
+
'price': 49.99, 'description': 'A timeless classic cotton shirt perfect for any occasion. Made from 100% premium cotton for maximum comfort.'},
|
| 66 |
+
{'name': 'Slim Fit Dress Shirt', 'category': 'shirt', 'fit_type': 'slim', 'gender': 'men',
|
| 67 |
+
'price': 59.99, 'description': 'Modern slim fit dress shirt with a tailored silhouette. Perfect for professional settings.'},
|
| 68 |
+
{'name': 'Casual Denim Jeans', 'category': 'pants', 'fit_type': 'regular', 'gender': 'men',
|
| 69 |
+
'price': 79.99, 'description': 'Comfortable denim jeans with a classic fit. Durable and stylish for everyday wear.'},
|
| 70 |
+
{'name': 'Slim Fit Chinos', 'category': 'pants', 'fit_type': 'slim', 'gender': 'men',
|
| 71 |
+
'price': 69.99, 'description': 'Versatile slim fit chinos that pair well with any outfit. Perfect blend of style and comfort.'},
|
| 72 |
+
{'name': 'Leather Jacket', 'category': 'jacket', 'fit_type': 'regular', 'gender': 'men',
|
| 73 |
+
'price': 199.99, 'description': 'Premium leather jacket with a classic design. Timeless piece that never goes out of style.'},
|
| 74 |
+
|
| 75 |
+
# Women's products
|
| 76 |
+
{'name': 'Elegant Blouse', 'category': 'shirt', 'fit_type': 'regular', 'gender': 'women',
|
| 77 |
+
'price': 54.99, 'description': 'Sophisticated blouse with delicate details. Perfect for both office and evening wear.'},
|
| 78 |
+
{'name': 'Fitted Blazer', 'category': 'jacket', 'fit_type': 'slim', 'gender': 'women',
|
| 79 |
+
'price': 129.99, 'description': 'Tailored blazer that adds polish to any outfit. Professional yet stylish.'},
|
| 80 |
+
{'name': 'High-Waist Trousers', 'category': 'pants', 'fit_type': 'regular', 'gender': 'women',
|
| 81 |
+
'price': 74.99, 'description': 'Flattering high-waist trousers with a comfortable fit. Versatile for any occasion.'},
|
| 82 |
+
{'name': 'Summer Dress', 'category': 'dress', 'fit_type': 'regular', 'gender': 'women',
|
| 83 |
+
'price': 89.99, 'description': 'Light and breezy summer dress perfect for warm weather. Comfortable and stylish.'},
|
| 84 |
+
{'name': 'Pencil Skirt', 'category': 'skirt', 'fit_type': 'slim', 'gender': 'women',
|
| 85 |
+
'price': 49.99, 'description': 'Classic pencil skirt with a flattering silhouette. Essential wardrobe piece.'},
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
products = []
|
| 89 |
+
for product_data in products_data:
|
| 90 |
+
product, created = Product.objects.get_or_create(
|
| 91 |
+
name=product_data['name'],
|
| 92 |
+
defaults=product_data
|
| 93 |
+
)
|
| 94 |
+
products.append(product)
|
| 95 |
+
|
| 96 |
+
# Create Product Variants and Inventory
|
| 97 |
+
self.stdout.write('Creating product variants and inventory...')
|
| 98 |
+
sizes = Size.objects.all()
|
| 99 |
+
colors = Color.objects.all()[:8] # Use first 8 colors
|
| 100 |
+
|
| 101 |
+
for product in products:
|
| 102 |
+
# Create 2-3 size variants per product
|
| 103 |
+
product_sizes = list(sizes)[1:4] # M, L, XL
|
| 104 |
+
product_colors = list(colors)[:3] # First 3 colors
|
| 105 |
+
|
| 106 |
+
counter = 1
|
| 107 |
+
for size in product_sizes:
|
| 108 |
+
for color in product_colors:
|
| 109 |
+
sku = f"{product.id}-{size.name}-{color.id}-{counter}"
|
| 110 |
+
variant, created = ProductVariant.objects.get_or_create(
|
| 111 |
+
product=product,
|
| 112 |
+
size=size,
|
| 113 |
+
color=color,
|
| 114 |
+
defaults={'sku': sku}
|
| 115 |
+
)
|
| 116 |
+
counter += 1
|
| 117 |
+
|
| 118 |
+
# Create inventory for this variant
|
| 119 |
+
if created:
|
| 120 |
+
import random
|
| 121 |
+
quantity = random.randint(0, 20) # Random stock between 0-20
|
| 122 |
+
Inventory.objects.create(
|
| 123 |
+
product_variant=variant,
|
| 124 |
+
quantity=quantity,
|
| 125 |
+
low_stock_threshold=5
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
self.stdout.write(self.style.SUCCESS('Successfully populated database!'))
|
| 129 |
+
self.stdout.write(f'Created {Size.objects.count()} sizes')
|
| 130 |
+
self.stdout.write(f'Created {Color.objects.count()} colors')
|
| 131 |
+
self.stdout.write(f'Created {Product.objects.count()} products')
|
| 132 |
+
self.stdout.write(f'Created {ProductVariant.objects.count()} product variants')
|
| 133 |
+
self.stdout.write(f'Created {Inventory.objects.count()} inventory records')
|
fitting_system/management/commands/populate_mvp.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
from django.core.management.base import BaseCommand
|
| 3 |
+
from fitting_system.models import Size, Color, Product, ProductVariant, Inventory
|
| 4 |
+
from fitting_system.color_palettes import get_all_unique_colors, SKIN_TONE_PALETTES
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Command(BaseCommand):
|
| 8 |
+
help = 'Populate database with MVP data - minimal clothing sets for men and women'
|
| 9 |
+
|
| 10 |
+
def handle(self, *args, **kwargs):
|
| 11 |
+
self.stdout.write('Clearing existing products...')
|
| 12 |
+
Inventory.objects.all().delete()
|
| 13 |
+
ProductVariant.objects.all().delete()
|
| 14 |
+
Product.objects.all().delete()
|
| 15 |
+
Color.objects.all().delete()
|
| 16 |
+
|
| 17 |
+
self.stdout.write('Populating database with MVP data...')
|
| 18 |
+
|
| 19 |
+
# ── Sizes ─────────────────────────────────────────────────
|
| 20 |
+
self.stdout.write('Ensuring sizes exist...')
|
| 21 |
+
sizes_data = [
|
| 22 |
+
{'name': 'S', 'chest_min': 85, 'chest_max': 92, 'waist_min': 70, 'waist_max': 77,
|
| 23 |
+
'shoulder_min': 40, 'shoulder_max': 43, 'height_min': 160, 'height_max': 170},
|
| 24 |
+
{'name': 'M', 'chest_min': 93, 'chest_max': 100, 'waist_min': 78, 'waist_max': 85,
|
| 25 |
+
'shoulder_min': 44, 'shoulder_max': 47, 'height_min': 168, 'height_max': 178},
|
| 26 |
+
{'name': 'L', 'chest_min': 101, 'chest_max': 108, 'waist_min': 86, 'waist_max': 93,
|
| 27 |
+
'shoulder_min': 48, 'shoulder_max': 51, 'height_min': 175, 'height_max': 185},
|
| 28 |
+
{'name': 'XL', 'chest_min': 109, 'chest_max': 116, 'waist_min': 94, 'waist_max': 101,
|
| 29 |
+
'shoulder_min': 52, 'shoulder_max': 55, 'height_min': 180, 'height_max': 190},
|
| 30 |
+
]
|
| 31 |
+
for sd in sizes_data:
|
| 32 |
+
Size.objects.get_or_create(name=sd['name'], defaults=sd)
|
| 33 |
+
|
| 34 |
+
# ── Colours from unified palette ──────────────────────────
|
| 35 |
+
self.stdout.write('Creating unified product colors...')
|
| 36 |
+
all_colors = get_all_unique_colors()
|
| 37 |
+
|
| 38 |
+
# Determine which colour names are used for shirts vs pants
|
| 39 |
+
shirt_color_names = set()
|
| 40 |
+
pants_color_names = set()
|
| 41 |
+
for palette in SKIN_TONE_PALETTES.values():
|
| 42 |
+
for c in palette['shirts']:
|
| 43 |
+
shirt_color_names.add(c['name'])
|
| 44 |
+
for c in palette['pants']:
|
| 45 |
+
pants_color_names.add(c['name'])
|
| 46 |
+
|
| 47 |
+
colors_map = {} # name → Color instance
|
| 48 |
+
for c in all_colors:
|
| 49 |
+
obj, _ = Color.objects.get_or_create(
|
| 50 |
+
name=c['name'],
|
| 51 |
+
defaults={'hex_code': c['hex'], 'category': 'neutral'},
|
| 52 |
+
)
|
| 53 |
+
colors_map[c['name']] = obj
|
| 54 |
+
self.stdout.write(f' 🎨 {len(colors_map)} unique colors created')
|
| 55 |
+
|
| 56 |
+
# ── Products ──────────────────────────────────────────────
|
| 57 |
+
self.stdout.write('Creating MVP products with palette colors...')
|
| 58 |
+
products_config = [
|
| 59 |
+
# ============ MEN'S SHIRTS ============
|
| 60 |
+
{'product': {'name': 'Slim Fit Cotton Shirt', 'category': 'shirt', 'fit_type': 'slim',
|
| 61 |
+
'gender': 'men', 'price': 54.99,
|
| 62 |
+
'description': 'Modern slim fit cotton shirt for a sleek, tailored look.'},
|
| 63 |
+
'color_pool': 'shirts'},
|
| 64 |
+
{'product': {'name': 'Classic Cotton Shirt', 'category': 'shirt', 'fit_type': 'regular',
|
| 65 |
+
'gender': 'men', 'price': 49.99,
|
| 66 |
+
'description': 'A timeless classic cotton shirt perfect for any occasion.'},
|
| 67 |
+
'color_pool': 'shirts'},
|
| 68 |
+
{'product': {'name': 'Relaxed Cotton Shirt', 'category': 'shirt', 'fit_type': 'oversize',
|
| 69 |
+
'gender': 'men', 'price': 52.99,
|
| 70 |
+
'description': 'Comfortable relaxed fit cotton shirt with extra room.'},
|
| 71 |
+
'color_pool': 'shirts'},
|
| 72 |
+
# ============ MEN'S PANTS ============
|
| 73 |
+
{'product': {'name': 'Slim Fit Jeans', 'category': 'pants', 'fit_type': 'slim',
|
| 74 |
+
'gender': 'men', 'price': 84.99,
|
| 75 |
+
'description': 'Modern slim fit denim jeans with a streamlined silhouette.'},
|
| 76 |
+
'color_pool': 'pants'},
|
| 77 |
+
{'product': {'name': 'Casual Denim Jeans', 'category': 'pants', 'fit_type': 'regular',
|
| 78 |
+
'gender': 'men', 'price': 79.99,
|
| 79 |
+
'description': 'Comfortable denim jeans with a classic fit.'},
|
| 80 |
+
'color_pool': 'pants'},
|
| 81 |
+
{'product': {'name': 'Loose Fit Jeans', 'category': 'pants', 'fit_type': 'oversize',
|
| 82 |
+
'gender': 'men', 'price': 82.99,
|
| 83 |
+
'description': 'Relaxed loose fit jeans for maximum comfort.'},
|
| 84 |
+
'color_pool': 'pants'},
|
| 85 |
+
# ============ MEN'S JACKETS ============
|
| 86 |
+
{'product': {'name': 'Fitted Leather Jacket', 'category': 'jacket', 'fit_type': 'slim',
|
| 87 |
+
'gender': 'men', 'price': 219.99,
|
| 88 |
+
'description': 'Sleek fitted leather jacket with a modern cut.'},
|
| 89 |
+
'color_pool': 'shirts'},
|
| 90 |
+
{'product': {'name': 'Leather Jacket', 'category': 'jacket', 'fit_type': 'regular',
|
| 91 |
+
'gender': 'men', 'price': 199.99,
|
| 92 |
+
'description': 'Premium leather jacket with a classic design.'},
|
| 93 |
+
'color_pool': 'shirts'},
|
| 94 |
+
{'product': {'name': 'Oversized Leather Jacket', 'category': 'jacket', 'fit_type': 'oversize',
|
| 95 |
+
'gender': 'men', 'price': 229.99,
|
| 96 |
+
'description': 'Bold oversized leather jacket for a modern streetwear look.'},
|
| 97 |
+
'color_pool': 'shirts'},
|
| 98 |
+
# ============ WOMEN'S BLOUSES ============
|
| 99 |
+
{'product': {'name': 'Fitted Blouse', 'category': 'shirt', 'fit_type': 'slim',
|
| 100 |
+
'gender': 'women', 'price': 59.99,
|
| 101 |
+
'description': 'Elegant fitted blouse with a tailored silhouette.'},
|
| 102 |
+
'color_pool': 'shirts'},
|
| 103 |
+
{'product': {'name': 'Elegant Blouse', 'category': 'shirt', 'fit_type': 'regular',
|
| 104 |
+
'gender': 'women', 'price': 54.99,
|
| 105 |
+
'description': 'Sophisticated blouse with delicate details.'},
|
| 106 |
+
'color_pool': 'shirts'},
|
| 107 |
+
{'product': {'name': 'Oversized Blouse', 'category': 'shirt', 'fit_type': 'oversize',
|
| 108 |
+
'gender': 'women', 'price': 56.99,
|
| 109 |
+
'description': 'Flowy oversized blouse for effortless chic style.'},
|
| 110 |
+
'color_pool': 'shirts'},
|
| 111 |
+
# ============ WOMEN'S DRESSES ============
|
| 112 |
+
{'product': {'name': 'Fitted Summer Dress', 'category': 'dress', 'fit_type': 'slim',
|
| 113 |
+
'gender': 'women', 'price': 94.99,
|
| 114 |
+
'description': 'Form-fitting summer dress that accentuates your silhouette.'},
|
| 115 |
+
'color_pool': 'shirts'},
|
| 116 |
+
{'product': {'name': 'Summer Dress', 'category': 'dress', 'fit_type': 'regular',
|
| 117 |
+
'gender': 'women', 'price': 89.99,
|
| 118 |
+
'description': 'Light and breezy summer dress perfect for warm weather.'},
|
| 119 |
+
'color_pool': 'shirts'},
|
| 120 |
+
{'product': {'name': 'Flowy Summer Dress', 'category': 'dress', 'fit_type': 'oversize',
|
| 121 |
+
'gender': 'women', 'price': 92.99,
|
| 122 |
+
'description': 'Airy flowy dress with a relaxed fit.'},
|
| 123 |
+
'color_pool': 'shirts'},
|
| 124 |
+
# ============ WOMEN'S TROUSERS ============
|
| 125 |
+
{'product': {'name': 'Slim Trousers', 'category': 'pants', 'fit_type': 'slim',
|
| 126 |
+
'gender': 'women', 'price': 79.99,
|
| 127 |
+
'description': 'Tailored slim fit trousers for a sleek, professional look.'},
|
| 128 |
+
'color_pool': 'pants'},
|
| 129 |
+
{'product': {'name': 'High-Waist Trousers', 'category': 'pants', 'fit_type': 'regular',
|
| 130 |
+
'gender': 'women', 'price': 74.99,
|
| 131 |
+
'description': 'Flattering high-waist trousers with a comfortable fit.'},
|
| 132 |
+
'color_pool': 'pants'},
|
| 133 |
+
{'product': {'name': 'Wide-Leg Trousers', 'category': 'pants', 'fit_type': 'oversize',
|
| 134 |
+
'gender': 'women', 'price': 77.99,
|
| 135 |
+
'description': 'Trendy wide-leg trousers with a relaxed, flowing silhouette.'},
|
| 136 |
+
'color_pool': 'pants'},
|
| 137 |
+
]
|
| 138 |
+
|
| 139 |
+
sizes = Size.objects.all()
|
| 140 |
+
|
| 141 |
+
for config in products_config:
|
| 142 |
+
product_data = config['product']
|
| 143 |
+
pool_key = config['color_pool'] # 'shirts' or 'pants'
|
| 144 |
+
|
| 145 |
+
product, created = Product.objects.get_or_create(
|
| 146 |
+
name=product_data['name'], defaults=product_data,
|
| 147 |
+
)
|
| 148 |
+
if created:
|
| 149 |
+
self.stdout.write(f' Created: {product.name}')
|
| 150 |
+
|
| 151 |
+
# Collect ALL unique colour names used in the chosen pool across
|
| 152 |
+
# every skin tone so the product is available in every palette.
|
| 153 |
+
pool_colors = set()
|
| 154 |
+
for palette in SKIN_TONE_PALETTES.values():
|
| 155 |
+
for c in palette[pool_key]:
|
| 156 |
+
pool_colors.add(c['name'])
|
| 157 |
+
|
| 158 |
+
counter = 1
|
| 159 |
+
for size in sizes:
|
| 160 |
+
for color_name in sorted(pool_colors):
|
| 161 |
+
color = colors_map.get(color_name)
|
| 162 |
+
if not color:
|
| 163 |
+
continue
|
| 164 |
+
sku = f"{product.id}-{size.name}-{color.id}-{counter}"
|
| 165 |
+
variant, v_created = ProductVariant.objects.get_or_create(
|
| 166 |
+
product=product, size=size, color=color,
|
| 167 |
+
defaults={'sku': sku},
|
| 168 |
+
)
|
| 169 |
+
counter += 1
|
| 170 |
+
if v_created:
|
| 171 |
+
Inventory.objects.create(
|
| 172 |
+
product_variant=variant,
|
| 173 |
+
quantity=random.randint(10, 25),
|
| 174 |
+
low_stock_threshold=5,
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
variant_colors = product.variants.values_list('color__name', flat=True).distinct()
|
| 178 |
+
self.stdout.write(f' Colors: {", ".join(sorted(variant_colors))}')
|
| 179 |
+
|
| 180 |
+
# ── Summary ───────────────────────────────────────────────
|
| 181 |
+
self.stdout.write(self.style.SUCCESS('\n✅ Successfully populated MVP database!'))
|
| 182 |
+
self.stdout.write(f'📦 Products: {Product.objects.count()}')
|
| 183 |
+
self.stdout.write(f'📏 Sizes: {Size.objects.count()}')
|
| 184 |
+
self.stdout.write(f'🎨 Colors: {Color.objects.count()}')
|
| 185 |
+
self.stdout.write(f'🏷️ Variants: {ProductVariant.objects.count()}')
|
| 186 |
+
self.stdout.write(f'📊 Inventory: {Inventory.objects.count()}')
|
| 187 |
+
|
| 188 |
+
self.stdout.write('\n📋 MVP Product Summary:')
|
| 189 |
+
self.stdout.write(" Men's Set:")
|
| 190 |
+
for p in Product.objects.filter(gender='men').order_by('category', 'fit_type'):
|
| 191 |
+
cl = p.variants.values_list('color__name', flat=True).distinct()
|
| 192 |
+
self.stdout.write(f' • {p.name} ({p.category} - {p.fit_type})')
|
| 193 |
+
self.stdout.write(f' Colors: {", ".join(sorted(cl))}')
|
| 194 |
+
self.stdout.write(" Women's Set:")
|
| 195 |
+
for p in Product.objects.filter(gender='women').order_by('category', 'fit_type'):
|
| 196 |
+
cl = p.variants.values_list('color__name', flat=True).distinct()
|
| 197 |
+
self.stdout.write(f' • {p.name} ({p.category} - {p.fit_type})')
|
| 198 |
+
self.stdout.write(f' Colors: {", ".join(sorted(cl))}')
|
fitting_system/management/commands/reset_catalog.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
reset_catalog.py — management command
|
| 3 |
+
Wipes all Product/ProductVariant/Inventory/Color data and re-seeds:
|
| 4 |
+
• All colours from color_palettes.py (single source of truth)
|
| 5 |
+
• Exactly 4 products using colors guaranteed to exist in the palette
|
| 6 |
+
• 32 variants per product (4 sizes × 8 colors) = 128 inventory records
|
| 7 |
+
"""
|
| 8 |
+
from django.core.management.base import BaseCommand
|
| 9 |
+
from fitting_system.models import Product, ProductVariant, Inventory, Color, Size
|
| 10 |
+
from fitting_system.color_palettes import get_all_unique_colors
|
| 11 |
+
|
| 12 |
+
# ── 8 catalog colors — names MUST match color_palettes.py exactly ──────────
|
| 13 |
+
# These span multiple skin tones so every customer type is covered:
|
| 14 |
+
# Ash Grey → very_light pants Sky Blue → light shirts
|
| 15 |
+
# Black → very_light/light White → light shirts
|
| 16 |
+
# Blush Pink → very_light shirts Navy → tan shirts
|
| 17 |
+
# Cream → tan shirts Burgundy → intermediate pants
|
| 18 |
+
CATALOG_COLOR_NAMES = [
|
| 19 |
+
'Ash Grey', # #cfd8dc
|
| 20 |
+
'Black', # #111111
|
| 21 |
+
'White', # #ffffff
|
| 22 |
+
'Navy', # #01579b
|
| 23 |
+
'Cream', # #fffde7
|
| 24 |
+
'Burgundy', # #880e4f
|
| 25 |
+
'Sky Blue', # #bbdefb
|
| 26 |
+
'Blush Pink', # #f7c5d0
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
CATALOG = [
|
| 30 |
+
{
|
| 31 |
+
'name': 'Classic Cotton Shirt',
|
| 32 |
+
'category': 'shirt',
|
| 33 |
+
'gender': 'men',
|
| 34 |
+
'price': '49.99',
|
| 35 |
+
'description': (
|
| 36 |
+
'A timeless classic cotton shirt perfect for any occasion. '
|
| 37 |
+
'Made from 100% premium cotton for maximum comfort and breathability.'
|
| 38 |
+
),
|
| 39 |
+
'stock': [
|
| 40 |
+
# size, color, qty
|
| 41 |
+
('S', 'Black', 45),
|
| 42 |
+
('S', 'White', 50),
|
| 43 |
+
('S', 'Navy', 30),
|
| 44 |
+
('S', 'Ash Grey', 20),
|
| 45 |
+
('S', 'Cream', 4), # low
|
| 46 |
+
('S', 'Burgundy', 0), # OOS
|
| 47 |
+
('S', 'Sky Blue', 22),
|
| 48 |
+
('S', 'Blush Pink', 0), # OOS
|
| 49 |
+
('M', 'Black', 50),
|
| 50 |
+
('M', 'White', 50),
|
| 51 |
+
('M', 'Navy', 35),
|
| 52 |
+
('M', 'Ash Grey', 28),
|
| 53 |
+
('M', 'Cream', 15),
|
| 54 |
+
('M', 'Burgundy', 3), # low
|
| 55 |
+
('M', 'Sky Blue', 40),
|
| 56 |
+
('M', 'Blush Pink', 0), # OOS
|
| 57 |
+
('L', 'Black', 38),
|
| 58 |
+
('L', 'White', 42),
|
| 59 |
+
('L', 'Navy', 18),
|
| 60 |
+
('L', 'Ash Grey', 10),
|
| 61 |
+
('L', 'Cream', 2), # low
|
| 62 |
+
('L', 'Burgundy', 0), # OOS
|
| 63 |
+
('L', 'Sky Blue', 25),
|
| 64 |
+
('L', 'Blush Pink', 0), # OOS
|
| 65 |
+
('XL', 'Black', 20),
|
| 66 |
+
('XL', 'White', 15),
|
| 67 |
+
('XL', 'Navy', 4), # low
|
| 68 |
+
('XL', 'Ash Grey', 0), # OOS
|
| 69 |
+
('XL', 'Cream', 0), # OOS
|
| 70 |
+
('XL', 'Burgundy', 0), # OOS
|
| 71 |
+
('XL', 'Sky Blue', 8),
|
| 72 |
+
('XL', 'Blush Pink', 0), # OOS
|
| 73 |
+
],
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
'name': 'Casual Denim Jeans',
|
| 77 |
+
'category': 'pants',
|
| 78 |
+
'gender': 'men',
|
| 79 |
+
'price': '79.99',
|
| 80 |
+
'description': (
|
| 81 |
+
'Comfortable denim jeans with a classic fit. Durable and stylish '
|
| 82 |
+
'for everyday wear with premium denim fabric.'
|
| 83 |
+
),
|
| 84 |
+
'stock': [
|
| 85 |
+
('S', 'Black', 30),
|
| 86 |
+
('S', 'White', 0), # OOS
|
| 87 |
+
('S', 'Navy', 25),
|
| 88 |
+
('S', 'Ash Grey', 12),
|
| 89 |
+
('S', 'Cream', 0), # OOS
|
| 90 |
+
('S', 'Burgundy', 0), # OOS
|
| 91 |
+
('S', 'Sky Blue', 40),
|
| 92 |
+
('S', 'Blush Pink', 0), # OOS
|
| 93 |
+
('M', 'Black', 45),
|
| 94 |
+
('M', 'White', 0), # OOS
|
| 95 |
+
('M', 'Navy', 38),
|
| 96 |
+
('M', 'Ash Grey', 20),
|
| 97 |
+
('M', 'Cream', 0), # OOS
|
| 98 |
+
('M', 'Burgundy', 0), # OOS
|
| 99 |
+
('M', 'Sky Blue', 50),
|
| 100 |
+
('M', 'Blush Pink', 0), # OOS
|
| 101 |
+
('L', 'Black', 35),
|
| 102 |
+
('L', 'White', 0), # OOS
|
| 103 |
+
('L', 'Navy', 22),
|
| 104 |
+
('L', 'Ash Grey', 5), # low
|
| 105 |
+
('L', 'Cream', 0), # OOS
|
| 106 |
+
('L', 'Burgundy', 0), # OOS
|
| 107 |
+
('L', 'Sky Blue', 28),
|
| 108 |
+
('L', 'Blush Pink', 0), # OOS
|
| 109 |
+
('XL', 'Black', 18),
|
| 110 |
+
('XL', 'White', 0), # OOS
|
| 111 |
+
('XL', 'Navy', 3), # low
|
| 112 |
+
('XL', 'Ash Grey', 0), # OOS
|
| 113 |
+
('XL', 'Cream', 0), # OOS
|
| 114 |
+
('XL', 'Burgundy', 0), # OOS
|
| 115 |
+
('XL', 'Sky Blue', 10),
|
| 116 |
+
('XL', 'Blush Pink', 0), # OOS
|
| 117 |
+
],
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
'name': 'Oversized Blouse',
|
| 121 |
+
'category': 'shirt',
|
| 122 |
+
'gender': 'women',
|
| 123 |
+
'price': '54.99',
|
| 124 |
+
'description': (
|
| 125 |
+
'Relaxed oversized blouse with an elegant drape. Perfect for '
|
| 126 |
+
'casual or smart-casual occasions with lightweight breathable fabric.'
|
| 127 |
+
),
|
| 128 |
+
'stock': [
|
| 129 |
+
('S', 'Black', 35),
|
| 130 |
+
('S', 'White', 40),
|
| 131 |
+
('S', 'Navy', 0), # OOS
|
| 132 |
+
('S', 'Ash Grey', 8),
|
| 133 |
+
('S', 'Cream', 30),
|
| 134 |
+
('S', 'Burgundy', 12),
|
| 135 |
+
('S', 'Sky Blue', 4), # low
|
| 136 |
+
('S', 'Blush Pink', 45),
|
| 137 |
+
('M', 'Black', 40),
|
| 138 |
+
('M', 'White', 50),
|
| 139 |
+
('M', 'Navy', 0), # OOS
|
| 140 |
+
('M', 'Ash Grey', 15),
|
| 141 |
+
('M', 'Cream', 35),
|
| 142 |
+
('M', 'Burgundy', 20),
|
| 143 |
+
('M', 'Sky Blue', 2), # low
|
| 144 |
+
('M', 'Blush Pink', 48),
|
| 145 |
+
('L', 'Black', 28),
|
| 146 |
+
('L', 'White', 32),
|
| 147 |
+
('L', 'Navy', 0), # OOS
|
| 148 |
+
('L', 'Ash Grey', 3), # low
|
| 149 |
+
('L', 'Cream', 18),
|
| 150 |
+
('L', 'Burgundy', 5), # low
|
| 151 |
+
('L', 'Sky Blue', 0), # OOS
|
| 152 |
+
('L', 'Blush Pink', 30),
|
| 153 |
+
('XL', 'Black', 10),
|
| 154 |
+
('XL', 'White', 12),
|
| 155 |
+
('XL', 'Navy', 0), # OOS
|
| 156 |
+
('XL', 'Ash Grey', 0), # OOS
|
| 157 |
+
('XL', 'Cream', 4), # low
|
| 158 |
+
('XL', 'Burgundy', 0), # OOS
|
| 159 |
+
('XL', 'Sky Blue', 0), # OOS
|
| 160 |
+
('XL', 'Blush Pink', 8),
|
| 161 |
+
],
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
'name': 'Slim Trousers',
|
| 165 |
+
'category': 'pants',
|
| 166 |
+
'gender': 'women',
|
| 167 |
+
'price': '69.99',
|
| 168 |
+
'description': (
|
| 169 |
+
'Tailored slim trousers with a flattering silhouette. '
|
| 170 |
+
'Versatile for office or casual settings with a premium fabric blend.'
|
| 171 |
+
),
|
| 172 |
+
'stock': [
|
| 173 |
+
('S', 'Black', 48),
|
| 174 |
+
('S', 'White', 10),
|
| 175 |
+
('S', 'Navy', 22),
|
| 176 |
+
('S', 'Ash Grey', 18),
|
| 177 |
+
('S', 'Cream', 25),
|
| 178 |
+
('S', 'Burgundy', 4), # low
|
| 179 |
+
('S', 'Sky Blue', 0), # OOS
|
| 180 |
+
('S', 'Blush Pink', 0), # OOS
|
| 181 |
+
('M', 'Black', 50),
|
| 182 |
+
('M', 'White', 14),
|
| 183 |
+
('M', 'Navy', 30),
|
| 184 |
+
('M', 'Ash Grey', 22),
|
| 185 |
+
('M', 'Cream', 28),
|
| 186 |
+
('M', 'Burgundy', 3), # low
|
| 187 |
+
('M', 'Sky Blue', 0), # OOS
|
| 188 |
+
('M', 'Blush Pink', 0), # OOS
|
| 189 |
+
('L', 'Black', 35),
|
| 190 |
+
('L', 'White', 5), # low
|
| 191 |
+
('L', 'Navy', 16),
|
| 192 |
+
('L', 'Ash Grey', 10),
|
| 193 |
+
('L', 'Cream', 12),
|
| 194 |
+
('L', 'Burgundy', 0), # OOS
|
| 195 |
+
('L', 'Sky Blue', 0), # OOS
|
| 196 |
+
('L', 'Blush Pink', 0), # OOS
|
| 197 |
+
('XL', 'Black', 20),
|
| 198 |
+
('XL', 'White', 0), # OOS
|
| 199 |
+
('XL', 'Navy', 4), # low
|
| 200 |
+
('XL', 'Ash Grey', 0), # OOS
|
| 201 |
+
('XL', 'Cream', 6),
|
| 202 |
+
('XL', 'Burgundy', 0), # OOS
|
| 203 |
+
('XL', 'Sky Blue', 0), # OOS
|
| 204 |
+
('XL', 'Blush Pink', 0), # OOS
|
| 205 |
+
],
|
| 206 |
+
},
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class Command(BaseCommand):
|
| 211 |
+
help = (
|
| 212 |
+
'Wipe all products/variants/inventory and re-seed the catalog with '
|
| 213 |
+
'exactly 4 products (128 variants) using colors from color_palettes.py.'
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
def handle(self, *args, **options):
|
| 217 |
+
# ── Step 1: wipe old data ──────────────────────────────────────────
|
| 218 |
+
self.stdout.write('🗑️ Deleting existing products, variants, inventory and colors...')
|
| 219 |
+
Inventory.objects.all().delete()
|
| 220 |
+
ProductVariant.objects.all().delete()
|
| 221 |
+
Product.objects.all().delete()
|
| 222 |
+
Color.objects.all().delete()
|
| 223 |
+
self.stdout.write(self.style.SUCCESS(' Done.'))
|
| 224 |
+
|
| 225 |
+
# ── Step 2: seed ALL colors from color_palettes.py ─────────────────
|
| 226 |
+
self.stdout.write('🎨 Seeding colors from unified palette...')
|
| 227 |
+
palette_colors = get_all_unique_colors()
|
| 228 |
+
for c in palette_colors:
|
| 229 |
+
Color.objects.get_or_create(
|
| 230 |
+
name=c['name'],
|
| 231 |
+
defaults={'hex_code': c['hex'], 'category': 'neutral'},
|
| 232 |
+
)
|
| 233 |
+
self.stdout.write(f' {len(palette_colors)} colors seeded.')
|
| 234 |
+
|
| 235 |
+
# ── Step 3: verify all 8 catalog colors exist ───────────────────────
|
| 236 |
+
missing = [n for n in CATALOG_COLOR_NAMES if not Color.objects.filter(name=n).exists()]
|
| 237 |
+
if missing:
|
| 238 |
+
self.stdout.write(self.style.ERROR(
|
| 239 |
+
f'❌ Catalog colors not found in color_palettes.py: {missing}\n'
|
| 240 |
+
f' Add them to color_palettes.py and rerun.'
|
| 241 |
+
))
|
| 242 |
+
return
|
| 243 |
+
|
| 244 |
+
# ── Step 4: ensure sizes exist ───────────────────��──────────────────
|
| 245 |
+
sizes_data = [
|
| 246 |
+
{'name': 'S', 'chest_min': 85, 'chest_max': 92, 'waist_min': 70, 'waist_max': 77,
|
| 247 |
+
'shoulder_min': 40, 'shoulder_max': 43, 'height_min': 160, 'height_max': 170},
|
| 248 |
+
{'name': 'M', 'chest_min': 93, 'chest_max': 100, 'waist_min': 78, 'waist_max': 85,
|
| 249 |
+
'shoulder_min': 44, 'shoulder_max': 47, 'height_min': 168, 'height_max': 178},
|
| 250 |
+
{'name': 'L', 'chest_min': 101, 'chest_max': 108, 'waist_min': 86, 'waist_max': 93,
|
| 251 |
+
'shoulder_min': 48, 'shoulder_max': 51, 'height_min': 175, 'height_max': 185},
|
| 252 |
+
{'name': 'XL', 'chest_min': 109, 'chest_max': 116, 'waist_min': 94, 'waist_max': 101,
|
| 253 |
+
'shoulder_min': 52, 'shoulder_max': 55, 'height_min': 180, 'height_max': 190},
|
| 254 |
+
]
|
| 255 |
+
for sd in sizes_data:
|
| 256 |
+
Size.objects.get_or_create(name=sd['name'], defaults=sd)
|
| 257 |
+
|
| 258 |
+
# ── Step 5: create products + variants ──────────────────────────────
|
| 259 |
+
counts = {'products': 0, 'variants': 0, 'in_stock': 0, 'low_stock': 0, 'oos': 0}
|
| 260 |
+
|
| 261 |
+
for item in CATALOG:
|
| 262 |
+
product = Product.objects.create(
|
| 263 |
+
name=item['name'],
|
| 264 |
+
category=item['category'],
|
| 265 |
+
gender=item['gender'],
|
| 266 |
+
price=item['price'],
|
| 267 |
+
description=item['description'],
|
| 268 |
+
)
|
| 269 |
+
counts['products'] += 1
|
| 270 |
+
self.stdout.write(f' ✅ Product: {product.name}')
|
| 271 |
+
|
| 272 |
+
for size_name, color_name, qty in item['stock']:
|
| 273 |
+
size = Size.objects.filter(name=size_name).first()
|
| 274 |
+
color = Color.objects.filter(name=color_name).first()
|
| 275 |
+
|
| 276 |
+
if not size:
|
| 277 |
+
self.stdout.write(self.style.WARNING(f' ⚠️ Size "{size_name}" not found — skipping'))
|
| 278 |
+
continue
|
| 279 |
+
if not color:
|
| 280 |
+
self.stdout.write(self.style.WARNING(f' ⚠️ Color "{color_name}" not found — skipping'))
|
| 281 |
+
continue
|
| 282 |
+
|
| 283 |
+
sku = f'{product.id}-{size_name}-{color_name[:3].upper()}'
|
| 284 |
+
variant, _ = ProductVariant.objects.get_or_create(
|
| 285 |
+
product=product, size=size, color=color,
|
| 286 |
+
defaults={'sku': sku},
|
| 287 |
+
)
|
| 288 |
+
Inventory.objects.update_or_create(
|
| 289 |
+
product_variant=variant,
|
| 290 |
+
defaults={'quantity': qty, 'low_stock_threshold': 5},
|
| 291 |
+
)
|
| 292 |
+
counts['variants'] += 1
|
| 293 |
+
if qty > 5:
|
| 294 |
+
counts['in_stock'] += 1
|
| 295 |
+
elif qty > 0:
|
| 296 |
+
counts['low_stock'] += 1
|
| 297 |
+
else:
|
| 298 |
+
counts['oos'] += 1
|
| 299 |
+
|
| 300 |
+
self.stdout.write('')
|
| 301 |
+
self.stdout.write(self.style.SUCCESS('📦 Catalog reset complete!'))
|
| 302 |
+
self.stdout.write(f" Products : {counts['products']} (expected 4)")
|
| 303 |
+
self.stdout.write(f" Variants : {counts['variants']} (expected 128)")
|
| 304 |
+
self.stdout.write(f" In stock : {counts['in_stock']}")
|
| 305 |
+
self.stdout.write(f" Low stock : {counts['low_stock']}")
|
| 306 |
+
self.stdout.write(f" Out of stock : {counts['oos']}")
|
| 307 |
+
self.stdout.write(f" Colors in DB : {Color.objects.count()}")
|
fitting_system/management/commands/set_password.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.core.management.base import BaseCommand
|
| 2 |
+
from django.contrib.auth import get_user_model
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Command(BaseCommand):
|
| 6 |
+
help = 'Set admin password to admin123'
|
| 7 |
+
|
| 8 |
+
def handle(self, *args, **kwargs):
|
| 9 |
+
User = get_user_model()
|
| 10 |
+
try:
|
| 11 |
+
admin = User.objects.get(username='admin')
|
| 12 |
+
admin.set_password('admin123')
|
| 13 |
+
admin.save()
|
| 14 |
+
self.stdout.write(self.style.SUCCESS('Password set successfully for admin user'))
|
| 15 |
+
self.stdout.write('Username: admin')
|
| 16 |
+
self.stdout.write('Password: admin123')
|
| 17 |
+
except User.DoesNotExist:
|
| 18 |
+
self.stdout.write(self.style.ERROR('Admin user not found'))
|
fitting_system/migrations/0001_initial.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.5 on 2026-01-07 12:09
|
| 2 |
+
|
| 3 |
+
import django.db.models.deletion
|
| 4 |
+
import uuid
|
| 5 |
+
from django.db import migrations, models
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Migration(migrations.Migration):
|
| 9 |
+
|
| 10 |
+
initial = True
|
| 11 |
+
|
| 12 |
+
dependencies = [
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
operations = [
|
| 16 |
+
migrations.CreateModel(
|
| 17 |
+
name='BodyScan',
|
| 18 |
+
fields=[
|
| 19 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 20 |
+
('session_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
| 21 |
+
('height', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 22 |
+
('shoulder_width', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 23 |
+
('chest', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 24 |
+
('waist', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 25 |
+
('skin_tone', models.CharField(choices=[('light', 'Light'), ('medium', 'Medium'), ('dark', 'Dark')], max_length=10)),
|
| 26 |
+
('scanned_at', models.DateTimeField(auto_now_add=True)),
|
| 27 |
+
],
|
| 28 |
+
options={
|
| 29 |
+
'ordering': ['-scanned_at'],
|
| 30 |
+
},
|
| 31 |
+
),
|
| 32 |
+
migrations.CreateModel(
|
| 33 |
+
name='Color',
|
| 34 |
+
fields=[
|
| 35 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 36 |
+
('name', models.CharField(max_length=50)),
|
| 37 |
+
('hex_code', models.CharField(max_length=7)),
|
| 38 |
+
('category', models.CharField(choices=[('light', 'Light Colors'), ('medium', 'Medium Colors'), ('dark', 'Dark Colors'), ('neutral', 'Neutral Colors'), ('vibrant', 'Vibrant Colors')], max_length=20)),
|
| 39 |
+
],
|
| 40 |
+
options={
|
| 41 |
+
'ordering': ['name'],
|
| 42 |
+
},
|
| 43 |
+
),
|
| 44 |
+
migrations.CreateModel(
|
| 45 |
+
name='Product',
|
| 46 |
+
fields=[
|
| 47 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 48 |
+
('name', models.CharField(max_length=200)),
|
| 49 |
+
('category', models.CharField(choices=[('shirt', 'Shirt'), ('pants', 'Pants'), ('jacket', 'Jacket'), ('dress', 'Dress'), ('skirt', 'Skirt')], max_length=20)),
|
| 50 |
+
('fit_type', models.CharField(choices=[('slim', 'Slim Fit'), ('regular', 'Regular Fit'), ('oversize', 'Oversize Fit')], max_length=20)),
|
| 51 |
+
('gender', models.CharField(choices=[('men', 'Men'), ('women', 'Women'), ('unisex', 'Unisex')], max_length=10)),
|
| 52 |
+
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
| 53 |
+
('description', models.TextField()),
|
| 54 |
+
('image_url', models.URLField(blank=True, null=True)),
|
| 55 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 56 |
+
],
|
| 57 |
+
options={
|
| 58 |
+
'ordering': ['name'],
|
| 59 |
+
},
|
| 60 |
+
),
|
| 61 |
+
migrations.CreateModel(
|
| 62 |
+
name='Size',
|
| 63 |
+
fields=[
|
| 64 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 65 |
+
('name', models.CharField(max_length=10, unique=True)),
|
| 66 |
+
('chest_min', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 67 |
+
('chest_max', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 68 |
+
('waist_min', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 69 |
+
('waist_max', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 70 |
+
('shoulder_min', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 71 |
+
('shoulder_max', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 72 |
+
('height_min', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 73 |
+
('height_max', models.DecimalField(decimal_places=2, max_digits=5)),
|
| 74 |
+
],
|
| 75 |
+
options={
|
| 76 |
+
'ordering': ['id'],
|
| 77 |
+
},
|
| 78 |
+
),
|
| 79 |
+
migrations.CreateModel(
|
| 80 |
+
name='ProductVariant',
|
| 81 |
+
fields=[
|
| 82 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 83 |
+
('sku', models.CharField(max_length=50, unique=True)),
|
| 84 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 85 |
+
('color', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fitting_system.color')),
|
| 86 |
+
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variants', to='fitting_system.product')),
|
| 87 |
+
('size', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fitting_system.size')),
|
| 88 |
+
],
|
| 89 |
+
options={
|
| 90 |
+
'ordering': ['product', 'size', 'color'],
|
| 91 |
+
'unique_together': {('product', 'size', 'color')},
|
| 92 |
+
},
|
| 93 |
+
),
|
| 94 |
+
migrations.CreateModel(
|
| 95 |
+
name='Inventory',
|
| 96 |
+
fields=[
|
| 97 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 98 |
+
('quantity', models.IntegerField(default=0)),
|
| 99 |
+
('low_stock_threshold', models.IntegerField(default=5)),
|
| 100 |
+
('last_updated', models.DateTimeField(auto_now=True)),
|
| 101 |
+
('product_variant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='inventory', to='fitting_system.productvariant')),
|
| 102 |
+
],
|
| 103 |
+
options={
|
| 104 |
+
'verbose_name_plural': 'Inventories',
|
| 105 |
+
},
|
| 106 |
+
),
|
| 107 |
+
migrations.CreateModel(
|
| 108 |
+
name='Recommendation',
|
| 109 |
+
fields=[
|
| 110 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 111 |
+
('recommended_size', models.CharField(max_length=10)),
|
| 112 |
+
('recommended_fit', models.CharField(max_length=20)),
|
| 113 |
+
('recommended_colors', models.TextField()),
|
| 114 |
+
('priority', models.IntegerField(default=0)),
|
| 115 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 116 |
+
('body_scan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recommendations', to='fitting_system.bodyscan')),
|
| 117 |
+
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fitting_system.product')),
|
| 118 |
+
],
|
| 119 |
+
options={
|
| 120 |
+
'ordering': ['-priority', 'product'],
|
| 121 |
+
},
|
| 122 |
+
),
|
| 123 |
+
]
|
fitting_system/migrations/0002_add_undertone_and_expand_skin_tone.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.5 on 2026-01-31 13:51
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('fitting_system', '0001_initial'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AddField(
|
| 14 |
+
model_name='bodyscan',
|
| 15 |
+
name='undertone',
|
| 16 |
+
field=models.CharField(choices=[('warm', 'Warm'), ('cool', 'Cool')], default='warm', max_length=10),
|
| 17 |
+
),
|
| 18 |
+
migrations.AlterField(
|
| 19 |
+
model_name='bodyscan',
|
| 20 |
+
name='skin_tone',
|
| 21 |
+
field=models.CharField(choices=[('very_light', 'Very Light'), ('light', 'Light'), ('intermediate', 'Intermediate'), ('tan', 'Tan'), ('dark', 'Dark')], max_length=15),
|
| 22 |
+
),
|
| 23 |
+
]
|
fitting_system/migrations/0003_bodyscan_arm_length_bodyscan_body_shape_and_more.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.5 on 2026-02-01 13:56
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('fitting_system', '0002_add_undertone_and_expand_skin_tone'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AddField(
|
| 14 |
+
model_name='bodyscan',
|
| 15 |
+
name='arm_length',
|
| 16 |
+
field=models.DecimalField(blank=True, decimal_places=1, max_digits=5, null=True),
|
| 17 |
+
),
|
| 18 |
+
migrations.AddField(
|
| 19 |
+
model_name='bodyscan',
|
| 20 |
+
name='body_shape',
|
| 21 |
+
field=models.CharField(blank=True, choices=[('hourglass', 'Hourglass'), ('rectangle', 'Rectangle'), ('triangle', 'Triangle (Pear)'), ('inverted_triangle', 'Inverted Triangle (Athletic)'), ('oval', 'Oval (Apple)')], max_length=20, null=True),
|
| 22 |
+
),
|
| 23 |
+
migrations.AddField(
|
| 24 |
+
model_name='bodyscan',
|
| 25 |
+
name='confidence_score',
|
| 26 |
+
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=3),
|
| 27 |
+
),
|
| 28 |
+
migrations.AddField(
|
| 29 |
+
model_name='bodyscan',
|
| 30 |
+
name='frame_count',
|
| 31 |
+
field=models.IntegerField(default=1),
|
| 32 |
+
),
|
| 33 |
+
migrations.AddField(
|
| 34 |
+
model_name='bodyscan',
|
| 35 |
+
name='hip',
|
| 36 |
+
field=models.DecimalField(blank=True, decimal_places=1, max_digits=5, null=True),
|
| 37 |
+
),
|
| 38 |
+
migrations.AddField(
|
| 39 |
+
model_name='bodyscan',
|
| 40 |
+
name='inseam',
|
| 41 |
+
field=models.DecimalField(blank=True, decimal_places=1, max_digits=5, null=True),
|
| 42 |
+
),
|
| 43 |
+
migrations.AddField(
|
| 44 |
+
model_name='bodyscan',
|
| 45 |
+
name='torso_length',
|
| 46 |
+
field=models.DecimalField(blank=True, decimal_places=1, max_digits=5, null=True),
|
| 47 |
+
),
|
| 48 |
+
migrations.AlterField(
|
| 49 |
+
model_name='bodyscan',
|
| 50 |
+
name='chest',
|
| 51 |
+
field=models.DecimalField(decimal_places=1, max_digits=5),
|
| 52 |
+
),
|
| 53 |
+
migrations.AlterField(
|
| 54 |
+
model_name='bodyscan',
|
| 55 |
+
name='height',
|
| 56 |
+
field=models.DecimalField(decimal_places=1, max_digits=5),
|
| 57 |
+
),
|
| 58 |
+
migrations.AlterField(
|
| 59 |
+
model_name='bodyscan',
|
| 60 |
+
name='shoulder_width',
|
| 61 |
+
field=models.DecimalField(decimal_places=1, max_digits=5),
|
| 62 |
+
),
|
| 63 |
+
migrations.AlterField(
|
| 64 |
+
model_name='bodyscan',
|
| 65 |
+
name='waist',
|
| 66 |
+
field=models.DecimalField(decimal_places=1, max_digits=5),
|
| 67 |
+
),
|
| 68 |
+
]
|
fitting_system/migrations/0004_bodyscan_error_message_bodyscan_is_fallback.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 4.2.7 on 2026-02-14 12:34
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('fitting_system', '0003_bodyscan_arm_length_bodyscan_body_shape_and_more'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AddField(
|
| 14 |
+
model_name='bodyscan',
|
| 15 |
+
name='error_message',
|
| 16 |
+
field=models.TextField(blank=True, null=True),
|
| 17 |
+
),
|
| 18 |
+
migrations.AddField(
|
| 19 |
+
model_name='bodyscan',
|
| 20 |
+
name='is_fallback',
|
| 21 |
+
field=models.BooleanField(default=False),
|
| 22 |
+
),
|
| 23 |
+
]
|
fitting_system/migrations/0004_product_image.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 4.2.7 on 2026-02-24 03:25
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('fitting_system', '0003_bodyscan_arm_length_bodyscan_body_shape_and_more'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AddField(
|
| 14 |
+
model_name='product',
|
| 15 |
+
name='image',
|
| 16 |
+
field=models.ImageField(blank=True, null=True, upload_to='products/'),
|
| 17 |
+
),
|
| 18 |
+
]
|
fitting_system/migrations/0005_remove_product_image_url.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 4.2.7 on 2026-02-24 21:16
|
| 2 |
+
|
| 3 |
+
from django.db import migrations
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('fitting_system', '0004_product_image'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.RemoveField(
|
| 14 |
+
model_name='product',
|
| 15 |
+
name='image_url',
|
| 16 |
+
),
|
| 17 |
+
]
|
fitting_system/migrations/0006_merge_20260225_1734.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 5.2.5 on 2026-02-25 14:34
|
| 2 |
+
|
| 3 |
+
from django.db import migrations
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('fitting_system', '0004_bodyscan_error_message_bodyscan_is_fallback'),
|
| 10 |
+
('fitting_system', '0005_remove_product_image_url'),
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
operations = [
|
| 14 |
+
]
|
fitting_system/migrations/0007_alter_product_image.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 4.2.7 on 2026-03-01 20:29
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('fitting_system', '0006_merge_20260225_1734'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.AlterField(
|
| 14 |
+
model_name='product',
|
| 15 |
+
name='image',
|
| 16 |
+
field=models.ImageField(blank=True, null=True, upload_to='images/products/'),
|
| 17 |
+
),
|
| 18 |
+
]
|
fitting_system/migrations/0008_alter_product_image.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 4.2.7 on 2026-03-01 20:58
|
| 2 |
+
|
| 3 |
+
from django.db import migrations, models
|
| 4 |
+
import fitting_system.storage
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Migration(migrations.Migration):
|
| 8 |
+
|
| 9 |
+
dependencies = [
|
| 10 |
+
('fitting_system', '0007_alter_product_image'),
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
operations = [
|
| 14 |
+
migrations.AlterField(
|
| 15 |
+
model_name='product',
|
| 16 |
+
name='image',
|
| 17 |
+
field=models.ImageField(blank=True, null=True, storage=fitting_system.storage.ProductImageStorage(), upload_to='images/products/'),
|
| 18 |
+
),
|
| 19 |
+
]
|
fitting_system/migrations/0009_remove_product_fit_type.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 4.2.7 on 2026-03-02 22:53
|
| 2 |
+
|
| 3 |
+
from django.db import migrations
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Migration(migrations.Migration):
|
| 7 |
+
|
| 8 |
+
dependencies = [
|
| 9 |
+
('fitting_system', '0008_alter_product_image'),
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
operations = [
|
| 13 |
+
migrations.RemoveField(
|
| 14 |
+
model_name='product',
|
| 15 |
+
name='fit_type',
|
| 16 |
+
),
|
| 17 |
+
]
|
fitting_system/migrations/__init__.py
ADDED
|
File without changes
|
fitting_system/models.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.db import models
|
| 2 |
+
from django.utils import timezone
|
| 3 |
+
import uuid
|
| 4 |
+
from .storage import ProductImageStorage
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
product_image_storage = ProductImageStorage()
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Size(models.Model):
|
| 11 |
+
"""Size definitions with measurement ranges"""
|
| 12 |
+
name = models.CharField(max_length=10, unique=True) # S, M, L, XL, XXL
|
| 13 |
+
chest_min = models.DecimalField(max_digits=5, decimal_places=2) # cm
|
| 14 |
+
chest_max = models.DecimalField(max_digits=5, decimal_places=2) # cm
|
| 15 |
+
waist_min = models.DecimalField(max_digits=5, decimal_places=2) # cm
|
| 16 |
+
waist_max = models.DecimalField(max_digits=5, decimal_places=2) # cm
|
| 17 |
+
shoulder_min = models.DecimalField(max_digits=5, decimal_places=2) # cm
|
| 18 |
+
shoulder_max = models.DecimalField(max_digits=5, decimal_places=2) # cm
|
| 19 |
+
height_min = models.DecimalField(max_digits=5, decimal_places=2) # cm
|
| 20 |
+
height_max = models.DecimalField(max_digits=5, decimal_places=2) # cm
|
| 21 |
+
|
| 22 |
+
class Meta:
|
| 23 |
+
ordering = ['id']
|
| 24 |
+
|
| 25 |
+
def __str__(self):
|
| 26 |
+
return self.name
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class Color(models.Model):
|
| 30 |
+
"""Color definitions with hex codes"""
|
| 31 |
+
CATEGORY_CHOICES = [
|
| 32 |
+
('light', 'Light Colors'),
|
| 33 |
+
('medium', 'Medium Colors'),
|
| 34 |
+
('dark', 'Dark Colors'),
|
| 35 |
+
('neutral', 'Neutral Colors'),
|
| 36 |
+
('vibrant', 'Vibrant Colors'),
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
name = models.CharField(max_length=50)
|
| 40 |
+
hex_code = models.CharField(max_length=7) # e.g., #FF5733
|
| 41 |
+
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
|
| 42 |
+
|
| 43 |
+
class Meta:
|
| 44 |
+
ordering = ['name']
|
| 45 |
+
|
| 46 |
+
def __str__(self):
|
| 47 |
+
return self.name
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class Product(models.Model):
|
| 51 |
+
"""Clothing products"""
|
| 52 |
+
CATEGORY_CHOICES = [
|
| 53 |
+
('shirt', 'Shirt'),
|
| 54 |
+
('pants', 'Pants'),
|
| 55 |
+
('jacket', 'Jacket'),
|
| 56 |
+
('dress', 'Dress'),
|
| 57 |
+
('skirt', 'Skirt'),
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
GENDER_CHOICES = [
|
| 61 |
+
('men', 'Men'),
|
| 62 |
+
('women', 'Women'),
|
| 63 |
+
('unisex', 'Unisex'),
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
name = models.CharField(max_length=200)
|
| 67 |
+
category = models.CharField(max_length=20, choices=CATEGORY_CHOICES)
|
| 68 |
+
gender = models.CharField(max_length=10, choices=GENDER_CHOICES)
|
| 69 |
+
price = models.DecimalField(max_digits=10, decimal_places=2)
|
| 70 |
+
description = models.TextField()
|
| 71 |
+
image = models.ImageField(
|
| 72 |
+
upload_to='images/products/',
|
| 73 |
+
storage=product_image_storage,
|
| 74 |
+
blank=True,
|
| 75 |
+
null=True,
|
| 76 |
+
)
|
| 77 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 78 |
+
|
| 79 |
+
class Meta:
|
| 80 |
+
ordering = ['name']
|
| 81 |
+
|
| 82 |
+
def __str__(self):
|
| 83 |
+
return f"{self.name} ({self.gender})"
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class ProductVariant(models.Model):
|
| 87 |
+
"""Combination of product + size + color"""
|
| 88 |
+
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='variants')
|
| 89 |
+
size = models.ForeignKey(Size, on_delete=models.CASCADE)
|
| 90 |
+
color = models.ForeignKey(Color, on_delete=models.CASCADE)
|
| 91 |
+
sku = models.CharField(max_length=50, unique=True)
|
| 92 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 93 |
+
|
| 94 |
+
class Meta:
|
| 95 |
+
ordering = ['product', 'size', 'color']
|
| 96 |
+
unique_together = ['product', 'size', 'color']
|
| 97 |
+
|
| 98 |
+
def __str__(self):
|
| 99 |
+
return f"{self.product.name} - {self.size.name} - {self.color.name}"
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class Inventory(models.Model):
|
| 103 |
+
"""Stock tracking for each variant"""
|
| 104 |
+
product_variant = models.OneToOneField(ProductVariant, on_delete=models.CASCADE, related_name='inventory')
|
| 105 |
+
quantity = models.IntegerField(default=0)
|
| 106 |
+
low_stock_threshold = models.IntegerField(default=5)
|
| 107 |
+
last_updated = models.DateTimeField(auto_now=True)
|
| 108 |
+
|
| 109 |
+
class Meta:
|
| 110 |
+
verbose_name_plural = 'Inventories'
|
| 111 |
+
|
| 112 |
+
def __str__(self):
|
| 113 |
+
return f"{self.product_variant} - Stock: {self.quantity}"
|
| 114 |
+
|
| 115 |
+
@property
|
| 116 |
+
def is_low_stock(self):
|
| 117 |
+
return 0 < self.quantity <= self.low_stock_threshold
|
| 118 |
+
|
| 119 |
+
@property
|
| 120 |
+
def is_out_of_stock(self):
|
| 121 |
+
return self.quantity <= 0
|
| 122 |
+
|
| 123 |
+
@property
|
| 124 |
+
def is_available(self):
|
| 125 |
+
return self.quantity > 0
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class BodyScan(models.Model):
|
| 129 |
+
"""Stored measurement data (no images)"""
|
| 130 |
+
SKIN_TONE_CHOICES = [
|
| 131 |
+
('very_light', 'Very Light'),
|
| 132 |
+
('light', 'Light'),
|
| 133 |
+
('intermediate', 'Intermediate'),
|
| 134 |
+
('tan', 'Tan'),
|
| 135 |
+
('dark', 'Dark'),
|
| 136 |
+
]
|
| 137 |
+
|
| 138 |
+
UNDERTONE_CHOICES = [
|
| 139 |
+
('warm', 'Warm'),
|
| 140 |
+
('cool', 'Cool'),
|
| 141 |
+
]
|
| 142 |
+
|
| 143 |
+
BODY_SHAPE_CHOICES = [
|
| 144 |
+
('hourglass', 'Hourglass'),
|
| 145 |
+
('rectangle', 'Rectangle'),
|
| 146 |
+
('triangle', 'Triangle (Pear)'),
|
| 147 |
+
('inverted_triangle', 'Inverted Triangle (Athletic)'),
|
| 148 |
+
('oval', 'Oval (Apple)'),
|
| 149 |
+
]
|
| 150 |
+
|
| 151 |
+
session_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
| 152 |
+
|
| 153 |
+
# Core measurements (required)
|
| 154 |
+
height = models.DecimalField(max_digits=5, decimal_places=1) # cm - used for calibration
|
| 155 |
+
shoulder_width = models.DecimalField(max_digits=5, decimal_places=1) # cm
|
| 156 |
+
chest = models.DecimalField(max_digits=5, decimal_places=1) # cm - circumference
|
| 157 |
+
waist = models.DecimalField(max_digits=5, decimal_places=1) # cm - circumference
|
| 158 |
+
|
| 159 |
+
# Fashion-specific measurements (nullable for backward compatibility)
|
| 160 |
+
hip = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True) # cm - circumference
|
| 161 |
+
torso_length = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True) # cm
|
| 162 |
+
arm_length = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True) # cm
|
| 163 |
+
inseam = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True) # cm - for pants
|
| 164 |
+
|
| 165 |
+
# Body shape classification
|
| 166 |
+
body_shape = models.CharField(max_length=20, choices=BODY_SHAPE_CHOICES, null=True, blank=True)
|
| 167 |
+
|
| 168 |
+
# Skin analysis
|
| 169 |
+
skin_tone = models.CharField(max_length=15, choices=SKIN_TONE_CHOICES)
|
| 170 |
+
undertone = models.CharField(max_length=10, choices=UNDERTONE_CHOICES, default='warm')
|
| 171 |
+
|
| 172 |
+
# Measurement quality metrics
|
| 173 |
+
confidence_score = models.DecimalField(max_digits=3, decimal_places=2, default=1.0) # 0.0-1.0
|
| 174 |
+
frame_count = models.IntegerField(default=1) # Number of frames used for averaging
|
| 175 |
+
is_fallback = models.BooleanField(default=False)
|
| 176 |
+
error_message = models.TextField(null=True, blank=True)
|
| 177 |
+
|
| 178 |
+
scanned_at = models.DateTimeField(auto_now_add=True)
|
| 179 |
+
|
| 180 |
+
class Meta:
|
| 181 |
+
ordering = ['-scanned_at']
|
| 182 |
+
|
| 183 |
+
def __str__(self):
|
| 184 |
+
return f"Scan {self.session_id} - {self.scanned_at.strftime('%Y-%m-%d %H:%M')}"
|
| 185 |
+
|
| 186 |
+
@property
|
| 187 |
+
def chest_to_waist_ratio(self):
|
| 188 |
+
"""Calculate body proportion ratio for fit recommendation"""
|
| 189 |
+
if self.waist > 0:
|
| 190 |
+
return float(self.chest) / float(self.waist)
|
| 191 |
+
return 1.0
|
| 192 |
+
|
| 193 |
+
@property
|
| 194 |
+
def body_shape_display(self):
|
| 195 |
+
"""Get human-readable body shape name"""
|
| 196 |
+
if self.body_shape:
|
| 197 |
+
return dict(self.BODY_SHAPE_CHOICES).get(self.body_shape, self.body_shape)
|
| 198 |
+
return None
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
class Recommendation(models.Model):
|
| 202 |
+
"""Generated recommendations"""
|
| 203 |
+
body_scan = models.ForeignKey(BodyScan, on_delete=models.CASCADE, related_name='recommendations')
|
| 204 |
+
product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
| 205 |
+
recommended_size = models.CharField(max_length=10)
|
| 206 |
+
recommended_fit = models.CharField(max_length=20)
|
| 207 |
+
recommended_colors = models.TextField() # Comma-separated color names
|
| 208 |
+
priority = models.IntegerField(default=0) # Higher = more relevant
|
| 209 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 210 |
+
|
| 211 |
+
class Meta:
|
| 212 |
+
ordering = ['-priority', 'product']
|
| 213 |
+
|
| 214 |
+
def __str__(self):
|
| 215 |
+
return f"Recommendation for {self.body_scan.session_id} - {self.product.name}"
|
| 216 |
+
|
| 217 |
+
def get_recommended_colors_list(self):
|
| 218 |
+
"""Return recommended colors as a list"""
|
| 219 |
+
return [c.strip() for c in self.recommended_colors.split(',') if c.strip()]
|
fitting_system/storage.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
from django.conf import settings
|
| 4 |
+
from django.core.files.storage import FileSystemStorage
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class ProductImageStorage(FileSystemStorage):
|
| 8 |
+
"""
|
| 9 |
+
Save new product uploads into static/images/products while keeping legacy
|
| 10 |
+
media/products files readable.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, *args, **kwargs):
|
| 14 |
+
static_base_url = settings.STATIC_URL if settings.STATIC_URL.endswith("/") else f"{settings.STATIC_URL}/"
|
| 15 |
+
media_base_url = settings.MEDIA_URL if settings.MEDIA_URL.endswith("/") else f"{settings.MEDIA_URL}/"
|
| 16 |
+
|
| 17 |
+
super().__init__(
|
| 18 |
+
location=Path(settings.BASE_DIR) / "static",
|
| 19 |
+
base_url=static_base_url,
|
| 20 |
+
)
|
| 21 |
+
self.legacy_storage = FileSystemStorage(
|
| 22 |
+
location=Path(settings.BASE_DIR) / "media",
|
| 23 |
+
base_url=media_base_url,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
def exists(self, name):
|
| 27 |
+
return super().exists(name) or self.legacy_storage.exists(name)
|
| 28 |
+
|
| 29 |
+
def open(self, name, mode="rb"):
|
| 30 |
+
if super().exists(name):
|
| 31 |
+
return super().open(name, mode)
|
| 32 |
+
return self.legacy_storage.open(name, mode)
|
| 33 |
+
|
| 34 |
+
def url(self, name):
|
| 35 |
+
if super().exists(name):
|
| 36 |
+
return super().url(name)
|
| 37 |
+
if self.legacy_storage.exists(name):
|
| 38 |
+
return self.legacy_storage.url(name)
|
| 39 |
+
return super().url(name)
|
fitting_system/templates/avatar.html
ADDED
|
@@ -0,0 +1,739 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% load static %}
|
| 3 |
+
{% load i18n %}
|
| 4 |
+
|
| 5 |
+
{% block title %}{% trans "Your 3D Avatar - Virtual Fitting System" %}{% endblock %}
|
| 6 |
+
|
| 7 |
+
{% block extra_css %}
|
| 8 |
+
<style>
|
| 9 |
+
/* Avatar page overrides — dark theme for the 3D viewer area */
|
| 10 |
+
.avatar-page {
|
| 11 |
+
display: flex;
|
| 12 |
+
min-height: calc(100vh - 64px);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/* Sidebar */
|
| 16 |
+
.avatar-sidebar {
|
| 17 |
+
width: 320px;
|
| 18 |
+
min-width: 320px;
|
| 19 |
+
max-height: calc(100vh - 64px);
|
| 20 |
+
background: linear-gradient(160deg, #0f1729 0%, #1B3A6B 60%, #16213e 100%);
|
| 21 |
+
border-right: 1px solid rgba(255, 255, 255, 0.07);
|
| 22 |
+
overflow-y: auto;
|
| 23 |
+
position: relative;
|
| 24 |
+
z-index: 10;
|
| 25 |
+
scrollbar-width: thin;
|
| 26 |
+
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.avatar-sidebar::-webkit-scrollbar {
|
| 30 |
+
width: 4px;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.avatar-sidebar::-webkit-scrollbar-thumb {
|
| 34 |
+
background: rgba(255, 255, 255, 0.1);
|
| 35 |
+
border-radius: 2px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.sidebar-inner {
|
| 39 |
+
padding: 24px 20px;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Canvas area */
|
| 43 |
+
.avatar-canvas-area {
|
| 44 |
+
flex: 1;
|
| 45 |
+
position: relative;
|
| 46 |
+
overflow: hidden;
|
| 47 |
+
background: linear-gradient(135deg, #0f1729 0%, #1a1a2e 50%, #16213e 100%);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.avatar-canvas-area canvas {
|
| 51 |
+
display: block;
|
| 52 |
+
width: 100% !important;
|
| 53 |
+
height: 100% !important;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Section styling */
|
| 57 |
+
.av-section {
|
| 58 |
+
margin-bottom: 20px;
|
| 59 |
+
animation: avFadeUp 0.5s ease both;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.av-section:nth-child(2) {
|
| 63 |
+
animation-delay: 0.05s;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.av-section:nth-child(3) {
|
| 67 |
+
animation-delay: 0.10s;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.av-section:nth-child(4) {
|
| 71 |
+
animation-delay: 0.15s;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.av-section-header {
|
| 75 |
+
display: flex;
|
| 76 |
+
align-items: center;
|
| 77 |
+
gap: 8px;
|
| 78 |
+
margin-bottom: 10px;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.av-section-icon {
|
| 82 |
+
width: 28px;
|
| 83 |
+
height: 28px;
|
| 84 |
+
border-radius: 8px;
|
| 85 |
+
display: flex;
|
| 86 |
+
align-items: center;
|
| 87 |
+
justify-content: center;
|
| 88 |
+
font-size: 13px;
|
| 89 |
+
flex-shrink: 0;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.av-section-icon.skin {
|
| 93 |
+
background: rgba(201, 169, 110, 0.15);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.av-section-icon.shirt {
|
| 97 |
+
background: rgba(99, 179, 237, 0.12);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.av-section-icon.pants {
|
| 101 |
+
background: rgba(154, 117, 255, 0.12);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.av-section-title {
|
| 105 |
+
font-size: 10px;
|
| 106 |
+
font-weight: 500;
|
| 107 |
+
letter-spacing: 2.5px;
|
| 108 |
+
text-transform: uppercase;
|
| 109 |
+
color: rgba(255, 255, 255, 0.45);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/* Skin swatches grid */
|
| 113 |
+
.av-swatches-skin {
|
| 114 |
+
display: grid;
|
| 115 |
+
grid-template-columns: repeat(6, 1fr);
|
| 116 |
+
gap: 7px;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.av-swatches-clothes {
|
| 120 |
+
display: grid;
|
| 121 |
+
grid-template-columns: repeat(6, 1fr);
|
| 122 |
+
gap: 7px;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.av-swatch {
|
| 126 |
+
aspect-ratio: 1;
|
| 127 |
+
border-radius: 50%;
|
| 128 |
+
cursor: pointer;
|
| 129 |
+
border: 2px solid transparent;
|
| 130 |
+
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
|
| 131 |
+
position: relative;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.av-swatch:hover {
|
| 135 |
+
transform: scale(1.2);
|
| 136 |
+
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.av-swatch.active {
|
| 140 |
+
border-color: #fff;
|
| 141 |
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25), 0 4px 14px rgba(0, 0, 0, 0.5);
|
| 142 |
+
transform: scale(1.1);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.av-swatch[data-tip]:hover::after {
|
| 146 |
+
content: attr(data-tip);
|
| 147 |
+
position: absolute;
|
| 148 |
+
bottom: calc(100% + 6px);
|
| 149 |
+
left: 50%;
|
| 150 |
+
transform: translateX(-50%);
|
| 151 |
+
background: rgba(0, 0, 0, 0.9);
|
| 152 |
+
color: #fff;
|
| 153 |
+
font-size: 10px;
|
| 154 |
+
white-space: nowrap;
|
| 155 |
+
padding: 3px 8px;
|
| 156 |
+
border-radius: 4px;
|
| 157 |
+
pointer-events: none;
|
| 158 |
+
z-index: 300;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.av-suggest-label {
|
| 162 |
+
font-size: 9px;
|
| 163 |
+
letter-spacing: 1.5px;
|
| 164 |
+
text-transform: uppercase;
|
| 165 |
+
color: #c9a96e;
|
| 166 |
+
margin-bottom: 7px;
|
| 167 |
+
display: flex;
|
| 168 |
+
align-items: center;
|
| 169 |
+
gap: 5px;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.av-suggest-label::before {
|
| 173 |
+
content: '';
|
| 174 |
+
display: inline-block;
|
| 175 |
+
width: 18px;
|
| 176 |
+
height: 1px;
|
| 177 |
+
background: #c9a96e;
|
| 178 |
+
opacity: 0.5;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.av-divider {
|
| 182 |
+
height: 1px;
|
| 183 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.07), transparent);
|
| 184 |
+
margin: 4px 0 20px;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
/* Info cards inside sidebar */
|
| 188 |
+
.av-info-card {
|
| 189 |
+
background: rgba(255, 255, 255, 0.06);
|
| 190 |
+
border-radius: 12px;
|
| 191 |
+
padding: 14px 16px;
|
| 192 |
+
margin-bottom: 12px;
|
| 193 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.av-info-label {
|
| 197 |
+
font-size: 9px;
|
| 198 |
+
letter-spacing: 2px;
|
| 199 |
+
text-transform: uppercase;
|
| 200 |
+
color: rgba(255, 255, 255, 0.35);
|
| 201 |
+
margin-bottom: 4px;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.av-info-value {
|
| 205 |
+
color: #fff;
|
| 206 |
+
font-weight: 700;
|
| 207 |
+
font-size: 24px;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.av-info-sub {
|
| 211 |
+
color: rgba(255, 255, 255, 0.4);
|
| 212 |
+
font-size: 11px;
|
| 213 |
+
margin-top: 2px;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
/* Debug overlay */
|
| 217 |
+
#av-debug {
|
| 218 |
+
position: absolute;
|
| 219 |
+
bottom: 12px;
|
| 220 |
+
right: 12px;
|
| 221 |
+
background: rgba(0, 0, 0, 0.75);
|
| 222 |
+
color: #4eff91;
|
| 223 |
+
font-size: 10px;
|
| 224 |
+
font-family: monospace;
|
| 225 |
+
padding: 10px 14px;
|
| 226 |
+
border-radius: 6px;
|
| 227 |
+
z-index: 50;
|
| 228 |
+
max-width: 460px;
|
| 229 |
+
line-height: 1.8;
|
| 230 |
+
pointer-events: none;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/* Swatch pop animation */
|
| 234 |
+
@keyframes avSwatchPop {
|
| 235 |
+
from {
|
| 236 |
+
opacity: 0;
|
| 237 |
+
transform: scale(0.7);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
to {
|
| 241 |
+
opacity: 1;
|
| 242 |
+
transform: scale(1);
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.av-swatch.pop {
|
| 247 |
+
animation: avSwatchPop 0.22s ease both;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
@keyframes avFadeUp {
|
| 251 |
+
from {
|
| 252 |
+
opacity: 0;
|
| 253 |
+
transform: translateY(10px);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
to {
|
| 257 |
+
opacity: 1;
|
| 258 |
+
transform: translateY(0);
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/* Back link */
|
| 263 |
+
.av-back-link {
|
| 264 |
+
display: inline-flex;
|
| 265 |
+
align-items: center;
|
| 266 |
+
gap: 6px;
|
| 267 |
+
color: rgba(255, 255, 255, 0.5);
|
| 268 |
+
font-size: 12px;
|
| 269 |
+
font-weight: 500;
|
| 270 |
+
text-decoration: none;
|
| 271 |
+
margin-bottom: 20px;
|
| 272 |
+
transition: color 0.2s;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.av-back-link:hover {
|
| 276 |
+
color: #fff;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/* Responsive */
|
| 280 |
+
@media (max-width: 768px) {
|
| 281 |
+
.avatar-page {
|
| 282 |
+
flex-direction: column;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.avatar-sidebar {
|
| 286 |
+
width: 100%;
|
| 287 |
+
min-width: 100%;
|
| 288 |
+
max-height: none;
|
| 289 |
+
border-right: none;
|
| 290 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.avatar-canvas-area {
|
| 294 |
+
min-height: 50vh;
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
</style>
|
| 298 |
+
{% endblock %}
|
| 299 |
+
|
| 300 |
+
{% block content %}
|
| 301 |
+
<div class="avatar-page">
|
| 302 |
+
|
| 303 |
+
<!-- ===== SIDEBAR ===== -->
|
| 304 |
+
<div class="avatar-sidebar">
|
| 305 |
+
<div class="sidebar-inner">
|
| 306 |
+
|
| 307 |
+
<!-- Back link -->
|
| 308 |
+
<a href="{% url 'fitting_system:recommendations' body_scan.session_id %}" class="av-back-link">
|
| 309 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 310 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
| 311 |
+
</svg>
|
| 312 |
+
{% trans "Back to Recommendations" %}
|
| 313 |
+
</a>
|
| 314 |
+
|
| 315 |
+
<!-- Info Cards -->
|
| 316 |
+
<div class="av-info-card">
|
| 317 |
+
<div class="av-info-label">{% trans "AI Recommended Size" %}</div>
|
| 318 |
+
<div class="av-info-value">{{ recommended_size }}</div>
|
| 319 |
+
<div class="av-info-sub">{% trans "Based on your body scan" %}</div>
|
| 320 |
+
</div>
|
| 321 |
+
|
| 322 |
+
<div class="av-info-card" style="display: flex; align-items: center; gap: 12px;">
|
| 323 |
+
<div class="av-swatch"
|
| 324 |
+
style="background:#{{ skin_tone_hex }}; width: 40px; height: 40px; flex-shrink: 0; cursor: default; border: 2px solid rgba(255,255,255,0.2);">
|
| 325 |
+
</div>
|
| 326 |
+
<div>
|
| 327 |
+
<div class="av-info-label">{% trans "Your Skin Tone" %}</div>
|
| 328 |
+
<div style="color: #fff; font-weight: 600; font-size: 15px;">{{ skin_tone_display }}</div>
|
| 329 |
+
<div class="av-info-sub">{{ undertone_display }} {% trans "undertone" %}</div>
|
| 330 |
+
</div>
|
| 331 |
+
</div>
|
| 332 |
+
|
| 333 |
+
<div class="av-divider"></div>
|
| 334 |
+
|
| 335 |
+
<!-- SKIN TONE SWATCHES -->
|
| 336 |
+
<div class="av-section">
|
| 337 |
+
<div class="av-section-header">
|
| 338 |
+
<div class="av-section-title">{% trans "Skin Tone" %}</div>
|
| 339 |
+
</div>
|
| 340 |
+
<div class="av-swatches-skin" id="avSkinSwatches">
|
| 341 |
+
<div class="av-swatch" style="background:#fde8d0" data-hex="fde8d0" data-skin="0"
|
| 342 |
+
data-tip="{% trans 'Very Light' %}"></div>
|
| 343 |
+
<div class="av-swatch" style="background:#f5cba7" data-hex="f5cba7" data-skin="1" data-tip="{% trans 'Light' %}">
|
| 344 |
+
</div>
|
| 345 |
+
<div class="av-swatch" style="background:#e8a87c" data-hex="e8a87c" data-skin="2"
|
| 346 |
+
data-tip="{% trans 'Intermediate' %}">
|
| 347 |
+
</div>
|
| 348 |
+
<div class="av-swatch" style="background:#c68642" data-hex="c68642" data-skin="3" data-tip="{% trans 'Tan' %}">
|
| 349 |
+
</div>
|
| 350 |
+
<div class="av-swatch" style="background:#8d5524" data-hex="8d5524" data-skin="4" data-tip="{% trans 'Dark' %}">
|
| 351 |
+
</div>
|
| 352 |
+
</div>
|
| 353 |
+
</div>
|
| 354 |
+
|
| 355 |
+
<div class="av-divider"></div>
|
| 356 |
+
|
| 357 |
+
<!-- SHIRT COLORS -->
|
| 358 |
+
<div class="av-section">
|
| 359 |
+
<div class="av-section-header">
|
| 360 |
+
<div class="av-section-title">{% trans "T-Shirt Color" %}</div>
|
| 361 |
+
</div>
|
| 362 |
+
<div class="av-suggest-label" id="avShirtSuggest">{% trans "Suggested for your skin tone" %}</div>
|
| 363 |
+
<div class="av-swatches-clothes" id="avShirtSwatches"></div>
|
| 364 |
+
</div>
|
| 365 |
+
|
| 366 |
+
<div class="av-divider"></div>
|
| 367 |
+
|
| 368 |
+
<!-- PANTS COLORS -->
|
| 369 |
+
<div class="av-section">
|
| 370 |
+
<div class="av-section-header">
|
| 371 |
+
<div class="av-section-title">{% trans "Pants Color" %}</div>
|
| 372 |
+
</div>
|
| 373 |
+
<div class="av-suggest-label" id="avPantsSuggest">{% trans "Suggested for your skin tone" %}</div>
|
| 374 |
+
<div class="av-swatches-clothes" id="avPantsSwatches"></div>
|
| 375 |
+
</div>
|
| 376 |
+
|
| 377 |
+
<div class="av-divider"></div>
|
| 378 |
+
|
| 379 |
+
<!-- Footer hint -->
|
| 380 |
+
<div style="text-align: center; padding: 8px 0;">
|
| 381 |
+
<p style="font-size: 10px; color: rgba(255,255,255,0.2); line-height: 1.7;">
|
| 382 |
+
<span style="display: block; color: rgba(255,255,255,0.3); margin-bottom: 2px;">{% trans "Drag to rotate · Scroll to zoom" %}</span>
|
| 383 |
+
{% trans "Colors update with your skin tone" %}
|
| 384 |
+
</p>
|
| 385 |
+
</div>
|
| 386 |
+
</div>
|
| 387 |
+
</div>
|
| 388 |
+
|
| 389 |
+
<!-- ===== 3D CANVAS ===== -->
|
| 390 |
+
<div class="avatar-canvas-area" id="av-canvas-container">
|
| 391 |
+
<div id="av-debug">âڈ³ {% trans "Loading 3D model..." %}</div>
|
| 392 |
+
</div>
|
| 393 |
+
|
| 394 |
+
</div>
|
| 395 |
+
{% endblock %}
|
| 396 |
+
|
| 397 |
+
{% block extra_js %}
|
| 398 |
+
<script type="importmap">
|
| 399 |
+
{
|
| 400 |
+
"imports": {
|
| 401 |
+
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
| 402 |
+
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
</script>
|
| 406 |
+
|
| 407 |
+
<script type="module">
|
| 408 |
+
import * as THREE from 'three';
|
| 409 |
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
| 410 |
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
| 411 |
+
|
| 412 |
+
// ── Django context ──
|
| 413 |
+
const INITIAL_SKIN_INDEX = {{ skin_tone_index }};
|
| 414 |
+
const GENDER = '{{ gender }}';
|
| 415 |
+
const MODEL_URL = GENDER === 'women'
|
| 416 |
+
? '{% static "avatar/FEMALE.glb" %}'
|
| 417 |
+
: '{% static "avatar/MALE.glb" %}';
|
| 418 |
+
|
| 419 |
+
const dbg = document.getElementById('av-debug');
|
| 420 |
+
const container = document.getElementById('av-canvas-container');
|
| 421 |
+
|
| 422 |
+
// ────────────────────────────────────────────────────────────────
|
| 423 |
+
// DYNAMIC PALETTES — injected from Django / color_palettes.py
|
| 424 |
+
// ────────────────────────────────────────────────────────────────
|
| 425 |
+
const SKIN_TONE_KEYS = ['very_light', 'light', 'intermediate', 'tan', 'dark'];
|
| 426 |
+
const PALETTES_BY_KEY = JSON.parse('{{ palettes_json|escapejs }}');
|
| 427 |
+
const INITIAL_SKIN_KEY = '{{ skin_tone_key }}';
|
| 428 |
+
const REC_SHIRT_HEX = '{{ rec_shirt_hex }}';
|
| 429 |
+
const REC_PANTS_HEX = '{{ rec_pants_hex }}';
|
| 430 |
+
|
| 431 |
+
// Map skin swatch index → skin tone key
|
| 432 |
+
const INDEX_TO_KEY = { 0: 'very_light', 1: 'light', 2: 'intermediate', 3: 'tan', 4: 'dark' };
|
| 433 |
+
|
| 434 |
+
// ────────────────────────────────────────────────────────────────
|
| 435 |
+
// THREE.JS SETUP
|
| 436 |
+
// ────────────────────────────────────────────────────────────────
|
| 437 |
+
const scene = new THREE.Scene();
|
| 438 |
+
scene.background = new THREE.Color(0x0f1729);
|
| 439 |
+
|
| 440 |
+
const camera = new THREE.PerspectiveCamera(
|
| 441 |
+
36,
|
| 442 |
+
Math.max(container.clientWidth, 1) / Math.max(container.clientHeight, 1),
|
| 443 |
+
0.1, 100
|
| 444 |
+
);
|
| 445 |
+
camera.position.set(0, 1.0, 6);
|
| 446 |
+
|
| 447 |
+
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 448 |
+
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| 449 |
+
renderer.setSize(Math.max(container.clientWidth, 1), Math.max(container.clientHeight, 1));
|
| 450 |
+
renderer.shadowMap.enabled = true;
|
| 451 |
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
| 452 |
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
| 453 |
+
renderer.toneMappingExposure = 1.1;
|
| 454 |
+
container.appendChild(renderer.domElement);
|
| 455 |
+
|
| 456 |
+
const controls = new OrbitControls(camera, renderer.domElement);
|
| 457 |
+
controls.target.set(0, 1.0, 0);
|
| 458 |
+
controls.enableDamping = true;
|
| 459 |
+
controls.dampingFactor = 0.07;
|
| 460 |
+
controls.minDistance = 1.0;
|
| 461 |
+
controls.maxDistance = 20;
|
| 462 |
+
controls.update();
|
| 463 |
+
|
| 464 |
+
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
| 465 |
+
const key = new THREE.DirectionalLight(0xfff5e6, 1.4);
|
| 466 |
+
key.position.set(3, 6, 4); key.castShadow = true;
|
| 467 |
+
key.shadow.mapSize.set(1024, 1024); scene.add(key);
|
| 468 |
+
const fill = new THREE.DirectionalLight(0xd0e8ff, 0.6);
|
| 469 |
+
fill.position.set(-3, 3, -2); scene.add(fill);
|
| 470 |
+
const rim = new THREE.DirectionalLight(0xc9a96e, 0.8);
|
| 471 |
+
rim.position.set(0, 4, -4); scene.add(rim);
|
| 472 |
+
|
| 473 |
+
const floor = new THREE.Mesh(
|
| 474 |
+
new THREE.CircleGeometry(2.5, 64),
|
| 475 |
+
new THREE.MeshStandardMaterial({ color: 0x16213e, roughness: 0.85, metalness: 0.05, transparent: true, opacity: 0.65 })
|
| 476 |
+
);
|
| 477 |
+
floor.rotation.x = -Math.PI / 2;
|
| 478 |
+
floor.receiveShadow = true;
|
| 479 |
+
scene.add(floor);
|
| 480 |
+
|
| 481 |
+
// ────────────────────────────────────────────────────────────────
|
| 482 |
+
// MESH BUCKETS
|
| 483 |
+
// ────────────────────────────────────────────────────────────────
|
| 484 |
+
const skinMeshes = [];
|
| 485 |
+
const shirtMeshes = [];
|
| 486 |
+
const pantsMeshes = [];
|
| 487 |
+
const lockedMeshes = [];
|
| 488 |
+
|
| 489 |
+
function avgWorldY(mesh) {
|
| 490 |
+
const pos = mesh.geometry.attributes.position;
|
| 491 |
+
if (!pos || pos.count === 0) return 0;
|
| 492 |
+
let sum = 0;
|
| 493 |
+
const v = new THREE.Vector3();
|
| 494 |
+
for (let i = 0; i < pos.count; i++) {
|
| 495 |
+
v.fromBufferAttribute(pos, i);
|
| 496 |
+
v.applyMatrix4(mesh.matrixWorld);
|
| 497 |
+
sum += v.y;
|
| 498 |
+
}
|
| 499 |
+
return sum / pos.count;
|
| 500 |
+
}
|
| 501 |
+
function getMat(mesh) {
|
| 502 |
+
return Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
function applySkinColor(hexStr) {
|
| 506 |
+
const hex = parseInt(hexStr, 16);
|
| 507 |
+
const r = ((hex >> 16) & 0xff) / 255;
|
| 508 |
+
const g = ((hex >> 8) & 0xff) / 255;
|
| 509 |
+
const b = (hex & 0xff) / 255;
|
| 510 |
+
skinMeshes.forEach(mesh => {
|
| 511 |
+
const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
| 512 |
+
mats.forEach(mat => { mat.color.setRGB(r, g, b); mat.needsUpdate = true; });
|
| 513 |
+
});
|
| 514 |
+
lockedMeshes.forEach(applyLocked);
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
function applyClothingColor(meshList, hexStr) {
|
| 518 |
+
const hex = parseInt(hexStr, 16);
|
| 519 |
+
meshList.forEach(mesh => {
|
| 520 |
+
const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
| 521 |
+
mats.forEach(mat => { mat.color.setHex(hex); mat.map = null; mat.needsUpdate = true; });
|
| 522 |
+
});
|
| 523 |
+
lockedMeshes.forEach(applyLocked);
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
function applyLocked(entry) {
|
| 527 |
+
const mats = Array.isArray(entry.mesh.material) ? entry.mesh.material : [entry.mesh.material];
|
| 528 |
+
mats.forEach(mat => { mat.color.setHex(entry.hexColor); mat.map = null; mat.needsUpdate = true; });
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
// ────────────────────────────────────────────────────────────────
|
| 532 |
+
// PALETTE UI
|
| 533 |
+
// ────────────â���€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
|
| 534 |
+
let currentShirtHex = null;
|
| 535 |
+
let currentPantsHex = null;
|
| 536 |
+
|
| 537 |
+
function buildClothingSwatches(skinKey, defaultShirtHex, defaultPantsHex) {
|
| 538 |
+
const palette = PALETTES_BY_KEY[skinKey];
|
| 539 |
+
if (!palette) return;
|
| 540 |
+
const shirtGrid = document.getElementById('avShirtSwatches');
|
| 541 |
+
const pantsGrid = document.getElementById('avPantsSwatches');
|
| 542 |
+
|
| 543 |
+
const lightHexes = new Set(['ffffff', 'f5f5f5', 'fffde7', 'e0e0e0', 'cfd8dc', 'e8d5f5', 'b5d8f7', 'f7c5d0', 'bbdefb', 'a5d6a7', 'ef9a9a', 'fff176']);
|
| 544 |
+
|
| 545 |
+
// ── Shirt swatches ──
|
| 546 |
+
shirtGrid.innerHTML = '';
|
| 547 |
+
palette.shirts.forEach((item, i) => {
|
| 548 |
+
const sw = document.createElement('div');
|
| 549 |
+
sw.className = 'av-swatch pop';
|
| 550 |
+
sw.style.background = '#' + item.hex;
|
| 551 |
+
sw.style.animationDelay = (i * 0.025) + 's';
|
| 552 |
+
if (lightHexes.has(item.hex)) sw.style.borderColor = 'rgba(255,255,255,0.3)';
|
| 553 |
+
sw.dataset.hex = item.hex;
|
| 554 |
+
sw.dataset.tip = item.tip;
|
| 555 |
+
sw.addEventListener('click', () => {
|
| 556 |
+
shirtGrid.querySelectorAll('.av-swatch').forEach(s => s.classList.remove('active'));
|
| 557 |
+
sw.classList.add('active');
|
| 558 |
+
currentShirtHex = item.hex;
|
| 559 |
+
applyClothingColor(shirtMeshes, item.hex);
|
| 560 |
+
});
|
| 561 |
+
shirtGrid.appendChild(sw);
|
| 562 |
+
});
|
| 563 |
+
|
| 564 |
+
// ── Pants swatches ──
|
| 565 |
+
pantsGrid.innerHTML = '';
|
| 566 |
+
palette.pants.forEach((item, i) => {
|
| 567 |
+
const sw = document.createElement('div');
|
| 568 |
+
sw.className = 'av-swatch pop';
|
| 569 |
+
sw.style.background = '#' + item.hex;
|
| 570 |
+
sw.style.animationDelay = (i * 0.025) + 's';
|
| 571 |
+
if (lightHexes.has(item.hex)) sw.style.borderColor = 'rgba(255,255,255,0.3)';
|
| 572 |
+
sw.dataset.hex = item.hex;
|
| 573 |
+
sw.dataset.tip = item.tip;
|
| 574 |
+
sw.addEventListener('click', () => {
|
| 575 |
+
pantsGrid.querySelectorAll('.av-swatch').forEach(s => s.classList.remove('active'));
|
| 576 |
+
sw.classList.add('active');
|
| 577 |
+
currentPantsHex = item.hex;
|
| 578 |
+
applyClothingColor(pantsMeshes, item.hex);
|
| 579 |
+
});
|
| 580 |
+
pantsGrid.appendChild(sw);
|
| 581 |
+
});
|
| 582 |
+
|
| 583 |
+
// Auto-select: prefer the Gemini-recommended hex, else first swatch
|
| 584 |
+
const shirtSw = shirtGrid.querySelectorAll('.av-swatch');
|
| 585 |
+
let shirtSelected = false;
|
| 586 |
+
if (defaultShirtHex) {
|
| 587 |
+
shirtSw.forEach(s => { if (s.dataset.hex === defaultShirtHex) { s.classList.add('active'); shirtSelected = true; } });
|
| 588 |
+
}
|
| 589 |
+
if (!shirtSelected && shirtSw[0]) shirtSw[0].classList.add('active');
|
| 590 |
+
currentShirtHex = defaultShirtHex || palette.shirts[0].hex;
|
| 591 |
+
applyClothingColor(shirtMeshes, currentShirtHex);
|
| 592 |
+
|
| 593 |
+
const pantsSw = pantsGrid.querySelectorAll('.av-swatch');
|
| 594 |
+
let pantsSelected = false;
|
| 595 |
+
if (defaultPantsHex) {
|
| 596 |
+
pantsSw.forEach(s => { if (s.dataset.hex === defaultPantsHex) { s.classList.add('active'); pantsSelected = true; } });
|
| 597 |
+
}
|
| 598 |
+
if (!pantsSelected && pantsSw[0]) pantsSw[0].classList.add('active');
|
| 599 |
+
currentPantsHex = defaultPantsHex || palette.pants[0].hex;
|
| 600 |
+
applyClothingColor(pantsMeshes, currentPantsHex);
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
// ────────────────────────────────────────────────────────────────
|
| 604 |
+
// LOAD MODEL
|
| 605 |
+
// ────────────────────────────────────────────────────────────────
|
| 606 |
+
new GLTFLoader().load(
|
| 607 |
+
MODEL_URL,
|
| 608 |
+
function (gltf) {
|
| 609 |
+
const model = gltf.scene;
|
| 610 |
+
|
| 611 |
+
{
|
| 612 |
+
const box = new THREE.Box3().setFromObject(model);
|
| 613 |
+
const size = box.getSize(new THREE.Vector3());
|
| 614 |
+
model.scale.setScalar(2.0 / Math.max(size.x, size.y, size.z));
|
| 615 |
+
model.updateMatrixWorld(true);
|
| 616 |
+
const box2 = new THREE.Box3().setFromObject(model);
|
| 617 |
+
const cen = box2.getCenter(new THREE.Vector3());
|
| 618 |
+
model.position.set(-cen.x, -box2.min.y, -cen.z);
|
| 619 |
+
}
|
| 620 |
+
scene.add(model);
|
| 621 |
+
model.updateMatrixWorld(true);
|
| 622 |
+
|
| 623 |
+
const wb = new THREE.Box3().setFromObject(model);
|
| 624 |
+
const wh = wb.max.y - wb.min.y;
|
| 625 |
+
const minY = wb.min.y;
|
| 626 |
+
|
| 627 |
+
const ankleY = minY + wh * 0.11;
|
| 628 |
+
const waistY = minY + wh * 0.47;
|
| 629 |
+
const neckY = minY + wh * 0.80;
|
| 630 |
+
const midY = minY + wh * 0.50;
|
| 631 |
+
|
| 632 |
+
const fovRad = THREE.MathUtils.degToRad(camera.fov);
|
| 633 |
+
const camDist = (wh / 2 / Math.tan(fovRad / 2)) * 1.65;
|
| 634 |
+
camera.position.set(0, midY, camDist);
|
| 635 |
+
controls.target.set(0, midY, 0);
|
| 636 |
+
controls.minDistance = camDist * 0.25;
|
| 637 |
+
controls.maxDistance = camDist * 5;
|
| 638 |
+
controls.update();
|
| 639 |
+
|
| 640 |
+
const allMeshes = [];
|
| 641 |
+
model.traverse(child => {
|
| 642 |
+
if (!child.isMesh) return;
|
| 643 |
+
child.material = Array.isArray(child.material)
|
| 644 |
+
? child.material.map(m => m.clone())
|
| 645 |
+
: child.material.clone();
|
| 646 |
+
child.castShadow = true;
|
| 647 |
+
child.receiveShadow = true;
|
| 648 |
+
allMeshes.push(child);
|
| 649 |
+
});
|
| 650 |
+
|
| 651 |
+
const lockBlack = ['eye', 'lash', 'iris', 'pupil', 'cornea', 'sclera', 'brow', 'eyebrow', 'hair', 'shoe', 'boot', 'sole', 'lace', 'sneaker', 'eyelid'];
|
| 652 |
+
const lockWhite = ['sock'];
|
| 653 |
+
const lockDefault = ['teeth', 'tooth', 'tongue', 'lip', 'mouth', 'gum', 'nose', 'nail'];
|
| 654 |
+
const pantsWords = ['pant', 'trouser', 'jean', 'denim', 'short', 'skirt', 'bottom', 'lower'];
|
| 655 |
+
const shirtWords = ['shirt', 'tee', 'jacket', 'hoodie', 'sweater', 'cloth', 'wear', 'blouse'];
|
| 656 |
+
|
| 657 |
+
const unclassified = [];
|
| 658 |
+
|
| 659 |
+
allMeshes.forEach(mesh => {
|
| 660 |
+
const n = mesh.name.toLowerCase();
|
| 661 |
+
if (lockBlack.some(w => n.includes(w))) { lockedMeshes.push({ mesh, hexColor: 0x0a0a0a }); return; }
|
| 662 |
+
if (lockWhite.some(w => n.includes(w))) { lockedMeshes.push({ mesh, hexColor: 0xfafafa }); return; }
|
| 663 |
+
if (lockDefault.some(w => n.includes(w))) { return; }
|
| 664 |
+
if (pantsWords.some(w => n.includes(w))) { pantsMeshes.push(mesh); return; }
|
| 665 |
+
if (shirtWords.some(w => n.includes(w))) { shirtMeshes.push(mesh); return; }
|
| 666 |
+
|
| 667 |
+
const yc = avgWorldY(mesh);
|
| 668 |
+
const mat = getMat(mesh);
|
| 669 |
+
const { r, g, b } = mat ? mat.color : { r: 0.5, g: 0.5, b: 0.5 };
|
| 670 |
+
const isDark = (r + g + b) < 0.8 || (r < 0.25 && g < 0.25);
|
| 671 |
+
|
| 672 |
+
if (yc < ankleY) { return; }
|
| 673 |
+
if (yc <= waistY) { isDark ? pantsMeshes.push(mesh) : unclassified.push(mesh); return; }
|
| 674 |
+
if (yc <= neckY) { isDark ? shirtMeshes.push(mesh) : unclassified.push(mesh); return; }
|
| 675 |
+
unclassified.push(mesh);
|
| 676 |
+
});
|
| 677 |
+
|
| 678 |
+
unclassified.forEach(m => { skinMeshes.push(m); });
|
| 679 |
+
|
| 680 |
+
if (skinMeshes.length === 0) {
|
| 681 |
+
const used = new Set([...pantsMeshes, ...shirtMeshes, ...lockedMeshes.map(e => e.mesh)]);
|
| 682 |
+
allMeshes.forEach(m => { if (!used.has(m)) skinMeshes.push(m); });
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
lockedMeshes.forEach(applyLocked);
|
| 686 |
+
|
| 687 |
+
dbg.innerHTML =
|
| 688 |
+
`ًں§¬ Skin: <b>${skinMeshes.length}</b> ًں‘• Shirt: <b>${shirtMeshes.length}</b> ًں‘– Pants: <b>${pantsMeshes.length}</b> ًں”’ Locked: <b>${lockedMeshes.length}</b><br>` +
|
| 689 |
+
`<span style="opacity:.5;font-size:9px">Model: ${GENDER === 'women' ? 'FEMALE' : 'MALE'}.glb</span>`;
|
| 690 |
+
|
| 691 |
+
// Auto-select the detected skin tone
|
| 692 |
+
const skinSwatches = document.getElementById('avSkinSwatches').querySelectorAll('.av-swatch');
|
| 693 |
+
const targetSwatch = skinSwatches[INITIAL_SKIN_INDEX];
|
| 694 |
+
if (targetSwatch) {
|
| 695 |
+
skinSwatches.forEach(s => s.classList.remove('active'));
|
| 696 |
+
targetSwatch.classList.add('active');
|
| 697 |
+
applySkinColor(targetSwatch.dataset.hex);
|
| 698 |
+
buildClothingSwatches(INITIAL_SKIN_KEY, REC_SHIRT_HEX, REC_PANTS_HEX);
|
| 699 |
+
} else {
|
| 700 |
+
buildClothingSwatches('intermediate', REC_SHIRT_HEX, REC_PANTS_HEX);
|
| 701 |
+
}
|
| 702 |
+
},
|
| 703 |
+
undefined,
|
| 704 |
+
err => { console.error(err); dbg.innerHTML = `<span style="color:#ff6b6b">⌠${err.message}</span>`; }
|
| 705 |
+
);
|
| 706 |
+
|
| 707 |
+
// ────────────────────────────────────────────────────────────────
|
| 708 |
+
// SKIN SWATCH CLICK
|
| 709 |
+
// ────────────────────────────────────────────────────────────────
|
| 710 |
+
document.getElementById('avSkinSwatches').querySelectorAll('.av-swatch').forEach(sw => {
|
| 711 |
+
sw.addEventListener('click', () => {
|
| 712 |
+
document.getElementById('avSkinSwatches').querySelectorAll('.av-swatch').forEach(s => s.classList.remove('active'));
|
| 713 |
+
sw.classList.add('active');
|
| 714 |
+
const skinIndex = parseInt(sw.dataset.skin);
|
| 715 |
+
const skinKey = INDEX_TO_KEY[skinIndex] || 'intermediate';
|
| 716 |
+
applySkinColor(sw.dataset.hex);
|
| 717 |
+
buildClothingSwatches(skinKey, null, null);
|
| 718 |
+
});
|
| 719 |
+
});
|
| 720 |
+
|
| 721 |
+
// ── Render loop ──
|
| 722 |
+
(function animate() {
|
| 723 |
+
requestAnimationFrame(animate);
|
| 724 |
+
controls.update();
|
| 725 |
+
renderer.render(scene, camera);
|
| 726 |
+
})();
|
| 727 |
+
|
| 728 |
+
// ── Resize ──
|
| 729 |
+
new ResizeObserver(() => {
|
| 730 |
+
const w = Math.max(container.clientWidth, 1);
|
| 731 |
+
const h = Math.max(container.clientHeight, 1);
|
| 732 |
+
camera.aspect = w / h;
|
| 733 |
+
camera.updateProjectionMatrix();
|
| 734 |
+
renderer.setSize(w, h);
|
| 735 |
+
}).observe(container);
|
| 736 |
+
</script>
|
| 737 |
+
{% endblock %}
|
| 738 |
+
|
| 739 |
+
|
fitting_system/templates/base.html
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% load static %}
|
| 2 |
+
{% load i18n %}
|
| 3 |
+
{% load i18n_helpers %}
|
| 4 |
+
<!DOCTYPE html>
|
| 5 |
+
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}">
|
| 6 |
+
|
| 7 |
+
<head>
|
| 8 |
+
<meta charset="UTF-8">
|
| 9 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 10 |
+
<title>{% block title %}{% trans "Virtual Fitting System" %}{% endblock %}</title>
|
| 11 |
+
|
| 12 |
+
<!-- Tailwind CSS -->
|
| 13 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 14 |
+
|
| 15 |
+
<!-- Custom Tailwind Config -->
|
| 16 |
+
<script>
|
| 17 |
+
tailwind.config = {
|
| 18 |
+
theme: {
|
| 19 |
+
extend: {
|
| 20 |
+
colors: {
|
| 21 |
+
primary: '#1B3A6B',
|
| 22 |
+
secondary: '#2E5FA3',
|
| 23 |
+
accent: '#5B8FC9',
|
| 24 |
+
surface: '#7DB8D8',
|
| 25 |
+
background: '#EAF3F8',
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
</script>
|
| 31 |
+
|
| 32 |
+
<!-- Google Fonts -->
|
| 33 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 34 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 35 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
|
| 36 |
+
rel="stylesheet">
|
| 37 |
+
|
| 38 |
+
<style>
|
| 39 |
+
* {
|
| 40 |
+
scroll-behavior: smooth;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
body {
|
| 44 |
+
font-family: 'Inter', sans-serif;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.gradient-bg {
|
| 48 |
+
background: linear-gradient(135deg, #1B3A6B 0%, #2E5FA3 50%, #5B8FC9 100%);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* Glassmorphism Effects */
|
| 52 |
+
.glass-effect {
|
| 53 |
+
background: rgba(255, 255, 255, 0.1);
|
| 54 |
+
backdrop-filter: blur(10px);
|
| 55 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.glass-card {
|
| 59 |
+
background: rgba(255, 255, 255, 0.95);
|
| 60 |
+
backdrop-filter: blur(20px) saturate(180%);
|
| 61 |
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.glass-filter {
|
| 65 |
+
background: linear-gradient(135deg, rgba(27, 58, 107, 0.08) 0%, rgba(46, 95, 163, 0.08) 100%);
|
| 66 |
+
backdrop-filter: blur(16px) saturate(180%);
|
| 67 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/* Hover Effects */
|
| 71 |
+
.hover-lift {
|
| 72 |
+
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
|
| 73 |
+
box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.hover-lift:hover {
|
| 77 |
+
transform: translateY(-8px) scale(1.02);
|
| 78 |
+
box-shadow: 0 25px 50px rgba(27, 58, 107, 0.25);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.hover-glow {
|
| 82 |
+
transition: all 0.3s ease;
|
| 83 |
+
position: relative;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.hover-glow:hover {
|
| 87 |
+
box-shadow: 0 0 30px rgba(91, 143, 201, 0.4),
|
| 88 |
+
0 0 60px rgba(46, 95, 163, 0.2);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
/* Gradient Overlays */
|
| 92 |
+
.gradient-overlay {
|
| 93 |
+
position: relative;
|
| 94 |
+
overflow: hidden;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.gradient-overlay::before {
|
| 98 |
+
content: '';
|
| 99 |
+
position: absolute;
|
| 100 |
+
top: 0;
|
| 101 |
+
left: 0;
|
| 102 |
+
right: 0;
|
| 103 |
+
bottom: 0;
|
| 104 |
+
background: linear-gradient(135deg, rgba(27, 58, 107, 0.08) 0%, rgba(91, 143, 201, 0.08) 100%);
|
| 105 |
+
opacity: 0;
|
| 106 |
+
transition: opacity 0.3s ease;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.gradient-overlay:hover::before {
|
| 110 |
+
opacity: 1;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* Animations */
|
| 114 |
+
.pulse-animation {
|
| 115 |
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
@keyframes pulse {
|
| 119 |
+
|
| 120 |
+
0%,
|
| 121 |
+
100% {
|
| 122 |
+
opacity: 1;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
50% {
|
| 126 |
+
opacity: .7;
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.fade-in {
|
| 131 |
+
animation: fadeIn 0.6s ease-out forwards;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
@keyframes fadeIn {
|
| 135 |
+
from {
|
| 136 |
+
opacity: 0;
|
| 137 |
+
transform: translateY(20px);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
to {
|
| 141 |
+
opacity: 1;
|
| 142 |
+
transform: translateY(0);
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.slide-up {
|
| 147 |
+
animation: slideUp 0.5s ease-out forwards;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
@keyframes slideUp {
|
| 151 |
+
from {
|
| 152 |
+
opacity: 0;
|
| 153 |
+
transform: translateY(30px);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
to {
|
| 157 |
+
opacity: 1;
|
| 158 |
+
transform: translateY(0);
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.scale-in {
|
| 163 |
+
animation: scaleIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
@keyframes scaleIn {
|
| 167 |
+
from {
|
| 168 |
+
opacity: 0;
|
| 169 |
+
transform: scale(0.9);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
to {
|
| 173 |
+
opacity: 1;
|
| 174 |
+
transform: scale(1);
|
| 175 |
+
}
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/* Stagger animation delays */
|
| 179 |
+
.stagger-1 {
|
| 180 |
+
animation-delay: 0.1s;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.stagger-2 {
|
| 184 |
+
animation-delay: 0.2s;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.stagger-3 {
|
| 188 |
+
animation-delay: 0.3s;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.stagger-4 {
|
| 192 |
+
animation-delay: 0.4s;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/* Image zoom effect */
|
| 196 |
+
.image-zoom {
|
| 197 |
+
overflow: hidden;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.image-zoom img {
|
| 201 |
+
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.image-zoom:hover img {
|
| 205 |
+
transform: scale(1.1);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/* Floating badge */
|
| 209 |
+
.floating-badge {
|
| 210 |
+
animation: float 3s ease-in-out infinite;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
@keyframes float {
|
| 214 |
+
|
| 215 |
+
0%,
|
| 216 |
+
100% {
|
| 217 |
+
transform: translateY(0px);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
50% {
|
| 221 |
+
transform: translateY(-5px);
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
/* Shimmer effect */
|
| 226 |
+
.shimmer {
|
| 227 |
+
position: relative;
|
| 228 |
+
overflow: hidden;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.shimmer::after {
|
| 232 |
+
content: '';
|
| 233 |
+
position: absolute;
|
| 234 |
+
top: 0;
|
| 235 |
+
left: -100%;
|
| 236 |
+
width: 100%;
|
| 237 |
+
height: 100%;
|
| 238 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
| 239 |
+
animation: shimmer 2s infinite;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
@keyframes shimmer {
|
| 243 |
+
0% {
|
| 244 |
+
left: -100%;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
100% {
|
| 248 |
+
left: 100%;
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/* RTL support for Arabic */
|
| 253 |
+
[dir="rtl"] body {
|
| 254 |
+
text-align: right;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
[dir="rtl"] .space-x-6 > * + * {
|
| 258 |
+
margin-left: 0;
|
| 259 |
+
margin-right: 1.5rem;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
[dir="rtl"] .space-y-2 > * + * {
|
| 263 |
+
margin-top: 0.5rem;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
[dir="rtl"] .text-left {
|
| 267 |
+
text-align: right;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
[dir="rtl"] .text-right {
|
| 271 |
+
text-align: left;
|
| 272 |
+
}
|
| 273 |
+
</style>
|
| 274 |
+
|
| 275 |
+
{% block extra_css %}{% endblock %}
|
| 276 |
+
</head>
|
| 277 |
+
|
| 278 |
+
<body class="bg-[#EAF3F8] min-h-screen">
|
| 279 |
+
<!-- Navigation -->
|
| 280 |
+
<nav class="bg-white shadow-lg sticky top-0 z-50">
|
| 281 |
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 282 |
+
<div class="flex justify-between items-center h-16">
|
| 283 |
+
<div class="flex items-center" style="margin-left: -1rem;">
|
| 284 |
+
<a href="{% url 'fitting_system:index' %}" class="flex items-center">
|
| 285 |
+
<img src="{% static 'images/products/Gemini_Generated_Image_k8jh4tk8jh4tk8jh.png' %}"
|
| 286 |
+
alt="HRT Logo" class="h-14 w-auto">
|
| 287 |
+
</a>
|
| 288 |
+
</div>
|
| 289 |
+
|
| 290 |
+
<div class="hidden md:flex items-center gap-6">
|
| 291 |
+
<a href="{% url 'fitting_system:index' %}"
|
| 292 |
+
class="text-gray-700 hover:text-[#2E5FA3] font-medium transition">{% trans "Home" %}</a>
|
| 293 |
+
<a href="{% url 'fitting_system:store' %}"
|
| 294 |
+
class="text-gray-700 hover:text-[#2E5FA3] font-medium transition">{% trans "Store" %}</a>
|
| 295 |
+
<a href="{% url 'fitting_system:scan' %}"
|
| 296 |
+
class="bg-gradient-to-r from-[#1B3A6B] to-[#2E5FA3] text-white px-6 py-2 rounded-lg hover:shadow-lg transition transform hover:scale-105">
|
| 297 |
+
{% trans "Try Virtual Fitting" %}
|
| 298 |
+
</a>
|
| 299 |
+
<a href="{% url 'fitting_system:inventory' %}"
|
| 300 |
+
class="text-gray-700 hover:text-[#2E5FA3] font-medium transition">{% trans "Inventory" %}</a>
|
| 301 |
+
<form id="language-form" action="{% url 'set_language' %}" method="post" class="inline-flex items-center gap-2">
|
| 302 |
+
{% csrf_token %}
|
| 303 |
+
<input name="next" type="hidden" id="lang-next" value="">
|
| 304 |
+
<select name="language" id="lang-select" class="rounded border border-gray-300 px-2 py-1 text-sm focus:ring-[#2E5FA3] focus:border-[#2E5FA3]">
|
| 305 |
+
{% get_current_language as CURRENT_LANG %}
|
| 306 |
+
{% for code, name in LANGUAGES %}
|
| 307 |
+
<option value="{{ code }}" data-next="{% path_for_lang code %}" {% if code == CURRENT_LANG %}selected{% endif %}>{{ name }}</option>
|
| 308 |
+
{% endfor %}
|
| 309 |
+
</select>
|
| 310 |
+
</form>
|
| 311 |
+
</div>
|
| 312 |
+
|
| 313 |
+
<!-- Mobile menu button -->
|
| 314 |
+
<div class="md:hidden">
|
| 315 |
+
<button id="mobile-menu-button" class="text-gray-700 hover:text-[#2E5FA3]">
|
| 316 |
+
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 317 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 318 |
+
d="M4 6h16M4 12h16M4 18h16"></path>
|
| 319 |
+
</svg>
|
| 320 |
+
</button>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
|
| 325 |
+
<!-- Mobile menu -->
|
| 326 |
+
<div id="mobile-menu" class="hidden md:hidden bg-white border-t">
|
| 327 |
+
<div class="px-4 py-3 space-y-2">
|
| 328 |
+
<a href="{% url 'fitting_system:index' %}"
|
| 329 |
+
class="block text-gray-700 hover:text-[#2E5FA3] font-medium py-2">{% trans "Home" %}</a>
|
| 330 |
+
<a href="{% url 'fitting_system:store' %}"
|
| 331 |
+
class="block text-gray-700 hover:text-[#2E5FA3] font-medium py-2">{% trans "Store" %}</a>
|
| 332 |
+
<a href="{% url 'fitting_system:scan' %}"
|
| 333 |
+
class="block bg-gradient-to-r from-[#1B3A6B] to-[#2E5FA3] text-white px-6 py-2 rounded-lg text-center">{% trans "Try Virtual Fitting" %}</a>
|
| 334 |
+
<a href="{% url 'fitting_system:inventory' %}"
|
| 335 |
+
class="block text-gray-700 hover:text-[#2E5FA3] font-medium py-2">{% trans "Inventory" %}</a>
|
| 336 |
+
<form id="language-form-mobile" action="{% url 'set_language' %}" method="post" class="pt-2">
|
| 337 |
+
{% csrf_token %}
|
| 338 |
+
<input name="next" type="hidden" id="lang-next-mobile" value="">
|
| 339 |
+
<select name="language" id="lang-select-mobile" class="w-full rounded border border-gray-300 px-2 py-1 text-sm">
|
| 340 |
+
{% get_current_language as CURRENT_LANG %}
|
| 341 |
+
{% for code, name in LANGUAGES %}
|
| 342 |
+
<option value="{{ code }}" data-next="{% path_for_lang code %}" {% if code == CURRENT_LANG %}selected{% endif %}>{{ name }}</option>
|
| 343 |
+
{% endfor %}
|
| 344 |
+
</select>
|
| 345 |
+
</form>
|
| 346 |
+
</div>
|
| 347 |
+
</div>
|
| 348 |
+
</nav>
|
| 349 |
+
|
| 350 |
+
<!-- Main Content -->
|
| 351 |
+
<main>
|
| 352 |
+
{% block content %}{% endblock %}
|
| 353 |
+
</main>
|
| 354 |
+
|
| 355 |
+
<!-- Footer -->
|
| 356 |
+
<footer class="bg-[#1B3A6B] text-white mt-20">
|
| 357 |
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
| 358 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
| 359 |
+
<div>
|
| 360 |
+
<h3 class="text-lg font-bold mb-4">{% trans "Virtual Fitting System" %}</h3>
|
| 361 |
+
<p class="text-[#7DB8D8]">{% trans "AI-powered virtual fitting for the perfect size and style." %}</p>
|
| 362 |
+
</div>
|
| 363 |
+
<div>
|
| 364 |
+
<h3 class="text-lg font-bold mb-4">{% trans "Quick Links" %}</h3>
|
| 365 |
+
<ul class="space-y-2">
|
| 366 |
+
<li><a href="{% url 'fitting_system:index' %}"
|
| 367 |
+
class="text-[#7DB8D8] hover:text-white transition">{% trans "Home" %}</a></li>
|
| 368 |
+
<li><a href="{% url 'fitting_system:store' %}"
|
| 369 |
+
class="text-[#7DB8D8] hover:text-white transition">{% trans "Store" %}</a></li>
|
| 370 |
+
<li><a href="{% url 'fitting_system:scan' %}"
|
| 371 |
+
class="text-[#7DB8D8] hover:text-white transition">{% trans "Virtual Fitting" %}</a></li>
|
| 372 |
+
</ul>
|
| 373 |
+
</div>
|
| 374 |
+
<div>
|
| 375 |
+
<h3 class="text-lg font-bold mb-4">{% trans "Contact" %}</h3>
|
| 376 |
+
<p class="text-[#7DB8D8]">{% trans "In-store system prototype" %}</p>
|
| 377 |
+
<p class="text-[#7DB8D8] mt-2">© 2026 {% trans "Virtual Fitting System" %}</p>
|
| 378 |
+
</div>
|
| 379 |
+
</div>
|
| 380 |
+
</div>
|
| 381 |
+
</footer>
|
| 382 |
+
|
| 383 |
+
<!-- Mobile menu toggle script -->
|
| 384 |
+
<script>
|
| 385 |
+
document.getElementById('mobile-menu-button').addEventListener('click', function () {
|
| 386 |
+
const menu = document.getElementById('mobile-menu');
|
| 387 |
+
menu.classList.toggle('hidden');
|
| 388 |
+
});
|
| 389 |
+
|
| 390 |
+
function initLanguageForm(formId, nextId, selectId) {
|
| 391 |
+
var form = document.getElementById(formId);
|
| 392 |
+
var nextInput = document.getElementById(nextId);
|
| 393 |
+
var select = document.getElementById(selectId);
|
| 394 |
+
if (form && nextInput && select) {
|
| 395 |
+
select.addEventListener('change', function() {
|
| 396 |
+
var opt = select.options[select.selectedIndex];
|
| 397 |
+
nextInput.value = opt.getAttribute('data-next') || opt.value;
|
| 398 |
+
form.submit();
|
| 399 |
+
});
|
| 400 |
+
var opt = select.options[select.selectedIndex];
|
| 401 |
+
nextInput.value = opt.getAttribute('data-next') || opt.value;
|
| 402 |
+
}
|
| 403 |
+
}
|
| 404 |
+
initLanguageForm('language-form', 'lang-next', 'lang-select');
|
| 405 |
+
initLanguageForm('language-form-mobile', 'lang-next-mobile', 'lang-select-mobile');
|
| 406 |
+
</script>
|
| 407 |
+
|
| 408 |
+
{% block extra_js %}{% endblock %}
|
| 409 |
+
</body>
|
| 410 |
+
|
| 411 |
+
</html>
|
fitting_system/templates/index.html
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% load i18n %}
|
| 3 |
+
|
| 4 |
+
{% block title %}{% trans "Virtual Fitting System - Find Your Perfect Fit" %}{% endblock %}
|
| 5 |
+
|
| 6 |
+
{% block content %}
|
| 7 |
+
<!-- Hero Section -->
|
| 8 |
+
<div class="relative overflow-hidden">
|
| 9 |
+
<div class="gradient-bg">
|
| 10 |
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
| 11 |
+
<div class="text-center">
|
| 12 |
+
<h1 class="text-5xl md:text-6xl font-extrabold text-white mb-6 animate-fade-in">
|
| 13 |
+
{% trans "Find Your Perfect Fit" %}
|
| 14 |
+
<span class="block text-transparent bg-clip-text bg-gradient-to-r from-[#7DB8D8] to-white mt-2">
|
| 15 |
+
{% trans "With AI Technology" %}
|
| 16 |
+
</span>
|
| 17 |
+
</h1>
|
| 18 |
+
<p class="text-xl text-white/90 mb-8 max-w-2xl mx-auto">
|
| 19 |
+
{% trans "Get personalized clothing recommendations based on your body measurements and skin tone." %}
|
| 20 |
+
{% trans "No more guessing sizes!" %}
|
| 21 |
+
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
| 22 |
+
<a href="{% url 'fitting_system:scan' %}"
|
| 23 |
+
class="bg-white text-[#1B3A6B] px-8 py-4 rounded-lg font-bold text-lg hover:shadow-2xl transition transform hover:scale-105 inline-flex items-center justify-center">
|
| 24 |
+
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 25 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 26 |
+
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z">
|
| 27 |
+
</path>
|
| 28 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 29 |
+
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
| 30 |
+
</svg>
|
| 31 |
+
{% trans "Start Virtual Fitting" %}
|
| 32 |
+
</a>
|
| 33 |
+
<a href="{% url 'fitting_system:store' %}"
|
| 34 |
+
class="bg-transparent border-2 border-white text-white px-8 py-4 rounded-lg font-bold text-lg hover:bg-white hover:text-[#1B3A6B] transition transform hover:scale-105 inline-flex items-center justify-center">
|
| 35 |
+
<svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 36 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 37 |
+
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path>
|
| 38 |
+
</svg>
|
| 39 |
+
{% trans "Browse Store" %}
|
| 40 |
+
</a>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<!-- Wave decoration -->
|
| 47 |
+
<div class="absolute bottom-0 w-full">
|
| 48 |
+
<svg viewBox="0 0 1440 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 49 |
+
<path
|
| 50 |
+
d="M0 120L60 110C120 100 240 80 360 70C480 60 600 60 720 65C840 70 960 80 1080 85C1200 90 1320 90 1380 90L1440 90V120H1380C1320 120 1200 120 1080 120C960 120 840 120 720 120C600 120 480 120 360 120C240 120 120 120 60 120H0Z"
|
| 51 |
+
fill="#EAF3F8" />
|
| 52 |
+
</svg>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<!-- Features Section -->
|
| 57 |
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
| 58 |
+
<div class="text-center mb-16">
|
| 59 |
+
<h2 class="text-4xl font-bold text-gray-900 mb-4">{% trans "How It Works" %}</h2>
|
| 60 |
+
<p class="text-xl text-gray-600">{% trans "Three simple steps to find your perfect fit" %}</p>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
| 64 |
+
<!-- Feature 1 -->
|
| 65 |
+
<div class="bg-white rounded-2xl p-8 shadow-lg hover-lift">
|
| 66 |
+
<div
|
| 67 |
+
class="w-16 h-16 bg-gradient-to-br from-[#1B3A6B] to-[#2E5FA3] rounded-full flex items-center justify-center mb-6">
|
| 68 |
+
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 69 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 70 |
+
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z">
|
| 71 |
+
</path>
|
| 72 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 73 |
+
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
| 74 |
+
</svg>
|
| 75 |
+
</div>
|
| 76 |
+
<h3 class="text-2xl font-bold text-gray-900 mb-3">{% trans "1. Body Scan" %}</h3>
|
| 77 |
+
<p class="text-gray-600">{% trans "Stand in front of the camera and let our AI analyze your body measurements using advanced pose detection technology." %}</p>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<!-- Feature 2 -->
|
| 81 |
+
<div class="bg-white rounded-2xl p-8 shadow-lg hover-lift">
|
| 82 |
+
<div
|
| 83 |
+
class="w-16 h-16 bg-gradient-to-br from-[#2E5FA3] to-[#5B8FC9] rounded-full flex items-center justify-center mb-6">
|
| 84 |
+
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 85 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 86 |
+
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01">
|
| 87 |
+
</path>
|
| 88 |
+
</svg>
|
| 89 |
+
</div>
|
| 90 |
+
<h3 class="text-2xl font-bold text-gray-900 mb-3">{% trans "2. Skin Tone Analysis" %}</h3>
|
| 91 |
+
<p class="text-gray-600">{% trans "Our AI analyzes your skin tone to recommend colors that complement you best, ensuring you look amazing." %}</p>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<!-- Feature 3 -->
|
| 95 |
+
<div class="bg-white rounded-2xl p-8 shadow-lg hover-lift">
|
| 96 |
+
<div
|
| 97 |
+
class="w-16 h-16 bg-gradient-to-br from-[#5B8FC9] to-[#7DB8D8] rounded-full flex items-center justify-center mb-6">
|
| 98 |
+
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 99 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 100 |
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
| 101 |
+
</svg>
|
| 102 |
+
</div>
|
| 103 |
+
<h3 class="text-2xl font-bold text-gray-900 mb-3">{% trans "3. Get Recommendations" %}</h3>
|
| 104 |
+
<p class="text-gray-600">{% trans "Receive personalized size and color recommendations from your available inventory." %}</p>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
<!-- CTA Section -->
|
| 112 |
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
| 113 |
+
<div class="bg-gradient-to-r from-[#1B3A6B] to-[#2E5FA3] rounded-3xl shadow-2xl p-12 text-center">
|
| 114 |
+
<h2 class="text-4xl font-bold text-white mb-4">{% trans "Ready to Find Your Perfect Fit?" %}</h2>
|
| 115 |
+
<p class="text-xl text-white/90 mb-8">{% trans "Experience the future of clothing shopping today" %}</p>
|
| 116 |
+
<a href="{% url 'fitting_system:scan' %}"
|
| 117 |
+
class="bg-white text-[#1B3A6B] px-10 py-4 rounded-lg font-bold text-lg hover:shadow-2xl transition transform hover:scale-105 inline-block">
|
| 118 |
+
{% trans "Start Now" %}
|
| 119 |
+
</a>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
{% endblock %}
|
fitting_system/templates/inventory.html
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
{% load i18n %}
|
| 3 |
+
|
| 4 |
+
{% block title %}{% trans "Inventory Dashboard - Virtual Fitting System" %}{% endblock %}
|
| 5 |
+
|
| 6 |
+
{% block content %}
|
| 7 |
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
| 8 |
+
<div class="mb-8">
|
| 9 |
+
<h1 class="text-4xl font-bold text-gray-900 mb-2">{% trans "Inventory Dashboard" %}</h1>
|
| 10 |
+
<p class="text-xl text-gray-600">{% trans "Manage your product inventory" %}</p>
|
| 11 |
+
</div>
|
| 12 |
+
|
| 13 |
+
<!-- Summary Cards -->
|
| 14 |
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
| 15 |
+
<div class="bg-white rounded-xl shadow-md p-6">
|
| 16 |
+
<p class="text-gray-600 mb-2">{% trans "Total Variants" %}</p>
|
| 17 |
+
<p class="text-3xl font-bold text-gray-900">{{ total_variants }}</p>
|
| 18 |
+
</div>
|
| 19 |
+
<div class="bg-green-50 rounded-xl shadow-md p-6">
|
| 20 |
+
<p class="text-green-700 mb-2">{% trans "In Stock" %}</p>
|
| 21 |
+
<p class="text-3xl font-bold text-green-600">{{ in_stock|length }}</p>
|
| 22 |
+
</div>
|
| 23 |
+
<div class="bg-yellow-50 rounded-xl shadow-md p-6">
|
| 24 |
+
<p class="text-yellow-700 mb-2">{% trans "Low Stock" %}</p>
|
| 25 |
+
<p class="text-3xl font-bold text-yellow-600">{{ low_stock|length }}</p>
|
| 26 |
+
</div>
|
| 27 |
+
<div class="bg-red-50 rounded-xl shadow-md p-6">
|
| 28 |
+
<p class="text-red-700 mb-2">{% trans "Out of Stock" %}</p>
|
| 29 |
+
<p class="text-3xl font-bold text-red-600">{{ out_of_stock|length }}</p>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<!-- Low Stock Alert -->
|
| 34 |
+
{% if low_stock %}
|
| 35 |
+
<div class="bg-yellow-50 border-2 border-yellow-200 rounded-xl p-6 mb-8">
|
| 36 |
+
<h2 class="text-xl font-bold text-yellow-900 mb-4">{% trans "Low Stock Alerts" %}</h2>
|
| 37 |
+
<div class="space-y-2">
|
| 38 |
+
{% for variant in low_stock %}
|
| 39 |
+
<div class="flex items-center justify-between bg-white rounded-lg p-4">
|
| 40 |
+
<div>
|
| 41 |
+
<p class="font-medium text-gray-900">{{ variant.localized_product_name }}</p>
|
| 42 |
+
<p class="text-sm text-gray-600">{% trans "Size" %}: {{ variant.size.name }} | {% trans "Color" %}: {{ variant.localized_color_name }}</p>
|
| 43 |
+
</div>
|
| 44 |
+
<span class="px-4 py-2 bg-yellow-100 text-yellow-800 rounded-lg font-bold">
|
| 45 |
+
{{ variant.inventory.quantity|default:0 }} {% trans "left" %}
|
| 46 |
+
</span>
|
| 47 |
+
</div>
|
| 48 |
+
{% endfor %}
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
{% endif %}
|
| 52 |
+
|
| 53 |
+
<!-- Out of Stock Alert -->
|
| 54 |
+
{% if out_of_stock %}
|
| 55 |
+
<div class="bg-red-50 border-2 border-red-200 rounded-xl p-6 mb-8">
|
| 56 |
+
<h2 class="text-xl font-bold text-red-900 mb-4">{% trans "Out of Stock" %}</h2>
|
| 57 |
+
<div class="space-y-2">
|
| 58 |
+
{% for variant in out_of_stock %}
|
| 59 |
+
<div class="flex items-center justify-between bg-white rounded-lg p-4">
|
| 60 |
+
<div>
|
| 61 |
+
<p class="font-medium text-gray-900">{{ variant.localized_product_name }}</p>
|
| 62 |
+
<p class="text-sm text-gray-600">{% trans "Size" %}: {{ variant.size.name }} | {% trans "Color" %}: {{ variant.localized_color_name }}</p>
|
| 63 |
+
</div>
|
| 64 |
+
<span class="px-4 py-2 bg-red-100 text-red-800 rounded-lg font-bold">
|
| 65 |
+
{% trans "Out of Stock" %}
|
| 66 |
+
</span>
|
| 67 |
+
</div>
|
| 68 |
+
{% endfor %}
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
{% endif %}
|
| 72 |
+
|
| 73 |
+
<!-- All Inventory -->
|
| 74 |
+
<div class="bg-white rounded-xl shadow-md overflow-hidden">
|
| 75 |
+
<div class="p-6 border-b border-gray-200">
|
| 76 |
+
<h2 class="text-2xl font-bold text-gray-900">{% trans "All Inventory" %}</h2>
|
| 77 |
+
</div>
|
| 78 |
+
<div class="overflow-x-auto">
|
| 79 |
+
<table class="w-full">
|
| 80 |
+
<thead class="bg-[#EAF3F8]">
|
| 81 |
+
<tr>
|
| 82 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-[#1B3A6B] uppercase tracking-wider">
|
| 83 |
+
{% trans "Product" %}</th>
|
| 84 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-[#1B3A6B] uppercase tracking-wider">{% trans "Size" %}
|
| 85 |
+
</th>
|
| 86 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-[#1B3A6B] uppercase tracking-wider">
|
| 87 |
+
{% trans "Color" %}
|
| 88 |
+
</th>
|
| 89 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-[#1B3A6B] uppercase tracking-wider">
|
| 90 |
+
{% trans "Quantity" %}</th>
|
| 91 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-[#1B3A6B] uppercase tracking-wider">
|
| 92 |
+
{% trans "Status" %}</th>
|
| 93 |
+
</tr>
|
| 94 |
+
</thead>
|
| 95 |
+
<tbody class="bg-white divide-y divide-gray-200">
|
| 96 |
+
{% for variant in in_stock %}
|
| 97 |
+
<tr>
|
| 98 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 99 |
+
<div class="text-sm font-medium text-gray-900">{{ variant.localized_product_name }}</div>
|
| 100 |
+
<div class="text-sm text-gray-500">{{ variant.product.category|title }}</div>
|
| 101 |
+
</td>
|
| 102 |
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ variant.size.name }}</td>
|
| 103 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 104 |
+
<div class="flex items-center gap-2">
|
| 105 |
+
<div class="w-4 h-4 rounded-full border"
|
| 106 |
+
style="background-color: {{ variant.color.hex_code }};"></div>
|
| 107 |
+
<span class="text-sm text-gray-900">{{ variant.localized_color_name }}</span>
|
| 108 |
+
</div>
|
| 109 |
+
</td>
|
| 110 |
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-gray-900">
|
| 111 |
+
{{ variant.inventory.quantity|default:0 }}</td>
|
| 112 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 113 |
+
<span
|
| 114 |
+
class="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
| 115 |
+
{% trans "In Stock" %}
|
| 116 |
+
</span>
|
| 117 |
+
</td>
|
| 118 |
+
</tr>
|
| 119 |
+
{% endfor %}
|
| 120 |
+
{% for variant in low_stock %}
|
| 121 |
+
<tr class="bg-yellow-50">
|
| 122 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 123 |
+
<div class="text-sm font-medium text-gray-900">{{ variant.localized_product_name }}</div>
|
| 124 |
+
<div class="text-sm text-gray-500">{{ variant.product.category|title }}</div>
|
| 125 |
+
</td>
|
| 126 |
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ variant.size.name }}</td>
|
| 127 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 128 |
+
<div class="flex items-center gap-2">
|
| 129 |
+
<div class="w-4 h-4 rounded-full border"
|
| 130 |
+
style="background-color: {{ variant.color.hex_code }};"></div>
|
| 131 |
+
<span class="text-sm text-gray-900">{{ variant.localized_color_name }}</span>
|
| 132 |
+
</div>
|
| 133 |
+
</td>
|
| 134 |
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-yellow-900">
|
| 135 |
+
{{ variant.inventory.quantity|default:0 }}</td>
|
| 136 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 137 |
+
<span
|
| 138 |
+
class="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
| 139 |
+
{% trans "Low Stock" %}
|
| 140 |
+
</span>
|
| 141 |
+
</td>
|
| 142 |
+
</tr>
|
| 143 |
+
{% endfor %}
|
| 144 |
+
{% for variant in out_of_stock %}
|
| 145 |
+
<tr class="bg-red-50">
|
| 146 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 147 |
+
<div class="text-sm font-medium text-gray-900">{{ variant.localized_product_name }}</div>
|
| 148 |
+
<div class="text-sm text-gray-500">{{ variant.product.category|title }}</div>
|
| 149 |
+
</td>
|
| 150 |
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ variant.size.name }}</td>
|
| 151 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 152 |
+
<div class="flex items-center gap-2">
|
| 153 |
+
<div class="w-4 h-4 rounded-full border"
|
| 154 |
+
style="background-color: {{ variant.color.hex_code }};"></div>
|
| 155 |
+
<span class="text-sm text-gray-900">{{ variant.localized_color_name }}</span>
|
| 156 |
+
</div>
|
| 157 |
+
</td>
|
| 158 |
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-bold text-red-900">0</td>
|
| 159 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 160 |
+
<span
|
| 161 |
+
class="px-3 py-1 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">
|
| 162 |
+
{% trans "Out of Stock" %}
|
| 163 |
+
</span>
|
| 164 |
+
</td>
|
| 165 |
+
</tr>
|
| 166 |
+
{% endfor %}
|
| 167 |
+
</tbody>
|
| 168 |
+
</table>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
<div class="mt-8 text-center">
|
| 173 |
+
<a href="/admin/fitting_system/inventory/"
|
| 174 |
+
class="bg-[#1B3A6B] text-white px-8 py-3 rounded-lg font-bold hover:bg-[#2E5FA3] transition">
|
| 175 |
+
{% trans "Manage Inventory in Admin Panel" %}
|
| 176 |
+
</a>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
{% endblock %}
|