scottymcgee commited on
Commit
a24055c
·
verified ·
1 Parent(s): 9f09c1f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +162 -156
app.py CHANGED
@@ -1,11 +1,12 @@
1
  # app.py
2
  import gradio as gr
3
- from PIL import Image, ImageOps
4
  import numpy as np
5
  import pandas as pd
6
  import io
7
  import os
8
  import cv2
 
9
  from pathlib import Path
10
  from typing import List, Dict, Any, Tuple
11
 
@@ -34,21 +35,6 @@ def _to_np(img: Image.Image) -> np.ndarray:
34
  return np.array(_ensure_rgb(img))
35
 
36
 
37
- def _thumbnail(img: Image.Image, max_side: int = 320) -> Image.Image:
38
- """
39
- Create a *small* thumbnail for gallery display so you don't need to scroll.
40
- Keeps aspect ratio; pads to square so grids look neat.
41
- """
42
- img = _ensure_rgb(img.copy())
43
- img.thumbnail((max_side, max_side))
44
- # Pad to square with white (looks nicer for product photos)
45
- w, h = img.size
46
- side = max(w, h)
47
- bg = Image.new("RGB", (side, side), (255, 255, 255))
48
- bg.paste(img, ((side - w) // 2, (side - h) // 2))
49
- return bg
50
-
51
-
52
  def _hsv_hist_features(img: Image.Image) -> np.ndarray:
53
  """Return simple features for matching and scoring.
54
  - Hue histogram (18 bins)
@@ -83,9 +69,7 @@ def _complementary_hue_score(q_hist: np.ndarray, w_hist: np.ndarray) -> float:
83
  q_h = q_hist[:hb]
84
  w_h = w_hist[:hb]
85
  q_shift = np.roll(q_h, hb // 2)
86
- # cosine similarity on shifted hues
87
- denom = (np.linalg.norm(q_shift) * np.linalg.norm(w_h) + 1e-8)
88
- hue_sim = float(np.dot(q_shift, w_h) / denom)
89
 
90
  # Encourage pairing items with different edge density (texture contrast)
91
  q_ed = q_hist[-1]
@@ -270,9 +254,7 @@ def _get_embedder() -> _Embedder:
270
  # ----------------------------
271
  # State schema:
272
  # {
273
- # "wardrobe": [ {"id": int, "name": str, "image": PIL.Image, "thumb": PIL.Image,
274
- # "features": np.ndarray, "embedding": np.ndarray, "category": str,
275
- # "rating": int|None} ],
276
  # "selected_idx": int|None
277
  # }
278
 
@@ -280,8 +262,7 @@ def _blank_state() -> Dict[str, Any]:
280
  return {"wardrobe": [], "selected_idx": None}
281
 
282
 
283
- # ----------------------------
284
- # Wardrobe management
285
  # ----------------------------
286
 
287
  def add_wardrobe(files: List[Any], state: Dict[str, Any]):
@@ -289,13 +270,22 @@ def add_wardrobe(files: List[Any], state: Dict[str, Any]):
289
  state = _blank_state()
290
  next_id = 0 if not state["wardrobe"] else max(w["id"] for w in state["wardrobe"]) + 1
291
  if files is None:
292
- return state, _render_gallery(state), _ratings_df(state)
293
 
294
  scorer = _get_scorer()
295
  embedder = _get_embedder()
296
 
 
 
 
 
297
  for f in files:
298
  try:
 
 
 
 
 
299
  img = Image.open(f.name if hasattr(f, "name") else f)
300
  img = _ensure_rgb(img)
301
  feats = _hsv_hist_features(img)
@@ -306,50 +296,84 @@ def add_wardrobe(files: List[Any], state: Dict[str, Any]):
306
  category = _ALIAS_TO_CANON.get(alias, "shirt")
307
  except Exception:
308
  category = "shirt"
309
- name = os.path.basename(getattr(f, 'name', f))
310
  rating = scorer.predict_1to100(img)
311
  state["wardrobe"].append({
312
  "id": next_id,
313
  "name": name,
314
  "image": img,
315
- "thumb": _thumbnail(img),
316
  "features": feats,
317
  "embedding": emb,
318
  "category": category,
319
  "rating": int(rating),
320
  })
 
321
  next_id += 1
322
  except Exception:
323
  continue
324
 
325
  gallery = _render_gallery(state)
326
- return state, gallery, _ratings_df(state)
 
 
 
 
 
 
 
 
 
327
 
328
 
329
  def add_wardrobe_from_dir(example_dir: str, state: Dict[str, Any]):
330
- """Load all images in a folder into the wardrobe and auto-rate/classify them."""
 
 
331
  if not example_dir:
332
- return state, _render_gallery(state), _ratings_df(state)
333
  p = Path(example_dir)
334
- patterns = ["*.jpg", "*.jpeg", "*.png", "*.webp", "*.bmp", "*.tif", "*.tiff"]
335
  files = []
336
  for pat in patterns:
337
  files.extend([str(x) for x in p.glob(pat)])
338
- return add_wardrobe(files, state)
 
 
339
 
340
 
341
  def clear_wardrobe(state: Dict[str, Any]):
342
  state = _blank_state()
343
- return state, [], _ratings_df(state)
344
 
345
 
346
- def _render_gallery(state: Dict[str, Any]) -> List[Tuple[Image.Image, str]]:
347
- """Return list of (thumbnail, caption)."""
348
- out = []
349
- for w in state.get("wardrobe", []):
350
- caption = f"#{w['id']} · {w['name']} · {w.get('category','?')} · {w.get('rating','-')}/100"
351
- out.append((w["thumb"], caption))
352
- return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
 
355
  def _ratings_df(state: Dict[str, Any]) -> pd.DataFrame:
@@ -373,6 +397,25 @@ def export_ratings(state: Dict[str, Any]):
373
  df.to_csv(buf, index=False)
374
  buf.seek(0)
375
  return buf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
 
378
  # ----------------------------
@@ -381,9 +424,7 @@ def export_ratings(state: Dict[str, Any]):
381
 
382
  def rate_and_recommend(query_img: Image.Image, top_k: int, matching_mode: str, state: Dict[str, Any]):
383
  if query_img is None:
384
- return 0, "No image provided.", [], pd.DataFrame(columns=[
385
- "rank", "name", "category", "model_rating", "match_score"
386
- ])
387
 
388
  query_img = _ensure_rgb(query_img)
389
 
@@ -432,7 +473,7 @@ def rate_and_recommend(query_img: Image.Image, top_k: int, matching_mode: str, s
432
  else: # Complementary color + style
433
  final = 0.5 * ((cos + 1.0) / 2.0) + 0.5 * comp
434
 
435
- # Quality prior (prefer items you generally like)
436
  if w.get("rating") is not None:
437
  qual = 0.5 + 0.5 * (w["rating"] / 100.0)
438
  final *= qual
@@ -450,177 +491,147 @@ def rate_and_recommend(query_img: Image.Image, top_k: int, matching_mode: str, s
450
  final = 0.5 * ((cos + 1.0) / 2.0) + 0.5 * comp
451
  candidates.append((final, w))
452
 
453
- # Rank and prepare outputs
454
  candidates.sort(key=lambda x: x[0], reverse=True)
455
- chosen = candidates[: max(0, top_k)]
456
-
457
- if not chosen:
458
- txt = f"**Predicted wear score:** {pred}/100 \n" \
459
- f"**Detected category:** {qcat} \n" \
460
- f"**Results:** No compatible matches found in your wardrobe."
461
- return pred, txt, [], pd.DataFrame(columns=[
462
- "rank", "name", "category", "model_rating", "match_score"
463
- ])
464
 
465
- # Human-friendly summary
466
- top_names = ", ".join([f"{w['name']} ({w.get('category')})" for _, w in chosen])
 
 
 
467
  txt = (
468
  f"**Predicted wear score:** {pred}/100 \n"
469
- f"**Detected category:** `{qcat}` \n"
470
- f"**Matching mode:** `{matching_mode}` \n"
471
- f"**Suggested pairings:** {top_names} \n"
472
- f"_Scoring blends style similarity (CLIP) with color/texture complement and your model ratings._"
473
  )
 
474
 
475
- # Gallery (thumbnails) + table
476
- rec_gallery = [(w["thumb"], f"{w['name']} · {w.get('category')} · {w.get('rating','-')}/100")
477
- for _, w in chosen]
 
 
 
478
 
479
- rec_table = pd.DataFrame([{
480
- "rank": i + 1,
481
- "name": w["name"],
482
- "category": w.get("category"),
483
- "model_rating": w.get("rating"),
484
- "match_score": round(float(s), 3)
485
- } for i, (s, w) in enumerate(chosen)])
 
 
486
 
487
- return pred, txt, rec_gallery, rec_table
 
 
 
 
 
 
 
 
 
 
 
 
488
 
489
 
490
  # ----------------------------
491
  # Gradio UI
492
  # ----------------------------
493
-
494
- APP_DESCRIPTION = """
495
- # Wardrobe Rater + Recommender
496
-
497
- **What this app does (1 minute):**
498
- - **Upload** photos of items in your wardrobe (tops, pants, jackets, shoes).
499
- - The app **auto-rates** each item with your model and tags its category.
500
- - When you upload a **new item** (e.g., a shopping photo), it:
501
- 1) Predicts how likely **you** are to wear it (1–100),
502
- 2) Suggests the **best matches** from your wardrobe using style similarity and color/texture complement.
503
-
504
- **How to use it:**
505
- 1. Go to **“1) Wardrobe Manager”** and upload several wardrobe images (front-on, good lighting).
506
- 2. Then open **“2) Rate + Recommend New Item”**, upload your candidate item, pick the number of matches, and click **Rate + Recommend**.
507
- 3. Review the **score**, the **explanation**, the **top matches** (thumbnails), and the **table**.
508
- """
509
-
510
- WARDROBE_TIPS = """
511
- **What to upload:** clear product-style photos (JPG/PNG).
512
- Avoid group photos or cluttered backgrounds when possible.
513
- You can click thumbnails to preview full size.
514
- """
515
-
516
- QUERY_TIPS = """
517
- **Upload a single item** you’re considering (e.g., a screenshot or photo from a listing).
518
- Good lighting and centered framing helps the detector and embeddings.
519
- """
520
-
521
- with gr.Blocks(title="Wardrobe Rater + Recommender",
522
- css="""
523
- .gradio-container {max-width: 1100px}
524
- .small-note {font-size: 0.9em; color: #4b5563}
525
- """) as demo:
526
- gr.Markdown(APP_DESCRIPTION)
527
 
528
  app_state = gr.State(_blank_state())
529
 
530
  with gr.Tab("1) Wardrobe Manager"):
531
- gr.Markdown("### Upload your wardrobe")
532
  with gr.Row():
533
  wardrobe_uploader = gr.File(
534
- label="Upload wardrobe images",
535
- file_types=["image"],
536
- file_count="multiple"
 
537
  )
538
- gr.Markdown(WARDROBE_TIPS, elem_classes=["small-note"])
539
  with gr.Row():
540
- add_btn = gr.Button("Add to wardrobe (auto-rate + auto-category)", variant="primary")
541
- clear_btn = gr.Button("🗑️ Clear wardrobe", variant="secondary")
542
-
543
  gallery = gr.Gallery(
544
- label="Current wardrobe (click to preview)",
545
  columns=6,
546
  height=220,
547
- allow_preview=True,
548
- show_label=True
549
  )
 
550
 
551
- gr.Markdown("### Item summary")
552
- ratings_table = gr.Dataframe(
553
- headers=["id", "name", "category", "model_rating"],
554
- interactive=False,
555
- wrap=True
556
- )
557
-
558
- # Optional Example loader (kept hidden input textbox)
559
  gr.Markdown("### Or load an example wardrobe")
560
  example_dir = gr.Textbox(label="Example folder path", value="examples/wardrobe_basic", visible=False)
561
  gr.Examples(
562
  examples=[["examples/wardrobe_basic", None]],
563
  inputs=[example_dir, app_state],
564
- outputs=[app_state, gallery, ratings_table],
565
  fn=add_wardrobe_from_dir,
566
  cache_examples=False,
567
  run_on_click=True,
568
  )
569
 
570
  with gr.Tab("2) Rate + Recommend New Item"):
571
- gr.Markdown("### Upload a candidate item")
572
  with gr.Row():
573
  query_img = gr.Image(
574
- label="Upload or paste image (single item)",
575
- sources=["upload", "webcam", "clipboard"],
576
- type="pil"
 
 
577
  )
578
- controls_col = gr.Column()
579
- with controls_col:
580
  topk = gr.Slider(1, 6, value=3, step=1, label="# of matches to return")
581
  matching_mode = gr.Radio(
582
  ["Complementary color+style", "Similar style"],
583
  value="Complementary color+style",
584
  label="Matching mode"
585
  )
586
- go_btn = gr.Button("Rate + Recommend", variant="primary")
587
- gr.Markdown(QUERY_TIPS, elem_classes=["small-note"])
588
-
589
  with gr.Row():
590
  pred_score = gr.Number(label="Predicted wear score (1–100)")
591
- rec_text = gr.Markdown() # human-readable summary
592
-
593
- gr.Markdown("### Top matches (click to preview)")
594
  rec_gallery = gr.Gallery(
 
595
  columns=6,
596
  height=220,
597
- allow_preview=True,
598
- show_label=False
599
- )
600
-
601
- gr.Markdown("### Details")
602
- rec_table = gr.Dataframe(
603
- headers=["rank", "name", "category", "model_rating", "match_score"],
604
- interactive=False,
605
- wrap=True
606
  )
607
 
608
  # --- Wiring ---
609
  add_btn.click(
610
  add_wardrobe,
611
  inputs=[wardrobe_uploader, app_state],
612
- outputs=[app_state, gallery, ratings_table]
613
  )
614
  clear_btn.click(
615
  clear_wardrobe,
616
  inputs=[app_state],
617
- outputs=[app_state, gallery, ratings_table]
618
  )
619
 
620
  go_btn.click(
621
  rate_and_recommend,
622
  inputs=[query_img, topk, matching_mode, app_state],
623
- outputs=[pred_score, rec_text, rec_gallery, rec_table]
624
  )
625
 
626
  # Lightweight tests. Run only when RUN_TESTS=1
@@ -643,14 +654,9 @@ if __name__ == "__main__":
643
  # Test recommend path with small wardrobe
644
  st = _blank_state()
645
  img_b = solid(32)
646
- st["wardrobe"].append({
647
- "id":0, "name":"test.png", "image":img_b, "thumb":_thumbnail(img_b),
648
- "features":_hsv_hist_features(img_b), "embedding":_get_embedder().embed(img_b),
649
- "rating":50, "category":"pants"
650
- })
651
- pred, txt, recs, tbl = rate_and_recommend(solid(200), 1, "Similar style", st)
652
  assert isinstance(pred, int) and isinstance(txt, str) and isinstance(recs, list)
653
- assert isinstance(tbl, pd.DataFrame)
654
  print("Tests passed.")
655
  else:
656
  demo.launch()
 
1
  # app.py
2
  import gradio as gr
3
+ from PIL import Image
4
  import numpy as np
5
  import pandas as pd
6
  import io
7
  import os
8
  import cv2
9
+ import glob
10
  from pathlib import Path
11
  from typing import List, Dict, Any, Tuple
12
 
 
35
  return np.array(_ensure_rgb(img))
36
 
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  def _hsv_hist_features(img: Image.Image) -> np.ndarray:
39
  """Return simple features for matching and scoring.
40
  - Hue histogram (18 bins)
 
69
  q_h = q_hist[:hb]
70
  w_h = w_hist[:hb]
71
  q_shift = np.roll(q_h, hb // 2)
72
+ hue_sim = float(np.dot(q_shift, w_h) / (np.linalg.norm(q_shift) * np.linalg.norm(w_h) + 1e-8))
 
 
73
 
74
  # Encourage pairing items with different edge density (texture contrast)
75
  q_ed = q_hist[-1]
 
254
  # ----------------------------
255
  # State schema:
256
  # {
257
+ # "wardrobe": [ {"id": int, "name": str, "image": PIL.Image, "features": np.ndarray, "embedding": np.ndarray, "rating": int|None} ],
 
 
258
  # "selected_idx": int|None
259
  # }
260
 
 
262
  return {"wardrobe": [], "selected_idx": None}
263
 
264
 
265
+
 
266
  # ----------------------------
267
 
268
  def add_wardrobe(files: List[Any], state: Dict[str, Any]):
 
270
  state = _blank_state()
271
  next_id = 0 if not state["wardrobe"] else max(w["id"] for w in state["wardrobe"]) + 1
272
  if files is None:
273
+ return state, _render_gallery(state), _ratings_df(state), ""
274
 
275
  scorer = _get_scorer()
276
  embedder = _get_embedder()
277
 
278
+ warnings = []
279
+ added = 0
280
+ allowed_exts = {".png", ".jpg", ".jpeg"}
281
+
282
  for f in files:
283
  try:
284
+ fname = os.path.basename(getattr(f, 'name', f))
285
+ ext = Path(fname).suffix.lower()
286
+ if ext not in allowed_exts:
287
+ warnings.append(fname)
288
+ continue
289
  img = Image.open(f.name if hasattr(f, "name") else f)
290
  img = _ensure_rgb(img)
291
  feats = _hsv_hist_features(img)
 
296
  category = _ALIAS_TO_CANON.get(alias, "shirt")
297
  except Exception:
298
  category = "shirt"
299
+ name = fname
300
  rating = scorer.predict_1to100(img)
301
  state["wardrobe"].append({
302
  "id": next_id,
303
  "name": name,
304
  "image": img,
 
305
  "features": feats,
306
  "embedding": emb,
307
  "category": category,
308
  "rating": int(rating),
309
  })
310
+ added += 1
311
  next_id += 1
312
  except Exception:
313
  continue
314
 
315
  gallery = _render_gallery(state)
316
+ status_lines = []
317
+ if added:
318
+ status_lines.append(f"✅ Added {added} item(s) to your wardrobe.")
319
+ if warnings:
320
+ status_lines.append(
321
+ f"⚠️ Skipped {len(warnings)} file(s) (not PNG/JPG): " + ", ".join(warnings[:5]) + ("..." if len(warnings) > 5 else "")
322
+ )
323
+ status_lines.append("Please upload .png, .jpg, or .jpeg files.")
324
+ status_msg = "\n\n".join(status_lines)
325
+ return state, gallery, _ratings_df(state), status_msg
326
 
327
 
328
  def add_wardrobe_from_dir(example_dir: str, state: Dict[str, Any]):
329
+ """Load all images in a folder into the wardrobe and auto-rate/classify them.
330
+ Used by gr.Examples. Accepts relative paths in the Space repo.
331
+ """
332
  if not example_dir:
333
+ return state, _render_gallery(state), _ratings_df(state), ""
334
  p = Path(example_dir)
335
+ patterns = ["*.jpg", "*.jpeg", "*.png"] # keep examples aligned with allowed types
336
  files = []
337
  for pat in patterns:
338
  files.extend([str(x) for x in p.glob(pat)])
339
+ st, gal, df = add_wardrobe(files, state)[:3]
340
+ # add_wardrobe returns 4 now; reuse message-less return for examples
341
+ return st, gal, df, "Loaded example wardrobe."
342
 
343
 
344
  def clear_wardrobe(state: Dict[str, Any]):
345
  state = _blank_state()
346
+ return state, [], _ratings_df(state), "Wardrobe cleared."
347
 
348
 
349
+ def _render_gallery(state: Dict[str, Any]) -> List[Image.Image]:
350
+ return [w["image"] for w in state.get("wardrobe", [])]
351
+
352
+
353
+ def on_select_item(item_label: str, state: Dict[str, Any]):
354
+ if not item_label:
355
+ return state, None, gr.update(value=50)
356
+ # label format: "#<id> · <name>"
357
+ try:
358
+ item_id = int(item_label.split(" ")[0][1:])
359
+ except Exception:
360
+ return state, None, gr.update(value=50)
361
+
362
+ idx = next((i for i, w in enumerate(state["wardrobe"]) if w["id"] == item_id), None)
363
+ state["selected_idx"] = idx
364
+ if idx is None:
365
+ return state, None, gr.update(value=50)
366
+ w = state["wardrobe"][idx]
367
+ current_rating = w["rating"] if w["rating"] is not None else 50
368
+ return state, w["image"], gr.update(value=int(current_rating))
369
+
370
+
371
+ def save_rating(rating: int, state: Dict[str, Any]):
372
+ idx = state.get("selected_idx", None)
373
+ if idx is None:
374
+ return state, _ratings_df(state)
375
+ state["wardrobe"][idx]["rating"] = int(rating)
376
+ return state, _ratings_df(state)
377
 
378
 
379
  def _ratings_df(state: Dict[str, Any]) -> pd.DataFrame:
 
397
  df.to_csv(buf, index=False)
398
  buf.seek(0)
399
  return buf
400
+ buf = io.BytesIO()
401
+ df.to_csv(buf, index=False)
402
+ buf.seek(0)
403
+ return buf
404
+
405
+
406
+ def import_ratings(file_obj, state: Dict[str, Any]):
407
+ # Deprecated in auto-rating flow; keep no-op for compatibility
408
+ return state, _ratings_df(state)
409
+ try:
410
+ df = pd.read_csv(file_obj.name if hasattr(file_obj, "name") else file_obj)
411
+ names_to_rating = {str(row["name"]): int(row["rating"]) if not pd.isna(row["rating"]) else None
412
+ for _, row in df.iterrows()}
413
+ for w in state.get("wardrobe", []):
414
+ if w["name"] in names_to_rating:
415
+ w["rating"] = names_to_rating[w["name"]]
416
+ except Exception:
417
+ pass
418
+ return state, _ratings_df(state)
419
 
420
 
421
  # ----------------------------
 
424
 
425
  def rate_and_recommend(query_img: Image.Image, top_k: int, matching_mode: str, state: Dict[str, Any]):
426
  if query_img is None:
427
+ return 0, "Please upload a PNG/JPG image to get a rating and matches.", []
 
 
428
 
429
  query_img = _ensure_rgb(query_img)
430
 
 
473
  else: # Complementary color + style
474
  final = 0.5 * ((cos + 1.0) / 2.0) + 0.5 * comp
475
 
476
+ # Quality prior
477
  if w.get("rating") is not None:
478
  qual = 0.5 + 0.5 * (w["rating"] / 100.0)
479
  final *= qual
 
491
  final = 0.5 * ((cos + 1.0) / 2.0) + 0.5 * comp
492
  candidates.append((final, w))
493
 
 
494
  candidates.sort(key=lambda x: x[0], reverse=True)
495
+ top = candidates[: max(0, top_k)]
496
+ recs = []
497
+ for score, w in top:
498
+ caption = f"{w['name']} · {w.get('category','?')} · match {int(round(100*score))}%"
499
+ recs.append((w["image"], caption))
 
 
 
 
500
 
501
+ if len(recs) == 0:
502
+ txt = f"**Predicted wear score:** {pred}/100\n\n_No compatible matches found in your wardrobe._"
503
+ return pred, txt, []
504
+
505
+ top_names = ", ".join([f"{w['name']} ({w.get('category')})" for _, w in top])
506
  txt = (
507
  f"**Predicted wear score:** {pred}/100 \n"
508
+ f"**Detected category:** {qcat} \n"
509
+ f"**Top suggestions:** {top_names} \n"
510
+ f"_Matching mode:_ {matching_mode.lower()} with category filtering and quality prior."
 
511
  )
512
+ return pred, txt, recs
513
 
514
+ # (Unreached legacy block kept to minimize the overall diff)
515
+ try:
516
+ scorer = _get_scorer()
517
+ pred = scorer.predict_1to100(query_img)
518
+ except Exception:
519
+ pred = 50
520
 
521
+ qfeat = _hsv_hist_features(query_img)
522
+ candidates = []
523
+ for w in state.get("wardrobe", []):
524
+ comp = _complementary_hue_score(qfeat, w["features"])
525
+ user_w = 1.0
526
+ if w["rating"] is not None:
527
+ user_w = 0.5 + 0.5 * (w["rating"] / 100.0)
528
+ final = comp * user_w
529
+ candidates.append((final, w))
530
 
531
+ candidates.sort(key=lambda x: x[0], reverse=True)
532
+ recs = [w["image"] for _, w in candidates[: max(0, top_k)]]
533
+
534
+ if len(recs) == 0:
535
+ txt = f"Predicted rating: {pred}/100. No matches found in your wardrobe."
536
+ return pred, txt, []
537
+
538
+ top_names = ", ".join([w["name"] for _, w in candidates[: max(0, top_k)]])
539
+ txt = (
540
+ f"Predicted rating: {pred}/100. Suggested pairings from your wardrobe: {top_names}."
541
+ f"Logic: complementary hues + texture contrast + your cached ratings."
542
+ )
543
+ return pred, txt, recs
544
 
545
 
546
  # ----------------------------
547
  # Gradio UI
548
  # ----------------------------
549
+ with gr.Blocks(title="Wardrobe Rater + Recommender", css="""
550
+ .gradio-container {max-width: 1200px}
551
+ """) as demo:
552
+ gr.Markdown(
553
+ "# Wardrobe Rater + Recommender\n"
554
+ "**What this app does:** Scores how likely you are to wear a new item (1–100) and suggests compatible pieces from your wardrobe. \n"
555
+ "**How to use it:** (1) Upload a few wardrobe images first. (PNG/JPG only.) (2) Go to *Rate + Recommend* and upload a new item to get a score and matches."
556
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
557
 
558
  app_state = gr.State(_blank_state())
559
 
560
  with gr.Tab("1) Wardrobe Manager"):
 
561
  with gr.Row():
562
  wardrobe_uploader = gr.File(
563
+ label="Upload wardrobe images (PNG/JPG)",
564
+ file_types=[".png", ".jpg", ".jpeg"], # enforce png/jpg
565
+ file_count="multiple",
566
+ info="Tip: crop to the item; solid backgrounds work well."
567
  )
 
568
  with gr.Row():
569
+ add_btn = gr.Button("Add to wardrobe (auto-rate + auto-category)")
570
+ clear_btn = gr.Button("Clear wardrobe")
571
+ status_md = gr.Markdown("") # status / reminders (e.g., wrong file types)
572
  gallery = gr.Gallery(
573
+ label="Current wardrobe",
574
  columns=6,
575
  height=220,
576
+ object_fit="contain",
577
+ allow_preview=False
578
  )
579
+ ratings_table = gr.Dataframe(headers=["id", "name", "category", "model_rating"], interactive=False)
580
 
 
 
 
 
 
 
 
 
581
  gr.Markdown("### Or load an example wardrobe")
582
  example_dir = gr.Textbox(label="Example folder path", value="examples/wardrobe_basic", visible=False)
583
  gr.Examples(
584
  examples=[["examples/wardrobe_basic", None]],
585
  inputs=[example_dir, app_state],
586
+ outputs=[app_state, gallery, ratings_table, status_md],
587
  fn=add_wardrobe_from_dir,
588
  cache_examples=False,
589
  run_on_click=True,
590
  )
591
 
592
  with gr.Tab("2) Rate + Recommend New Item"):
 
593
  with gr.Row():
594
  query_img = gr.Image(
595
+ label="Upload or take photo (PNG/JPG)",
596
+ sources=["upload", "webcam","clipboard"],
597
+ type="pil",
598
+ image_mode="RGB",
599
+ info="Upload a clear photo of the item to score and match."
600
  )
 
 
601
  topk = gr.Slider(1, 6, value=3, step=1, label="# of matches to return")
602
  matching_mode = gr.Radio(
603
  ["Complementary color+style", "Similar style"],
604
  value="Complementary color+style",
605
  label="Matching mode"
606
  )
607
+ go_btn = gr.Button("Rate + Recommend")
 
 
608
  with gr.Row():
609
  pred_score = gr.Number(label="Predicted wear score (1–100)")
610
+ rec_text = gr.Markdown()
 
 
611
  rec_gallery = gr.Gallery(
612
+ label="Matches in your wardrobe",
613
  columns=6,
614
  height=220,
615
+ object_fit="contain",
616
+ allow_preview=False
 
 
 
 
 
 
 
617
  )
618
 
619
  # --- Wiring ---
620
  add_btn.click(
621
  add_wardrobe,
622
  inputs=[wardrobe_uploader, app_state],
623
+ outputs=[app_state, gallery, ratings_table, status_md]
624
  )
625
  clear_btn.click(
626
  clear_wardrobe,
627
  inputs=[app_state],
628
+ outputs=[app_state, gallery, ratings_table, status_md]
629
  )
630
 
631
  go_btn.click(
632
  rate_and_recommend,
633
  inputs=[query_img, topk, matching_mode, app_state],
634
+ outputs=[pred_score, rec_text, rec_gallery]
635
  )
636
 
637
  # Lightweight tests. Run only when RUN_TESTS=1
 
654
  # Test recommend path with small wardrobe
655
  st = _blank_state()
656
  img_b = solid(32)
657
+ st["wardrobe"].append({"id":0, "name":"test.png", "image":img_b, "features":_hsv_hist_features(img_b), "embedding":_get_embedder().embed(img_b), "rating":50})
658
+ pred, txt, recs = rate_and_recommend(solid(200), 1, "Similar style", st)
 
 
 
 
659
  assert isinstance(pred, int) and isinstance(txt, str) and isinstance(recs, list)
 
660
  print("Tests passed.")
661
  else:
662
  demo.launch()