jesusvilela commited on
Commit
d7a09e5
·
verified ·
1 Parent(s): 880bf7d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +296 -113
app.py CHANGED
@@ -1,6 +1,7 @@
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
@@ -17,7 +18,8 @@ from huggingface_hub import InferenceClient
17
  # ------------------------
18
  # Config & storage
19
  # ------------------------
20
- DATA_DIR = Path("data"); DATA_DIR.mkdir(exist_ok=True)
 
21
  TS_FMT = "%Y-%m-%d %H:%M:%S"
22
 
23
  DT_PATH = "./decision_tree_regressor.joblib"
@@ -29,23 +31,62 @@ _tokenizer = AutoTokenizer.from_pretrained(GEN_MODEL)
29
  _model = AutoModelForSeq2SeqLM.from_pretrained(GEN_MODEL)
30
  _generate_cpu = pipeline("text2text-generation", model=_model, tokenizer=_tokenizer, device=-1)
31
 
32
- # HF Inference API SOTA models
33
  SOTA_MODELS = [
34
- "Qwen/Qwen2.5-72B-Instruct", # default: high-quality open model available on HF
35
  "meta-llama/Meta-Llama-3.1-70B-Instruct",
36
  "mistralai/Mistral-Nemo-Instruct-2407",
37
  "Qwen/Qwen2.5-32B-Instruct",
38
- "Qwen/Qwen2.5-7B-Instruct"
 
39
  ]
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  def _hf_client(model_id: str) -> InferenceClient:
42
- token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
43
- return InferenceClient(model=model_id, token=token, timeout=120)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  def generate_with_hf_inference(prompt: str, model_id: str, max_new_tokens: int = 900) -> str:
46
  """
47
- Serverless generation via Hugging Face Inference API.
48
  Works on CPU-only Spaces and with ZeroGPU.
 
 
49
  """
50
  try:
51
  client = _hf_client(model_id)
@@ -58,11 +99,12 @@ def generate_with_hf_inference(prompt: str, model_id: str, max_new_tokens: int =
58
  stop=["</s>"],
59
  return_full_text=False,
60
  )
61
- return text.strip()
62
  except Exception as e:
63
  # Fall back to local tiny model inside a GPU window if available
64
  return f"(HF Inference error: {e})\n" + generate_on_gpu(prompt, max_new_tokens=min(max_new_tokens, 600))
65
 
 
66
  # ------------------------
67
  # ZeroGPU functions (presence at import satisfies ZeroGPU)
68
  # ------------------------
