jesusvilela commited on
Commit
8433bea
·
verified ·
1 Parent(s): 453e947

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +439 -121
app.py CHANGED
@@ -1,167 +1,485 @@
 
 
 
 
1
  import gradio as gr
2
  import pandas as pd
 
 
3
  import joblib
4
- from typing import Tuple, Optional
5
 
6
- from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, pipeline
7
- import spaces # <-- IMPORTANT for ZeroGPU
8
  import torch
9
 
10
- # ------------------
11
- # Load models (CPU by default; ZeroGPU will hand us a CUDA device inside GPU-decorated fns)
12
- # ------------------
 
 
 
 
 
 
 
13
  DT_PATH = "./decision_tree_regressor.joblib"
14
  decision_tree_regressor = joblib.load(DT_PATH)
15
 
16
- GEN_MODEL = "google/flan-t5-small"
 
17
  _tokenizer = AutoTokenizer.from_pretrained(GEN_MODEL)
18
  _model = AutoModelForSeq2SeqLM.from_pretrained(GEN_MODEL)
19
-
20
- # A CPU default pipeline; inside the GPU function we’ll create a CUDA pipeline
21
  _generate_cpu = pipeline("text2text-generation", model=_model, tokenizer=_tokenizer, device=-1)
22
 
23
- # ------------------
24
- # ZeroGPU-required functions
25
- # ------------------
26
  @spaces.GPU
27
  def gpu_warmup() -> str:
28
- """
29
- Minimal function so ZeroGPU detects a @spaces.GPU fn at startup.
30
- Also confirms CUDA availability when the function is executed.
31
- """
32
- return f"cuda_available={torch.cuda.is_available()}"
33
 
34
  @spaces.GPU
35
- def generate_on_gpu(prompt: str, max_new_tokens: int = 600) -> str:
36
- """
37
- Runs text generation within the GPU allocation window if available.
38
- Falls back to CPU if CUDA is not available for any reason.
39
  """
40
  try:
41
  if torch.cuda.is_available():
42
- # Recreate a pipeline bound to CUDA(0) to ensure use of GPU in the ZeroGPU window
43
  gen = pipeline(
44
  "text2text-generation",
45
  model=_model.to("cuda"),
46
  tokenizer=_tokenizer,
47
  device=0,
48
  )
49
- out = gen(prompt, max_new_tokens=max_new_tokens)
50
- return out[0]["generated_text"].strip()
51
  else:
52
- out = _generate_cpu(prompt, max_new_tokens=max_new_tokens)
53
- return out[0]["generated_text"].strip()
 
 
 
 
 
54
  except Exception as e:
55
- # Robust fallback
56
- out = _generate_cpu(prompt, max_new_tokens=max_new_tokens)
57
- return out[0]["generated_text"].strip() + f"\n\n(Note: GPU path failed: {e})"
58
 
59
- # Call once at import so the runtime sees at least one GPU function during startup
60
- # (Not strictly required to call, but it’s harmless and helps with diagnostics.)
61
  try:
62
  _ = gpu_warmup()
63
  except Exception:
64
  pass
65
 
66
- # ------------------
67
- # Helpers
68
- # ------------------
69
- def _parse_meal_time(hhmm: str) -> float:
70
- if not hhmm or ":" not in hhmm:
71
- raise ValueError("Enter meal time as HH:MM (e.g., 12:30).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  h, m = hhmm.split(":")
73
  h = int(h); m = int(m)
74
  if not (0 <= h <= 23 and 0 <= m <= 59):
75
- raise ValueError("Hour must be 0–23 and minute 0–59.")
76
- return h + m / 60.0
77
-
78
- def _collect_df(fasting_duration: float, meal_timing: str,
79
- body_weight: float, age: float, gender: str, height: float) -> pd.DataFrame:
80
- mt = _parse_meal_time(meal_timing)
81
- if fasting_duration < 0 or fasting_duration > 72:
82
- raise ValueError("Fasting duration must be between 0 and 72 hours.")
83
- if body_weight <= 0 or height <= 0 or age < 0:
84
- raise ValueError("Please use positive values for weight/height and non-negative age.")
85
-
86
- df = pd.DataFrame({
87
- "Fasting Duration (hours)": [fasting_duration],
88
- "Meal Timing (hour:minute)": [mt],
89
- "Body Weight (kg)": [body_weight],
90
- "Age (years)": [age],
91
- "Height (cm)": [height],
92
- "Gender_Male": [1 if gender == "Male" else 0],
93
- "Gender_Other": [1 if gender == "Other" else 0],
94
- })
95
- return df
96
-
97
- def _gen_recs(health_score: float, fasting_duration: float, meal_timing: str,
98
- body_weight: float, age: float, gender: str, height: float) -> str:
99
- if health_score is None:
100
- return "No score available."
101
- if health_score > 80:
102
- tone = "You're doing great! Keep up the good work."
103
- elif health_score > 60:
104
- tone = "Good score, and there’s room to improve."
105
- else:
106
- tone = "Consider some changes to improve your metabolic health."
107
-
108
- prompt = f"""
109
- You are a health coach. Based on the data below, write:
110
- 1) Three specific lifestyle changes (brief bullets).
111
- 2) A 7-day exercise plan with durations.
112
- 3) A 7-day eating schedule that RESPECTS a daily fasting window of {fasting_duration} hours.
113
- 4) A consolidated shopping list.
114
-
115
- Data:
116
- - Health Score: {health_score:.1f}
117
- - First meal at: {meal_timing} (HH:MM)
118
- - Weight: {body_weight} kg
119
- - Age: {age} years
120
- - Gender: {gender}
121
- - Height: {height} cm
122
-
123
- Start with a one-sentence summary in the same line as "{tone}"
124
- """.strip()
125
 
126
- # Use GPU-allocated function (ZeroGPU) for generation
127
- return generate_on_gpu(prompt, max_new_tokens=600)
128
 
129
- def predict_and_recommend(fasting_duration, meal_timing, body_weight, age, gender, height) -> Tuple[Optional[float], str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  try:
131
- df = _collect_df(fasting_duration, meal_timing, body_weight, age, gender, height)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  score = float(decision_tree_regressor.predict(df)[0])
133
- recs = _gen_recs(score, fasting_duration, meal_timing, body_weight, age, gender, height)
134
- return score, recs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  except Exception as e:
136
- return None, f"⚠️ {e}"
 
 
 
 
 
 
 
 
 
 
 
137
 
138
- # ------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  # UI
140
- # ------------------
141
- with gr.Blocks(title="Intermittent Fasting — Metabolic Health") as demo:
142
- gr.Markdown("## Intermittent Fasting — Metabolic Health Prediction\nEnter your details to get a predicted score and tailored suggestions. (ZeroGPU-enabled; falls back to CPU if needed.)")
143
-
144
- with gr.Row():
145
- with gr.Column():
146
- fasting_duration = gr.Number(label="Fasting Duration (hours)", value=16, minimum=0, maximum=72, step=0.5)
147
- meal_timing = gr.Textbox(label="First Meal Time (HH:MM)", placeholder="e.g., 12:30", value="12:30")
148
- body_weight = gr.Number(label="Body Weight (kg)", value=70, minimum=1, step=0.5)
149
- with gr.Column():
150
- age = gr.Slider(minimum=18, maximum=100, value=35, step=1, label="Age (years)")
151
- gender = gr.Radio(choices=["Male", "Female", "Other"], label="Gender", value="Male")
152
- height = gr.Number(label="Height (cm)", value=175, minimum=50, maximum=250, step=1)
153
-
154
- btn = gr.Button("Predict & Generate Plan", variant="primary")
155
-
156
- score_out = gr.Number(label="Predicted Metabolic Health Score")
157
- plan_out = gr.Textbox(label="Recommendations (copyable)", max_lines=40, show_copy_button=True)
158
-
159
- btn.click(
160
- predict_and_recommend,
161
- inputs=[fasting_duration, meal_timing, body_weight, age, gender, height],
162
- outputs=[score_out, plan_out],
163
- api_name="predict"
164
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  if __name__ == "__main__":
167
- demo.launch()
 
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Optional, Tuple, List, Dict
4
+
5
  import gradio as gr
6
  import pandas as pd
7
+ import numpy as np
8
+ import plotly.express as px
9
  import joblib
 
10
 
11
+ # ZeroGPU hooks (safe on CPU Spaces too)
12
+ import spaces
13
  import torch
14
 
15
+ # Optional micro-model to "polish" text when GPU window is available
16
+ from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, pipeline
17
+
18
+ # ---------------------
19
+ # Constants & storage
20
+ # ---------------------
21
+ DATA_DIR = Path("data"); DATA_DIR.mkdir(exist_ok=True)
22
+ TS_FMT = "%Y-%m-%d %H:%M:%S"
23
+
24
+ # Load your regressor
25
  DT_PATH = "./decision_tree_regressor.joblib"
26
  decision_tree_regressor = joblib.load(DT_PATH)
27
 
28
+ # Lightweight text model (CPU ok, faster on GPU)
29
+ GEN_MODEL = os.getenv("PLAN_POLISH_MODEL", "google/flan-t5-small")
30
  _tokenizer = AutoTokenizer.from_pretrained(GEN_MODEL)
31
  _model = AutoModelForSeq2SeqLM.from_pretrained(GEN_MODEL)
 
 
32
  _generate_cpu = pipeline("text2text-generation", model=_model, tokenizer=_tokenizer, device=-1)
33
 
34
+ # --------------
35
+ # ZeroGPU fns
36
+ # --------------
37
  @spaces.GPU
38
  def gpu_warmup() -> str:
39
+ return f"cuda={torch.cuda.is_available()}"
 
 
 
 
40
 
41
  @spaces.GPU
42
+ def polish_on_gpu(text: str, lang: str = "en") -> str:
43
+ """Polish/translate the already-generated plan inside a GPU window.
44
+ Falls back to CPU gracefully if needed.
 
45
  """
46
  try:
47
  if torch.cuda.is_available():
 
48
  gen = pipeline(
49
  "text2text-generation",
50
  model=_model.to("cuda"),
51
  tokenizer=_tokenizer,
52
  device=0,
53
  )
 
 
54
  else:
55
+ gen = _generate_cpu
56
+ prompt = (
57
+ "Rewrite the following fasting plan in a friendly coaching tone, keep markdown structure, "
58
+ f"and output language '{lang}'. Keep tables and numbered lists concise.\n\n" + text
59
+ )
60
+ out = gen(prompt, max_new_tokens=700)
61
+ return out[0]["generated_text"].strip()
62
  except Exception as e:
63
+ out = _generate_cpu(text, max_new_tokens=10)
64
+ return text + f"\n\n(Polish step skipped: {e})"
 
65
 
 
 
66
  try:
67
  _ = gpu_warmup()
68
  except Exception:
69
  pass
70
 
71
+ # ---------------------
72
+ # Utilities (metrics)
73
+ # ---------------------
74
+ ACTIVITY = {
75
+ "Sedentary": 1.2,
76
+ "Lightly active": 1.375,
77
+ "Moderately active": 1.55,
78
+ "Very active": 1.725,
79
+ "Athlete": 1.9,
80
+ }
81
+
82
+ GOAL_CAL_ADJ = { # % change to TDEE
83
+ "Fat loss": -0.15,
84
+ "Recomp/Maintenance": 0.0,
85
+ "Muscle gain": 0.10,
86
+ }
87
+
88
+ def bmi(weight_kg: float, height_cm: float) -> float:
89
+ return weight_kg / ((height_cm / 100) ** 2)
90
+
91
+
92
+ def bmr_mifflin(sex: str, weight_kg: float, height_cm: float, age: float) -> float:
93
+ s = 5 if sex == "Male" else -161
94
+ return 10 * weight_kg + 6.25 * height_cm - 5 * age + s
95
+
96
+
97
+ def tdee(bmr: float, activity: str) -> float:
98
+ return bmr * ACTIVITY.get(activity, 1.2)
99
+
100
+
101
+ def parse_hhmm(hhmm: str) -> Tuple[int, int]:
102
  h, m = hhmm.split(":")
103
  h = int(h); m = int(m)
104
  if not (0 <= h <= 23 and 0 <= m <= 59):
105
+ raise ValueError("Time must be HH:MM in 24h format.")
106
+ return h, m
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
 
 
108
 
109
+ def fmt_hhmm(h: int, m: int) -> str:
110
+ return f"{h:02d}:{m:02d}"
111
+
112
+ # ---------------------
113
+ # Plan generator (deterministic, rich)
114
+ # ---------------------
115
+ DIET_STYLES = ["Omnivore", "Mediterranean", "Vegetarian", "Vegan", "Low-carb"]
116
+
117
+ MEAL_IDEAS = {
118
+ "Omnivore": [
119
+ "Greek yogurt + berries + nuts",
120
+ "Chicken bowl (rice, veggies, olive oil)",
121
+ "Eggs, avocado, sourdough",
122
+ "Salmon, quinoa, asparagus",
123
+ "Lean beef, sweet potato, salad",
124
+ "Tuna whole-grain wrap",
125
+ "Cottage cheese + fruit + seeds",
126
+ ],
127
+ "Mediterranean": [
128
+ "Oats with dates, walnuts, olive oil drizzle",
129
+ "Grilled fish, lentil salad, greens",
130
+ "Hummus platter, wholegrain pita, veg",
131
+ "Chickpea tomato stew",
132
+ "Feta + olive salad, quinoa",
133
+ "Shakshuka + side salad",
134
+ "Lentils, roasted veg, tahini",
135
+ ],
136
+ "Vegetarian": [
137
+ "Tofu scramble, toast, avocado",
138
+ "Paneer tikka bowl",
139
+ "Bean chili + brown rice",
140
+ "Halloumi, couscous, veg",
141
+ "Greek salad + eggs",
142
+ "Tempeh stir-fry",
143
+ "Yogurt parfait + granola",
144
+ ],
145
+ "Vegan": [
146
+ "Tofu scramble, avocado toast",
147
+ "Lentil curry + basmati",
148
+ "Burrito bowl (beans, corn, salsa)",
149
+ "Seitan, roasted potatoes, veg",
150
+ "Tofu poke bowl",
151
+ "Chickpea pasta + marinara",
152
+ "Overnight oats + banana + peanut butter",
153
+ ],
154
+ "Low-carb": [
155
+ "Eggs, smoked salmon, salad",
156
+ "Chicken Caesar (no croutons)",
157
+ "Beef & greens stir-fry",
158
+ "Omelette + veg + cheese",
159
+ "Zoodles + turkey bolognese",
160
+ "Tofu salad w/ tahini",
161
+ "Yogurt + nuts (moderate)",
162
+ ],
163
+ }
164
+
165
+ WORKOUTS = {
166
+ "Fat loss": [
167
+ "3× LISS cardio 30–40min",
168
+ "2× full‑body strength 45min",
169
+ "1× intervals 12–16min",
170
+ "Daily 8–10k steps"
171
+ ],
172
+ "Recomp/Maintenance": [
173
+ "3× full‑body strength 45–60min",
174
+ "1–2× LISS cardio 30min",
175
+ "Mobility 10min daily",
176
+ "8–10k steps"
177
+ ],
178
+ "Muscle gain": [
179
+ "4× strength split 45–60min",
180
+ "Optional 1× LISS 20–30min",
181
+ "Mobility 10min",
182
+ "7–9k steps"
183
+ ],
184
+ }
185
+
186
+
187
+ def feeding_schedule(first_meal_hhmm: str, fasting_hours: float) -> List[Tuple[str, str]]:
188
+ """Return 7 (start,end) strings for the eating window each day."""
189
+ h, m = parse_hhmm(first_meal_hhmm)
190
+ window = max(0.0, 24 - float(fasting_hours))
191
+ start_minutes = h * 60 + m
192
+ end_minutes = int((start_minutes + window * 60) % (24 * 60))
193
+
194
+ sched = []
195
+ for _ in range(7):
196
+ start = fmt_hhmm(h, m)
197
+ end = fmt_hhmm(end_minutes // 60, end_minutes % 60)
198
+ sched.append((start, end))
199
+ return sched
200
+
201
+
202
+ def weekly_plan(diet: str, sched: List[Tuple[str, str]], kcal: int, protein_g: int) -> pd.DataFrame:
203
+ ideas = MEAL_IDEAS[diet]
204
+ rows = []
205
+ for i in range(7):
206
+ day = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"][i]
207
+ start, end = sched[i]
208
+ meal1 = ideas[i % len(ideas)]
209
+ meal2 = ideas[(i+3) % len(ideas)]
210
+ snack = "Fruit or nuts (optional)"
211
+ rows.append({
212
+ "Day": day,
213
+ "Feeding window": f"{start}–{end}",
214
+ "Meal 1": meal1,
215
+ "Meal 2": meal2,
216
+ "Protein target": f"≥ {protein_g} g",
217
+ "Daily kcal": kcal,
218
+ "Snack": snack,
219
+ })
220
+ return pd.DataFrame(rows)
221
+
222
+
223
+ def shopping_list(diet: str) -> List[str]:
224
+ core = [
225
+ "Leafy greens, mixed veg, berries",
226
+ "Olive oil, nuts/seeds, herbs & spices",
227
+ "Coffee/tea, mineral water, electrolytes",
228
+ ]
229
+ extras = {
230
+ "Omnivore": ["Chicken, fish, eggs, yogurt, cottage cheese", "Rice/quinoa/sourdough", "Beans/lentils"],
231
+ "Mediterranean": ["Fish, feta, olives", "Whole grains (bulgur, farro)", "Chickpeas/lentils"],
232
+ "Vegetarian": ["Eggs, dairy, paneer", "Legumes", "Tofu/tempeh"],
233
+ "Vegan": ["Tofu/tempeh/seitan", "Beans/lentils", "Plant yogurt/milk"],
234
+ "Low-carb": ["Eggs, fish, meat", "Green veg", "Greek yogurt, cheese"],
235
+ }
236
+ return core + extras[diet]
237
+
238
+ # ---------------------
239
+ # Tracker (history)
240
+ # ---------------------
241
+ active_fasts: Dict[str, pd.Timestamp] = {}
242
+
243
+ def _csv(u: str) -> Path:
244
+ safe = "".join(ch for ch in (u or "default") if ch.isalnum() or ch in ("_","-"))
245
+ return DATA_DIR / f"{safe}.csv"
246
+
247
+ def hist_load(u: str) -> pd.DataFrame:
248
+ p = _csv(u)
249
+ if p.exists():
250
+ d = pd.read_csv(p)
251
+ for c in ["start_time","end_time"]:
252
+ if c in d: d[c] = pd.to_datetime(d[c], errors="coerce")
253
+ return d
254
+ return pd.DataFrame(columns=["start_time","end_time","duration_hours","note"])
255
+
256
+ def hist_save(u: str, d: pd.DataFrame):
257
+ d.to_csv(_csv(u), index=False)
258
+
259
+ # ---------------------
260
+ # Core actions
261
+ # ---------------------
262
+
263
+ def predict_and_plan(fasting_duration, meal_timing, weight, age, gender, height,
264
+ activity, goal, diet, lang, ai_polish) -> Tuple[Optional[float], str, str, pd.DataFrame, object, str]:
265
  try:
266
+ # Validation
267
+ if fasting_duration < 0 or fasting_duration > 72:
268
+ raise ValueError("Fasting duration must be 0–72h.")
269
+ parse_hhmm(meal_timing)
270
+ if weight <= 0 or height <= 0 or age < 0:
271
+ raise ValueError("Check weight/height/age values.")
272
+
273
+ # Model score
274
+ df = pd.DataFrame({
275
+ "Fasting Duration (hours)": [float(fasting_duration)],
276
+ "Meal Timing (hour:minute)": [lambda t=meal_timing: int(t.split(":")[0]) + int(t.split(":")[1]) / 60.0][0](),
277
+ "Body Weight (kg)": [float(weight)],
278
+ "Age (years)": [float(age)],
279
+ "Height (cm)": [float(height)],
280
+ "Gender_Male": [1 if gender == "Male" else 0],
281
+ "Gender_Other": [1 if gender == "Other" else 0],
282
+ })
283
  score = float(decision_tree_regressor.predict(df)[0])
284
+
285
+ # Metrics
286
+ bmr = bmr_mifflin(gender, weight, height, age)
287
+ tdee_kcal = tdee(bmr, activity)
288
+ adj = GOAL_CAL_ADJ[goal]
289
+ target_kcal = int(round(tdee_kcal * (1 + adj)))
290
+ protein_g = int(round(max(1.6 * weight, 90 if goal == "Muscle gain" else 80)))
291
+ bmi_val = round(bmi(weight, height), 1)
292
+
293
+ # Schedule & tables
294
+ sched = feeding_schedule(meal_timing, float(fasting_duration))
295
+ plan_df = weekly_plan(diet, sched, target_kcal, protein_g)
296
+
297
+ # Chart (Gantt-style feeding window)
298
+ chart_df = pd.DataFrame({
299
+ "Day": ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],
300
+ "start": [int(s.split(":")[0])*60 + int(s.split(":")[1]) for s,_ in sched],
301
+ "length": [max(0, int((24 - float(fasting_duration))*60))]*7,
302
+ })
303
+ fig = px.bar(chart_df, y="Day", x="length", base="start", orientation="h", title="Feeding window each day (minutes)")
304
+ fig.update_layout(xaxis=dict(range=[0,1440], tickvals=[0,360,720,1080,1440], ticktext=["00:00","06:00","12:00","18:00","24:00"]))
305
+
306
+ # Markdown plan
307
+ hdr = {
308
+ "en": "## Your 7‑day intermittent fasting plan",
309
+ "es": "## Tu plan de ayuno intermitente de 7 días",
310
+ }[lang]
311
+ kpis = (
312
+ f"**Score:** {score:.1f} • **BMI:** {bmi_val} • **BMR:** {int(bmr)} kcal • **TDEE:** {int(tdee_kcal)} kcal • "
313
+ f"**Target:** {target_kcal} kcal • **Protein:** ≥ {protein_g} g • **Diet:** {diet}\n"
314
+ )
315
+ sched_md = "\n".join([f"- **{d}**: {s} – {e}" for d,(s,e) in zip(["Mon","Tue","Wed","Thu","Fri","Sat","Sun"], sched)])
316
+ workouts = "\n".join([f"- {w}" for w in WORKOUTS[goal]])
317
+ shop = "\n".join([f"- {x}" for x in shopping_list(diet)])
318
+
319
+ plan_md = f"""
320
+ {hdr}
321
+
322
+ {kpis}
323
+
324
+ ### Feeding window (daily)
325
+ {sched_md}
326
+
327
+ ### Weekly training
328
+ {workouts}
329
+
330
+ ### Daily meals (example week)
331
+ (See table below for details.)
332
+
333
+ ### Shopping list
334
+ {shop}
335
+
336
+ > Hydration & electrolytes during the fast, protein at each meal, whole foods, and 7–9 hours sleep.
337
+ """.strip()
338
+
339
+ # Optional AI polish (ZeroGPU window)
340
+ if ai_polish:
341
+ try:
342
+ plan_md = polish_on_gpu(plan_md, lang)
343
+ except Exception:
344
+ pass
345
+
346
+ # Export file path (Markdown)
347
+ md_path = DATA_DIR / "plan.md"
348
+ md_path.write_text(plan_md, encoding="utf-8")
349
+
350
+ return score, kpis, plan_md, plan_df, fig, str(md_path)
351
  except Exception as e:
352
+ return None, "", f"⚠️ {e}", pd.DataFrame(), None, ""
353
+
354
+ # ---------------------
355
+ # Tracker actions
356
+ # ---------------------
357
+
358
+ def start_fast(user: str, note: str):
359
+ if not user: return "Enter username in Settings.", None
360
+ if user in active_fasts: return f"Already fasting since {active_fasts[user]}.", None
361
+ active_fasts[user] = pd.Timestamp.now()
362
+ return f"✅ Fast started at {active_fasts[user].strftime(TS_FMT)}.", None
363
+
364
 
365
+ def end_fast(user: str):
366
+ if not user: return "Enter username in Settings.", None, None, None
367
+ if user not in active_fasts: return "No active fast.", None, None, None
368
+ end = pd.Timestamp.now(); start = active_fasts.pop(user)
369
+ dur = round((end - start).total_seconds()/3600, 2)
370
+ df = hist_load(user)
371
+ df.loc[len(df)] = [start, end, dur, ""]
372
+ hist_save(user, df)
373
+ chart = make_hist_chart(df)
374
+ return f"✅ Fast ended at {end.strftime(TS_FMT)} • {dur} h", df.tail(12), chart, hist_stats(df)
375
+
376
+
377
+ def refresh_hist(user: str):
378
+ df = hist_load(user)
379
+ return df.tail(12), make_hist_chart(df), hist_stats(df)
380
+
381
+
382
+ def make_hist_chart(df: pd.DataFrame):
383
+ if df.empty: return None
384
+ d = df.dropna(subset=["end_time"]).copy()
385
+ d["date"] = pd.to_datetime(d["end_time"]).dt.date
386
+ fig = px.bar(d, x="date", y="duration_hours", title="Fasting duration by day (h)")
387
+ fig.update_layout(height=300, margin=dict(l=10,r=10,t=40,b=10))
388
+ return fig
389
+
390
+
391
+ def hist_stats(df: pd.DataFrame) -> str:
392
+ if df.empty: return "No history yet."
393
+ last7 = df.tail(7)
394
+ avg = last7["duration_hours"].mean()
395
+ streak = compute_streak(df)
396
+ return f"Total fasts: {len(df)}\nAvg (last 7): {avg:.2f} h\nCurrent streak: {streak} day(s)"
397
+
398
+
399
+ def compute_streak(df: pd.DataFrame) -> int:
400
+ d = df.dropna(subset=["end_time"]).copy()
401
+ if d.empty: return 0
402
+ days = set(pd.to_datetime(d["end_time"]).dt.date)
403
+ cur = pd.Timestamp.now().date(); streak=0
404
+ while cur in days:
405
+ streak+=1; cur = cur - pd.Timedelta(days=1)
406
+ return streak
407
+
408
+ # ---------------------
409
  # UI
410
+ # ---------------------
411
+ with gr.Blocks(
412
+ title="Intermittent Fasting Coach Pro",
413
+ theme=gr.themes.Soft(primary_hue=gr.themes.colors.orange, neutral_hue=gr.themes.colors.gray),
414
+ ) as demo:
415
+ gr.Markdown("""
416
+ # 🥣 Intermittent Fasting Pro
417
+ Detailed coaching plans + tracker. ZeroGPU‑ready (with CPU fallback). All data stored locally in this Space.
418
+ """)
419
+
420
+ with gr.Tabs():
421
+ # --- Coach tab
422
+ with gr.TabItem("Coach"):
423
+ with gr.Row():
424
+ with gr.Column():
425
+ fasting_duration = gr.Number(label="Fasting Duration (h)", value=16, minimum=0, maximum=72, step=0.5)
426
+ meal_timing = gr.Textbox(label="First meal time (HH:MM)", value="12:30")
427
+ weight = gr.Number(label="Body Weight (kg)", value=70, step=0.5)
428
+ with gr.Column():
429
+ age = gr.Slider(label="Age (years)", minimum=18, maximum=100, value=35)
430
+ gender = gr.Radio(["Male","Female","Other"], label="Gender", value="Male")
431
+ height = gr.Number(label="Height (cm)", value=175)
432
+ with gr.Row():
433
+ activity = gr.Dropdown(choices=list(ACTIVITY.keys()), value="Lightly active", label="Activity")
434
+ goal = gr.Dropdown(choices=list(GOAL_CAL_ADJ.keys()), value="Recomp/Maintenance", label="Goal")
435
+ diet = gr.Dropdown(choices=DIET_STYLES, value="Mediterranean", label="Diet style")
436
+ lang = gr.Radio(["en","es"], value="en", label="Language")
437
+ ai_polish = gr.Checkbox(value=True, label="AI polish (uses ZeroGPU)")
438
+
439
+ btn = gr.Button("Predict & Build Plan", variant="primary")
440
+
441
+ score_out = gr.Number(label="Predicted score")
442
+ kpi_out = gr.Markdown()
443
+ plan_md = gr.Markdown()
444
+ plan_tbl = gr.Dataframe(headers=["Day","Feeding window","Meal 1","Meal 2","Protein target","Daily kcal","Snack"], interactive=False)
445
+ fig = gr.Plot()
446
+ dl = gr.DownloadButton(label="Download plan (.md)")
447
+
448
+ btn.click(
449
+ predict_and_plan,
450
+ inputs=[fasting_duration, meal_timing, weight, age, gender, height, activity, goal, diet, lang, ai_polish],
451
+ outputs=[score_out, kpi_out, plan_md, plan_tbl, fig, dl],
452
+ api_name="coach_plan"
453
+ )
454
+
455
+ # --- Tracker tab
456
+ with gr.TabItem("Tracker"):
457
+ with gr.Row():
458
+ user = gr.Textbox(label="Username", value="")
459
+ note = gr.Textbox(label="Note (optional)")
460
+ with gr.Row():
461
+ b1 = gr.Button("Start fast", variant="primary")
462
+ b2 = gr.Button("End fast")
463
+ b3 = gr.Button("Reload history")
464
+ status = gr.Markdown("Not fasting.")
465
+ hist = gr.Dataframe(interactive=False)
466
+ hist_fig = gr.Plot()
467
+ stats = gr.Markdown()
468
+
469
+ b1.click(start_fast, inputs=[user, note], outputs=[status, note])
470
+ b2.click(end_fast, inputs=[user], outputs=[status, hist, hist_fig, stats])
471
+ b3.click(refresh_hist, inputs=[user], outputs=[hist, hist_fig, stats])
472
+ demo.load(refresh_hist, inputs=[user], outputs=[hist, hist_fig, stats])
473
+
474
+ # --- About tab
475
+ with gr.TabItem("About"):
476
+ gr.Markdown("""
477
+ **How it works**
478
+ • Your predictor estimates a health score from inputs.
479
+ • The coach builds a 7‑day schedule matching your fasting window, goal, activity and diet style.
480
+ • Optional AI polish refines wording using a tiny model (ZeroGPU window).
481
+ • Tracker stores CSVs under `/data/` and never sends data elsewhere.
482
+ """)
483
 
484
  if __name__ == "__main__":
485
+ demo.queue().launch()