Liori25 commited on
Commit
5e9b2b0
ยท
verified ยท
1 Parent(s): bd6735e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +361 -242
app.py CHANGED
@@ -1,294 +1,413 @@
 
 
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()
 
 
 
 
 
 
 
 
 
1
+
2
+
3
  import gradio as gr
4
  import pandas as pd
5
+ import pickle
6
  import numpy as np
 
7
  import os
8
+ import random
9
+ import base64
10
+ from huggingface_hub import InferenceClient
11
+ from sklearn.metrics.pairwise import cosine_similarity
12
+ from IO_pipeline import RecipeDigitalizerPipeline
13
 
14
+ # ==========================================
15
+ # 1. SETUP & DATA LOADING
16
+ # ==========================================
17
+ hf_token = os.getenv("HF_TOKEN")
18
+ API_MODEL = "BAAI/bge-small-en-v1.5"
19
+ client = InferenceClient(token=hf_token) if hf_token else None
20
 
21
+ print("โณ Loading Data...")
22
  try:
23
+ df_recipes = pd.read_csv('RecipeData_10K.csv')
24
+ with open('recipe_embeddings.pkl', 'rb') as f:
25
+ data = pickle.load(f)
26
+ if isinstance(data, dict):
27
+ stored_embeddings = np.array(data['embeddings'])
28
+ elif isinstance(data, pd.DataFrame):
29
+ target_col = next((c for c in ['embedding', 'embeddings', 'vectors'] if c in data.columns), None)
30
+ stored_embeddings = np.vstack(data[target_col].values) if target_col else data
31
+ else:
32
+ stored_embeddings = data
33
+ print("โœ… Data Loaded!")
34
+ except Exception as e:
35
+ print(f"โŒ Error loading data: {e}")
36
+ df_recipes = pd.DataFrame({'Title': [], 'Raw_Output': []})
37
+ stored_embeddings = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ # ==========================================
40
+ # 2. HELPER: IMAGE TO BASE64
41
+ # ==========================================
42
+ def image_to_base64(image_path):
43
+ if not os.path.exists(image_path):
44
+ return "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
45
+ with open(image_path, "rb") as img_file:
46
+ return base64.b64encode(img_file.read()).decode('utf-8')
47
 
