HelloWorld0204 commited on
Commit
cc3be8b
·
verified ·
1 Parent(s): d6acfb1

Upload 22 files

Browse files
Files changed (2) hide show
  1. IMPLEMENTATION_README.md +13 -0
  2. app.py +113 -12
IMPLEMENTATION_README.md CHANGED
@@ -568,6 +568,11 @@ The main entry points are:
568
  - `score_pair_full(top, bottom, occasion, other=None)`
569
  - `recommend_outfits(tops, bottoms, occasion, others, locked_top, locked_bottom, locked_other)`
570
 
 
 
 
 
 
571
  ### Weights
572
 
573
  `WEIGHTS`:
@@ -697,6 +702,14 @@ This lets accessories, footwear, or outerwear influence the outfit without overp
697
 
698
  `build_reason()` creates user-facing text from the strongest and weakest scoring dimensions. `build_tip()` gives the styling advice shown in the UI.
699
 
 
 
 
 
 
 
 
 
700
  ## 14. Standalone Others
701
 
702
  The code treats `others` specially because many garments are complete outfits by themselves:
 
568
  - `score_pair_full(top, bottom, occasion, other=None)`
569
  - `recommend_outfits(tops, bottoms, occasion, others, locked_top, locked_bottom, locked_other)`
570
 
571
+ Runtime output details:
572
+
573
+ - `score_pair_full()` returns `engine_version: scoring-v2`.
574
+ - `recommend_outfits()` returns up to `TOP_K = 6` combinations.
575
+
576
  ### Weights
577
 
578
  `WEIGHTS`:
 
702
 
703
  `build_reason()` creates user-facing text from the strongest and weakest scoring dimensions. `build_tip()` gives the styling advice shown in the UI.
704
 
705
+ ### Diversity Penalty
706
+
707
+ `recommend_outfits()` applies `_apply_diversity_penalty()` before final ranking:
708
+
709
+ - Near-duplicate looks (same top color, bottom color, and other-item color) are penalized.
710
+ - Penalty is `-10` per prior similar outfit.
711
+ - Outfits are re-sorted after penalty and then truncated to top `TOP_K`.
712
+
713
  ## 14. Standalone Others
714
 
715
  The code treats `others` specially because many garments are complete outfits by themselves:
