Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -39,7 +39,7 @@ for root, _, files in os.walk("."):
|
|
| 39 |
ALL_IMAGE_PATHS.append(os.path.join(root, f))
|
| 40 |
|
| 41 |
def find_any_matching_image(dish_name):
|
| 42 |
-
"""Robust image search logic."""
|
| 43 |
name_clean = str(dish_name).lower()
|
| 44 |
keywords = [kw for kw in re.findall(r'\w+', name_clean) if len(kw) > 2]
|
| 45 |
if not keywords: return None
|
|
@@ -67,13 +67,19 @@ def get_hybrid_recommendations(user_query, user_name, city, hobbies, style, alph
|
|
| 67 |
return [None, "### ⚠️ Identity required."] * 3
|
| 68 |
|
| 69 |
user_profile_text = f"{user_name} from {city} loves {hobbies} and has a {style} style."
|
|
|
|
|
|
|
| 70 |
query_emb = model.encode([user_query])
|
| 71 |
profile_emb = model.encode([user_profile_text])
|
| 72 |
|
|
|
|
| 73 |
content_sim = cosine_similarity(query_emb, dish_embeddings).flatten()
|
| 74 |
user_sim = cosine_similarity(profile_emb, user_embeddings).flatten()
|
|
|
|
|
|
|
| 75 |
raw_scores = (alpha * content_sim) + (1 - alpha) * user_sim
|
| 76 |
|
|
|
|
| 77 |
min_s, max_s = raw_scores.min(), raw_scores.max()
|
| 78 |
scaled_scores = 0.74 + (raw_scores - min_s) * (0.25) / (max_s - min_s) if max_s > min_s else raw_scores
|
| 79 |
|
|
@@ -84,14 +90,25 @@ def get_hybrid_recommendations(user_query, user_name, city, hobbies, style, alph
|
|
| 84 |
output = []
|
| 85 |
for _, row in top_results.iterrows():
|
| 86 |
image_path = find_any_matching_image(row['dish_name'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
res_text = f"""
|
| 88 |
<div style='font-family: serif; color: #4a3f35; padding: 15px; background: #fffcf7; border: 1px solid #e0d8c3; margin-bottom: 10px;'>
|
| 89 |
<h2 style='margin-bottom: 2px; color: #5d4037; font-weight: normal; letter-spacing: 1px;'>{row['dish_name'].upper()}</h2>
|
| 90 |
<p style='font-style: italic; margin-top: 0; color: #8c7b6c;'>{row['restaurant_name']} — {row['food_vibe']}</p>
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
<hr style='border: 0; border-top: 1px solid #f0ece2; margin: 12px 0;'>
|
| 93 |
-
<p style='line-height: 1.6;'><b>Vision:</b> {row['visual_description']}</p>
|
| 94 |
-
<p style='line-height: 1.6;'><b>Experience:</b> {row['taste_review']}</p>
|
| 95 |
</div>
|
| 96 |
"""
|
| 97 |
output.extend([image_path, res_text])
|
|
|
|
| 39 |
ALL_IMAGE_PATHS.append(os.path.join(root, f))
|
| 40 |
|
| 41 |
def find_any_matching_image(dish_name):
|
| 42 |
+
"""Robust image search logic based on keyword matching."""
|
| 43 |
name_clean = str(dish_name).lower()
|
| 44 |
keywords = [kw for kw in re.findall(r'\w+', name_clean) if len(kw) > 2]
|
| 45 |
if not keywords: return None
|
|
|
|
| 67 |
return [None, "### ⚠️ Identity required."] * 3
|
| 68 |
|
| 69 |
user_profile_text = f"{user_name} from {city} loves {hobbies} and has a {style} style."
|
| 70 |
+
|
| 71 |
+
# NEW: Create embeddings for both query and persona
|
| 72 |
query_emb = model.encode([user_query])
|
| 73 |
profile_emb = model.encode([user_profile_text])
|
| 74 |
|
| 75 |
+
# Calculate similarities (The Hybrid Logic)
|
| 76 |
content_sim = cosine_similarity(query_emb, dish_embeddings).flatten()
|
| 77 |
user_sim = cosine_similarity(profile_emb, user_embeddings).flatten()
|
| 78 |
+
|
| 79 |
+
# Weighting: 65% Craving (Content) + 35% Persona (Demographics)
|
| 80 |
raw_scores = (alpha * content_sim) + (1 - alpha) * user_sim
|
| 81 |
|
| 82 |
+
# Normalizing scores for display (74%-99%)
|
| 83 |
min_s, max_s = raw_scores.min(), raw_scores.max()
|
| 84 |
scaled_scores = 0.74 + (raw_scores - min_s) * (0.25) / (max_s - min_s) if max_s > min_s else raw_scores
|
| 85 |
|
|
|
|
| 90 |
output = []
|
| 91 |
for _, row in top_results.iterrows():
|
| 92 |
image_path = find_any_matching_image(row['dish_name'])
|
| 93 |
+
|
| 94 |
+
# NEW: Generate the 'Culinary Twin' profile from the dataset metadata
|
| 95 |
+
# This provides transparency on WHY this dish was recommended
|
| 96 |
+
twin_info = f"A {row['user_age']} year old from {row['user_origin']}, with a {row['user_fashion_style']} style."
|
| 97 |
+
|
| 98 |
res_text = f"""
|
| 99 |
<div style='font-family: serif; color: #4a3f35; padding: 15px; background: #fffcf7; border: 1px solid #e0d8c3; margin-bottom: 10px;'>
|
| 100 |
<h2 style='margin-bottom: 2px; color: #5d4037; font-weight: normal; letter-spacing: 1px;'>{row['dish_name'].upper()}</h2>
|
| 101 |
<p style='font-style: italic; margin-top: 0; color: #8c7b6c;'>{row['restaurant_name']} — {row['food_vibe']}</p>
|
| 102 |
+
|
| 103 |
+
<div style='background-color: #f0ece2; padding: 10px; margin: 10px 0; border-radius: 4px; font-size: 0.9em; border-left: 4px solid #5d4037;'>
|
| 104 |
+
<strong style='color: #5d4037;'>👤 Recommended by your Culinary Twin:</strong><br>
|
| 105 |
+
{twin_info}
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<p style='font-size: 0.8em; letter-spacing: 1.5px;'>MATCH SCORE: {row['final_score']:.2%} | ⭐ {row['rating']}</p>
|
| 109 |
<hr style='border: 0; border-top: 1px solid #f0ece2; margin: 12px 0;'>
|
| 110 |
+
<p style='line-height: 1.6;'><b>The Vision:</b> {row['visual_description']}</p>
|
| 111 |
+
<p style='line-height: 1.6;'><b>The Experience:</b> {row['taste_review']}</p>
|
| 112 |
</div>
|
| 113 |
"""
|
| 114 |
output.extend([image_path, res_text])
|