Pepguy commited on
Commit
5282e3c
·
verified ·
1 Parent(s): 5ad717c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -2
app.py CHANGED
@@ -6,6 +6,7 @@ import base64
6
  import logging
7
  import uuid
8
  import time
 
9
  from typing import List, Dict, Any, Tuple, Optional
10
 
11
  from flask import Flask, request, jsonify
@@ -48,6 +49,106 @@ if FIREBASE_ADMIN_JSON and not FIREBASE_ADMIN_AVAILABLE:
48
  app = Flask(__name__)
49
  CORS(app)
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  # ---------- Firebase init helpers ----------
52
  _firebase_app = None
53
 
@@ -397,6 +498,16 @@ def process_image():
397
 
398
  itm["analysis"] = analysis
399
 
 
 
 
 
 
 
 
 
 
 
400
  item_id = itm.get("id") or str(uuid.uuid4())
401
  path = f"detected/{safe_uid}/{item_id}.jpg"
402
  try:
@@ -410,13 +521,14 @@ def process_image():
410
  "ai_brand": analysis.get("brand",""),
411
  "ai_summary": analysis.get("summary",""),
412
  "ai_tags": json.dumps(analysis.get("tags", [])),
 
413
  }
414
  url = upload_b64_to_firebase(b64, path, content_type="image/jpeg", metadata=metadata)
415
  itm["thumbnail_url"] = url
416
  itm["thumbnail_path"] = path
417
  itm.pop("thumbnail_b64", None)
418
  itm["_session_id"] = session_id
419
- log.debug("Auto-uploaded thumbnail for %s -> %s (session=%s)", item_id, url, session_id)
420
  except Exception as up_e:
421
  log.warning("Auto-upload failed for %s: %s", item_id, up_e)
422
  # keep thumbnail_b64 and analysis for client fallback
@@ -425,6 +537,12 @@ def process_image():
425
  log.info("FIREBASE_ADMIN_JSON not set; skipping server-side thumbnail upload.")
426
  else:
427
  log.info("Firebase admin SDK not available; skipping server-side thumbnail upload.")
 
 
 
 
 
 
428
 
429
  return jsonify({"ok": True, "items": items_out, "session_id": session_id, "debug": {"raw_model_text": (raw_text or "")[:1600]}}), 200
430
 
@@ -432,6 +550,10 @@ def process_image():
432
  log.exception("Processing error: %s", ex)
433
  try:
434
  items_out = fallback_contour_crops(bgr_img, max_items=8)
 
 
 
 
435
  return jsonify({"ok": True, "items": items_out, "session_id": session_id, "debug": {"error": str(ex)}}), 200
436
  except Exception as e2:
437
  log.exception("Fallback also failed: %s", e2)
@@ -509,10 +631,20 @@ def finalize_detections():
509
  ai_brand = md.get("ai_brand") or ""
510
  ai_summary = md.get("ai_summary") or ""
511
  ai_tags_raw = md.get("ai_tags") or "[]"
 
512
  try:
513
  ai_tags = json.loads(ai_tags_raw) if isinstance(ai_tags_raw, str) else ai_tags_raw
514
  except Exception:
515
  ai_tags = []
 
 
 
 
 
 
 
 
 
516
  kept.append({
517
  "id": item_id,
518
  "thumbnail_url": url,
@@ -522,7 +654,8 @@ def finalize_detections():
522
  "brand": ai_brand,
523
  "summary": ai_summary,
524
  "tags": ai_tags
525
- }
 
526
  })
527
  else:
528
  try:
 
6
  import logging
7
  import uuid
8
  import time
9
+ import re
10
  from typing import List, Dict, Any, Tuple, Optional
11
 
12
  from flask import Flask, request, jsonify
 
49
  app = Flask(__name__)
50
  CORS(app)
51
 