@@ -74,7 +116,12 @@ def generate_on_gpu(prompt: str, max_new_tokens: int = 600) -> str:
74
  """
75
  try:
76
  if torch.cuda.is_available():
77
- gen = pipeline("text2text-generation", model=_model.to("cuda"), tokenizer=_tokenizer, device=0)
 
 
 
 
 
78
  out = gen(prompt, max_new_tokens=max_new_tokens)
79
  else:
80
  out = _generate_cpu(prompt, max_new_tokens=max_new_tokens)
@@ -83,26 +130,45 @@ def generate_on_gpu(prompt: str, max_new_tokens: int = 600) -> str:
83
  out = _generate_cpu(prompt, max_new_tokens=max_new_tokens)
84
  return out[0]["generated_text"].strip() + f"\n\n(Note: GPU path failed: {e})"
85
 
 
86
  # ------------------------
87
  # Metrics & helpers
88
  # ------------------------
89
- ACTIVITY = {"Sedentary":1.2,"Lightly active":1.375,"Moderately active":1.55,"Very active":1.725,"Athlete":1.9}
90
- GOAL_CAL_ADJ = {"Fat loss":-0.15,"Recomp/Maintenance":0.0,"Muscle gain":0.10}
 
 
 
 
 
 
 
 
 
 
 
91
 
92
- def bmi(w,h): return w/((h/100)**2)
93
- def bmr_mifflin(sex,w,h,a): return 10*w+6.25*h-5*a+(5 if sex=="Male" else -161)
94
- def tdee(bmr,act): return bmr*ACTIVITY.get(act,1.2)
95
 
96
- def parse_hhmm(hhmm: str) -> Tuple[int,int]:
 
 
 
 
 
97
  h, m = hhmm.split(":")
98
- h = int(h); m = int(m)
 
99
  if not (0 <= h <= 23 and 0 <= m <= 59):
100
  raise ValueError("Time must be HH:MM (24h).")
101
  return h, m
102
 
 
103
  def fmt_hhmm(h: int, m: int) -> str:
104
  return f"{h:02d}:{m:02d}"
105
 
 
106
  # Meal ideas, workouts, etc.
107
  DIET_STYLES = ["Mediterranean", "Omnivore", "Vegetarian", "Vegan", "Low-carb"]
108
  MEAL_IDEAS = {
@@ -113,7 +179,7 @@ MEAL_IDEAS = {
113
  "Chickpea tomato stew",
114
  "Feta & olive salad, quinoa",
115
  "Shakshuka + side salad",
116
- "Lentils, roasted veg, tahini"
117
  ],
118
  "Omnivore": [
119
  "Yogurt + berries + nuts",
@@ -122,7 +188,7 @@ MEAL_IDEAS = {
122
  "Salmon, quinoa, asparagus",
123
  "Lean beef, sweet potato, salad",
124
  "Tuna whole-grain wrap",
125
- "Cottage cheese + fruit + seeds"
126
  ],
127
  "Vegetarian": [
128
  "Tofu scramble, toast, avocado",
@@ -131,7 +197,7 @@ MEAL_IDEAS = {
131
  "Halloumi, couscous, veg",
132
  "Greek salad + eggs",
133
  "Tempeh stir-fry",
134
- "Yogurt parfait + granola"
135
  ],
136
  "Vegan": [
137
  "Tofu scramble, avocado toast",
@@ -140,7 +206,7 @@ MEAL_IDEAS = {
140
  "Seitan, roasted potatoes, veg",
141
  "Tofu poke bowl",
142
  "Chickpea pasta + marinara",
143
- "Overnight oats + banana + PB"
144
  ],
145
  "Low-carb": [
146
  "Eggs, smoked salmon, salad",
@@ -149,30 +215,31 @@ MEAL_IDEAS = {
149
  "Omelette + veg + cheese",
150
  "Zoodles + turkey bolognese",
151
  "Tofu salad w/ tahini",
152
- "Yogurt + nuts (moderate)"
153
- ]
154
  }
155
  WORKOUTS = {
156
  "Fat loss": [
157
  "3× LISS cardio 30–40min",
158
  "2× full-body strength 45min",
159
  "1× intervals 12–16min",
160
- "Daily 8–10k steps"
161
  ],
162
  "Recomp/Maintenance": [
163
  "3× full-body strength 45–60min",
164
  "1–2× LISS cardio 30min",
165
  "Mobility 10min daily",
166
- "8–10k steps"
167
  ],
168
  "Muscle gain": [
169
  "4× strength split 45–60min",
170
  "Optional 1× LISS 20–30min",
171
  "Mobility 10min",
172
- "7–9k steps"
173
- ]
174
  }
175
 
 
176
  def feeding_schedule(first_meal_hhmm: str, fasting_hours: float) -> List[Tuple[str, str]]:
177
  h, m = parse_hhmm(first_meal_hhmm)
178
  window = max(0.0, 24 - float(fasting_hours))
@@ -185,26 +252,30 @@ def feeding_schedule(first_meal_hhmm: str, fasting_hours: float) -> List[Tuple[s
185
  sched.append((start, end))
186
  return sched
187
 
 
188
  def weekly_plan(diet: str, sched: List[Tuple[str, str]], kcal: int, protein_g: int) -> pd.DataFrame:
189
  ideas = MEAL_IDEAS[diet]
190
  rows = []
191
  for i in range(7):
192
- day = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"][i]
193
  start, end = sched[i]
194
  meal1 = ideas[i % len(ideas)]
195
- meal2 = ideas[(i+3) % len(ideas)]
196
  snack = "Fruit or nuts (optional)"
197
- rows.append({
198
- "Day": day,
199
- "Feeding window": f"{start}–{end}",
200
- "Meal 1": meal1,
201
- "Meal 2": meal2,
202
- "Protein target": f"≥ {protein_g} g",
203
- "Daily kcal": kcal,
204
- "Snack": snack,
205
- })
 
 
206
  return pd.DataFrame(rows)
207
 
 
208
  def shopping_list(diet: str) -> List[str]:
209
  core = [
210
  "Leafy greens, mixed veg, berries",
@@ -212,36 +283,59 @@ def shopping_list(diet: str) -> List[str]:
212
  "Coffee/tea, mineral water, electrolytes",
213
  ]
214
  extras = {
215
- "Omnivore": ["Chicken, fish, eggs, yogurt, cottage cheese", "Rice/quinoa/sourdough", "Beans/lentils"],
216
- "Mediterranean": ["Fish, feta, olives", "Whole grains (bulgur, farro)", "Chickpeas/lentils"],
 
 
 
 
 
 
 
 
217
  "Vegetarian": ["Eggs, dairy, paneer", "Legumes", "Tofu/tempeh"],
218
  "Vegan": ["Tofu/tempeh/seitan", "Beans/lentils", "Plant yogurt/milk"],
219
  "Low-carb": ["Eggs, fish, meat", "Green veg", "Greek yogurt, cheese"],
220
  }
221
  return core + extras[diet]
222
 
 
223
  # ------------------------
224
  # Plan builder (with SOTA + local fallback)
225
  # ------------------------
226
  def predict_and_plan(
227
- fasting_duration, meal_timing, weight, age, gender, height,
228
- activity, goal, diet, lang, use_sota_model, sota_model_id
 
 
 
 
 
 
 
 
 
 
229
  ) -> Tuple[Optional[float], str, str, pd.DataFrame, object, str]:
230
  try:
231
- if fasting_duration < 0 or fasting_duration > 72: raise ValueError("Fasting must be 0–72h.")
 
232
  h, m = parse_hhmm(meal_timing)
233
- if weight <= 0 or height <= 0 or age < 0: raise ValueError("Invalid weight/height/age.")
 
234
 
235
  # Predict score
236
- df = pd.DataFrame({
237
- "Fasting Duration (hours)": [float(fasting_duration)],
238
- "Meal Timing (hour:minute)": [h + m/60],
239
- "Body Weight (kg)": [float(weight)],
240
- "Age (years)": [float(age)],
241
- "Height (cm)": [float(height)],
242
- "Gender_Male": [1 if gender == "Male" else 0],
243
- "Gender_Other": [1 if gender == "Other" else 0],
244
- })
 
 
245
  score = float(decision_tree_regressor.predict(df)[0])
246
 
247
  # Metrics
@@ -255,65 +349,90 @@ def predict_and_plan(
255
  sched = feeding_schedule(meal_timing, float(fasting_duration))
256
  plan_df = weekly_plan(diet, sched, target_kcal, protein_g)
257
 
258
- chart_df = pd.DataFrame({
259
- "Day": ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],
260
- "start": [int(s.split(":")[0])*60 + int(s.split(":")[1]) for s,_ in sched],
261
- "length": [max(0, int((24 - float(fasting_duration))*60))]*7,
262
- })
263
- fig = px.bar(chart_df, y="Day", x="length", base="start", orientation="h",
264
- title="Feeding window each day (minutes)")
 
 
 
 
 
 
 
 
265
  fig.update_layout(
266
- xaxis=dict(range=[0,1440], tickvals=[0,360,720,1080,1440],
267
- ticktext=["00:00","06:00","12:00","18:00","24:00"]),
268
- height=300, margin=dict(l=10,r=10,t=40,b=10)
 
 
 
 
269
  )
270
 
271
- # Base markdown (deterministic, structured). We’ll optionally enhance with SOTA.
272
  kpis = (
273
  f"**Score:** {score:.1f} • **BMI:** {bmi_val} • **BMR:** {int(bmr)} kcal • "
274
  f"**TDEE:** {int(tdee_kcal)} kcal • **Target:** {target_kcal} kcal • **Protein:** ≥ {protein_g} g • "
275
  f"**Diet:** {diet}"
276
  )
277
- sched_md = "\n".join([f"- **{d}**: {s} – {e}" for d,(s,e) in zip(["Mon","Tue","Wed","Thu","Fri","Sat","Sun"], sched)])
 
 
 
 
 
278
  workouts_md = "\n".join([f"- {w}" for w in WORKOUTS[goal]])
279
  shop_md = "\n".join([f"- {x}" for x in shopping_list(diet)])
280
 
281
  base_plan_md = f"""
