Spaces:
Sleeping
Sleeping
| 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 ------------------- | |
| 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 ------------------- | |
| 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 | |