Iris314 commited on
Commit
08768c4
·
verified ·
1 Parent(s): f603b1e

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +681 -0
app.py ADDED
@@ -0,0 +1,681 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio application for the smart fridge detector + recipe recommendation pipeline."""
2
+
3
+ import json
4
+ import tempfile
5
+ from pathlib import Path
6
+ from typing import List, Tuple, Dict, Any
7
+
8
+ import cv2
9
+ import gradio as gr
10
+ import numpy as np
11
+ from PIL import Image
12
+ import pandas as pd
13
+
14
+ from frige_detect.detect import (
15
+ detect_and_generate,
16
+ load_roboflow_credentials,
17
+ RoboflowCredentials,
18
+ )
19
+ from recipe_recommendation.main import (
20
+ load_recipes,
21
+ recommend_recipes,
22
+ save_user_profile,
23
+ get_feedback,
24
+ USER_DATA_DIR,
25
+ )
26
+
27
+ from sklearn.metrics import ndcg_score
28
+ import joblib
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Global resources
32
+ # ---------------------------------------------------------------------------
33
+ CREDENTIALS_PATH = Path("frige_detect/roboflow_credentials.txt")
34
+ ROBOFLOW_CREDENTIALS: RoboflowCredentials = load_roboflow_credentials(str(CREDENTIALS_PATH))
35
+ RECIPES_DF = load_recipes()
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Predefined user profiles for examples
39
+ # ---------------------------------------------------------------------------
40
+ # EXAMPLE_PROFILES = {
41
+ # "user_1": {
42
+ # "vegetarian_type": "flexible",
43
+ # "allergies": "",
44
+ # "regions": "North America",
45
+ # "calorie_min": 250,
46
+ # "calorie_max": 2000,
47
+ # "protein_min": 10,
48
+ # "protein_max": 160,
49
+ # "preferred_main": "",
50
+ # "disliked_main": "",
51
+ # "cooking_time": 45,
52
+ # },
53
+ # "user_2": {
54
+ # "vegetarian_type": "flexible_vegetarian",
55
+ # "allergies": "shrimp",
56
+ # "regions": "Asia",
57
+ # "calorie_min": 400,
58
+ # "calorie_max": 1500,
59
+ # "protein_min": 40,
60
+ # "protein_max": 120,
61
+ # "preferred_main": "tofu",
62
+ # "disliked_main": "beef",
63
+ # "cooking_time": 60,
64
+ # },
65
+ # "user_3": {
66
+ # "vegetarian_type": "non_vegetarian",
67
+ # "allergies": "",
68
+ # "regions": "Europe",
69
+ # "calorie_min": 500,
70
+ # "calorie_max": 2000,
71
+ # "protein_min": 80,
72
+ # "protein_max": 160,
73
+ # "preferred_main": "beef, chicken",
74
+ # "disliked_main": "",
75
+ # "cooking_time": 45,
76
+ # },
77
+ # }
78
+
79
+ EXAMPLE_IDS = [
80
+ uid for uid in ("user_1", "user_2", "user_3", "user_5")
81
+ if (USER_DATA_DIR / uid / "user_profile.json").exists()
82
+ ]
83
+
84
+ # Predefined example images
85
+ EXAMPLE_IMAGES = [
86
+ "frige_detect/demo/t1.jpg",
87
+ "frige_detect/demo/t2.jpg",
88
+ "frige_detect/demo/t3.jpg",
89
+ ]
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Helper utilities
94
+ # ---------------------------------------------------------------------------
95
+ def parse_csv_list(text: str) -> List[str]:
96
+ if not text:
97
+ return []
98
+ parts = [item.strip() for item in text.split(",") if item.strip()]
99
+ return parts
100
+
101
+
102
+ def ensure_numpy_image(image: Any) -> np.ndarray:
103
+ """Convert incoming image (PIL or numpy) to RGB numpy array."""
104
+ if image is None:
105
+ raise ValueError("Please upload a fridge photo before running detection.")
106
+ if isinstance(image, np.ndarray):
107
+ return image
108
+ if isinstance(image, Image.Image):
109
+ return np.array(image.convert("RGB"))
110
+ raise ValueError("Unsupported image format provided.")
111
+
112
+
113
+ def write_temp_image(image: np.ndarray) -> str:
114
+ """Write numpy image to a temporary file and return the path."""
115
+ temp_dir = Path(tempfile.mkdtemp(prefix="fridge_upload_"))
116
+ temp_path = temp_dir / "upload.jpg"
117
+ bgr_image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
118
+ cv2.imwrite(str(temp_path), bgr_image)
119
+ return str(temp_path)
120
+
121
+
122
+ def build_user_profile(
123
+ user_id: str,
124
+ vegetarian_type: str,
125
+ allergies: str,
126
+ regions: str,
127
+ calorie_range: Tuple[float, float],
128
+ protein_range: Tuple[float, float],
129
+ preferred_main: str,
130
+ disliked_main: str,
131
+ cooking_time: float,
132
+ ) -> Dict[str, Any]:
133
+ """
134
+ Build and save user profile. This function ALWAYS creates or overwrites the profile
135
+ with the current input values, enabling users to modify preferences on-the-fly.
136
+ """
137
+ user_id = user_id.strip()
138
+ if not user_id:
139
+ raise ValueError("User ID cannot be empty.")
140
+
141
+ profile_dir = USER_DATA_DIR / user_id
142
+ profile_path = profile_dir / "user_profile.json"
143
+
144
+ # Preserve feedback count if profile exists
145
+ num_feedback = 0
146
+ if profile_path.exists():
147
+ try:
148
+ existing = json.loads(profile_path.read_text(encoding="utf-8"))
149
+ num_feedback = existing.get("num_feedback", 0)
150
+ except Exception:
151
+ pass
152
+
153
+ profile = {
154
+ "user_id": user_id,
155
+ "num_feedback": num_feedback,
156
+ "diet": {"vegetarian_type": vegetarian_type},
157
+ "allergies": parse_csv_list(allergies),
158
+ "region_preference": parse_csv_list(regions),
159
+ "nutritional_goals": {
160
+ "calories": {"min": int(calorie_range[0]), "max": int(calorie_range[1])},
161
+ "protein": {"min": int(protein_range[0]), "max": int(protein_range[1])},
162
+ },
163
+ "other_preferences": {
164
+ "preferred_main": parse_csv_list(preferred_main),
165
+ "disliked_main": parse_csv_list(disliked_main),
166
+ "cooking_time_max": int(cooking_time) if cooking_time else None,
167
+ },
168
+ }
169
+
170
+ # Always save the profile (create new or overwrite existing)
171
+ save_user_profile(user_id, profile)
172
+ print(f"[app] Profile saved/updated for user '{user_id}'")
173
+
174
+ return profile
175
+
176
+
177
+ def summarize_ingredients(
178
+ user_parents: List[str],
179
+ high_conf: List[str],
180
+ low_conf: List[str],
181
+ ) -> str:
182
+ lines = ["### Ingredient Mapping"]
183
+ if user_parents:
184
+ lines.append("- **Mapped parent ingredients:** " + ", ".join(sorted(user_parents)))
185
+ else:
186
+ lines.append("- **Mapped parent ingredients:** none")
187
+ if high_conf:
188
+ lines.append("- **High confidence detections:** " + ", ".join(sorted(high_conf)))
189
+ if low_conf:
190
+ lines.append("- **Low confidence detections:** " + ", ".join(sorted(set(low_conf))))
191
+ return "\n".join(lines)
192
+
193
+
194
+ def _ensure_iterable(value: Any) -> List[str]:
195
+ if value is None:
196
+ return []
197
+ if isinstance(value, set):
198
+ return sorted(value)
199
+ if isinstance(value, list):
200
+ return value
201
+ if isinstance(value, str):
202
+ return [value]
203
+ return list(value)
204
+
205
+
206
+ def render_recommendations(df, user_parents=None):
207
+ """
208
+ Render the top-k recommendation list in Markdown format with
209
+ ✅/❌ marks for ingredients. Uses original ingredient names instead of parent names.
210
+ """
211
+ if user_parents is None:
212
+ user_parents = set()
213
+ else:
214
+ user_parents = set(user_parents)
215
+
216
+ feedback_rows = []
217
+ md_lines = []
218
+
219
+ for i, row in df.iterrows():
220
+ # --- Header line with score ---
221
+ name = row.get("name", "Unknown")
222
+ score = row.get("score", None)
223
+ if score is not None:
224
+ line = f"**{i+1}. {name} — score {score:.1f}%**"
225
+ else:
226
+ line = f"**{i+1}. {name}**"
227
+ md_lines.append(line)
228
+
229
+ # --- Region / Cuisine ---
230
+ region = row.get("region", "Unavailable")
231
+ cuisine = _ensure_iterable(row.get("cuisine_attr"))
232
+ cuisine_str = ", ".join(cuisine) if cuisine else "Unavailable"
233
+ md_lines.append(f" - Region: {region}")
234
+ md_lines.append(f" - Cuisine: {cuisine_str}")
235
+
236
+ # --- Nutrition ---
237
+ calories = row.get("calories", "N/A")
238
+ protein = row.get("protein", "N/A")
239
+ md_lines.append(f" - Calories: {calories}")
240
+ md_lines.append(f" - Protein: {protein}")
241
+
242
+ # --- Build mapping: parent -> list of original ingredient strings ---
243
+ ingredient_list = _ensure_iterable(row.get("ingredients"))
244
+ parent_to_ing = {}
245
+
246
+ for ing in ingredient_list:
247
+ ing_lower = ing.lower()
248
+ for parent_cat in ["main_parent", "staple_parent", "other_parent", "seasoning_parent"]:
249
+ parents = _ensure_iterable(row.get(parent_cat))
250
+ for p in parents:
251
+ if p in ing_lower:
252
+ parent_to_ing.setdefault(p, []).append(ing)
253
+
254
+ # --- Ingredient categories with ✅/❌ based on parent presence ---
255
+ for key, label in [
256
+ ("main_parent", "Main Ingredients"),
257
+ ("staple_parent", "Staple Ingredients"),
258
+ ("other_parent", "Other Ingredients")
259
+ ]:
260
+ parents = _ensure_iterable(row.get(key))
261
+ if parents:
262
+ annotated = []
263
+ for p in parents:
264
+ ing_names = parent_to_ing.get(p, [p]) # fallback to parent name if no match
265
+ mark = "✅" if p in user_parents else "❌"
266
+ annotated.append(f"{', '.join(ing_names)} {mark}")
267
+ md_lines.append(f" - {label}: {', '.join(annotated)}")
268
+
269
+ # --- Seasoning (no marks) ---
270
+ seasoning_parents = _ensure_iterable(row.get("seasoning_parent"))
271
+ if seasoning_parents:
272
+ seasoning_names = []
273
+ for p in seasoning_parents:
274
+ seasoning_names.extend(parent_to_ing.get(p, [p]))
275
+ md_lines.append(f" - Seasoning: {', '.join(seasoning_names)}")
276
+
277
+ md_lines.append("") # spacing
278
+ feedback_rows.append({
279
+ "recipe_name": name,
280
+ "recipe_id": row.get("recipe_id"),
281
+ "full_row": row.to_dict(),
282
+ })
283
+
284
+ return "\n".join(md_lines), feedback_rows
285
+
286
+
287
+ def load_example_profile(profile_name: str):
288
+ """Load example user profile from recipe_recommendation/user_data/<user_x>/user_profile.json"""
289
+ try:
290
+ p = USER_DATA_DIR / profile_name / "user_profile.json"
291
+ data = json.loads(p.read_text(encoding="utf-8"))
292
+
293
+ veg_type = (data.get("diet", {}).get("vegetarian_type") or "flexible")
294
+ allergies = ",".join(data.get("allergies", []) or [])
295
+ regions = ",".join(data.get("region_preference", []) or [])
296
+
297
+ ng = data.get("nutritional_goals", {})
298
+ cal_min = int(ng.get("calories", {}).get("min", 400))
299
+ cal_max = int(ng.get("calories", {}).get("max", 2000))
300
+ pro_min = int(ng.get("protein", {}).get("min", 10))
301
+ pro_max = int(ng.get("protein", {}).get("max", 160))
302
+
303
+ op = data.get("other_preferences", {})
304
+ preferred_main = ",".join(op.get("preferred_main", []) or [])
305
+ disliked_main = ",".join(op.get("disliked_main", []) or [])
306
+ cooking_time = int(op.get("cooking_time_max", 45) or 45)
307
+
308
+ return (
309
+ profile_name,
310
+ veg_type,
311
+ allergies,
312
+ regions,
313
+ cal_min,
314
+ cal_max,
315
+ pro_min,
316
+ pro_max,
317
+ preferred_main,
318
+ disliked_main,
319
+ cooking_time,
320
+ )
321
+ except Exception as exc:
322
+ return ("user_custom", "flexible", "", "", 400, 2000, 10, 160, "", "", 45)
323
+
324
+
325
+ def load_example_image(image_path: str):
326
+ """Load an example image."""
327
+ return image_path
328
+
329
+
330
+ def run_pipeline(
331
+ image,
332
+ user_id,
333
+ vegetarian_type,
334
+ allergies,
335
+ regions,
336
+ calorie_min,
337
+ calorie_max,
338
+ protein_min,
339
+ protein_max,
340
+ preferred_main,
341
+ disliked_main,
342
+ cooking_time,
343
+ ):
344
+ """
345
+ Main pipeline function.
346
+ This ALWAYS creates/updates the user profile based on current input values,
347
+ then runs detection and recommendation.
348
+ """
349
+ try:
350
+ rgb_image = ensure_numpy_image(image)
351
+ upload_path = write_temp_image(rgb_image)
352
+ temp_dir = Path(tempfile.mkdtemp(prefix="fridge_outputs_"))
353
+ output_json = temp_dir / "recipe_input.json"
354
+ output_image = temp_dir / "annotated_image.jpg"
355
+
356
+ detection_result = detect_and_generate(
357
+ image_path=upload_path,
358
+ credentials=ROBOFLOW_CREDENTIALS,
359
+ conf_threshold=0.4,
360
+ overlap_threshold=0.3,
361
+ conf_split=0.7,
362
+ output_json=str(output_json),
363
+ output_image=str(output_image),
364
+ )
365
+ Path(upload_path).unlink(missing_ok=True)
366
+
367
+ #2: Always create/update user profile with current UI values
368
+ profile = build_user_profile(
369
+ user_id,
370
+ vegetarian_type,
371
+ allergies,
372
+ regions,
373
+ (calorie_min, calorie_max),
374
+ (protein_min, protein_max),
375
+ preferred_main,
376
+ disliked_main,
377
+ cooking_time,
378
+ )
379
+
380
+
381
+ import time
382
+ time.sleep(1)
383
+
384
+ detection_payload = detection_result["recipe_json"]
385
+ detection_payload_json = json.dumps(detection_payload, ensure_ascii=False, indent=2)
386
+ ml_top, user_parents, high_conf, low_conf = recommend_recipes(
387
+ detection_payload,
388
+ user_id,
389
+ RECIPES_DF,
390
+ topk=5,
391
+ )
392
+
393
+ ingredient_summary = summarize_ingredients(user_parents, high_conf, low_conf)
394
+ recommendation_md, feedback_rows = render_recommendations(ml_top, user_parents)
395
+
396
+
397
+ dropdown_choices = [
398
+ f"{idx + 1}. {row.get('recipe_name', 'Recipe')}" for idx, row in enumerate(feedback_rows)
399
+ ]
400
+
401
+
402
+ status = "" if feedback_rows else "No recipes available for feedback yet."
403
+
404
+ # Add success message about profile creation/update
405
+ profile_status = f"✓ Profile '{user_id}' has been saved/updated with your current preferences."
406
+
407
+ return (
408
+ str(output_image),
409
+ detection_payload_json,
410
+ ingredient_summary,
411
+ recommendation_md,
412
+ gr.update(choices=dropdown_choices, value=None),
413
+ feedback_rows,
414
+ profile_status,
415
+ )
416
+ except Exception as exc:
417
+ import traceback
418
+ error_detail = traceback.format_exc()
419
+ return (
420
+ None,
421
+ "",
422
+ "",
423
+ f"⚠️ Error: {exc}\n\nDetails:\n{error_detail}",
424
+ gr.update(choices=[], value=None),
425
+ [],
426
+ f"⚠️ Error: {exc}",
427
+ )
428
+
429
+
430
+ def record_feedback(selected_recipe: str, user_id: str, feedback_rows: List[Dict[str, Any]]):
431
+ if not selected_recipe:
432
+ return "Please select a recipe before submitting feedback."
433
+ if not user_id:
434
+ return "Please provide a valid user ID."
435
+ if not feedback_rows:
436
+ return "No recommendation data available. Run the pipeline first."
437
+
438
+ try:
439
+ index = int(selected_recipe.split(".")[0]) - 1
440
+ except (ValueError, IndexError):
441
+ return "Unable to parse the selected recipe."
442
+
443
+ if index < 0 or index >= len(feedback_rows):
444
+ return "Selected recipe is out of range."
445
+
446
+ recipe_row = feedback_rows[index]
447
+ get_feedback(user_id, recipe_row)
448
+
449
+ profile_path = USER_DATA_DIR / user_id / "user_profile.json"
450
+ if profile_path.exists():
451
+ data = json.loads(profile_path.read_text(encoding="utf-8"))
452
+ data["num_feedback"] = data.get("num_feedback", 0) + 1
453
+ save_user_profile(user_id, data)
454
+
455
+ return f"✓ Feedback recorded for {recipe_row.get('recipe_name', 'selected recipe')}!"
456
+
457
+
458
+ # ---------------------------------------------------------------------------
459
+ # Gradio UI definition
460
+ # ---------------------------------------------------------------------------
461
+
462
+ # config cheker
463
+ # def check_config():
464
+ # profile_path = USER_DATA_DIR / "user_custom" / "user_profile.json"
465
+ # if not profile_path.exists():
466
+ # print("⚠️ No profile found for user_custom yet")
467
+ # return
468
+
469
+ # with open(profile_path) as f:
470
+ # profile = json.load(f)
471
+
472
+ # from recipe_recommendation.main import normalize_user_profile, prepare_recipes_df
473
+ # profile = normalize_user_profile(profile)
474
+
475
+ # ng = profile['nutritional_goals']
476
+ # cal = ng['calories']
477
+ # pro = ng['protein']
478
+
479
+ # print("\n" + "="*60)
480
+ # print("⚙️ USER CONFIG CHECK")
481
+ # print("="*60)
482
+ # print(f"Calories: {cal['min']} - {cal['max']}")
483
+ # print(f"Protein: {pro['min']} - {pro['max']}g")
484
+
485
+ # df = prepare_recipes_df(RECIPES_DF.copy())
486
+
487
+ # # Test how many pass
488
+ # passed = df[(df['calories'] >= cal['min']) & (df['calories'] <= cal['max']) &
489
+ # (df['protein'] >= pro['min']) & (df['protein'] <= pro['max'])]
490
+
491
+ # print(f"\nRecipes matching your ranges: {len(passed)}/{len(df)} ({len(passed)/len(df)*100:.1f}%)")
492
+
493
+ # if len(passed) == 0:
494
+ # print("\n❌ NO RECIPES match your settings!")
495
+ # print(f"Try: Calories 200-1500, Protein 10-120")
496
+ # else:
497
+ # print(f"\n✅ OK - showing sample:")
498
+ # for _, r in passed.head(3).iterrows():
499
+ # print(f" - {r['name'][:40]}: {r['calories']:.0f} cal, {r['protein']:.0f}g")
500
+ # print("="*60 + "\n")
501
+
502
+ # check_config()
503
+
504
+ def split_ranges(calorie_range, protein_range):
505
+ cal_min, cal_max = calorie_range
506
+ pro_min, pro_max = protein_range
507
+ return cal_min, cal_max, pro_min, pro_max
508
+
509
+
510
+ with gr.Blocks(title="Smart Fridge Recipe Assistant", theme=gr.themes.Soft()) as demo:
511
+ gr.Markdown(
512
+ """
513
+ # Smart Fridge Recipe Assistant
514
+ **How to use:**
515
+ 1. (Optional) Select an example profile and/or image from dropdowns
516
+ 2. Modify any preferences in the form - your profile will be saved automatically when you click Analyze
517
+ 3. Upload or select a fridge image
518
+ 4. Click "Analyze fridge & recommend recipes"
519
+ """
520
+ )
521
+
522
+ with gr.Row():
523
+ with gr.Column(scale=1):
524
+ gr.Markdown("### Quick Start Examples")
525
+ profile_selector = gr.Dropdown(
526
+ label="Choose a predefined user profile",
527
+ choices=EXAMPLE_IDS,
528
+ value=None,
529
+ )
530
+
531
+ image_selector = gr.Dropdown(
532
+ label="Choose an example fridge image",
533
+ choices=[f"Image {i+1}: {img}" for i, img in enumerate(EXAMPLE_IMAGES)],
534
+ value=None,
535
+ )
536
+
537
+ image_input = gr.Image(
538
+ label="Fridge photo (upload or use example)",
539
+ type="pil",
540
+ height=350,
541
+ )
542
+
543
+ detection_json = gr.JSON(label="Detection payload")
544
+ annotated_output = gr.Image(label="Annotated detection", height=350)
545
+
546
+ with gr.Column(scale=1):
547
+ gr.Markdown("### User Preferences (auto-saved on each run)")
548
+ user_id_box = gr.Textbox(
549
+ label="User ID (will create new profile if doesn't exist)",
550
+ value="user_custom",
551
+ placeholder="e.g. my_new_profile",
552
+ )
553
+ vegetarian_radio = gr.Radio(
554
+ [
555
+ "flexible",
556
+ "flexible_vegetarian",
557
+ "ovo_vegetarian",
558
+ "lacto_vegetarian",
559
+ "vegan",
560
+ "non_vegetarian",
561
+ ],
562
+ label="Vegetarian preference",
563
+ value="flexible",
564
+ )
565
+ allergies_box = gr.Textbox(
566
+ label="Allergies (comma separated)",
567
+ placeholder="peanut, shrimp",
568
+ )
569
+ regions_box = gr.Textbox(
570
+ label="Preferred regions (comma separated)",
571
+ placeholder="Asia, Europe",
572
+ )
573
+ calorie_min_slider = gr.Slider(0, 4000, value=400, step=50, label="Min Calories")
574
+ calorie_max_slider = gr.Slider(0, 4000, value=2000, step=50, label="Max Calories")
575
+
576
+ protein_min_slider = gr.Slider(
577
+ minimum=0,
578
+ maximum=250,
579
+ value=10,
580
+ step=5,
581
+ label="Protein Min (g)",
582
+ container=False
583
+ )
584
+ protein_max_slider = gr.Slider(
585
+ minimum=0,
586
+ maximum=250,
587
+ value=160,
588
+ step=5,
589
+ label="Protein Max (g)",
590
+ container=False
591
+ )
592
+ preferred_box = gr.Textbox(
593
+ label="Preferred main ingredients",
594
+ placeholder="chicken, tofu",
595
+ )
596
+ disliked_box = gr.Textbox(
597
+ label="Disliked main ingredients",
598
+ placeholder="lamb",
599
+ )
600
+ cooking_slider = gr.Slider(
601
+ minimum=0,
602
+ maximum=180,
603
+ value=45,
604
+ step=5,
605
+ label="Max cooking time (minutes)",
606
+ )
607
+ run_button = gr.Button("Analyze fridge & recommend recipes", variant="primary")
608
+ ingredient_md = gr.Markdown()
609
+ recommendation_md = gr.Markdown()
610
+ feedback_dropdown = gr.Dropdown(label="Select a recipe for positive feedback", choices=[])
611
+ feedback_button = gr.Button("Save feedback")
612
+ feedback_status = gr.Markdown()
613
+ feedback_state = gr.State([])
614
+
615
+ # Connect profile selector
616
+ profile_selector.change(
617
+ fn=load_example_profile,
618
+ inputs=[profile_selector],
619
+ outputs=[
620
+ user_id_box,
621
+ vegetarian_radio,
622
+ allergies_box,
623
+ regions_box,
624
+ calorie_min_slider,
625
+ calorie_max_slider,
626
+ protein_min_slider,
627
+ protein_max_slider,
628
+ preferred_box,
629
+ disliked_box,
630
+ cooking_slider,
631
+ ],
632
+ )
633
+
634
+ # Connect image selector
635
+ def select_image(choice):
636
+ if choice:
637
+ idx = int(choice.split(":")[0].replace("Image ", "")) - 1
638
+ return EXAMPLE_IMAGES[idx]
639
+ return None
640
+
641
+ image_selector.change(
642
+ fn=select_image,
643
+ inputs=[image_selector],
644
+ outputs=[image_input],
645
+ )
646
+
647
+ run_button.click(
648
+ fn=run_pipeline,
649
+ inputs=[
650
+ image_input,
651
+ user_id_box,
652
+ vegetarian_radio,
653
+ allergies_box,
654
+ regions_box,
655
+ calorie_min_slider,
656
+ calorie_max_slider,
657
+ protein_min_slider,
658
+ protein_max_slider,
659
+ preferred_box,
660
+ disliked_box,
661
+ cooking_slider,
662
+ ],
663
+ outputs=[
664
+ annotated_output,
665
+ detection_json,
666
+ ingredient_md,
667
+ recommendation_md,
668
+ feedback_dropdown,
669
+ feedback_state,
670
+ feedback_status,
671
+ ],
672
+ )
673
+
674
+ feedback_button.click(
675
+ fn=record_feedback,
676
+ inputs=[feedback_dropdown, user_id_box, feedback_state],
677
+ outputs=feedback_status,
678
+ )
679
+
680
+ if __name__ == "__main__":
681
+ demo.launch(share=True)