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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +362 -108
app.py CHANGED
@@ -8,6 +8,7 @@ import numpy as np
8
  import plotly.express as px
9
  import joblib
10
 
 
11
  import spaces
12
  import torch
13
  from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, pipeline
@@ -22,15 +23,15 @@ TS_FMT = "%Y-%m-%d %H:%M:%S"
22
  DT_PATH = "./decision_tree_regressor.joblib"
23
  decision_tree_regressor = joblib.load(DT_PATH)
24
 
25
- # Local lightweight model for fallback
26
  GEN_MODEL = "google/flan-t5-small"
27
  _tokenizer = AutoTokenizer.from_pretrained(GEN_MODEL)
28
  _model = AutoModelForSeq2SeqLM.from_pretrained(GEN_MODEL)
29
  _generate_cpu = pipeline("text2text-generation", model=_model, tokenizer=_tokenizer, device=-1)
30
 
31
- # SOTA models available via Inference API
32
  SOTA_MODELS = [
33
- "Qwen/Qwen2.5-72B-Instruct", # default
34
  "meta-llama/Meta-Llama-3.1-70B-Instruct",
35
  "mistralai/Mistral-Nemo-Instruct-2407",
36
  "Qwen/Qwen2.5-32B-Instruct",
@@ -39,9 +40,13 @@ SOTA_MODELS = [
39
 
40
  def _hf_client(model_id: str) -> InferenceClient:
41
  token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN")
42
- return InferenceClient(model=model_id, token=token, timeout=90)
43
 
44
- def generate_with_hf_inference(prompt: str, model_id: str, max_new_tokens: int = 700) -> str:
 
 
 
 
45
  try:
46
  client = _hf_client(model_id)
47
  text = client.text_generation(
@@ -55,17 +60,18 @@ def generate_with_hf_inference(prompt: str, model_id: str, max_new_tokens: int =
55
  )
56
  return text.strip()
57
  except Exception as e:
58
- return f"(HF Inference error: {e})\n" + generate_on_gpu(prompt, max_new_tokens=max_new_tokens)
 
59
 
60
  # ------------------------
61
- # ZeroGPU functions
62
  # ------------------------
63
- @spaces.GPU
64
- def gpu_warmup() -> str:
65
- return f"cuda={torch.cuda.is_available()}"
66
-
67
  @spaces.GPU
68
  def generate_on_gpu(prompt: str, max_new_tokens: int = 600) -> str:
 
 
 
 
69
  try:
70
  if torch.cuda.is_available():
71
  gen = pipeline("text2text-generation", model=_model.to("cuda"), tokenizer=_tokenizer, device=0)
@@ -77,13 +83,8 @@ def generate_on_gpu(prompt: str, max_new_tokens: int = 600) -> str:
77
  out = _generate_cpu(prompt, max_new_tokens=max_new_tokens)
78
  return out[0]["generated_text"].strip() + f"\n\n(Note: GPU path failed: {e})"
79
 
80
- try:
81
- _ = gpu_warmup()
82
- except Exception:
83
- pass
84
-
85
  # ------------------------
86
- # Metrics
87
  # ------------------------
88
  ACTIVITY = {"Sedentary":1.2,"Lightly active":1.375,"Moderately active":1.55,"Very active":1.725,"Athlete":1.9}
89
  GOAL_CAL_ADJ = {"Fat loss":-0.15,"Recomp/Maintenance":0.0,"Muscle gain":0.10}
@@ -92,128 +93,381 @@ def bmi(w,h): return w/((h/100)**2)
92
  def bmr_mifflin(sex,w,h,a): return 10*w+6.25*h-5*a+(5 if sex=="Male" else -161)
93
  def tdee(bmr,act): return bmr*ACTIVITY.get(act,1.2)
94
 
95
- # ------------------------
96
- # Tracker storage
97
- # ------------------------
98
- active_fasts: Dict[str,pd.Timestamp] = {}
99
- def _csv(u): return DATA_DIR/f"{''.join(ch for ch in u if ch.isalnum() or ch in ('_','-'))}.csv"
100
-
101
- def hist_load(u):
102
- p=_csv(u)
103
- if p.exists():
104
- d=pd.read_csv(p);
105
- for c in ["start_time","end_time"]:
106
- if c in d: d[c]=pd.to_datetime(d[c],errors="coerce")
107
- return d
108
- return pd.DataFrame(columns=["start_time","end_time","duration_hours","note"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
- def hist_save(u,d): d.to_csv(_csv(u),index=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  # ------------------------
113
- # Plan generator
114
  # ------------------------
115
  def predict_and_plan(
116
  fasting_duration, meal_timing, weight, age, gender, height,
117
- activity, goal, diet, lang,
118
- ai_polish, use_sota_model, sota_model_id
119
  ) -> Tuple[Optional[float], str, str, pd.DataFrame, object, str]:
120
-
121
  try:
122
- # Input check
123
- if fasting_duration<0 or fasting_duration>72: raise ValueError("Fasting must be 0–72h.")
124
- h,m = map(int,meal_timing.split(":")); assert 0<=h<24 and 0<=m<60
125
- if weight<=0 or height<=0 or age<0: raise ValueError("Invalid weight/height/age.")
126
-
127
- # Score
128
- df=pd.DataFrame({
129
- "Fasting Duration (hours)":[float(fasting_duration)],
130
- "Meal Timing (hour:minute)":[h+m/60],
131
- "Body Weight (kg)":[float(weight)],
132
- "Age (years)":[float(age)],
133
- "Height (cm)":[float(height)],
134
- "Gender_Male":[1 if gender=="Male" else 0],
135
- "Gender_Other":[1 if gender=="Other" else 0],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  })
137
- score=float(decision_tree_regressor.predict(df)[0])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
- bmr=bmr_mifflin(gender,weight,height,age)
140
- tdee_kcal=tdee(bmr,activity)
141
- adj=GOAL_CAL_ADJ[goal]; target_kcal=int(round(tdee_kcal*(1+adj)))
142
- protein_g=int(round(max(1.6*weight,80)))
143
- bmi_val=round(bmi(weight,height),1)
144
 
145
- plan_md=f"""
146
- ## Intermittent Fasting Plan
147
 
148
- **Score:** {score:.1f} • **BMI:** {bmi_val} • **BMR:** {int(bmr)} kcal • **TDEE:** {int(tdee_kcal)} kcal • **Target:** {target_kcal} kcal • **Protein:** ≥ {protein_g} g
 
149
 
150
- - Goal: {goal}
151
- - Diet: {diet}
152
- - First meal: {meal_timing}
153
- - Fasting duration: {fasting_duration} h
154
- """
155
- # Choose generator
156
  if use_sota_model:
157
- plan_md = generate_with_hf_inference(plan_md, sota_model_id, max_new_tokens=700)
158
- elif ai_polish:
159
- plan_md = generate_on_gpu(plan_md, max_new_tokens=600)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
- md_path=DATA_DIR/"plan.md"; md_path.write_text(plan_md,encoding="utf-8")
162
- return score, f"Target kcal {target_kcal}, protein {protein_g} g", plan_md, pd.DataFrame(), None, str(md_path)
163
  except Exception as e:
164
- return None,"",f"⚠️ {e}",pd.DataFrame(),None,""
165
 
166
  # ------------------------
167
- # Tracker actions
168
  # ------------------------
169
- def start_fast(user,note):
170
- if not user: return "Enter username.",None
171
- if user in active_fasts: return f"Already fasting since {active_fasts[user]}.",None
172
- active_fasts[user]=pd.Timestamp.now(); return f"✅ Fast started at {active_fasts[user].strftime(TS_FMT)}.",None
173
-
174
- def end_fast(user):
175
- if not user: return "Enter username.",None,None,None
176
- if user not in active_fasts: return "No active fast.",None,None,None
177
- end=pd.Timestamp.now(); start=active_fasts.pop(user)
178
- dur=round((end-start).total_seconds()/3600,2)
179
- df=hist_load(user); df.loc[len(df)]=[start,end,dur,""]; hist_save(user,df)
180
- return f"✅ Fast ended {end.strftime(TS_FMT)} {dur} h", df.tail(12), None, f"Total fasts {len(df)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
  # ------------------------
183
  # UI
184
  # ------------------------
185
- with gr.Blocks(title="Fasting Coach — SOTA", theme=gr.themes.Soft()) as demo:
186
- gr.Markdown("# 🥣 Intermittent Fasting Coach — SOTA\nZeroGPU + HF Inference API")
 
 
 
 
 
 
187
 
188
  with gr.Tabs():
 
189
  with gr.TabItem("Coach"):
190
  with gr.Row():
191
  with gr.Column():
192
- fasting_duration=gr.Number(label="Fasting Duration (h)",value=16,minimum=0,maximum=72)
193
- meal_timing=gr.Textbox(label="First meal (HH:MM)",value="12:30")
194
- weight=gr.Number(label="Weight (kg)",value=70)
195
  with gr.Column():
196
- age=gr.Slider(label="Age",minimum=18,maximum=100,value=35)
197
- gender=gr.Radio(["Male","Female","Other"],label="Gender",value="Male")
198
- height=gr.Number(label="Height (cm)",value=175)
 
199
  with gr.Row():
200
- activity=gr.Dropdown(list(ACTIVITY.keys()),label="Activity",value="Lightly active")
201
- goal=gr.Dropdown(list(GOAL_CAL_ADJ.keys()),label="Goal",value="Recomp/Maintenance")
202
- diet=gr.Dropdown(["Mediterranean","Omnivore","Vegan"],label="Diet",value="Mediterranean")
203
- lang=gr.Radio(["en","es"],label="Language",value="en")
204
- ai_polish=gr.Checkbox(label="Local polish (tiny model)")
205
- use_sota_model=gr.Checkbox(label="Use SOTA model (HF Inference)",value=True)
206
- sota_model_id=gr.Dropdown(SOTA_MODELS,value=SOTA_MODELS[0],label="HF model")
207
- btn=gr.Button("Predict & Build Plan",variant="primary")
208
- score_out=gr.Number(label="Score"); kpi_out=gr.Markdown(); plan_md=gr.Markdown(); plan_tbl=gr.Dataframe(); fig=gr.Plot(); dl=gr.DownloadButton(label="Download plan")
209
- btn.click(predict_and_plan,inputs=[fasting_duration,meal_timing,weight,age,gender,height,activity,goal,diet,lang,ai_polish,use_sota_model,sota_model_id],outputs=[score_out,kpi_out,plan_md,plan_tbl,fig,dl])
 
 
 
 
 
210
 
 
 
 
 
 
 
 
 
211
  with gr.TabItem("Tracker"):
212
- user=gr.Textbox(label="Username"); note=gr.Textbox(label="Note")
213
- b1=gr.Button("Start fast"); b2=gr.Button("End fast")
214
- status=gr.Markdown(); hist=gr.Dataframe(); stats=gr.Markdown()
215
- b1.click(start_fast,inputs=[user,note],outputs=[status,note])
216
- b2.click(end_fast,inputs=[user],outputs=[status,hist,None,stats])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
- if __name__=="__main__":
219
  demo.queue().launch()
 
8
  import plotly.express as px
9
  import joblib
10
 
11
+ # ZeroGPU + models
12
  import spaces
13
  import torch
14
  from transformers import AutoModelForSeq2SeqLM, AutoTokenizer, pipeline
 
23
  DT_PATH = "./decision_tree_regressor.joblib"
24
  decision_tree_regressor = joblib.load(DT_PATH)
25
 
26
+ # Local lightweight model (fallback)
27
  GEN_MODEL = "google/flan-t5-small"
28
  _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",
 
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)
52
  text = client.text_generation(
 
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
  # ------------------------
 
 
 
 
69
  @spaces.GPU
70
  def generate_on_gpu(prompt: str, max_new_tokens: int = 600) -> str:
71
+ """
72
+ Generate with tiny local model. If CUDA is available in the ZeroGPU window,
73
+ bind pipeline to GPU; otherwise use CPU.
74
+ """
75
  try:
76
  if torch.cuda.is_available():
77
  gen = pipeline("text2text-generation", model=_model.to("cuda"), tokenizer=_tokenizer, device=0)
 
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}
 
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 = {
109
+ "Mediterranean": [
110
+ "Oats + dates + walnuts + olive oil",
111
+ "Grilled fish, lentil salad, greens",
112
+ "Hummus, wholegrain pita, veggies",
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",
120
+ "Chicken bowl (rice, veg, 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
+ "Vegetarian": [
128
+ "Tofu scramble, toast, avocado",
129
+ "Paneer tikka bowl",
130
+ "Bean chili + brown rice",
131
+ "Halloumi, couscous, veg",
132
+ "Greek salad + eggs",
133
+ "Tempeh stir-fry",
134
+ "Yogurt parfait + granola"
135
+ ],
136
+ "Vegan": [
137
+ "Tofu scramble, avocado toast",
138
+ "Lentil curry + basmati",
139
+ "Burrito bowl (beans, corn, salsa)",
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",
147
+ "Chicken Caesar (no croutons)",
148
+ "Beef & greens stir-fry",
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))
179
+ start_minutes = h * 60 + m
180
+ end_minutes = int((start_minutes + window * 60) % (24 * 60))
181
+ sched = []
182
+ for _ in range(7):
183
+ start = fmt_hhmm(h, m)
184
+ end = fmt_hhmm(end_minutes // 60, end_minutes % 60)
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",
211
+ "Olive oil, nuts/seeds, herbs & spices",
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
248
+ bmr = bmr_mifflin(gender, weight, height, age)
249
+ tdee_kcal = tdee(bmr, activity)
250
+ target_kcal = int(round(tdee_kcal * (1 + GOAL_CAL_ADJ[goal])))
251
+ protein_g = int(round(max(1.6 * weight, 80)))
252
+ bmi_val = round(bmi(weight, height), 1)
253
+
254
+ # Schedule, plan table, chart
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
320
+ md_path = DATA_DIR / "plan.md"
321
+ md_path.write_text(plan_md, encoding="utf-8")
322
+
323
+ return score, kpis, plan_md, plan_df, fig, str(md_path)
324
 
 
 
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
+
427
+ btn = gr.Button("Predict & Build Plan", variant="primary")
428
+
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
444
  with gr.TabItem("Tracker"):
445
+ with gr.Row():
446
+ user = gr.Textbox(label="Username", value="")
447
+ note = gr.Textbox(label="Note (optional)")
448
+ with gr.Row():
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()