48
+ logo_b64 = image_to_base64("logo.jpg")
49
+ profile_b64 = image_to_base64("232px-Tv_the_muppet_show_bein_green.jpg")
50
+ # NEW: Load the process preview image
51
+ process_b64 = image_to_base64("preview of process.jpg")
52
+
53
+ # ==========================================
54
+ # 3. BACKEND LOGIC
55
+ # ==========================================
56
+ def get_embedding_via_api(text):
57
+ if not client: raise ValueError("HF_TOKEN missing")
58
+ response = client.feature_extraction(text, model=API_MODEL)
59
+ return np.array(response)
60
+
61
+ def find_similar_recipes_list(query_text):
62
+ if stored_embeddings is None: return ["Database error."] * 3
63
+ query_vec = get_embedding_via_api("Represent this recipe for retrieving similar dishes: " + query_text)
64
+ if len(query_vec.shape) == 1: query_vec = query_vec.reshape(1, -1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ scores = cosine_similarity(query_vec, stored_embeddings)[0]
67
+ top_indices = scores.argsort()[-3:][::-1]
68
 
69
+ results_list = []
70
+ for idx in top_indices:
71
+ score = scores[idx]
72
+ row = df_recipes.iloc[idx]
73
+ title = row['Title']
74
+ desc = str(row['Raw_Output'])
75
+ score_display = f"{score:.0%}"
76
 
77
+ card_content = (
78
+ f"### ๐Ÿ† {title}\n"
79
+ f"<span style='color:#1877f2; font-weight:bold; font-size:14px;'>Match Score: {score_display}</span>\n\n"
80
+ f"<div class='sim-scroll'>{desc}</div>"
81
+ )
82
+ results_list.append(card_content)
83
+
84
+ while len(results_list) < 3:
85
+ results_list.append("")
86
 
87
+ return results_list
88
 
89
+ def format_recipe(json_data):
90
+ if "error" in json_data: return f"Error: {json_data['error']}", ""
91
+ title = json_data.get("title", "Unknown")
92
+ ing = "\n".join([f"- {x}" for x in json_data.get("ingredients", [])])
93
+ inst = "\n".join([f"{i+1}. {x}" for i, x in enumerate(json_data.get("instructions", []))])
94
+ text = f"๐Ÿฝ๏ธ {title}\n\n๐Ÿ›’ INGREDIENTS:\n{ing}\n\n๐Ÿณ INSTRUCTIONS:\n{inst}"
95
+ return text, f"{title} {ing} {inst}"
96
+
97
+ def ui_update_pipeline(image_path):
98
+ if not hf_token:
99
+ return "Error: HF_TOKEN missing", "", gr.update(), gr.update(), "", gr.update(), ""
100
  try:
101
+ os.environ["HF_TOKEN"] = hf_token
102
+ digitizer = RecipeDigitalizerPipeline()
103
+ json_res = digitizer.run_pipeline(image_path)
104
+ readable, query = format_recipe(json_res)
105
+ if query:
106
+ sim_list = find_similar_recipes_list(query)
107
+ else:
108
+ sim_list = ["No query generated.", "", ""]
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ return (readable, sim_list[0], gr.update(visible=True), gr.update(visible=True), sim_list[1], gr.update(visible=True), sim_list[2])
111
+ except Exception as e:
112
+ return f"Error: {e}", "Error", gr.update(), gr.update(), "", gr.update(), ""
113
 
114
+ # ==========================================
115
+ # 4. MODERN UI THEME & CSS
116
+ # ==========================================
117
+ theme = gr.themes.Soft(
118
+ primary_hue="indigo",
119
+ secondary_hue="blue",
120
+ neutral_hue="slate",
121
+ font=[gr.themes.GoogleFont('Inter'), 'ui-sans-serif', 'system-ui']
122
+ )
123
 
124
+ modern_css = """
125
+ body, .gradio-container { background-color: #f0f2f5; }
126
 
127
+ /* Sticky Header */
128
+ .custom-header {
129
+ background: rgba(255, 255, 255, 0.95);
130
+ backdrop-filter: blur(10px);
131
+ border-bottom: 1px solid #e4e6eb;
132
+ padding: 15px 20px;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: space-between;
136
+ position: sticky;
137
+ top: 0;
138
+ z-index: 1000;
139
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
140
  }
141
 
142
+ .logo-area { display: flex; align-items: center; gap: 20px; }
143
+ .logo-img { height: 120px; width: 120px; border-radius: 12px; object-fit: cover; border: 1px solid #ddd; }
144
+ .text-area { display: flex; flex-direction: column; }
145
+ .app-name {
146
+ font-weight: 800;
147
+ font-size: 32px;
148
+ background: -webkit-linear-gradient(45deg, #1877f2, #6b21a8);
149
+ -webkit-background-clip: text;
150
+ -webkit-text-fill-color: transparent;
151
+ line-height: 1.2;
152
  }
153
+ .app-slogan { font-size: 16px; color: #65676b; font-weight: 500; }
154
 
155
+ /* Sidebar Navigation */
156
+ .nav-btn {
157
+ text-align: left !important;
158
+ justify-content: flex-start !important;
159
+ background: transparent !important;
160
+ border: none !important;
161
+ box-shadow: none !important;
162
+ color: #65676b !important;
163
+ font-weight: 600 !important;
164
+ font-size: 16px !important;
165
+ padding: 12px 16px !important;
166
+ border-radius: 10px !important;
167
+ transition: all 0.2s ease;
168
  }
169
+ .nav-btn:hover { background-color: #e4e6eb !important; color: #050505 !important; }
170
+ .nav-btn.selected {
171
+ background-color: #e7f3ff !important;
172
+ color: #1877f2 !important;
173
+ border-left: 4px solid #1877f2 !important;
 
 
174
  }
175
 
176
+ /* Feed Styling */
177
+ #feed-container {
178
+ gap: 0px !important;
179
+ padding: 0px !important;
180
+ }
181
+ #feed-container > .form {
182
+ gap: 0px !important;
 
183
  }
184
 
185
+ .content-card {
186
+ background-color: #ffffff !important;
187
+ background: #ffffff !important;
188
+ border-radius: 12px;
189
+ box-shadow: 0 1px 2px rgba(0,0,0,0.1);
190
+ border: 1px solid #ddd;
191
+ padding: 20px;
192
+ margin-bottom: 7px !important;
193
+ margin-top: 0px !important;
194
+ width: 100%;
195
+ display: block;
196
  }
197
 
198
+ /* Similar Recipe Cards */
199
+ .sim-card {
 
 
 
 
200
  background: #fff;
201
+ border: 1px solid #eee;
202
  border-radius: 8px;
203
+ padding: 15px;
204
+ height: 100%;
205
+ border-top: 4px solid #1877f2;
206
+ display: flex;
207
+ flex-direction: column;
208
+ justify-content: space-between;
209
  }
210
+
211
+ .sim-scroll {
212
+ height: 400px;
213
+ overflow-y: auto;
214
+ margin-bottom: 10px;
215
+ padding-right: 5px;
216
+ font-size: 14px;
217
+ color: #4b4f56;
218
  }
219
+
220
+ .trend-box {
221
+ background:white;
222
+ padding:10px;
223
+ border-radius:8px;
224
+ margin-bottom:10px;
225
+ box-shadow:0 1px 2px rgba(0,0,0,0.1);
226
+ transition: background 0.2s;
227
+ }
228
+ .trend-box:hover { background: #f0f2f5; cursor: pointer; }
229
+
230
+ .gap-fix { gap: 25px !important; }
231
+
232
+ .gradio-examples {
233
  display: flex;
 
234
  justify-content: center;
235
+ width: 100%;
236
  }
237
+
238
+ button.gallery-item {
239
+ transition: transform 0.2s ease, box-shadow 0.2s ease !important;
240
+ z-index: 1;
241
  }
242
+ button.gallery-item:hover {
243
+ transform: scale(2.5) !important;
244
+ z-index: 1000 !important;
245
+ box-shadow: 0 10px 25px rgba(0,0,0,0.3) !important;
246
+ border: 2px solid white !important;
247
+ border-radius: 8px !important;
248
  }
249
  """
250
 
251
+ # ==========================================
252
+ # 5. LAYOUT CONSTRUCTION
253
+ # ==========================================
254
+ with gr.Blocks(title="Legacy Kitchen") as demo:
 
255
 
256
+ # --- HEADER ---
257
+ gr.HTML(f"""
258
+ <div class="custom-header">
259
  <div class="logo-area">
260
+ <img src="data:image/jpeg;base64,{logo_b64}" class="logo-img">
261
+ <div class="text-area">
262
+ <span class="app-name">Legacy Kitchen</span>
263
+ <span class="app-slogan">Turning Handwritten Notes into a Digital Cookbook.</span>
264
+ </div>
 
 
265
  </div>
266
+ <div style="color: #65676b; font-weight: 600;">v4.4</div>
267
  </div>
268
  """)
269
 
270
  with gr.Row():
271
+
272
+ # --- LEFT SIDEBAR ---
273
+ with gr.Column(scale=1, min_width=200):
274
+ gr.HTML(f"""
275
+ <div style="display:flex; align-items:center; padding: 10px 10px 5px 10px;">
276
+ <img src="data:image/jpeg;base64,{profile_b64}" style="width:40px; height:40px; border-radius:50%; margin-right:10px; object-fit:cover;">
277
+ <b style="font-size: 16px;">My Profile</b>
278
+ </div>
279
+ """)
280
+ gr.HTML("<hr style='border: 0; border-top: 1px solid #e4e6eb; margin: 10px 0 20px 0;'>")
281
+
282
+ nav_digital = gr.Button("โœจ AI Digitizer", elem_classes=["nav-btn", "selected"])
283
+ nav_feed = gr.Button("๐Ÿ“ฐ News Feed", elem_classes=["nav-btn"])
284
+ nav_about = gr.Button("โ„น๏ธ About", elem_classes=["nav-btn"])
285
+
286
+ # --- CENTER CONTENT ---
287
+ with gr.Column(scale=3):
288
 
289
+ # === VIEW 1: AI DIGITALIZER ===
290
+ with gr.Group(visible=True) as digitalizer_view:
291
+ with gr.Row(elem_classes=["gap-fix"]):
292
+ with gr.Column(scale=1):
293
+ with gr.Group(elem_classes=["content-card"]):
294
+ input_img = gr.Image(type="filepath", label="Upload", height=300)
295
+ magic_btn = gr.Button("โœจ Convert to Digital", variant="primary", size="lg")
296
+ gr.Examples(
297
+ examples=[
298
+ ["quick_tries_images/applecrisp.jpg"],
299
+ ["quick_tries_images/meatballs recipe.jpg"],
300
+ ["quick_tries_images/chocolateballs.png"]
301
+ ],
302
+ inputs=input_img,
303
+ label="Or try these examples:",
304
+ cache_examples=False
305
+ )
306
+ with gr.Column(scale=1):
307
+ with gr.Group(elem_classes=["content-card"]):
308
+ out_text = gr.Textbox(label="Result", value="Here your digitalized recipe will be presented", lines=20, interactive=False, show_label=False)
309
+
310
+ gr.HTML("<div style='height: 35px;'></div>")
311
+ gr.Markdown("### 3. Similar Recipes from Database")
312
+
313
+ with gr.Row():
314
+ with gr.Column(elem_classes=["sim-card"]) as c1_box:
315
+ sim1 = gr.Markdown("Once you will upload your scanned recipe, we will share similar recipes!")
316
+ with gr.Row(visible=False) as c1_btns:
317
+ gr.Button("๐Ÿ‘ Like", size="sm", variant="secondary")
318
+ gr.Button("โ†—๏ธ Share", size="sm", variant="secondary")
319
+
320
+ with gr.Column(elem_classes=["sim-card"], visible=False) as c2_box:
321
+ sim2 = gr.Markdown("")
322
+ with gr.Row():
323
+ gr.Button("๐Ÿ‘ Like", size="sm", variant="secondary")
324
+ gr.Button("โ†—๏ธ Share", size="sm", variant="secondary")
325
+
326
+ with gr.Column(elem_classes=["sim-card"], visible=False) as c3_box:
327
+ sim3 = gr.Markdown("")
328
+ with gr.Row():
329
+ gr.Button("๐Ÿ‘ Like", size="sm", variant="secondary")
330
+ gr.Button("โ†—๏ธ Share", size="sm", variant="secondary")
331
+
332
+ magic_btn.click(ui_update_pipeline, input_img, [out_text, sim1, c1_btns, c2_box, sim2, c3_box, sim3])
333
+
334
+ # === VIEW 2: FEED ===
335
+ with gr.Column(visible=False, elem_id="feed-container") as feed_view:
336
+ if not df_recipes.empty:
337
+ feed_samples = df_recipes.sample(10)
338
+ for index, row in feed_samples.iterrows():
339
+ user_name = random.choice(["Grandma Rose", "Chef Mike", "Sarah J."])
340
+ emoji = random.choice(["๐Ÿฅ˜", "๐Ÿฅ—", "๐Ÿฐ", "๐ŸŒฎ"])
341
+ time_options = ["2h", "3h", "4h", "6h", "9h", "12h", "a day ago", "2 days ago"]
342
+ post_time = random.choice(time_options)
343
+
344
+ with gr.Group(elem_classes=["content-card"]):
345
+ gr.HTML(f"""
346
+ <div style="display:flex; gap:10px; align-items:center; margin-bottom:12px;">
347
+ <div style="width:40px; height:40px; background:#e4e6eb; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:20px;">{emoji}</div>
348
+ <div><b>{user_name}</b><br><span style="color:gray; font-size:12px;">{post_time} ยท ๐ŸŒ Public</span></div>
349
+ </div>
350
+ """)
351
+ gr.Markdown(f"### {row['Title']}")
352
+ gr.Markdown(f"{str(row['Raw_Output'])[:250]}...")
353
+ with gr.Row():
354
+ gr.Button("๐Ÿ‘ Like", size="sm", variant="secondary")
355
+ gr.Button("๐Ÿ’ฌ Comment", size="sm", variant="secondary")
356
+ gr.Button("โ†—๏ธ Share", size="sm", variant="secondary")
357
+ else:
358
+ gr.Markdown("โš ๏ธ Database is empty.")
359
+
360
+ # === VIEW 3: ABOUT (UPDATED) ===
361
+ with gr.Group(visible=False) as about_view:
362
+ with gr.Group(elem_classes=["content-card"]):
363
+ gr.Markdown("""
364
+ # Goal Project
365
+
366
+ The goal of this project is to develop an app that takes a scanned image of a handwritten recipe as input, generates text using a VLM, and based on the extracted text, suggests 3 similar recipes from a 10K dataset of synthetic recipes. Our app will bridge the gap between analog culinary heritage and digital discovery.
367
+
368
+ ### About Us
369
+ This app was developed by **Shahar Firshtman** and **Lior Feinstein**, 2nd year students for Economics and data science.
370
+ """)
371
+
372
+ # Process Image
373
+ gr.HTML(f"""
374
+ <div style="margin-top: 20px;">
375
+ <h3 style="color: #444;">Process Overview</h3>
376
+ <img src="data:image/jpeg;base64,{process_b64}" style="width: 100%; height: auto; border-radius: 8px; border: 1px solid #ddd;">
377
+ </div>
378
+ """)
379
+
380
+ # --- RIGHT COLUMN ---
381
+ with gr.Column(scale=1, min_width=200):
382
+ gr.Markdown("### Trending Recipes")
383
+ def trend_box(title, likes):
384
+ return f"<div class='trend-box'><b>{title}</b><br><span style='color:gray; font-size:12px;'>{likes} likes</span></div>"
385
+ gr.HTML(trend_box("๐Ÿœ Ramen Hack", "12k") + trend_box("๐Ÿช Best Cookies", "8k") + trend_box("๐Ÿฐ Cheese Cake", "15k") + trend_box("๐Ÿช Nana's Tahini Cookies", "9k"))
386
+
387
+ # ==========================================
388
+ # 6. JAVASCRIPT LOGIC
389
+ # ==========================================
390
+ def go_digi():
391
+ return (gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(elem_classes=["nav-btn", "selected"]), gr.update(elem_classes=["nav-btn"]), gr.update(elem_classes=["nav-btn"]))
392
 
393
+ def go_feed():
394
+ 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"]))
395
+
396
+ def go_about():
397
+ 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"]))
398
+
399
+ outputs_ui = [digitalizer_view, feed_view, about_view, nav_digital, nav_feed, nav_about]
400
+ nav_digital.click(go_digi, None, outputs_ui)
401
+ nav_feed.click(go_feed, None, outputs_ui)
402
+ nav_about.click(go_about, None, outputs_ui)
403
 
404
  if __name__ == "__main__":
405
+ demo.launch(theme=theme, css=modern_css)
406
+
407
+
408
+
409
+
410
+
411
+
412
+
413
+