Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import pandas as pd | |
| import pickle | |
| import numpy as np | |
| import os | |
| import random | |
| import base64 | |
| from huggingface_hub import InferenceClient | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| from IO_pipeline import RecipeDigitalizerPipeline | |
| # ========================================== | |
| # 1. SETUP & DATA LOADING | |
| # ========================================== | |
| hf_token = os.getenv("HF_TOKEN") | |
| API_MODEL = "BAAI/bge-small-en-v1.5" | |
| client = InferenceClient(token=hf_token) if hf_token else None | |
| print("โณ Loading Data...") | |
| try: | |
| df_recipes = pd.read_csv('RecipeData_10K.csv') | |
| with open('recipe_embeddings.pkl', 'rb') as f: | |
| data = pickle.load(f) | |
| if isinstance(data, dict): | |
| stored_embeddings = np.array(data['embeddings']) | |
| elif isinstance(data, pd.DataFrame): | |
| target_col = next((c for c in ['embedding', 'embeddings', 'vectors'] if c in data.columns), None) | |
| stored_embeddings = np.vstack(data[target_col].values) if target_col else data | |
| else: | |
| stored_embeddings = data | |
| print("โ Data Loaded!") | |
| except Exception as e: | |
| print(f"โ Error loading data: {e}") | |
| df_recipes = pd.DataFrame({'Title': [], 'Raw_Output': []}) | |
| stored_embeddings = None | |
| # ========================================== | |
| # 2. HELPER: IMAGE TO BASE64 | |
| # ========================================== | |
| def image_to_base64(image_path): | |
| if not os.path.exists(image_path): | |
| return "" | |
| with open(image_path, "rb") as img_file: | |
| return base64.b64encode(img_file.read()).decode('utf-8') | |
| # Load local images if they exist, otherwise use placeholders | |
| try: | |
| logo_b64 = image_to_base64("logo.jpg") | |
| profile_b64 = image_to_base64("232px-Tv_the_muppet_show_bein_green.jpg") | |
| process_b64 = image_to_base64("preview of process.jpg") | |
| except: | |
| logo_b64 = "" | |
| profile_b64 = "" | |
| process_b64 = "" | |
| # ========================================== | |
| # 3. BACKEND LOGIC | |
| # ========================================== | |
| def get_embedding_via_api(text): | |
| if not client: raise ValueError("HF_TOKEN missing") | |
| response = client.feature_extraction(text, model=API_MODEL) | |
| return np.array(response) | |
| def find_similar_recipes_list(query_text): | |
| if stored_embeddings is None: return ["Database error."] * 3 | |
| query_vec = get_embedding_via_api("Represent this recipe for retrieving similar dishes: " + query_text) | |
| if len(query_vec.shape) == 1: query_vec = query_vec.reshape(1, -1) | |
| scores = cosine_similarity(query_vec, stored_embeddings)[0] | |
| top_indices = scores.argsort()[-3:][::-1] | |
| results_list = [] | |
| for idx in top_indices: | |
| score = scores[idx] | |
| row = df_recipes.iloc[idx] | |
| title = row['Title'] | |
| desc = str(row['Raw_Output']) | |
| score_display = f"{score:.0%}" | |
| # HTML styled as a "Suggested Post" | |
| card_content = ( | |
| f"<div class='suggested-header'>Suggested for you ยท {score_display} Match</div>" | |
| f"### ๐ฅ {title}\n" | |
| f"<div class='sim-scroll'>{desc}</div>" | |
| ) | |
| results_list.append(card_content) | |
| while len(results_list) < 3: | |
| results_list.append("") | |
| return results_list | |
| def format_recipe(json_data): | |
| if "error" in json_data: return f"Error: {json_data['error']}", "" | |
| title = json_data.get("title", "Unknown") | |
| ing = "\n".join([f"- {x}" for x in json_data.get("ingredients", [])]) | |
| inst = "\n".join([f"{i+1}. {x}" for i, x in enumerate(json_data.get("instructions", []))]) | |
| text = f"๐ฝ๏ธ {title}\n\n๐ INGREDIENTS:\n{ing}\n\n๐ณ INSTRUCTIONS:\n{inst}" | |
| return text, f"{title} {ing} {inst}" | |
| def ui_update_pipeline(image_path): | |
| if not hf_token: | |
| return "Error: HF_TOKEN missing", "", gr.update(), gr.update(), "", gr.update(), "" | |
| try: | |
| os.environ["HF_TOKEN"] = hf_token | |
| digitizer = RecipeDigitalizerPipeline() | |
| json_res = digitizer.run_pipeline(image_path) | |
| readable, query = format_recipe(json_res) | |
| if query: | |
| sim_list = find_similar_recipes_list(query) | |
| else: | |
| sim_list = ["No query generated.", "", ""] | |
| return (readable, sim_list[0], gr.update(visible=True), gr.update(visible=True), sim_list[1], gr.update(visible=True), sim_list[2]) | |
| except Exception as e: | |
| return f"Error: {e}", "Error", gr.update(), gr.update(), "", gr.update(), "" | |
| # ========================================== | |
| # 4. FACEBOOK UI THEME & CSS | |
| # ========================================== | |
| theme = gr.themes.Default( | |
| primary_hue="blue", | |
| radius_size="sm", | |
| font=['Helvetica', 'Arial', 'sans-serif'] | |
| ) | |
| facebook_css = """ | |
| body { background-color: #f0f2f5; margin: 0; padding: 0; } | |
| .gradio-container { background-color: #f0f2f5 !important; max-width: 100% !important; margin: 0 !important; padding: 0 !important; } | |
| footer { display: none !important; } | |
| /* 1. STICKY NAVBAR */ | |
| .fb-navbar { | |
| background: white; | |
| height: 56px; | |
| padding: 0 16px; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.1); | |
| display: flex; | |
| align-items: center; | |
| position: sticky; | |
| top: 0; | |
| z-index: 999; | |
| justify-content: space-between; | |
| } | |
| .fb-logo { | |
| color: #1877F2; | |
| font-size: 28px; | |
| font-weight: bold; | |
| letter-spacing: -0.5px; | |
| margin-right: 20px; | |
| } | |
| .fb-search { | |
| background: #f0f2f5; | |
| border-radius: 50px; | |
| padding: 10px 20px; | |
| color: #65676B; | |
| width: 250px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| /* 2. LAYOUT COLUMNS */ | |
| .main-layout { display: flex; justify-content: center; padding-top: 20px; gap: 20px; } | |
| .col-left { width: 300px; display: none; } /* Hidden on mobile */ | |
| .col-feed { width: 600px; max-width: 100%; } | |
| .col-right { width: 300px; display: none; } | |
| /* 3. CARD STYLING */ | |
| .fb-card { | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.2); | |
| margin-bottom: 15px; | |
| overflow: hidden; | |
| padding: 12px 16px; | |
| } | |
| /* 4. NAVIGATION BUTTONS */ | |
| .nav-btn { | |
| text-align: left !important; | |
| justify-content: flex-start !important; | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| color: #050505 !important; | |
| font-weight: 600 !important; | |
| font-size: 15px !important; | |
| padding: 10px 8px !important; | |
| border-radius: 8px !important; | |
| margin-bottom: 5px !important; | |
| } | |
| .nav-btn:hover { background-color: #e4e6eb !important; } | |
| .nav-btn.selected { background-color: #e7f3ff !important; color: #1877f2 !important; } | |
| /* 5. POST HEADER */ | |
| .post-header { display: flex; align-items: center; margin-bottom: 12px; } | |
| .user-avatar { width: 40px; height: 40px; border-radius: 50%; background: #ddd; margin-right: 10px; object-fit: cover; } | |
| .post-info { display: flex; flex-direction: column; } | |
| .post-author { font-weight: 600; color: #050505; font-size: 15px; } | |
| .post-meta { font-size: 13px; color: #65676B; } | |
| /* 6. POST ACTIONS */ | |
| .post-actions { | |
| border-top: 1px solid #ced0d4; | |
| margin-top: 10px; | |
| padding-top: 5px; | |
| display: flex; | |
| justify-content: space-around; | |
| } | |
| .action-btn { background: transparent !important; color: #65676B !important; box-shadow: none !important; } | |
| .action-btn:hover { background: #f2f2f2 !important; } | |
| /* 7. CONTACTS */ | |
| .contact-row { | |
| display: flex; align-items: center; padding: 8px; border-radius: 8px; cursor: pointer; | |
| } | |
| .contact-row:hover { background-color: #e4e6eb; } | |
| .contact-name { font-weight: 500; font-size: 14px; margin-left: 10px; color: #050505; } | |
| /* UTILS */ | |
| .sim-scroll { height: 200px; overflow-y: auto; font-size: 13px; color: #050505; } | |
| .suggested-header { font-size: 12px; font-weight: bold; color: #65676B; margin-bottom: 5px; } | |
| /* Media Queries */ | |
| @media (min-width: 1100px) { .col-left, .col-right { display: block; } } | |
| """ | |
| # ========================================== | |
| # 5. LAYOUT CONSTRUCTION | |
| # ========================================== | |
| with gr.Blocks(title="Legacy Kitchen", css=facebook_css, theme=theme) as demo: | |
| # --- 1. FACEBOOK NAVBAR --- | |
| gr.HTML(f""" | |
| <div class="fb-navbar"> | |
| <div style="display:flex; align-items:center;"> | |
| <div class="fb-logo">facebook</div> | |
| <div class="fb-search">๐ Search Legacy Kitchen</div> | |
| </div> | |
| <div style="display:flex; gap:10px;"> | |
| <div style="width:40px;height:40px;background:#e4e6eb;border-radius:50%;display:flex;align-items:center;justify-content:center;">โ</div> | |
| <div style="width:40px;height:40px;background:#e4e6eb;border-radius:50%;display:flex;align-items:center;justify-content:center;">๐ฌ</div> | |
| <img src="data:image/jpeg;base64,{profile_b64}" style="width:40px; height:40px; border-radius:50%; object-fit:cover;"> | |
| </div> | |
| </div> | |
| """) | |
| # --- 2. MAIN 3-COLUMN LAYOUT --- | |
| with gr.Row(elem_classes=["main-layout"]): | |
| # === LEFT COLUMN (Sidebar Navigation) === | |
| with gr.Column(elem_classes=["col-left"]): | |
| gr.HTML(f""" | |
| <div class="contact-row"> | |
| <img src="data:image/jpeg;base64,{profile_b64}" style="width:36px; height:36px; border-radius:50%;"> | |
| <div class="contact-name">Welcome User</div> | |
| </div> | |
| """) | |
| nav_digital = gr.Button("โจ AI Digitizer (Create)", elem_classes=["nav-btn", "selected"]) | |
| nav_feed = gr.Button("๐ฐ News Feed", elem_classes=["nav-btn"]) | |
| nav_saved = gr.Button("๐ Saved Recipes", elem_classes=["nav-btn"]) | |
| nav_about = gr.Button("โน๏ธ About Project", elem_classes=["nav-btn"]) | |
| gr.HTML("<hr style='border:0; border-top:1px solid #ced0d4; margin: 10px 0;'>") | |
| gr.Markdown("### Your Shortcuts") | |
| gr.Markdown("๐ฅ Culinary Arts Group\n\n๐ฅง Grandmother's Secrets\n\n๐ฅ Healthy Eating") | |
| # === CENTER COLUMN (The Feed & App Logic) === | |
| with gr.Column(elem_classes=["col-feed"]): | |
| # --- VIEW 1: AI DIGITIZER (Styled as "Create Post") --- | |
| with gr.Group(visible=True) as digitalizer_view: | |
| # "Create Post" Card | |
| with gr.Group(elem_classes=["fb-card"]): | |
| gr.Markdown("### Create Post") | |
| gr.HTML("<hr style='border:0; border-top:1px solid #e4e6eb; margin: 10px 0;'>") | |
| with gr.Row(): | |
| input_img = gr.Image(type="filepath", label="Add to your post", height=250, container=True) | |
| gr.HTML("<div style='margin-top:10px; font-size:14px; color:#65676B;'>Add to your post: ๐ข Photo/Video ๐ค Tag People ๐ Location</div>") | |
| magic_btn = gr.Button("Post", variant="primary") # Blue button | |
| # Examples | |
| gr.Examples( | |
| examples=[["quick_tries_images/applecrisp.jpg"], ["quick_tries_images/meatballs recipe.jpg"]], | |
| inputs=input_img, label="Quick Try (Click to load image)" | |
| ) | |
| # Output ("The Resulting Post") | |
| with gr.Group(elem_classes=["fb-card"]): | |
| gr.HTML(""" | |
| <div class="post-header"> | |
| <div class="user-avatar" style="background:#1877F2;"></div> | |
| <div class="post-info"> | |
| <div class="post-author">Legacy Kitchen AI</div> | |
| <div class="post-meta">Just now ยท ๐</div> | |
| </div> | |
| </div> | |
| """) | |
| out_text = gr.Textbox(label="Transcription Result", placeholder="Your digitized recipe text will appear here...", lines=10, show_label=False) | |
| with gr.Row(elem_classes=["post-actions"]): | |
| gr.Button("๐ Like", elem_classes=["action-btn"], size="sm") | |
| gr.Button("๐ฌ Comment", elem_classes=["action-btn"], size="sm") | |
| gr.Button("โช Share", elem_classes=["action-btn"], size="sm") | |
| # Similar Recipes (Styled as "Suggested Posts") | |
| gr.Markdown("### Suggested for you") | |
| # Sim Result 1 | |
| with gr.Group(elem_classes=["fb-card"], visible=False) as c1_box: | |
| sim1 = gr.Markdown("Similar Recipe 1") | |
| with gr.Row(elem_classes=["post-actions"], visible=False) as c1_btns: | |
| gr.Button("Save", elem_classes=["action-btn"], size="sm") | |
| # Sim Result 2 | |
| with gr.Group(elem_classes=["fb-card"], visible=False) as c2_box: | |
| sim2 = gr.Markdown("Similar Recipe 2") | |
| with gr.Row(elem_classes=["post-actions"]): | |
| gr.Button("Save", elem_classes=["action-btn"], size="sm") | |
| # Sim Result 3 | |
| with gr.Group(elem_classes=["fb-card"], visible=False) as c3_box: | |
| sim3 = gr.Markdown("Similar Recipe 3") | |
| with gr.Row(elem_classes=["post-actions"]): | |
| gr.Button("Save", elem_classes=["action-btn"], size="sm") | |
| magic_btn.click(ui_update_pipeline, input_img, [out_text, sim1, c1_btns, c2_box, sim2, c3_box, sim3]) | |
| # --- VIEW 2: FEED --- | |
| with gr.Column(visible=False) as feed_view: | |
| if not df_recipes.empty: | |
| feed_samples = df_recipes.sample(10) | |
| for index, row in feed_samples.iterrows(): | |
| user_name = random.choice(["Grandma Rose", "Chef Mike", "Sarah J."]) | |
| emoji = random.choice(["๐ฅ", "๐ฅ", "๐ฐ", "๐ฎ"]) | |
| post_time = random.choice(["2h", "3h", "6h", "Yesterday"]) | |
| with gr.Group(elem_classes=["fb-card"]): | |
| # Custom HTML Header for the card | |
| gr.HTML(f""" | |
| <div class="post-header"> | |
| <div class="user-avatar" style="background:#e4e6eb; display:flex; align-items:center; justify-content:center; font-size:20px;">{emoji}</div> | |
| <div class="post-info"> | |
| <div class="post-author">{user_name}</div> | |
| <div class="post-meta">{post_time} ยท ๐</div> | |
| </div> | |
| </div> | |
| """) | |
| gr.Markdown(f"**{row['Title']}**") | |
| gr.Markdown(f"{str(row['Raw_Output'])[:300]}... <span style='color:#1877F2; cursor:pointer;'>See more</span>") | |
| # Action Bar | |
| with gr.Row(elem_classes=["post-actions"]): | |
| gr.Button("๐ Like", elem_classes=["action-btn"], size="sm") | |
| gr.Button("๐ฌ Comment", elem_classes=["action-btn"], size="sm") | |
| gr.Button("โช Share", elem_classes=["action-btn"], size="sm") | |
| else: | |
| gr.Markdown("โ ๏ธ Database is empty.") | |
| # --- VIEW 3: SAVED RECIPES --- | |
| with gr.Column(visible=False) as saved_view: | |
| gr.Markdown("### ๐ Saved Items") | |
| if not df_recipes.empty: | |
| saved_batch = df_recipes.head(20) | |
| for index, row in saved_batch.iterrows(): | |
| with gr.Group(elem_classes=["fb-card"]): | |
| with gr.Row(): | |
| gr.HTML("<div style='width:80px; height:80px; background:#eee; border-radius:8px;'></div>") | |
| with gr.Column(): | |
| gr.Markdown(f"**{row['Title']}**") | |
| gr.HTML("<span style='color:#65676B; font-size:12px;'>Saved from News Feed</span>") | |
| gr.Button("View Collection", size="sm", variant="secondary") | |
| # --- VIEW 4: ABOUT --- | |
| with gr.Group(visible=False) as about_view: | |
| with gr.Group(elem_classes=["fb-card"]): | |
| gr.Markdown("# About Legacy Kitchen") | |
| gr.Markdown("Developed by **Shahar Firshtman** and **Lior Feinstein**.") | |
| gr.HTML(f""" | |
| <div style="margin-top: 20px;"> | |
| <img src="data:image/jpeg;base64,{process_b64}" style="width: 100%; border-radius: 8px;"> | |
| </div> | |
| """) | |
| # === RIGHT COLUMN (Contacts / Sponsored) === | |
| with gr.Column(elem_classes=["col-right"]): | |
| gr.Markdown("### Sponsored") | |
| with gr.Group(elem_classes=["fb-card"]): | |
| gr.Markdown("**Culinary School**\nJoin 10,000+ students learning to cook today!") | |
| gr.Image("https://picsum.photos/300/150", show_label=False, interactive=False, height=150) | |
| gr.HTML("<hr style='border:0; border-top:1px solid #ced0d4; margin: 10px 0;'>") | |
| gr.Markdown("### Contacts") | |
| # Fake contacts list | |
| contacts = ["Elon Musk", "Gordon Ramsay", "Jamie Oliver", "Martha Stewart"] | |
| for contact in contacts: | |
| gr.HTML(f""" | |
| <div class="contact-row"> | |
| <div style="position:relative;"> | |
| <img src="https://ui-avatars.com/api/?name={contact}&background=random" style="width:36px; height:36px; border-radius:50%;"> | |
| <div style="position:absolute; bottom:0; right:0; width:10px; height:10px; background:#31a24c; border-radius:50%; border:2px solid white;"></div> | |
| </div> | |
| <div class="contact-name">{contact}</div> | |
| </div> | |
| """) | |
| # ========================================== | |
| # 6. JAVASCRIPT NAVIGATION LOGIC | |
| # ========================================== | |
| def go_digi(): | |
| return ( | |
| gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), | |
| gr.update(elem_classes=["nav-btn", "selected"]), gr.update(elem_classes=["nav-btn"]), gr.update(elem_classes=["nav-btn"]), gr.update(elem_classes=["nav-btn"]) | |
| ) | |
| def go_feed(): | |
| return ( | |
| gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), | |
| gr.update(elem_classes=["nav-btn"]), gr.update(elem_classes=["nav-btn", "selected"]), gr.update(elem_classes=["nav-btn"]), gr.update(elem_classes=["nav-btn"]) | |
| ) | |
| def go_saved(): | |
| return ( | |
| gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), | |
| gr.update(elem_classes=["nav-btn"]), gr.update(elem_classes=["nav-btn"]), gr.update(elem_classes=["nav-btn", "selected"]), gr.update(elem_classes=["nav-btn"]) | |
| ) | |
| def go_about(): | |
| return ( | |
| gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), | |
| gr.update(elem_classes=["nav-btn"]), gr.update(elem_classes=["nav-btn"]), gr.update(elem_classes=["nav-btn"]), gr.update(elem_classes=["nav-btn", "selected"]) | |
| ) | |
| outputs_ui = [digitalizer_view, feed_view, saved_view, about_view, nav_digital, nav_feed, nav_saved, nav_about] | |
| nav_digital.click(go_digi, None, outputs_ui) | |
| nav_feed.click(go_feed, None, outputs_ui) | |
| nav_saved.click(go_saved, None, outputs_ui) | |
| nav_about.click(go_about, None, outputs_ui) | |
| if __name__ == "__main__": | |
| demo.launch() |