phi2-gym-bot / nutrition.py
MarvinRoque's picture
a
7869fa7
raw
history blame
16.2 kB
from __future__ import annotations
from dataclasses import dataclass, field, asdict
from typing import Optional, Dict, Tuple, Literal
import random
import json
import random
import logging
logging.basicConfig(level=logging.DEBUG)
# Carregar o JSON
with open("alimentos.json", "r", encoding="utf-8") as f:
alimentos_db = json.load(f)
# ------------------- Tipos -------------------
NivelAtividade = Literal[
"sedentário",
"leve",
"moderado",
"ativo",
"muito_ativo",
]
Objetivo = Literal["manutenção", "bulking", "cutting"]
Intensidade = Literal["leve", "moderado", "agressivo"]
# ------------------- Perfil do Usuário -------------------
@dataclass
class PerfilUsuario:
idade: int
sexo: Literal["homem", "mulher"]
peso_kg: float
altura_cm: float
atividade: NivelAtividade = "moderado"
percentual_gordura: Optional[float] = None
def __post_init__(self):
if self.peso_kg <= 0 or self.altura_cm <= 0:
raise ValueError("Peso e altura devem ser maiores que zero")
# ------------------- Plano Nutricional -------------------
@dataclass
class PlanoNutricional:
calorias_alvo: int
proteina_g: int
carboidratos_g: int
gorduras_g: int
nota: Optional[str] = None
tags: Tuple[str, ...] = field(default_factory=tuple)
def as_dict(self) -> Dict:
return asdict(self)
# ------------------- Fórmulas -------------------
def bmr_mifflin_st_jeor(perfil: PerfilUsuario) -> float:
"""Calcula BMR (Taxa Metabólica Basal) usando Mifflin-St Jeor."""
if perfil.sexo == "homem":
s = 5
else:
s = -161
bmr = 10 * perfil.peso_kg + 6.25 * perfil.altura_cm - 5 * perfil.idade + s
return float(bmr)
_FATORES_ATIVIDADE: Dict[NivelAtividade, float] = {
"sedentário": 1.2, # pouco ou nenhum exercício
"leve": 1.375, # exercício leve 1-3 dias/semana
"moderado": 1.55, # exercício moderado 3-5 dias/semana
"ativo": 1.725, # exercício intenso 6-7 dias/semana
"muito_ativo": 1.9, # trabalho físico pesado ou treino 2x/dia
}
def tdee(perfil: PerfilUsuario) -> int:
"""Calcula TDEE (gasto energético total)."""
bmr = bmr_mifflin_st_jeor(perfil)
fator = _FATORES_ATIVIDADE.get(perfil.atividade, 1.55)
return int(round(bmr * fator))
# ------------------- Metas calóricas -------------------
def calorias_para_objetivo(
tdee_base: int,
objetivo: Objetivo,
intensidade: Intensidade = "moderada"
) -> int:
ajustes = {
"bulking": {"leve": 1.05, "moderada": 1.10, "agressiva": 1.20},
"cutting": {"leve": 0.90, "moderada": 0.80, "agressiva": 0.70},
"manutenção": {"leve": 1.0, "moderada": 1.0, "agressiva": 1.0},
}
fator = ajustes[objetivo][intensidade]
return int(round(tdee_base * fator))
# ------------------- Macros -------------------
_CAL_POR_G = {
"proteina": 4,
"carboidrato": 4,
"gordura": 9,
}
def macros_por_calorias(
calorias: int,
proteina_g: Optional[float] = None,
proteina_g_por_kg: Optional[float] = None,
perfil: Optional[PerfilUsuario] = None,
proporcao_carbo: Optional[float] = None,
proporcao_gordura: Optional[float] = None
) -> PlanoNutricional:
if proteina_g is None:
if proteina_g_por_kg is None:
proteina_g_por_kg = 1.8
if perfil is None:
raise ValueError("Necessário fornecer `perfil` para calcular proteína por kg")
proteina_g = proteina_g_por_kg * perfil.peso_kg
proteina_g = max(0, proteina_g)
if proporcao_gordura is None and proporcao_carbo is None:
proporcao_gordura = 0.25
elif proporcao_gordura is None:
proporcao_gordura = max(
0.15,
1 - proporcao_carbo - (proteina_g * _CAL_POR_G["proteina"] / calorias)
)
calorias_proteina = proteina_g * _CAL_POR_G["proteina"]
calorias_gordura = int(round(calorias * proporcao_gordura))
gordura_g = calorias_gordura / _CAL_POR_G["gordura"]
calorias_restantes = calorias - (calorias_proteina + calorias_gordura)
carboidratos_g = max(0, calorias_restantes / _CAL_POR_G["carboidrato"]) if calorias_restantes > 0 else 0
return PlanoNutricional(
calorias_alvo=int(round(calorias)),
proteina_g=int(round(proteina_g)),
carboidratos_g=int(round(carboidratos_g)),
gorduras_g=int(round(gordura_g)),
)
def recomendacao_proteina(
perfil: PerfilUsuario,
objetivo: Objetivo = "manutenção"
) -> Tuple[float, float]:
baixo = 1.6
alto = 2.2
if objetivo == "cutting":
alto = 2.4
return (baixo, alto)
# ------------------- Micronutrientes -------------------
_MICRONUTRIENTES = {
"vitamina_d": "Importante para saúde óssea e imunidade; muitos têm défice.",
"ferro": "Crucial para transporte de oxigénio — atenção especial em mulheres.",
"cálcio": "Saúde óssea; combinar com vitamina D.",
"magnésio": "Recuperação muscular, sono e função neuromuscular.",
}
def info_micronutriente(nome: str) -> str:
return _MICRONUTRIENTES.get(nome.lower(), "Sem informação disponível.")
# ------------------- Suplementação -------------------
_SUPLEMENTOS = {
"whey": "Proteína de rápida digestão útil para atingir ingestão proteica.",
"creatina": "Creatina monohidratada: melhora força e desempenho; segura e estudada.",
"cafeína": "Melhora performance e foco; atenção à tolerância/sono.",
"ômega3": "Ácidos gordos essenciais benéficos para saúde cardiovascular.",
}
def recomendacao_suplemento(nome: str) -> str:
return _SUPLEMENTOS.get(nome.lower(), "Sem recomendação específica.")
# ------------------- Função principal -------------------
def construir_plano_basico(
perfil: PerfilUsuario,
objetivo: Objetivo = "manutenção",
intensidade: Intensidade = "moderada"
) -> PlanoNutricional:
tdee_base = tdee(perfil)
calorias = calorias_para_objetivo(tdee_base, objetivo, intensidade)
faixa_proteina = recomendacao_proteina(perfil, objetivo)
proteina_media = (faixa_proteina[0] + faixa_proteina[1]) / 2
plano = macros_por_calorias(calorias, proteina_g_por_kg=proteina_media, perfil=perfil)
plano.nota = f"TDEE estimado: {tdee_base} kcal. Proteína alvo: {proteina_media:.2f} g/kg."
plano.tags = (objetivo, intensidade)
return plano
# ------------------- Aliases para compatibilidade -------------------
UserProfile = PerfilUsuario
NutritionPlan = PlanoNutricional
build_basic_plan = construir_plano_basico
# Mapa de faixas calóricas para nº de refeições
_MAPA_REFEICOES = {
(0, 1800): 3,
(1801, 2500): 4,
(2501, 3200): 5,
(3201, float("inf")): 6,
}
# Mapa de nomes de refeições conforme nº de refeições
_NOMES_REFEICOES = {
3: ["matabicho", "almoço", "jantar"],
4: ["matabicho", "almoço", "lanche1", "jantar"],
5: ["matabicho", "lanche1", "almoço", "lanche2", "jantar"],
6: ["matabicho", "lanche1", "almoço", "lanche2", "jantar", "lanche3"],
}
def sugestao_refeicoes(plano: PlanoNutricional) -> Dict[str, Dict[str, int]]:
"""Sugere nº de refeições baseado no total calórico e distribui macros."""
calorias = plano.calorias_alvo
# Determinar nº de refeições pelo mapa
n_refeicoes = next(
refeicoes for (low, high), refeicoes in _MAPA_REFEICOES.items()
if low <= calorias <= high
)
# Obter nomes das refeições
nomes_refeicoes = _NOMES_REFEICOES[n_refeicoes]
# Distribuição simples e igualitária dos macros
macros_por_refeicao = {
"calorias": plano.calorias_alvo // n_refeicoes,
"proteina": plano.proteina_g // n_refeicoes,
"carboidrato": plano.carboidratos_g // n_refeicoes,
"gordura": plano.gorduras_g // n_refeicoes,
}
logging.debug(f"n refeicoes: {n_refeicoes}")
# Criar plano de refeições
refeicoes = {nome: macros_por_refeicao for nome in nomes_refeicoes}
return refeicoes
def sugerir_alimentos_para_refeicao(nome_refeicao, macros_ref, restricoes=None, usados=None):
restricoes = restricoes or []
usados = usados or set()
sugestao = []
calorias_restantes = macros_ref["calorias"]
prot_restante = macros_ref["proteina"]
carb_restante = macros_ref["carboidrato"]
gord_restante = macros_ref["gordura"]
# Limites por categoria
limites_categoria = {
"proteina_rica": (100, 350),
"proteina_extra": (30, 250),
"carboidrato": (50, 200),
"gordura": (15,120)
}
logging.debug(f"=== Início {nome_refeicao.upper()} ===")
# -------------------- 1. PROTEÍNA RICA --------------------
proteinas_rica = [
a for a in alimentos_db
if nome_refeicao in a.get("refeicao", [])
and a["macros"]["proteina"] >= 20
and a["categoria"] != "oleaginosa"
and a["nome"] not in usados
and not any(allerg in restricoes for allerg in a.get("alergias", []))
and not (nome_refeicao == "almoco" and a.get("categoria") == "sandes")
]
if proteinas_rica:
proteinas_rica = sorted(proteinas_rica, key=lambda x: x["macros"]["calorias"])
top_prot_rica = proteinas_rica[:7] if len(proteinas_rica) > 7 else proteinas_rica
prot_rica = random.choice(top_prot_rica)
qtd_g = prot_restante / prot_rica["macros"]["proteina"] * prot_rica["porcao"]
qtd_g = max(limites_categoria["proteina_rica"][0],
min(qtd_g, limites_categoria["proteina_rica"][1]))
fator = qtd_g / prot_rica["porcao"]
macros_add = {k: fator * v for k, v in prot_rica["macros"].items()}
sugestao.append({"alimento": prot_rica["nome"], "quantidade_g": round(qtd_g), "macros": macros_add})
usados.add(prot_rica["nome"])
calorias_restantes -= macros_add["calorias"]
prot_restante -= macros_add["proteina"]
carb_restante -= macros_add["carboidrato"]
gord_restante -= macros_add["gordura"]
logging.debug(f"Selecionou proteína RICA: {prot_rica['nome']} | {qtd_g:.1f}g | macros={macros_add}")
# -------------------- 2 & 3. CARBO + GORDURA --------------------
while calorias_restantes > 50:
# Carboidrato
carb_opcoes = [
a for a in alimentos_db
if nome_refeicao in a.get("refeicao", [])
and a["macros"]["carboidrato"] >= 10
and a["nome"] not in usados
and a["categoria"] != "oleaginosa"
and not any(allerg in restricoes for allerg in a.get("alergias", []))
]
if carb_opcoes:
carb_opcoes = sorted(carb_opcoes, key=lambda x: x["macros"]["calorias"])
top_carb = carb_opcoes[:7] if len(carb_opcoes) > 7 else carb_opcoes
carb = random.choice(top_carb)
qtd_g = carb_restante / carb["macros"]["carboidrato"] * carb["porcao"]
qtd_g = max(limites_categoria["carboidrato"][0],
min(qtd_g, limites_categoria["carboidrato"][1]))
fator = qtd_g / carb["porcao"]
macros_add = {k: fator * v for k, v in carb["macros"].items()}
sugestao.append({"alimento": carb["nome"], "quantidade_g": round(qtd_g), "macros": macros_add})
usados.add(carb["nome"])
calorias_restantes -= macros_add["calorias"]
prot_restante -= macros_add["proteina"]
carb_restante -= macros_add["carboidrato"]
gord_restante -= macros_add["gordura"]
# Gordura
gordura_opcoes = [
a for a in alimentos_db
if nome_refeicao in a.get("refeicao", [])
and a["macros"]["gordura"] >= 5
and a["nome"] not in usados
and not any(allerg in restricoes for allerg in a.get("alergias", []))
]
logging.debug(f"Gordura opções disponíveis antes do filtro: {len(gordura_opcoes)} itens")
for g in gordura_opcoes:
logging.debug(f" -> {g['nome']} | gord={g['macros']['gordura']} | categoria={g['categoria']} | usado={g['nome'] in usados}")
if gordura_opcoes and calorias_restantes > 50:
gordura_opcoes = sorted(gordura_opcoes, key=lambda x: x["macros"]["calorias"])
top_gord = gordura_opcoes[:7] if len(gordura_opcoes) > 7 else gordura_opcoes
gord = random.choice(top_gord)
qtd_g = gord_restante / gord["macros"]["gordura"] * gord["porcao"]
qtd_g = max(limites_categoria["gordura"][0],
min(qtd_g, limites_categoria["gordura"][1]))
fator = qtd_g / gord["porcao"]
macros_add = {k: fator * v for k, v in gord["macros"].items()}
sugestao.append({"alimento": gord["nome"], "quantidade_g": round(qtd_g), "macros": macros_add})
usados.add(gord["nome"])
calorias_restantes -= macros_add["calorias"]
prot_restante -= macros_add["proteina"]
carb_restante -= macros_add["carboidrato"]
gord_restante -= macros_add["gordura"]
logging.debug(f"Selecionou gordura: {gord['nome']} | {qtd_g:.1f}g | macros={macros_add}")
else:
logging.debug("Nenhuma opção de gordura disponível ou calorias_restantes <= 200")
break
# -------------------- 4. AJUSTE FINAL --------------------
total_cal = sum(item["macros"]["calorias"] for item in sugestao)
dif_cal = macros_ref["calorias"] - total_cal
# Ajuste proporcional se faltar calorias com gordura
if dif_cal > 0:
gorduras_disponiveis = [item for item in sugestao if item["macros"].get("gordura", 0) > 0]
if gorduras_disponiveis:
gord_item = random.choice(gorduras_disponiveis)
cal_gordura_por_g = 9
qtd_extra = min(dif_cal / cal_gordura_por_g, 60) # limite de 60g
original_g = gord_item["quantidade_g"]
gord_item["quantidade_g"] += round(qtd_extra)
fator = gord_item["quantidade_g"] / original_g
for k in gord_item["macros"]:
gord_item["macros"][k] *= fator
total_cal = sum(item["macros"]["calorias"] for item in sugestao)
dif_cal = macros_ref["calorias"] - total_cal
# Se ainda faltar calorias, adiciona bebida ajustada
if dif_cal > 0:
bebidas = [a for a in alimentos_db if a.get("categoria") == "bebida" and a["nome"] not in usados]
if bebidas:
bebida = random.choice(bebidas)
cal_por_g = bebida["macros"]["calorias"] / bebida["porcao"]
qtd_g = round(dif_cal / cal_por_g)
qtd_g = max(50, min(qtd_g, 250)) # limitar a quantidade plausível
macros_add = {k: (v * qtd_g / bebida["porcao"]) for k, v in bebida["macros"].items()}
sugestao.append({"alimento": bebida["nome"], "quantidade_g": qtd_g, "macros": macros_add})
usados.add(bebida["nome"])
return sugestao
def gerar_plano_diario(plano: PlanoNutricional, n_refeicoes: Optional[int] = None):
# Determinar nº de refeições automaticamente se não for passado
if n_refeicoes is None:
refeicoes_macros = sugestao_refeicoes(plano)
else:
# nomes fixos de refeições
nomes_refeicoes = ["matabicho", "lanche1", "almoco", "lanche2", "jantar", "lanche3"][:n_refeicoes]
macros_base = {
"calorias": plano.calorias_alvo // len(nomes_refeicoes),
"proteina": plano.proteina_g // len(nomes_refeicoes),
"carboidrato": plano.carboidratos_g // len(nomes_refeicoes),
"gordura": plano.gorduras_g // len(nomes_refeicoes),
}
refeicoes_macros = {r: macros_base for r in nomes_refeicoes}
logging.debug(f"Refeições e macros: {refeicoes_macros}")
plano_diario = {}
usados = set() # evita repetir alimentos
for ref, macros in refeicoes_macros.items():
sugestao = sugerir_alimentos_para_refeicao(
ref,
macros,
restricoes=[],
usados=usados,
)
plano_diario[ref] = sugestao
return plano_diario