Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -21,7 +21,7 @@ import torch
|
|
21 |
print(f"===== Application Startup at {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')} =====")
|
22 |
|
23 |
# --------------------- Config ---------------------
|
24 |
-
TITLE = "# ๐ GIfty+
|
25 |
|
26 |
DATASET_ID = os.getenv("DATASET_ID", "Danielos100/Amazon_products_clean")
|
27 |
DATASET_SPLIT = os.getenv("DATASET_SPLIT", "train")
|
@@ -246,7 +246,6 @@ if "tok_set" not in CATALOG.columns:
|
|
246 |
).map(_tok_set)
|
247 |
|
248 |
# ====================== Recommendations โ Hybrid Ranker v2 ======================
|
249 |
-
# ืืืฉ ืืืง ืืืชืจ ืขื ืชืืืืืื; Gender/Age ืืกื ื ืื ืืืื; ืืืืืง Occasion; ืจืืจืื ืงืจ ืืืคืฆืืื ืื; ืืืืื (MMR)
|
250 |
try:
|
251 |
from sentence_transformers import CrossEncoder
|
252 |
except Exception:
|
@@ -292,7 +291,6 @@ def expand_with_synonyms(tokens: List[str]) -> List[str]:
|
|
292 |
return out
|
293 |
|
294 |
def profile_to_query(profile: Dict) -> str:
|
295 |
-
# ืืืฉ ร3 ืืชืืืืืื; ืืื ืืืืืจ ืืื/ืืืืจ ืื ืืกื ืืื ืื ืืชืช ืืฉืงื ืกืื ืื
|
296 |
inter = [i.lower() for i in profile.get("interests", []) if i]
|
297 |
expanded = expand_with_synonyms(inter) * 3
|
298 |
rel_tokens = REL_TO_TOKENS.get(profile.get("relationship","Friend"), [])
|
@@ -318,7 +316,6 @@ def _gender_ok_mask(gender: str) -> np.ndarray:
|
|
318 |
return np.ones(len(blob), dtype=bool)
|
319 |
|
320 |
def _mask_by_age(age: str, blob: pd.Series) -> np.ndarray:
|
321 |
-
# ืกืื ืื ืืืื (ืืื ืขืื ืฉ/ืืฉืงื)
|
322 |
KIDS_RX = r"\b(?:kid|kids|child|children|toddler|baby|boys?|girls?|kid\'s|children\'s)\b"
|
323 |
TEEN_RX = r"\b(?:teen|teens|young adult|ya)\b"
|
324 |
is_kidsy = blob.str.contains(KIDS_RX, regex=True, na=False)
|
@@ -339,7 +336,7 @@ def _interest_bonus(profile: Dict, idx: np.ndarray) -> np.ndarray:
|
|
339 |
return np.zeros(len(idx), dtype="float32")
|
340 |
counts = np.array([len(CATALOG["tok_set"].iat[i] & vocab) for i in idx], dtype="float32")
|
341 |
counts = np.clip(counts, 0, 6)
|
342 |
-
return 0.10 * counts
|
343 |
|
344 |
def _occasion_bonus(idx: np.ndarray, occ_ui: str) -> np.ndarray:
|
345 |
slug = OCCASION_CANON.get(occ_ui or "Birthday", "birthday")
|
@@ -389,7 +386,6 @@ def _mmr_select(cand_idx: np.ndarray, scores: np.ndarray, k: int, lambda_: float
|
|
389 |
return cand_idx[np.array(picked, dtype=int)]
|
390 |
|
391 |
def recommend_top3_budget_first(profile: Dict) -> pd.DataFrame:
|
392 |
-
# 1) ืกืื ืื ืืคื ืชืงืฆืื + ืืื (ืกืื ืื ืืืื) + ืืืืจ (ืกืื ืื ืืืื)
|
393 |
lo = float(profile.get("budget_min", 0))
|
394 |
hi = float(profile.get("budget_max", 1e9))
|
395 |
m_price = (CATALOG["price_usd"].values >= lo) & (CATALOG["price_usd"].values <= hi)
|
@@ -409,7 +405,6 @@ def recommend_top3_budget_first(profile: Dict) -> pd.DataFrame:
|
|
409 |
res["similarity"] = np.nan
|
410 |
return res[["name","short_desc","price_usd","image_url","similarity"]].reset_index(drop=True)
|
411 |
|
412 |
-
# 2) ืืืืืื ื + ืืืืจ + ืชืืืืืื + Occasion
|
413 |
q = profile_to_query(profile)
|
414 |
qv = EMB.query_vec(q).astype("float32")
|
415 |
X = np.asarray(EMB.embs, dtype="float32")[idx]
|
@@ -424,7 +419,6 @@ def recommend_top3_budget_first(profile: Dict) -> pd.DataFrame:
|
|
424 |
|
425 |
pre_score = emb_sims + price_bonus + int_bonus + occ_bonus
|
426 |
|
427 |
-
# 3) ืืืขืืืื
|
428 |
K1 = min(64, idx.size)
|
429 |
top_local = np.argpartition(-pre_score, K1-1)[:K1]
|
430 |
cand_idx = idx[top_local]
|
@@ -432,9 +426,8 @@ def recommend_top3_budget_first(profile: Dict) -> pd.DataFrame:
|
|
432 |
emb_norm = _minmax(emb_sims[top_local])
|
433 |
price_norm = _minmax(price_bonus[top_local])
|
434 |
int_norm = _minmax(int_bonus[top_local])
|
435 |
-
occ_norm = _minmax(occ_bonus[top_local])
|
436 |
|
437 |
-
# 4) ืจืืจืื ืงืจ ืืืคืฆืืื ืื (ืื ืืคืฉืจ)
|
438 |
try:
|
439 |
from sentence_transformers import CrossEncoder as _CE
|
440 |
ce = _load_cross_encoder()
|
@@ -448,7 +441,6 @@ def recommend_top3_budget_first(profile: Dict) -> pd.DataFrame:
|
|
448 |
except Exception:
|
449 |
ce_norm = np.zeros_like(emb_norm)
|
450 |
|
451 |
-
# 5) ืฆืืื ืกืืคื (ืืืฉ ืืืง ืืชืืืืืื ืืจื int_norm + ืืฉืืืืชื)
|
452 |
final = (
|
453 |
0.56 * emb_norm +
|
454 |
0.26 * ce_norm +
|
@@ -457,7 +449,6 @@ def recommend_top3_budget_first(profile: Dict) -> pd.DataFrame:
|
|
457 |
0.03 * price_norm
|
458 |
).astype("float32")
|
459 |
|
460 |
-
# 6) ืืืืื MMR ืืืืืจืช ืืืค-3
|
461 |
pick_idx = _mmr_select(cand_idx, final, k=min(3, cand_idx.size), lambda_=0.7)
|
462 |
|
463 |
res = CATALOG.loc[pick_idx].copy()
|
@@ -626,7 +617,6 @@ def diy_generate(profile: Dict) -> Tuple[dict, str]:
|
|
626 |
lang = "English"
|
627 |
ints_str = ", ".join(p["interests"]) or "general"
|
628 |
|
629 |
-
# 1) NAME
|
630 |
prompt_name = (
|
631 |
f"Return ONLY a DIY gift NAME in Title Case (4โ8 words). "
|
632 |
f"Must include at least one interest token from: {', '.join(sum(([it]+INTEREST_ALIASES.get(it,[]) for it in p['interests']), [])) or 'gift'}. "
|
@@ -641,7 +631,6 @@ def diy_generate(profile: Dict) -> Tuple[dict, str]:
|
|
641 |
raw_name = _gen(tok, mdl, prompt_name, max_new_tokens=24, do_sample=False)
|
642 |
name = _sanitize_name(raw_name, p["interests"])
|
643 |
|
644 |
-
# 2) OVERVIEW
|
645 |
prompt_over = (
|
646 |
f"Write EXACTLY 2 sentences in {lang} for a handmade gift called '{name}'. "
|
647 |
f"Mention {p['recipient_name']} ({p['relationship']}) and the occasion ({p['occ_ui']}). "
|
@@ -650,7 +639,6 @@ def diy_generate(profile: Dict) -> Tuple[dict, str]:
|
|
650 |
)
|
651 |
overview = _gen(tok, mdl, prompt_over, max_new_tokens=80, do_sample=True, temperature=0.9, top_p=0.95)
|
652 |
|
653 |
-
# 3) MATERIALS
|
654 |
prompt_mat = (
|
655 |
f"List 6 concise materials with quantities to make '{name}' cheaply. "
|
656 |
f"Keep total within {p['budget_min']}-{p['budget_max']} USD. "
|
@@ -659,7 +647,6 @@ def diy_generate(profile: Dict) -> Tuple[dict, str]:
|
|
659 |
mats_txt = _gen(tok, mdl, prompt_mat, max_new_tokens=96, do_sample=False)
|
660 |
materials = _split_list_text(mats_txt, [",", ";"])
|
661 |
|
662 |
-
# 4) STEPS
|
663 |
prompt_steps = (
|
664 |
f"Write 6 short imperative steps to make '{name}'. "
|
665 |
"Output ONLY a semicolon-separated list."
|
@@ -667,14 +654,12 @@ def diy_generate(profile: Dict) -> Tuple[dict, str]:
|
|
667 |
steps_txt = _gen(tok, mdl, prompt_steps, max_new_tokens=120, do_sample=True, temperature=0.9, top_p=0.95)
|
668 |
steps = _split_list_text(steps_txt, [";", "\n"])
|
669 |
|
670 |
-
# 5) COST
|
671 |
prompt_cost = (
|
672 |
f"Return ONE integer total cost in USD between {p['budget_min']}-{p['budget_max']}. Output NUMBER only."
|
673 |
)
|
674 |
cost_txt = _gen(tok, mdl, prompt_cost, max_new_tokens=6, do_sample=False)
|
675 |
cost = _only_int(cost_txt)
|
676 |
|
677 |
-
# 6) MINUTES
|
678 |
time_txt = _gen(tok, mdl, "Return ONE integer minutes between 20 and 180. Output NUMBER only.",
|
679 |
max_new_tokens=6, do_sample=False)
|
680 |
minutes = _only_int(time_txt)
|
@@ -690,7 +675,6 @@ def diy_generate(profile: Dict) -> Tuple[dict, str]:
|
|
690 |
return idea, "ok"
|
691 |
|
692 |
# --------------------- Personalized Message (FLAN, ืืืืื + ืืืืืฆืื) ---------------------
|
693 |
-
# ืืืืกืก ืืื-ืืืื ืขื ืืงืื ืืืงืืืื ืฉืื, ืืืชืื ืืฉืืืืฉ ืืฉืืจ ืืืคืืืงืฆืื
|
694 |
MSG_MODEL_ID = "google/flan-t5-small"
|
695 |
MSG_DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
696 |
TEMP_RANGE = (0.88, 1.10)
|
@@ -721,9 +705,9 @@ TONE_STYLES: Dict[str, Dict[str, List[str]]] = {
|
|
721 |
"Funny": {
|
722 |
"system": "Write 2โ3 witty sentences with playful humor.",
|
723 |
"rules": [
|
724 |
-
|
725 |
-
|
726 |
-
|
727 |
],
|
728 |
},
|
729 |
"Heartfelt": {
|
@@ -951,6 +935,24 @@ with gr.Blocks(
|
|
951 |
/* ืืกืชืจืช ืืกืืจืช/ืืืืืืื ืืชืืืื ืฉื ืืืืช ืืืืืืืืช */
|
952 |
.handsontable .wtBorder, .handsontable .htBorders, .handsontable .wtBorder.current { display: none !important; }
|
953 |
.gr-dataframe table td:focus { outline: none !important; box-shadow: none !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
954 |
"""
|
955 |
) as demo:
|
956 |
gr.Markdown(TITLE)
|
@@ -991,7 +993,7 @@ with gr.Blocks(
|
|
991 |
tone = gr.Dropdown(label="Message tone", choices=MESSAGE_TONES, value="Funny")
|
992 |
|
993 |
# Action button and outputs
|
994 |
-
go = gr.Button("Get GIfty")
|
995 |
gr.Markdown("### ๐ฏ Recommendations")
|
996 |
out_top3 = gr.HTML()
|
997 |
gr.Markdown("### ๐ ๏ธ DIY Gift")
|
@@ -999,6 +1001,9 @@ with gr.Blocks(
|
|
999 |
gr.Markdown("### ๐ Personalized Message")
|
1000 |
out_msg = gr.Markdown()
|
1001 |
|
|
|
|
|
|
|
1002 |
# ---- row click handler (fill form) ----
|
1003 |
def _on_example_select(evt: gr.SelectData):
|
1004 |
r = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
|
@@ -1016,7 +1021,7 @@ with gr.Blocks(
|
|
1016 |
outputs=[interests, occasion, budget_min, budget_max, recipient_name, relationship, age, gender, tone]
|
1017 |
)
|
1018 |
|
1019 |
-
# ----
|
1020 |
def render_diy_md(j: dict) -> str:
|
1021 |
if not j: return "_DIY generation failed._"
|
1022 |
steps = j.get('step_by_step_instructions', j.get('steps', []))
|
@@ -1035,14 +1040,14 @@ with gr.Blocks(
|
|
1035 |
]
|
1036 |
return "\n".join(parts)
|
1037 |
|
1038 |
-
|
|
|
1039 |
try:
|
1040 |
bmin = float(bmin); bmax = float(bmax)
|
1041 |
except Exception:
|
1042 |
bmin, bmax = 5.0, 500.0
|
1043 |
if bmin > bmax: bmin, bmax = bmax, bmin
|
1044 |
-
|
1045 |
-
profile = {
|
1046 |
"recipient_name": name or "Friend",
|
1047 |
"relationship": rel or "Friend",
|
1048 |
"interests": interests_list or [],
|
@@ -1054,21 +1059,60 @@ with gr.Blocks(
|
|
1054 |
"tone": tone_val or "Heartfelt",
|
1055 |
}
|
1056 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1057 |
top3 = recommend_top3_budget_first(profile)
|
1058 |
top3_html = render_top3_html(top3, age_label)
|
|
|
1059 |
|
|
|
|
|
|
|
1060 |
diy_json, _status = diy_generate(profile)
|
1061 |
diy_md = render_diy_md(diy_json)
|
|
|
1062 |
|
|
|
|
|
|
|
1063 |
msg_obj = generate_personal_message(profile)
|
1064 |
msg = msg_obj["message"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1065 |
|
1066 |
-
|
1067 |
-
|
1068 |
-
|
1069 |
-
|
1070 |
-
[
|
1071 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1072 |
)
|
1073 |
|
1074 |
if __name__ == "__main__":
|
|
|
21 |
print(f"===== Application Startup at {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')} =====")
|
22 |
|
23 |
# --------------------- Config ---------------------
|
24 |
+
TITLE = "# ๐ GIfty+ Smart Gift Recommender\n*Top-3 catalog picks + 1 DIY gift + personalized message*"
|
25 |
|
26 |
DATASET_ID = os.getenv("DATASET_ID", "Danielos100/Amazon_products_clean")
|
27 |
DATASET_SPLIT = os.getenv("DATASET_SPLIT", "train")
|
|
|
246 |
).map(_tok_set)
|
247 |
|
248 |
# ====================== Recommendations โ Hybrid Ranker v2 ======================
|
|
|
249 |
try:
|
250 |
from sentence_transformers import CrossEncoder
|
251 |
except Exception:
|
|
|
291 |
return out
|
292 |
|
293 |
def profile_to_query(profile: Dict) -> str:
|
|
|
294 |
inter = [i.lower() for i in profile.get("interests", []) if i]
|
295 |
expanded = expand_with_synonyms(inter) * 3
|
296 |
rel_tokens = REL_TO_TOKENS.get(profile.get("relationship","Friend"), [])
|
|
|
316 |
return np.ones(len(blob), dtype=bool)
|
317 |
|
318 |
def _mask_by_age(age: str, blob: pd.Series) -> np.ndarray:
|
|
|
319 |
KIDS_RX = r"\b(?:kid|kids|child|children|toddler|baby|boys?|girls?|kid\'s|children\'s)\b"
|
320 |
TEEN_RX = r"\b(?:teen|teens|young adult|ya)\b"
|
321 |
is_kidsy = blob.str.contains(KIDS_RX, regex=True, na=False)
|
|
|
336 |
return np.zeros(len(idx), dtype="float32")
|
337 |
counts = np.array([len(CATALOG["tok_set"].iat[i] & vocab) for i in idx], dtype="float32")
|
338 |
counts = np.clip(counts, 0, 6)
|
339 |
+
return 0.10 * counts
|
340 |
|
341 |
def _occasion_bonus(idx: np.ndarray, occ_ui: str) -> np.ndarray:
|
342 |
slug = OCCASION_CANON.get(occ_ui or "Birthday", "birthday")
|
|
|
386 |
return cand_idx[np.array(picked, dtype=int)]
|
387 |
|
388 |
def recommend_top3_budget_first(profile: Dict) -> pd.DataFrame:
|
|
|
389 |
lo = float(profile.get("budget_min", 0))
|
390 |
hi = float(profile.get("budget_max", 1e9))
|
391 |
m_price = (CATALOG["price_usd"].values >= lo) & (CATALOG["price_usd"].values <= hi)
|
|
|
405 |
res["similarity"] = np.nan
|
406 |
return res[["name","short_desc","price_usd","image_url","similarity"]].reset_index(drop=True)
|
407 |
|
|
|
408 |
q = profile_to_query(profile)
|
409 |
qv = EMB.query_vec(q).astype("float32")
|
410 |
X = np.asarray(EMB.embs, dtype="float32")[idx]
|
|
|
419 |
|
420 |
pre_score = emb_sims + price_bonus + int_bonus + occ_bonus
|
421 |
|
|
|
422 |
K1 = min(64, idx.size)
|
423 |
top_local = np.argpartition(-pre_score, K1-1)[:K1]
|
424 |
cand_idx = idx[top_local]
|
|
|
426 |
emb_norm = _minmax(emb_sims[top_local])
|
427 |
price_norm = _minmax(price_bonus[top_local])
|
428 |
int_norm = _minmax(int_bonus[top_local])
|
429 |
+
occ_norm = _minmax(oc_bonus := occ_bonus[top_local])
|
430 |
|
|
|
431 |
try:
|
432 |
from sentence_transformers import CrossEncoder as _CE
|
433 |
ce = _load_cross_encoder()
|
|
|
441 |
except Exception:
|
442 |
ce_norm = np.zeros_like(emb_norm)
|
443 |
|
|
|
444 |
final = (
|
445 |
0.56 * emb_norm +
|
446 |
0.26 * ce_norm +
|
|
|
449 |
0.03 * price_norm
|
450 |
).astype("float32")
|
451 |
|
|
|
452 |
pick_idx = _mmr_select(cand_idx, final, k=min(3, cand_idx.size), lambda_=0.7)
|
453 |
|
454 |
res = CATALOG.loc[pick_idx].copy()
|
|
|
617 |
lang = "English"
|
618 |
ints_str = ", ".join(p["interests"]) or "general"
|
619 |
|
|
|
620 |
prompt_name = (
|
621 |
f"Return ONLY a DIY gift NAME in Title Case (4โ8 words). "
|
622 |
f"Must include at least one interest token from: {', '.join(sum(([it]+INTEREST_ALIASES.get(it,[]) for it in p['interests']), [])) or 'gift'}. "
|
|
|
631 |
raw_name = _gen(tok, mdl, prompt_name, max_new_tokens=24, do_sample=False)
|
632 |
name = _sanitize_name(raw_name, p["interests"])
|
633 |
|
|
|
634 |
prompt_over = (
|
635 |
f"Write EXACTLY 2 sentences in {lang} for a handmade gift called '{name}'. "
|
636 |
f"Mention {p['recipient_name']} ({p['relationship']}) and the occasion ({p['occ_ui']}). "
|
|
|
639 |
)
|
640 |
overview = _gen(tok, mdl, prompt_over, max_new_tokens=80, do_sample=True, temperature=0.9, top_p=0.95)
|
641 |
|
|
|
642 |
prompt_mat = (
|
643 |
f"List 6 concise materials with quantities to make '{name}' cheaply. "
|
644 |
f"Keep total within {p['budget_min']}-{p['budget_max']} USD. "
|
|
|
647 |
mats_txt = _gen(tok, mdl, prompt_mat, max_new_tokens=96, do_sample=False)
|
648 |
materials = _split_list_text(mats_txt, [",", ";"])
|
649 |
|
|
|
650 |
prompt_steps = (
|
651 |
f"Write 6 short imperative steps to make '{name}'. "
|
652 |
"Output ONLY a semicolon-separated list."
|
|
|
654 |
steps_txt = _gen(tok, mdl, prompt_steps, max_new_tokens=120, do_sample=True, temperature=0.9, top_p=0.95)
|
655 |
steps = _split_list_text(steps_txt, [";", "\n"])
|
656 |
|
|
|
657 |
prompt_cost = (
|
658 |
f"Return ONE integer total cost in USD between {p['budget_min']}-{p['budget_max']}. Output NUMBER only."
|
659 |
)
|
660 |
cost_txt = _gen(tok, mdl, prompt_cost, max_new_tokens=6, do_sample=False)
|
661 |
cost = _only_int(cost_txt)
|
662 |
|
|
|
663 |
time_txt = _gen(tok, mdl, "Return ONE integer minutes between 20 and 180. Output NUMBER only.",
|
664 |
max_new_tokens=6, do_sample=False)
|
665 |
minutes = _only_int(time_txt)
|
|
|
675 |
return idea, "ok"
|
676 |
|
677 |
# --------------------- Personalized Message (FLAN, ืืืืื + ืืืืืฆืื) ---------------------
|
|
|
678 |
MSG_MODEL_ID = "google/flan-t5-small"
|
679 |
MSG_DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
680 |
TEMP_RANGE = (0.88, 1.10)
|
|
|
705 |
"Funny": {
|
706 |
"system": "Write 2โ3 witty sentences with playful humor.",
|
707 |
"rules": [
|
708 |
+
"Add one subtle pun linked to the occasion or interests.",
|
709 |
+
"No slapstick; keep it tasteful.",
|
710 |
+
"End with a cheeky nudge."
|
711 |
],
|
712 |
},
|
713 |
"Heartfelt": {
|
|
|
935 |
/* ืืกืชืจืช ืืกืืจืช/ืืืืืืื ืืชืืืื ืฉื ืืืืช ืืืืืืืืช */
|
936 |
.handsontable .wtBorder, .handsontable .htBorders, .handsontable .wtBorder.current { display: none !important; }
|
937 |
.gr-dataframe table td:focus { outline: none !important; box-shadow: none !important; }
|
938 |
+
|
939 |
+
/* === ืืืคื ืื ืฉืืจืช ืืืืื ื"ืืคืชืืจ" === */
|
940 |
+
.gr-dataframe thead { display:none; }
|
941 |
+
.gr-dataframe table { border-collapse: separate !important; border-spacing: 0 8px !important; }
|
942 |
+
.gr-dataframe tbody tr {
|
943 |
+
cursor: pointer;
|
944 |
+
background: #fff;
|
945 |
+
border-radius: 12px;
|
946 |
+
box-shadow: 0 1px 0 rgba(0,0,0,.06);
|
947 |
+
}
|
948 |
+
.gr-dataframe tbody tr td {
|
949 |
+
border-top: 1px solid #eee !important;
|
950 |
+
border-bottom: 1px solid #eee !important;
|
951 |
+
}
|
952 |
+
.gr-dataframe tbody tr:hover {
|
953 |
+
background: #f7fafc;
|
954 |
+
box-shadow: 0 0 0 2px #e2e8f0 inset;
|
955 |
+
}
|
956 |
"""
|
957 |
) as demo:
|
958 |
gr.Markdown(TITLE)
|
|
|
993 |
tone = gr.Dropdown(label="Message tone", choices=MESSAGE_TONES, value="Funny")
|
994 |
|
995 |
# Action button and outputs
|
996 |
+
go = gr.Button("Get GIfty!")
|
997 |
gr.Markdown("### ๐ฏ Recommendations")
|
998 |
out_top3 = gr.HTML()
|
999 |
gr.Markdown("### ๐ ๏ธ DIY Gift")
|
|
|
1001 |
gr.Markdown("### ๐ Personalized Message")
|
1002 |
out_msg = gr.Markdown()
|
1003 |
|
1004 |
+
# --- State: run token (to "override" previous runs safely) ---
|
1005 |
+
run_token = gr.State(0)
|
1006 |
+
|
1007 |
# ---- row click handler (fill form) ----
|
1008 |
def _on_example_select(evt: gr.SelectData):
|
1009 |
r = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
|
|
|
1021 |
outputs=[interests, occasion, budget_min, budget_max, recipient_name, relationship, age, gender, tone]
|
1022 |
)
|
1023 |
|
1024 |
+
# ---- helper: render DIY markdown (unchanged logic) ----
|
1025 |
def render_diy_md(j: dict) -> str:
|
1026 |
if not j: return "_DIY generation failed._"
|
1027 |
steps = j.get('step_by_step_instructions', j.get('steps', []))
|
|
|
1040 |
]
|
1041 |
return "\n".join(parts)
|
1042 |
|
1043 |
+
# ---- Build profile dict (shared) ----
|
1044 |
+
def _build_profile(interests_list, occasion_val, bmin, bmax, name, rel, age_label, gender_val, tone_val):
|
1045 |
try:
|
1046 |
bmin = float(bmin); bmax = float(bmax)
|
1047 |
except Exception:
|
1048 |
bmin, bmax = 5.0, 500.0
|
1049 |
if bmin > bmax: bmin, bmax = bmax, bmin
|
1050 |
+
return {
|
|
|
1051 |
"recipient_name": name or "Friend",
|
1052 |
"relationship": rel or "Friend",
|
1053 |
"interests": interests_list or [],
|
|
|
1059 |
"tone": tone_val or "Heartfelt",
|
1060 |
}
|
1061 |
|
1062 |
+
# ---- NEW: split into 3 functions (partial results) + token check ----
|
1063 |
+
def start_run(curr_token):
|
1064 |
+
# increments token (very fast; allows immediate second click)
|
1065 |
+
return int(curr_token or 0) + 1
|
1066 |
+
|
1067 |
+
def predict_recs_only(rt, interests_list, occasion_val, bmin, bmax, name, rel, age_label, gender_val, tone_val):
|
1068 |
+
# compute only recommendations; ignore if token is stale
|
1069 |
+
latest = rt
|
1070 |
+
profile = _build_profile(interests_list, occasion_val, bmin, bmax, name, rel, age_label, gender_val, tone_val)
|
1071 |
top3 = recommend_top3_budget_first(profile)
|
1072 |
top3_html = render_top3_html(top3, age_label)
|
1073 |
+
return gr.update(value=top3_html, visible=True), latest
|
1074 |
|
1075 |
+
def predict_diy_only(rt, interests_list, occasion_val, bmin, bmax, name, rel, age_label, gender_val, tone_val):
|
1076 |
+
latest = rt
|
1077 |
+
profile = _build_profile(interests_list, occasion_val, bmin, bmax, name, rel, age_label, gender_val, tone_val)
|
1078 |
diy_json, _status = diy_generate(profile)
|
1079 |
diy_md = render_diy_md(diy_json)
|
1080 |
+
return gr.update(value=diy_md, visible=True), latest
|
1081 |
|
1082 |
+
def predict_msg_only(rt, interests_list, occasion_val, bmin, bmax, name, rel, age_label, gender_val, tone_val):
|
1083 |
+
latest = rt
|
1084 |
+
profile = _build_profile(interests_list, occasion_val, bmin, bmax, name, rel, age_label, gender_val, tone_val)
|
1085 |
msg_obj = generate_personal_message(profile)
|
1086 |
msg = msg_obj["message"]
|
1087 |
+
return gr.update(value=msg, visible=True), latest
|
1088 |
+
|
1089 |
+
# --- Wire events: one short "start", then 3 parallel tasks that each update its output ASAP ---
|
1090 |
+
# Start: bump token
|
1091 |
+
ev_start = go.click(
|
1092 |
+
start_run,
|
1093 |
+
inputs=[run_token],
|
1094 |
+
outputs=[run_token],
|
1095 |
+
queue=True,
|
1096 |
+
)
|
1097 |
|
1098 |
+
# Run three tasks in parallel (each returns its output + echoes token to keep it "fresh")
|
1099 |
+
ev_rec = ev_start.then(
|
1100 |
+
predict_recs_only,
|
1101 |
+
inputs=[run_token, interests, occasion, budget_min, budget_max, recipient_name, relationship, age, gender, tone],
|
1102 |
+
outputs=[out_top3, run_token],
|
1103 |
+
queue=True,
|
1104 |
+
)
|
1105 |
+
ev_diy = ev_start.then(
|
1106 |
+
predict_diy_only,
|
1107 |
+
inputs=[run_token, interests, occasion, budget_min, budget_max, recipient_name, relationship, age, gender, tone],
|
1108 |
+
outputs=[out_diy_md, run_token],
|
1109 |
+
queue=True,
|
1110 |
+
)
|
1111 |
+
ev_msg = ev_start.then(
|
1112 |
+
predict_msg_only,
|
1113 |
+
inputs=[run_token, interests, occasion, budget_min, budget_max, recipient_name, relationship, age, gender, tone],
|
1114 |
+
outputs=[out_msg, run_token],
|
1115 |
+
queue=True,
|
1116 |
)
|
1117 |
|
1118 |
if __name__ == "__main__":
|