Spaces:
Sleeping
Sleeping
Upload 22 files
Browse files- IMPLEMENTATION_README.md +13 -0
- 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 |
-
|
| 3150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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>
|