Ali Mohsin commited on
Commit
5e89af2
·
1 Parent(s): dbb02ac

Refactor image loading and tag processing logic; enhance validation for color and fit preferences. Remove deprecated Gradio API client guide. Improve code readability and maintainability.

Browse files
Files changed (4) hide show
  1. GRADIO_API_CLIENT_GUIDE.md +0 -355
  2. app.py +29 -18
  3. inference.py +11 -10
  4. utils/tag_system.py +29 -69
GRADIO_API_CLIENT_GUIDE.md DELETED
@@ -1,355 +0,0 @@
1
- # Gradio API Client Usage Guide
2
-
3
- ## Problem: File Type Validation Error
4
-
5
- When using the Gradio Client API, you may encounter:
6
- ```
7
- gradio.exceptions.Error: "Invalid file type. Please upload a file that is one of these formats: ['.jpg', '.jpeg', '.png', ...]"
8
- ```
9
-
10
- ## Solution
11
-
12
- The file type restriction has been removed from the Gradio interface to allow API client flexibility. File validation is now handled by our internal image loading utilities.
13
-
14
- ## Using Gradio Client API
15
-
16
- ### Method 1: Using File Paths (Recommended)
17
-
18
- ```python
19
- import gradio_client as grc
20
-
21
- # Connect to your Gradio server
22
- client = grc.Client("http://your-server:7860")
23
-
24
- # Prepare file paths (must be accessible from the server)
25
- file_paths = [
26
- "/path/to/image1.jpg",
27
- "/path/to/image2.png",
28
- "/path/to/image3.webp"
29
- ]
30
-
31
- # Call the API
32
- result = client.predict(
33
- files=file_paths,
34
- occasion="casual",
35
- weather="warm",
36
- num_outfits=3,
37
- outfit_style="casual",
38
- color_preference=None,
39
- fit_preference=None,
40
- material_preference=None,
41
- season=None,
42
- time_of_day=None,
43
- budget=None,
44
- personal_style=None,
45
- api_name="/gradio_recommend"
46
- )
47
-
48
- # Result contains:
49
- # - result[0]: List of stitched outfit images (PIL Images)
50
- # - result[1]: JSON dict with outfit details
51
- images, json_data = result
52
- ```
53
-
54
- ### Method 2: Using Temporary Files
55
-
56
- If you have image data in memory, save to temporary files first:
57
-
58
- ```python
59
- import gradio_client as grc
60
- import tempfile
61
- from PIL import Image
62
- import os
63
-
64
- # Connect to server
65
- client = grc.Client("http://your-server:7860")
66
-
67
- # Your image data (e.g., from requests, base64, etc.)
68
- image_data_list = [...] # Your image bytes or PIL Images
69
-
70
- # Save to temporary files
71
- temp_files = []
72
- for i, img_data in enumerate(image_data_list):
73
- if isinstance(img_data, bytes):
74
- # If bytes, save directly
75
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.jpg')
76
- temp_file.write(img_data)
77
- temp_file.close()
78
- temp_files.append(temp_file.name)
79
- elif isinstance(img_data, Image.Image):
80
- # If PIL Image, save to temp file
81
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
82
- img_data.save(temp_file.name)
83
- temp_file.close()
84
- temp_files.append(temp_file.name)
85
- else:
86
- # Assume it's a file path
87
- temp_files.append(img_data)
88
-
89
- # Call API
90
- result = client.predict(
91
- files=temp_files,
92
- occasion="formal",
93
- weather="cool",
94
- num_outfits=5,
95
- outfit_style="formal",
96
- api_name="/gradio_recommend"
97
- )
98
-
99
- # Clean up temp files
100
- for temp_file in temp_files:
101
- if os.path.exists(temp_file):
102
- os.unlink(temp_file)
103
- ```
104
-
105
- ### Method 3: Using Base64 (Convert to Files)
106
-
107
- ```python
108
- import gradio_client as grc
109
- import base64
110
- import tempfile
111
- from io import BytesIO
112
- from PIL import Image
113
-
114
- def base64_to_temp_file(base64_string, suffix='.jpg'):
115
- """Convert base64 string to temporary file"""
116
- image_data = base64.b64decode(base64_string)
117
- img = Image.open(BytesIO(image_data))
118
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
119
- img.save(temp_file.name)
120
- temp_file.close()
121
- return temp_file.name
122
-
123
- # Your base64 image strings
124
- base64_images = [
125
- "iVBORw0KGgoAAAANSUhEUgAA...", # Base64 encoded image 1
126
- "iVBORw0KGgoAAAANSUhEUgAA...", # Base64 encoded image 2
127
- ]
128
-
129
- # Convert to temp files
130
- file_paths = [base64_to_temp_file(img) for img in base64_images]
131
-
132
- # Use with Gradio client
133
- client = grc.Client("http://your-server:7860")
134
- result = client.predict(
135
- files=file_paths,
136
- occasion="casual",
137
- weather="warm",
138
- num_outfits=3,
139
- api_name="/gradio_recommend"
140
- )
141
- ```
142
-
143
- ## Alternative: Use FastAPI Endpoints Instead
144
-
145
- For better API integration, consider using the FastAPI endpoints directly:
146
-
147
- ### Option 1: `/compose/upload` (Multipart Form-Data)
148
-
149
- ```python
150
- import requests
151
-
152
- url = "http://your-server:8000/compose/upload"
153
-
154
- # Prepare files
155
- files = [
156
- ('files', ('image1.jpg', open('image1.jpg', 'rb'), 'image/jpeg')),
157
- ('files', ('image2.png', open('image2.png', 'rb'), 'image/png')),
158
- ]
159
-
160
- # Prepare form data
161
- data = {
162
- 'occasion': 'casual',
163
- 'weather': 'warm',
164
- 'style': 'casual',
165
- 'num_outfits': 3,
166
- 'color_preference': None,
167
- # ... other optional tags
168
- }
169
-
170
- # Make request
171
- response = requests.post(url, files=files, data=data)
172
- result = response.json()
173
- ```
174
-
175
- ### Option 2: `/compose` (JSON with Base64)
176
-
177
- ```python
178
- import requests
179
- import base64
180
-
181
- def image_to_base64(image_path):
182
- with open(image_path, 'rb') as f:
183
- return base64.b64encode(f.read()).decode('utf-8')
184
-
185
- url = "http://your-server:8000/compose"
186
-
187
- # Prepare items with base64 images
188
- items = [
189
- {
190
- "id": "item_0",
191
- "image_base64": image_to_base64("image1.jpg"),
192
- "category": None # Auto-detected
193
- },
194
- {
195
- "id": "item_1",
196
- "image_base64": image_to_base64("image2.png"),
197
- "category": None
198
- }
199
- ]
200
-
201
- # Prepare request
202
- payload = {
203
- "items": items,
204
- "occasion": "casual",
205
- "weather": "warm",
206
- "style": "casual",
207
- "num_outfits": 3,
208
- "color_preference": None,
209
- # ... other optional tags
210
- }
211
-
212
- # Make request
213
- response = requests.post(url, json=payload)
214
- result = response.json()
215
- ```
216
-
217
- ## Supported Image Formats
218
-
219
- The system supports all major image formats:
220
- - **JPEG/JPG** (`.jpg`, `.jpeg`)
221
- - **PNG** (`.png`)
222
- - **WEBP** (`.webp`)
223
- - **GIF** (`.gif`)
224
- - **BMP** (`.bmp`)
225
- - **TIFF** (`.tiff`, `.tif`)
226
-
227
- Images are automatically:
228
- - Validated for format
229
- - Converted to RGB mode
230
- - Handled for transparency (PNG/GIF)
231
- - Processed for model compatibility
232
-
233
- ## Troubleshooting
234
-
235
- ### Error: "Could not load images"
236
-
237
- **Possible causes:**
238
- 1. Files don't exist at the specified paths
239
- 2. Files are not valid image formats
240
- 3. Files are corrupted
241
- 4. Permission issues accessing files
242
-
243
- **Solutions:**
244
- 1. Verify file paths are correct and accessible
245
- 2. Check file extensions match actual format
246
- 3. Try opening files with PIL/Pillow to verify they're valid
247
- 4. Ensure server has read permissions for files
248
-
249
- ### Error: "No files uploaded"
250
-
251
- **Possible causes:**
252
- 1. Empty file list passed
253
- 2. Files parameter not provided correctly
254
-
255
- **Solutions:**
256
- 1. Ensure at least 2 files are provided
257
- 2. Check file paths are in a list format: `["file1.jpg", "file2.png"]`
258
-
259
- ### Error: File type validation (if still occurring)
260
-
261
- **Solution:**
262
- - The file type restriction has been removed. If you still see this error:
263
- 1. Update to the latest version of the code
264
- 2. Restart the Gradio server
265
- 3. Ensure you're not using an old cached version
266
-
267
- ## Best Practices
268
-
269
- 1. **Use FastAPI endpoints for production**: More reliable and flexible than Gradio Client API
270
- 2. **Validate images before sending**: Check that files are valid images
271
- 3. **Handle errors gracefully**: Check response for error messages
272
- 4. **Use appropriate image formats**: JPG for photos, PNG for transparency, WEBP for web
273
- 5. **Clean up temp files**: If using temporary files, delete them after use
274
-
275
- ## Example: Complete Client Script
276
-
277
- ```python
278
- import gradio_client as grc
279
- import os
280
-
281
- def recommend_outfits(
282
- image_paths: list,
283
- occasion: str = "casual",
284
- weather: str = "warm",
285
- num_outfits: int = 3,
286
- outfit_style: str = "casual",
287
- **optional_tags
288
- ):
289
- """Complete example of using Gradio API for recommendations"""
290
-
291
- # Validate inputs
292
- if not image_paths or len(image_paths) < 2:
293
- raise ValueError("At least 2 images required")
294
-
295
- for path in image_paths:
296
- if not os.path.exists(path):
297
- raise FileNotFoundError(f"Image not found: {path}")
298
-
299
- # Connect to server
300
- client = grc.Client("http://localhost:7860")
301
-
302
- # Prepare parameters
303
- params = {
304
- "files": image_paths,
305
- "occasion": occasion,
306
- "weather": weather,
307
- "num_outfits": num_outfits,
308
- "outfit_style": outfit_style,
309
- **{k: v for k, v in optional_tags.items() if v is not None}
310
- }
311
-
312
- # Make request
313
- try:
314
- result = client.predict(api_name="/gradio_recommend", **params)
315
- images, json_data = result
316
-
317
- # Check for errors
318
- if "error" in json_data:
319
- raise RuntimeError(f"API Error: {json_data['error']}")
320
-
321
- return images, json_data["outfits"]
322
-
323
- except Exception as e:
324
- raise RuntimeError(f"Failed to get recommendations: {str(e)}")
325
-
326
- # Usage
327
- if __name__ == "__main__":
328
- images, outfits = recommend_outfits(
329
- image_paths=["shirt.jpg", "pants.jpg", "shoes.jpg"],
330
- occasion="formal",
331
- weather="cool",
332
- num_outfits=5,
333
- outfit_style="formal",
334
- color_preference="neutral",
335
- fit_preference="tailored"
336
- )
337
-
338
- print(f"Generated {len(outfits)} outfits")
339
- for i, outfit in enumerate(outfits):
340
- print(f"Outfit {i+1}: {outfit['item_ids']}")
341
- ```
342
-
343
- ## API Endpoints Summary
344
-
345
- | Endpoint | Method | Use Case |
346
- |----------|--------|----------|
347
- | `/compose` | POST | JSON API with base64 images |
348
- | `/compose/upload` | POST | Multipart form-data with files |
349
- | `/tags` | GET | Get all available tag options |
350
- | `/image-formats` | GET | Get supported image formats |
351
- | `/health` | GET | Check model status |
352
- | Gradio `/gradio_recommend` | POST | Gradio Client API (file paths) |
353
-
354
- For production use, prefer FastAPI endpoints (`/compose` or `/compose/upload`) over Gradio Client API for better reliability and error handling.
355
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -840,9 +840,9 @@ def gradio_recommend(
840
 
841
  try:
842
  print(f"🔍 DEBUG: Attempting to load {len(files)} images...")
843
- images = _load_images_from_files(files)
844
  print(f"🔍 DEBUG: Successfully loaded {len(images)} images from {len(files)} files")
845
- if not images:
846
  error_msg = "Could not load images from uploaded files.\n\n"
847
  error_msg += f"Files received: {len(files)}\n"
848
  error_msg += f"File details: {', '.join(file_info[:3])}\n\n"
@@ -869,7 +869,7 @@ def gradio_recommend(
869
 
870
  value_lower = value.lower().strip()
871
 
872
- # Color preference mappings
873
  if tag_name == "color_preference":
874
  color_mappings = {
875
  "monochrome": "monochromatic", # Frontend uses "monochrome", backend uses "monochromatic"
@@ -877,7 +877,14 @@ def gradio_recommend(
877
  "single_color": "monochromatic",
878
  "one_color": "monochromatic",
879
  }
880
- return color_mappings.get(value_lower, value) # Return mapped value or original
 
 
 
 
 
 
 
881
 
882
  # Fit preference mappings
883
  if tag_name == "fit_preference":
@@ -887,14 +894,18 @@ def gradio_recommend(
887
  "baggy": "loose",
888
  "wide": "loose",
889
  }
890
- return fit_mappings.get(value_lower, value)
 
 
 
 
 
 
891
 
892
- # Season mappings
893
  if tag_name == "season":
894
- season_mappings = {
895
- "autumn": "fall", # Both are valid, but normalize to "fall"
896
- }
897
- return season_mappings.get(value_lower, value)
898
 
899
  # Return original value if no mapping found
900
  return value
@@ -1017,7 +1028,7 @@ def start_training_advanced(
1017
  return "❌ Dataset not ready. Please wait for bootstrap to complete."
1018
 
1019
  log_message = "🚀 Advanced training started with custom parameters! Check the log below for progress."
1020
- def _runner():
1021
  nonlocal log_message
1022
  try:
1023
  import subprocess
@@ -1219,13 +1230,13 @@ def start_training_simple(dataset_size: str, res_epochs: int, vit_epochs: int):
1219
  log_message = f"Starting training on {dataset_size} samples..."
1220
  def _runner():
1221
  nonlocal log_message
1222
- try:
1223
- import subprocess
1224
- if not DATASET_ROOT:
1225
  log_message = "Dataset not ready."
1226
- return
1227
- export_dir = os.getenv("EXPORT_DIR", "models/exports")
1228
- os.makedirs(export_dir, exist_ok=True)
1229
  log_message = f"Training ResNet on {dataset_size} samples...\n"
1230
  # Add dataset size limit if not full
1231
  dataset_args = []
@@ -1274,7 +1285,7 @@ def start_training_simple(dataset_size: str, res_epochs: int, vit_epochs: int):
1274
  else:
1275
  log_message += f"❌ ViT training failed: {vit_result.stderr}\n"
1276
  return log_message
1277
- service.reload_models()
1278
 
1279
  # Check if models loaded successfully
1280
  model_status = service.get_model_status()
 
840
 
841
  try:
842
  print(f"🔍 DEBUG: Attempting to load {len(files)} images...")
843
+ images = _load_images_from_files(files)
844
  print(f"🔍 DEBUG: Successfully loaded {len(images)} images from {len(files)} files")
845
+ if not images:
846
  error_msg = "Could not load images from uploaded files.\n\n"
847
  error_msg += f"Files received: {len(files)}\n"
848
  error_msg += f"File details: {', '.join(file_info[:3])}\n\n"
 
869
 
870
  value_lower = value.lower().strip()
871
 
872
+ # Color preference mappings - normalize variations to standard values
873
  if tag_name == "color_preference":
874
  color_mappings = {
875
  "monochrome": "monochromatic", # Frontend uses "monochrome", backend uses "monochromatic"
 
877
  "single_color": "monochromatic",
878
  "one_color": "monochromatic",
879
  }
880
+ normalized = color_mappings.get(value_lower, value)
881
+ # Validate against allowed values
882
+ allowed_colors = ["neutral", "monochromatic", "complementary", "bold", "subtle",
883
+ "bright", "muted", "pastel", "dark", "light", "earth_tones",
884
+ "jewel_tones", "black_white", "navy_white", "colorful", "minimal_color"]
885
+ if normalized in allowed_colors:
886
+ return normalized
887
+ return value # Return original if not in allowed list
888
 
889
  # Fit preference mappings
890
  if tag_name == "fit_preference":
 
894
  "baggy": "loose",
895
  "wide": "loose",
896
  }
897
+ normalized = fit_mappings.get(value_lower, value)
898
+ # Validate against allowed values
899
+ allowed_fits = ["fitted", "loose", "oversized", "relaxed", "comfortable",
900
+ "structured", "flowy", "tailored", "athletic_fit", "regular_fit"]
901
+ if normalized in allowed_fits:
902
+ return normalized
903
+ return value
904
 
905
+ # Season mappings - both "fall" and "autumn" are valid, keep as-is
906
  if tag_name == "season":
907
+ # Both "fall" and "autumn" are in the Literal type, so no normalization needed
908
+ return value
 
 
909
 
910
  # Return original value if no mapping found
911
  return value
 
1028
  return "❌ Dataset not ready. Please wait for bootstrap to complete."
1029
 
1030
  log_message = "🚀 Advanced training started with custom parameters! Check the log below for progress."
1031
+ def _runner():
1032
  nonlocal log_message
1033
  try:
1034
  import subprocess
 
1230
  log_message = f"Starting training on {dataset_size} samples..."
1231
  def _runner():
1232
  nonlocal log_message
1233
+ try:
1234
+ import subprocess
1235
+ if not DATASET_ROOT:
1236
  log_message = "Dataset not ready."
1237
+ return
1238
+ export_dir = os.getenv("EXPORT_DIR", "models/exports")
1239
+ os.makedirs(export_dir, exist_ok=True)
1240
  log_message = f"Training ResNet on {dataset_size} samples...\n"
1241
  # Add dataset size limit if not full
1242
  dataset_args = []
 
1285
  else:
1286
  log_message += f"❌ ViT training failed: {vit_result.stderr}\n"
1287
  return log_message
1288
+ service.reload_models()
1289
 
1290
  # Check if models loaded successfully
1291
  model_status = service.get_model_status()
inference.py CHANGED
@@ -59,8 +59,8 @@ class InferenceService:
59
  # Disable gradients
60
  for m in [self.resnet, self.vit]:
61
  if m is not None:
62
- for p in m.parameters():
63
- p.requires_grad_(False)
64
 
65
  # Update overall status
66
  self.models_loaded = self.resnet_loaded and self.vit_loaded
@@ -299,8 +299,8 @@ class InferenceService:
299
  # Disable gradients
300
  for m in [self.resnet, self.vit]:
301
  if m is not None:
302
- for p in m.parameters():
303
- p.requires_grad_(False)
304
 
305
  # Update overall status
306
  self.models_loaded = self.resnet_loaded and self.vit_loaded
@@ -347,11 +347,11 @@ class InferenceService:
347
 
348
  try:
349
  batch = torch.stack([self.transform(img) for img in processed_images])
350
- batch = batch.to(self.device, memory_format=torch.channels_last)
351
- use_amp = (self.device == "cuda")
352
- with torch.autocast(device_type=("cuda" if use_amp else "cpu"), enabled=use_amp):
353
- emb = self.resnet(batch)
354
- emb = nn.functional.normalize(emb, dim=-1)
355
  result = [e.detach().cpu().numpy().astype(np.float32) for e in emb]
356
  print(f"🔍 DEBUG: Successfully generated {len(result)} embeddings")
357
  return result
@@ -518,7 +518,8 @@ class InferenceService:
518
  # Extract primary tags (backward compatible)
519
  occasion = context.get("occasion", processed_tags["primary_tags"].get("occasion", "casual"))
520
  weather = context.get("weather", processed_tags["primary_tags"].get("weather", "any"))
521
- outfit_style = context.get("outfit_style", context.get("style", processed_tags["primary_tags"].get("style", "casual")))
 
522
 
523
  # Select base template
524
  template_name = outfit_style
 
59
  # Disable gradients
60
  for m in [self.resnet, self.vit]:
61
  if m is not None:
62
+ for p in m.parameters():
63
+ p.requires_grad_(False)
64
 
65
  # Update overall status
66
  self.models_loaded = self.resnet_loaded and self.vit_loaded
 
299
  # Disable gradients
300
  for m in [self.resnet, self.vit]:
301
  if m is not None:
302
+ for p in m.parameters():
303
+ p.requires_grad_(False)
304
 
305
  # Update overall status
306
  self.models_loaded = self.resnet_loaded and self.vit_loaded
 
347
 
348
  try:
349
  batch = torch.stack([self.transform(img) for img in processed_images])
350
+ batch = batch.to(self.device, memory_format=torch.channels_last)
351
+ use_amp = (self.device == "cuda")
352
+ with torch.autocast(device_type=("cuda" if use_amp else "cpu"), enabled=use_amp):
353
+ emb = self.resnet(batch)
354
+ emb = nn.functional.normalize(emb, dim=-1)
355
  result = [e.detach().cpu().numpy().astype(np.float32) for e in emb]
356
  print(f"🔍 DEBUG: Successfully generated {len(result)} embeddings")
357
  return result
 
518
  # Extract primary tags (backward compatible)
519
  occasion = context.get("occasion", processed_tags["primary_tags"].get("occasion", "casual"))
520
  weather = context.get("weather", processed_tags["primary_tags"].get("weather", "any"))
521
+ # Support both "outfit_style" and "style" for backward compatibility
522
+ outfit_style = context.get("outfit_style") or context.get("style") or processed_tags["primary_tags"].get("outfit_style") or processed_tags["primary_tags"].get("style", "casual")
523
 
524
  # Select base template
525
  template_name = outfit_style
utils/tag_system.py CHANGED
@@ -10,7 +10,7 @@ from enum import Enum
10
 
11
 
12
  class OccasionTag(str, Enum):
13
- """Expanded occasion tags for different events and contexts"""
14
  CASUAL = "casual"
15
  BUSINESS = "business"
16
  FORMAL = "formal"
@@ -25,29 +25,17 @@ class OccasionTag(str, Enum):
25
  TRAVEL = "travel"
26
  BEACH = "beach"
27
  OUTDOOR = "outdoor"
28
- INDOOR = "indoor"
29
  NIGHT_OUT = "night_out"
30
  BRUNCH = "brunch"
31
- SHOPPING = "shopping"
32
- CULTURAL = "cultural"
33
- RELIGIOUS = "religious"
34
- FESTIVAL = "festival"
35
- CONCERT = "concert"
36
- THEATER = "theater"
37
- MUSEUM = "museum"
38
  DINNER = "dinner"
39
- LUNCH = "lunch"
40
  MEETING = "meeting"
41
- PRESENTATION = "presentation"
42
  INTERVIEW = "interview"
43
- GRADUATION = "graduation"
44
- PROM = "prom"
45
- GALA = "gala"
46
- AWARD_CEREMONY = "award_ceremony"
47
 
48
 
49
  class WeatherTag(str, Enum):
50
- """Expanded weather tags for climate-appropriate recommendations"""
51
  ANY = "any"
52
  HOT = "hot"
53
  WARM = "warm"
@@ -59,16 +47,12 @@ class WeatherTag(str, Enum):
59
  SNOW = "snow"
60
  WINDY = "windy"
61
  HUMID = "humid"
62
- DRY = "dry"
63
  SUNNY = "sunny"
64
  CLOUDY = "cloudy"
65
- OVERCAST = "overcast"
66
- STORMY = "stormy"
67
- FOGGY = "foggy"
68
 
69
 
70
  class StyleTag(str, Enum):
71
- """Expanded style tags for different fashion aesthetics"""
72
  CASUAL = "casual"
73
  SMART_CASUAL = "smart_casual"
74
  FORMAL = "formal"
@@ -76,39 +60,19 @@ class StyleTag(str, Enum):
76
  ATHLETIC = "athletic"
77
  STREETWEAR = "streetwear"
78
  MINIMALIST = "minimalist"
79
- BOHEMIAN = "bohemian"
80
- VINTAGE = "vintage"
81
  CLASSIC = "classic"
82
  MODERN = "modern"
83
- EDGY = "edgy"
84
  ELEGANT = "elegant"
85
  SOPHISTICATED = "sophisticated"
86
- CHIC = "chic"
87
- GLAMOROUS = "glamorous"
88
- ROMANTIC = "romantic"
89
- PREPPY = "preppy"
90
- GRUNGE = "grunge"
91
- PUNK = "punk"
92
- GOTH = "goth"
93
  TRADITIONAL = "traditional"
94
  ETHNIC = "ethnic"
95
- CULTURAL = "cultural"
96
- WESTERN = "western"
97
- EASTERN = "eastern"
98
- CONTEMPORARY = "contemporary"
99
- AVANT_GARDE = "avant_garde"
100
- ARTSY = "artsy"
101
- COMFORTABLE = "comfortable"
102
- PRACTICAL = "practical"
103
 
104
 
105
  class ColorPreferenceTag(str, Enum):
106
- """Color preference tags for personalized color schemes"""
107
  NEUTRAL = "neutral"
108
  MONOCHROMATIC = "monochromatic"
109
  COMPLEMENTARY = "complementary"
110
- ANALOGOUS = "analogous"
111
- TRIADIC = "triadic"
112
  BOLD = "bold"
113
  SUBTLE = "subtle"
114
  BRIGHT = "bright"
@@ -118,33 +82,28 @@ class ColorPreferenceTag(str, Enum):
118
  LIGHT = "light"
119
  EARTH_TONES = "earth_tones"
120
  JEWEL_TONES = "jewel_tones"
121
- PRIMARY_COLORS = "primary_colors"
122
  BLACK_WHITE = "black_white"
123
  NAVY_WHITE = "navy_white"
124
- BROWN_BEIGE = "brown_beige"
125
  COLORFUL = "colorful"
126
  MINIMAL_COLOR = "minimal_color"
127
 
128
 
129
  class FitPreferenceTag(str, Enum):
130
- """Fit preference tags for body type and comfort"""
131
  FITTED = "fitted"
132
  LOOSE = "loose"
133
  OVERSIZED = "oversized"
134
- SKINNY = "skinny"
135
  RELAXED = "relaxed"
136
- TIGHT = "tight"
137
  COMFORTABLE = "comfortable"
138
  STRUCTURED = "structured"
139
  FLOWY = "flowy"
140
  TAILORED = "tailored"
141
- BAGGY = "baggy"
142
  ATHLETIC_FIT = "athletic_fit"
143
  REGULAR_FIT = "regular_fit"
144
 
145
 
146
  class MaterialPreferenceTag(str, Enum):
147
- """Material preference tags for fabric choices"""
148
  COTTON = "cotton"
149
  LINEN = "linen"
150
  SILK = "silk"
@@ -152,30 +111,20 @@ class MaterialPreferenceTag(str, Enum):
152
  CASHMERE = "cashmere"
153
  DENIM = "denim"
154
  LEATHER = "leather"
155
- SUEDE = "suede"
156
- SYNTHETIC = "synthetic"
157
- POLYESTER = "polyester"
158
- RAYON = "rayon"
159
- BAMBOO = "bamboo"
160
- ORGANIC = "organic"
161
- SUSTAINABLE = "sustainable"
162
  BREATHABLE = "breathable"
163
  WATERPROOF = "waterproof"
164
  MOISTURE_WICKING = "moisture_wicking"
165
- STRETCH = "stretch"
166
- NON_STRETCH = "non_stretch"
167
 
168
 
169
  class BudgetTag(str, Enum):
170
- """Budget preference tags for price-conscious recommendations"""
171
  LUXURY = "luxury"
172
  PREMIUM = "premium"
173
  MID_RANGE = "mid_range"
174
  AFFORDABLE = "affordable"
175
  BUDGET = "budget"
176
  VALUE = "value"
177
- INVESTMENT = "investment"
178
- DISPOSABLE = "disposable"
179
 
180
 
181
  class AgeGroupTag(str, Enum):
@@ -303,10 +252,10 @@ class TagProcessor:
303
  "constraints": {}
304
  }
305
 
306
- # Extract and validate tags
307
  occasion = tags.get("occasion", "casual")
308
  weather = tags.get("weather", "any")
309
- style = tags.get("style", "casual")
310
  color_preference = tags.get("color_preference", None)
311
  fit_preference = tags.get("fit_preference", None)
312
  material_preference = tags.get("material_preference", None)
@@ -321,7 +270,7 @@ class TagProcessor:
321
  processed["primary_tags"] = {
322
  "occasion": occasion,
323
  "weather": weather,
324
- "style": style
325
  }
326
 
327
  # Secondary tags (medium influence)
@@ -431,17 +380,27 @@ class TagProcessor:
431
  "style_keywords": []
432
  }
433
 
434
- # Map tags to preferences
 
435
  style_mapping = {
436
  "formal": ["blazer", "jacket", "dress shirt", "dress pant", "oxford"],
437
  "casual": ["tshirt", "jean", "sneaker", "hoodie"],
438
  "sporty": ["athletic shirt", "jogger", "running shoe", "tank"],
 
439
  "business": ["shirt", "pants", "loafer", "blazer"],
440
- "traditional": ["kameez", "kurta", "shalwar", "peshawari"]
 
 
 
 
 
 
 
 
441
  }
442
 
443
- if tags.get("style") in style_mapping:
444
- preferences["preferred_categories"].extend(style_mapping[tags["style"]])
445
 
446
  # Add synergies as style keywords
447
  preferences["style_keywords"].extend(synergies)
@@ -488,7 +447,8 @@ def get_all_tag_options() -> Dict[str, List[str]]:
488
  return {
489
  "occasion": [tag.value for tag in OccasionTag],
490
  "weather": [tag.value for tag in WeatherTag],
491
- "style": [tag.value for tag in StyleTag],
 
492
  "color_preference": [tag.value for tag in ColorPreferenceTag],
493
  "fit_preference": [tag.value for tag in FitPreferenceTag],
494
  "material_preference": [tag.value for tag in MaterialPreferenceTag],
 
10
 
11
 
12
  class OccasionTag(str, Enum):
13
+ """Occasion tags matching API Literal types"""
14
  CASUAL = "casual"
15
  BUSINESS = "business"
16
  FORMAL = "formal"
 
25
  TRAVEL = "travel"
26
  BEACH = "beach"
27
  OUTDOOR = "outdoor"
 
28
  NIGHT_OUT = "night_out"
29
  BRUNCH = "brunch"
 
 
 
 
 
 
 
30
  DINNER = "dinner"
 
31
  MEETING = "meeting"
 
32
  INTERVIEW = "interview"
33
+ CULTURAL = "cultural"
34
+ TRADITIONAL = "traditional"
 
 
35
 
36
 
37
  class WeatherTag(str, Enum):
38
+ """Weather tags matching API Literal types"""
39
  ANY = "any"
40
  HOT = "hot"
41
  WARM = "warm"
 
47
  SNOW = "snow"
48
  WINDY = "windy"
49
  HUMID = "humid"
 
50
  SUNNY = "sunny"
51
  CLOUDY = "cloudy"
 
 
 
52
 
53
 
54
  class StyleTag(str, Enum):
55
+ """Style tags matching API Literal types (outfit_style)"""
56
  CASUAL = "casual"
57
  SMART_CASUAL = "smart_casual"
58
  FORMAL = "formal"
 
60
  ATHLETIC = "athletic"
61
  STREETWEAR = "streetwear"
62
  MINIMALIST = "minimalist"
 
 
63
  CLASSIC = "classic"
64
  MODERN = "modern"
 
65
  ELEGANT = "elegant"
66
  SOPHISTICATED = "sophisticated"
 
 
 
 
 
 
 
67
  TRADITIONAL = "traditional"
68
  ETHNIC = "ethnic"
 
 
 
 
 
 
 
 
69
 
70
 
71
  class ColorPreferenceTag(str, Enum):
72
+ """Color preference tags matching API Literal types"""
73
  NEUTRAL = "neutral"
74
  MONOCHROMATIC = "monochromatic"
75
  COMPLEMENTARY = "complementary"
 
 
76
  BOLD = "bold"
77
  SUBTLE = "subtle"
78
  BRIGHT = "bright"
 
82
  LIGHT = "light"
83
  EARTH_TONES = "earth_tones"
84
  JEWEL_TONES = "jewel_tones"
 
85
  BLACK_WHITE = "black_white"
86
  NAVY_WHITE = "navy_white"
 
87
  COLORFUL = "colorful"
88
  MINIMAL_COLOR = "minimal_color"
89
 
90
 
91
  class FitPreferenceTag(str, Enum):
92
+ """Fit preference tags matching API Literal types"""
93
  FITTED = "fitted"
94
  LOOSE = "loose"
95
  OVERSIZED = "oversized"
 
96
  RELAXED = "relaxed"
 
97
  COMFORTABLE = "comfortable"
98
  STRUCTURED = "structured"
99
  FLOWY = "flowy"
100
  TAILORED = "tailored"
 
101
  ATHLETIC_FIT = "athletic_fit"
102
  REGULAR_FIT = "regular_fit"
103
 
104
 
105
  class MaterialPreferenceTag(str, Enum):
106
+ """Material preference tags matching API Literal types"""
107
  COTTON = "cotton"
108
  LINEN = "linen"
109
  SILK = "silk"
 
111
  CASHMERE = "cashmere"
112
  DENIM = "denim"
113
  LEATHER = "leather"
 
 
 
 
 
 
 
114
  BREATHABLE = "breathable"
115
  WATERPROOF = "waterproof"
116
  MOISTURE_WICKING = "moisture_wicking"
117
+ SUSTAINABLE = "sustainable"
 
118
 
119
 
120
  class BudgetTag(str, Enum):
121
+ """Budget preference tags matching API Literal types"""
122
  LUXURY = "luxury"
123
  PREMIUM = "premium"
124
  MID_RANGE = "mid_range"
125
  AFFORDABLE = "affordable"
126
  BUDGET = "budget"
127
  VALUE = "value"
 
 
128
 
129
 
130
  class AgeGroupTag(str, Enum):
 
252
  "constraints": {}
253
  }
254
 
255
+ # Extract and validate tags (support both "style" and "outfit_style" for backward compatibility)
256
  occasion = tags.get("occasion", "casual")
257
  weather = tags.get("weather", "any")
258
+ outfit_style = tags.get("outfit_style") or tags.get("style", "casual") # Support both keys
259
  color_preference = tags.get("color_preference", None)
260
  fit_preference = tags.get("fit_preference", None)
261
  material_preference = tags.get("material_preference", None)
 
270
  processed["primary_tags"] = {
271
  "occasion": occasion,
272
  "weather": weather,
273
+ "outfit_style": outfit_style # Use outfit_style consistently
274
  }
275
 
276
  # Secondary tags (medium influence)
 
380
  "style_keywords": []
381
  }
