MatanYehudaDataAnalyst commited on
Commit
08e20a5
·
verified ·
1 Parent(s): 6bd637b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -85
app.py CHANGED
@@ -9,126 +9,139 @@ from sklearn.metrics.pairwise import cosine_similarity
9
  # ==========================================
10
  # 1. SETUP & DATA LOADING
11
  # ==========================================
12
- csv_path = "cleaned_dataset_10k.csv"
13
- pkl_path = "final_embeddings_10k.pkl"
 
14
 
15
- if not os.path.exists(csv_path) or not os.path.exists(pkl_path):
16
- raise FileNotFoundError(f"Error: Files not found. I see: {os.listdir('.')}")
17
 
18
- # Load Data
19
- df = pd.read_csv(csv_path)
20
  df.columns = [c.strip().lower().replace(' ', '_') for c in df.columns]
21
 
22
- # Helper to find column names
23
- def get_col(candidates, default):
24
- for c in candidates:
25
- if c in df.columns: return c
26
- return default
27
-
28
- col_name = get_col(['restaurant_name', 'name', 'place'], 'restaurant_name')
29
- col_rating = get_col(['rating', 'rating_score', 'stars'], 'rating')
30
- col_review = get_col(['review', 'review_content', 'review_content_clean'], 'review')
31
- col_persona = get_col(['reviewer_persona', 'persona', 'type'], 'reviewer_persona')
32
-
33
- # Load Embeddings
34
- with open(pkl_path, 'rb') as f:
35
  embedding_data = pickle.load(f)
36
- if isinstance(embedding_data, dict) and 'embeddings' in embedding_data:
37
- dataset_embeddings = embedding_data['embeddings']
38
- else:
39
- dataset_embeddings = embedding_data
40
 
41
- # Load Model
42
  model = SentenceTransformer('sentence-transformers/all-mpnet-base-v2')
43
 
44
- # Calculate Personas
45
  persona_profiles = {}
46
- if col_persona in df.columns:
47
- for persona in df[col_persona].unique():
48
- if pd.isna(persona): continue
49
- indices = df[df[col_persona] == persona].index
50
- valid_indices = [i for i in indices if i < len(dataset_embeddings)]
51
- if valid_indices:
52
- persona_vectors = dataset_embeddings[valid_indices]
53
- persona_profiles[persona] = np.mean(persona_vectors, axis=0)
54
- else:
55
- persona_profiles['Default'] = np.mean(dataset_embeddings, axis=0)
56
 
57
  # ==========================================
58
- # 2. LOGIC ENGINE
59
  # ==========================================
60
  def run_ven_engine(budget, dietary, company, purpose, noise):
 
 
 
 
 
61
  user_context = f"Searching for a {budget} experience, {dietary} friendly. Group: {company}. Occasion: {purpose}. Atmosphere: {noise}."
62
  query_vec = model.encode([user_context])
63
 
64
- similarities = {p: cosine_similarity(query_vec, v.reshape(1, -1))[0][0] for p, v in persona_profiles.items()}
65
- closest_persona = max(similarities, key=similarities.get)
 
 
66
 
67
- if col_persona in df.columns:
68
- persona_df = df[df[col_persona] == closest_persona]
69
- if persona_df.empty: persona_df = df
70
- else:
71
- persona_df = df
72
 
73
- top_match = persona_df.sort_values(by=col_rating, ascending=False).iloc[0]
 
 
74
 
75
- match_pct = int(similarities[closest_persona] * 100)
76
- review_text = str(top_match[col_review])[:160] + "..."
 
77
 
78
- # --- VISUAL FIX ---
 
 
 
 
 
 
 