282
  ## Your 7-day intermittent fasting plan
283
-
284
  {kpis}
285
-
286
  ### Feeding window (daily)
287
  {sched_md}
288
-
289
  ### Weekly training
290
  {workouts_md}
291
-
292
  ### Daily meals (example week)
293
  (See the table below.)
294
-
295
  ### Shopping list
296
  {shop_md}
297
-
298
  > Hydration & electrolytes during the fast, protein at each meal, whole foods, and 7–9 hours sleep.
299
  """.strip()
300
 
301
  # Enhance/format with chosen generator
302
  if use_sota_model:
303
- plan_md = generate_with_hf_inference(
304
- prompt=(
305
- "You are an expert health coach. Refine the following intermittent fasting plan. "
306
- "Keep markdown headings and bullets; be concise and specific; keep the meaning. "
307
- f"Language: '{lang}'.\n\n{base_plan_md}"
308
- ),
309
- model_id=sota_model_id,
310
- max_new_tokens=900,
311
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  else:
313
- # Use local tiny model inside ZeroGPU window (or CPU fallback)
314
  plan_md = generate_on_gpu(
315
- "Rewrite in a friendly coaching tone; keep markdown structure; do not remove tables or metrics.\n\n" + base_plan_md,
316
- max_new_tokens=700
 
317
  )
318
 
319
  # Save for download
@@ -325,102 +444,141 @@ def predict_and_plan(
325
  except Exception as e:
326
  return None, "", f"⚠️ {e}", pd.DataFrame(), None, ""
327
 
 
328
  # ------------------------
329
  # Tracker logic
330
  # ------------------------
331
  active_fasts: Dict[str, pd.Timestamp] = {}
332
 
 
333
  def _csv(user: str) -> Path:
334
- safe = "".join(ch for ch in (user or "default") if ch.isalnum() or ch in ("_","-"))
335
  return DATA_DIR / f"{safe}.csv"
336
 
 
337
  def hist_load(user: str) -> pd.DataFrame:
338
  p = _csv(user)
339
  if p.exists():
340
  d = pd.read_csv(p)
341
- for c in ["start_time","end_time"]:
342
- if c in d: d[c] = pd.to_datetime(d[c], errors="coerce")
 
343
  return d
344
- return pd.DataFrame(columns=["start_time","end_time","duration_hours","note"])
 
345
 
346
  def hist_save(user: str, d: pd.DataFrame):
347
  d.to_csv(_csv(user), index=False)
348
 
 
349
  def make_hist_chart(df: pd.DataFrame):
350
- if df.empty: return None
 
351
  d = df.dropna(subset=["end_time"]).copy()
352
- if d.empty: return None
 
353
  d["date"] = pd.to_datetime(d["end_time"]).dt.date
354
  fig = px.bar(d, x="date", y="duration_hours", title="Fasting duration by day (h)")
355
- fig.update_layout(height=300, margin=dict(l=10,r=10,t=40,b=10))
356
  return fig
357
 
 
358
  def compute_streak(df: pd.DataFrame) -> int:
359
  d = df.dropna(subset=["end_time"]).copy()
360
- if d.empty: return 0
 
361
  days = set(pd.to_datetime(d["end_time"]).dt.date)
362
- cur = pd.Timestamp.now().date(); streak = 0
 
363
  while cur in days:
364
- streak += 1; cur = cur - pd.Timedelta(days=1)
 
365
  return streak
366
 
 
367
  def hist_stats(df: pd.DataFrame) -> str:
368
- if df.empty: return "No history yet."
 
369
  last7 = df.tail(7)
370
  avg = last7["duration_hours"].mean()
371
  streak = compute_streak(df)
372
  return f"Total fasts: {len(df)}\nAvg (last 7): {avg:.2f} h\nCurrent streak: {streak} day(s)"
373
 
 
374
  def start_fast(user: str, note: str):
375
- if not user: return "Enter username in Tracker.", None
376
- if user in active_fasts: return f"Already fasting since {active_fasts[user].strftime(TS_FMT)}.", None
 
 
377
  active_fasts[user] = pd.Timestamp.now()
378
  return f"✅ Fast started at {active_fasts[user].strftime(TS_FMT)}.", None
379
 
 
380
  def end_fast(user: str):
381
- if not user: return "Enter username.", None, None, None
382
- if user not in active_fasts: return "No active fast.", None, None, None
383
- end = pd.Timestamp.now(); start = active_fasts.pop(user)
384
- dur = round((end - start).total_seconds()/3600, 2)
 
 
 
385
  df = hist_load(user)
386
  df.loc[len(df)] = [start, end, dur, ""]
387
  hist_save(user, df)
388
  return f"✅ Fast ended at {end.strftime(TS_FMT)} • {dur} h", df.tail(12), make_hist_chart(df), hist_stats(df)
389
 
 
390
  def refresh_hist(user: str):
391
  df = hist_load(user)
392
  return df.tail(12), make_hist_chart(df), hist_stats(df)
393
 
 
394
  # ------------------------
395
  # UI
396
  # ------------------------
 
 
 
 
 
 
 
397
  with gr.Blocks(
398
  title="Intermittent Fasting Coach — Pro (SOTA)",
399
- theme=gr.themes.Soft(primary_hue=gr.themes.colors.orange, neutral_hue=gr.themes.colors.gray),
 
 
 
400
  ) as demo:
401
- gr.Markdown("""
 