app.py CHANGED
@@ -3118,7 +3118,7 @@ def product_urls(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[s
3118
 
3119
  @app.post("/suggestions")
3120
  @app.post("/api/suggestions")
3121
- def suggestions(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
3122
  occasion = str(payload.get("occasion") or "casual")
3123
  target_category = str(payload.get("target_category") or payload.get("targetCategory") or "both")
3124
  gender_preference = str(payload.get("gender_preference") or payload.get("genderPreference") or "any")
@@ -3142,12 +3142,99 @@ def suggestions(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[st
3142
  raise HTTPException(status_code=502, detail=str(exc)) from exc
3143
  except NvidiaPayloadError as exc:
3144
  raise HTTPException(status_code=502, detail=str(exc)) from exc
3145
- except requests.RequestException as exc:
3146
- raise HTTPException(status_code=502, detail=f"Failed to fetch {store.title()} pages: {exc}") from exc
3147
-
3148
-
3149
- @app.post("/scraper/recommend")
3150
- def scraper_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3151
  user_prompt = str(payload.get("user_prompt") or payload.get("prompt") or "").strip()
3152
  inferred = _infer_structured_request_from_prompt(user_prompt)
3153
  inferred_target_category = _normalize_target_category(inferred.get("target_category"))
@@ -3194,10 +3281,24 @@ def scraper_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> d
3194
  max_products = int(max_products_raw) if max_products_raw not in {None, ""} else None
3195
  store = _normalize_store_name(str(payload.get("store") or SCRAPER_DEFAULT_STORE or "nike"))
3196
 
3197
- if isinstance(max_products, int) and max_products < 1:
3198
- raise HTTPException(status_code=400, detail="max_products must be at least 1")
3199
-
3200
- # Check cache first for faster repeat queries
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3201
  cache_key = _scraper_cache_key(user_prompt, store, gender, target_category)
3202
  with SCRAPER_QUERY_CACHE_LOCK:
3203
  if cache_key in SCRAPER_QUERY_CACHE:
@@ -3316,7 +3417,7 @@ def scraper_page() -> Response:
3316
  </div>
3317
  </div>
3318
  <label for="preferences">Other Preferences</label>
3319
- <textarea id="preferences" placeholder="Example: formal office look, breathable fabric, neutral tones, regular fit, avoid oversized silhouettes"></textarea>
3320
  <button id="runBtn">Generate Kimi Query and Scrape</button>
3321
  <div id="status" class="status"></div>
3322
  </div>
 
3118
 
3119
  @app.post("/suggestions")
3120
  @app.post("/api/suggestions")
3121
+ def suggestions(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
3122
  occasion = str(payload.get("occasion") or "casual")
3123
  target_category = str(payload.get("target_category") or payload.get("targetCategory") or "both")
3124
  gender_preference = str(payload.get("gender_preference") or payload.get("genderPreference") or "any")
 
3142
  raise HTTPException(status_code=502, detail=str(exc)) from exc
3143
  except NvidiaPayloadError as exc:
3144
  raise HTTPException(status_code=502, detail=str(exc)) from exc
3145
+ except requests.RequestException as exc:
3146
+ raise HTTPException(status_code=502, detail=f"Failed to fetch {store.title()} pages: {exc}") from exc
3147
+
3148
+
3149
+ def _build_static_scraper_result(
3150
+ static_plan: dict[str, Any],
3151
+ *,
3152
+ occasion: str,
3153
+ gender: str,
3154
+ preferences: str,
3155
+ store: str,
3156
+ max_products: int | None,
3157
+ ) -> dict[str, Any]:
3158
+ query = str(static_plan.get("query") or "").strip()
3159
+ color = str(static_plan.get("color") or "").strip()
3160
+ category = str(static_plan.get("category") or "").strip()
3161
+ if not query:
3162
+ query = " ".join(part for part in [gender, color, category] if str(part or "").strip()).strip()
3163
+ if not query:
3164
+ raise HTTPException(status_code=400, detail="static_query_plan.query is required")
3165
+
3166
+ plan_occasion = str(static_plan.get("occasion") or occasion or "casual").strip() or "casual"
3167
+ plan_gender = _normalize_scraper_gender(gender) or _normalize_scraper_gender(static_plan.get("gender")) or None
3168
+ limit = max_products if isinstance(max_products, int) and max_products > 0 else 12
3169
+ search_urls = _build_store_search_urls_from_query(query, store=store, gender=plan_gender)
3170
+
3171
+ products: list[dict[str, Any]] = []
3172
+ seen_links: set[str] = set()
3173
+ errors: list[str] = []
3174
+ for search_url in search_urls:
3175
+ try:
3176
+ for product in _extract_store_product_summaries(search_url, store=store):
3177
+ item_link = str(product.get("item_link") or "").strip()
3178
+ if not item_link or item_link in seen_links:
3179
+ continue
3180
+ seen_links.add(item_link)
3181
+ products.append(product)
3182
+ if len(products) >= limit:
3183
+ break
3184
+ except requests.RequestException as exc:
3185
+ errors.append(str(exc))
3186
+ if len(products) >= limit:
3187
+ break
3188
+
3189
+ query_plan_payload = {
3190
+ "target_category": static_plan.get("target_category") or _normalize_target_category(static_plan.get("targetCategory")),
3191
+ "color": color or "neutral",
3192
+ "category": category or "mixed",
3193
+ "gender": plan_gender,
3194
+ "style_direction": str(static_plan.get("style_direction") or "direct-static").strip() or "direct-static",
3195
+ "occasion_bucket": _occasion_bucket(plan_occasion),
3196
+ "reference_item_ids": [],
3197
+ "query": query,
3198
+ "final_query": query,
3199
+ "wardrobe_grounding": "Static example query selected from the shopping suggestions page.",
3200
+ "reason": "Used a predefined query plan and URL builder without model planning.",
3201
+ "source": "static",
3202
+ }
3203
+
3204
+ response_payload: dict[str, Any] = {
3205
+ "runtime_id": str(uuid.uuid4()),
3206
+ "created_at": _now_iso(),
3207
+ "store": store,
3208
+ "occasion": plan_occasion,
3209
+ "gender": plan_gender or gender or "",
3210
+ "preferences": preferences,
3211
+ "wardrobe_snapshot": _wardrobe_metadata_snapshot(limit=12),
3212
+ "query_plan": query_plan_payload,
3213
+ "search_urls": search_urls,
3214
+ "product_urls": [item["item_link"] for item in products if item.get("item_link")],
3215
+ "products": products,
3216
+ "count": len(products),
3217
+ "intermediate_steps": [
3218
+ {
3219
+ "step": "static_query_plan",
3220
+ "query": query,
3221
+ "url_count": len(search_urls),
3222
+ "new_products": len(products),
3223
+ "total_products": len(products),
3224
+ "errors": errors,
3225
+ "message": "Predefined webpage query used; model planner skipped.",
3226
+ }
3227
+ ],
3228
+ "plan_source": "static",
3229
+ "plan_error": None,
3230
+ "scrape_error": "; ".join(errors) if errors and not products else None,
3231
+ }
3232
+ response_payload["saved_json_path"] = _save_scraper_json_payload("product_urls", response_payload)
3233
+ return _store_scraper_runtime_result(response_payload)
3234
+
3235
+
3236
+ @app.post("/scraper/recommend")
3237
+ def scraper_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> dict[str, Any]:
3238
  user_prompt = str(payload.get("user_prompt") or payload.get("prompt") or "").strip()
3239
  inferred = _infer_structured_request_from_prompt(user_prompt)
3240
  inferred_target_category = _normalize_target_category(inferred.get("target_category"))
 
3281
  max_products = int(max_products_raw) if max_products_raw not in {None, ""} else None
3282
  store = _normalize_store_name(str(payload.get("store") or SCRAPER_DEFAULT_STORE or "nike"))
3283
 
3284
+ if isinstance(max_products, int) and max_products < 1:
3285
+ raise HTTPException(status_code=400, detail="max_products must be at least 1")
3286
+
3287
+ static_plan = payload.get("static_query_plan") or payload.get("staticQueryPlan")
3288
+ if isinstance(static_plan, dict):
3289
+ try:
3290
+ return _build_static_scraper_result(
3291
+ static_plan,
3292
+ occasion=occasion,
3293
+ gender=gender,
3294
+ preferences=preferences,
3295
+ store=store,
3296
+ max_products=max_products,
3297
+ )
3298
+ except requests.RequestException as exc:
3299
+ raise HTTPException(status_code=502, detail=f"Failed to fetch {store.title()} pages: {exc}") from exc
3300
+
3301
+ # Check cache first for faster repeat queries
3302
  cache_key = _scraper_cache_key(user_prompt, store, gender, target_category)
3303
  with SCRAPER_QUERY_CACHE_LOCK:
3304
  if cache_key in SCRAPER_QUERY_CACHE:
 
3417
  </div>
3418
  </div>
3419
  <label for="preferences">Other Preferences</label>
3420
+ <textarea id="preferences" placeholder="Example: Category: shirt. Color: navy. Occasion: formal office. Style: structured minimal. Avoid: oversized."></textarea>
3421
  <button id="runBtn">Generate Kimi Query and Scrape</button>
3422
  <div id="status" class="status"></div>
3423
  </div>