HelloWorld0204 commited on
Commit
46c84fd
·
verified ·
1 Parent(s): e08551d

Upload 21 files

Browse files
.gitattributes CHANGED
@@ -1,35 +1,35 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,183 +1,183 @@
1
- ---
2
- title: Wardrobe Backend API
3
- sdk: gradio
4
- pinned: false
5
- ---
6
-
7
- # Wardrobe Backend API
8
-
9
- Production backend for Wardrobe Assistant, designed to run on Hugging Face Spaces.
10
-
11
- The service provides:
12
- - garment classification from uploaded images,
13
- - wardrobe item persistence,
14
- - AI outfit scoring and recommendation,
15
- - shopping suggestion and product URL extraction,
16
- - lightweight feedback capture for preference signals.
17
-
18
- The API is built with FastAPI, uses SQLite for persistence, and integrates external AI providers for inference.
19
-
20
- ## Architecture Summary
21
-
22
- - Runtime: FastAPI + Uvicorn
23
- - Storage: SQLite (persistent when `/data` is mounted on Hugging Face)
24
- - Inference: Hugging Face-hosted fine-tuned Qwen model (primary); NVIDIA-hosted chat completions used as fallback (default fallback model: `qwen/qwen3.5-122b-a10b`)
25
- - Retrieval: Web scraping pipeline for product discovery (Nike and Zalando logic in code)
26
-
27
- Core modules:
28
- - `app.py`: API routes, orchestration, inference calls, scraper flow
29
- - `db.py`: SQLite schema and CRUD/caching helpers
30
- - `scoring.py`: deterministic fallback scoring logic
31
- - `fashion_ai/`: recommendation service and ranking support
32
-
33
- ## Repository Contents for Deployment
34
-
35
- Upload this backend directory as your Hugging Face Space source (or sync it via Git):
36
-
37
- - `app.py`
38
- - `db.py`
39
- - `scoring.py`
40
- - `scraper.py`
41
- - `zalando_scraper.py`
42
- - `requirements.txt`
43
- - `packages.txt`
44
- - `fashion_ai/`
45
-
46
- ## Hugging Face Deployment
47
-
48
- 1. Create a new Space.
49
- 2. Select `Gradio` SDK.
50
- 3. Use CPU hardware (inference is delegated to external APIs).
51
- 4. Enable Persistent Storage if you want data durability across restarts.
52
- 5. Add the required environment variables.
53
- 6. Deploy the backend files.
54
-
55
- ### Required Environment Variables
56
-
57
- - `HF_API_KEY`: API key for the primary Hugging Face-hosted fine-tuned Qwen model.
58
- - `NVIDIA_API_KEY`: API key for the NVIDIA inference fallback.
59
-
60
- ### Common Optional Environment Variables
61
-
62
- Inference and reliability:
63
- - `HF_MODEL_ID` (default: your fine-tuned Qwen model on Hugging Face)
64
- - `HF_INVOKE_URL` (default: Hugging Face Inference API endpoint for the fine-tuned model)
65
- - `NVIDIA_MODEL_ID` (fallback; default: `qwen/qwen3.5-122b-a10b`)
66
- - `NVIDIA_INVOKE_URL` (fallback; default: `https://integrate.api.nvidia.com/v1/chat/completions`)
67
- - `OPENAI_MODEL_ID` (secondary fallback; OpenAI-compatible model ID if both primary and NVIDIA fallback are unavailable)
68
- - `OPENAI_API_KEY` (secondary fallback; required only if OpenAI fallback is enabled)
69
- - `NVIDIA_MAX_TOKENS` (default: `16384`)
70
- - `NVIDIA_REASONING_MAX_TOKENS` (default: `16384`)
71
- - `NVIDIA_TEMPERATURE` (default: `0.60`)
72
- - `NVIDIA_TOP_P` (default: `0.95`)
73
- - `NVIDIA_TIMEOUT_SECONDS` (default: `180`)
74
- - `NVIDIA_MAX_RETRIES` (default: `3`)
75
- - `NVIDIA_RETRY_BACKOFF_SECONDS` (default: `0.8`)
76
- - `NVIDIA_ENABLE_THINKING` (default: `false`)
77
- - `NVIDIA_FALLBACK_MODEL_IDS` (comma-separated fallback list)
78
-
79
- Matching and cache:
80
- - `MATCHING_RESULT_CACHE_MAX` (default: `500`)
81
- - `MATCHING_RESULT_CACHE_TTL_SECONDS` (default: `86400`)
82
-
83
- Scraper and planner:
84
- - `SCRAPER_DEFAULT_STORE` (default: `nike`)
85
- - `KIMI_MODEL_ID` (default: `moonshotai/kimi-k2.5`)
86
- - `KIMI_MAX_TOKENS` (default: `800`)
87
-
88
- Database path:
89
- - `DB_PATH` (optional override)
90
-
91
- When `DB_PATH` is not provided, the app uses:
92
- - `/data/wardrobe.db` if `/data` exists,
93
- - otherwise `./wardrobe.db`.
94
-
95
- ## Inference Priority
96
-
97
- The service resolves inference providers in the following order:
98
-
99
- 1. **Primary** - Fine-tuned Qwen model hosted on Hugging Face (`HF_MODEL_ID`).
100
- 2. **Fallback 1** - NVIDIA-hosted chat completions (`NVIDIA_MODEL_ID`, default: `qwen/qwen3.5-122b-a10b`). Used when the primary model is unavailable or returns an error.
101
- 3. **Fallback 2** - OpenAI-compatible model (`OPENAI_MODEL_ID`). Used when both the primary and NVIDIA fallback are unavailable.
102
-
103
- AI-powered routes return a service-level error only when all three providers are exhausted or unconfigured.
104
-
105
- ## API Endpoints
106
-
107
- Health and service metadata:
108
- - `GET /`
109
- - `GET /health`
110
-
111
- Wardrobe ingestion and CRUD:
112
- - `POST /classify`
113
- - `POST /upload`
114
- - `GET /items`
115
- - `PUT /items/{item_id}`
116
- - `DELETE /items/{item_id}`
117
-
118
- Outfit intelligence:
119
- - `POST /ai/score-outfit`
120
- - `POST /ai/gap-analysis`
121
- - `POST /ai/recommend-outfits`
122
- - `POST /feedback`
123
-
124
- Shopping and scraping:
125
- - `POST /product-urls`
126
- - `POST /suggestions`
127
- - `POST /api/suggestions`
128
- - `POST /scraper/recommend`
129
- - `GET /scraper`
130
- - `GET /image-proxy`
131
-
132
- ## Local Development
133
-
134
- ### 1. Install dependencies
135
-
136
- ```bash
137
- pip install -r requirements.txt
138
- ```
139
-
140
- ### 2. Export environment variables
141
-
142
- Linux/macOS:
143
-
144
- ```bash
145
- export HF_API_KEY=""
146
- export NVIDIA_API_KEY="" # fallback
147
- export OPENAI_API_KEY="" # secondary fallback, optional
148
- ```
149
-
150
- Windows PowerShell:
151
-
152
- ```powershell
153
- $env:HF_API_KEY = ""
154
- $env:NVIDIA_API_KEY = "" # fallback
155
- $env:OPENAI_API_KEY = "" # secondary fallback, optional
156
- ```
157
-
158
- ### 3. Run the API
159
-
160
- ```bash
161
- python app.py
162
- ```
163
-
164
- The service starts on `http://0.0.0.0:7860`.
165
-
166
- ## Smoke Checks
167
-
168
- Health:
169
-
170
- ```bash
171
- curl "http://127.0.0.1:7860/health"
172
- ```
173
-
174
- Image classification:
175
-
176
- ```bash
177
- curl -X POST "http://127.0.0.1:7860/classify" \
178
- -F "image=@/path/to/garment.jpg"
179
- ```
180
-
181
- Expected post-deploy health signal:
182
- - `hf_api_configured` should be `"True"` (primary model).
183
  - `nvidia_api_configured` should be `"True"` (fallback model).
 