402
  # 🥣 Intermittent Fasting — Pro (SOTA)
403
  Detailed coaching plans + tracker. ZeroGPU-ready (with CPU fallback). Data stored locally in this Space.
404
- """)
 
405
 
406
  with gr.Tabs():
407
  # --- Coach tab
408
  with gr.TabItem("Coach"):
 
 
409
  with gr.Row():
410
  with gr.Column():
411
- fasting_duration = gr.Number(label="Fasting duration (h)", value=16, minimum=0, maximum=72, step=0.5)
 
 
412
  meal_timing = gr.Textbox(label="First meal time (HH:MM)", value="12:30")
413
  weight = gr.Number(label="Body weight (kg)", value=70, step=0.5)
414
  with gr.Column():
415
  age = gr.Slider(label="Age (years)", minimum=18, maximum=100, value=35)
416
- gender = gr.Radio(["Male","Female","Other"], label="Gender", value="Male")
417
  height = gr.Number(label="Height (cm)", value=175)
418
 
419
  with gr.Row():
420
  activity = gr.Dropdown(choices=list(ACTIVITY.keys()), value="Lightly active", label="Activity")
421
  goal = gr.Dropdown(choices=list(GOAL_CAL_ADJ.keys()), value="Recomp/Maintenance", label="Goal")
422
  diet = gr.Dropdown(choices=DIET_STYLES, value="Mediterranean", label="Diet style")
423
- lang = gr.Radio(["en","es"], value="en", label="Language")
424
  use_sota_model = gr.Checkbox(value=True, label="Use SOTA model (HF Inference)")
425
  sota_model_id = gr.Dropdown(choices=SOTA_MODELS, value=SOTA_MODELS[0], label="HF model")
426
 
@@ -429,15 +587,31 @@ Detailed coaching plans + tracker. ZeroGPU-ready (with CPU fallback). Data store
429
  score_out = gr.Number(label="Predicted score")
430
  kpi_out = gr.Markdown()
431
  plan_md = gr.Markdown()
432
- plan_tbl = gr.Dataframe(headers=["Day","Feeding window","Meal 1","Meal 2","Protein target","Daily kcal","Snack"], interactive=False)
 
 
 
433
  fig = gr.Plot()
434
  dl = gr.DownloadButton(label="Download plan (.md)")
435
 
436
  btn.click(
437
  predict_and_plan,
438
- inputs=[fasting_duration, meal_timing, weight, age, gender, height, activity, goal, diet, lang, use_sota_model, sota_model_id],
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  outputs=[score_out, kpi_out, plan_md, plan_tbl, fig, dl],
440
- api_name="coach_plan"
441
  )
442
 
443
  # --- Tracker tab
@@ -449,25 +623,34 @@ Detailed coaching plans + tracker. ZeroGPU-ready (with CPU fallback). Data store
449
  b1 = gr.Button("Start fast", variant="primary")
450
  b2 = gr.Button("End fast")
451
  b3 = gr.Button("Reload history")
 
452
  status = gr.Markdown("Not fasting.")
453
  hist = gr.Dataframe(interactive=False)
454
  hist_fig = gr.Plot()
455
  stats = gr.Markdown()
456
 
457
  b1.click(start_fast, inputs=[user, note], outputs=[status, note])
458
- b2.click(end_fast, inputs=[user], outputs=[status, hist, hist_fig, stats]) # <-- FIXED: no None
459
  b3.click(refresh_hist, inputs=[user], outputs=[hist, hist_fig, stats])
460
  demo.load(refresh_hist, inputs=[user], outputs=[hist, hist_fig, stats])
461
 
462
  # --- About tab
463
  with gr.TabItem("About"):
464
- gr.Markdown("""
 
465
  **How it works**
466
  • The predictor estimates a health score from inputs.
467
  • The coach builds a 7-day schedule matching your fasting window, goal, activity and diet style.
468
- • SOTA option uses Hugging Face Inference API; fallback uses a tiny local model in the ZeroGPU window.
469
  • Tracker stores CSVs under `/data/` and never sends data elsewhere.
