CookBookAI / app.py
Liori25's picture
Update app.py
318e870 verified
raw
history blame
10.6 kB
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()