Spaces:
Paused
Paused
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- GRADIO_API_CLIENT_GUIDE.md +0 -355
- app.py +29 -18
- inference.py +11 -10
- 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 |
-
|
| 844 |
print(f"🔍 DEBUG: Successfully loaded {len(images)} images from {len(files)} files")
|
| 845 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
|
| 892 |
-
# Season mappings
|
| 893 |
if tag_name == "season":
|
| 894 |
-
|
| 895 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1223 |
-
|
| 1224 |
-
|
| 1225 |
log_message = "Dataset not ready."
|
| 1226 |
-
|
| 1227 |
-
|
| 1228 |
-
|
| 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 |
-
|
| 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 |
-
|
| 63 |
-
|
| 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 |
-
|
| 303 |
-
|
| 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 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
"""
|
| 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 |
-
|
| 44 |
-
|
| 45 |
-
GALA = "gala"
|
| 46 |
-
AWARD_CEREMONY = "award_ceremony"
|
| 47 |
|
| 48 |
|
| 49 |
class WeatherTag(str, Enum):
|
| 50 |
-
"""
|
| 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 |
-
"""
|
| 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
|
| 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
|
| 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
|
| 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 |
-
|
| 166 |
-
NON_STRETCH = "non_stretch"
|
| 167 |
|
| 168 |
|
| 169 |
class BudgetTag(str, Enum):
|
| 170 |
-
"""Budget preference tags
|
| 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 |
-
|
| 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 |
-
"
|
| 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 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
}
|
| 442 |
|
| 443 |
-
if
|
| 444 |
-
preferences["preferred_categories"].extend(style_mapping[
|
| 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 |
-
"
|
|
|
|
| 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],
|