470
- """)
 
 
 
 
 
 
 
471
 
472
  if __name__ == "__main__":
473
- demo.queue().launch()
 
1
  import os
2
  from pathlib import Path
3
  from typing import Optional, Tuple, List, Dict
4
+ from functools import lru_cache
5
 
6
  import gradio as gr
7
  import pandas as pd
 
18
  # ------------------------
19
  # Config & storage
20
  # ------------------------
21
+ DATA_DIR = Path("data")
22
+ DATA_DIR.mkdir(exist_ok=True)
23
  TS_FMT = "%Y-%m-%d %H:%M:%S"
24
 
25
  DT_PATH = "./decision_tree_regressor.joblib"
 
31
  _model = AutoModelForSeq2SeqLM.from_pretrained(GEN_MODEL)
32
  _generate_cpu = pipeline("text2text-generation", model=_model, tokenizer=_tokenizer, device=-1)
33
 
34
+ # HF Inference / Inference Providers models
35
  SOTA_MODELS = [
36
+ "Qwen/Qwen2.5-72B-Instruct",
37
  "meta-llama/Meta-Llama-3.1-70B-Instruct",
38
  "mistralai/Mistral-Nemo-Instruct-2407",
39
  "Qwen/Qwen2.5-32B-Instruct",
40
+ "Qwen/Qwen2.5-7B-Instruct",
41
+ "jesusvilela/manifoldgl", # <-- added
42
  ]
43
 
44
+ # ------------------------
45
+ # HF token handling (Space Secrets)
46
+ # ------------------------
47
+ def get_hf_api_key() -> Optional[str]:
48
+ """
49
+ Grab Hugging Face token from env vars (Spaces Secrets).
50
+
51
+ Priority:
52
+ 1) HF_API_KEY (requested)
53
+ 2) HF_TOKEN
54
+ 3) common Hub token env vars
55
+ """
56
+ return (
57
+ os.getenv("HF_API_KEY")
58
+ or os.getenv("HF_TOKEN")
59
+ or os.getenv("HUGGINGFACEHUB_API_TOKEN")
60
+ or os.getenv("HUGGING_FACE_HUB_TOKEN")
61
+ )
62
+
63
+
64
+ @lru_cache(maxsize=32)
65
  def _hf_client(model_id: str) -> InferenceClient:
66
+ """
67
+ Cached client per model. Compatible with huggingface_hub versions where auth kwarg
68
+ may be `api_key` (newer) or `token` (older).
69
+ """
70
+ api_key = get_hf_api_key()
71
+ if not api_key:
72
+ raise RuntimeError(
73
+ "Missing HF_API_KEY. Add a Space secret named HF_API_KEY (or HF_TOKEN) to enable HF inference."
74
+ )
75
+
76
+ try:
77
+ # Newer huggingface_hub
78
+ return InferenceClient(model=model_id, api_key=api_key, timeout=120)
79
+ except TypeError:
80
+ # Older huggingface_hub
81
+ return InferenceClient(model=model_id, token=api_key, timeout=120)
82
+
83
 
84
  def generate_with_hf_inference(prompt: str, model_id: str, max_new_tokens: int = 900) -> str:
85
  """
86
+ Generation via Hugging Face Inference (and/or Inference Providers).
87
  Works on CPU-only Spaces and with ZeroGPU.
88
+
89
+ Requires a token in Space Secrets (HF_API_KEY recommended).
90
  """
91
  try:
92
  client = _hf_client(model_id)
 
99
  stop=["</s>"],
100
  return_full_text=False,
101
  )
102
+ return (text or "").strip()
103
  except Exception as e:
104
  # Fall back to local tiny model inside a GPU window if available
105
  return f"(HF Inference error: {e})\n" + generate_on_gpu(prompt, max_new_tokens=min(max_new_tokens, 600))
106
 
107
+
108
  # ------------------------
109
  # ZeroGPU functions (presence at import satisfies ZeroGPU)
110
  # ------------------------
 
116
  """
117
  try:
118
  if torch.cuda.is_available():
119
+ gen = pipeline(
120
+ "text2text-generation",
121
+ model=_model.to("cuda"),
122
+ tokenizer=_tokenizer,
123
+ device=0,
124
+ )
125
  out = gen(prompt, max_new_tokens=max_new_tokens)
126
  else:
127
  out = _generate_cpu(prompt, max_new_tokens=max_new_tokens)
 
130
  out = _generate_cpu(prompt, max_new_tokens=max_new_tokens)
131
  return out[0]["generated_text"].strip() + f"\n\n(Note: GPU path failed: {e})"
132
 
133
+
134
  # ------------------------
135
  # Metrics & helpers
136
  # ------------------------
137
+ ACTIVITY = {
138
+ "Sedentary": 1.2,
139
+ "Lightly active": 1.375,
140
+ "Moderately active": 1.55,
141
+ "Very active": 1.725,
142
+ "Athlete": 1.9,
143
+ }
144
+ GOAL_CAL_ADJ = {"Fat loss": -0.15, "Recomp/Maintenance": 0.0, "Muscle gain": 0.10}
145
+
146
+
147
+ def bmi(w, h):
148
+ return w / ((h / 100) ** 2)
149
+
150
 
151
+ def bmr_mifflin(sex, w, h, a):
152
+ return 10 * w + 6.25 * h - 5 * a + (5 if sex == "Male" else -161)
 
153
 
154
+
155
+ def tdee(bmr, act):
156
+ return bmr * ACTIVITY.get(act, 1.2)
157
+
158
+
159
+ def parse_hhmm(hhmm: str) -> Tuple[int, int]:
160
  h, m = hhmm.split(":")
161
+ h = int(h)
162
+ m = int(m)
163
  if not (0 <= h <= 23 and 0 <= m <= 59):
164
  raise ValueError("Time must be HH:MM (24h).")
165
  return h, m
166
 
167
+
168
  def fmt_hhmm(h: int, m: int) -> str:
169
  return f"{h:02d}:{m:02d}"
170
 
171
+
172
  # Meal ideas, workouts, etc.
173
  DIET_STYLES = ["Mediterranean", "Omnivore", "Vegetarian", "Vegan", "Low-carb"]
174
  MEAL_IDEAS = {
 
179
  "Chickpea tomato stew",
180
  "Feta & olive salad, quinoa",
181
  "Shakshuka + side salad",
182
+ "Lentils, roasted veg, tahini",
183
  ],
184
  "Omnivore": [
185
  "Yogurt + berries + nuts",
 
188
  "Salmon, quinoa, asparagus",
189
  "Lean beef, sweet potato, salad",
190
  "Tuna whole-grain wrap",
191
+ "Cottage cheese + fruit + seeds",
192
  ],
193
  "Vegetarian": [
194
  "Tofu scramble, toast, avocado",
 
197
  "Halloumi, couscous, veg",
198
  "Greek salad + eggs",
199
  "Tempeh stir-fry",
200
+ "Yogurt parfait + granola",
201
  ],
202
  "Vegan": [
203
  "Tofu scramble, avocado toast",
 
206
  "Seitan, roasted potatoes, veg",
207
  "Tofu poke bowl",
208
  "Chickpea pasta + marinara",
209
+ "Overnight oats + banana + PB",
210
  ],
