benjac8 commited on
Commit
b393d63
·
verified ·
1 Parent(s): 2677271

Upload 6 files

Browse files
Files changed (6) hide show
  1. DEPLOY_GUIDE.md +79 -0
  2. README.md +53 -7
  3. app.py +377 -0
  4. biobite_embeddings.parquet +3 -0
  5. recovery_guidance.json +32 -0
  6. requirements.txt +11 -0
DEPLOY_GUIDE.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deploying Bio-Bite to a Hugging Face Space
2
+
3
+ ## 1. Create the Space
4
+
5
+ 1. Go to [huggingface.co](https://huggingface.co) → profile picture → **New Space**
6
+ 2. **Space name:** `bio-bite` (owner: `benjac8`)
7
+ 3. **License:** MIT
8
+ 4. **SDK:** **Gradio**
9
+ 5. **Hardware:** **ZeroGPU** ⚠️ *this is the important one* — the free CPU tier is far too slow for a 3B model
10
+ 6. **Visibility:** Public
11
+ 7. Create
12
+
13
+ > ZeroGPU is free but requires a verified email and an account older than 30 days, with a limit of 2 ZeroGPU Spaces per free account. If ZeroGPU isn't offered, see *Troubleshooting* below.
14
+
15
+ ## 2. Upload the four files
16
+
17
+ **Files** tab → **Add file → Upload files**, then drag in all of these:
18
+
19
+ | File | Size | Purpose |
20
+ |---|---|---|
21
+ | `app.py` | 13 KB | The application |
22
+ | `requirements.txt` | <1 KB | Dependencies |
23
+ | `README.md` | 3 KB | Space card (its YAML header configures the Space) |
24
+ | `biobite_embeddings.parquet` | 22 MB | Precomputed embeddings (uploads via Git LFS automatically) |
25
+ | `recovery_guidance.json` | 2 KB | The coded recovery science |
26
+
27
+ Commit. The Space will start building — watch the **Logs** tab.
28
+
29
+ First build takes ~5–10 minutes (installing torch/transformers, then downloading the models on first run).
30
+
31
+ ## 3. Optional: the live-data bonus
32
+
33
+ **Settings → Variables and secrets → New secret**
34
+
35
+ - Name: `SPOONACULAR_API_KEY`
36
+ - Value: your free key from [spoonacular.com/food-api](https://spoonacular.com/food-api)
37
+
38
+ The app then shows a real photo of the generated dish. Without the key it simply skips the image — nothing breaks.
39
+
40
+ ## 4. Test it
41
+
42
+ Click a **Quick Starter**, then **Generate My Bio-Bite**. Expect ~15–25 seconds for the first response (model warm-up), faster afterwards.
43
+
44
+ Verify:
45
+
46
+ - [ ] 3 recipe cards appear with sensible macros
47
+ - [ ] The detected recovery state matches the description
48
+ - [ ] The generated recipe respects the constraint (e.g. no salmon when you said "no salmon")
49
+ - [ ] "Why this works" mentions relevant nutrients
50
+ - [ ] Tomorrow's plan has timed bullets
51
+ - [ ] Disclaimer is visible
52
+
53
+ ## 5. Submit
54
+
55
+ Put both links on Moodle:
56
+
57
+ - **Dataset:** `https://huggingface.co/datasets/benjac8/bio-bite-recovery-nutrition`
58
+ - **Space:** `https://huggingface.co/spaces/benjac8/bio-bite`
59
+
60
+ ---
61
+
62
+ ## Troubleshooting
63
+
64
+ **Build fails on `spaces` import** — that's normal locally; on a ZeroGPU Space the package is preinstalled. If you're on CPU hardware, the app falls back automatically (just slowly).
65
+
66
+ **"GPU quota exceeded"** — the free ZeroGPU allowance is ~5 minutes of GPU time per day (roughly 15–20 requests). Don't burn it on casual testing; save it for the demo. It resets daily.
67
+
68
+ **Out of memory** — reduce `max_new_tokens` in `app.py` (760 → 500), or switch `GEN_MODEL` to `Qwen/Qwen2.5-1.5B-Instruct` (note: it failed JSON validation in benchmarking, so quality will drop).
69
+
70
+ **Model returns unexpected format** — the app catches this and asks the user to press again. It's occasional and expected with sampling; pressing again resolves it.
71
+
72
+ **Slow first request** — the models download on first run (~6 GB). Subsequent requests are much faster. Warm the Space up a few minutes before presenting.
73
+
74
+ ## Demo-day checklist
75
+
76
+ - Open the Space ~10 minutes early and run one query to warm it up
77
+ - Have the dataset page open in a second tab
78
+ - Know your headline numbers: 10,000 rows · precision@3 = 0.806 · 3 embedding models compared · 3 generators benchmarked
79
+ - Be ready to explain: *why the science is coded rather than generated*, and *why the 3B model was both faster and more reliable than the smaller ones*
README.md CHANGED
@@ -1,13 +1,59 @@
1
  ---
2
- title: Bio Bite
3
- emoji:
4
- colorFrom: red
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.22.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Bio-Bite Recovery Nutrition Engine
3
+ emoji: 🥗
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.9.1
 
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
+ short_description: Turn wearable recovery data into a meal and a next-day plan
12
  ---
13
 
14
+ # 🥗 Bio-Bite Recovery Nutrition Engine
15
+
16
+ Your smartwatch tells you that you slept 5 hours and hit a strain of 18/21. **So what should you eat?**
17
+
18
+ Bio-Bite closes the gap between *seeing* your recovery numbers and *knowing what to do with them*. Describe your day in plain language and it returns three matched recovery meals, one brand-new recipe adapted to what's actually in your fridge, the science behind it, and a plan for tomorrow.
19
+
20
+ ## How it works
21
+
22
+ ```
23
+ USER INPUT (free text + wearable numbers)
24
+
25
+ route to 1 of 6 recovery states (majority vote over retrieved rows)
26
+
27
+ embed query → FAISS search over 10,000 recipes
28
+
29
+ 3 recommended recipes + RAG generation
30
+
31
+ AI OUTPUT: new recipe · why it works · tomorrow's plan
32
+ ```
33
+
34
+ | Component | Choice | Why |
35
+ |---|---|---|
36
+ | **Dataset** | [`benjac8/bio-bite-recovery-nutrition`](https://huggingface.co/datasets/benjac8/bio-bite-recovery-nutrition) — 10,000 rows | Read live from the Hub |
37
+ | **Embeddings** | `BAAI/bge-small-en-v1.5` | Won a 3-model comparison: **precision@3 = 0.806** vs 0.759 (MiniLM) and 0.676 (E5) |
38
+ | **Search** | FAISS `IndexFlatIP` on normalised vectors | Cosine similarity, fast enough for live use |
39
+ | **Generation** | `Qwen/Qwen2.5-3B-Instruct` | Benchmarked fastest (15.4s) **and** the only candidate producing valid JSON |
40
+
41
+ ### Grounded, not hallucinated
42
+
43
+ The recovery science is **encoded in code**, not invented by the model. Each of the 6 recovery states maps to fixed, evidence-based guidance (protein for muscle repair, carbohydrate for glycogen, magnesium for sleep, electrolytes for rehydration). The language model only *phrases* the recipe and the plan — it cannot contradict the physiology.
44
+
45
+ ## Files
46
+
47
+ - `app.py` — the Gradio application (retrieval + generation)
48
+ - `biobite_embeddings.parquet` — 10,000 × 384 precomputed embeddings
49
+ - `recovery_guidance.json` — the coded recovery science
50
+
51
+ ## Optional: live dish photos
52
+
53
+ Set a `SPOONACULAR_API_KEY` secret in the Space settings to fetch a real photo of the generated dish. The app works fine without it.
54
+
55
+ ---
56
+
57
+ ⚠️ **Educational prototype — not medical, nutritional or training advice.** The dataset is synthetic (generated by a language model) and has not been reviewed by a registered dietitian. Consult a qualified professional for personal guidance.
58
+
59
+ *Built for the Intro to Data Science final project, Reichman University.*
app.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Bio-Bite — Recovery Nutrition Engine
3
+ ====================================
4
+ Reads the recovery data your smartwatch already collects (strain, sleep, HRV)
5
+ and turns it into a personalized recovery meal and a next-day plan.
6
+
7
+ Pipeline: USER INPUT -> embed -> FAISS top-3 -> RAG generation -> AI OUTPUT
8
+
9
+ - Dataset : read directly from the Hugging Face Dataset repo
10
+ - Embedder : BAAI/bge-small-en-v1.5 (winner of a 3-model comparison, P@3 = 0.806)
11
+ - Generator: Qwen/Qwen2.5-3B-Instruct (fastest AND only model producing valid JSON)
12
+ """
13
+
14
+ import json
15
+ import os
16
+ import re
17
+
18
+ import faiss
19
+ import gradio as gr
20
+ import numpy as np
21
+ import pandas as pd
22
+ import torch
23
+ from datasets import load_dataset
24
+ from sentence_transformers import SentenceTransformer
25
+ from transformers import pipeline
26
+
27
+ # ZeroGPU support (falls back gracefully when running locally / on CPU)
28
+ try:
29
+ import spaces
30
+ ZERO_GPU = True
31
+ except ImportError: # local run
32
+ ZERO_GPU = False
33
+
34
+ class _Dummy:
35
+ @staticmethod
36
+ def GPU(*a, **k):
37
+ def deco(fn):
38
+ return fn
39
+ return deco
40
+ spaces = _Dummy()
41
+
42
+ # --------------------------------------------------------------------------
43
+ # Configuration
44
+ # --------------------------------------------------------------------------
45
+ SEED = 42
46
+ HF_DATASET = "benjac8/bio-bite-recovery-nutrition"
47
+ EMBED_MODEL = "BAAI/bge-small-en-v1.5"
48
+ QUERY_PREFIX = "Represent this sentence for searching relevant passages: "
49
+ GEN_MODEL = "Qwen/Qwen2.5-3B-Instruct"
50
+ EMB_FILE = "biobite_embeddings.parquet"
51
+ GUIDANCE_FILE = "recovery_guidance.json"
52
+ SPOONACULAR_KEY = os.environ.get("SPOONACULAR_API_KEY", "")
53
+
54
+ np.random.seed(SEED)
55
+ torch.manual_seed(SEED)
56
+
57
+ # --------------------------------------------------------------------------
58
+ # Load data, index and models (once, at startup)
59
+ # --------------------------------------------------------------------------
60
+ print("Loading dataset from Hugging Face…")
61
+ df = load_dataset(HF_DATASET, split="train").to_pandas()
62
+
63
+ print("Loading embeddings…")
64
+ doc_emb = pd.read_parquet(EMB_FILE).to_numpy().astype("float32")
65
+ index = faiss.IndexFlatIP(doc_emb.shape[1])
66
+ index.add(doc_emb)
67
+
68
+ with open(GUIDANCE_FILE) as fh:
69
+ NEXT_DAY_GUIDANCE = json.load(fh)
70
+
71
+ print("Loading models…")
72
+ embedder = SentenceTransformer(EMBED_MODEL)
73
+ generator = pipeline(
74
+ "text-generation",
75
+ model=GEN_MODEL,
76
+ torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
77
+ device_map="auto" if torch.cuda.is_available() else None,
78
+ )
79
+ generator.tokenizer.pad_token_id = generator.tokenizer.eos_token_id
80
+ print(f"Ready — {len(df)} recipes indexed.")
81
+
82
+
83
+ # --------------------------------------------------------------------------
84
+ # Retrieval
85
+ # --------------------------------------------------------------------------
86
+ def retrieve(user_text, k=3, diet=None, max_prep=None, pool=250):
87
+ """Top-k recovery recipes for a free-text description of the user's day."""
88
+ q = embedder.encode(
89
+ [QUERY_PREFIX + user_text], convert_to_numpy=True, normalize_embeddings=True
90
+ ).astype("float32")
91
+ scores, idx = index.search(q, pool)
92
+ cand = df.iloc[idx[0]].copy()
93
+ cand["similarity"] = scores[0]
94
+ if diet and diet != "Any":
95
+ cand = cand[cand["diet_tag"] == diet]
96
+ if max_prep:
97
+ cand = cand[cand["Prep_Time"] <= max_prep]
98
+ if len(cand) == 0: # filters too strict -> fall back to unfiltered
99
+ cand = df.iloc[idx[0]].copy()
100
+ cand["similarity"] = scores[0]
101
+ return cand.head(k)
102
+
103
+
104
+ def infer_recovery_category(user_text, k=7):
105
+ """Route free text to a recovery state by majority vote over retrieved rows."""
106
+ return retrieve(user_text, k=k)["recovery_category"].mode().iloc[0]
107
+
108
+
109
+ # --------------------------------------------------------------------------
110
+ # Generation (single combined call keeps latency ~15s within ZeroGPU quota)
111
+ # --------------------------------------------------------------------------
112
+ PROMPT = """You are an expert sports-nutrition dietitian and recovery coach.
113
+
114
+ The athlete describes their day as: "{state}" ({numbers})
115
+ Their recovery goal is: {category}
116
+ Nutritional need: {need}
117
+
118
+ A recommended recipe from our database, to use as inspiration:
119
+ - Name: {name}
120
+ - Ingredients: {ingredients}
121
+ - Prep time: {prep} minutes
122
+
123
+ The athlete now says: "{constraint}"
124
+
125
+ Evidence-based recovery guidance you MUST follow (do not contradict it):
126
+ - Training tomorrow: {training}
127
+ - Nutrition tomorrow: {nutrition}
128
+ - Sleep tonight: {sleep}
129
+
130
+ Adapt the recipe into a NEW dish that respects the athlete's request while still
131
+ meeting the nutritional need. Then write tomorrow's recovery plan.
132
+ Write in ENGLISH only.
133
+ Reply with ONE valid JSON object and NOTHING else, with exactly these keys:
134
+ - "Recipe_Name": string (an original name for the new dish)
135
+ - "Ingredients": string (comma-separated)
136
+ - "Instructions": string (numbered steps)
137
+ - "prep_time_min": integer
138
+ - "why_it_works": string (2-3 sentences naming the key nutrients and the
139
+ mechanism, e.g. protein for muscle repair, magnesium for sleep quality)
140
+ - "next_day_plan": string (5 bullet points, each starting with a clock time like
141
+ "07:30 - ", covering hydration, meals, training or rest, and a bedtime target;
142
+ max 18 words per bullet, separated by newlines)
143
+ """
144
+
145
+ REQUIRED_KEYS = ["Recipe_Name", "Ingredients", "Instructions",
146
+ "prep_time_min", "why_it_works", "next_day_plan"]
147
+
148
+
149
+ @spaces.GPU(duration=110)
150
+ def generate_biobite(source_row, state, constraint, category, numbers):
151
+ """RAG generation: retrieved recipe + coded science -> new recipe + plan."""
152
+ g = NEXT_DAY_GUIDANCE[category]
153
+ prompt = PROMPT.format(
154
+ state=state, numbers=numbers, category=category,
155
+ need=source_row["Nutritional_Need"], name=source_row["Recipe_Name"],
156
+ ingredients=source_row["Ingredients"], prep=source_row["Prep_Time"],
157
+ constraint=constraint, training=g["training"],
158
+ nutrition=g["nutrition"], sleep=g["sleep"],
159
+ )
160
+ out = generator(
161
+ [{"role": "user", "content": prompt}],
162
+ max_new_tokens=760, do_sample=True, temperature=0.7, top_p=0.9,
163
+ pad_token_id=generator.tokenizer.eos_token_id,
164
+ )
165
+ raw = out[0]["generated_text"][-1]["content"]
166
+
167
+ m = re.search(r"\{.*\}", raw, re.DOTALL)
168
+ if not m:
169
+ return None
170
+ try:
171
+ obj = json.loads(m.group(0))
172
+ except json.JSONDecodeError:
173
+ return None
174
+ if any(k not in obj for k in REQUIRED_KEYS):
175
+ return None
176
+ if isinstance(obj["Ingredients"], list):
177
+ obj["Ingredients"] = ", ".join(map(str, obj["Ingredients"]))
178
+ if isinstance(obj["next_day_plan"], list):
179
+ obj["next_day_plan"] = "\n".join(map(str, obj["next_day_plan"]))
180
+ return obj
181
+
182
+
183
+ # --------------------------------------------------------------------------
184
+ # Bonus: fetch a real dish photo from a live recipe API
185
+ # --------------------------------------------------------------------------
186
+ def fetch_dish_image(recipe_name):
187
+ """Live-data bonus. Returns an image URL or None (never breaks the app)."""
188
+ if not SPOONACULAR_KEY:
189
+ return None
190
+ try:
191
+ import requests
192
+ r = requests.get(
193
+ "https://api.spoonacular.com/recipes/complexSearch",
194
+ params={"query": recipe_name, "number": 1, "apiKey": SPOONACULAR_KEY},
195
+ timeout=6,
196
+ )
197
+ hits = r.json().get("results", [])
198
+ return hits[0].get("image") if hits else None
199
+ except Exception:
200
+ return None
201
+
202
+
203
+ # --------------------------------------------------------------------------
204
+ # HTML rendering
205
+ # --------------------------------------------------------------------------
206
+ def cards_html(rows):
207
+ cards = []
208
+ for _, r in rows.iterrows():
209
+ cards.append(f"""
210
+ <div style="flex:1;min-width:210px;border:1px solid #e3e3e3;border-radius:12px;
211
+ padding:14px;background:#fff;">
212
+ <div style="font-size:11px;text-transform:uppercase;letter-spacing:.5px;
213
+ color:#888;">{r['recovery_category']}</div>
214
+ <div style="font-weight:600;font-size:15px;margin:6px 0 10px;">
215
+ {r['Recipe_Name']}</div>
216
+ <div style="font-size:12px;color:#555;line-height:1.7;">
217
+ ⏱ {r['Prep_Time']} min &nbsp;·&nbsp; 🔥 {r['calories']} kcal<br>
218
+ 🥩 {r['protein_g']}g protein &nbsp;·&nbsp; 🌾 {r['carbs_g']}g carbs<br>
219
+ ✨ {r['magnesium_mg']}mg magnesium<br>
220
+ <span style="color:#888;">{r['cuisine']} · {r['diet_tag']}</span>
221
+ </div>
222
+ </div>""")
223
+ return ('<div style="display:flex;gap:12px;flex-wrap:wrap;">'
224
+ + "".join(cards) + "</div>")
225
+
226
+
227
+ def recipe_html(obj, image_url=None):
228
+ img = (f'<img src="{image_url}" style="width:100%;max-height:210px;'
229
+ f'object-fit:cover;border-radius:10px;margin-bottom:12px;">'
230
+ if image_url else "")
231
+ steps = str(obj["Instructions"]).replace("\n", "<br>")
232
+ return f"""
233
+ <div style="border:2px solid #4C72B0;border-radius:14px;padding:18px;background:#fff;">
234
+ {img}
235
+ <div style="font-size:19px;font-weight:700;margin-bottom:4px;">
236
+ 🍳 {obj['Recipe_Name']}</div>
237
+ <div style="font-size:12px;color:#777;margin-bottom:12px;">
238
+ Ready in {obj['prep_time_min']} minutes</div>
239
+ <div style="font-size:13px;margin-bottom:10px;">
240
+ <b>Ingredients</b><br>{obj['Ingredients']}</div>
241
+ <div style="font-size:13px;margin-bottom:14px;">
242
+ <b>Instructions</b><br>{steps}</div>
243
+ <div style="background:#eef3fa;border-radius:10px;padding:12px;font-size:13px;">
244
+ <b>🔬 Why this works for you</b><br>{obj['why_it_works']}</div>
245
+ </div>"""
246
+
247
+
248
+ def plan_html(plan_text, category):
249
+ bullets = [b.strip(" -•\t") for b in str(plan_text).split("\n") if b.strip()]
250
+ items = "".join(
251
+ f'<li style="margin-bottom:7px;">{b}</li>' for b in bullets)
252
+ return f"""
253
+ <div style="border:1px solid #e3e3e3;border-radius:14px;padding:18px;background:#fff;">
254
+ <div style="font-size:17px;font-weight:700;margin-bottom:2px;">
255
+ 📅 Tomorrow's Recovery Plan</div>
256
+ <div style="font-size:12px;color:#777;margin-bottom:12px;">
257
+ Based on your recovery state: <b>{category}</b></div>
258
+ <ul style="font-size:13px;line-height:1.6;padding-left:20px;margin:0;">{items}</ul>
259
+ </div>"""
260
+
261
+
262
+ # --------------------------------------------------------------------------
263
+ # Main callback
264
+ # --------------------------------------------------------------------------
265
+ def run(state, constraint, diet, max_prep, sleep_hours, strain):
266
+ if not state or not state.strip():
267
+ return ("⚠️ Please describe your day first.", "", "")
268
+
269
+ nums = []
270
+ if sleep_hours:
271
+ nums.append(f"slept {sleep_hours}h")
272
+ if strain:
273
+ nums.append(f"strain {strain}/21")
274
+ numbers = ", ".join(nums) if nums else "no wearable numbers given"
275
+
276
+ category = infer_recovery_category(state)
277
+ top3 = retrieve(state, k=3, diet=diet,
278
+ max_prep=int(max_prep) if max_prep else None)
279
+
280
+ header = (f'<div style="font-size:13px;color:#555;margin-bottom:10px;">'
281
+ f'Detected recovery state: <b>{category}</b> · {numbers}</div>')
282
+ recs = header + cards_html(top3)
283
+
284
+ if not constraint or not constraint.strip():
285
+ constraint = "Keep it simple with easy-to-find ingredients."
286
+
287
+ obj = generate_biobite(top3.iloc[0], state, constraint, category, numbers)
288
+ if obj is None:
289
+ return (recs,
290
+ '<div style="padding:16px;">The model returned an unexpected '
291
+ 'format. Please press the button again.</div>', "")
292
+
293
+ img = fetch_dish_image(obj["Recipe_Name"])
294
+ return recs, recipe_html(obj, img), plan_html(obj["next_day_plan"], category)
295
+
296
+
297
+ # --------------------------------------------------------------------------
298
+ # UI
299
+ # --------------------------------------------------------------------------
300
+ QUICK_STARTERS = [
301
+ ["I did a heavy CrossFit workout today but only slept 4 hours and I'm exhausted",
302
+ "No salmon — only tofu or chicken, and I have just 15 minutes", "Any", 20, 4, 18],
303
+ ["I ran a half marathon this morning and I'm completely drained",
304
+ "I'm vegetarian and want something carb-heavy", "vegetarian", 40, 7, 19],
305
+ ["Really stressful week at work, barely sleeping, my HRV has dropped",
306
+ "Something calming, no caffeine, I have spinach and nuts", "Any", 30, 5, 8],
307
+ ]
308
+
309
+ with gr.Blocks(title="Bio-Bite", theme=gr.themes.Soft()) as demo:
310
+ gr.Markdown(
311
+ """
312
+ # 🥗 Bio-Bite — your recovery, on a plate
313
+ Your watch tells you that you slept 5 hours and hit a strain of 18. **So what should you eat?**
314
+ Bio-Bite turns your recovery data into a personalized meal and a plan for tomorrow.
315
+ """
316
+ )
317
+
318
+ with gr.Row():
319
+ with gr.Column(scale=3):
320
+ state = gr.Textbox(
321
+ label="How was your day, physically?",
322
+ placeholder="e.g. Heavy leg day at the gym, slept badly, feeling wrecked…",
323
+ lines=3,
324
+ )
325
+ constraint = gr.Textbox(
326
+ label="👨‍🍳 What's in your fridge? Any constraints?",
327
+ placeholder="e.g. Only tofu and rice, 15 minutes, no nuts",
328
+ lines=2,
329
+ )
330
+ with gr.Column(scale=2):
331
+ diet = gr.Dropdown(
332
+ ["Any", "omnivore", "vegetarian", "vegan", "pescatarian", "gluten-free"],
333
+ value="Any", label="Diet",
334
+ )
335
+ max_prep = gr.Slider(10, 60, value=30, step=5, label="Max prep time (min)")
336
+ sleep_hours = gr.Slider(0, 10, value=7, step=0.5, label="Sleep last night (h)")
337
+ strain = gr.Slider(0, 21, value=10, step=1, label="Strain today (0–21)")
338
+
339
+ btn = gr.Button("🍽️ Generate My Bio-Bite", variant="primary", size="lg")
340
+
341
+ gr.Markdown("### ⚡ Quick starters — one click to try it")
342
+ gr.Examples(
343
+ examples=QUICK_STARTERS,
344
+ inputs=[state, constraint, diet, max_prep, sleep_hours, strain],
345
+ label="",
346
+ )
347
+
348
+ gr.Markdown("### 🔍 Three recovery meals matched to your state")
349
+ out_recs = gr.HTML()
350
+ with gr.Row():
351
+ with gr.Column():
352
+ gr.Markdown("### ✨ Your personalized Bio-Bite")
353
+ out_recipe = gr.HTML()
354
+ with gr.Column():
355
+ gr.Markdown("### 📅 Your plan for tomorrow")
356
+ out_plan = gr.HTML()
357
+
358
+ gr.Markdown(
359
+ """
360
+ ---
361
+ ⚠️ **Educational prototype — not medical, nutritional or training advice.**
362
+ Recipes come from a synthetic dataset generated by a language model and have not been
363
+ reviewed by a registered dietitian. Consult a qualified professional for personal guidance.
364
+
365
+ *Dataset: [benjac8/bio-bite-recovery-nutrition](https://huggingface.co/datasets/benjac8/bio-bite-recovery-nutrition)
366
+ · Embeddings: BAAI/bge-small-en-v1.5 · Generation: Qwen2.5-3B-Instruct*
367
+ """
368
+ )
369
+
370
+ btn.click(
371
+ run,
372
+ inputs=[state, constraint, diet, max_prep, sleep_hours, strain],
373
+ outputs=[out_recs, out_recipe, out_plan],
374
+ )
375
+
376
+ if __name__ == "__main__":
377
+ demo.launch()
biobite_embeddings.parquet ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b91a021a5f85739848457672745b4b7db8f51c84303b5d3b294753d2df5451db
3
+ size 22280382
recovery_guidance.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "strength-recovery": {
3
+ "training": "Light active recovery or mobility work tomorrow; train the same muscle group again after ~48h.",
4
+ "nutrition": "Spread protein across 3-4 meals (~every 3-4 hours) to sustain muscle protein synthesis.",
5
+ "sleep": "Aim for 8 hours; deep sleep drives growth-hormone release and tissue repair."
6
+ },
7
+ "endurance-recovery": {
8
+ "training": "Easy aerobic (zone 2) session or a full rest day; avoid another hard effort.",
9
+ "nutrition": "Carbohydrate-forward meals to rebuild glycogen; include electrolytes.",
10
+ "sleep": "8 hours; consistent bed and wake times support aerobic adaptation."
11
+ },
12
+ "sleep-deprived": {
13
+ "training": "Reduce intensity - skip high-strain sessions until sleep is restored.",
14
+ "nutrition": "Regular balanced meals; cut caffeine after early afternoon.",
15
+ "sleep": "Prioritise 8-9 hours tonight; dim screens an hour before bed."
16
+ },
17
+ "high-stress": {
18
+ "training": "Gentle movement only - a walk, easy yoga, or breathwork.",
19
+ "nutrition": "Magnesium and omega-3 rich foods; limit caffeine and refined sugar.",
20
+ "sleep": "Consistent bedtime with a wind-down routine to support HRV recovery."
21
+ },
22
+ "rest-day": {
23
+ "training": "Normal training tomorrow is fine - you are recovered.",
24
+ "nutrition": "Balanced, micronutrient-dense meals; keep hydration steady.",
25
+ "sleep": "Maintain your usual 7-9 hour routine."
26
+ },
27
+ "rehydration": {
28
+ "training": "Train at moderate intensity only once fully rehydrated.",
29
+ "nutrition": "Fluids with sodium and potassium; include water-rich foods.",
30
+ "sleep": "7-9 hours; avoid alcohol, which worsens dehydration."
31
+ }
32
+ }
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==5.9.1
2
+ torch
3
+ transformers>=4.44
4
+ accelerate
5
+ sentence-transformers>=3.0
6
+ faiss-cpu
7
+ datasets
8
+ pandas
9
+ pyarrow
10
+ numpy
11
+ requests