52
+ # ---------- Category mapping (must match frontend) ----------
53
+ # These values intentionally match the CATEGORY_OPTIONS array on the frontend.
54
+ CATEGORIES = [
55
+ "Heels",
56
+ "Sneakers",
57
+ "Loafers",
58
+ "Boots",
59
+ "Dress",
60
+ "Jeans",
61
+ "Skirt",
62
+ "Jacket",
63
+ "Blazer",
64
+ "T-Shirt",
65
+ "Coat",
66
+ "Shorts",
67
+ ]
68
+
69
+ # simple synonyms / keyword -> category mapping (lowercase keys)
70
+ SYNONYMS: Dict[str, str] = {
71
+ "heel": "Heels",
72
+ "heels": "Heels",
73
+ "sneaker": "Sneakers",
74
+ "sneakers": "Sneakers",
75
+ "trainer": "Sneakers",
76
+ "trainers": "Sneakers",
77
+ "loafer": "Loafers",
78
+ "loafers": "Loafers",
79
+ "boot": "Boots",
80
+ "boots": "Boots",
81
+ "dress": "Dress",
82
+ "gown": "Dress",
83
+ "jean": "Jeans",
84
+ "jeans": "Jeans",
85
+ "denim": "Jeans",
86
+ "skirt": "Skirt",
87
+ "jacket": "Jacket",
88
+ "coat": "Coat",
89
+ "blazer": "Blazer",
90
+ "t-shirt": "T-Shirt",
91
+ "t shirt": "T-Shirt",
92
+ "tee": "T-Shirt",
93
+ "shirt": "T-Shirt",
94
+ "top": "T-Shirt",
95
+ "short": "Shorts",
96
+ "shorts": "Shorts",
97
+ "shoe": "Sneakers", # generic shoe -> map to Sneakers as fallback
98
+ "shoes": "Sneakers",
99
+ "sandal": "Heels", # if ambiguous, map sandals to Heels bucket (you can adjust)
100
+ "sandals": "Heels",
101
+ }
102
+
103
+ def normalize_text(s: str) -> str:
104
+ return re.sub(r'[^a-z0-9\s\-]', ' ', s.lower()).strip()
105
+
106
+ def choose_category_from_candidates(*candidates: Optional[str], tags: Optional[List[str]] = None) -> str:
107
+ """
108
+ Given a list of candidate strings (analysis.type, label, summary, etc.) and optional tags,
109
+ attempt to pick a category from CATEGORIES. Returns a category string guaranteed to be in CATEGORIES.
110
+ Falls back to "T-Shirt" if nothing matches.
111
+ """
112
+ # try tags first (explicit tag likely to indicate category)
113
+ if tags:
114
+ for t in tags:
115
+ if not t:
116
+ continue
117
+ tok = normalize_text(str(t))
118
+ # direct synonym match
119
+ if tok in SYNONYMS:
120
+ return SYNONYMS[tok]
121
+ # partial substring match
122
+ for key, cat in SYNONYMS.items():
123
+ if key in tok:
124
+ return cat
125
+ # try direct category name match
126
+ for cat in CATEGORIES:
127
+ if tok == cat.lower() or cat.lower() in tok:
128
+ return cat
129
+
130
+ # iterate through candidate strings in order provided
131
+ for c in candidates:
132
+ if not c:
133
+ continue
134
+ s = normalize_text(str(c))
135
+ # exact category match
136
+ for cat in CATEGORIES:
137
+ if s == cat.lower() or cat.lower() in s:
138
+ return cat
139
+ # check synonyms dictionary words
140
+ words = s.split()
141
+ for w in words:
142
+ if w in SYNONYMS:
143
+ return SYNONYMS[w]
144
+ # check substrings (e.g., "sneaker" inside longer text)
145
+ for key, cat in SYNONYMS.items():
146
+ if key in s:
147
+ return cat
148
+
149
+ # If nothing found, return a safe default present in CATEGORIES
150
+ return "T-Shirt"
151
+
152
  # ---------- Firebase init helpers ----------
153
  _firebase_app = None
154
 
 
498
 
