Upload 6 files
Browse files- DEPLOY_GUIDE.md +79 -0
- README.md +53 -7
- app.py +377 -0
- biobite_embeddings.parquet +3 -0
- recovery_guidance.json +32 -0
- 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
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 · 🔥 {r['calories']} kcal<br>
|
| 218 |
+
🥩 {r['protein_g']}g protein · 🌾 {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
|