"""Rule-based catalog recommendation (spec section 7). Style-tag overlap + light category context. No ML, no training. Cosine-style Jaccard is included as a tie-breaker but the dominant signal is plain tag match. """ from __future__ import annotations import functools import json from ..config import settings # A few detected COCO labels -> catalog categories, for gentle room context. LABEL_TO_CATEGORY = { "bed": "bed", "couch": "sofa", "chair": "chair", "dining table": "table", "tv": "storage", "potted plant": "decor", } @functools.lru_cache(maxsize=1) def load_catalog() -> list[dict]: return json.loads(settings.CATALOG_JSON.read_text(encoding="utf-8")) def filter_and_rank( category: str | None = None, styles: list[str] | None = None, detected_labels: list[str] | None = None, ) -> list[dict]: """Return catalog items, optionally filtered, ranked recommendation-first.""" items = load_catalog() wanted = {s.lower() for s in (styles or [])} if category: items = [i for i in items if i["category"] == category] context_cats = { LABEL_TO_CATEGORY[lbl] for lbl in (detected_labels or []) if lbl in LABEL_TO_CATEGORY } ranked: list[dict] = [] for it in items: tags = {t.lower() for t in it["style_tags"]} overlap = len(tags & wanted) jaccard = overlap / (len(tags | wanted) or 1) score = overlap + jaccard # exact tag matches dominate if it["category"] in context_cats: score += 0.5 ranked.append({**it, "score": round(float(score), 3)}) ranked.sort(key=lambda x: (-x["score"], x["price"])) return ranked