CookBookAI / app.py
Liori25's picture
Update app.py
6ff0916 verified
raw
history blame
14.1 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 "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)