499
  itm["analysis"] = analysis
500
 
501
+ # choose a frontend-category-compatible title
502
+ # prefer analysis.type, then label, then tags, then summary
503
+ title = choose_category_from_candidates(
504
+ analysis.get("type", ""),
505
+ itm.get("label", ""),
506
+ ' '.join(analysis.get("tags", [])),
507
+ tags=analysis.get("tags", [])
508
+ )
509
+ itm["title"] = title
510
+
511
  item_id = itm.get("id") or str(uuid.uuid4())
512
  path = f"detected/{safe_uid}/{item_id}.jpg"
513
  try:
 
521
  "ai_brand": analysis.get("brand",""),
522
  "ai_summary": analysis.get("summary",""),
523
  "ai_tags": json.dumps(analysis.get("tags", [])),
524
+ "title": title,
525
  }
526
  url = upload_b64_to_firebase(b64, path, content_type="image/jpeg", metadata=metadata)
527
  itm["thumbnail_url"] = url
528
  itm["thumbnail_path"] = path
529
  itm.pop("thumbnail_b64", None)
530
  itm["_session_id"] = session_id
531
+ log.debug("Auto-uploaded thumbnail for %s -> %s (session=%s) title=%s", item_id, url, session_id, title)
532
  except Exception as up_e:
533
  log.warning("Auto-upload failed for %s: %s", item_id, up_e)
534
  # keep thumbnail_b64 and analysis for client fallback
 
537
  log.info("FIREBASE_ADMIN_JSON not set; skipping server-side thumbnail upload.")
538
  else:
539
  log.info("Firebase admin SDK not available; skipping server-side thumbnail upload.")
540
+ # For non-upload path, still add a title derived from label/unknown so frontend has it
541
+ for itm in items_out:
542
+ if "title" not in itm:
543
+ analysis = itm.get("analysis") or {"type":"unknown","tags":[]}
544
+ title = choose_category_from_candidates(analysis.get("type",""), itm.get("label",""), tags=analysis.get("tags", []))
545
+ itm["title"] = title
546
 
547
  return jsonify({"ok": True, "items": items_out, "session_id": session_id, "debug": {"raw_model_text": (raw_text or "")[:1600]}}), 200
548
 
 
550
  log.exception("Processing error: %s", ex)
551
  try:
552
  items_out = fallback_contour_crops(bgr_img, max_items=8)
553
+ # give fallback items a default title so frontend can filter
554
+ for itm in items_out:
555
+ if "title" not in itm:
556
+ itm["title"] = choose_category_from_candidates(itm.get("label","unknown"))
557
  return jsonify({"ok": True, "items": items_out, "session_id": session_id, "debug": {"error": str(ex)}}), 200
558
  except Exception as e2:
559
  log.exception("Fallback also failed: %s", e2)
 
631
  ai_brand = md.get("ai_brand") or ""
632
  ai_summary = md.get("ai_summary") or ""
633
  ai_tags_raw = md.get("ai_tags") or "[]"
634
+ title_meta = md.get("title") or ""
635
  try:
636
  ai_tags = json.loads(ai_tags_raw) if isinstance(ai_tags_raw, str) else ai_tags_raw
637
  except Exception:
638
  ai_tags = []
639
+ # derive title: prefer stored metadata title, then ai_type/tags/summary
640
+ title = None
641
+ if title_meta:
642
+ try:
643
+ title = json.loads(title_meta) if (title_meta.startswith('[') or title_meta.startswith('{')) else str(title_meta)
644
+ except Exception:
645
+ title = str(title_meta)
646
+ if not title:
647
+ title = choose_category_from_candidates(ai_type, ai_summary, tags=ai_tags)
648
  kept.append({
649
  "id": item_id,
650
  "thumbnail_url": url,
 
654
  "brand": ai_brand,
655
  "summary": ai_summary,
656
  "tags": ai_tags
657
+ },
658
+ "title": title
659
  })
660
  else:
661
  try: