HelloWorld0204 commited on
Commit
ca9045a
·
verified ·
1 Parent(s): fea9d68

Upload 22 files

Browse files
Files changed (2) hide show
  1. README.md +5 -5
  2. app.py +51 -48
README.md CHANGED
@@ -80,10 +80,10 @@ Matching and cache:
80
  - `MATCHING_RESULT_CACHE_MAX` (default: `500`)
81
  - `MATCHING_RESULT_CACHE_TTL_SECONDS` (default: `86400`)
82
 
83
- Scraper and planner:
84
- - `SCRAPER_DEFAULT_STORE` (default: `nike`)
85
- - `KIMI_MODEL_ID` (default: `moonshotai/kimi-k2.5`)
86
- - `KIMI_MAX_TOKENS` (default: `800`)
87
 
88
  Database path:
89
  - `DB_PATH` (optional override)
@@ -180,4 +180,4 @@ curl -X POST "http://127.0.0.1:7860/classify" \
180
 
181
  Expected post-deploy health signal:
182
  - `hf_api_configured` should be `"True"` (primary model).
183
- - `nvidia_api_configured` should be `"True"` (fallback model).
 
80
  - `MATCHING_RESULT_CACHE_MAX` (default: `500`)
81
  - `MATCHING_RESULT_CACHE_TTL_SECONDS` (default: `86400`)
82
 
83
+ Scraper and planner:
84
+ - `SCRAPER_DEFAULT_STORE` (default: `nike`)
85
+ - `SCRAPER_PLANNER_MODEL_ID` (default: `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning`)
86
+ - `SCRAPER_PLANNER_MAX_TOKENS` (default: `800`)
87
 
88
  Database path:
89
  - `DB_PATH` (optional override)
 
180
 
181
  Expected post-deploy health signal:
182
  - `hf_api_configured` should be `"True"` (primary model).
