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') | |
| logo_b64 = image_to_base64("logo.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. CSS STYLING | |
| # ========================================== | |
| fb_css = """ | |
| body {background-color: #f0f2f5 !important; font-family: Helvetica, Arial, sans-serif;} | |
| /* --- HEADER --- */ | |
| .fb-header { | |
| background-color: #ffffff; | |
| color: #1877f2; | |
| padding: 15px 20px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| border-bottom: 1px solid #ddd; | |
| margin-bottom: 20px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .logo-img { | |
| height: 80px; /* UPDATED SIZE */ | |
| width: 80px; /* UPDATED SIZE */ | |
| object-fit: cover; | |
| margin-right: 20px; | |
| border-radius: 8px; | |
| border: 1px solid #eee; | |
| } | |
| .header-text-col { | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| } | |
| .app-title { font-size: 28px; font-weight: 900; line-height: 1.1; } | |
| .app-slogan { font-size: 16px; font-weight: normal; color: #606770; } | |
| /* --- SIDEBAR NAV --- */ | |
| .sidebar-btn { | |
| background-color: transparent !important; | |
| color: #050505 !important; | |
| /* ALIGNMENT FIXES */ | |
| text-align: left !important; | |
| justify-content: flex-start !important; | |
| display: flex !important; | |
| align-items: center !important; | |
| border: none !important; | |
| font-weight: 600 !important; | |
| padding: 15px 10px !important; | |
| font-size: 16px !important; | |
| box-shadow: none !important; | |
| width: 100% !important; | |
| border-radius: 8px !important; | |
| /* SEPARATION EFFECT */ | |
| border-bottom: 1px solid #e4e6eb !important; | |
| margin-bottom: 8px !important; | |
| } | |
| .sidebar-btn:hover { background-color: #e4e6eb !important; } | |
| /* --- CARDS --- */ | |
| .fb-card { | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.2); | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .avatar { | |
| width: 40px; height: 40px; | |
| background-color: #e4e6eb; | |
| border-radius: 50%; | |
| text-align: center; | |
| line-height: 40px; | |
| margin-right: 10px; | |
| } | |
| """ | |
| # ========================================== | |
| # 5. LAYOUT | |
| # ========================================== | |
| with gr.Blocks(css=fb_css, title="Legacy Kitchen") as demo: | |
| # --- HEADER --- | |
| gr.HTML(f""" | |
| <div class="fb-header"> | |
| <img src="data:image/jpeg;base64,{logo_b64}" class="logo-img" alt="Logo"> | |
| <div class="header-text-col"> | |
| <span class="app-title">Legacy Kitchen</span> | |
| <span class="app-slogan">Your Family Cookbook</span> | |
| </div> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| # --- LEFT SIDEBAR --- | |
| with gr.Column(scale=1, min_width=250): | |
| gr.HTML('<div style="display:flex; align-items:center; margin-bottom:20px; padding-left:10px;"><div class="avatar">π€</div><b>My Profile</b></div>') | |
| nav_feed = gr.Button("π° Feed", elem_classes=["sidebar-btn"]) | |
| nav_digital = gr.Button("πΈ Digitizer", elem_classes=["sidebar-btn"]) | |
| nav_about = gr.Button("βΉοΈ About Us", elem_classes=["sidebar-btn"]) | |
| gr.Markdown("---") | |
| gr.Markdown("Privacy Β· Terms Β· Cookies") | |
| # --- RIGHT CONTENT --- | |
| with gr.Column(scale=3): | |
| # VIEW 1: FEED | |
| with gr.Group(visible=True) as feed_view: | |
| with gr.Group(elem_classes=["fb-card"]): | |
| with gr.Row(): | |
| gr.HTML('<div class="avatar">π€</div>') | |
| gr.Textbox(placeholder="What's cooking?", show_label=False, container=False, scale=5) | |
| if not df_recipes.empty: | |
| feed_samples = df_recipes.sample(10) | |
| for index, row in feed_samples.iterrows(): | |
| with gr.Group(elem_classes=["fb-card"]): | |
| user = random.choice(["Grandma Rose", "Chef Mike", "Sarah J."]) | |
| emoji = random.choice(["π₯", "π₯", "π°"]) | |
| gr.HTML(f""" | |
| <div style="display:flex; margin-bottom:10px;"> | |
| <div class="avatar">{emoji}</div> | |
| <div><b>{user}</b><br><span style="color:gray; font-size:12px;">2h Β· π</span></div> | |
| </div> | |
| """) | |
| gr.Markdown(f"**{row['Title']}**\n\n{str(row['Raw_Output'])[:200]}... [Read More]") | |
| with gr.Row(): | |
| like_btn = gr.Button("π Like", size="sm") | |
| gr.Button("π¬ Comment", size="sm") | |
| gr.Button("βοΈ Share", size="sm") | |
| def toggle_like(x): return "π Liked!" | |
| like_btn.click(toggle_like, like_btn, like_btn) | |
| else: | |
| gr.Markdown("Database empty.") | |
| # VIEW 2: DIGITALIZER | |
| with gr.Group(visible=False) as digitalizer_view: | |
| gr.Markdown("### πΈ Digitize Recipe", elem_classes=["fb-card"]) | |
| with gr.Group(elem_classes=["fb-card"]): | |
| with gr.Row(): | |
| with gr.Column(): | |
| input_img = gr.Image(type="filepath", label="Upload Photo") | |
| magic_btn = gr.Button("β¨ Digitize", variant="primary") | |
| with gr.Column(): | |
| out_text = gr.Textbox(label="Text", lines=10) | |
| out_sim = gr.Textbox(label="Similar", lines=10) | |
| magic_btn.click(magic_pipeline, input_img, [out_text, out_sim]) | |
| # VIEW 3: ABOUT US | |
| with gr.Group(visible=False) as about_view: | |
| gr.Markdown("### βΉοΈ About Us", elem_classes=["fb-card"]) | |
| with gr.Group(elem_classes=["fb-card"]): | |
| gr.Markdown(""" | |
| **Legacy Kitchen** is a project dedicated to preserving culinary history. | |
| We believe that every handwritten recipe tells a story. Our AI-powered platform helps you: | |
| 1. **Digitize** old handwritten notes into clear text. | |
| 2. **Discover** similar dishes from a database of 10,000+ recipes. | |
| 3. **Connect** with a community of food lovers. | |
| *Preserving the Past, Cooking for the Future.* | |
| """) | |
| # Nav Logic | |
| nav_feed.click(lambda: (gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)), None, [feed_view, digitalizer_view, about_view]) | |
| nav_digital.click(lambda: (gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)), None, [feed_view, digitalizer_view, about_view]) | |
| nav_about.click(lambda: (gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)), None, [feed_view, digitalizer_view, about_view]) | |
| if __name__ == "__main__": | |
| demo.launch() |