211
  "Low-carb": [
212
  "Eggs, smoked salmon, salad",
 
215
  "Omelette + veg + cheese",
216
  "Zoodles + turkey bolognese",
217
  "Tofu salad w/ tahini",
218
+ "Yogurt + nuts (moderate)",
219
+ ],
220
  }
221
  WORKOUTS = {
222
  "Fat loss": [
223
  "3× LISS cardio 30–40min",
224
  "2× full-body strength 45min",
225
  "1× intervals 12–16min",
226
+ "Daily 8–10k steps",
227
  ],
228
  "Recomp/Maintenance": [
229
  "3× full-body strength 45–60min",
230
  "1–2× LISS cardio 30min",
231
  "Mobility 10min daily",
232
+ "8–10k steps",
233
  ],
234
  "Muscle gain": [
235
  "4× strength split 45–60min",
236
  "Optional 1× LISS 20–30min",
237
  "Mobility 10min",
238
+ "7–9k steps",
239
+ ],
240
  }
241
 
242
+
243
  def feeding_schedule(first_meal_hhmm: str, fasting_hours: float) -> List[Tuple[str, str]]:
244
  h, m = parse_hhmm(first_meal_hhmm)
245
  window = max(0.0, 24 - float(fasting_hours))
 
252
  sched.append((start, end))
253
  return sched
254
 
255
+
256
  def weekly_plan(diet: str, sched: List[Tuple[str, str]], kcal: int, protein_g: int) -> pd.DataFrame:
257
  ideas = MEAL_IDEAS[diet]
258
  rows = []
259
  for i in range(7):
260
+ day = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][i]
261
  start, end = sched[i]
262
  meal1 = ideas[i % len(ideas)]
263
+ meal2 = ideas[(i + 3) % len(ideas)]
264
  snack = "Fruit or nuts (optional)"
265
+ rows.append(
266
+ {
267
+ "Day": day,
268
+ "Feeding window": f"{start}–{end}",
269
+ "Meal 1": meal1,
270
+ "Meal 2": meal2,
271
+ "Protein target": f"≥ {protein_g} g",
272
+ "Daily kcal": kcal,
273
+ "Snack": snack,
274
+ }
275
+ )
276
  return pd.DataFrame(rows)
277
 
278
+
279
  def shopping_list(diet: str) -> List[str]:
280
  core = [
281
  "Leafy greens, mixed veg, berries",
 
283
  "Coffee/tea, mineral water, electrolytes",
284
  ]
285
  extras = {
286
+ "Omnivore": [
287
+ "Chicken, fish, eggs, yogurt, cottage cheese",
288
+ "Rice/quinoa/sourdough",
289
+ "Beans/lentils",
290
+ ],
291
+ "Mediterranean": [
292
+ "Fish, feta, olives",
293
+ "Whole grains (bulgur, farro)",
294
+ "Chickpeas/lentils",
295
+ ],
296
  "Vegetarian": ["Eggs, dairy, paneer", "Legumes", "Tofu/tempeh"],
297
  "Vegan": ["Tofu/tempeh/seitan", "Beans/lentils", "Plant yogurt/milk"],
298
  "Low-carb": ["Eggs, fish, meat", "Green veg", "Greek yogurt, cheese"],
299
  }
300
  return core + extras[diet]
301
 
302
+
303
  # ------------------------
304
  # Plan builder (with SOTA + local fallback)
305
  # ------------------------
306
  def predict_and_plan(
307
+ fasting_duration,
308
+ meal_timing,
309
+ weight,
310
+ age,
311
+ gender,
312
+ height,
313
+ activity,
314
+ goal,
315
+ diet,
316
+ lang,
317
+ use_sota_model,
318
+ sota_model_id,
319
  ) -> Tuple[Optional[float], str, str, pd.DataFrame, object, str]:
320
  try:
321
+ if fasting_duration < 0 or fasting_duration > 72:
322
+ raise ValueError("Fasting must be 0–72h.")
323
  h, m = parse_hhmm(meal_timing)
324
+ if weight <= 0 or height <= 0 or age < 0:
325
+ raise ValueError("Invalid weight/height/age.")
326
 
327
  # Predict score
328
+ df = pd.DataFrame(
329
+ {
330
+ "Fasting Duration (hours)": [float(fasting_duration)],
331
+ "Meal Timing (hour:minute)": [h + m / 60],
332
+ "Body Weight (kg)": [float(weight)],
333
+ "Age (years)": [float(age)],
334
+ "Height (cm)": [float(height)],
335
+ "Gender_Male": [1 if gender == "Male" else 0],
336
+ "Gender_Other": [1 if gender == "Other" else 0],
337
+ }
338
+ )
339
  score = float(decision_tree_regressor.predict(df)[0])
340
 
341
  # Metrics
 
349
  sched = feeding_schedule(meal_timing, float(fasting_duration))
350
  plan_df = weekly_plan(diet, sched, target_kcal, protein_g)
351
 
352
+ chart_df = pd.DataFrame(
353
+ {
354
+ "Day": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
355
+ "start": [int(s.split(":")[0]) * 60 + int(s.split(":")[1]) for s, _ in sched],
356
+ "length": [max(0, int((24 - float(fasting_duration)) * 60))] * 7,
357
+ }
358
+ )
359
+ fig = px.bar(
360
+ chart_df,
361
+ y="Day",
362
+ x="length",
363
+ base="start",
364
+ orientation="h",
365
+ title="Feeding window each day (minutes)",
366
+ )
367
  fig.update_layout(
368
+ xaxis=dict(
369
+ range=[0, 1440],
370
+ tickvals=[0, 360, 720, 1080, 1440],
371
+ ticktext=["00:00", "06:00", "12:00", "18:00", "24:00"],
372
+ ),
373
+ height=300,
374
+ margin=dict(l=10, r=10, t=40, b=10),
375
  )
376
 
