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 "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" | |
| with open(image_path, "rb") as img_file: | |
| return base64.b64encode(img_file.read()).decode('utf-8') | |
| logo_b64 = image_to_base64("logo.jpg") | |
| profile_b64 = image_to_base64("232px-Tv_the_muppet_show_bein_green.jpg") | |
| # ========================================== | |
| # 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(query_text): | |
| if stored_embeddings is None: return "Database error." | |
| 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 = "" | |
| for idx in top_indices: | |
| row = df_recipes.iloc[idx] | |
| desc = str(row['Raw_Output'])[:150] + "..." | |
| results += f"๐ Match: {row['Title']}\n๐ {desc}\n\n" | |
| return results | |
| 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 magic_pipeline(image_path): | |
| if not hf_token: return "Error: HF_TOKEN missing", "" | |
| try: | |
| os.environ["HF_TOKEN"] = hf_token | |
| digitizer = RecipeDigitalizerPipeline() | |
| json_res = digitizer.run_pipeline(image_path) | |
| readable, query = format_recipe(json_res) | |
| similar = find_similar_recipes(query) if query else "No search query." | |
| return readable, similar | |
| except Exception as e: | |
| return f"Error: {e}", f"Error: {e}" | |
| # ========================================== | |
| # 4. MODERN UI THEME & CSS | |
| # ========================================== | |
| theme = gr.themes.Soft( | |
| primary_hue="indigo", | |
| secondary_hue="blue", | |
| neutral_hue="slate", | |
| font=[gr.themes.GoogleFont('Inter'), 'ui-sans-serif', 'system-ui'] | |
| ) | |
| modern_css = """ | |
| body, .gradio-container { background-color: #f0f2f5; } | |
| /* Sticky Header */ | |
| .custom-header { | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| border-bottom: 1px solid #e4e6eb; | |
| padding: 10px 20px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| position: sticky; | |
| top: 0; | |
| z-index: 1000; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| } | |
| .logo-area { display: flex; align-items: center; gap: 15px; } | |
| .logo-img { height: 120px; width: 120px; border-radius: 12px; object-fit: cover; border: 1px solid #ddd; } | |
| .app-name { | |
| font-weight: 800; | |
| font-size: 28px; | |
| background: -webkit-linear-gradient(45deg, #1877f2, #6b21a8); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| /* Sidebar Navigation */ | |
| .nav-btn { | |
| text-align: left !important; | |
| justify-content: flex-start !important; | |
| background: transparent !important; | |
| border: none !important; | |
| box-shadow: none !important; | |
| color: #65676b !important; | |
| font-weight: 600 !important; | |
| font-size: 16px !important; | |
| padding: 12px 16px !important; | |
| border-radius: 10px !important; | |
| transition: all 0.2s ease; | |
| } | |
| .nav-btn:hover { background-color: #e4e6eb !important; color: #050505 !important; } | |
| /* Selected State */ | |
| .nav-btn.selected { | |
| background-color: #e7f3ff !important; | |
| color: #1877f2 !important; | |
| border-left: 4px solid #1877f2 !important; | |
| } | |
| /* Feed Cards */ | |
| .content-card { | |
| background: white; | |
| border-radius: 12px; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.1); | |
| border: 1px solid #ddd; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| transition: transform 0.2s; | |
| } | |
| .content-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); transform: translateY(-2px); } | |
| /* Feed User Info */ | |
| .feed-user-row { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; } | |
| .feed-avatar { width: 40px; height: 40px; background: #e4e6eb; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; } | |
| .feed-meta { display: flex; flex-direction: column; } | |
| .feed-name { font-weight: 700; color: #050505; font-size: 15px; } | |
| .feed-time { font-size: 12px; color: #65676b; } | |
| /* Innovation/Digitalizer Styling */ | |
| .digitalizer-container { | |
| background: linear-gradient(135deg, #ffffff 0%, #f0f7ff 100%); | |
| border: 1px solid #1877f2; | |
| } | |
| /* Right Side Trending Box */ | |
| .trend-box { | |
| background:white; | |
| padding:10px; | |
| border-radius:8px; | |
| margin-bottom:10px; | |
| box-shadow:0 1px 2px rgba(0,0,0,0.1); | |
| transition: background 0.2s; | |
| } | |
| .trend-box:hover { background: #f0f2f5; cursor: pointer; } | |
| """ | |
| # ========================================== | |
| # 5. LAYOUT CONSTRUCTION | |
| # ========================================== | |
| with gr.Blocks(title="Legacy Kitchen") as demo: | |
| # --- HEADER --- | |
| gr.HTML(f""" | |
| <div class="custom-header"> | |
| <div class="logo-area"> | |
| <img src="data:image/jpeg;base64,{logo_b64}" class="logo-img"> | |
| <span class="app-name">Legacy Kitchen</span> | |
| </div> | |
| <div style="color: #65676b; font-weight: 600;">v2.2 Beta</div> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| # --- LEFT SIDEBAR (Navigation) --- | |
| with gr.Column(scale=1, min_width=200): | |
| # My Profile Section | |
| gr.HTML(f""" | |
| <div style="display:flex; align-items:center; padding: 10px 10px 5px 10px;"> | |
| <img src="data:image/jpeg;base64,{profile_b64}" style="width:40px; height:40px; border-radius:50%; margin-right:10px; object-fit:cover;"> | |
| <b style="font-size: 16px;">My Profile</b> | |
| </div> | |
| """) | |
| # Separator | |
| gr.HTML("<hr style='border: 0; border-top: 1px solid #e4e6eb; margin: 10px 0 20px 0;'>") | |
| # FIX: Digitalizer is now FIRST and SELECTED by default | |
| nav_digital = gr.Button("โจ AI Digitizer", elem_classes=["nav-btn", "selected"]) | |
| nav_feed = gr.Button("๐ฐ News Feed", elem_classes=["nav-btn"]) | |
| nav_about = gr.Button("โน๏ธ About", elem_classes=["nav-btn"]) | |
| # --- CENTER CONTENT --- | |
| with gr.Column(scale=3): | |
| # === VIEW 1: AI DIGITALIZER (Now DEFAULT: visible=True) === | |
| with gr.Group(visible=True) as digitalizer_view: | |
| gr.Markdown("## ๐ธ Recipe Digitalizer", elem_classes=["content-card"]) | |
| with gr.Row(): | |
| with gr.Column(elem_classes=["content-card", "digitalizer-container"]): | |
| input_img = gr.Image(type="filepath", label="Upload Handwritten Note", height=300) | |
| magic_btn = gr.Button("โจ Convert to Text", variant="primary", size="lg") | |
| with gr.Column(elem_classes=["content-card"]): | |
| out_text = gr.Textbox(label="Digitized Text", lines=6, interactive=False) | |
| out_sim = gr.Textbox(label="Similar Recipes in Database", lines=6) | |
| magic_btn.click(magic_pipeline, input_img, [out_text, out_sim]) | |
| # === VIEW 2: FEED (Now HIDDEN by default: visible=False) === | |
| with gr.Group(visible=False) as feed_view: | |
| # "What's on your mind" Box | |
| with gr.Group(elem_classes=["content-card"]): | |
| with gr.Row(): | |
| gr.HTML(f'<img src="data:image/jpeg;base64,{profile_b64}" style="width:40px; height:40px; border-radius:50%; object-fit:cover;">') | |
| gr.Textbox(placeholder=f"What recipe are you cooking today?", show_label=False, container=False, scale=10) | |
| # Dynamic Feed Generation | |
| 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(["๐ฅ", "๐ฅ", "๐ฐ", "๐ฎ"]) | |
| with gr.Group(elem_classes=["content-card"]): | |
| gr.HTML(f""" | |
| <div class="feed-user-row"> | |
| <div class="feed-avatar">{emoji}</div> | |
| <div class="feed-meta"> | |
| <span class="feed-name">{user_name}</span> | |
| <span class="feed-time">2h ยท ๐ Public</span> | |
| </div> | |
| </div> | |
| """) | |
| gr.Markdown(f"### {row['Title']}") | |
| gr.Markdown(f"{str(row['Raw_Output'])[:250]}...") | |
| gr.Markdown(f"*[Click to see full recipe...]*") | |
| with gr.Row(): | |
| gr.Button("๐ Like", size="sm", variant="secondary") | |
| gr.Button("๐ฌ Comment", size="sm", variant="secondary") | |
| gr.Button("โ๏ธ Share", size="sm", variant="secondary") | |
| else: | |
| gr.Markdown("โ ๏ธ Database is empty. Please check your CSV file.") | |
| # === VIEW 3: ABOUT === | |
| with gr.Group(visible=False) as about_view: | |
| with gr.Group(elem_classes=["content-card"]): | |
| gr.Markdown(""" | |
| # โน๏ธ About Legacy Kitchen | |
| **Preserving Heritage through AI.** | |
| Legacy Kitchen allows you to upload photos of your grandmother's handwritten recipe cards and instantly converts them into digital text using OCR and AI models. | |
| """) | |
| # --- RIGHT COLUMN (Trending) --- | |
| with gr.Column(scale=1, min_width=200): | |
| gr.Markdown("### Trending Recipes") | |
| # Helper to create trending box | |
| def trend_box(title, likes): | |
| return f""" | |
| <div class="trend-box"> | |
| <b>{title}</b><br> | |
| <span style="color:gray; font-size:12px;">{likes} likes</span> | |
| </div>""" | |
| # FIX: Added the 2 requested items | |
| gr.HTML(trend_box("๐ Ramen Hack", "12k") + | |
| trend_box("๐ช Best Cookies", "8k") + | |
| trend_box("๐ฐ Cheese Cake", "15k") + | |
| trend_box("๐ช Nana's Tahini Cookies", "9k")) | |
| # ========================================== | |
| # 6. JAVASCRIPT LOGIC (Client Side) | |
| # ========================================== | |
| # FIX: Updated logic to match the new button order | |
| # ORDER OF OUTPUTS: [DigitalizerView, FeedView, AboutView, NavDigital, NavFeed, NavAbout] | |
| def go_digi(): | |
| return ( | |
| gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), # Views: Digi, Feed, About | |
| gr.update(elem_classes=["nav-btn", "selected"]), # Nav Digi | |
| gr.update(elem_classes=["nav-btn"]), # Nav Feed | |
| gr.update(elem_classes=["nav-btn"]) # Nav About | |
| ) | |
| def go_feed(): | |
| return ( | |
| gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), | |
| 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=True), | |
| 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, about_view, nav_digital, nav_feed, nav_about] | |
| nav_digital.click(go_digi, None, outputs_ui) | |
| nav_feed.click(go_feed, None, outputs_ui) | |
| nav_about.click(go_about, None, outputs_ui) | |
| if __name__ == "__main__": | |
| demo.launch(theme=theme, css=modern_css) |