Ox1 commited on
Commit
d81d91f
·
1 Parent(s): 63a6b7d

fix (llm): improve prompts for choosing outfits

Browse files
Files changed (3) hide show
  1. README.md +135 -49
  2. data/outfits.json +0 -72
  3. src/combinations.py +31 -49
README.md CHANGED
@@ -13,62 +13,98 @@ hardware: cpu-basic
13
  short_description: AI wardrobe. catalog, combine and ask about your clothes
14
  ---
15
 
16
- # 👕 Wardrobe Us
17
 
18
- **An AI-powered wardrobe assistant that helps you understand, organize, and make better use of the clothes you already own.**
19
 
20
  Built for the [Gradio × Hugging Face Build Small Hackathon](https://huggingface.co/build-small-hackathon) (June 2026).
21
 
 
 
22
  ---
23
 
24
  ## What it does
25
 
26
- 1. **Capture** Upload photos of your clothes. AI detects individual garments, crops them, and extracts structured attributes (type, color, material, pattern, season, formality).
27
- 2. **Catalog** — Browse your digital wardrobe with all extracted metadata. Search and filter by any attribute.
28
- 3. **Combine** Generate outfit combinations ranked by style rules. Optionally describe an occasion ("dinner on a terrace, summer") and the LLM re-ranks combinations for that context.
29
- 4. **Ask** Chat with your wardrobe. "What should I wear for a job interview?" gets answered based on what you actually own.
 
 
 
 
30
 
31
  ---
32
 
33
- ## Tech Stack
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- | Component | Model / Library |
36
- |-----------|----------------|
37
- | Vision + Chat LLM | **Gemma 3 4B** (Q4_K_M GGUF) via `llama-cpp-python` |
38
- | Garment Detection | YOLOS-tiny (`transformers`) / YOLOv8n / GroundingDINO |
39
- | Runtime | llama.cpp CPU on Space, GPU locally |
40
- | UI | Gradio 6.17 (`gr.Server` + Alpine.js default) |
 
 
41
  | Storage | Local filesystem or S3 (configurable) |
 
 
 
 
42
 
43
- Total parameters: **4 billion** Q4_K_M fits in 16 GB RAM on CPU Basic.
44
 
45
  ---
46
 
47
- ## Bonus Quests
48
 
49
  | Badge | Status |
50
  |-------|--------|
51
- | 🔌 Off the Grid | All inference runs on the Space hardware. No external APIs. |
52
- | 🦙 Llama Champion | Model runs through llama.cpp runtime (`llama-cpp-python`). |
53
- | 🐜 Tiny Titan | Gemma 3 4B — well under the 4B threshold. |
54
- | 🎨 Off-Brand | Custom frontend via `gr.Server` + Alpine.js in `app.py`. |
55
  | 📡 Sharing is Caring | Agent trace shared on the Hub. |
56
- | 📓 Field Notes | Build report documenting the process (`FIELD_NOTES.md`). |
57
 
58
  ---
59
 
60
- ## How to Use
 
 
61
 
62
- ### On HuggingFace Spaces
63
 
64
- The app runs on **CPU Basic** (2 vCPU, 16 GB RAM). All inference is CPU-only. On first use:
65
- 1. Click **"Obtener Dataset"** to load a sample wardrobe from a HuggingFace dataset (takes ~1545 minutes on CPU).
66
- 2. Or upload your own clothes photos in the **Captura** tab (~3090 s per garment).
67
- 3. Explore combinations in **Combina** and ask questions in **Pregunta** (~5–15 s per response).
68
 
69
- Set `HF_TOKEN` in Space Secrets before first use (required for model and dataset downloads).
70
 
71
- ### Local Development (GPU accelerated)
 
 
 
 
 
72
 
73
  ```bash
74
  cd packages/wardrobe-us
@@ -82,63 +118,113 @@ pip install llama-cpp-python==0.3.28 \
82
  --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124 \
83
  --force-reinstall --no-deps
84
 
85
- # Custom minimal frontend (default — gr.Server + Alpine.js):
86
  python app.py
87
 
88
- # Standard Gradio UI (full feature set):
89
  python app.py --default
90
  ```
91
 
92
- Requires a CUDA GPU with at least 8GB VRAM for GPU mode. Without CUDA, the app falls back to CPU inference automatically. Set `HF_TOKEN` in `.env` for model downloads.
 
 
93
 
94
- The default mode (`--ui`) uses `gradio.Server` with a vanilla HTML/CSS/JS + Alpine.js interface designed for non-technical users. Pass `--default` for the full Gradio Blocks UI with all advanced features (manual bounding box annotation, detection backend settings, etc.).
 
 
95
 
96
  ---
97
 
98
  ## Architecture
99
 
100
  ```
101
- app.py # Unified entry point (--ui or --default)
102
  src/
103
  ui/
104
- index.html # Custom frontend (Alpine.js + @gradio/client)
105
- style.css # Minimal CSS
106
- model_loader.py # GGUF singleton (Gemma 3 4B)
107
- vision.py # VLM attribute extraction pipeline
108
- detector/ # Pluggable garment detection (YOLOS/YOLOv8/GroundingDINO)
109
- catalog.py # JSON catalog CRUD
110
- combinations.py # Outfit generation + LLM ranking
111
- assistant.py # Chat with wardrobe context
112
- storage.py # Local/S3 image storage
113
- settings.py # Runtime configuration
 
 
 
 
 
 
 
114
  ```
115
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  ---
117
 
118
- ## Environment Variables (Secrets)
119
 
120
  | Variable | Required | Description |
121
  |----------|----------|-------------|
122
- | `HF_TOKEN` | Yes | HuggingFace token for model/dataset downloads |
123
  | `STORAGE_BACKEND` | No | `local` (default) or `s3` |
124
  | `S3_BUCKET_NAME` | If S3 | Bucket name |
125
  | `S3_ENDPOINT_URL` | If S3 | S3 endpoint |
126
  | `AWS_ACCESS_KEY_ID` | If S3 | AWS credentials |
127
  | `AWS_SECRET_ACCESS_KEY` | If S3 | AWS credentials |
128
  | `DETECTION_BACKEND` | No | `yolos` (default), `yolov8`, or `grounding_dino` |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  ---
131
 
132
- ## Agent Trace (Sharing is Caring)
133
 
134
- The full agent trace (conversation history used to build this project) is published as a dataset on the Hub:
135
 
136
  ```bash
137
- # To publish your agent trace:
138
  huggingface-cli upload-large-folder build-small-hackathon/wardrobe-us-agent-trace ./agent-trace --repo-type dataset
139
  ```
140
 
141
- The trace is a JSONL file containing the full development conversation — architecture decisions, debugging sessions, feature implementation, and deployment configuration.
142
 
143
  ---
144
 
 
13
  short_description: AI wardrobe. catalog, combine and ask about your clothes
14
  ---
15
 
16
+ # 👕 Wardrobe AI
17
 
18
+ **Turn a physical wardrobe into a searchable, AI-powered catalog and get outfit ideas from clothes you already own.**
19
 
20
  Built for the [Gradio × Hugging Face Build Small Hackathon](https://huggingface.co/build-small-hackathon) (June 2026).
21
 
22
+ The original motivation: help someone with 200+ garments who forgets what they own, buys duplicates, and struggles to combine outfits every morning. Wardrobe AI is not a shopping app — it helps you *use* what you already have.
23
+
24
  ---
25
 
26
  ## What it does
27
 
28
+ | Step | Description |
29
+ |------|-------------|
30
+ | **Capture** | Upload photos of your clothes. A detector finds garments, you can adjust bounding boxes, and a VLM extracts structured attributes (type, color, material, pattern, season, formality, description). |
31
+ | **Catalog** | Browse your digital wardrobe with images and metadata. Click any garment for a detail panel with full attributes. |
32
+ | **Combine** | Generate top+bottom outfit combinations filtered by season and formality rules. Describe an occasion and the LLM re-ranks the best matches. Like/dislike outfits to build style preferences. |
33
+ | **Ask** | Chat with your wardrobe in natural language. Answers reference your actual garments with images and descriptions. |
34
+
35
+ All inference runs locally — no external APIs.
36
 
37
  ---
38
 
39
+ ## Two frontends
40
+
41
+ The app ships with two UIs sharing the same backend:
42
+
43
+ | | **Custom UI** (default) | **Gradio Blocks** (`--default`) |
44
+ |---|---|---|
45
+ | Launch | `python app.py` | `python app.py --default` |
46
+ | Stack | `gradio.Server` + Alpine.js + `@gradio/client` | Gradio 6.17 Blocks |
47
+ | Language | English | Spanish |
48
+ | Best for | End users — clean, minimal UX | Power users — full settings |
49
+ | Manual crop editor | Annotorious v3 bounding-box editor | `gradio-image-annotation` |
50
+ | Detection backend switch | — | Dropdown in settings |
51
+ | Dataset load logs | Real-time log dock (streaming) | Markdown + gallery preview |
52
+ | Ask tab | Garment chips with images in replies | Streaming chatbot |
53
+
54
+ Both modes support sample dataset loading, outfit generation, and wardrobe chat.
55
+
56
+ ---
57
 
58
+ ## Tech stack
59
+
60
+ | Component | Choice |
61
+ |-----------|--------|
62
+ | VLM + Chat LLM | **Gemma 3 4B IT** (Q4_K_M GGUF) via `llama-cpp-python` |
63
+ | Garment detection | **YOLOS-tiny** (default), YOLOv8n, or GroundingDINO — pluggable registry |
64
+ | Runtime | llama.cpp — CPU on HF Spaces, CUDA locally |
65
+ | UI | `gradio.Server` + Alpine.js (default) or Gradio Blocks |
66
  | Storage | Local filesystem or S3 (configurable) |
67
+ | Catalog | `data/catalog.json` + `data/garments/*.jpg` |
68
+ | Preferences | `data/outfits.json` (liked combinations) |
69
+
70
+ **Total parameters: 4 billion** — fits Tiny Titan (≤4B) and runs on CPU Basic (16 GB RAM) with Q4_K_M quantization (~3 GB model).
71
 
72
+ The same Gemma 3 4B model handles vision extraction, outfit ranking, and chat. A singleton `_ModelManager` hot-swaps between vision (MTMD) and text-only modes.
73
 
74
  ---
75
 
76
+ ## Bonus quests
77
 
78
  | Badge | Status |
79
  |-------|--------|
80
+ | 🔌 Off the Grid | All inference on Space hardware. No external APIs. |
81
+ | 🦙 Llama Champion | Model runs through llama.cpp (`llama-cpp-python`). |
82
+ | 🐜 Tiny Titan | Gemma 3 4B — under the 4B threshold. |
83
+ | 🎨 Off-Brand | Custom frontend via `gr.Server` + Alpine.js. |
84
  | 📡 Sharing is Caring | Agent trace shared on the Hub. |
85
+ | 📓 Field Notes | Build report in `FIELD_NOTES.md`. |
86
 
87
  ---
88
 
89
+ ## How to use
90
+
91
+ ### On Hugging Face Spaces
92
 
93
+ Runs on **CPU Basic** (2 vCPU, 16 GB RAM). Set `HF_TOKEN` in Space Secrets before first use.
94
 
95
+ 1. **Load a sample wardrobe** *Add Clothes* → *Load Dataset* (50 garments from a public HF dataset; ~15–45 min on CPU with live progress logs).
96
+ 2. **Or upload your own** drag a flat-lay photo, review auto-detected boxes, click *Analyse* (~3090 s per garment on CPU).
97
+ 3. **Get Dressed** type an occasion, hit *Generate* (~515 s for LLM ranking).
98
+ 4. **Ask** chat about outfits, care, or what you own (~5–15 s per response).
99
 
100
+ **Sample datasets:**
101
 
102
+ | Key | Dataset | Notes |
103
+ |-----|---------|-------|
104
+ | `second-hand` | `fnauman/fashion-second-hand-front-only-rgb` | Individual garments, no detection step |
105
+ | `fashion-1k` | `Codatta/Fashion-1K` | Multi-garment photos, slower (needs detection) |
106
+
107
+ ### Local development (GPU accelerated)
108
 
109
  ```bash
110
  cd packages/wardrobe-us
 
118
  --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124 \
119
  --force-reinstall --no-deps
120
 
121
+ # Custom minimal frontend (default):
122
  python app.py
123
 
124
+ # Full Gradio Blocks UI:
125
  python app.py --default
126
  ```
127
 
128
+ Requires a CUDA GPU with ≥8 GB VRAM for GPU mode. Without CUDA, inference falls back to CPU automatically. Copy `.env.example` to `.env` and set `HF_TOKEN`.
129
+
130
+ **Pre-build a sample catalog offline** (optional):
131
 
132
+ ```bash
133
+ python scripts/build_sample_wardrobe.py --dataset second-hand --target 50
134
+ ```
135
 
136
  ---
137
 
138
  ## Architecture
139
 
140
  ```
141
+ app.py # Entry point (--ui default | --default)
142
  src/
143
  ui/
144
+ index.html # Custom frontend (Alpine.js + Annotorious)
145
+ style.css
146
+ model_loader.py # GGUF singleton (Gemma 3 4B, n_ctx=4096)
147
+ vision.py # VLM attribute extraction pipeline
148
+ detector/ # Pluggable garment detection
149
+ _registry.py # @register("yolos") pattern
150
+ backends/ # yolos | yolov8 | grounding_dino
151
+ catalog.py # JSON catalog CRUD
152
+ combinations.py # Outfit generation + LLM ranking
153
+ assistant.py # Chat with wardrobe context
154
+ storage.py # Local / S3 image storage
155
+ settings.py # Runtime config (data/settings.json)
156
+ data/
157
+ catalog.json # Garment metadata
158
+ garments/ # Cropped garment images
159
+ outfits.json # Liked outfit preferences
160
+ _uploads/ # Temp images during crop workflow
161
  ```
162
 
163
+ ### API endpoints (custom UI)
164
+
165
+ Exposed via `gradio.Server` and consumed by `@gradio/client`:
166
+
167
+ | Endpoint | Purpose |
168
+ |----------|---------|
169
+ | `prepare_image` | Save upload, auto-detect boxes → token + image URL for editor |
170
+ | `analyze_boxes` | Crop user-confirmed boxes, VLM extract, add to catalog |
171
+ | `add_photo` | One-shot upload + auto-detect + extract (no manual crop) |
172
+ | `get_wardrobe` | Full catalog with cache-busted image URLs |
173
+ | `get_combinations` | Generate + LLM-rank outfits (top 20 returned) |
174
+ | `rate_outfit` | Save like/dislike preference |
175
+ | `ask_question` | Natural-language wardrobe chat |
176
+ | `load_dataset` | Stream dataset processing progress (generator) |
177
+
178
+ Static mounts: `/garments` (catalog images), `/uploads` (temp crop images).
179
+
180
+ ### Outfit ranking
181
+
182
+ 1. Rule-based generation: all compatible top+bottom pairs (season + formality filters).
183
+ 2. LLM ranking: up to 20 diverse combinations sent to Gemma 3 4B with a compact prompt (fits `n_ctx=4096`). Remaining combos appended in original order.
184
+ 3. User likes feed back into future ranking prompts as style signals.
185
+
186
  ---
187
 
188
+ ## Environment variables
189
 
190
  | Variable | Required | Description |
191
  |----------|----------|-------------|
192
+ | `HF_TOKEN` | Yes | Hugging Face token for model/dataset downloads |
193
  | `STORAGE_BACKEND` | No | `local` (default) or `s3` |
194
  | `S3_BUCKET_NAME` | If S3 | Bucket name |
195
  | `S3_ENDPOINT_URL` | If S3 | S3 endpoint |
196
  | `AWS_ACCESS_KEY_ID` | If S3 | AWS credentials |
197
  | `AWS_SECRET_ACCESS_KEY` | If S3 | AWS credentials |
198
  | `DETECTION_BACKEND` | No | `yolos` (default), `yolov8`, or `grounding_dino` |
199
+ | `CUDA_VISIBLE_DEVICES` | No | GPU index (local only; forced to CPU on Spaces) |
200
+
201
+ ---
202
+
203
+ ## Performance notes
204
+
205
+ | Task | CPU Basic (Space) | Local GPU |
206
+ |------|-------------------|-----------|
207
+ | First model download | ~2–3 min | ~2–3 min |
208
+ | Garment extraction | ~30–90 s each | ~3–10 s each |
209
+ | Dataset load (50 items) | ~15–45 min | ~5–15 min |
210
+ | Outfit ranking | ~5–15 s | ~2–5 s |
211
+ | Ask response | ~5–15 s | ~2–5 s |
212
+
213
+ **Detection tips:** YOLOS-tiny works best on flat-lay photos. Hanger or worn-garment photos are harder — use the manual bounding-box editor as fallback.
214
+
215
+ **VLM accuracy:** At 4B parameters, color and type labels are usually good but not perfect (e.g. navy vs black). Descriptions and structured JSON parsing with regex fallback help reliability.
216
 
217
  ---
218
 
219
+ ## Agent trace (Sharing is Caring)
220
 
221
+ The full development conversation is published as a dataset on the Hub:
222
 
223
  ```bash
 
224
  huggingface-cli upload-large-folder build-small-hackathon/wardrobe-us-agent-trace ./agent-trace --repo-type dataset
225
  ```
226
 
227
+ See also `FIELD_NOTES.md` for architecture decisions, what worked, and lessons learned.
228
 
229
  ---
230
 
data/outfits.json DELETED
@@ -1,72 +0,0 @@
1
- [
2
- {
3
- "id": "outfit_001",
4
- "top": "garment_001",
5
- "bottom": "garment_005",
6
- "liked": false,
7
- "timestamp": "2026-06-13T13:21:28.920888+00:00"
8
- },
9
- {
10
- "id": "outfit_002",
11
- "top": "garment_001",
12
- "bottom": "garment_010",
13
- "liked": false,
14
- "timestamp": "2026-06-13T13:21:29.395598+00:00"
15
- },
16
- {
17
- "id": "outfit_003",
18
- "top": "garment_001",
19
- "bottom": "garment_015",
20
- "liked": false,
21
- "timestamp": "2026-06-13T13:21:29.550680+00:00"
22
- },
23
- {
24
- "id": "outfit_004",
25
- "top": "garment_001",
26
- "bottom": "garment_020",
27
- "liked": false,
28
- "timestamp": "2026-06-13T13:21:29.733320+00:00"
29
- },
30
- {
31
- "id": "outfit_005",
32
- "top": "garment_001",
33
- "bottom": "garment_025",
34
- "liked": false,
35
- "timestamp": "2026-06-13T13:21:29.901984+00:00"
36
- },
37
- {
38
- "id": "outfit_006",
39
- "top": "garment_001",
40
- "bottom": "garment_030",
41
- "liked": false,
42
- "timestamp": "2026-06-13T13:21:30.053074+00:00"
43
- },
44
- {
45
- "id": "outfit_007",
46
- "top": "garment_004",
47
- "bottom": "garment_005",
48
- "liked": false,
49
- "timestamp": "2026-06-13T13:21:30.481244+00:00"
50
- },
51
- {
52
- "id": "outfit_008",
53
- "top": "garment_036",
54
- "bottom": "garment_018",
55
- "liked": false,
56
- "timestamp": "2026-06-13T15:29:44.676129+00:00"
57
- },
58
- {
59
- "id": "outfit_009",
60
- "top": "garment_036",
61
- "bottom": "garment_022",
62
- "liked": false,
63
- "timestamp": "2026-06-13T15:29:47.770911+00:00"
64
- },
65
- {
66
- "id": "outfit_010",
67
- "top": "garment_036",
68
- "bottom": "garment_026",
69
- "liked": false,
70
- "timestamp": "2026-06-13T15:29:49.066766+00:00"
71
- }
72
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/combinations.py CHANGED
@@ -35,36 +35,26 @@ FORMALITY_COMPAT = {
35
  "formal": {"smart-casual", "formal"},
36
  }
37
 
38
- RANKING_SYSTEM_PROMPT = """You are an experienced personal stylist helping someone dress from their own wardrobe.
39
-
40
- Your job is to rank outfit combinations (top + bottom) from best to worst. Apply these criteria in order of importance:
41
-
42
- 1. Occasion fit — match the stated occasion, weather, and activity level.
43
- 2. Color harmony — prefer complementary or neutral pairings; penalize clashing brights or competing patterns unless intentional.
44
- 3. Formality coherence top and bottom should feel like they belong together (no suit pants with a gym tank).
45
- 4. Season appropriateness — fabrics and weights should suit the weather implied by the occasion.
46
- 5. Pattern balance — if one piece is bold (stripes, plaid, floral), pair with a solid or subtle counterpart.
47
- 6. Versatility — favour polished, wearable looks over gimmicky or repetitive pairings.
48
-
49
- Return ONLY a valid JSON array of outfit IDs, best first. No explanation, no markdown fences."""
50
-
51
-
52
- def _format_garment_line(garment: dict, role: str) -> str:
53
- """Format a garment with structured attributes for the ranking prompt."""
54
- desc = garment.get("description", "")
55
- parts = [
56
- f"{role}={garment.get('color', '?')} {garment.get('type', '?')}",
57
- f"material={garment.get('material', '?')}",
58
- f"pattern={garment.get('pattern', '?')}",
59
- f"season={garment.get('season', '?')}",
60
- f"formality={garment.get('formality', '?')}",
61
- ]
62
- if desc:
63
- parts.append(f"note={desc[:100]}")
64
- return ", ".join(parts)
65
 
66
 
67
- def _select_combos_for_ranking(combinations: list[dict], max_items: int = 40) -> list[dict]:
68
  """Pick a diverse subset when the list is too large for the LLM context."""
69
  if len(combinations) <= max_items:
70
  return combinations
@@ -94,13 +84,13 @@ def _format_liked_hint() -> str:
94
  if not liked:
95
  return ""
96
 
97
- lines = ["\nThe user previously liked these combinations (use as style signal):"]
98
- for outfit in liked[:5]:
99
  top = outfit["top"]
100
  bottom = outfit["bottom"]
101
- top_label = top.get("description") or f"{top.get('color')} {top.get('type')}"
102
- bottom_label = bottom.get("description") or f"{bottom.get('color')} {bottom.get('type')}"
103
- lines.append(f"- {top_label} + {bottom_label}")
104
  return "\n".join(lines)
105
 
106
 
@@ -207,7 +197,7 @@ def generate_combinations(
207
  def rank_combinations_prompt(
208
  combinations: list[dict],
209
  context: str = "",
210
- max_items: int = 40,
211
  ) -> tuple[str, str]:
212
  """Build system + user prompts for the LLM to rank outfit combinations.
213
 
@@ -217,32 +207,24 @@ def rank_combinations_prompt(
217
  return "", ""
218
 
219
  subset = _select_combos_for_ranking(combinations, max_items=max_items)
220
- occasion = context.strip() if context and context.strip() else (
221
- "everyday wear — versatile, practical outfits suitable for most casual situations"
222
- )
223
 
224
  user_lines = [
225
  f"Occasion: {occasion}",
226
- "",
227
- f"Rank these {len(subset)} outfit combinations from BEST to WORST for this occasion.",
228
- "Each line is one outfit. Use the outfit ID exactly as shown.",
229
  "",
230
  ]
231
 
232
  for combo in subset:
233
- top_line = _format_garment_line(combo["top"], "top")
234
- bottom_line = _format_garment_line(combo["bottom"], "bottom")
235
- user_lines.append(f"- {combo['id']}: {top_line} | {bottom_line}")
236
 
237
  liked_hint = _format_liked_hint()
238
  if liked_hint:
239
  user_lines.append(liked_hint)
240
 
241
- user_lines.append("")
242
- user_lines.append(
243
- f"Return a JSON array of all {len(subset)} outfit IDs reordered best-to-worst, "
244
- "e.g. [\"outfit_003\", \"outfit_001\", ...]"
245
- )
246
 
247
  return RANKING_SYSTEM_PROMPT, "\n".join(user_lines)
248
 
@@ -273,7 +255,7 @@ def rank_with_llm(combinations: list[dict], context: str = "") -> list[dict]:
273
  {"role": "system", "content": system_prompt},
274
  {"role": "user", "content": user_prompt},
275
  ],
276
- max_tokens=1024,
277
  temperature=0.2,
278
  )
279
 
 
35
  "formal": {"smart-casual", "formal"},
36
  }
37
 
38
+ RANKING_SYSTEM_PROMPT = (
39
+ "You are a personal stylist. Rank outfit combinations (top + bottom) best-to-worst "
40
+ "for the given occasion. Prioritise: occasion fit, color harmony, formality match, "
41
+ "season, pattern balance. Return ONLY a JSON array of outfit IDs, best first."
42
+ )
43
+
44
+ # Max combos sent to the LLM must fit in n_ctx=4096 alongside the response.
45
+ MAX_RANKING_ITEMS = 20
46
+
47
+
48
+ def _format_garment_line(garment: dict) -> str:
49
+ """Compact one-line garment summary for the ranking prompt."""
50
+ return (
51
+ f"{garment.get('color', '?')} {garment.get('type', '?')}"
52
+ f" ({garment.get('pattern', 'solid')}, {garment.get('season', 'all')},"
53
+ f" {garment.get('formality', 'casual')})"
54
+ )
 
 
 
 
 
 
 
 
 
 
55
 
56
 
57
+ def _select_combos_for_ranking(combinations: list[dict], max_items: int = MAX_RANKING_ITEMS) -> list[dict]:
58
  """Pick a diverse subset when the list is too large for the LLM context."""
59
  if len(combinations) <= max_items:
60
  return combinations
 
84
  if not liked:
85
  return ""
86
 
87
+ lines = ["\nUser liked (style signal):"]
88
+ for outfit in liked[:3]:
89
  top = outfit["top"]
90
  bottom = outfit["bottom"]
91
+ lines.append(
92
+ f"- {_format_garment_line(top)} + {_format_garment_line(bottom)}"
93
+ )
94
  return "\n".join(lines)
95
 
96
 
 
197
  def rank_combinations_prompt(
198
  combinations: list[dict],
199
  context: str = "",
200
+ max_items: int = MAX_RANKING_ITEMS,
201
  ) -> tuple[str, str]:
202
  """Build system + user prompts for the LLM to rank outfit combinations.
203
 
 
207
  return "", ""
208
 
209
  subset = _select_combos_for_ranking(combinations, max_items=max_items)
210
+ occasion = context.strip() if context and context.strip() else "everyday casual wear"
 
 
211
 
212
  user_lines = [
213
  f"Occasion: {occasion}",
214
+ f"Rank these {len(subset)} outfits best-to-worst. Return JSON array of IDs only.",
 
 
215
  "",
216
  ]
217
 
218
  for combo in subset:
219
+ top = _format_garment_line(combo["top"])
220
+ bottom = _format_garment_line(combo["bottom"])
221
+ user_lines.append(f"- {combo['id']}: {top} + {bottom}")
222
 
223
  liked_hint = _format_liked_hint()
224
  if liked_hint:
225
  user_lines.append(liked_hint)
226
 
227
+ user_lines.append(f'Return: ["outfit_XXX", ...] with all {len(subset)} IDs reordered.')
 
 
 
 
228
 
229
  return RANKING_SYSTEM_PROMPT, "\n".join(user_lines)
230
 
 
255
  {"role": "system", "content": system_prompt},
256
  {"role": "user", "content": user_prompt},
257
  ],
258
+ max_tokens=512,
259
  temperature=0.2,
260
  )
261