Liori25 commited on
Commit
bd6735e
·
verified ·
1 Parent(s): 27881b7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +276 -183
app.py CHANGED
@@ -1,201 +1,294 @@
1
- import streamlit as st
2
  import pandas as pd
3
  import numpy as np
 
 
4
  import pickle
5
- from PIL import Image
6
- from sklearn.metrics.pairwise import cosine_similarity
7
 
8
- # --- IMPORT YOUR LOCAL PIPELINE ---
9
- # This imports the IO_pipeline.py file you uploaded to the Space
 
 
 
10
  try:
11
- import IO_pipeline
12
  except ImportError:
13
- st.error("🚨 IO_pipeline.py not found! Please upload it to the Files tab.")
14
-
15
- # --- CONFIGURATION ---
16
- st.set_page_config(
17
- page_title="CookBook - AI Digitalizer",
18
- page_icon="🍳",
19
- layout="wide",
20
- initial_sidebar_state="expanded"
21
- )
22
-
23
- # --- CUSTOM CSS (Facebook/CookBook Theme) ---
24
- st.markdown("""
25
- <style>
26
- .stApp { background-color: #F0F2F5; font-family: 'Segoe UI', sans-serif; }
27
- section[data-testid="stSidebar"] { background-color: #FFFFFF; box-shadow: 1px 0 5px rgba(0,0,0,0.1); }
28
- .css-card { background-color: #FFFFFF; border-radius: 8px; padding: 20px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); margin-bottom: 20px; }
29
- h1, h2, h3 { color: #050505; font-weight: 700; }
30
- .stButton > button { background-color: #1877F2; color: white; border-radius: 6px; width: 100%; border: none; padding: 10px; font-weight: 600; transition: 0.3s; }
31
- .stButton > button:hover { background-color: #166fe5; }
32
- .list-item { background-color: #F0F2F5; padding: 8px 12px; border-radius: 20px; margin-bottom: 8px; font-size: 14px; color: #050505; }
33
- .step-number { font-weight: bold; color: #1877F2; margin-right: 5px; }
34
- header {visibility: hidden;}
35
- footer {visibility: hidden;}
36
- </style>
37
- """, unsafe_allow_html=True)
38
-
39
- # --- BACKEND FUNCTIONS ---
40
-
41
- @st.cache_resource
42
- def load_dataset():
43
- """Loads the recipe_embeddings.pkl file."""
44
- try:
45
- with open('recipe_embeddings.pkl', 'rb') as f:
46
- data = pickle.load(f)
47
- return data
48
- except FileNotFoundError:
49
- st.warning("⚠️ recipe_embeddings.pkl not found. Recommendations will be random.")
50
- return None
51
-
52
- def find_similar_recipes(user_text, dataset):
53
- """
54
- Finds recipes in the dataset similar to the user_text.
55
- """
56
- if dataset is None or user_text is None:
57
- return []
58
 
59
- try:
60
- # CASE 1: Dataset is a DataFrame with 'embeddings' column
61
- if isinstance(dataset, pd.DataFrame) and 'embeddings' in dataset.columns:
62
- # IMPORTANT: We need to vectorize user_text using the SAME method as the pickle.
63
- # If IO_pipeline has an embedding function, use it:
64
- if hasattr(IO_pipeline, 'get_embedding'):
65
- user_embedding = IO_pipeline.get_embedding(user_text)
66
- # Calculate similarity
67
- dataset['similarity'] = dataset['embeddings'].apply(lambda x: cosine_similarity([user_embedding], [x])[0][0])
68
- top_3 = dataset.sort_values(by='similarity', ascending=False).head(3)
69
- return top_3.to_dict('records')
70
-
71
- else:
72
- # Fallback if we can't generate new embeddings: Return random samples
73
- return dataset.sample(3).to_dict('records')
74
 
75
- # CASE 2: Dataset is just a list/dict (Fallback)
76
- elif isinstance(dataset, pd.DataFrame):
77
- return dataset.sample(3).to_dict('records')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- except Exception as e:
80
- st.error(f"Error finding similarities: {e}")
81
-
82
- return []
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- def parse_recipe_text(text):
85
- """Parses raw text into Title, Ingredients, Instructions."""
86
- if not text: return "Digitized Recipe", [], []
87
 