183
+ - `nvidia_api_configured` should be `"True"` (fallback model).
app.py CHANGED
@@ -246,12 +246,15 @@ def _gap_suggestions(wardrobe: list[dict[str, Any]], occasion: str) -> list[dict
246
  return suggestions[:4]
247
 
248
 
249
- SCRAPER_OUTPUT_DIR = Path(__file__).resolve().parent / "scraped_json"
250
- SCRAPER_RUNTIME_RESULTS: dict[str, dict[str, Any]] = {}
251
- SCRAPER_RUNTIME_LOCK = threading.Lock()
252
- KIMI_MODEL_ID = os.getenv("KIMI_MODEL_ID", "moonshotai/kimi-k2.5")
253
- KIMI_MAX_TOKENS = int(os.getenv("KIMI_MAX_TOKENS", "800"))
254
- SCRAPER_DEFAULT_STORE = str(os.getenv("SCRAPER_DEFAULT_STORE", "nike")).strip().lower()
 
 
 
255
 
256
 
257
  def _save_scraper_json_payload(prefix: str, payload: dict[str, Any]) -> str:
@@ -405,8 +408,8 @@ def _run_text_inference_with_model(primary_model_id: str, prompt: str, max_token
405
  )
406
 
407
 
408
- def run_kimi_text_inference(prompt: str, max_tokens: int = KIMI_MAX_TOKENS) -> str:
409
- return _run_text_inference_with_model(KIMI_MODEL_ID, prompt, max_tokens)
410
 
411
 
412
  def _normalize_store_name(value: str | None) -> str:
@@ -454,7 +457,7 @@ def _build_store_search_urls_from_query(
454
  gender=gender,
455
  wardrobe_items=wardrobe_items,
456
  requested_category=requested_category,
457
- # URL generation should follow Kimi planner output + deterministic rules.
458
  # GPT OSS remains reserved for post-scrape cleanup only.
459
  completion_fn=None,
460
  )
@@ -631,8 +634,8 @@ def _recover_scraper_plan_from_text(
631
  "style_direction": planning_context.get("style_direction", "occasion-aligned"),
632
  "reference_item_ids": planning_context.get("reference_item_ids", []),
633
  "query": query,
634
- "reason": "Recovered Kimi planner output from semi-structured response.",
635
- "source": "kimi",
636
  }
637
 
638
 
@@ -1290,7 +1293,7 @@ def _fallback_scraper_plan(
1290
  }
1291
 
1292
 
1293
- def _generate_scraper_plan_with_kimi(
1294
  occasion: str,
1295
  gender: str,
1296
  preferences: str,
@@ -1299,7 +1302,7 @@ def _generate_scraper_plan_with_kimi(
1299
  filters: dict[str, Any],
1300
  max_products: int | None,
1301
  store: str,
1302
- strict_kimi: bool = False,
1303
  ) -> dict[str, Any]:
1304
  wardrobe_snapshot = _wardrobe_metadata_snapshot()
1305
  requested_target = _normalize_target_category(target_category)
@@ -1325,31 +1328,31 @@ def _generate_scraper_plan_with_kimi(
1325
  store=store,
1326
  )
1327
 
1328
- plan_source = "kimi"
1329
- plan_error: str | None = None
1330
- try:
1331
- model_text = run_kimi_text_inference(prompt, max_tokens=KIMI_MAX_TOKENS)
1332
  parsed = _recover_scraper_plan_from_text(
1333
  model_text=model_text,
1334
  planning_context=planning_context,
1335
  occasion=occasion,
1336
  gender=gender,
1337
- )
1338
- if not isinstance(parsed, dict) or not parsed:
1339
- raise NvidiaPayloadError("Kimi scraper planner returned empty or invalid JSON payload.")
1340
- except Exception as exc:
1341
- if strict_kimi:
1342
- raise NvidiaPayloadError(f"Kimi planner unavailable: {exc}") from exc
1343
- plan_source = "fallback"
1344
- plan_error = str(exc)
1345
- parsed = _fallback_scraper_plan(
1346
  planning_context=planning_context,
1347
  occasion=occasion,
1348
- gender=gender,
1349
- reason=(
1350
- "Live Kimi query planning was unavailable, so a deterministic fallback planner was used."
1351
- ),
1352
- )
1353
 
1354
  resolved_target = _normalize_target_category(
1355
  parsed.get("target_category") or planning_context.get("resolved_target_category") or requested_target
@@ -1410,7 +1413,7 @@ def _generate_scraper_plan_with_kimi(
1410
  )
1411
 
1412
  wardrobe_grounding = str(parsed.get("wardrobe_grounding") or default_grounding)
1413
- reason = str(parsed.get("reason") or "Kimi generated a wardrobe-aware shopping query.")
1414
 
1415
  recommendation = ScraperRecommendation(
1416
  color=color,
@@ -1606,7 +1609,7 @@ def _build_shopping_suggestions_from_scraper(
1606
  ]
1607
  )
1608
 
1609
- runtime_payload = _generate_scraper_plan_with_kimi(
1610
  occasion=occasion,
1611
  gender=gender_preference,
1612
  preferences=preferences,
@@ -1633,7 +1636,7 @@ def _build_shopping_suggestions_from_scraper(
1633
  "image_url": str(product.get("image_url") or ""),
1634
  "store": str(runtime_payload.get("store") or store or "nike").title(),
1635
  "match_score": max(65, 95 - index * 4),
1636
- "reason": str(product.get("reason") or query_plan.get("reason") or "Kimi generated a wardrobe-aware shopping query."),
1637
  "product_category": str(query_plan.get("category") or "shopping"),
1638
  "color": str(query_plan.get("color") or "black"),
1639
  "pattern": "solid",
@@ -1719,11 +1722,11 @@ NVIDIA_MAX_RETRIES = int(os.getenv("NVIDIA_MAX_RETRIES", "3"))
1719
  NVIDIA_RETRY_BACKOFF_SECONDS = float(os.getenv("NVIDIA_RETRY_BACKOFF_SECONDS", "0.8"))
1720
  NVIDIA_ENABLE_THINKING = str(os.getenv("NVIDIA_ENABLE_THINKING", "false")).strip().lower() == "true"
1721
  NVIDIA_IMAGE_MAX_DIM = int(os.getenv("NVIDIA_IMAGE_MAX_DIM", "1400"))
1722
- NVIDIA_FALLBACK_MODEL_IDS = [
1723
- model_id.strip()
1724
- for model_id in os.getenv("NVIDIA_FALLBACK_MODEL_IDS", "moonshotai/kimi-k2.5").split(",")
1725
- if model_id.strip()
1726
- ]
1727
  NVIDIA_API_KEY_MISSING_DETAIL = "NVIDIA_API_KEY is not configured on this Space."
1728
 
1729
 
@@ -1778,7 +1781,7 @@ OUTFIT_ANCHOR_MIN_SCORE = int(os.getenv("OUTFIT_ANCHOR_MIN_SCORE", "45"))
1778
  OUTFIT_TEXT_PRESELECT_ENABLED = str(os.getenv("OUTFIT_TEXT_PRESELECT_ENABLED", "false")).strip().lower() == "true"
1779
  OUTFIT_TEXT_SELECTOR_MAX_TOKENS = int(os.getenv("OUTFIT_TEXT_SELECTOR_MAX_TOKENS", "400"))
1780
  OUTFIT_AI_MAX_TOKENS = int(os.getenv("OUTFIT_AI_MAX_TOKENS", "1200"))
1781
- OUTFIT_TEXT_SELECTOR_NAME = "kimi-text-preselect-v1"
1782
  OUTFIT_AI_SCORER_NAME = "ai-grid-v1"
1783
  OUTFIT_FALLBACK_SCORER_NAME = "fallback-current-v1"
1784
  OUTFIT_GRID_SCORING_PROMPT_TEMPLATE = """You are an expert multimodal outfit matching engine.
@@ -3050,7 +3053,7 @@ def health() -> dict[str, str]:
3050
  "nvidia_api_configured": str(bool(_nvidia_api_key())),
3051
  "nvidia_invoke_url": NVIDIA_INVOKE_URL,
3052
  "engine_version": "scoring-v2",
3053
- "outfit_matching_provider": "kimi",
3054
  }
3055
 
3056
 
@@ -3187,7 +3190,7 @@ def scraper_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> d
3187
  raise HTTPException(status_code=400, detail="max_products must be at least 1")
3188
 
3189
  try:
3190
- return _generate_scraper_plan_with_kimi(
3191
  occasion=occasion,
3192
  gender=gender,
3193
  preferences=preferences,
@@ -3196,7 +3199,7 @@ def scraper_recommend(payload: dict[str, Any] = Body(default_factory=dict)) -> d
3196
  filters=filters,
3197
  max_products=max_products,
3198
  store=store,
3199
- strict_kimi=True,
3200
  )
3201
  except NvidiaGatewayError as exc:
3202
  raise HTTPException(status_code=502, detail=str(exc)) from exc
@@ -3256,7 +3259,7 @@ def scraper_page() -> Response:
3256
  <div class="hero">
3257
  <div class="card">
3258
  <h1>Wardrobe Assistant Child</h1>
3259
- <p>Kimi reads wardrobe metadata, builds a context-aware shopping query, and returns matching products with links, names, prices, and images.</p>
3260
  <div class="grid">
3261
  <div>
3262
  <label for="occasion">Occasion</label>
@@ -3286,18 +3289,18 @@ def scraper_page() -> Response:
3286
  </div>
3287
  <label for="preferences">Other Preferences</label>
3288
  <textarea id="preferences" placeholder="Example: formal office look, breathable fabric, neutral tones, regular fit, avoid oversized silhouettes"></textarea>
3289
- <button id="runBtn">Generate Kimi Query and Scrape</button>
3290
  <div id="status" class="status"></div>
3291
  </div>
3292
  <div class="card">
3293
  <h2 style="margin-top:0;">Wardrobe Metadata Snapshot</h2>
3294
- <p class="muted">Current items loaded from the database are used by Kimi to shape the shopping query.</p>
3295
  <pre id="wardrobeSnapshot">{wardrobe_json}</pre>
3296
  </div>
3297
  </div>
3298
 
3299
  <div class="card" style="margin-top:16px;">
3300
- <h2 style="margin-top:0;">Kimi Query Plan</h2>
3301
  <pre id="queryPlan">Run the search to generate a wardrobe-aware query.</pre>
3302
  </div>
3303
 
@@ -3341,7 +3344,7 @@ def scraper_page() -> Response:
3341
  }}
3342
 
3343
  runBtn.addEventListener('click', async () => {{
3344
- statusEl.textContent = 'Generating query with Kimi and scraping products...';
3345
  productsEl.innerHTML = '';
3346
  try {{
3347
  const payload = {{
 
246
  return suggestions[:4]
247
 
248
 
249
+ SCRAPER_OUTPUT_DIR = Path(__file__).resolve().parent / "scraped_json"
250
+ SCRAPER_RUNTIME_RESULTS: dict[str, dict[str, Any]] = {}
251
+ SCRAPER_RUNTIME_LOCK = threading.Lock()
252
+ SCRAPER_PLANNER_MODEL_ID = os.getenv(
253
+ "SCRAPER_PLANNER_MODEL_ID",
254
+ "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
255
+ )
256
+ SCRAPER_PLANNER_MAX_TOKENS = int(os.getenv("SCRAPER_PLANNER_MAX_TOKENS", "800"))
257
+ SCRAPER_DEFAULT_STORE = str(os.getenv("SCRAPER_DEFAULT_STORE", "nike")).strip().lower()
258
 
259
 
260
  def _save_scraper_json_payload(prefix: str, payload: dict[str, Any]) -> str:
 
408
  )
409
 
410
 
411
+ def run_scraper_planner_text_inference(prompt: str, max_tokens: int = SCRAPER_PLANNER_MAX_TOKENS) -> str:
412
+ return _run_text_inference_with_model(SCRAPER_PLANNER_MODEL_ID, prompt, max_tokens)
413
 
414
 
415
  def _normalize_store_name(value: str | None) -> str:
 
457
  gender=gender,
458
  wardrobe_items=wardrobe_items,
459
  requested_category=requested_category,
460
+ # URL generation should follow planner output + deterministic rules.
461
  # GPT OSS remains reserved for post-scrape cleanup only.
462
  completion_fn=None,
463
  )
 
634
  "style_direction": planning_context.get("style_direction", "occasion-aligned"),
635
  "reference_item_ids": planning_context.get("reference_item_ids", []),
636
  "query": query,
637
+ "reason": "Recovered Nemotron planner output from semi-structured response.",
638
+ "source": "nemotron",
639
  }
640
 
641
 
 
1293
  }
1294
 
1295
 
1296
+ def _generate_scraper_plan_with_nemotron(
1297
  occasion: str,
1298
  gender: str,
1299
  preferences: str,
 
1302
  filters: dict[str, Any],
1303
  max_products: int | None,
1304
  store: str,
1305
+ strict_nemotron: bool = False,
1306
  ) -> dict[str, Any]:
1307
  wardrobe_snapshot = _wardrobe_metadata_snapshot()
1308
  requested_target = _normalize_target_category(target_category)
 
1328
  store=store,
1329
  )
1330
 
1331
+ plan_source = "nemotron"
1332
+ plan_error: str | None = None
1333
+ try:
1334
+ model_text = run_scraper_planner_text_inference(prompt, max_tokens=SCRAPER_PLANNER_MAX_TOKENS)
1335
  parsed = _recover_scraper_plan_from_text(
1336
  model_text=model_text,
1337
  planning_context=planning_context,
1338
  occasion=occasion,
1339
  gender=gender,
1340
+ )
1341
+ if not isinstance(parsed, dict) or not parsed:
1342
+ raise NvidiaPayloadError("Nemotron scraper planner returned empty or invalid JSON payload.")
1343
+ except Exception as exc:
1344
+ if strict_nemotron:
1345
+ raise NvidiaPayloadError(f"Nemotron planner unavailable: {exc}") from exc
1346
+ plan_source = "fallback"
1347
+ plan_error = str(exc)
1348
+ parsed = _fallback_scraper_plan(
1349
  planning_context=planning_context,
1350
  occasion=occasion,
1351
+ gender=gender,
1352
+ reason=(
1353
+ "Live Nemotron query planning was unavailable, so a deterministic fallback planner was used."
1354
+ ),
1355
+ )
1356
 
1357
  resolved_target = _normalize_target_category(
1358
  parsed.get("target_category") or planning_context.get("resolved_target_category") or requested_target
 
1413
  )
1414
 
1415
  wardrobe_grounding = str(parsed.get("wardrobe_grounding") or default_grounding)
1416
+ reason = str(parsed.get("reason") or "Nemotron generated a wardrobe-aware shopping query.")
1417
 
1418
  recommendation = ScraperRecommendation(
1419
  color=color,
 
1609
  ]
1610
  )
1611
 
1612
+ runtime_payload = _generate_scraper_plan_with_nemotron(
1613
  occasion=occasion,
1614
  gender=gender_preference,
1615
  preferences=preferences,
 
1636
  "image_url": str(product.get("image_url") or ""),
1637
  "store": str(runtime_payload.get("store") or store or "nike").title(),
1638
  "match_score": max(65, 95 - index * 4),
1639
+ "reason": str(product.get("reason") or query_plan.get("reason") or "Nemotron generated a wardrobe-aware shopping query."),
1640
  "product_category": str(query_plan.get("category") or "shopping"),
1641
  "color": str(query_plan.get("color") or "black"),
1642
  "pattern": "solid",
 
1722
  NVIDIA_RETRY_BACKOFF_SECONDS = float(os.getenv("NVIDIA_RETRY_BACKOFF_SECONDS", "0.8"))
1723
  NVIDIA_ENABLE_THINKING = str(os.getenv("NVIDIA_ENABLE_THINKING", "false")).strip().lower() == "true"
1724
  NVIDIA_IMAGE_MAX_DIM = int(os.getenv("NVIDIA_IMAGE_MAX_DIM", "1400"))
1725
+ NVIDIA_FALLBACK_MODEL_IDS = [
1726
+ model_id.strip()
1727
+ for model_id in os.getenv("NVIDIA_FALLBACK_MODEL_IDS", "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning").split(",")
1728
+ if model_id.strip()
1729
+ ]
1730
  NVIDIA_API_KEY_MISSING_DETAIL = "NVIDIA_API_KEY is not configured on this Space."
1731
 
1732
 
 
1781
  OUTFIT_TEXT_PRESELECT_ENABLED = str(os.getenv("OUTFIT_TEXT_PRESELECT_ENABLED", "false")).strip().lower() == "true"
1782
  OUTFIT_TEXT_SELECTOR_MAX_TOKENS = int(os.getenv("OUTFIT_TEXT_SELECTOR_MAX_TOKENS", "400"))
1783
  OUTFIT_AI_MAX_TOKENS = int(os.getenv("OUTFIT_AI_MAX_TOKENS", "1200"))
1784
+ OUTFIT_TEXT_SELECTOR_NAME = "nemotron-text-preselect-v1"
1785
  OUTFIT_AI_SCORER_NAME = "ai-grid-v1"
1786
  OUTFIT_FALLBACK_SCORER_NAME = "fallback-current-v1"
1787
  OUTFIT_GRID_SCORING_PROMPT_TEMPLATE = """You are an expert multimodal outfit matching engine.
 
3053
  "nvidia_api_configured": str(bool(_nvidia_api_key())),
3054
  "nvidia_invoke_url": NVIDIA_INVOKE_URL,
3055
  "engine_version": "scoring-v2",
3056
+ "outfit_matching_provider": "nemotron",
3057
  }
3058
 
3059
 
 
3190
  raise HTTPException(status_code=400, detail="max_products must be at least 1")
3191
 
3192
  try:
3193
+ return _generate_scraper_plan_with_nemotron(
3194
  occasion=occasion,
3195
  gender=gender,
3196
  preferences=preferences,
 
3199
  filters=filters,
3200
  max_products=max_products,
3201
  store=store,
3202
+ strict_nemotron=True,
3203
  )
3204
  except NvidiaGatewayError as exc:
3205
  raise HTTPException(status_code=502, detail=str(exc)) from exc
 
3259
  <div class="hero">
3260
  <div class="card">
3261
  <h1>Wardrobe Assistant Child</h1>
3262
+ <p>Nemotron reads wardrobe metadata, builds a context-aware shopping query, and returns matching products with links, names, prices, and images.</p>
3263
  <div class="grid">
3264
  <div>
3265
  <label for="occasion">Occasion</label>
 
3289
  </div>
3290
  <label for="preferences">Other Preferences</label>
3291
  <textarea id="preferences" placeholder="Example: formal office look, breathable fabric, neutral tones, regular fit, avoid oversized silhouettes"></textarea>
3292
+ <button id="runBtn">Generate Nemotron Query and Scrape</button>
3293
  <div id="status" class="status"></div>
3294
  </div>
3295
  <div class="card">
3296
  <h2 style="margin-top:0;">Wardrobe Metadata Snapshot</h2>
3297
+ <p class="muted">Current items loaded from the database are used by Nemotron to shape the shopping query.</p>
3298
  <pre id="wardrobeSnapshot">{wardrobe_json}</pre>
3299
  </div>
3300
  </div>
3301
 
3302
  <div class="card" style="margin-top:16px;">
3303
+ <h2 style="margin-top:0;">Nemotron Query Plan</h2>
3304
  <pre id="queryPlan">Run the search to generate a wardrobe-aware query.</pre>
3305
  </div>
3306
 
 
3344
  }}
3345
 
3346
  runBtn.addEventListener('click', async () => {{
3347
+ statusEl.textContent = 'Generating query with Nemotron and scraping products...';
3348
  productsEl.innerHTML = '';
3349
  try {{
3350
  const payload = {{