377
+ # Base markdown (deterministic, structured). Optionally enhance with SOTA.
378
  kpis = (
379
  f"**Score:** {score:.1f} • **BMI:** {bmi_val} • **BMR:** {int(bmr)} kcal • "
380
  f"**TDEE:** {int(tdee_kcal)} kcal • **Target:** {target_kcal} kcal • **Protein:** ≥ {protein_g} g • "
381
  f"**Diet:** {diet}"
382
  )
383
+ sched_md = "\n".join(
384
+ [
385
+ f"- **{d}**: {s} – {e}"
386
+ for d, (s, e) in zip(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], sched)
387
+ ]
388
+ )
389
  workouts_md = "\n".join([f"- {w}" for w in WORKOUTS[goal]])
390
  shop_md = "\n".join([f"- {x}" for x in shopping_list(diet)])
391
 
392
  base_plan_md = f"""
393
  ## Your 7-day intermittent fasting plan
 
394
  {kpis}
 
395
  ### Feeding window (daily)
396
  {sched_md}
 
397
  ### Weekly training
398
  {workouts_md}
 
399
  ### Daily meals (example week)
400
  (See the table below.)
 
401
  ### Shopping list
402
  {shop_md}
 
403
  > Hydration & electrolytes during the fast, protein at each meal, whole foods, and 7–9 hours sleep.
404
  """.strip()
405
 
406
  # Enhance/format with chosen generator
407
  if use_sota_model:
408
+ if not get_hf_api_key():
409
+ # Friendly guidance + fallback
410
+ plan_md = (
411
+ "⚠️ **HF Inference is enabled but no token was found.**\n\n"
412
+ "Add a Space secret named `HF_API_KEY` (or `HF_TOKEN`) in **Settings → Repository secrets**, "
413
+ "or uncheck **Use SOTA model** to use the local fallback.\n\n"
414
+ + generate_on_gpu(
415
+ "Rewrite in a friendly coaching tone; keep markdown structure; do not remove tables or metrics.\n\n"
416
+ + base_plan_md,
417
+ max_new_tokens=700,
418
+ )
419
+ )
420
+ else:
421
+ plan_md = generate_with_hf_inference(
422
+ prompt=(
423
+ "You are an expert health coach. Refine the following intermittent fasting plan. "
424
+ "Keep markdown headings and bullets; be concise and specific; keep the meaning. "
425
+ f"Language: '{lang}'.\n\n{base_plan_md}"
426
+ ),
427
+ model_id=sota_model_id,
428
+ max_new_tokens=900,
429
+ )
430
  else:
431
+ # Local tiny model inside ZeroGPU window (or CPU fallback)
432
  plan_md = generate_on_gpu(
433
+ "Rewrite in a friendly coaching tone; keep markdown structure; do not remove tables or metrics.\n\n"
434
+ + base_plan_md,
435
+ max_new_tokens=700,
436
  )
437
 
438
  # Save for download
 
444
  except Exception as e:
445
  return None, "", f"⚠️ {e}", pd.DataFrame(), None, ""
446
 
447
+
448
  # ------------------------
449
  # Tracker logic
450
  # ------------------------
451
  active_fasts: Dict[str, pd.Timestamp] = {}
452
 
453
+
454
  def _csv(user: str) -> Path:
455
+ safe = "".join(ch for ch in (user or "default") if ch.isalnum() or ch in ("_", "-"))
456
  return DATA_DIR / f"{safe}.csv"
457
 
458
+
459
  def hist_load(user: str) -> pd.DataFrame:
460
  p = _csv(user)
461
  if p.exists():
462
  d = pd.read_csv(p)
463
+ for c in ["start_time", "end_time"]:
464
+ if c in d:
465
+ d[c] = pd.to_datetime(d[c], errors="coerce")
466
  return d
467
+ return pd.DataFrame(columns=["start_time", "end_time", "duration_hours", "note"])
468
+
469
 
470
  def hist_save(user: str, d: pd.DataFrame):
471
  d.to_csv(_csv(user), index=False)
472
 
473
+
474
  def make_hist_chart(df: pd.DataFrame):
475
+ if df.empty:
476
+ return None
477
  d = df.dropna(subset=["end_time"]).copy()
478
+ if d.empty:
479
+ return None
480
  d["date"] = pd.to_datetime(d["end_time"]).dt.date
481
  fig = px.bar(d, x="date", y="duration_hours", title="Fasting duration by day (h)")
482
+ fig.update_layout(height=300, margin=dict(l=10, r=10, t=40, b=10))
483
  return fig
484
 
485
+
486
  def compute_streak(df: pd.DataFrame) -> int:
487
  d = df.dropna(subset=["end_time"]).copy()
488
+ if d.empty:
489
+ return 0
490
  days = set(pd.to_datetime(d["end_time"]).dt.date)
491
+ cur = pd.Timestamp.now().date()
492
+ streak = 0
493
  while cur in days:
494
+ streak += 1
495
+ cur = cur - pd.Timedelta(days=1)
496
  return streak
497
 
498
+
499
  def hist_stats(df: pd.DataFrame) -> str:
500
+ if df.empty:
501
+ return "No history yet."
502
  last7 = df.tail(7)
503
  avg = last7["duration_hours"].mean()
504
  streak = compute_streak(df)
505
  return f"Total fasts: {len(df)}\nAvg (last 7): {avg:.2f} h\nCurrent streak: {streak} day(s)"
506
 
507
+
508
  def start_fast(user: str, note: str):
509
+ if not user:
510
+ return "Enter username in Tracker.", None
511
+ if user in active_fasts:
512
+ return f"Already fasting since {active_fasts[user].strftime(TS_FMT)}.", None
513
  active_fasts[user] = pd.Timestamp.now()
514
  return f"✅ Fast started at {active_fasts[user].strftime(TS_FMT)}.", None
515
 
516
+
517
  def end_fast(user: str):