88
- lines = text.split('\n')
89
- title = "Digitized Recipe"
90
- ingredients = []
91
- instructions = []
92
- current_section = None
93
-
94
- for line in lines:
95
- line = line.strip()
96
- if not line: continue
97
- lower = line.lower()
98
-
99
- # Detect sections
100
- if 'ingredient' in lower:
101
- current_section = 'ing'
102
- continue
103
- elif 'instruction' in lower or 'method' in lower or 'step' in lower:
104
- current_section = 'inst'
105
- continue
106
 
107
- if current_section == 'ing':
108
- ingredients.append(line)
109
- elif current_section == 'inst':
110
- instructions.append(line)
111
- elif current_section is None:
112
- # Assume early lines are title
113
- if len(title) < 20: title = line
114
-
115
- return title, ingredients, instructions
116
-
117
- # --- UI LAYOUT ---
118
-
119
- with st.sidebar:
120
- st.markdown("""<div style="display: flex; align-items: center; margin-bottom: 20px;"><div style="background-color: #e4e6eb; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; margin-right: 10px;"><span style="font-size: 20px;">👨‍🍳</span></div><div><div style="font-weight: bold;">Master Chef</div><div style="font-size: 12px; color: gray;">Recipe Creator</div></div></div>""", unsafe_allow_html=True)
121
- nav = st.radio("Navigation", ["AI Digitalizer", "Feed", "About Us"], label_visibility="collapsed")
122
- st.markdown("---")
123
- st.info("**Pro Tip!** 💡\nUpload clear, well-lit photos.")
124
-
125
- # Search Bar (Visual)
126
- st.markdown("""<div style="background-color: white; padding: 10px; border-radius: 8px; margin-bottom: 20px; display: flex; align-items: center; box-shadow: 0 1px 2px rgba(0,0,0,0.1);"><span style="color: #1877F2; font-weight: 900; font-size: 20px; margin-right: 20px;">CookBook</span><input type="text" placeholder="Search recipes..." style="background-color: #F0F2F5; border: none; padding: 8px 15px; border-radius: 20px; width: 100%; outline: none;"></div>""", unsafe_allow_html=True)
127
-
128
- # Load Dataset
129
- dataset = load_dataset()
130
-
131
- if nav == "AI Digitalizer":
132
- st.markdown('<div class="css-card">', unsafe_allow_html=True)
133
- st.markdown("### ✨ AI Recipe Digitalizer")
134
- st.markdown("Upload a photo of any recipe to extract ingredients and instructions")
135
 
136
- uploaded_file = st.file_uploader("Choose a recipe image", type=['jpg', 'png', 'jpeg'], label_visibility="collapsed")
 
137
 
138
- if uploaded_file is not None:
139
- image = Image.open(uploaded_file)
140
- st.image(image, caption="Uploaded Recipe", use_container_width=True)
 
 
 
 
 
 
 
 
141
 
142
- if st.button("✨ Digitize Recipe"):
143
- with st.spinner("Processing image with IO_pipeline..."):
144
- try:
145
- # 1. CALL LOCAL PIPELINE
146
- generated_text = IO_pipeline.image_to_text(uploaded_file) # or pass 'image' object depending on your pipeline
147
-
148
- # 2. PARSE TEXT
149
- title, ingredients, instructions = parse_recipe_text(generated_text)
150
-
151
- # 3. SAVE STATE
152
- st.session_state['digitized_title'] = title
153
- st.session_state['digitized_ing'] = ingredients
154
- st.session_state['digitized_inst'] = instructions
155
- st.session_state['full_text'] = generated_text
156
- st.session_state['has_results'] = True
157
-
158
- except Exception as e:
159
- st.error(f"Pipeline Error: {e}")
160
- st.error("Check if your IO_pipeline.image_to_text() accepts a file path or PIL image.")
161
-
162
- st.markdown('</div>', unsafe_allow_html=True)
163
-
164
- # --- RESULTS ---
165
- if st.session_state.get('has_results'):
166
- st.markdown(f"""<div class="css-card"><div style="display: flex; align-items: center;"><div style="background-color: #E7F3FF; padding: 10px; border-radius: 50%; margin-right: 10px;"><span style="color: #1877F2; font-size: 20px;">✔</span></div><div><h2 style="margin: 0;">{st.session_state['digitized_title']}</h2><span style="color: green; font-size: 14px;">Successfully digitized!</span></div></div></div>""", unsafe_allow_html=True)
167
 
