Spaces:
Sleeping
Sleeping
| 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() |