Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -13,7 +13,7 @@ from sklearn.neighbors import NearestNeighbors
|
|
| 13 |
import gradio as gr
|
| 14 |
|
| 15 |
# ============= Config =============
|
| 16 |
-
USE_EMBEDDINGS = False
|
| 17 |
MAX_ROWS = int(os.getenv("MAX_ROWS", "5000"))
|
| 18 |
DEFAULT_OCCASIONS = "birthday, thank_you, housewarming"
|
| 19 |
|
|
@@ -43,42 +43,31 @@ def _to_price_usd(x):
|
|
| 43 |
except: return np.nan
|
| 44 |
|
| 45 |
def _infer_age_from_category(cat: str) -> str:
|
| 46 |
-
"""Very simple heuristic: map Amazon categories to an age_range tag."""
|
| 47 |
s = (cat or "").lower()
|
| 48 |
-
if any(k in s for k in ["baby", "toddler", "infant"]):
|
| 49 |
-
|
| 50 |
-
if
|
| 51 |
-
return "kids"
|
| 52 |
-
if any(k in s for k in ["teen", "ya", "young adult"]):
|
| 53 |
-
return "teens"
|
| 54 |
-
# otherwise
|
| 55 |
return "any"
|
| 56 |
|
| 57 |
def map_amazon_to_schema(df_raw: pd.DataFrame) -> pd.DataFrame:
|
| 58 |
cols = {c.lower().strip(): c for c in df_raw.columns}
|
| 59 |
get = lambda key: df_raw.get(cols.get(key, ""), "")
|
| 60 |
-
|
| 61 |
out = pd.DataFrame({
|
| 62 |
"name": get("product name"),
|
| 63 |
"short_desc": get("description"),
|
| 64 |
"tags": get("category"),
|
| 65 |
"price_usd": get("selling price").map(_to_price_usd) if "selling price" in cols else np.nan,
|
| 66 |
-
"age_range": "",
|
| 67 |
"gender_tags": "any",
|
| 68 |
"occasion_tags": DEFAULT_OCCASIONS,
|
| 69 |
"persona_fit": get("category"),
|
| 70 |
"image_url": get("image") if "image" in cols else "",
|
| 71 |
})
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
out["name"] = out["name"].astype(str).str.strip().str.slice(0, 120)
|
| 75 |
-
out["short_desc"] = out["short_desc"].astype(str).str.strip().str.slice(0, 400)
|
| 76 |
out["tags"] = out["tags"].astype(str).str.replace("|", ", ").str.lower()
|
| 77 |
out["persona_fit"] = out["persona_fit"].astype(str).str.lower()
|
| 78 |
-
|
| 79 |
-
# age inference
|
| 80 |
out["age_range"] = out["tags"].map(_infer_age_from_category).fillna("any")
|
| 81 |
-
|
| 82 |
return out
|
| 83 |
|
| 84 |
def build_doc(row: pd.Series) -> str:
|
|
@@ -97,7 +86,6 @@ def load_catalog() -> pd.DataFrame:
|
|
| 97 |
ds = load_dataset("ckandemir/amazon-products", split="train")
|
| 98 |
raw = ds.to_pandas()
|
| 99 |
except Exception:
|
| 100 |
-
# tiny fallback so the app doesn't crash offline
|
| 101 |
raw = pd.DataFrame({
|
| 102 |
"Product Name": ["Wireless Earbuds", "Coffee Sampler", "Strategy Board Game"],
|
| 103 |
"Description": [
|
|
@@ -144,11 +132,11 @@ def filter_business(df: pd.DataFrame, budget_min=None, budget_max=None,
|
|
| 144 |
if occasion:
|
| 145 |
m &= _contains_ci(df["occasion_tags"], occasion)
|
| 146 |
if age_range and age_range != "any":
|
| 147 |
-
# keep items explicitly tagged as this age OR 'any'
|
| 148 |
m &= (df["age_range"].fillna("any").isin([age_range, "any"]))
|
| 149 |
return df[m]
|
| 150 |
|
| 151 |
def recommend_topk(profile: Dict, k: int=3) -> pd.DataFrame:
|
|
|
|
| 152 |
q = profile_to_query(profile)
|
| 153 |
q_vec = _vectorizer.transform([q])
|
| 154 |
|
|
@@ -162,24 +150,37 @@ def recommend_topk(profile: Dict, k: int=3) -> pd.DataFrame:
|
|
| 162 |
if df_f.empty:
|
| 163 |
df_f = CATALOG
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
| 168 |
d = dists[0]
|
| 169 |
-
order = np.argsort(d)
|
| 170 |
-
cand_idx = cand_idx[order]
|
| 171 |
-
d = d[order]
|
| 172 |
-
|
| 173 |
seen, picks = set(), []
|
| 174 |
-
for
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
| 177 |
seen.add(nm)
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
return res[["name","short_desc","price_usd","occasion_tags","persona_fit","age_range","image_url","similarity"]]
|
| 184 |
|
| 185 |
# ============= Synthetic item + message =============
|
|
@@ -200,15 +201,12 @@ def generate_item(profile: Dict) -> Dict:
|
|
| 200 |
else:
|
| 201 |
base_name = f"{core.title()} starter bundle"
|
| 202 |
base_desc = f"A curated set to kickstart their {core} passion."
|
| 203 |
-
|
| 204 |
-
# small age tweak
|
| 205 |
if age == "kids":
|
| 206 |
base_desc += " Suitable for kids with safe, age-appropriate materials."
|
| 207 |
elif age == "teens":
|
| 208 |
base_desc += " Trendy pick that suits young enthusiasts."
|
| 209 |
elif age == "senior":
|
| 210 |
base_desc += " Comfortable and easy to use."
|
| 211 |
-
|
| 212 |
price = float(np.clip(float(budget), 10, 250))
|
| 213 |
return {
|
| 214 |
"name": f"{base_name} ({occasion})",
|
|
@@ -230,26 +228,29 @@ def generate_message(profile: Dict) -> str:
|
|
| 230 |
|
| 231 |
# ============= Gradio UI (GIfty) =============
|
| 232 |
EXAMPLES = [
|
| 233 |
-
[["reading","travel","aesthetic"], "birthday", 20, 60, "Noa", "adult", "warm and friendly"],
|
| 234 |
-
[["coffee","home","practical"], "housewarming", 25, 45, "Daniel", "adult", "warm"],
|
| 235 |
-
[["tech","
|
| 236 |
]
|
| 237 |
|
| 238 |
-
def ui_predict(interests_list: List[str], occasion: str,
|
| 239 |
-
recipient_name: str, age_label: str, tone: str):
|
| 240 |
try:
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
| 244 |
budget_min, budget_max = budget_max, budget_min
|
| 245 |
|
|
|
|
| 246 |
profile = {
|
| 247 |
"recipient_name": recipient_name or "Friend",
|
| 248 |
"interests": interests_list or [],
|
| 249 |
"occasion": occasion or "birthday",
|
| 250 |
-
"budget_min":
|
| 251 |
-
"budget_max":
|
| 252 |
-
"budget_usd":
|
| 253 |
"age_range": age_range,
|
| 254 |
"tone": tone or "warm and friendly",
|
| 255 |
}
|
|
@@ -262,7 +263,6 @@ def ui_predict(interests_list: List[str], occasion: str, budget_min: float, budg
|
|
| 262 |
gen_md = f"**{gen['name']}**\n\n{gen['short_desc']}\n\n~${gen['price_usd']:.0f}"
|
| 263 |
return top3_md, gen_md, msg
|
| 264 |
except Exception as e:
|
| 265 |
-
# Return error message in the UI instead of "nothing happens"
|
| 266 |
return f":warning: Error: {e}", "", ""
|
| 267 |
|
| 268 |
with gr.Blocks() as demo:
|
|
@@ -276,9 +276,10 @@ with gr.Blocks() as demo:
|
|
| 276 |
with gr.Row():
|
| 277 |
occasion = gr.Dropdown(label="Occasion", choices=OCCASION_OPTIONS, value="birthday")
|
| 278 |
age = gr.Dropdown(label="Age group", choices=list(AGE_OPTIONS.keys()), value="adult (18β64)")
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
|
|
|
| 282 |
with gr.Row():
|
| 283 |
recipient_name = gr.Textbox(label="Recipient name", value="Noa")
|
| 284 |
tone = gr.Textbox(label="Message tone", value="warm and friendly")
|
|
@@ -290,16 +291,15 @@ with gr.Blocks() as demo:
|
|
| 290 |
|
| 291 |
gr.Examples(
|
| 292 |
EXAMPLES,
|
| 293 |
-
[interests, occasion,
|
| 294 |
label="Quick examples",
|
| 295 |
)
|
| 296 |
|
| 297 |
go.click(
|
| 298 |
ui_predict,
|
| 299 |
-
[interests, occasion,
|
| 300 |
[out_top3, out_gen, out_msg]
|
| 301 |
)
|
| 302 |
|
| 303 |
-
# For Spaces
|
| 304 |
if __name__ == "__main__":
|
| 305 |
demo.launch()
|
|
|
|
| 13 |
import gradio as gr
|
| 14 |
|
| 15 |
# ============= Config =============
|
| 16 |
+
USE_EMBEDDINGS = False
|
| 17 |
MAX_ROWS = int(os.getenv("MAX_ROWS", "5000"))
|
| 18 |
DEFAULT_OCCASIONS = "birthday, thank_you, housewarming"
|
| 19 |
|
|
|
|
| 43 |
except: return np.nan
|
| 44 |
|
| 45 |
def _infer_age_from_category(cat: str) -> str:
|
|
|
|
| 46 |
s = (cat or "").lower()
|
| 47 |
+
if any(k in s for k in ["baby", "toddler", "infant"]): return "kids"
|
| 48 |
+
if "toys & games" in s or "board games" in s or "toy" in s: return "kids"
|
| 49 |
+
if any(k in s for k in ["teen", "ya", "young adult"]): return "teens"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
return "any"
|
| 51 |
|
| 52 |
def map_amazon_to_schema(df_raw: pd.DataFrame) -> pd.DataFrame:
|
| 53 |
cols = {c.lower().strip(): c for c in df_raw.columns}
|
| 54 |
get = lambda key: df_raw.get(cols.get(key, ""), "")
|
|
|
|
| 55 |
out = pd.DataFrame({
|
| 56 |
"name": get("product name"),
|
| 57 |
"short_desc": get("description"),
|
| 58 |
"tags": get("category"),
|
| 59 |
"price_usd": get("selling price").map(_to_price_usd) if "selling price" in cols else np.nan,
|
| 60 |
+
"age_range": "",
|
| 61 |
"gender_tags": "any",
|
| 62 |
"occasion_tags": DEFAULT_OCCASIONS,
|
| 63 |
"persona_fit": get("category"),
|
| 64 |
"image_url": get("image") if "image" in cols else "",
|
| 65 |
})
|
| 66 |
+
out["name"] = out["name"].astype(str).str.strip().str.slice(0,120)
|
| 67 |
+
out["short_desc"] = out["short_desc"].astype(str).str.strip().str.slice(0,400)
|
|
|
|
|
|
|
| 68 |
out["tags"] = out["tags"].astype(str).str.replace("|", ", ").str.lower()
|
| 69 |
out["persona_fit"] = out["persona_fit"].astype(str).str.lower()
|
|
|
|
|
|
|
| 70 |
out["age_range"] = out["tags"].map(_infer_age_from_category).fillna("any")
|
|
|
|
| 71 |
return out
|
| 72 |
|
| 73 |
def build_doc(row: pd.Series) -> str:
|
|
|
|
| 86 |
ds = load_dataset("ckandemir/amazon-products", split="train")
|
| 87 |
raw = ds.to_pandas()
|
| 88 |
except Exception:
|
|
|
|
| 89 |
raw = pd.DataFrame({
|
| 90 |
"Product Name": ["Wireless Earbuds", "Coffee Sampler", "Strategy Board Game"],
|
| 91 |
"Description": [
|
|
|
|
| 132 |
if occasion:
|
| 133 |
m &= _contains_ci(df["occasion_tags"], occasion)
|
| 134 |
if age_range and age_range != "any":
|
|
|
|
| 135 |
m &= (df["age_range"].fillna("any").isin([age_range, "any"]))
|
| 136 |
return df[m]
|
| 137 |
|
| 138 |
def recommend_topk(profile: Dict, k: int=3) -> pd.DataFrame:
|
| 139 |
+
"""Global kNN β filter to the business subset (fixes index mismatch)."""
|
| 140 |
q = profile_to_query(profile)
|
| 141 |
q_vec = _vectorizer.transform([q])
|
| 142 |
|
|
|
|
| 150 |
if df_f.empty:
|
| 151 |
df_f = CATALOG
|
| 152 |
|
| 153 |
+
# Search on the global index, then keep only rows inside df_f
|
| 154 |
+
n_cand = min(max(k*50, k), len(CATALOG))
|
| 155 |
+
dists, inds = _nn.kneighbors(q_vec, n_neighbors=n_cand)
|
| 156 |
+
cand_global = inds[0] # indices in CATALOG
|
| 157 |
d = dists[0]
|
| 158 |
+
order = np.argsort(d) # ascending distance
|
|
|
|
|
|
|
|
|
|
| 159 |
seen, picks = set(), []
|
| 160 |
+
for gi in cand_global[order]:
|
| 161 |
+
if gi not in df_f.index: # keep only filtered subset
|
| 162 |
+
continue
|
| 163 |
+
nm = CATALOG.loc[gi, "name"]
|
| 164 |
+
if nm in seen:
|
| 165 |
+
continue
|
| 166 |
seen.add(nm)
|
| 167 |
+
# similarity = 1 - distance
|
| 168 |
+
sim = 1 - float(_nn.kneighbors_graph(q_vec, n_neighbors=1, mode="distance")[0, gi]) if False else 1.0
|
| 169 |
+
# we already have distances in d; recompute sim from them using same order index:
|
| 170 |
+
# get distance for this gi:
|
| 171 |
+
# (for simplicity we just set sim to 1 - current min distance; not critical for UI ranking)
|
| 172 |
+
picks.append((gi, None))
|
| 173 |
+
if len(picks) >= k:
|
| 174 |
+
break
|
| 175 |
+
|
| 176 |
+
if not picks:
|
| 177 |
+
return df_f.head(k).assign(similarity=np.nan)[["name","short_desc","price_usd","occasion_tags","persona_fit","age_range","image_url","similarity"]]
|
| 178 |
+
|
| 179 |
+
sel = [gi for gi,_ in picks]
|
| 180 |
+
res = CATALOG.loc[sel].copy()
|
| 181 |
+
# compute similarity from the original distances vector for display
|
| 182 |
+
gi_to_dist = {int(gi): float(dist) for gi, dist in zip(cand_global, d)}
|
| 183 |
+
res["similarity"] = [1.0 - gi_to_dist.get(int(gi), 0.0) for gi in sel]
|
| 184 |
return res[["name","short_desc","price_usd","occasion_tags","persona_fit","age_range","image_url","similarity"]]
|
| 185 |
|
| 186 |
# ============= Synthetic item + message =============
|
|
|
|
| 201 |
else:
|
| 202 |
base_name = f"{core.title()} starter bundle"
|
| 203 |
base_desc = f"A curated set to kickstart their {core} passion."
|
|
|
|
|
|
|
| 204 |
if age == "kids":
|
| 205 |
base_desc += " Suitable for kids with safe, age-appropriate materials."
|
| 206 |
elif age == "teens":
|
| 207 |
base_desc += " Trendy pick that suits young enthusiasts."
|
| 208 |
elif age == "senior":
|
| 209 |
base_desc += " Comfortable and easy to use."
|
|
|
|
| 210 |
price = float(np.clip(float(budget), 10, 250))
|
| 211 |
return {
|
| 212 |
"name": f"{base_name} ({occasion})",
|
|
|
|
| 228 |
|
| 229 |
# ============= Gradio UI (GIfty) =============
|
| 230 |
EXAMPLES = [
|
| 231 |
+
[["reading","travel","aesthetic"], "birthday", [20, 60], "Noa", "adult (18β64)", "warm and friendly"],
|
| 232 |
+
[["coffee","home","practical"], "housewarming", [25, 45], "Daniel", "adult (18β64)", "warm"],
|
| 233 |
+
[["tech","digital"], "birthday", [30, 120], "Omer", "teen (13β17)", "fun"],
|
| 234 |
]
|
| 235 |
|
| 236 |
+
def ui_predict(interests_list: List[str], occasion: str, budget_range, recipient_name: str, age_label: str, tone: str):
|
|
|
|
| 237 |
try:
|
| 238 |
+
# budget_range is a tuple/list: (min, max)
|
| 239 |
+
if isinstance(budget_range, (list, tuple)) and len(budget_range) == 2:
|
| 240 |
+
budget_min, budget_max = float(budget_range[0]), float(budget_range[1])
|
| 241 |
+
else:
|
| 242 |
+
budget_min, budget_max = 20.0, 60.0
|
| 243 |
+
if budget_min > budget_max:
|
| 244 |
budget_min, budget_max = budget_max, budget_min
|
| 245 |
|
| 246 |
+
age_range = AGE_OPTIONS.get(age_label, "any")
|
| 247 |
profile = {
|
| 248 |
"recipient_name": recipient_name or "Friend",
|
| 249 |
"interests": interests_list or [],
|
| 250 |
"occasion": occasion or "birthday",
|
| 251 |
+
"budget_min": budget_min,
|
| 252 |
+
"budget_max": budget_max,
|
| 253 |
+
"budget_usd": budget_max,
|
| 254 |
"age_range": age_range,
|
| 255 |
"tone": tone or "warm and friendly",
|
| 256 |
}
|
|
|
|
| 263 |
gen_md = f"**{gen['name']}**\n\n{gen['short_desc']}\n\n~${gen['price_usd']:.0f}"
|
| 264 |
return top3_md, gen_md, msg
|
| 265 |
except Exception as e:
|
|
|
|
| 266 |
return f":warning: Error: {e}", "", ""
|
| 267 |
|
| 268 |
with gr.Blocks() as demo:
|
|
|
|
| 276 |
with gr.Row():
|
| 277 |
occasion = gr.Dropdown(label="Occasion", choices=OCCASION_OPTIONS, value="birthday")
|
| 278 |
age = gr.Dropdown(label="Age group", choices=list(AGE_OPTIONS.keys()), value="adult (18β64)")
|
| 279 |
+
|
| 280 |
+
# Range slider for budget (two handles)
|
| 281 |
+
budget = gr.Slider(label="Budget (USD)", minimum=5, maximum=500, step=1, value=(20, 60))
|
| 282 |
+
|
| 283 |
with gr.Row():
|
| 284 |
recipient_name = gr.Textbox(label="Recipient name", value="Noa")
|
| 285 |
tone = gr.Textbox(label="Message tone", value="warm and friendly")
|
|
|
|
| 291 |
|
| 292 |
gr.Examples(
|
| 293 |
EXAMPLES,
|
| 294 |
+
[interests, occasion, budget, recipient_name, age, tone],
|
| 295 |
label="Quick examples",
|
| 296 |
)
|
| 297 |
|
| 298 |
go.click(
|
| 299 |
ui_predict,
|
| 300 |
+
[interests, occasion, budget, recipient_name, age, tone],
|
| 301 |
[out_top3, out_gen, out_msg]
|
| 302 |
)
|
| 303 |
|
|
|
|
| 304 |
if __name__ == "__main__":
|
| 305 |
demo.launch()
|