79
  return f"""
80
- <div style="background: white; border: 1px solid #e2e8f0; border-radius: 20px; padding: 24px; color: #000000 !important;">
81
- <div style="display:flex; justify-content:space-between;">
82
- <div>
83
- <div style="font-size: 22px; font-weight: 800; color: #000000 !important;">{top_match[col_name]}</div>
84
- <div style="font-size: 14px; color: #333333 !important; font-weight: 600;">Match for: {closest_persona}</div>
85
  </div>
86
- <div style="text-align:right;">
87
- <div style="font-size: 28px; font-weight: 900; color: #2563eb !important;">{top_match[col_rating]}</div>
88
- <div style="font-size:12px; font-weight:bold; color: #000000 !important;">RATING</div>
89
  </div>
90
  </div>
91
- <hr style="border:0; border-top:1px solid #cbd5e1; margin: 15px 0;">
92
-
93
- <p style="color: #000000 !important; line-height:1.6; font-size: 16px; font-weight: 500; margin-top: 10px;">
94
- <i style="color: #000000 !important;">"{review_text}"</i>
95
  </p>
96
-
97
- <div style="margin-top:15px; font-size:13px; font-weight:700; color:#2563eb !important;">Match Confidence: {match_pct}%</div>
 
 
98
  </div>
99
  """
100
 
101
  # ==========================================
102
- # 3. APP UI & CSS FIX
103
  # ==========================================
 
104
  ven_css = """
105
- body { background-color: #0f172a !important; font-family: sans-serif !important; }
106
- /* Global White Text for Dark Mode */
107
- h1, h2, h3, h4, h5, h6 { color: white !important; }
108
- p, span, div, label { color: white; }
109
-
110
- /* Override: Force Black Text inside the Results Card */
111
- .gradio-html div { color: #000000 !important; }
112
- .gradio-html p { color: #000000 !important; }
113
- .gradio-html i { color: #000000 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  """
115
 
116
- with gr.Blocks(css=ven_css, title="VEN Project") as demo:
117
  gr.Markdown("# 🍔 VEN: Restaurant Matchmaker")
118
 
119
  with gr.Row():
120
- with gr.Column():
121
- in_budget = gr.Dropdown(["Budget-friendly", "Mid-range", "Premium"], label="Budget", value="Mid-range")
122
- in_diet = gr.Dropdown(["Anything", "Vegetarian", "Vegan", "Meat-lover"], label="Diet", value="Anything")
123
- in_company = gr.Radio(["Solo", "Date/Couple", "Friends", "Business"], label="With who?", value="Date/Couple")
124
- in_purpose = gr.Dropdown(["Casual dinner", "Special occasion", "Quick bite", "Professional meeting"], label="Occasion", value="Casual dinner")
125
- in_noise = gr.Radio(["Quiet/Intimate", "Moderate/Social", "Lively/Music"], label="Environment", value="Moderate/Social")
126
- btn = gr.Button("Find My Table", variant="primary")
127
-
128
- with gr.Column():
129
- output_ui = gr.HTML("<h4>Recommendation will appear here...</h4>")
 
130
 
