Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# server_single_suggest.py
|
| 2 |
"""
|
| 3 |
Single-endpoint suggestion server.
|
| 4 |
|
|
@@ -15,6 +14,7 @@ import logging
|
|
| 15 |
import uuid
|
| 16 |
import time
|
| 17 |
import difflib
|
|
|
|
| 18 |
from typing import List, Dict, Any, Set, Optional
|
| 19 |
|
| 20 |
from flask import Flask, request, jsonify
|
|
@@ -127,12 +127,52 @@ def _safe_item_brand(itm: Dict[str, Any]) -> str:
|
|
| 127 |
return str(brand).strip()
|
| 128 |
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
# ---------- Simple local candidate generator ----------
|
| 131 |
def naive_generate_candidates(wardrobe_items: List[Dict[str, Any]],
|
| 132 |
user_inputs: Dict[str, Any],
|
| 133 |
user_profile: Dict[str, Any],
|
| 134 |
past_week_items: List[Dict[str, Any]],
|
| 135 |
max_candidates: int = 6) -> List[Dict[str, Any]]:
|
|
|
|
|
|
|
|
|
|
| 136 |
grouped = {}
|
| 137 |
for itm in wardrobe_items:
|
| 138 |
title = (itm.get("title") or (itm.get("analysis") or {}).get("type") or itm.get("label") or "")
|
|
@@ -177,7 +217,15 @@ def naive_generate_candidates(wardrobe_items: List[Dict[str, Any]],
|
|
| 177 |
score = max(0, min(1, score + (0.02 * ((hash(ids) % 100) / 100.0))))
|
| 178 |
candidate = {
|
| 179 |
"id": str(uuid.uuid4()),
|
| 180 |
-
"items": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
"score": round(float(score), 3),
|
| 182 |
"reason": "Auto combo",
|
| 183 |
"notes": "",
|
|
@@ -435,16 +483,19 @@ def fetch_wardrobe_from_firestore(uid: str) -> List[Dict[str, Any]]:
|
|
| 435 |
items = []
|
| 436 |
for d in docs:
|
| 437 |
dd = d.to_dict() or {}
|
|
|
|
|
|
|
| 438 |
items.append({
|
| 439 |
"id": dd.get("id") or d.id,
|
| 440 |
"label": dd.get("label") or dd.get("title") or "item",
|
| 441 |
"title": dd.get("title") or dd.get("label") or "",
|
| 442 |
-
"thumbnail_url":
|
| 443 |
"analysis": dd.get("analysis", {}),
|
| 444 |
"confidence": dd.get("confidence", 0.8),
|
| 445 |
})
|
| 446 |
if items:
|
| 447 |
-
return
|
|
|
|
| 448 |
except Exception as e:
|
| 449 |
log.warning("users/{uid}/wardrobe subcollection read failed: %s", e)
|
| 450 |
|
|
@@ -455,15 +506,16 @@ def fetch_wardrobe_from_firestore(uid: str) -> List[Dict[str, Any]]:
|
|
| 455 |
items = []
|
| 456 |
for d in docs:
|
| 457 |
dd = d.to_dict() or {}
|
|
|
|
| 458 |
items.append({
|
| 459 |
"id": dd.get("id") or d.id,
|
| 460 |
"label": dd.get("label") or dd.get("title") or "item",
|
| 461 |
"title": dd.get("title") or dd.get("label") or "",
|
| 462 |
-
"thumbnail_url":
|
| 463 |
"analysis": dd.get("analysis", {}),
|
| 464 |
"confidence": dd.get("confidence", 0.8),
|
| 465 |
})
|
| 466 |
-
return items
|
| 467 |
except Exception as e:
|
| 468 |
log.warning("wardrobe collection query failed: %s", e)
|
| 469 |
return []
|
|
@@ -505,6 +557,8 @@ def suggest_all():
|
|
| 505 |
"""
|
| 506 |
is_multipart = request.content_type and request.content_type.startswith("multipart/form-data")
|
| 507 |
try:
|
|
|
|
|
|
|
| 508 |
if is_multipart:
|
| 509 |
# access form fields and files
|
| 510 |
form = request.form
|
|
@@ -516,21 +570,20 @@ def suggest_all():
|
|
| 516 |
if ui_raw:
|
| 517 |
user_inputs = json.loads(ui_raw)
|
| 518 |
else:
|
| 519 |
-
# collect obvious form fields into user_inputs if given
|
| 520 |
user_inputs = {}
|
| 521 |
except Exception:
|
| 522 |
user_inputs = {}
|
| 523 |
max_c = int(form.get("max_candidates") or 6)
|
| 524 |
-
|
| 525 |
w_raw = form.get("wardrobe_items")
|
| 526 |
if w_raw:
|
| 527 |
try:
|
| 528 |
wardrobe_items = json.loads(w_raw)
|
| 529 |
except Exception:
|
| 530 |
wardrobe_items = []
|
|
|
|
| 531 |
# audio file
|
| 532 |
audio_file = files.get("audio")
|
| 533 |
-
audio_b64 = None
|
| 534 |
if audio_file:
|
| 535 |
try:
|
| 536 |
audio_bytes = audio_file.read()
|
|
@@ -548,6 +601,12 @@ def suggest_all():
|
|
| 548 |
log.exception("Invalid request payload: %s", e)
|
| 549 |
return jsonify({"error": "invalid request payload"}), 400
|
| 550 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
# if wardrobe_items empty, attempt to fetch from Firestore for uid
|
| 552 |
if not wardrobe_items:
|
| 553 |
try:
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
Single-endpoint suggestion server.
|
| 3 |
|
|
|
|
| 14 |
import uuid
|
| 15 |
import time
|
| 16 |
import difflib
|
| 17 |
+
import base64
|
| 18 |
from typing import List, Dict, Any, Set, Optional
|
| 19 |
|
| 20 |
from flask import Flask, request, jsonify
|
|
|
|
| 127 |
return str(brand).strip()
|
| 128 |
|
| 129 |
|
| 130 |
+
# ---------- Normalization helpers ----------
|
| 131 |
+
def normalize_wardrobe_item(itm: Dict[str, Any]) -> Dict[str, Any]:
|
| 132 |
+
"""
|
| 133 |
+
Accepts various incoming key naming schemes (camelCase or snake_case) and returns a normalized dict
|
| 134 |
+
with keys: id, label, title, thumbnail_url, analysis, confidence, brand, etc.
|
| 135 |
+
"""
|
| 136 |
+
if not isinstance(itm, dict):
|
| 137 |
+
return {}
|
| 138 |
+
|
| 139 |
+
# Prefer explicit thumbnail_url, then camelCase thumbnailUrl, then thumbnail, then thumbnailPath
|
| 140 |
+
thumb = itm.get("thumbnail_url") or itm.get("thumbnailUrl") or itm.get("thumbnail") or itm.get("thumbnailPath") or None
|
| 141 |
+
|
| 142 |
+
title = itm.get("title") or itm.get("label") or (itm.get("analysis") or {}).get("type") or ""
|
| 143 |
+
label = itm.get("label") or itm.get("title") or title or ""
|
| 144 |
+
confidence = itm.get("confidence")
|
| 145 |
+
if confidence is None:
|
| 146 |
+
confidence = itm.get("score") or itm.get("prob") or 0.8
|
| 147 |
+
|
| 148 |
+
normalized = {
|
| 149 |
+
"id": itm.get("id") or itm.get("doc_id") or "",
|
| 150 |
+
"label": label,
|
| 151 |
+
"title": title,
|
| 152 |
+
"thumbnail_url": thumb,
|
| 153 |
+
"analysis": itm.get("analysis") or {},
|
| 154 |
+
"confidence": float(confidence) if confidence is not None else 0.8,
|
| 155 |
+
# keep original payload for debugging if needed
|
| 156 |
+
"_raw": itm,
|
| 157 |
+
}
|
| 158 |
+
# try to include brand/top-level fields if present
|
| 159 |
+
if itm.get("brand"):
|
| 160 |
+
try:
|
| 161 |
+
normalized["analysis"]["brand"] = itm.get("brand")
|
| 162 |
+
except Exception:
|
| 163 |
+
pass
|
| 164 |
+
return normalized
|
| 165 |
+
|
| 166 |
+
|
| 167 |
# ---------- Simple local candidate generator ----------
|
| 168 |
def naive_generate_candidates(wardrobe_items: List[Dict[str, Any]],
|
| 169 |
user_inputs: Dict[str, Any],
|
| 170 |
user_profile: Dict[str, Any],
|
| 171 |
past_week_items: List[Dict[str, Any]],
|
| 172 |
max_candidates: int = 6) -> List[Dict[str, Any]]:
|
| 173 |
+
# ensure items are normalized (safe guard)
|
| 174 |
+
wardrobe_items = [normalize_wardrobe_item(it) for it in (wardrobe_items or [])]
|
| 175 |
+
|
| 176 |
grouped = {}
|
| 177 |
for itm in wardrobe_items:
|
| 178 |
title = (itm.get("title") or (itm.get("analysis") or {}).get("type") or itm.get("label") or "")
|
|
|
|
| 217 |
score = max(0, min(1, score + (0.02 * ((hash(ids) % 100) / 100.0))))
|
| 218 |
candidate = {
|
| 219 |
"id": str(uuid.uuid4()),
|
| 220 |
+
"items": [
|
| 221 |
+
{
|
| 222 |
+
"id": x.get("id"),
|
| 223 |
+
"label": x.get("label"),
|
| 224 |
+
"title": x.get("title"),
|
| 225 |
+
"thumbnail_url": x.get("thumbnail_url"),
|
| 226 |
+
"analysis": x.get("analysis", {})
|
| 227 |
+
} for x in items
|
| 228 |
+
],
|
| 229 |
"score": round(float(score), 3),
|
| 230 |
"reason": "Auto combo",
|
| 231 |
"notes": "",
|
|
|
|
| 483 |
items = []
|
| 484 |
for d in docs:
|
| 485 |
dd = d.to_dict() or {}
|
| 486 |
+
# accept both snake_case and camelCase thumbnail fields
|
| 487 |
+
thumb = dd.get("thumbnail_url") or dd.get("thumbnailUrl") or dd.get("thumbnail") or dd.get("thumbnailPath") or None
|
| 488 |
items.append({
|
| 489 |
"id": dd.get("id") or d.id,
|
| 490 |
"label": dd.get("label") or dd.get("title") or "item",
|
| 491 |
"title": dd.get("title") or dd.get("label") or "",
|
| 492 |
+
"thumbnail_url": thumb,
|
| 493 |
"analysis": dd.get("analysis", {}),
|
| 494 |
"confidence": dd.get("confidence", 0.8),
|
| 495 |
})
|
| 496 |
if items:
|
| 497 |
+
# normalize and return
|
| 498 |
+
return [normalize_wardrobe_item(it) for it in items]
|
| 499 |
except Exception as e:
|
| 500 |
log.warning("users/{uid}/wardrobe subcollection read failed: %s", e)
|
| 501 |
|
|
|
|
| 506 |
items = []
|
| 507 |
for d in docs:
|
| 508 |
dd = d.to_dict() or {}
|
| 509 |
+
thumb = dd.get("thumbnail_url") or dd.get("thumbnailUrl") or dd.get("thumbnail") or dd.get("thumbnailPath") or None
|
| 510 |
items.append({
|
| 511 |
"id": dd.get("id") or d.id,
|
| 512 |
"label": dd.get("label") or dd.get("title") or "item",
|
| 513 |
"title": dd.get("title") or dd.get("label") or "",
|
| 514 |
+
"thumbnail_url": thumb,
|
| 515 |
"analysis": dd.get("analysis", {}),
|
| 516 |
"confidence": dd.get("confidence", 0.8),
|
| 517 |
})
|
| 518 |
+
return [normalize_wardrobe_item(it) for it in items]
|
| 519 |
except Exception as e:
|
| 520 |
log.warning("wardrobe collection query failed: %s", e)
|
| 521 |
return []
|
|
|
|
| 557 |
"""
|
| 558 |
is_multipart = request.content_type and request.content_type.startswith("multipart/form-data")
|
| 559 |
try:
|
| 560 |
+
wardrobe_items = []
|
| 561 |
+
audio_b64 = None
|
| 562 |
if is_multipart:
|
| 563 |
# access form fields and files
|
| 564 |
form = request.form
|
|
|
|
| 570 |
if ui_raw:
|
| 571 |
user_inputs = json.loads(ui_raw)
|
| 572 |
else:
|
|
|
|
| 573 |
user_inputs = {}
|
| 574 |
except Exception:
|
| 575 |
user_inputs = {}
|
| 576 |
max_c = int(form.get("max_candidates") or 6)
|
| 577 |
+
|
| 578 |
w_raw = form.get("wardrobe_items")
|
| 579 |
if w_raw:
|
| 580 |
try:
|
| 581 |
wardrobe_items = json.loads(w_raw)
|
| 582 |
except Exception:
|
| 583 |
wardrobe_items = []
|
| 584 |
+
|
| 585 |
# audio file
|
| 586 |
audio_file = files.get("audio")
|
|
|
|
| 587 |
if audio_file:
|
| 588 |
try:
|
| 589 |
audio_bytes = audio_file.read()
|
|
|
|
| 601 |
log.exception("Invalid request payload: %s", e)
|
| 602 |
return jsonify({"error": "invalid request payload"}), 400
|
| 603 |
|
| 604 |
+
# normalize any incoming wardrobe_items (accept camelCase/snake_case)
|
| 605 |
+
try:
|
| 606 |
+
wardrobe_items = [normalize_wardrobe_item(it) for it in (wardrobe_items or [])]
|
| 607 |
+
except Exception:
|
| 608 |
+
wardrobe_items = []
|
| 609 |
+
|
| 610 |
# if wardrobe_items empty, attempt to fetch from Firestore for uid
|
| 611 |
if not wardrobe_items:
|
| 612 |
try:
|