1
+ ---
2
+ title: Wardrobe Backend API
3
+ sdk: gradio
4
+ pinned: false
5
+ ---
6
+
7
+ # Wardrobe Backend API
8
+
9
+ Production backend for Wardrobe Assistant, designed to run on Hugging Face Spaces.
10
+
11
+ The service provides:
12
+ - garment classification from uploaded images,
13
+ - wardrobe item persistence,
14
+ - AI outfit scoring and recommendation,
15
+ - shopping suggestion and product URL extraction,
16
+ - lightweight feedback capture for preference signals.
17
+
18
+ The API is built with FastAPI, uses SQLite for persistence, and integrates external AI providers for inference.
19
+
20
+ ## Architecture Summary
21
+
22
+ - Runtime: FastAPI + Uvicorn
23
+ - Storage: SQLite (persistent when `/data` is mounted on Hugging Face)
24
+ - Inference: Hugging Face-hosted fine-tuned Qwen model (primary); NVIDIA-hosted chat completions used as fallback (default fallback model: `qwen/qwen3.5-122b-a10b`)
25
+ - Retrieval: Web scraping pipeline for product discovery (Nike and Zalando logic in code)
26
+
27
+ Core modules:
28
+ - `app.py`: API routes, orchestration, inference calls, scraper flow
29
+ - `db.py`: SQLite schema and CRUD/caching helpers
30
+ - `scoring.py`: deterministic fallback scoring logic
31
+ - `fashion_ai/`: recommendation service and ranking support
32
+
33
+ ## Repository Contents for Deployment
34
+
35
+ Upload this backend directory as your Hugging Face Space source (or sync it via Git):
36
+
37
+ - `app.py`
38
+ - `db.py`
39
+ - `scoring.py`
40
+ - `scraper.py`
41
+ - `zalando_scraper.py`
42
+ - `requirements.txt`
43
+ - `packages.txt`
44
+ - `fashion_ai/`
45
+
46
+ ## Hugging Face Deployment
47
+
48
+ 1. Create a new Space.
49
+ 2. Select `Gradio` SDK.
50
+ 3. Use CPU hardware (inference is delegated to external APIs).
51
+ 4. Enable Persistent Storage if you want data durability across restarts.
52
+ 5. Add the required environment variables.
53
+ 6. Deploy the backend files.
54
+
55
+ ### Required Environment Variables
56
+
57
+ - `HF_API_KEY`: API key for the primary Hugging Face-hosted fine-tuned Qwen model.
58
+ - `NVIDIA_API_KEY`: API key for the NVIDIA inference fallback.
59
+
60
+ ### Common Optional Environment Variables
61
+
62
+ Inference and reliability:
63
+ - `HF_MODEL_ID` (default: your fine-tuned Qwen model on Hugging Face)
64
+ - `HF_INVOKE_URL` (default: Hugging Face Inference API endpoint for the fine-tuned model)
65
+ - `NVIDIA_MODEL_ID` (fallback; default: `qwen/qwen3.5-122b-a10b`)
66
+ - `NVIDIA_INVOKE_URL` (fallback; default: `https://integrate.api.nvidia.com/v1/chat/completions`)
67
+ - `OPENAI_MODEL_ID` (secondary fallback; OpenAI-compatible model ID if both primary and NVIDIA fallback are unavailable)
68
+ - `OPENAI_API_KEY` (secondary fallback; required only if OpenAI fallback is enabled)
69
+ - `NVIDIA_MAX_TOKENS` (default: `16384`)
70
+ - `NVIDIA_REASONING_MAX_TOKENS` (default: `16384`)
71
+ - `NVIDIA_TEMPERATURE` (default: `0.60`)
72
+ - `NVIDIA_TOP_P` (default: `0.95`)
73
+ - `NVIDIA_TIMEOUT_SECONDS` (default: `180`)
74
+ - `NVIDIA_MAX_RETRIES` (default: `3`)
75
+ - `NVIDIA_RETRY_BACKOFF_SECONDS` (default: `0.8`)
76
+ - `NVIDIA_ENABLE_THINKING` (default: `false`)
77
+ - `NVIDIA_FALLBACK_MODEL_IDS` (comma-separated fallback list)
78
+
79
+ Matching and cache:
80
+ - `MATCHING_RESULT_CACHE_MAX` (default: `500`)
81
+ - `MATCHING_RESULT_CACHE_TTL_SECONDS` (default: `86400`)
82
+
83
+ Scraper and planner:
84
+ - `SCRAPER_DEFAULT_STORE` (default: `nike`)
85
+ - `KIMI_MODEL_ID` (default: `moonshotai/kimi-k2.5`)
86
+ - `KIMI_MAX_TOKENS` (default: `800`)
87
+
88
+ Database path:
89
+ - `DB_PATH` (optional override)
90
+
91
+ When `DB_PATH` is not provided, the app uses:
92
+ - `/data/wardrobe.db` if `/data` exists,
93
+ - otherwise `./wardrobe.db`.
94
+
95
+ ## Inference Priority
96
+
97
+ The service resolves inference providers in the following order:
98
+
99
+ 1. **Primary** - Fine-tuned Qwen model hosted on Hugging Face (`HF_MODEL_ID`).
100
+ 2. **Fallback 1** - NVIDIA-hosted chat completions (`NVIDIA_MODEL_ID`, default: `qwen/qwen3.5-122b-a10b`). Used when the primary model is unavailable or returns an error.
101
+ 3. **Fallback 2** - OpenAI-compatible model (`OPENAI_MODEL_ID`). Used when both the primary and NVIDIA fallback are unavailable.
102
+
103
+ AI-powered routes return a service-level error only when all three providers are exhausted or unconfigured.
104
+
105
+ ## API Endpoints
106
+
107
+ Health and service metadata:
108
+ - `GET /`
109
+ - `GET /health`
110
+
111
+ Wardrobe ingestion and CRUD:
112
+ - `POST /classify`
113
+ - `POST /upload`
114
+ - `GET /items`
115
+ - `PUT /items/{item_id}`
116
+ - `DELETE /items/{item_id}`
117
+
118
+ Outfit intelligence:
119
+ - `POST /ai/score-outfit`
120
+ - `POST /ai/gap-analysis`
121
+ - `POST /ai/recommend-outfits`
122
+ - `POST /feedback`
123
+
124
+ Shopping and scraping:
125
+ - `POST /product-urls`
126
+ - `POST /suggestions`
127
+ - `POST /api/suggestions`
128
+ - `POST /scraper/recommend`
129
+ - `GET /scraper`
130
+ - `GET /image-proxy`
131
+
132
+ ## Local Development
133
+
134
+ ### 1. Install dependencies
135
+
136
+ ```bash
137
+ pip install -r requirements.txt
138
+ ```
139
+
140
+ ### 2. Export environment variables
141
+
142
+ Linux/macOS:
143
+
144
+ ```bash
145
+ export HF_API_KEY=""
146
+ export NVIDIA_API_KEY="" # fallback
147
+ export OPENAI_API_KEY="" # secondary fallback, optional
148
+ ```
149
+
150
+ Windows PowerShell:
151
+
152
+ ```powershell
153
+ $env:HF_API_KEY = ""
154
+ $env:NVIDIA_API_KEY = "" # fallback
155
+ $env:OPENAI_API_KEY = "" # secondary fallback, optional
156
+ ```
157
+
158
+ ### 3. Run the API
159
+
160
+ ```bash
161
+ python app.py
162
+ ```
163
+
164
+ The service starts on `http://0.0.0.0:7860`.
165
+
166
+ ## Smoke Checks
167
+
168
+ Health:
169
+
170
+ ```bash
171
+ curl "http://127.0.0.1:7860/health"
172
+ ```
173
+
174
+ Image classification:
175
+
176
+ ```bash
177
+ curl -X POST "http://127.0.0.1:7860/classify" \
178
+ -F "image=@/path/to/garment.jpg"
179
+ ```
180
+
181
+ Expected post-deploy health signal:
182
+ - `hf_api_configured` should be `"True"` (primary model).
183
  - `nvidia_api_configured` should be `"True"` (fallback model).