518
+ if not user:
519
+ return "Enter username.", None, None, None
520
+ if user not in active_fasts:
521
+ return "No active fast.", None, None, None
522
+ end = pd.Timestamp.now()
523
+ start = active_fasts.pop(user)
524
+ dur = round((end - start).total_seconds() / 3600, 2)
525
  df = hist_load(user)
526
  df.loc[len(df)] = [start, end, dur, ""]
527
  hist_save(user, df)
528
  return f"✅ Fast ended at {end.strftime(TS_FMT)} • {dur} h", df.tail(12), make_hist_chart(df), hist_stats(df)
529
 
530
+
531
  def refresh_hist(user: str):
532
  df = hist_load(user)
533
  return df.tail(12), make_hist_chart(df), hist_stats(df)
534
 
535
+
536
  # ------------------------
537
  # UI
538
  # ------------------------
539
+ def hf_status_md() -> str:
540
+ key = get_hf_api_key()
541
+ if key:
542
+ return "✅ **HF API key detected** (SOTA inference will work)."
543
+ return "⚠️ **HF API key not detected.** Add a Space secret named `HF_API_KEY` (or `HF_TOKEN`) to enable SOTA inference."
544
+
545
+
546
  with gr.Blocks(
547
  title="Intermittent Fasting Coach — Pro (SOTA)",
548
+ theme=gr.themes.Soft(
549
+ primary_hue=gr.themes.colors.orange,
550
+ neutral_hue=gr.themes.colors.gray,
551
+ ),
552
  ) as demo:
553
+ gr.Markdown(
554
+ """
555
  # 🥣 Intermittent Fasting — Pro (SOTA)
556
  Detailed coaching plans + tracker. ZeroGPU-ready (with CPU fallback). Data stored locally in this Space.
557
+ """
558
+ )
559
 
560
  with gr.Tabs():
561
  # --- Coach tab
562
  with gr.TabItem("Coach"):
563
+ hf_status = gr.Markdown()
564
+
565
  with gr.Row():
566
  with gr.Column():
567
+ fasting_duration = gr.Number(
568
+ label="Fasting duration (h)", value=16, minimum=0, maximum=72, step=0.5
569
+ )
570
  meal_timing = gr.Textbox(label="First meal time (HH:MM)", value="12:30")
571
  weight = gr.Number(label="Body weight (kg)", value=70, step=0.5)
572
  with gr.Column():
573
  age = gr.Slider(label="Age (years)", minimum=18, maximum=100, value=35)
574
+ gender = gr.Radio(["Male", "Female", "Other"], label="Gender", value="Male")
575
  height = gr.Number(label="Height (cm)", value=175)
576
 
577
  with gr.Row():
578
  activity = gr.Dropdown(choices=list(ACTIVITY.keys()), value="Lightly active", label="Activity")
579
  goal = gr.Dropdown(choices=list(GOAL_CAL_ADJ.keys()), value="Recomp/Maintenance", label="Goal")
580
  diet = gr.Dropdown(choices=DIET_STYLES, value="Mediterranean", label="Diet style")
581
+ lang = gr.Radio(["en", "es"], value="en", label="Language")
582
  use_sota_model = gr.Checkbox(value=True, label="Use SOTA model (HF Inference)")
583
  sota_model_id = gr.Dropdown(choices=SOTA_MODELS, value=SOTA_MODELS[0], label="HF model")
584
 
 
587
  score_out = gr.Number(label="Predicted score")
588
  kpi_out = gr.Markdown()
589
  plan_md = gr.Markdown()
590
+ plan_tbl = gr.Dataframe(
591
+ headers=["Day", "Feeding window", "Meal 1", "Meal 2", "Protein target", "Daily kcal", "Snack"],
592
+ interactive=False,
593
+ )
594
  fig = gr.Plot()
595
  dl = gr.DownloadButton(label="Download plan (.md)")
596
 
597
  btn.click(
598
  predict_and_plan,
599
+ inputs=[
600
+ fasting_duration,
601
+ meal_timing,
602
+ weight,
603
+ age,
604
+ gender,
605
+ height,
606
+ activity,
607
+ goal,
608
+ diet,
609
+ lang,
610
+ use_sota_model,
611
+ sota_model_id,
612
+ ],
613
  outputs=[score_out, kpi_out, plan_md, plan_tbl, fig, dl],
614
+ api_name="coach_plan",
615
  )
616
 
617
  # --- Tracker tab
 
623
  b1 = gr.Button("Start fast", variant="primary")
624
  b2 = gr.Button("End fast")
625
  b3 = gr.Button("Reload history")
626
+
627
  status = gr.Markdown("Not fasting.")
628
  hist = gr.Dataframe(interactive=False)
629
  hist_fig = gr.Plot()
630
  stats = gr.Markdown()
631
 
632
  b1.click(start_fast, inputs=[user, note], outputs=[status, note])
633
+ b2.click(end_fast, inputs=[user], outputs=[status, hist, hist_fig, stats])
634
  b3.click(refresh_hist, inputs=[user], outputs=[hist, hist_fig, stats])
635
  demo.load(refresh_hist, inputs=[user], outputs=[hist, hist_fig, stats])
636
 
637
  # --- About tab
638
  with gr.TabItem("About"):
639
+ gr.Markdown(
640
+ """
641
  **How it works**
642
  • The predictor estimates a health score from inputs.
643
  • The coach builds a 7-day schedule matching your fasting window, goal, activity and diet style.
644
+ • SOTA option uses Hugging Face Inference; fallback uses a tiny local model in the ZeroGPU window.
645
  • Tracker stores CSVs under `/data/` and never sends data elsewhere.
646
+
647
+ **Enable SOTA inference**
648
+ Add a Space secret named `HF_API_KEY` (recommended) or `HF_TOKEN` in **Settings → Repository secrets**.
649
+ """
650
+ )
651
+
652
+ # Show whether token is detected (does not reveal the token)
653
+ demo.load(hf_status_md, outputs=[hf_status])
654
 
655
  if __name__ == "__main__":
656
+ demo.queue().launch()