131
- gr.Markdown("### 🚀 Quick Starters (One-Click)")
132
  gr.Examples(
133
  examples=[
134
  ["Budget-friendly", "Vegetarian", "Friends", "Quick bite", "Moderate/Social"],
@@ -138,9 +151,9 @@ with gr.Blocks(css=ven_css, title="VEN Project") as demo:
138
  inputs=[in_budget, in_diet, in_company, in_purpose, in_noise],
139
  outputs=output_ui,
140
  fn=run_ven_engine,
141
- cache_examples=True,
142
  )
143
-
144
  btn.click(run_ven_engine, inputs=[in_budget, in_diet, in_company, in_purpose, in_noise], outputs=output_ui)
145
 
146
  if __name__ == "__main__":
 
9
  # ==========================================
10
  # 1. SETUP & DATA LOADING
11
  # ==========================================
12
+ # File paths (Assuming files are in the same root directory as app.py on Hugging Face)
13
+ CSV_PATH = "cleaned_dataset_10k.csv"
14
+ PKL_PATH = "final_embeddings_10k.pkl"
15
 
16
+ if not os.path.exists(CSV_PATH) or not os.path.exists(PKL_PATH):
17
+ raise FileNotFoundError("Missing required data files. Please ensure CSV and PKL are uploaded.")
18
 
19
+ # Load the dataset
20
+ df = pd.read_csv(CSV_PATH)
21
  df.columns = [c.strip().lower().replace(' ', '_') for c in df.columns]
22
 
23
+ # Load pre-computed embeddings
24
+ with open(PKL_PATH, 'rb') as f:
 
 
 
 
 
 
 
 
 
 
 
25
  embedding_data = pickle.load(f)
26
+ dataset_embeddings = embedding_data['embeddings'] if isinstance(embedding_data, dict) else embedding_data
 
 
 
27
 
28
+ # Load the semantic model (MPNet for high accuracy)
29
  model = SentenceTransformer('sentence-transformers/all-mpnet-base-v2')
30
 
31
+ # Pre-calculate Persona Taste Profiles (Mean vectors)
32
  persona_profiles = {}
33
+ for persona in df['reviewer_persona'].unique():
34
+ if pd.isna(persona): continue
35
+ indices = df[df['reviewer_persona'] == persona].index
36
+ persona_vectors = dataset_embeddings[indices]
37
+ persona_profiles[persona] = np.mean(persona_vectors, axis=0)
 
 
 
 
 
38
 
39
  # ==========================================
40
+ # 2. CORE RECOMMENDATION ENGINE (HYBRID)
41
  # ==========================================
42
  def run_ven_engine(budget, dietary, company, purpose, noise):
43
+ """
44
+ Finds the best restaurant match using a combination of
45
+ Persona Matching and Contextual Semantic Similarity.
46
+ """
47
+ # Create the user's context string
48
  user_context = f"Searching for a {budget} experience, {dietary} friendly. Group: {company}. Occasion: {purpose}. Atmosphere: {noise}."
49
  query_vec = model.encode([user_context])
50
 
51
+ # Step A: Identify the closest Persona Profile
52
+ persona_sims = {p: cosine_similarity(query_vec, v.reshape(1, -1))[0][0]
53
+ for p, v in persona_profiles.items()}
54
+ closest_persona = max(persona_sims, key=persona_sims.get)
55
 
56
+ # Step B: Filter reviews by that persona
57
+ persona_indices = df[df['reviewer_persona'] == closest_persona].index
58
+ persona_embeddings = dataset_embeddings[persona_indices]
 
 
59
 
60
+ # Step C: Calculate Contextual Similarity for specific reviews within that persona
61
+ # This prevents getting the same result every time
62
+ sub_similarities = cosine_similarity(query_vec, persona_embeddings)[0]
63
 
64
+ persona_df = df.loc[persona_indices].copy()
65
+ persona_df['semantic_fit'] = sub_similarities
66
+ persona_df['norm_rating'] = persona_df['rating_score'] / 5.0
67
 
68
+ # HYBRID SCORE: 70% Contextual Fit + 30% Rating Quality
69
+ persona_df['final_score'] = (persona_df['semantic_fit'] * 0.7) + (persona_df['norm_rating'] * 0.3)
70
+
71
+ # Pick the top result after re-ranking
72
+ top_match = persona_df.sort_values(by='final_score', ascending=False).iloc[0]
73
+ match_pct = int(top_match['final_score'] * 100)
74
+
75
+ # Format Recommendation Card (HTML)
76
  return f"""
77
+ <div style="background: white; border-radius: 20px; padding: 25px; color: #0f172a !important; text-align: left; border-left: 10px solid #f97316; box-shadow: 0 10px 25px rgba(0,0,0,0.2);">
78
+ <div style="display:flex; justify-content:space-between; align-items: flex-start;">
79
+ <div style="flex: 1;">
80
+ <h2 style="margin:0; font-size: 24px; font-weight: 900; color: #0f172a !important;">{top_match['restaurant_name']}</h2>
81
+ <div style="font-size: 14px; color: #475569 !important; font-weight: 700; margin-top: 4px;">Matched for: {closest_persona} profile</div>
82
  </div>
83
+ <div style="text-align:right; background: #f8fafc; padding: 10px; border-radius: 12px; border: 1px solid #e2e8f0;">
84
+ <div style="font-size: 30px; font-weight: 950; color: #2563eb !important; line-height: 1;">{top_match['rating_score']}</div>
85
+ <div style="font-size:10px; font-weight:900; color: #64748b !important; letter-spacing: 1px; margin-top: 5px;">RATING</div>
86
  </div>
87
  </div>
88
+ <hr style="border:0; border-top: 1px solid #e2e8f0; margin: 15px 0;">
89
+ <p style="color: #1e293b !important; line-height:1.6; font-size: 16px; font-weight: 500;">
90
+ <i style="color: #334155 !important;">"{top_match['review_content_clean'][:200]}..."</i>
 
91
  </p>
92
+ <div style="margin-top:20px; display: flex; justify-content: space-between; align-items: center;">
93
+ <span style="font-size: 13px; font-weight: 800; color: #f97316;">VEN Match Confidence: {match_pct}%</span>
94
+ <span style="font-size: 11px; background: #0f172a; color: white; padding: 4px 10px; border-radius: 6px; font-weight: 700;">AI MATCH</span>
95
+ </div>
96
  </div>
97
  """
98
 
99
  # ==========================================
100
+ # 3. GRADIO UI SETUP (HF OPTIMIZED)
101
  # ==========================================
102
+ # CSS Fixes for Visibility and Theme consistency
103
  ven_css = """
104
+ .gradio-container { background-color: #0f172a !important; }
105
+ h1 { color: white !important; text-align: center; font-weight: 900 !important; font-size: 2.5rem !important; margin-bottom: 20px !important; }
106
+
107
+ /* Force input labels to be white and readable */
108
+ label span { color: white !important; font-weight: 700 !important; font-size: 15px !important; margin-bottom: 5px !important; }
109
+
110
+ /* Force Radio button text options to be white and bold */
111
+ .gr-radio label span { color: white !important; font-weight: 600 !important; }
112
+
113
+ /* Style the primary orange button */
114
+ .ven-button {
115
+ background-color: #f97316 !important;
116
+ color: white !important;
117
+ border: none !important;
118
+ font-weight: 900 !important;
119
+ font-size: 18px !important;
120
+ height: 50px !important;
121
+ border-radius: 12px !important;
122
+ }
123
+
124
+ /* Fix example table colors */
125
+ .gr-samples-table { background-color: #1e293b !important; color: white !important; }
126
  """
127
 
128
+ with gr.Blocks(css=ven_css, title="VEN - AI Restaurant Match") as demo:
129
  gr.Markdown("# 🍔 VEN: Restaurant Matchmaker")
130
 
131
  with gr.Row():
132
+ with gr.Column(scale=1):
133
+ with gr.Group():
134
+ in_budget = gr.Dropdown(["Budget-friendly", "Mid-range", "Premium"], label="1. Wallet Size", value="Mid-range")
135
+ in_diet = gr.Dropdown(["Anything", "Vegetarian", "Vegan", "Meat-lover"], label="2. Diet Preference", value="Anything")
136
+ in_company = gr.Radio(["Solo", "Date/Couple", "Friends", "Business"], label="3. Social Context", value="Date/Couple")
137
+ in_purpose = gr.Dropdown(["Casual dinner", "Special occasion", "Quick bite", "Professional meeting"], label="4. The Mission", value="Casual dinner")
138
+ in_noise = gr.Radio(["Quiet/Intimate", "Moderate/Social", "Lively/Music"], label="5. Vibe / Noise", value="Moderate/Social")
139
+ btn = gr.Button("Find My Table", variant="primary", elem_classes="ven-button")
140
+
141
+ with gr.Column(scale=1):
142
+ output_ui = gr.HTML("<div style='text-align:center; padding:100px; color:#64748b; font-weight:700; border: 2px dashed #1e293b; border-radius: 20px;'>Your personalized AI match will appear here</div>")
143
 
144
+ gr.Markdown("### 🚀 Quick Vibe Starters")
145
  gr.Examples(
146
  examples=[
147
  ["Budget-friendly", "Vegetarian", "Friends", "Quick bite", "Moderate/Social"],
 
151
  inputs=[in_budget, in_diet, in_company, in_purpose, in_noise],
152
  outputs=output_ui,
153
  fn=run_ven_engine,
154
+ cache_examples=False,
155
  )
156
+
157
  btn.click(run_ven_engine, inputs=[in_budget, in_diet, in_company, in_purpose, in_noise], outputs=output_ui)
158
 
159
  if __name__ == "__main__":