app.py CHANGED
@@ -3707,6 +3707,86 @@ def ai_recommend_outfits(payload: dict[str, Any] = Body(default_factory=dict)) -
3707
  bottoms=bottoms,
3708
  others=priority_other_candidates,
3709
  ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3710
  @app.get("/image-proxy")
3711
  def image_proxy(url: str = Query(..., description="Remote image URL")) -> Response:
3712
  parsed = urlparse(url)
 
3707
  bottoms=bottoms,
3708
  others=priority_other_candidates,
3709
  ))
3710
+
3711
+
3712
+ @app.post("/ai/classify-item")
3713
+ def ai_classify_item(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
3714
+ """
3715
+ Classify a fashion item using NVIDIA model (primary) with HuggingFace fallback.
3716
+
3717
+ Args:
3718
+ item: Wardrobe item dict with metadata and/or image_url
3719
+
3720
+ Returns:
3721
+ Classification result with category, confidence, and attributes
3722
+ """
3723
+ try:
3724
+ item = payload.get("item")
3725
+ if not isinstance(item, dict):
3726
+ raise HTTPException(status_code=400, detail="'item' must be a dictionary")
3727
+
3728
+ service = get_recommendation_service()
3729
+ result = service.classify_item(item)
3730
+
3731
+ return {
3732
+ "success": True,
3733
+ "classification": result,
3734
+ "model_backend": result.get("backend", "unknown"),
3735
+ }
3736
+ except HTTPException:
3737
+ raise
3738
+ except Exception as e:
3739
+ print(f"[classify-item] Error: {e}")
3740
+ _raise_http_error(e)
3741
+
3742
+
3743
+ @app.post("/ai/match-items")
3744
+ def ai_match_items(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
3745
+ """
3746
+ Determine if two fashion items match well together.
3747
+
3748
+ Uses NVIDIA model as primary with HuggingFace as fallback.
3749
+
3750
+ Args:
3751
+ item1: First wardrobe item dict
3752
+ item2: Second wardrobe item dict
3753
+ match_threshold: Confidence threshold (0-1), default 0.5
3754
+
3755
+ Returns:
3756
+ Match result with compatibility scores and reason
3757
+ """
3758
+ try:
3759
+ item1 = payload.get("item1")
3760
+ item2 = payload.get("item2")
3761
+ match_threshold = float(payload.get("match_threshold", 0.5))
3762
+
3763
+ if not isinstance(item1, dict):
3764
+ raise HTTPException(status_code=400, detail="'item1' must be a dictionary")
3765
+ if not isinstance(item2, dict):
3766
+ raise HTTPException(status_code=400, detail="'item2' must be a dictionary")
3767
+
3768
+ if match_threshold < 0 or match_threshold > 1:
3769
+ raise HTTPException(status_code=400, detail="'match_threshold' must be between 0 and 1")
3770
+
3771
+ service = get_recommendation_service()
3772
+ result = service.match_items(item1, item2, match_threshold)
3773
+
3774
+ return {
3775
+ "success": True,
3776
+ "item1_id": item1.get("id", "unknown"),
3777
+ "item2_id": item2.get("id", "unknown"),
3778
+ "match": result.get("match", False),
3779
+ "match_score": result.get("score", 0.0),
3780
+ "reason": result.get("reason", ""),
3781
+ "compatibility_breakdown": result.get("compatibility", {}),
3782
+ }
3783
+ except HTTPException:
3784
+ raise
3785
+ except Exception as e:
3786
+ print(f"[match-items] Error: {e}")
3787
+ _raise_http_error(e)
3788
+
3789
+
3790
  @app.get("/image-proxy")
3791
  def image_proxy(url: str = Query(..., description="Remote image URL")) -> Response:
3792
  parsed = urlparse(url)
fashion_ai/__init__.py CHANGED
@@ -1,9 +1,11 @@
 
1
  from .encoder import FashionItemEncoder
2
  from .ranker import OutfitCompatibilityRanker
3
  from .retriever import OutfitCandidateRetriever
4
  from .service import MultimodalOutfitRecommendationService, get_recommendation_service
5
 
6
  __all__ = [
 
7
  "FashionItemEncoder",
8
  "MultimodalOutfitRecommendationService",
9
  "OutfitCandidateRetriever",
 
1
+ from .classifier import FashionClassifier
2
  from .encoder import FashionItemEncoder
3
  from .ranker import OutfitCompatibilityRanker
4
  from .retriever import OutfitCandidateRetriever
5
  from .service import MultimodalOutfitRecommendationService, get_recommendation_service
6
 
7
  __all__ = [
8
+ "FashionClassifier",
9
  "FashionItemEncoder",
10
  "MultimodalOutfitRecommendationService",
11
  "OutfitCandidateRetriever",
fashion_ai/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (504 Bytes). View file
 
fashion_ai/__pycache__/classifier.cpython-313.pyc ADDED
Binary file (22.5 kB). View file
 
fashion_ai/__pycache__/service.cpython-313.pyc ADDED
Binary file (17.9 kB). View file
 
fashion_ai/classifier.py ADDED
@@ -0,0 +1,576 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Fashion Item Classifier with Dual Model Support
3
+
4
+ Primary: NVIDIA optimized model (high performance)
5
+ Fallback: HuggingFace HelloWorld0204/Classification-StyleWell-model
6
+
7
+ Provides classification and matching capabilities for wardrobe items.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import json
14
+ from typing import Any
15
+ from collections import OrderedDict
16
+
17
+ import numpy as np
18
+ import torch
19
+ from PIL import Image
20
+ from transformers import AutoModelForImageClassification, AutoProcessor, pipeline
21
+
22
+ DEFAULT_NVIDIA_MODEL_ID = os.getenv(
23
+ "FASHION_CLASSIFIER_NVIDIA_MODEL",
24
+ "nvidia/ViT-B-32-quickgelu" # Fast NVIDIA-optimized Vision Transformer
25
+ )
26
+ DEFAULT_HF_MODEL_ID = os.getenv(
27
+ "FASHION_CLASSIFIER_HF_MODEL",
28
+ "HelloWorld0204/Classification-StyleWell-model"
29
+ )
30
+ DEFAULT_CACHE_SIZE = int(os.getenv("FASHION_CLASSIFIER_CACHE_SIZE", "512"))
31
+
32
+
33
+ class FashionClassifier:
34
+ """
35
+ Dual-model fashion classifier with NVIDIA primary and HuggingFace fallback.
36
+
37
+ Supports:
38
+ - Item classification (category, type, pattern, color, fit, style)
39
+ - Outfit matching between items
40
+ - Confidence scoring
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ nvidia_model_id: str = DEFAULT_NVIDIA_MODEL_ID,
46
+ hf_model_id: str = DEFAULT_HF_MODEL_ID,
47
+ device: str | None = None,
48
+ cache_size: int = DEFAULT_CACHE_SIZE,
49
+ ) -> None:
50
+ self.nvidia_model_id = nvidia_model_id
51
+ self.hf_model_id = hf_model_id
52
+ self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
53
+ self.cache_size = cache_size
54
+
55
+ self._classifier = None
56
+ self._processor = None
57
+ self._model = None
58
+ self._backend = None
59
+ self._load_attempted = False
60
+
61
+ # Classification cache
62
+ self._classification_cache: OrderedDict[str, dict[str, Any]] = OrderedDict()
63
+
64
+ # Predefined fashion categories
65
+ self._fashion_categories = {
66
+ "topwear": ["shirt", "t-shirt", "blouse", "hoodie", "jacket", "blazer", "sweater", "coat"],
67
+ "bottomwear": ["jeans", "trousers", "pants", "shorts", "skirt", "joggers", "leggings"],
68
+ "footwear": ["sneaker", "boot", "loafer", "sandal", "heel", "shoe"],
69
+ "accessories": ["bag", "belt", "watch", "cap", "scarf", "sunglasses", "jewelry"],
70
+ "dress": ["dress", "gown", "jumpsuit", "romper"],
71
+ }
72
+
73
+ @property
74
+ def backend_name(self) -> str:
75
+ """Get the name of the currently loaded backend."""
76
+ self._ensure_model_loaded()
77
+ return self._backend or "none"
78
+
79
+ def _ensure_model_loaded(self) -> None:
80
+ """Load the model on first use with fallback mechanism."""
81
+ if self._load_attempted:
82
+ return
83
+
84
+ self._load_attempted = True
85
+
86
+ # Try NVIDIA model first
87
+ if self._try_load_nvidia_model():
88
+ self._backend = "nvidia"
89
+ return
90
+
91
+ # Fall back to HuggingFace
92
+ if self._try_load_hf_model():
93
+ self._backend = "huggingface"
94
+ return
95
+
96
+ self._backend = "none"
97
+ print("[FashionClassifier] Failed to load both NVIDIA and HuggingFace models. Using fallback classification.")
98
+
99
+ def _try_load_nvidia_model(self) -> bool:
100
+ """Attempt to load NVIDIA optimized model."""
101
+ try:
102
+ print(f"[FashionClassifier] Loading NVIDIA model: {self.nvidia_model_id}")
103
+
104
+ # Try to load as image classification model
105
+ try:
106
+ self._model = AutoModelForImageClassification.from_pretrained(
107
+ self.nvidia_model_id,
108
+ trust_remote_code=True,
109
+ )
110
+ self._processor = AutoProcessor.from_pretrained(
111
+ self.nvidia_model_id,
112
+ trust_remote_code=True,
113
+ )
114
+ self._model.to(self.device)
115
+ self._model.eval()
116
+ print(f"[FashionClassifier] Successfully loaded NVIDIA model")
117
+ return True
118
+ except Exception:
119
+ # If direct model load fails, try via pipeline
120
+ self._classifier = pipeline(
121
+ "image-classification",
122
+ model=self.nvidia_model_id,
123
+ device=0 if self.device == "cuda" else -1,
124
+ )
125
+ print(f"[FashionClassifier] Successfully loaded NVIDIA model via pipeline")
126
+ return True
127
+
128
+ except Exception as e:
129
+ print(f"[FashionClassifier] Failed to load NVIDIA model: {e}")
130
+ return False
131
+
132
+ def _try_load_hf_model(self) -> bool:
133
+ """Attempt to load HuggingFace fallback model."""
134
+ try:
135
+ print(f"[FashionClassifier] Loading HuggingFace model: {self.hf_model_id}")
136
+
137
+ try:
138
+ self._model = AutoModelForImageClassification.from_pretrained(
139
+ self.hf_model_id,
140
+ trust_remote_code=True,
141
+ )
142
+ self._processor = AutoProcessor.from_pretrained(
143
+ self.hf_model_id,
144
+ trust_remote_code=True,
145
+ )
146
+ self._model.to(self.device)
147
+ self._model.eval()
148
+ print(f"[FashionClassifier] Successfully loaded HuggingFace model")
149
+ return True
150
+ except Exception:
151
+ # If direct model load fails, try via pipeline
152
+ self._classifier = pipeline(
153
+ "image-classification",
154
+ model=self.hf_model_id,
155
+ device=0 if self.device == "cuda" else -1,
156
+ )
157
+ print(f"[FashionClassifier] Successfully loaded HuggingFace model via pipeline")
158
+ return True
159
+
160
+ except Exception as e:
161
+ print(f"[FashionClassifier] Failed to load HuggingFace model: {e}")
162
+ return False
163
+
164
+ def classify_image(self, image: Image.Image | str) -> dict[str, Any]:
165
+ """
166
+ Classify a fashion item from image.
167
+
168
+ Args:
169
+ image: PIL Image or URL string
170
+
171
+ Returns:
172
+ Dict with classification results:
173
+ {
174
+ "category": "topwear",
175
+ "confidence": 0.95,
176
+ "top_5": [{"label": "shirt", "score": 0.95}, ...],
177
+ "backend": "nvidia|huggingface",
178
+ "attributes": {
179
+ "color": "blue",
180
+ "pattern": "solid",
181
+ "fit": "regular",
182
+ "style": "casual"
183
+ }
184
+ }
185
+ """
186
+ self._ensure_model_loaded()
187
+
188
+ # Generate cache key
189
+ if isinstance(image, str):
190
+ cache_key = f"image:{image}"
191
+ else:
192
+ # For PIL images, use a simple hash
193
+ cache_key = f"image:{id(image)}"
194
+
195
+ cached = self._classification_cache.get(cache_key)
196
+ if cached is not None:
197
+ self._classification_cache.move_to_end(cache_key)
198
+ return cached
199
+
200
+ # Load image if needed
201
+ if isinstance(image, str):
202
+ try:
203
+ from PIL import Image as PILImage
204
+ image = PILImage.open(image)
205
+ except Exception:
206
+ return self._fallback_classification()
207
+
208
+ # Classify
209
+ if self._backend == "nvidia" or self._backend == "huggingface":
210
+ result = self._classify_with_model(image)
211
+ else:
212
+ result = self._fallback_classification()
213
+
214
+ # Cache result
215
+ self._remember_classification(cache_key, result)
216
+
217
+ return result
218
+
219
+ def _classify_with_model(self, image: Image.Image) -> dict[str, Any]:
220
+ """Classify image using loaded model."""
221
+ try:
222
+ if self._classifier is not None:
223
+ # Using pipeline
224
+ predictions = self._classifier(image)
225
+
226
+ return {
227
+ "category": predictions[0]["label"] if predictions else "unknown",
228
+ "confidence": float(predictions[0]["score"]) if predictions else 0.0,
229
+ "top_5": [
230
+ {"label": p["label"], "score": float(p["score"])}
231
+ for p in predictions[:5]
232
+ ],
233
+ "backend": self._backend,
234
+ "attributes": self._infer_attributes(predictions),
235
+ }
236
+
237
+ elif self._model is not None and self._processor is not None:
238
+ # Using direct model
239
+ with torch.inference_mode():
240
+ inputs = self._processor(images=image, return_tensors="pt")
241
+ inputs = {k: v.to(self.device) for k, v in inputs.items()}
242
+ outputs = self._model(**inputs)
243
+ logits = outputs.logits
244
+
245
+ # Get top predictions
246
+ probs = torch.softmax(logits, dim=-1)
247
+ top_k = torch.topk(probs[0], k=5)
248
+
249
+ predictions = [
250
+ {
251
+ "label": self._model.config.id2label.get(
252
+ idx.item(),
253
+ f"class_{idx.item()}"
254
+ ),
255
+ "score": score.item(),
256
+ }
257
+ for idx, score in zip(top_k.indices, top_k.values)
258
+ ]
259
+
260
+ return {
261
+ "category": predictions[0]["label"],
262
+ "confidence": float(predictions[0]["score"]),
263
+ "top_5": predictions,
264
+ "backend": self._backend,
265
+ "attributes": self._infer_attributes(predictions),
266
+ }
267
+
268
+ except Exception as e:
269
+ print(f"[FashionClassifier] Classification failed: {e}")
270
+
271
+ return self._fallback_classification()
272
+
273
+ def classify_item(self, item: dict[str, Any]) -> dict[str, Any]:
274
+ """
275
+ Classify a wardrobe item from metadata.
276
+
277
+ Args:
278
+ item: Wardrobe item dict with 'type', 'category', 'description', 'image_url'
279
+
280
+ Returns:
281
+ Classification result with category, confidence, and attributes
282
+ """
283
+ # Try image classification first
284
+ image_url = item.get("image_url")
285
+ if image_url:
286
+ try:
287
+ return self.classify_image(image_url)
288
+ except Exception as e:
289
+ print(f"[FashionClassifier] Image classification failed: {e}")
290
+
291
+ # Fall back to metadata-based classification
292
+ return self._classify_from_metadata(item)
293
+
294
+ def _classify_from_metadata(self, item: dict[str, Any]) -> dict[str, Any]:
295
+ """Classify item based on metadata when image unavailable."""
296
+ type_str = str(item.get("type", "")).lower()
297
+ category_str = str(item.get("category", "")).lower()
298
+ description = item.get("description", {})
299
+ if isinstance(description, dict):
300
+ desc_str = " ".join([
301
+ str(description.get("type", "")),
302
+ str(description.get("category", "")),
303
+ ]).lower()
304
+ else:
305
+ desc_str = str(description).lower()
306
+
307
+ full_text = f"{type_str} {category_str} {desc_str}".lower()
308
+
309
+ # Find best category match
310
+ best_category = "unknown"
311
+ best_match_count = 0
312
+
313
+ for category, keywords in self._fashion_categories.items():
314
+ match_count = sum(1 for kw in keywords if kw in full_text)
315
+ if match_count > best_match_count:
316
+ best_match_count = match_count
317
+ best_category = category
318
+
319
+ return {
320
+ "category": best_category,
321
+ "confidence": 0.7 if best_match_count > 0 else 0.3,
322
+ "top_5": [
323
+ {"label": best_category, "score": 0.7 if best_match_count > 0 else 0.3}
324
+ ],
325
+ "backend": "metadata",
326
+ "attributes": self._infer_attributes_from_metadata(item),
327
+ }
328
+
329
+ def match_items(
330
+ self,
331
+ item1: dict[str, Any] | Image.Image,
332
+ item2: dict[str, Any] | Image.Image,
333
+ match_threshold: float = 0.5,
334
+ ) -> dict[str, Any]:
335
+ """
336
+ Determine if two fashion items match well together.
337
+
338
+ Args:
339
+ item1: First wardrobe item or image
340
+ item2: Second wardrobe item or image
341
+ match_threshold: Confidence threshold for match (0-1)
342
+
343
+ Returns:
344
+ Dict with match result:
345
+ {
346
+ "match": True/False,
347
+ "score": 0.85,
348
+ "reason": "Colors complement well",
349
+ "compatibility": {
350
+ "color": 0.9,
351
+ "style": 0.8,
352
+ "pattern": 0.7,
353
+ "fit": 0.8
354
+ }
355
+ }
356
+ """
357
+ # Classify both items
358
+ if isinstance(item1, dict):
359
+ class1 = self.classify_item(item1)
360
+ else:
361
+ class1 = self.classify_image(item1)
362
+
363
+ if isinstance(item2, dict):
364
+ class2 = self.classify_item(item2)
365
+ else:
366
+ class2 = self.classify_image(item2)
367
+
368
+ # Calculate compatibility scores
369
+ compatibility = {
370
+ "category": self._category_compatibility(class1["category"], class2["category"]),
371
+ "color": self._color_compatibility(
372
+ class1["attributes"].get("color"),
373
+ class2["attributes"].get("color"),
374
+ ),
375
+ "style": self._style_compatibility(
376
+ class1["attributes"].get("style"),
377
+ class2["attributes"].get("style"),
378
+ ),
379
+ "pattern": self._pattern_compatibility(
380
+ class1["attributes"].get("pattern"),
381
+ class2["attributes"].get("pattern"),
382
+ ),
383
+ "fit": self._fit_compatibility(
384
+ class1["attributes"].get("fit"),
385
+ class2["attributes"].get("fit"),
386
+ ),
387
+ }
388
+
389
+ # Calculate overall match score
390
+ overall_score = np.mean(list(compatibility.values()))
391
+
392
+ # Determine reason
393
+ reason = self._generate_match_reason(compatibility, class1, class2)
394
+
395
+ return {
396
+ "match": overall_score >= match_threshold,
397
+ "score": float(overall_score),
398
+ "reason": reason,
399
+ "compatibility": {k: float(v) for k, v in compatibility.items()},
400
+ }
401
+
402
+ def _infer_attributes(self, predictions: list[dict]) -> dict[str, str]:
403
+ """Infer fashion attributes from predictions."""
404
+ label_str = " ".join([p.get("label", "") for p in predictions[:3]]).lower()
405
+
406
+ return {
407
+ "color": self._extract_attribute(label_str, ["black", "white", "blue", "red", "green", "yellow", "pink", "gray", "brown"], "neutral"),
408
+ "pattern": self._extract_attribute(label_str, ["solid", "striped", "plaid", "floral", "geometric", "checkered"], "solid"),
409
+ "fit": self._extract_attribute(label_str, ["slim", "regular", "loose", "oversized", "fitted"], "regular"),
410
+ "style": self._extract_attribute(label_str, ["casual", "formal", "sporty", "vintage", "bohemian"], "casual"),
411
+ }
412
+
413
+ def _infer_attributes_from_metadata(self, item: dict[str, Any]) -> dict[str, str]:
414
+ """Infer attributes from item metadata."""
415
+ metadata = json.dumps(item).lower()
416
+
417
+ return {
418
+ "color": self._extract_attribute(metadata, ["black", "white", "blue", "red", "green", "yellow", "pink", "gray", "brown"], "neutral"),
419
+ "pattern": self._extract_attribute(metadata, ["solid", "striped", "plaid", "floral", "geometric", "checkered"], "solid"),
420
+ "fit": self._extract_attribute(metadata, ["slim", "regular", "loose", "oversized", "fitted"], "regular"),
421
+ "style": self._extract_attribute(metadata, ["casual", "formal", "sporty", "vintage", "bohemian"], "casual"),
422
+ }
423
+
424
+ def _extract_attribute(self, text: str, options: list[str], default: str) -> str:
425
+ """Extract attribute from text by matching keywords."""
426
+ for option in options:
427
+ if option in text:
428
+ return option
429
+ return default
430
+
431
+ def _category_compatibility(self, cat1: str, cat2: str) -> float:
432
+ """Score category compatibility (0-1)."""
433
+ # Complementary categories
434
+ complementary = {
435
+ "topwear": ["bottomwear", "dress"],
436
+ "bottomwear": ["topwear"],
437
+ "footwear": ["topwear", "bottomwear", "dress"],
438
+ "accessories": ["topwear", "bottomwear", "footwear", "dress"],
439
+ "dress": ["footwear", "accessories"],
440
+ }
441
+
442
+ if cat1 == cat2:
443
+ return 0.5 # Same category can work but usually not as primary match
444
+
445
+ if cat1 in complementary and cat2 in complementary[cat1]:
446
+ return 1.0
447
+
448
+ return 0.6
449
+
450
+ def _color_compatibility(self, color1: str | None, color2: str | None) -> float:
451
+ """Score color compatibility (0-1)."""
452
+ if not color1 or not color2:
453
+ return 0.7 # Unknown colors get neutral score
454
+
455
+ # Complementary color pairs
456
+ complementary_pairs = {
457
+ ("blue", "orange"),
458
+ ("red", "green"),
459
+ ("yellow", "purple"),
460
+ }
461
+
462
+ if {color1, color2} in complementary_pairs:
463
+ return 1.0
464
+
465
+ # Neutral colors work with everything
466
+ neutral = {"black", "white", "gray", "beige", "brown"}
467
+ if color1 in neutral or color2 in neutral:
468
+ return 0.85
469
+
470
+ # Same color
471
+ if color1 == color2:
472
+ return 0.75
473
+
474
+ return 0.65
475
+
476
+ def _style_compatibility(self, style1: str | None, style2: str | None) -> float:
477
+ """Score style compatibility (0-1)."""
478
+ if not style1 or not style2:
479
+ return 0.7
480
+
481
+ if style1 == style2:
482
+ return 0.9
483
+
484
+ # Some styles mix well
485
+ mixable = {
486
+ ("casual", "sporty"),
487
+ ("formal", "vintage"),
488
+ }
489
+
490
+ if {style1, style2} in mixable:
491
+ return 0.8
492
+
493
+ return 0.6
494
+
495
+ def _pattern_compatibility(self, pattern1: str | None, pattern2: str | None) -> float:
496
+ """Score pattern compatibility (0-1)."""
497
+ if not pattern1 or not pattern2:
498
+ return 0.7
499
+
500
+ # Solid goes well with anything
501
+ if pattern1 == "solid" or pattern2 == "solid":
502
+ return 0.85
503
+
504
+ # Same pattern can work
505
+ if pattern1 == pattern2:
506
+ return 0.75
507
+
508
+ # Different patterns are riskier
509
+ return 0.6
510
+
511
+ def _fit_compatibility(self, fit1: str | None, fit2: str | None) -> float:
512
+ """Score fit compatibility (0-1)."""
513
+ if not fit1 or not fit2:
514
+ return 0.7
515
+
516
+ if fit1 == fit2:
517
+ return 0.85
518
+
519
+ # Loose top with fitted bottom is good
520
+ if {fit1, fit2} == {"loose", "fitted"}:
521
+ return 0.9
522
+
523
+ # Different fits can still work
524
+ return 0.7
525
+
526
+ def _generate_match_reason(
527
+ self,
528
+ compatibility: dict[str, float],
529
+ class1: dict[str, Any],
530
+ class2: dict[str, Any],
531
+ ) -> str:
532
+ """Generate human-readable match reason."""
533
+ reasons = []
534
+
535
+ if compatibility["color"] >= 0.85:
536
+ reasons.append("Colors complement each other well")
537
+
538
+ if compatibility["style"] >= 0.85:
539
+ reasons.append("Styles match perfectly")
540
+
541
+ if compatibility["pattern"] >= 0.85:
542
+ reasons.append("Patterns work well together")
543
+
544
+ if compatibility["fit"] >= 0.85:
545
+ reasons.append("Fit proportions are balanced")
546
+
547
+ if not reasons:
548
+ if compatibility["category"] >= 0.85:
549
+ reasons.append("Items are from complementary categories")
550
+ else:
551
+ reasons.append("Items are compatible")
552
+
553
+ return ". ".join(reasons)
554
+
555
+ def _fallback_classification(self) -> dict[str, Any]:
556
+ """Return fallback classification when models fail."""
557
+ return {
558
+ "category": "unknown",
559
+ "confidence": 0.0,
560
+ "top_5": [],
561
+ "backend": "fallback",
562
+ "attributes": {
563
+ "color": "neutral",
564
+ "pattern": "solid",
565
+ "fit": "regular",
566
+ "style": "casual",
567
+ },
568
+ }
569
+
570
+ def _remember_classification(self, cache_key: str, result: dict[str, Any]) -> None:
571
+ """Store classification in cache with size limit."""
572
+ self._classification_cache[cache_key] = result
573
+ self._classification_cache.move_to_end(cache_key)
574
+
575
+ while len(self._classification_cache) > self.cache_size:
576
+ self._classification_cache.popitem(last=False)
fashion_ai/service.py CHANGED
@@ -5,6 +5,7 @@ from typing import Any
5
 
6
  import numpy as np
7
 
 
8
  from .encoder import FashionItemEncoder
9
  from .ranker import NeuralOutfitScorer
10
  from .retriever import OutfitCandidateRetriever
@@ -32,6 +33,7 @@ class MultimodalOutfitRecommendationService:
32
  encoder: FashionItemEncoder | None = None,
33
  retriever: OutfitCandidateRetriever | None = None,
34
  scorer: NeuralOutfitScorer | None = None,
 
35
  top_k: int = DEFAULT_TOP_K,
36
  candidate_pool: int = DEFAULT_CANDIDATE_POOL,
37
  max_beam: int = DEFAULT_MAX_BEAM,
@@ -43,6 +45,7 @@ class MultimodalOutfitRecommendationService:
43
  slot_pool_size=candidate_pool,
44
  )
45
  self.scorer = scorer or NeuralOutfitScorer(d_model=self.encoder.embedding_dim)
 
46
  self.top_k = top_k
47
  self.candidate_pool = candidate_pool
48
  self.max_beam = max_beam
@@ -325,6 +328,41 @@ class MultimodalOutfitRecommendationService:
325
  return 0.0
326
  return float(np.dot(left_vec / left_norm, right_vec / right_norm))
327
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
  def get_recommendation_service() -> MultimodalOutfitRecommendationService:
330
  global _SERVICE_SINGLETON
 
5
 
6
  import numpy as np
7
 
8
+ from .classifier import FashionClassifier
9
  from .encoder import FashionItemEncoder
10
  from .ranker import NeuralOutfitScorer
11
  from .retriever import OutfitCandidateRetriever
 
33
  encoder: FashionItemEncoder | None = None,
34
  retriever: OutfitCandidateRetriever | None = None,
35
  scorer: NeuralOutfitScorer | None = None,
36
+ classifier: FashionClassifier | None = None,
37
  top_k: int = DEFAULT_TOP_K,
38
  candidate_pool: int = DEFAULT_CANDIDATE_POOL,
39
  max_beam: int = DEFAULT_MAX_BEAM,
 
45
  slot_pool_size=candidate_pool,
46
  )
47
  self.scorer = scorer or NeuralOutfitScorer(d_model=self.encoder.embedding_dim)
48
+ self.classifier = classifier or FashionClassifier()
49
  self.top_k = top_k
50
  self.candidate_pool = candidate_pool
51
  self.max_beam = max_beam
 
328
  return 0.0
329
  return float(np.dot(left_vec / left_norm, right_vec / right_norm))
330
 
331
+ def classify_item(self, item: dict[str, Any]) -> dict[str, Any]:
332
+ """
333
+ Classify a fashion item using the integrated classifier.
334
+
335
+ Uses NVIDIA model as primary, HuggingFace as fallback.
336
+
337
+ Args:
338
+ item: Wardrobe item dict with metadata and/or image_url
339
+
340
+ Returns:
341
+ Classification result with category, confidence, attributes
342
+ """
343
+ return self.classifier.classify_item(item)
344
+
345
+ def match_items(
346
+ self,
347
+ item1: dict[str, Any],
348
+ item2: dict[str, Any],
349
+ match_threshold: float = 0.5,
350
+ ) -> dict[str, Any]:
351
+ """
352
+ Determine if two fashion items match well together.
353
+
354
+ Uses NVIDIA model as primary, HuggingFace as fallback.
355
+
356
+ Args:
357
+ item1: First wardrobe item
358
+ item2: Second wardrobe item
359
+ match_threshold: Confidence threshold for match (0-1)
360
+
361
+ Returns:
362
+ Dict with match result, score, reason, and compatibility breakdown
363
+ """
364
+ return self.classifier.match_items(item1, item2, match_threshold)
365
+
366
 
367
  def get_recommendation_service() -> MultimodalOutfitRecommendationService:
368
  global _SERVICE_SINGLETON
requirements.txt CHANGED
@@ -14,3 +14,6 @@ accelerate
14
  gradio
15
  open_clip_torch
16
  apify-client
 
 
 
 
14
  gradio
15
  open_clip_torch
16
  apify-client
17
+ timm
18
+ onnx
19
+ onnxruntime-gpu