168
- col1, col2 = st.columns(2)
169
- with col1:
170
- st.markdown('<div class="css-card"><h4>INGREDIENTS</h4>', unsafe_allow_html=True)
171
- for i, ing in enumerate(st.session_state['digitized_ing']):
172
- st.markdown(f'<div class="list-item"><span class="step-number">{i+1}</span> {ing}</div>', unsafe_allow_html=True)
173
- st.markdown('</div>', unsafe_allow_html=True)
174
- with col2:
175
- st.markdown('<div class="css-card"><h4>INSTRUCTIONS</h4>', unsafe_allow_html=True)
176
- for i, inst in enumerate(st.session_state['digitized_inst']):
177
- st.markdown(f'<div class="list-item"><span class="step-number">{i+1}</span> {inst}</div>', unsafe_allow_html=True)
178
- st.markdown('</div>', unsafe_allow_html=True)
179
 
180
- # --- RECOMMENDATIONS ---
181
- st.markdown("### ✨ Similar Recipes")
182
- recommendations = find_similar_recipes(st.session_state['full_text'], dataset)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
- rec_cols = st.columns(3)
185
- for i in range(3):
186
- with rec_cols[i]:
187
- # Handle dictionary access safely
188
- if i < len(recommendations):
189
- rec = recommendations[i]
190
- rec_title = rec.get('title', rec.get('Title', f"Recipe {i+1}")) # Check capitalization
191
- else:
192
- rec_title = "Delicious Recipe"
193
-
194
- st.markdown(f"""
195
- <div class="css-card" style="height: 150px; text-align:center; display:flex; flex-direction:column; justify-content:space-between;">
196
- <b>{rec_title}</b>
197
- <button style="background: #e4e6eb; border: none; padding: 5px; width: 100%; border-radius:5px; margin-top:10px;">View</button>
198
- </div>""", unsafe_allow_html=True)
199
-
200
- elif nav == "Feed":
201
- st.info("Feed functionality coming soon!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
  import pandas as pd
3
  import numpy as np
4
+ import torch
5
+ import os
6
  import pickle
7
+ from sentence_transformers import SentenceTransformer, util
 
8
 
9
+ # -----------------------------------------------------------------------------
10
+ # 1. SETUP & IMPORTS
11
+ # -----------------------------------------------------------------------------
12
+
13
+ # Try to import the custom pipeline, otherwise mock it
14
  try:
15
+ from IO_pipeline import image_to_text
16
  except ImportError:
17
+ print("WARNING: IO_pipeline.py not found. Using mock function.")
18
+ def image_to_text(image):
19
+ return "Grilled Chicken Salad\n\nIngredients:\n- Chicken Breast\n- Lettuce\n\nInstructions:\n1. Grill chicken.\n2. Toss with veggies."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
+ # -----------------------------------------------------------------------------
22
+ # 2. LOAD DATA & MODEL
23
+ # -----------------------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ # Global variables
26
+ DF_RECIPES = None
27
+ EMBEDDINGS = None
28
+ MODEL = None
29
+
30
+ def load_data():
31
+ global DF_RECIPES, EMBEDDINGS, MODEL
32
+
33
+ print("--- Loading Resources ---")
34
+
35
+ # 1. Load Model
36
+ # 'all-MiniLM-L6-v2' is fast and efficient for this task
37
+ MODEL = SentenceTransformer('all-MiniLM-L6-v2')
38
+
39
+ # 2. Load CSV Data
40
+ csv_path = "RecipeData_10K.csv"
41
+ if os.path.exists(csv_path):
42
+ try:
43
+ DF_RECIPES = pd.read_csv(csv_path)
44
+ print(f"Loaded {len(DF_RECIPES)} recipes from {csv_path}")
45
 
46
+ # Basic cleaning: Ensure we have a text column to embed
47
+ # We combine Title + Ingredients for the search context
48
+ # Adjust column names 'Title', 'Ingredients' based on your actual CSV headers
49
+ if 'combined_text' not in DF_RECIPES.columns:
50
+ # Fallback checks for column names
51
+ title_col = 'Title' if 'Title' in DF_RECIPES.columns else DF_RECIPES.columns[0]
52
+ ing_col = 'Ingredients' if 'Ingredients' in DF_RECIPES.columns else DF_RECIPES.columns[1]
53
+
54
+ DF_RECIPES['combined_text'] = DF_RECIPES[title_col].astype(str) + " " + DF_RECIPES[ing_col].astype(str)
55
+
56
+ except Exception as e:
57
+ print(f"Error loading CSV: {e}")
58
+ DF_RECIPES = pd.DataFrame()
59
+ else:
60
+ print("Error: RecipeData_10K.csv not found.")
61
+ DF_RECIPES = pd.DataFrame()
62
 
63
+ # 3. Generate or Load Embeddings
64
+ embedding_cache_path = "cached_embeddings.pkl"
 
65
 
66
+ if not DF_RECIPES.empty:
67
+ if os.path.exists(embedding_cache_path):
68
+ print("Loading cached embeddings...")
69
+ with open(embedding_cache_path, "rb") as f:
70
+ EMBEDDINGS = pickle.load(f)
71
+ else:
72
+ print("Generating embeddings for 10k recipes (this may take a few minutes)...")
73
+ # Encode the combined text column
74
+ corpus = DF_RECIPES['combined_text'].tolist()
75
+ EMBEDDINGS = MODEL.encode(corpus, convert_to_tensor=True, show_progress_bar=True)
 
 
 
 
 
 
 
 
76
 
77
+ # Save for next time (optional, helps if space restarts)
78
+ with open(embedding_cache_path, "wb") as f:
79
+ pickle.dump(EMBEDDINGS, f)
80
+ print("Embeddings generated and saved.")
81
+
82
+ # Run setup immediately
83
+ load_data()
84
+
85
+ # -----------------------------------------------------------------------------
86
+ # 3. SEARCH LOGIC
87
+ # -----------------------------------------------------------------------------
88
+
89
+ def get_recommendations(query_text, k=3):
90
+ """
91
+ Finds top k similar recipes from the DataFrame.
92
+ """
93
+ if DF_RECIPES is None or DF_RECIPES.empty or EMBEDDINGS is None:
94
+ return [("No Data", "Please ensure RecipeData_10K.csv is uploaded.")]
95
+
96
+ # 1. Encode user query
97
+ query_embedding = MODEL.encode(query_text, convert_to_tensor=True)
 
 
 
 
 
 
 
98
 
99
+ # 2. Compute Cosine Similarity
100
+ cos_scores = util.cos_sim(query_embedding, EMBEDDINGS)[0]
101
 
102
+ # 3. Get top k results
103
+ top_results = torch.topk(cos_scores, k=k)
104
+
105
+ results = []
106
+ for score, idx in zip(top_results.values, top_results.indices):
107
+ idx = int(idx) # Convert tensor index to int
108
+ row = DF_RECIPES.iloc[idx]
109
+
110
+ # Adjust these keys to match your CSV Column Names
111
+ # Example: row['Title'], row['Instructions']
112
+ title = row.get('Title', 'Untitled Recipe')
113
 
114
+ # Create a short snippet for the description
115
+ instructions = str(row.get('Instructions', ''))
116
+ snippet = instructions[:120] + "..." if len(instructions) > 120 else instructions
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
+ results.append((title, snippet))
 
 
 
 
 
 
 
 
 
 
119
 
120
+ return results
121
+
122
+ def process_pipeline(image):
123
+ if image is None:
124
+ return "", "Please upload an image."
125
+
126
+ # 1. Image -> Text
127
+ try:
128
+ generated_text = image_to_text(image)
129
+ except Exception as e:
130
+ return f"Error extracting text: {str(e)}", ""
131
+
132
+ # 2. Text -> Recommendations
133
+ recs = get_recommendations(generated_text)
134
+
135
+ # 3. Format Output (HTML)
136
+ rec_html = ""
137
+ for title, desc in recs:
138
+ rec_html += f"""
139
+ <div class="recipe-card">
140
+ <div class="recipe-icon">🍳</div>
141
+ <div class="recipe-info">
142
+ <h4>{title}</h4>
143
+ <p>{desc}</p>
144
+ </div>
145
+ </div>
146
+ """
147
 
148
+ return generated_text, rec_html
149
+
150
+ # -----------------------------------------------------------------------------
151
+ # 4. CSS & UI (Facebook Style)
152
+ # -----------------------------------------------------------------------------
153
+
154
+ custom_css = """
155
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
156
+
157
+ :root {
158
+ --primary: #1877F2;
159
+ --bg-color: #F0F2F5;
160
+ --card-bg: #FFFFFF;
161
+ --text-main: #050505;
162
+ --text-muted: #65676B;
163
+ }
164
+
165
+ body, .gradio-container {
166
+ background-color: var(--bg-color) !important;
167
+ font-family: 'Inter', sans-serif !important;
168
+ }
169
+
170
+ /* Custom Header */
171
+ .fb-header {
172
+ background: white;
173
+ padding: 0.8rem 1.5rem;
174
+ box-shadow: 0 1px 2px rgba(0,0,0,0.1);
175
+ display: flex;
176
+ align-items: center;
177
+ gap: 1rem;
178
+ margin-bottom: 2rem;
179
+ border-radius: 0 0 8px 8px;
180
+ }
181
+ .logo-area {
182
+ display: flex;
183
+ align-items: center;
184
+ gap: 10px;
185
+ color: var(--primary);
186
+ font-weight: 700;
187
+ font-size: 1.5rem;
188
+ }
189
+
190
+ /* Cards & Groups */
191
+ .group-box {
192
+ background: var(--card-bg);
193
+ border: none !important;
194
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
195
+ border-radius: 12px;
196
+ padding: 1rem !important;
197
+ margin-bottom: 1rem;
198
+ }
199
+
200
+ /* Buttons */
201
+ .primary-btn {
202
+ background-color: var(--primary) !important;
203
+ color: white !important;
204
+ border-radius: 6px !important;
205
+ font-weight: 600 !important;
206
+ border: none !important;
207
+ padding: 10px !important;
208
+ }
209
+
210
+ /* Recipe Cards */
211
+ .recipe-card {
212
+ display: flex;
213
+ gap: 15px;
214
+ padding: 15px;
215
+ margin-bottom: 10px;
216
+ background: #fff;
217
+ border: 1px solid #ddd;
218
+ border-radius: 8px;
219
+ transition: transform 0.2s, box-shadow 0.2s;
220
+ }
221
+ .recipe-card:hover {
222
+ transform: translateY(-2px);
223
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
224
+ }
225
+ .recipe-icon {
226
+ min-width: 50px;
227
+ height: 50px;
228
+ background: #EBF5FF;
229
+ border-radius: 8px;
230
+ display: flex;
231
+ align-items: center;
232
+ justify-content: center;
233
+ font-size: 24px;
234
+ }
235
+ .recipe-info h4 {
236
+ margin: 0 0 5px 0;
237
+ color: var(--primary);
238
+ font-weight: 600;
239
+ }
240
+ .recipe-info p {
241
+ margin: 0;
242
+ color: var(--text-muted);
243
+ font-size: 0.9rem;
244
+ line-height: 1.4;
245
+ }
246
+ """
247
+
248
+ # -----------------------------------------------------------------------------
249
+ # 5. GRADIO INTERFACE
250
+ # -----------------------------------------------------------------------------
251
+
252
+ with gr.Blocks(css=custom_css, title="CookBook") as demo:
253
+
254
+ # Header
255
+ gr.HTML("""
256
+ <div class="fb-header">
257
+ <div class="logo-area">
258
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 13.87A4 4 0 0 1 7.41 6a5.11 5.11 0 0 1 1.05-1.54 5 5 0 0 1 7.08 0A5.11 5.11 0 0 1 16.59 6 4 4 0 0 1 18 13.87V21H6Z"/><line x1="6" y1="17" x2="18" y2="17"/></svg>
259
+ <span>CookBook</span>
260
+ </div>
261
+ <div style="flex-grow:1;"></div>
262
+ <div style="display:flex; align-items:center; gap:10px;">
263
+ <span style="font-weight:600; color:#050505;">Welcome, Chef!</span>
264
+ <img src="https://api.dicebear.com/7.x/avataaars/svg?seed=chef" style="width:40px; height:40px; border-radius:50%; background:#e4e6eb;">
265
+ </div>
266
+ </div>
267
+ """)
268
+
269
+ with gr.Row():
270
+ # Left Column
271
+ with gr.Column(scale=1):
272
+ gr.Markdown("### 📸 Post a Recipe")
273
+ with gr.Group(elem_classes="group-box"):
274
+ input_image = gr.Image(type="pil", label="Upload Photo", elem_id="upload-zone")
275
+ submit_btn = gr.Button("Find Similar Recipes", elem_classes="primary-btn")
276
+
277
+ # Right Column
278
+ with gr.Column(scale=1):
279
+ gr.Markdown("### 📝 Extracted Details")
280
+ with gr.Group(elem_classes="group-box"):
281
+ output_text = gr.Textbox(label="Recipe Text", lines=6, show_label=False)
282
+
283
+ gr.Markdown("### 🥗 You might also like")
284
+ output_recommendations = gr.HTML(label="Recommendations")
285
+
286
+ # Actions
287
+ submit_btn.click(
288
+ fn=process_pipeline,
289
+ inputs=[input_image],
290
+ outputs=[output_text, output_recommendations]
291
+ )
292
+
293
+ if __name__ == "__main__":
294
+ demo.launch()