Bite_Wise_Final / app.py
galcomis's picture
Update app.py
3bfb218 verified
import gradio as gr
import pandas as pd
import numpy as np
import pickle
import os
import zipfile
import re
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
# --- 1. Automatic ZIP Extraction ---
zip_filename = 'Dishes_Images_Zip_Final.zip'
if os.path.exists(zip_filename):
try:
with zipfile.ZipFile(zip_filename, 'r') as zip_ref:
zip_ref.extractall('.')
except Exception as e:
print(f"Extraction error: {e}")
# --- 2. Load Data and Models ---
model = SentenceTransformer('all-MiniLM-L6-v2')
df = pd.read_csv('bitewise_clean_dataset.csv')
with open('BiteWise_Dish_Embeddings.pkl', 'rb') as f:
dish_embeddings = pickle.load(f)
with open('BiteWise_User_Embeddings.pkl', 'rb') as f:
user_embeddings = pickle.load(f)
min_len = min(len(df), len(dish_embeddings), len(user_embeddings))
df = df.iloc[:min_len].copy()
dish_embeddings = dish_embeddings[:min_len]
user_embeddings = user_embeddings[:min_len]
# Create a master list of all available image paths once at startup
ALL_IMAGE_PATHS = []
for root, _, files in os.walk("."):
for f in files:
if f.lower().endswith(('.jpg', '.jpeg', '.png')):
ALL_IMAGE_PATHS.append(os.path.join(root, f))
def find_any_matching_image(dish_name):
"""Robust image search logic based on keyword matching."""
name_clean = str(dish_name).lower()
keywords = [kw for kw in re.findall(r'\w+', name_clean) if len(kw) > 2]
if not keywords: return None
# Try 1: Exact keyword match
for p in ALL_IMAGE_PATHS:
f_name = os.path.basename(p).lower()
if all(kw in f_name for kw in keywords): return p
# Try 2: Partial match
if len(keywords) >= 2:
for p in ALL_IMAGE_PATHS:
f_name = os.path.basename(p).lower()
if keywords[0] in f_name and keywords[1] in f_name: return p
# Try 3: Primary keyword match
primary_kw = max(keywords, key=len)
for p in ALL_IMAGE_PATHS:
if primary_kw in os.path.basename(p).lower(): return p
return None
def get_hybrid_recommendations(user_query, user_name, city, hobbies, style, alpha=0.65):
if not user_query or not user_name:
return [None, "### ⚠️ Identity required."] * 3
user_profile_text = f"{user_name} from {city} loves {hobbies} and has a {style} style."
# NEW: Create embeddings for both query and persona
query_emb = model.encode([user_query])
profile_emb = model.encode([user_profile_text])
# Calculate similarities (The Hybrid Logic)
content_sim = cosine_similarity(query_emb, dish_embeddings).flatten()
user_sim = cosine_similarity(profile_emb, user_embeddings).flatten()
# Weighting: 65% Craving (Content) + 35% Persona (Demographics)
raw_scores = (alpha * content_sim) + (1 - alpha) * user_sim
# Normalizing scores for display (74%-99%)
min_s, max_s = raw_scores.min(), raw_scores.max()
scaled_scores = 0.74 + (raw_scores - min_s) * (0.25) / (max_s - min_s) if max_s > min_s else raw_scores
df['final_score'] = scaled_scores
unique_df = df.sort_values(by='final_score', ascending=False).drop_duplicates(subset=['dish_name'])
top_results = unique_df.head(3)
output = []
for _, row in top_results.iterrows():
image_path = find_any_matching_image(row['dish_name'])
# NEW: Generate the 'Culinary Twin' profile from the dataset metadata
# This provides transparency on WHY this dish was recommended
twin_info = f"A {row['user_age']} year old from {row['user_origin']}, with a {row['user_fashion_style']} style."
res_text = f"""
<div style='font-family: serif; color: #4a3f35; padding: 15px; background: #fffcf7; border: 1px solid #e0d8c3; margin-bottom: 10px;'>
<h2 style='margin-bottom: 2px; color: #5d4037; font-weight: normal; letter-spacing: 1px;'>{row['dish_name'].upper()}</h2>
<p style='font-style: italic; margin-top: 0; color: #8c7b6c;'>{row['restaurant_name']} β€” {row['food_vibe']}</p>
<div style='background-color: #f0ece2; padding: 10px; margin: 10px 0; border-radius: 4px; font-size: 0.9em; border-left: 4px solid #5d4037;'>
<strong style='color: #5d4037;'>πŸ‘€ Recommended by your Culinary Twin:</strong><br>
{twin_info}
</div>
<p style='font-size: 0.8em; letter-spacing: 1.5px;'>MATCH SCORE: {row['final_score']:.2%} | ⭐ {row['rating']}</p>
<hr style='border: 0; border-top: 1px solid #f0ece2; margin: 12px 0;'>
<p style='line-height: 1.6;'><b>The Vision:</b> {row['visual_description']}</p>
<p style='line-height: 1.6;'><b>The Experience:</b> {row['taste_review']}</p>
</div>
"""
output.extend([image_path, res_text])
return output
def log_journal_entry(dish, restaurant, rating, experience):
return f"### βœ… Entry for '{dish}' logged in your Journal."
# --- 3. CUSTOM BOUTIQUE CSS ---
custom_css = """
.gradio-container {background-color: #f4f1ea !important; color: #4a3f35 !important;}
.tabs {border: none !important;}
.tab-nav {border-bottom: 1px solid #dcd7c9 !important; justify-content: center !important; gap: 40px !important;}
.tab-nav button {font-family: 'Georgia', serif !important; color: #8c7b6c !important; background: transparent !important; border: none !important; font-size: 1.1em !important;}
.tab-nav button.selected {color: #5d4037 !important; border-bottom: 2px solid #5d4037 !important; font-weight: bold !important;}
input, textarea, .dropdown {background-color: #fdfdfb !important; border: 1px solid #dcd7c9 !important; border-radius: 0px !important; color: #4a3f35 !important;}
input:focus, textarea:focus {border-color: #5d4037 !important; outline: none !important; box-shadow: 0 0 0 1px #5d4037 !important;}
.primary {background-color: #5d4037 !important; color: #f4f1ea !important; border-radius: 0px !important; font-family: 'Georgia', serif !important; text-transform: uppercase; letter-spacing: 3px;}
.gr-image {border: 8px solid #fff !important; box-shadow: 0 10px 25px rgba(0,0,0,0.08) !important;}
footer {display: none !important;}
"""
with gr.Blocks(css=custom_css) as demo:
gr.Markdown("<h1 style='text-align: center; font-family: serif; color: #5d4037; letter-spacing: 8px; padding-top: 30px;'>BITEWISE</h1>")
gr.Markdown("<p style='text-align: center; font-style: italic; color: #8c7b6c; margin-bottom: 40px;'>β€” A Curation of Taste and Sentiment β€”</p>")
with gr.Tabs():
with gr.TabItem("I. THE PERSONA"):
with gr.Row():
u_name = gr.Textbox(label="Full Name")
u_city = gr.Dropdown(choices=sorted(df['user_origin'].unique().tolist()), label="City")
with gr.Row():
u_hobbies = gr.Dropdown(choices=sorted(df['user_hobbies'].unique().tolist()), label="Pursuit")
u_style = gr.Dropdown(choices=sorted(df['user_fashion_style'].unique().tolist()), label="Aesthetic")
save_btn = gr.Button("Establish Identity", variant="primary")
save_msg = gr.Markdown()
save_btn.click(lambda x: f"<p style='text-align: center; font-style: italic;'>Welcome, {x}. Identity stored.</p>", inputs=u_name, outputs=save_msg)
with gr.TabItem("II. THE DISCOVERY"):
u_query = gr.Textbox(label="WHAT DO YOU CRAVE?", placeholder="Describe a flavor...")
search_btn = gr.Button("Search Archive", variant="primary")
with gr.Row():
with gr.Column(): im1 = gr.Image(show_label=False, height=400); tx1 = gr.HTML()
with gr.Column(): im2 = gr.Image(show_label=False, height=400); tx2 = gr.HTML()
with gr.Column(): im3 = gr.Image(show_label=False, height=400); tx3 = gr.HTML()
search_btn.click(get_hybrid_recommendations, inputs=[u_query, u_name, u_city, u_hobbies, u_style], outputs=[im1, tx1, im2, tx2, im3, tx3])
with gr.TabItem("III. THE JOURNAL"):
gr.Markdown("<p style='text-align: center; font-family: serif; color: #5d4037;'>SHARE YOUR CULINARY REFLECTIONS</p>")
with gr.Row():
j_dish = gr.Textbox(label="Dish Visited")
j_rest = gr.Textbox(label="Restaurant")
j_rating = gr.Slider(minimum=1, maximum=5, step=0.5, label="Sentiment Rating")
j_exp = gr.TextArea(label="Personal Reflection", placeholder="Describe the soul of the dish...")
submit_btn = gr.Button("Commit Entry", variant="primary")
j_msg = gr.Markdown()
submit_btn.click(log_journal_entry, inputs=[j_dish, j_rest, j_rating, j_exp], outputs=j_msg)
if __name__ == "__main__":
demo.launch()