ammar101 commited on
Commit
0bb49b0
·
1 Parent(s): 35c2836

Deploy application code and models

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +18 -0
  2. .gemini/implementation_plan_store_recommendations.md +392 -0
  3. .gitignore +10 -0
  4. Dockerfile +41 -0
  5. HF_Deployment_Report.md +70 -0
  6. README.md +232 -0
  7. YOLOv8_Body_Measurement_and_Clothing_Recommendation_Updated (1).ipynb +0 -0
  8. avatar/FEMALE.glb +3 -0
  9. avatar/MALE.glb +3 -0
  10. avatar/index.html +699 -0
  11. compile_locale.py +31 -0
  12. config/__init__.py +0 -0
  13. config/asgi.py +16 -0
  14. config/settings.py +152 -0
  15. config/urls.py +38 -0
  16. config/wsgi.py +16 -0
  17. debug_gemini.py +37 -0
  18. fitting_system/__init__.py +0 -0
  19. fitting_system/admin.py +92 -0
  20. fitting_system/ai_modules/__init__.py +1 -0
  21. fitting_system/ai_modules/body_measurement.py +310 -0
  22. fitting_system/ai_modules/gemini_client.py +282 -0
  23. fitting_system/ai_modules/recommendation_engine.py +322 -0
  24. fitting_system/ai_modules/yolo_analyzer.py +564 -0
  25. fitting_system/apps.py +6 -0
  26. fitting_system/color_palettes.py +125 -0
  27. fitting_system/fixtures/initial_data.json +3296 -0
  28. fitting_system/management/__init__.py +1 -0
  29. fitting_system/management/commands/__init__.py +1 -0
  30. fitting_system/management/commands/populate_data.py +133 -0
  31. fitting_system/management/commands/populate_mvp.py +198 -0
  32. fitting_system/management/commands/reset_catalog.py +307 -0
  33. fitting_system/management/commands/set_password.py +18 -0
  34. fitting_system/migrations/0001_initial.py +123 -0
  35. fitting_system/migrations/0002_add_undertone_and_expand_skin_tone.py +23 -0
  36. fitting_system/migrations/0003_bodyscan_arm_length_bodyscan_body_shape_and_more.py +68 -0
  37. fitting_system/migrations/0004_bodyscan_error_message_bodyscan_is_fallback.py +23 -0
  38. fitting_system/migrations/0004_product_image.py +18 -0
  39. fitting_system/migrations/0005_remove_product_image_url.py +17 -0
  40. fitting_system/migrations/0006_merge_20260225_1734.py +14 -0
  41. fitting_system/migrations/0007_alter_product_image.py +18 -0
  42. fitting_system/migrations/0008_alter_product_image.py +19 -0
  43. fitting_system/migrations/0009_remove_product_fit_type.py +17 -0
  44. fitting_system/migrations/__init__.py +0 -0
  45. fitting_system/models.py +219 -0
  46. fitting_system/storage.py +39 -0
  47. fitting_system/templates/avatar.html +739 -0
  48. fitting_system/templates/base.html +411 -0
  49. fitting_system/templates/index.html +122 -0
  50. 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> &nbsp;👕 Shirt: <b>${shirtMeshes.length}</b> &nbsp;👖 Pants: <b>${pantsMeshes.length}</b> &nbsp;🔒 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> &nbsp;ًں‘• Shirt: <b>${shirtMeshes.length}</b> &nbsp;ًں‘– Pants: <b>${pantsMeshes.length}</b> &nbsp;ًں”’ 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 %}