Spaces:
Sleeping
Sleeping
Commit
·
93e01f8
1
Parent(s):
72ddeef
nutricao
Browse files- app.py +94 -6
- nutrition.py +192 -0
app.py
CHANGED
|
@@ -7,6 +7,7 @@ import unicodedata
|
|
| 7 |
import json
|
| 8 |
import re
|
| 9 |
import random
|
|
|
|
| 10 |
|
| 11 |
# Carregar o JSON
|
| 12 |
with open("exercicios.json", "r", encoding="utf-8") as f:
|
|
@@ -494,6 +495,27 @@ def gerar_split(dias, budget=45, objetivos=["hipertrofia"], lesoes=[]):
|
|
| 494 |
|
| 495 |
return treino_semana
|
| 496 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
|
| 498 |
|
| 499 |
# -------------------------
|
|
@@ -572,15 +594,81 @@ def responder(prompt: str):
|
|
| 572 |
"treino": treino
|
| 573 |
}
|
| 574 |
|
| 575 |
-
|
| 576 |
# 🚀 NUTRIÇÃO
|
| 577 |
# -------------------------------
|
| 578 |
if intenções["nutricao"]:
|
| 579 |
-
#
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
|
| 585 |
# -------------------------------
|
| 586 |
# 🚀 Recuperação (extra opcional)
|
|
|
|
| 7 |
import json
|
| 8 |
import re
|
| 9 |
import random
|
| 10 |
+
from nutrition import UserProfile, build_basic_plan
|
| 11 |
|
| 12 |
# Carregar o JSON
|
| 13 |
with open("exercicios.json", "r", encoding="utf-8") as f:
|
|
|
|
| 495 |
|
| 496 |
return treino_semana
|
| 497 |
|
| 498 |
+
def gerar_plano(age, sex, weight, height, activity, goal, intensity):
|
| 499 |
+
try:
|
| 500 |
+
user = UserProfile(
|
| 501 |
+
age=int(age),
|
| 502 |
+
sex=sex,
|
| 503 |
+
weight_kg=float(weight),
|
| 504 |
+
height_cm=float(height),
|
| 505 |
+
activity=activity,
|
| 506 |
+
)
|
| 507 |
+
plan = build_basic_plan(user, goal=goal, intensity=intensity)
|
| 508 |
+
return (
|
| 509 |
+
f"📊 **Plano Nutricional**\n\n"
|
| 510 |
+
f"- Calorias alvo: {plan.calories_target} kcal\n"
|
| 511 |
+
f"- Proteína: {plan.protein_g} g\n"
|
| 512 |
+
f"- Carboidratos: {plan.carbs_g} g\n"
|
| 513 |
+
f"- Gorduras: {plan.fats_g} g\n\n"
|
| 514 |
+
f"ℹ️ {plan.note}\n"
|
| 515 |
+
)
|
| 516 |
+
except Exception as e:
|
| 517 |
+
return f"Erro: {str(e)}"
|
| 518 |
+
|
| 519 |
|
| 520 |
|
| 521 |
# -------------------------
|
|
|
|
| 594 |
"treino": treino
|
| 595 |
}
|
| 596 |
|
| 597 |
+
# -------------------------------
|
| 598 |
# 🚀 NUTRIÇÃO
|
| 599 |
# -------------------------------
|
| 600 |
if intenções["nutricao"]:
|
| 601 |
+
# tenta extrair infos do prompt
|
| 602 |
+
age = 25
|
| 603 |
+
weight = 70
|
| 604 |
+
height = 175
|
| 605 |
+
sex = "male"
|
| 606 |
+
activity = "moderate"
|
| 607 |
+
goal = "maintenance"
|
| 608 |
+
intensity = "moderate"
|
| 609 |
+
|
| 610 |
+
# idade
|
| 611 |
+
match = re.search(r"(\d+)\s*anos", prompt_norm)
|
| 612 |
+
if match:
|
| 613 |
+
age = int(match.group(1))
|
| 614 |
+
|
| 615 |
+
# peso
|
| 616 |
+
match = re.search(r"(\d+)\s*(kg|quilo|kilos)", prompt_norm)
|
| 617 |
+
if match:
|
| 618 |
+
weight = float(match.group(1))
|
| 619 |
+
|
| 620 |
+
# altura
|
| 621 |
+
match = re.search(r"(\d+)\s*(cm|centimetro|centímetros)", prompt_norm)
|
| 622 |
+
if match:
|
| 623 |
+
height = float(match.group(1))
|
| 624 |
+
|
| 625 |
+
# sexo
|
| 626 |
+
if "mulher" in prompt_norm or "feminino" in prompt_norm:
|
| 627 |
+
sex = "female"
|
| 628 |
+
elif "homem" in prompt_norm or "masculino" in prompt_norm:
|
| 629 |
+
sex = "male"
|
| 630 |
+
|
| 631 |
+
# objetivo
|
| 632 |
+
if "bulking" in prompt_norm or "ganhar" in prompt_norm or "massa" in prompt_norm:
|
| 633 |
+
goal = "bulking"
|
| 634 |
+
elif "cutting" in prompt_norm or "emagrecer" in prompt_norm or "perder" in prompt_norm:
|
| 635 |
+
goal = "cutting"
|
| 636 |
+
elif "manutenção" in prompt_norm or "manter" in prompt_norm:
|
| 637 |
+
goal = "maintenance"
|
| 638 |
+
|
| 639 |
+
# intensidade
|
| 640 |
+
if "leve" in prompt_norm or "leve" in prompt_norm:
|
| 641 |
+
intensity = "mild"
|
| 642 |
+
elif "moderado" in prompt_norm or "moderada" in prompt_norm:
|
| 643 |
+
intensity = "moderate"
|
| 644 |
+
elif "agressivo" in prompt_norm or "rápido" in prompt_norm:
|
| 645 |
+
intensity = "aggressive"
|
| 646 |
+
|
| 647 |
+
try:
|
| 648 |
+
user = UserProfile(
|
| 649 |
+
age=age,
|
| 650 |
+
sex=sex,
|
| 651 |
+
weight_kg=weight,
|
| 652 |
+
height_cm=height,
|
| 653 |
+
activity=activity,
|
| 654 |
+
)
|
| 655 |
+
plan = build_basic_plan(user, goal=goal, intensity=intensity)
|
| 656 |
+
|
| 657 |
+
resposta_final["nutricao"] = {
|
| 658 |
+
"calorias_alvo": plan.calories_target,
|
| 659 |
+
"proteina_g": plan.protein_g,
|
| 660 |
+
"carboidratos_g": plan.carbs_g,
|
| 661 |
+
"gorduras_g": plan.fats_g,
|
| 662 |
+
"nota": plan.note,
|
| 663 |
+
"goal": goal,
|
| 664 |
+
"intensidade": intensity
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
except Exception as e:
|
| 668 |
+
resposta_final["nutricao"] = {
|
| 669 |
+
"erro": str(e)
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
|
| 673 |
# -------------------------------
|
| 674 |
# 🚀 Recuperação (extra opcional)
|
nutrition.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from dataclasses import dataclass, field, asdict
|
| 3 |
+
from typing import Optional, Dict, Tuple, Literal
|
| 4 |
+
import math
|
| 5 |
+
|
| 6 |
+
ActivityLevel = Literal[
|
| 7 |
+
"sedentary",
|
| 8 |
+
"light",
|
| 9 |
+
"moderate",
|
| 10 |
+
"active",
|
| 11 |
+
"very_active",
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class UserProfile:
|
| 17 |
+
age: int
|
| 18 |
+
sex: Literal["male", "female"]
|
| 19 |
+
weight_kg: float
|
| 20 |
+
height_cm: float
|
| 21 |
+
activity: ActivityLevel = "moderate"
|
| 22 |
+
body_fat_percent: Optional[float] = None
|
| 23 |
+
|
| 24 |
+
def __post_init__(self):
|
| 25 |
+
if self.weight_kg <= 0 or self.height_cm <= 0:
|
| 26 |
+
raise ValueError("Peso e altura devem ser maiores que zero")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass
|
| 30 |
+
class NutritionPlan:
|
| 31 |
+
calories_target: int
|
| 32 |
+
protein_g: int
|
| 33 |
+
carbs_g: int
|
| 34 |
+
fats_g: int
|
| 35 |
+
note: Optional[str] = None
|
| 36 |
+
tags: Tuple[str, ...] = field(default_factory=tuple)
|
| 37 |
+
|
| 38 |
+
def as_dict(self) -> Dict:
|
| 39 |
+
return asdict(self)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# ------------------- Básicas (BMR, TDEE) -------------------
|
| 43 |
+
def bmr_mifflin_st_jeor(profile: UserProfile) -> float:
|
| 44 |
+
"""Calcula BMR (kcal/dia) usando Mifflin-St Jeor."""
|
| 45 |
+
if profile.sex == "male":
|
| 46 |
+
s = 5
|
| 47 |
+
else:
|
| 48 |
+
s = -161
|
| 49 |
+
bmr = 10 * profile.weight_kg + 6.25 * profile.height_cm - 5 * profile.age + s
|
| 50 |
+
return float(bmr)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
_ACTIVITY_FACTORS: Dict[ActivityLevel, float] = {
|
| 54 |
+
"sedentary": 1.2, # pouco ou nenhum exercício
|
| 55 |
+
"light": 1.375, # exercício leve 1-3 dias/semana
|
| 56 |
+
"moderate": 1.55, # exercício moderado 3-5 dias/semana
|
| 57 |
+
"active": 1.725, # exercício intenso 6-7 dias/semana
|
| 58 |
+
"very_active": 1.9, # trabalho físico ou treino 2x/dia
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def tdee(profile: UserProfile) -> int:
|
| 63 |
+
"""Calcula TDEE aproximado (kcal/dia).
|
| 64 |
+
|
| 65 |
+
Nota: é uma estimativa — medir e ajustar com base na evolução real.
|
| 66 |
+
"""
|
| 67 |
+
bmr = bmr_mifflin_st_jeor(profile)
|
| 68 |
+
factor = _ACTIVITY_FACTORS.get(profile.activity, 1.55)
|
| 69 |
+
return int(round(bmr * factor))
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ------------------- Metas calóricas: bulking / cutting -------------------
|
| 73 |
+
def calorie_target_for_goal(base_tdee: int, goal: Literal["maintenance", "bulking", "cutting"], intensity: Literal["mild", "moderate", "aggressive"] = "moderate") -> int:
|
| 74 |
+
"""Calcula target calórico com base no objetivo e intensidade."""
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
adjustments = {
|
| 78 |
+
"bulking": {"mild": 1.05, "moderate": 1.10, "aggressive": 1.20},
|
| 79 |
+
"cutting": {"mild": 0.90, "moderate": 0.80, "aggressive": 0.70},
|
| 80 |
+
"maintenance": {"mild": 1.0, "moderate": 1.0, "aggressive": 1.0},
|
| 81 |
+
}
|
| 82 |
+
factor = adjustments[goal][intensity]
|
| 83 |
+
return int(round(base_tdee * factor))
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# ------------------- Macros -------------------
|
| 87 |
+
_CAL_PER_G = {
|
| 88 |
+
"protein": 4,
|
| 89 |
+
"carb": 4,
|
| 90 |
+
"fat": 9,
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def macros_from_calories(calories: int, protein_g: Optional[float] = None, protein_g_per_kg: Optional[float] = None, profile: Optional[UserProfile] = None, carb_ratio: Optional[float] = None, fat_ratio: Optional[float] = None) -> NutritionPlan:
|
| 95 |
+
"""Cria uma divisão de macros a partir de calorias e algumas preferências.
|
| 96 |
+
|
| 97 |
+
Estratégia padrão quando não há especificações:
|
| 98 |
+
- Protein: se profile + protein_g_per_kg presente usa isso, senão 1.8 g/kg
|
| 99 |
+
- Fat: 25% das calorias
|
| 100 |
+
- Carb: restante
|
| 101 |
+
|
| 102 |
+
Retorna: NutritionPlan com macros em gramas.
|
| 103 |
+
"""
|
| 104 |
+
if protein_g is None:
|
| 105 |
+
if protein_g_per_kg is None:
|
| 106 |
+
protein_g_per_kg = 1.8
|
| 107 |
+
if profile is None:
|
| 108 |
+
raise ValueError("Para calcular proteína por kg é necessário fornecer `profile`")
|
| 109 |
+
protein_g = protein_g_per_kg * profile.weight_kg
|
| 110 |
+
|
| 111 |
+
# aplicar limites sensatos
|
| 112 |
+
protein_g = max(0, protein_g)
|
| 113 |
+
|
| 114 |
+
if fat_ratio is None and carb_ratio is None:
|
| 115 |
+
fat_ratio = 0.25
|
| 116 |
+
elif fat_ratio is None:
|
| 117 |
+
fat_ratio = max(0.15, 1 - carb_ratio - (protein_g * _CAL_PER_G["protein"] / calories))
|
| 118 |
+
|
| 119 |
+
# calorias provenientes de proteína e gordura
|
| 120 |
+
cal_from_protein = protein_g * _CAL_PER_G["protein"]
|
| 121 |
+
cal_from_fat = int(round(calories * fat_ratio))
|
| 122 |
+
fat_g = cal_from_fat / _CAL_PER_G["fat"]
|
| 123 |
+
|
| 124 |
+
remaining_cal = calories - (cal_from_protein + cal_from_fat)
|
| 125 |
+
carbs_g = max(0, remaining_cal / _CAL_PER_G["carb"]) if remaining_cal > 0 else 0
|
| 126 |
+
|
| 127 |
+
return NutritionPlan(
|
| 128 |
+
calories_target=int(round(calories)),
|
| 129 |
+
protein_g=int(round(protein_g)),
|
| 130 |
+
carbs_g=int(round(carbs_g)),
|
| 131 |
+
fats_g=int(round(fat_g)),
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def protein_recommendation(profile: UserProfile, goal: Literal["bulking", "cutting", "maintenance"] = "maintenance") -> Tuple[float, float]:
|
| 136 |
+
"""Retorna faixa recomendada de proteína em g/kg.
|
| 137 |
+
|
| 138 |
+
- Preservação/ganho muscular: 1.6 - 2.2 g/kg (pode subir ligeiramente no cutting intenso).
|
| 139 |
+
"""
|
| 140 |
+
low = 1.6
|
| 141 |
+
high = 2.2
|
| 142 |
+
if goal == "cutting":
|
| 143 |
+
high = 2.4
|
| 144 |
+
if profile.weight_kg < 60:
|
| 145 |
+
# ajustar para pesos muito baixos pode reduzir a faixa absoluta, porém manter g/kg
|
| 146 |
+
pass
|
| 147 |
+
return (low, high)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
# ------------------- Micronutrientes (educativo) -------------------
|
| 151 |
+
_MICRONUTRIENT_HIGHLIGHTS = {
|
| 152 |
+
"vitamin_d": "Importante para saúde óssea e sistema imunitário; muitos têm défice, exposição solar e suplementação podem ser necessárias.",
|
| 153 |
+
"iron": "Crucial para transporte de oxigénio — atenção especial em mulheres com menstruação intensa.",
|
| 154 |
+
"calcium": "Saúde óssea; combinar com vitamina D.",
|
| 155 |
+
"magnesium": "Recuperação muscular, sono e função neuromuscular.",
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def micronutrient_info(nutrient: str) -> str:
|
| 160 |
+
return _MICRONUTRIENT_HIGHLIGHTS.get(nutrient.lower(), "Informação não disponível para esse micronutriente.")
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# ------------------- Suplementação (informativa) -------------------
|
| 164 |
+
_SUPPLEMENT_OVERVIEW = {
|
| 165 |
+
"whey": "Proteína de rápida digestão útil para atingir ingestão proteica diária; não é obrigatória se a dieta fornecer proteína suficiente.",
|
| 166 |
+
"creatine": "Creatina monohidratada: melhora força e desempenho; uma das mais estudadas e seguras.",
|
| 167 |
+
"caffeine": "Melhora performance e foco; usar com cuidado e atenção a tolerância/sono.",
|
| 168 |
+
"omega3": "Ácidos gordos essenciais benéficos para saúde cardiovascular e inflamação.",
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def supplement_recommendation(supplement: str) -> str:
|
| 173 |
+
return _SUPPLEMENT_OVERVIEW.get(supplement.lower(), "Sem recomendação específica para este suplemento.")
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
# ------------------- Funções utilitárias / Exemplo de fluxo -------------------
|
| 177 |
+
def build_basic_plan(profile: UserProfile, goal: Literal["bulking", "cutting", "maintenance"] = "maintenance", intensity: Literal["mild", "moderate", "aggressive"] = "moderate") -> NutritionPlan:
|
| 178 |
+
"""Fluxo simples que calcula TDEE, target calórico e macros padrão.
|
| 179 |
+
|
| 180 |
+
Ideal para uso no UI enquanto não existe geração completa de dietas.
|
| 181 |
+
"""
|
| 182 |
+
base = tdee(profile)
|
| 183 |
+
calories = calorie_target_for_goal(base, goal, intensity)
|
| 184 |
+
prot_range = protein_recommendation(profile, goal)
|
| 185 |
+
# usar valor médio da faixa de proteína
|
| 186 |
+
protein_g_per_kg = (prot_range[0] + prot_range[1]) / 2
|
| 187 |
+
plan = macros_from_calories(calories, protein_g_per_kg=protein_g_per_kg, profile=profile)
|
| 188 |
+
plan.note = f"TDEE estimado: {base} kcal. Proteína target: {protein_g_per_kg:.2f} g/kg."
|
| 189 |
+
plan.tags = (goal, intensity)
|
| 190 |
+
return plan
|
| 191 |
+
|
| 192 |
+
|