382
 
383
+ # Map tags to preferences (support both "style" and "outfit_style")
384
+ style_value = tags.get("outfit_style") or tags.get("style", "casual")
385
  style_mapping = {
386
  "formal": ["blazer", "jacket", "dress shirt", "dress pant", "oxford"],
387
  "casual": ["tshirt", "jean", "sneaker", "hoodie"],
388
  "sporty": ["athletic shirt", "jogger", "running shoe", "tank"],
389
+ "athletic": ["athletic shirt", "jogger", "running shoe", "tank"],
390
  "business": ["shirt", "pants", "loafer", "blazer"],
391
+ "smart_casual": ["shirt", "chino", "loafer", "blazer"],
392
+ "traditional": ["kameez", "kurta", "shalwar", "peshawari"],
393
+ "ethnic": ["kameez", "kurta", "shalwar", "peshawari"],
394
+ "elegant": ["blazer", "dress shirt", "dress pant", "oxford"],
395
+ "sophisticated": ["blazer", "dress shirt", "dress pant", "oxford"],
396
+ "minimalist": ["tshirt", "jean", "sneaker", "hoodie"],
397
+ "classic": ["shirt", "pants", "loafer", "blazer"],
398
+ "modern": ["tshirt", "jean", "sneaker", "hoodie"],
399
+ "streetwear": ["tshirt", "jean", "sneaker", "hoodie"]
400
  }
401
 
402
+ if style_value in style_mapping:
403
+ preferences["preferred_categories"].extend(style_mapping[style_value])
404
 
405
  # Add synergies as style keywords
406
  preferences["style_keywords"].extend(synergies)
 
447
  return {
448
  "occasion": [tag.value for tag in OccasionTag],
449
  "weather": [tag.value for tag in WeatherTag],
450
+ "outfit_style": [tag.value for tag in StyleTag], # Use outfit_style to match API
451
+ "style": [tag.value for tag in StyleTag], # Keep for backward compatibility
452
  "color_preference": [tag.value for tag in ColorPreferenceTag],
453
  "fit_preference": [tag.value for tag in FitPreferenceTag],
454
  "material_preference": [tag.value for tag in MaterialPreferenceTag],