phi2-gym-bot / app.py
MarvinRoque's picture
Update app.py
61e0568
import gradio as gr
from transformers import AutoModelForCausalLM, AutoTokenizer
from sentence_transformers import SentenceTransformer, util
import torch
import torch.nn.functional as F
import unicodedata
import json
import re
import random
from nutrition import UserProfile, build_basic_plan, gerar_plano_diario, formatar_plano_nutricional
# Carregar o JSON
with open("exercicios.json", "r", encoding="utf-8") as f:
exercicios_db = json.load(f)
# -------------------------
# Config
# -------------------------
EMBEDDING_MODEL = "rufimelo/bert-large-portuguese-cased-sts"
LLM_MODEL = "TucanoBR/Tucano-2b4-Instruct"
THRESHOLD = 0.50 # score mínimo para aceitar como fitness
KEYWORD_WEIGHT = 0.15 # peso por conceito identificado
MAX_KEYWORD_BONUS = 0.60 # limite do bônus por conceitos
KW_SIM_THRESHOLD = 0.45 # similaridade para considerar conceito detectado
MUSCLE_SIM_THRESHOLD = 0.75 # similaridade para considerar músculo detectado via embedding
# -------------------------
# Normalização
# -------------------------
def normalize_text(text: str) -> str:
if text is None:
return ""
text = unicodedata.normalize("NFD", text)
text = "".join(ch for ch in text if unicodedata.category(ch) != "Mn")
return text.lower().strip()
# -------------------------
# Carregamento de modelos
# -------------------------
embedder = SentenceTransformer(EMBEDDING_MODEL)
tokenizer = AutoTokenizer.from_pretrained(LLM_MODEL, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
LLM_MODEL,
torch_dtype=torch.float32,
device_map=None # 👈 evita tentar usar offload para "disk"
).to("cpu")
# -------------------------
# Domínio fitness (frases representativas)
# -------------------------
fitness_domains = [
"exercícios de musculação",
"treino de academia",
"programa de treino para ganhar força e massa muscular",
"condicionamento físico, resistência, explosividade e velocidade",
# treino por grupo (inclui panturrilha)
"exercícios para pernas, glúteos e panturrilhas",
"exercícios para costas e bíceps",
"exercícios para peito e tríceps",
"treino de abdômen e core",
"treino de ombros e trapézio",
"treino de antebraços",
"treino de panturrilhas",
"treino de corpo inteiro",
"treino funcional para atletas",
# nutrição
"dieta para ganhar massa muscular",
"dieta para emagrecimento",
"alimentação pré e pós treino",
"suplementação para hipertrofia",
"suplementação para recuperação muscular",
"planejamento alimentar para atletas",
# recuperação
"recuperação e descanso muscular",
"sono e desempenho esportivo",
"alongamento e aquecimento antes do treino",
# saúde e prevenção
"prevenção de lesões articulares e tendíneas",
"treino adaptado para lesão no joelho",
"treino adaptado para lesão no ombro",
"treino adaptado para lesão na lombar",
"treino adaptado para lesão no quadril",
"treino adaptado para lesão no tornozelo",
"fisioterapia e reabilitação esportiva"
]
# -------------------------
# Conceitos (keywords agrupadas)
# -------------------------
concept_keywords = {
"treino": ["treino", "treinar", "treinos", "workout", "malhar", "musculacao", "musculação", "gym"],
"hipertrofia": ["hipertrofia", "ganhar massa", "massa muscular"],
"força": ["forca", "força", "ganho de força", "explosividade"],
"resistência": ["resistencia", "resistência", "condicionamento", "cardio"],
"dieta": ["dieta", "alimentacao", "alimentação", "plano alimentar", "nutrição", "nutricao","emagrecer", "perder peso", "cutting", "secar"],
"suplementos": ["suplemento", "suplementos", "creatina", "whey", "proteina", "proteína", "bcaa", "pre treino", "pré treino", "pos treino", "pós treino"],
"recuperação": ["recuperacao", "recuperação", "descanso", "sono", "alongamento", "aquecimento"],
"lesões": ["lesao", "lesão", "lesoes", "lesões", "joelho", "ombro", "lombar", "coluna", "tendinite", "fisioterapia", "reabilitação", "reabilitacao"],
"estratégias": ["divisao de treino", "divisão de treino", "periodizacao", "periodização", "circuito", "hiit", "fullbody", "corpo inteiro"],
"cardio": ["corrida", "correr", "bicicleta", "bike", "esteira", "natação", "natacao"]
}
# -------------------------
# Grupos musculares (keywords)
# -------------------------
muscle_keywords = {
"pernas": ["perna", "pernas", "inferiores", "lower body", "treino inferior", "leg day", "legday", "leg"],
"quadriceps": ["quadriceps", "quads", "coxa da frente", "frontal da coxa"],
"posterior_de_coxa": ["posterior", "posterior de coxa", "isquiotibiais", "hamstrings"],
"gluteo": ["gluteo", "gluteos", "bumbum", "gluteus"],
"panturrilhas": ["panturrilha", "panturrilhas", "batata da perna", "gastrocnemio", "soleo"],
"costas": ["costas", "costa"],
"dorsal": ["dorsal", "latissimo", "lats", "latissimus", "latissimus dorsi", "dorso"],
"lombar": ["lombar", "parte baixa das costas", "erectores", "eretores da espinha"],
"trapezio": ["trapezio", "trapezio", "pescoço largo"],
"peito": ["peito", "peitoral", "chest"],
"bracos": ["braco", "braco", "bracos", "braços", "arm", "arms", "treino de bracos", "treino de braços"],
"biceps": ["biceps", "biceps", "bíceps"],
"triceps": ["triceps", "triceps", "tríceps"],
"antebraco": ["antebraco", "antebraco", "antebracos", "antebraços", "forearm"],
"ombro": ["ombro", "ombros", "deltoide", "deltoides", "shoulder"],
"abdomen": ["abdomen", "abdominal", "reto abdominal", "abs"],
"obliquos": ["obliquos", "obliquo", "obliquo"],
"core": ["core", "centro do corpo", "estabilizadores"],
"superiores": ["superior", "superiores", "upper body", "treino superior"],
"puxar": ["puxar", "puxada", "puxadas", "pull"],
"empurrar": ["empurrar", "empurrada", "empurradas", "push"],
}
# Expansão de grupos compostos
group_hierarchy = {
"pernas": ["quadriceps", "posterior_de_coxa", "gluteo", "panturrilhas"],
"costas": ["lombar", "trapezio","dorsal"],
"superiores": ["peito", "dorsal", "trapezio", "biceps", "triceps", "ombro", "antebraco"],
"bracos": ["biceps", "triceps", "antebraco"],
"puxar": ["biceps", "dorsal", "lombar", "trapezio", "antebraco"],
"empurrar": ["triceps", "peito", "ombro"]
}
# -------------------------
# Lesões (keywords)
# -------------------------
lesao_context_keywords = [
"dor", "dói","doi", "doe", "magoado", "magoada", "lesao", "lesoes", "lesoes", "rompido", "lesionado",
"inflamado", "inflamacao", "luxacao", "ruptura", "tendinite", "entorse",
"condromalacia", "bursite", "hernia", "hernia",
"machuquei", "machucou", "machucada", "machucado", "puxei", "puxado"
]
lesao_keywords = {
"joelho": [
"joelho", "ligamento cruzado", "lca", "menisco", "condromalacia",
"joelho direito", "joelho esquerdo", "torci o joelho"
],
"ombro": [
"ombro", "manguito", "manguito rotador", "luxação de ombro",
"tendinite no ombro", "ombro direito", "ombro esquerdo"
],
"lombar": [
"lombar", "coluna lombar", "coluna", "hernia lombar", "hérnia lombar",
"hernia de disco", "hérnia de disco", "ciática", "dor nas costas"
],
"quadril": [
"quadril", "artrose no quadril", "bursite no quadril", "quadril direito", "quadril esquerdo"
],
"tornozelo": [
"tornozelo", "entorse de tornozelo", "lesão no tornozelo", "torci o tornozelo"
],
"cotovelo": [
"cotovelo", "epicondilite", "tennis elbow", "cotovelo de tenista", "cotovelo de golfista"
],
"punho": [
"punho", "síndrome do túnel do carpo", "punho dolorido", "punhos"
],
}
def detectar_lesoes(texto: str) -> list[str]:
texto = texto.lower()
# 1️⃣ Verifica se existe algum contexto de lesão
if not any(k in texto for k in lesao_context_keywords):
return []
# 2️⃣ Só então procura as articulações/problemas
detectadas = []
for lesao, termos in lesao_keywords.items():
for termo in termos:
if termo in texto:
detectadas.append(lesao)
break
detectadas = list(set(detectadas))
return detectadas
def is_safe_for_lesoes(exercicio, lesoes: list[str]) -> bool:
"""
Retorna False se o exercício tiver intensidade 'alta'
em alguma articulação lesionada.
"""
if not lesoes:
return True # sem lesão, tudo liberado
for lesao in lesoes:
if lesao in exercicio.get("intensidade_articulacao", {}):
intensidade = exercicio["intensidade_articulacao"][lesao]
if intensidade == "alta":
return False
return True
def escolher_variacao(ex, lesoes):
"""
Se não houver lesão, retorna variação aleatória.
Se houver, tenta priorizar variações de menor impacto/custo.
"""
variacoes = ex["variacoes"]
if not lesoes:
return random.choice(variacoes)
# 🎯 Priorizando custo menor (proxy para menor impacto articular)
variacoes_ordenadas = sorted(variacoes, key=lambda v: v["custo"])
return variacoes_ordenadas[0]
# -------------------------
# Pré-calcular embeddings (normalize)
# -------------------------
fitness_embeddings = embedder.encode([normalize_text(s) for s in fitness_domains],
convert_to_tensor=True)
fitness_embeddings = F.normalize(fitness_embeddings, p=2, dim=1)
# concept embeddings: média das palavras do conceito
concept_embeddings = {}
for concept, words in concept_keywords.items():
emb = embedder.encode([normalize_text(w) for w in words], convert_to_tensor=True)
emb = F.normalize(emb, p=2, dim=1)
concept_embeddings[concept] = torch.mean(emb, dim=0, keepdim=True)
# muscle embeddings: média das palavras do músculo
muscle_embeddings = {}
muscle_keywords_norm = {}
for muscle, words in muscle_keywords.items():
words_norm = [normalize_text(w) for w in words]
muscle_keywords_norm[muscle] = words_norm
emb = embedder.encode(words_norm, convert_to_tensor=True)
emb = F.normalize(emb, p=2, dim=1)
muscle_embeddings[muscle] = torch.mean(emb, dim=0, keepdim=True)
# -------------------------
# Helpers: detectar conceitos e músculos
# -------------------------
def detectar_conceitos(prompt: str):
"""
Detecta conceitos e intenções dentro do domínio fitness.
"""
prompt_norm = normalize_text(prompt or "")
if not prompt_norm:
return []
# ----------------------------------------
# 0️⃣ Embedding base do prompt
# ----------------------------------------
prompt_emb = embedder.encode([prompt_norm], convert_to_tensor=True)
prompt_emb = F.normalize(prompt_emb, p=2, dim=1)
conceitos_detectados = []
def add_conceito(tipo, score, source, subtipo=None):
conceito = {
"tipo": tipo,
"subtipo": subtipo or "generico",
"score": score,
"source": source
}
conceitos_detectados.append(conceito)
# ----------------------------------------
# 1️⃣ Regex base
# ----------------------------------------
padroes = {
# 🏋️ TREINO
"treino": (
r"\b("
r"treino|treinos|treinar|treinando|treinei|"
r"malhar|malho|malhando|malhei|"
r"academia|academias|"
r"muscul[aã]o|muscula[cç][aã]o|musculacao|"
r"exerc[ií]cio|exerc[ií]cios|exercicio|exercicios|"
r"for[cç]a|forcas|forças|"
r"resist[eê]ncia|resistencia|resistencias|"
r"hipertrofia|hipertrofias|hipertrofico|hipertrofica|hipertrofique|"
r"condicionamento|condicionado|"
r"cardio|alongamento|alongar|aquecimento|aquecer|"
r"musculo|músculo|musculos|músculos"
r")\b"
),
# 🍽️ NUTRIÇÃO
"nutricao": (
r"\b("
r"dieta|dietas|dietar|"
r"aliment[aç][aã]o|alimentacao|alimenta[cç][aã]o|"
r"plano alimentar|planos alimentares|"
r"nutri[cç][aã]o|nutricao|nutricional|nutricionista|nutricionistas|"
r"emagrecer|emagrecimento|emagre[cç]a|"
r"ganhar massa|ganho de massa|massa magra|massa muscular|"
r"cutting|bulking|"
r"suplemento|suplementos|suplementar|suplementa[cç][aã]o|suplementacao|"
r"refei[cç][aã]o|refei[cç][oõ]es|refeicao|refeicoes|"
r"macro|macros|macronutriente|macronutrientes|"
r"prote[ií]na|proteinas|carboidrato|carboidratos|gordura|gorduras|"
r"caloria|calorias|cal[oó]rico|cal[oó]rica|cal[oó]ricas"
r")\b"
),
}
for tipo, regex in padroes.items():
if re.search(regex, prompt_norm):
add_conceito(tipo, 1.0, "regex-base")
# ----------------------------------------
# 2️⃣ Subtipos
# ----------------------------------------
# ---------- TREINO ----------
if re.search(
r"\b("
r"split|splits|"
r"dividido|divididos|divis[aã]o|divisoes|"
r"treino dividido|treinos divididos|"
r"treino semanal|treinos semanais|"
r"rotina semanal|rotinas semanais|"
r"rotina de treino|rotina de treinos|"
r"planejamento semanal|programa semanal|"
r"dividir treino|dividir os treinos|"
r"estrutura de treino|organiza[cç][aã]o de treino|"
r"abc|abc[ddef]?|abcde|abcd|abcd[eé]?|"
r"push pull legs|push/pull/legs|upper lower|upper/lower|full body split|"
r"plano\s*(de\s*)?treino|programa\s*(de\s*)?treino|"
r"treino\s*\d+\s*(vezes|dias)\s*(na|por)?\s*semana|"
r"rotina\s*\d+\s*dias|"
r"programa\s*\d+\s*dias|"
r"treino\s*(di[aá]rio|semanal)|"
r"plano\s*inicial|"
r"plano\s*completo|"
r"plano\s*estruturado|"
r"plano\s*de\s*exerc[ií]cios?|"
r"treino\s*inicial|"
r"treino\s*para\s*iniciantes?|"
r"sugest(ão|oes)\s*de\s*treino|"
r"sugira\s*treino|" # ← TROQUEI VÍRGULA POR |
r"plano.*treino|treino.*plano" # ← AGORA É UMA STRING SÓ
r")\b",
prompt_norm,
flags=re.IGNORECASE
):
add_conceito("treino", 1.0, "regex", "split")
elif re.search(
r"\b("
# 🔹 Expressões gerais
r"treino\s*(leve|moderado|pesado)|"
r"isolado|isolados|único|unico|individual|focado|espec[ií]fico|"
r"treino\s*(de|para)\s*[a-zçã]+|"
r"exerc[ií]cios?\s*(para|de)\s*[a-zçã]+|"
r"passa\s*um\s*treino\s*(para|de)\s*[a-zçã]+|"
r"da\s*um\s*treino\s*(para|de)\s*[a-zçã]+|"
r"quero\s*treinar\s*[a-zçã]+|"
r"preciso\s*de\s*um\s*treino\s*(para|de)\s*[a-zçã]+|"
r"trabalhar\s*(o|a|os|as)?\s*[a-zçã]+|"
r"focar\s*(em|no|na|nos|nas)\s*[a-zçã]+|"
r"parte\s*(superior|inferior|do\s*corpo)|"
r"upper\s*body|lower\s*body|leg\s*day|arm\s*day|push\s*day|pull\s*day|core\s*day|abs\s*day|"
# 🔹 Grupos musculares principais
r"peito|peitoral|peitorais|"
r"costas|dorsal|dorsais|lats?|"
r"ombro|ombros|deltoide?s?|"
r"bra[cç]o|bra[cç]os|b[ií]ceps|tr[ií]ceps|antebra[cç]o|antebra[cç]os|"
r"perna|pernas|quadr[ií]ceps|posterior\s*de\s*coxa|isquiotibiais?|"
r"gl[uú]teo|gl[uú]teos|bumbum|"
r"abd[oô]men|abdominais?|core|"
r"panturrilha|panturrilhas?|g[eé]meos?|"
r"trap[eé]zio|trap[eé]zios|pesco[cç]o|pesco[cç]os|"
r"lombar|lombares|"
r"coxa|coxas|"
r"posterior|posteriores|"
r"inferior|superior"
r")\b",
prompt_norm,
):
add_conceito("treino", 1.0, "regex", "isolado")
elif re.search(
r"\b("
# 🔹 Estruturas clássicas de pergunta
r"o\s*que\s*(é|e|seria|significa)?|"
r"qual(?:\s*é|\s*são)?|"
r"como\s*(fa[cz]|devo|posso|fazer|montar|melhorar|aumentar|iniciar)?|"
r"quando\s*(devo|é|seria|come[çc]ar)?|"
r"por\s*que|pq|porque|pra\s*que|para\s*que|"
r"quanto\s*(tempo|peso|descanso|repouso|volume|frequ[eê]ncia|dias)?|"
r"quantas?\s*(vezes|repeti[cç][oõ]es|s[eé]ries|dias)?|"
r"d[eê]vo|posso|preciso|necess[aá]rio|vale\s*a\s*pena|funciona|serve|ajuda|eficaz|eficiente|"
r"tem\s*problema|faz\s*mal|faz\s*bem|melhor\s*jeito|maneira\s*correta|jeito\s*certo|forma\s*certa|"
# 🔹 Expressões diretas sobre treino
r"dicas?\s*(de|para)\s*treino|"
r"sugest(ão|oes)\s*(de|para)\s*treino|"
r"melhor\s*(treino|forma|maneira)|"
r"diferen[çc]a\s*(entre|do|da)\s*treino|"
r"comparar\s*treino|"
r"vale\s*a\s*pena\s*fazer\s*treino|"
r"tipo\s*(de|de\s*)?treino|"
r"recomenda[çc][aã]o\s*(de|para)\s*treino|"
r"o\s*melhor\s*exerc[ií]cio|"
r"o\s*melhor\s*para\s*emagrecer|"
r"o\s*melhor\s*para\s*ganhar\s*massa|"
r"como\s*saber\s*se\s*meu\s*treino\s*est[aá]\s*certo|"
r"quais\s*s[aã]o\s*os\s*melhores\s*exerc[ií]cios|"
r"qual\s*o\s*melhor\s*hor[aá]rio\s*para\s*treinar"
r")\b",
prompt_norm,
):
add_conceito("treino", 0.9, "regex", "pergunta")
# ---------- NUTRIÇÃO ---------
# Caso específico: "sugestões de treino e alimentação" ou "sugira treino e dieta"
if re.search(
r"\b(sugest[õo]es|sugest[ãa]o|sugira)\s+(de\s+)?treino\s+e\s+(alimenta[cç][aã]o|dieta)\b",
prompt_norm,
flags=re.IGNORECASE
):
add_conceito("treino", 1.0, "regex", "split")
add_conceito("nutricao", 1.0, "regex", "plano")
# Caso específico: "treino e alimentação" ou "treino e dieta"
elif re.search(
r"\b(treino\s+e\s+(alimenta[cç][aã]o|dieta)|(alimenta[cç][aã]o|dieta)\s+e\s+treino)\b",
prompt_norm,
flags=re.IGNORECASE
):
add_conceito("treino", 1.0, "regex", "split")
add_conceito("nutricao", 1.0, "regex", "plano")
# CASO NOVO: "Plano semanal de treino e refeições"
elif re.search(
r"\bplano\s*(semanal|mensal|di[aá]rio)?\s*(de\s+)?treino\s+e\s+refei[cç][oõ]es\b",
prompt_norm,
flags=re.IGNORECASE
):
add_conceito("treino", 1.0, "regex", "split")
add_conceito("nutricao", 1.0, "regex", "plano")
# Padrões existentes de plano alimentar
elif re.search(
r"\b("
# --- Expressões diretas de plano alimentar ---
r"plano\s*alimentar|plano\s*de\s*dieta|"
r"card[aá]pio|menu\s*di[aá]rio|"
r"dieta\s*estruturada|dieta\s*completa|"
# --- Pedidos com "dieta" ou "alimentação" ou "refeições" ---
r"(sugest[ãa]o|sugest[õo]es|sugira)\s*de\s*(dieta|alimenta[cç][aã]o|refei[cç][oõ]es)|"
r"(cria|criar|monte|montar|passa|passar|faz|fa[çc]a)\s*(um[a]?)?\s*(dieta|alimenta[cç][aã]o|refei[cç][oõ]es)|"
r"preciso\s*de\s*um\s*(dieta|alimenta[cç][aã]o|refei[cç][oõ]es)|"
r"quero\s*um\s*(dieta|alimenta[cç][aã]o|refei[cç][oõ]es)|"
r"gostaria\s*de\s*um\s*(dieta|alimenta[cç][aã]o|refei[cç][oõ]es)|"
# --- "Plano" junto com termos de nutrição ---
r"plano.*(dieta|alimenta[cç][aã]o|refei[cç][oõ]es)|"
r"(dieta|alimenta[cç][aã]o|refei[cç][oõ]es).*plano"
r")\b",
prompt_norm,
flags=re.IGNORECASE
):
add_conceito("nutricao", 1.0, "regex", "plano")
elif re.search(
r"\b("
# 🔹 Estruturas clássicas de pergunta
r"o\s*que\s*(é|e|seria|significa)?|"
r"qual(?:\s*é|\s*são)?|"
r"como\s*(fa[cz]|devo|posso|montar|seguir|melhorar|fazer)?|"
r"quando\s*(devo|é|seria|come[çc]ar)?|"
r"por\s*que|pq|porque|pra\s*que|para\s*que|"
r"quanto\s*(tempo|peso|caloria|prote[ií]na|carboidrato|gordura|macro|macros)?|"
r"quantas?\s*(refei[cç][oõ]es|vezes|gramas)?|"
r"d[eê]vo|posso|preciso|necess[aá]rio|vale\s*a\s*pena|funciona|serve|ajuda|eficaz|eficiente|"
r"tem\s*problema|faz\s*mal|faz\s*bem|melhor\s*jeito|maneira\s*correta|forma\s*certa|"
# 🔹 Expressões diretas sobre dieta e nutrição
r"dicas?\s*(de|para)\s*(dieta|aliment[aç][aã]o|comer)|"
r"sugest(ão|oes)\s*(de|para)\s*dieta|"
r"melhor\s*(dieta|forma|maneira|estrat[eé]gia)|"
r"diferen[çc]a\s*(entre|do|da)\s*dieta|"
r"comparar\s*dieta|"
r"vale\s*a\s*pena\s*fazer\s*dieta|"
r"tipo\s*(de|de\s*)?dieta|"
r"recomenda[çc][aã]o\s*(de|para)\s*dieta|"
r"o\s*melhor\s*alimento|"
r"o\s*melhor\s*para\s*emagrecer|"
r"o\s*melhor\s*para\s*ganhar\s*massa|"
r"como\s*saber\s*se\s*minha\s*dieta\s*est[aá]\s*certa|"
r"quais\s*s[aã]o\s*os\s*melhores\s*alimentos|"
r"qual\s*o\s*melhor\s*hor[aá]rio\s*para\s*comer|"
r"quanto\s*de\s*(prote[ií]na|carboidrato|gordura|caloria)\s*(por\s*dia|devo\s*comer)"
r")\b",
prompt_norm,
):
add_conceito("nutricao", 0.9, "regex", "pergunta")
# ----------------------------------------
# 3️⃣ Embeddings — reforço semântico
# ----------------------------------------
sims = util.cos_sim(prompt_emb, fitness_embeddings)[0]
max_idx = int(torch.argmax(sims))
max_score = float(sims[max_idx].item())
domain_str = fitness_domains[max_idx].lower()
if max_score >= THRESHOLD:
tipo = None
subtipo = "generico"
# --- Detecta tipo principal
if any(k in domain_str for k in ["treino", "muscul", "força", "resist", "explosiv", "aeróbico", "cardio"]):
tipo = "treino"
elif any(k in domain_str for k in ["dieta", "nutri", "aliment", "suplement", "refeição", "caloria", "macro"]):
tipo = "nutricao"
# --- Detecta subtipo aproximado via contexto semântico
if tipo == "treino":
if any(k in domain_str for k in ["split", "semana", "abc", "rotina", "divisão"]):
subtipo = "split"
elif any(k in domain_str for k in ["isolado", "grupo", "bíceps", "costas", "ombro", "peito", "perna", "glúteo", "tríceps", "abdômen"]):
subtipo = "isolado"
elif any(k in domain_str for k in ["como", "quantas", "diferença", "melhor treino", "dicas"]):
subtipo = "pergunta"
elif tipo == "nutricao":
if any(k in domain_str for k in ["plano", "alimentar", "cardápio", "refeição", "menu", "rotina", "dieta personalizada"]):
subtipo = "plano"
elif any(k in domain_str for k in ["como", "quanto", "por que", "diferença", "dicas", "melhor", "horário", "refeições"]):
subtipo = "pergunta"
if tipo:
add_conceito(tipo, max_score, "embeddings", subtipo)
# ----------------------------------------
# 5️⃣ Pós-processamento — prioriza subtipos específicos
# ----------------------------------------
def filtrar_especificos(conceitos):
prioridade = {"generico": 0, "pergunta": 1, "isolado": 2, "split": 3, "plano": 3}
escolhidos = {}
for c in conceitos:
tipo = c["tipo"]
atual = escolhidos.get(tipo)
if not atual or prioridade.get(c["subtipo"], 0) > prioridade.get(atual["subtipo"], 0):
escolhidos[tipo] = c
return list(escolhidos.values())
conceitos_detectados = filtrar_especificos(conceitos_detectados)
return conceitos_detectados
def detectar_musculos(texto: str) -> list[str]:
if not texto:
return []
texto_norm = normalize_text(texto)
# build term -> list(muscle_key)
term_map = {}
for muscle_key, terms in muscle_keywords.items():
for t in terms:
tn = normalize_text(t)
term_map.setdefault(tn, []).append(muscle_key)
# sort terms by length desc to prefer multiword matches first
sorted_terms = sorted(term_map.items(), key=lambda x: len(x[0]), reverse=True)
detected = set()
for term, muscles_for_term in sorted_terms:
# use word-boundary-aware search for the term (term may contain spaces)
pattern = r"\b" + re.escape(term) + r"\b"
if re.search(pattern, texto_norm):
for m in muscles_for_term:
detected.add(m)
# Expansão: se um grupo composto detectado, substitui pelo(s) subgrupo(s)
# (somente adiciona subgrupos que existem como chaves em muscle_keywords)
expanded = set()
for grupo, subgrupos in group_hierarchy.items():
if grupo in detected:
# remove the group and add each subgrupo if it's a known muscle key
# (defensive: only add subgroups that appear in muscle_keywords)
for s in subgrupos:
if s in muscle_keywords:
expanded.add(s)
# don't add the original group
detected.discard(grupo)
detected.update(expanded)
# Hierarquia reversa: se um grupo e seus subgrupos estiverem presentes, priorizar subgrupos
for grupo, subgrupos in group_hierarchy.items():
if grupo in detected and any(s in detected for s in subgrupos):
detected.discard(grupo)
# retorno ordenado para testes determinísticos
return sorted(detected)
# -------------------------
# Objetivos (keywords)
# -------------------------
objetivo_keywords = {
"hipertrofia": [
"hipertrofia", "massa", "crescimento muscular", "ganhar tamanho",
"volume", "aumentar músculos", "ficar maior", "crescer",
"ganhar corpo", "musculação", "muscle growth"
],
"forca": [
"força", "forca", "powerlifting", "power", "pesado", "ganhar força",
"melhorar força", "ficar mais forte", "maximo", "1rm", "força máxima",
"força bruta", "strength", "strong", "stronger"
],
"condicionamento": [
"resistência", "resistencia", "condicionamento", "endurance",
"cardio", "alta repeticao", "repetições altas", "definição",
"tonificar", "cutting", "resistente", "leve"
],
"explosividade": [
"explosivo", "explosividade", "pliometria", "saltar", "sprints",
"potência", "potencia", "explosão", "velocidade", "agilidade", "power",
"rápido", "rapido", "quickness", "potente"
],
}
def detectar_objetivos(texto: str) -> list[str]:
if not texto:
return ["hipertrofia"]
texto = normalize_text(texto)
objetivos_detectados = []
for objetivo, termos in objetivo_keywords.items():
for termo in termos:
termo_norm = normalize_text(termo)
if termo_norm in texto:
objetivos_detectados.append(objetivo)
break
if not objetivos_detectados:
return ["hipertrofia"]
return sorted(set(objetivos_detectados))
def detectar_intencao(prompt_norm: str, musculos_detectados: list[str], dados_usuario: dict):
"""
Retorna:
("split", dias) -> se detectar pedido de divisão semanal (baseado em dados_usuario)
("isolado", musculos) -> se detectar treino de músculos específicos
Regras:
- nivel_atividade:
leve → 2 dias
moderado → 3 dias
ativo → aleatório entre 4 e 5
muito ativo → 6 dias
- Se houver músculos detectados → treino isolado
- Caso contrário → full body padrão
"""
texto = normalize_text(prompt_norm or "")
# 🔹 Determinar dias com base no nível de atividade do usuário
nivel = (dados_usuario.get("atividade"))
dias = None
if "leve" in nivel:
dias = 2
elif "moderado" in nivel:
dias = 3
elif "ativo" in nivel and "muito" not in nivel:
dias = random.choice([4, 5])
elif "muito_ativo" in nivel or "intenso" in nivel:
dias = 6
# 🔹 Se conseguimos determinar os dias → retorna split
if dias is not None:
return "split", dias
# # 🔹 Se mencionou treino semanal no texto, também retorna split
# padrao_split = re.search(r"\b(\d+)\s*(x|vezes|dias)(\s*(por|na|em)?\s*(semana|semanais)?)?\b", texto)
# if padrao_split:
# try:
# dias_detectado = int(padrao_split.group(1))
# if 1 <= dias_detectado <= 7:
# return "split", dias_detectado
# except ValueError:
# pass # ignora se não for número válido
# 🔹 Caso tenha músculos específicos, prioriza treino isolado
if musculos_detectados:
return "isolado", musculos_detectados
# 🔹 Default → treino full body
full_body = ["peito", "costas", "ombro", "biceps", "triceps", "pernas", "core"]
return "isolado", full_body
def montar_treino(musculos_alvo, budget=45, objetivos=["hipertrofia"], lesoes=[]):
treino = []
custo_total = 0
usados = set()
musculos_cobertos = set()
# 🔹 Pré-filtrar exercícios seguros
exercicios_validos = [ex for ex in exercicios_db if is_safe_for_lesoes(ex, lesoes)]
# 🔹 Se "explosividade" NÃO está nos objetivos, remove exercícios pliométricos
if "explosividade" not in objetivos:
exercicios_validos = [ex for ex in exercicios_validos if not ex.get("pliometrico", False)]
# 1️⃣ Faixas de repetições por objetivo
faixas_reps = {
"hipertrofia": (6, 15),
"forca": (2, 5),
"condicionamento": (15, 50),
"explosividade": (5, 12)
}
def escolher_reps(objetivo):
faixa = faixas_reps.get(objetivo, (8, 12))
return random.randint(*faixa)
def add_exercicio(ex, variacao, series, objetivo_escolhido):
nonlocal custo_total
custo_ex = variacao["custo"] * series
reps = escolher_reps(objetivo_escolhido)
if ex["nome"] in usados:
return False
if custo_total + custo_ex <= budget:
descricao_final = variacao["descricao"]
if objetivo_escolhido == "explosividade" and not ex.get("pliometrico", False):
if ex.get("equipamento") == "peso_livre":
descricao_final += " (executar com carga moderada e máxima velocidade)"
else:
return False
treino.append({
"nome": ex["nome"],
"descricao": descricao_final,
"series": series,
"reps": reps,
"custo_total": custo_ex,
"custo_unit": variacao["custo"],
"video": variacao["video"],
"objetivo": objetivo_escolhido,
"musculos": ex["musculos"]
})
custo_total += custo_ex
usados.add(ex["nome"])
musculos_cobertos.update(ex["musculos"])
return True
return False
# 2️⃣ Multiarticulado principal
candidatos_multi = []
for ex in exercicios_validos:
if any(m in ex["musculos"] for m in musculos_alvo):
for v in ex["variacoes"]:
if v["custo"] == 5:
cobertura = len(set(ex["musculos"]) & set(musculos_alvo))
candidatos_multi.append((ex, v, cobertura))
if candidatos_multi:
candidatos_multi.sort(key=lambda x: x[2], reverse=True)
melhor_cobertura = candidatos_multi[0][2]
top = [c for c in candidatos_multi if c[2] == melhor_cobertura]
ex, variacao, _ = random.choice(top)
obj_escolhido = random.choice(objetivos)
add_exercicio(ex, variacao, series=4, objetivo_escolhido=obj_escolhido)
# 3️⃣ Garantir pelo menos 1 exercício por músculo
for alvo in musculos_alvo:
if alvo not in musculos_cobertos:
candidatos = []
for ex in exercicios_validos:
if alvo in ex["musculos"] and ex["nome"] not in usados:
v = escolher_variacao(ex, lesoes)
candidatos.append((ex, v))
if candidatos:
candidatos.sort(key=lambda x: x[1]["custo"])
top_custo = candidatos[0][1]["custo"]
top = [c for c in candidatos if c[1]["custo"] == top_custo]
ex, variacao = random.choice(top)
obj_escolhido = random.choice(objetivos)
add_exercicio(ex, variacao, series=3, objetivo_escolhido=obj_escolhido)
# 3.5️⃣ Distribuir resto do budget de forma equilibrada entre músculos
mapa = {m: 0 for m in musculos_alvo}
for ex in treino:
for m in ex["musculos"]:
if m in mapa:
mapa[m] += 1
while custo_total < budget:
# Ordena músculos pelo número atual de exercícios
musculos_ordenados = sorted(mapa.items(), key=lambda x: x[1])
adicionou = False
for alvo, _ in musculos_ordenados:
candidatos = []
for ex in exercicios_validos:
if alvo in ex["musculos"] and ex["nome"] not in usados:
v = escolher_variacao(ex, lesoes)
if v and v["custo"] <= 4: # evitar só exercícios caros
candidatos.append((ex, v))
if candidatos:
# Pega o mais barato viável
candidatos.sort(key=lambda x: x[1]["custo"])
ex, variacao = candidatos[0]
obj_escolhido = random.choice(objetivos)
if add_exercicio(ex, variacao, series=3, objetivo_escolhido=obj_escolhido):
mapa[alvo] += 1
adicionou = True
break # vai para o próximo loop
if not adicionou:
break # não dá para adicionar mais nada
# 🔹 Ordem de prioridade dos músculos
ordem_musculos = {
"quadriceps": 1,
"posterior_de_coxa": 2,
"gluteo": 3,
"panturrilhas": 4,
"core": 5,
"peito": 6,
"ombro": 7,
"triceps": 8,
"dorsal": 9,
"trapezio": 10,
"biceps": 11,
"antebracos": 12,
"deltoide_frontal": 13,
"deltoide_lateral": 14,
"deltoide_posterior": 15,
"romboides": 16,
"lombar": 17
}
# 🔹 Ordenar treino:
treino.sort(
key=lambda x: (
-x["custo_total"], # 1️⃣ Primeiro custo (maior primeiro)
min([ordem_musculos.get(m, 99) for m in x["musculos"]]) # 2️⃣ Depois prioridade do músculo
)
)
return treino, custo_total
def formatar_treino_humano(treino_data):
"""
Converte o JSON do treino em uma descrição em linguagem natural formatada.
"""
texto_final = []
split_nome = treino_data.get("split_nome", "Treino")
texto_final.append(f"🏋️ **{split_nome}**\n")
dias = treino_data.get("dias", {})
for idx, (dia, info) in enumerate(dias.items(), start=1):
# Quebra de linha extra entre os dias (mas não antes do primeiro)
if idx > 1:
texto_final.append("")
# Cabeçalho do dia
musculos = ", ".join(info.get("musculos_alvo", []))
texto_final.append(f"📅 **{dia}** — Músculos alvo: {musculos}")
# Lista de exercícios
for ex in info.get("treino", []):
nome = ex.get("nome", "Exercício")
desc = ex.get("descricao", "")
series = ex.get("series", "?")
reps = ex.get("reps", "?")
objetivo = ex.get("objetivo", "")
video = ex.get("video", "")
# Formata o vídeo, se existir
link_video = f" 🎥 [Tutorial]({video})" if video else ""
texto_final.append(
f"- {nome} ({desc}) — **{series}** séries de **{reps}** repetições "
f"para **{objetivo}**.{link_video}"
)
return "\n".join(texto_final)
# 🔹 Carregar splits.json uma vez
with open("splits.json", "r", encoding="utf-8") as f:
splits_por_dias = json.load(f)
with open("splits_mulher.json", "r", encoding="utf-8") as f:
splits_por_dias_mulher = json.load(f)
def gerar_split(sexo="homem", dias = 5, budget=45, objetivos=["hipertrofia"], lesoes=[]):
"""
Gera um plano semanal de treino baseado no número de dias escolhido.
- dias: número de dias de treino por semana (1 a 6)
- budget: "tempo/esforço" máximo (compatível com montar_treino)
- objetivos: lista de objetivos (ex: ["hipertrofia", "forca"])
- lesoes: lista de articulações com lesões (ex: ["joelho"])
"""
dias_str = str(dias) # as chaves do JSON são strings
if dias_str not in splits_por_dias:
raise ValueError(f"Não há split configurado para {dias} dias/semana.")
# 🔹 Escolher aleatoriamente um split entre os disponíveis para esse número de dias
if(sexo == "homem"):
split_escolhido = random.choice(splits_por_dias[dias_str])
else:
split_escolhido = random.choice(splits_por_dias_mulher[dias_str])
treino_semana = {
"split_nome": split_escolhido["nome"],
"dias": {}
}
# 🔹 Montar treino para cada dia do split
for i, musculos_dia in enumerate(split_escolhido["dias"], start=1):
treino, custo = montar_treino(
musculos_dia,
budget=budget,
objetivos=objetivos,
lesoes=lesoes
)
treino_semana["dias"][f"Dia {i}"] = {
"musculos_alvo": musculos_dia,
"treino": treino,
"custo_total": custo
}
return treino_semana
def gerar_plano(idade, sexo, peso, altura, atividade, objetivo, intensidade, n_refeicoes=5, alergias=[]):
try:
user = UserProfile(
idade=int(idade),
sexo=sexo,
peso_kg=float(peso),
altura_cm=float(altura),
atividade=atividade,
)
plano = build_basic_plan(user, objetivo=objetivo, intensidade=intensidade)
# Gerar plano diário de refeições
plano_diario = gerar_plano_diario(plano, n_refeicoes=n_refeicoes, alergias=alergias)
resumo = (
f"📊 **Plano Nutricional**\n"
f"- Calorias alvo: {plano.calorias_alvo} kcal\n"
f"- Proteína: {plano.proteina_g} g\n"
f"- Carboidratos: {plano.carboidratos_g} g\n"
f"- Gorduras: {plano.gorduras_g} g\n"
f"ℹ️ {plano.nota}\n"
)
return resumo, plano_diario
except Exception as e:
return f"Erro: {str(e)}", None
import re
def extrair_dados_usuario(prompt_norm: str):
dados = {}
# Normalização básica
prompt_norm = normalize_text(prompt_norm)
prompt_norm = re.sub(r"\s+", " ", prompt_norm.strip())
# -----------------------------
# Peso (kg)
# -----------------------------
peso_match = re.search(
r"(?<!\d)(\d{2,3}(?:[.,]\d{1,2})?)\s*(?:kg|quilo?s?|kilos?|kgs?)\b",
prompt_norm
)
if peso_match:
dados["peso"] = float(peso_match.group(1).replace(",", "."))
# -----------------------------
# Altura (m ou cm)
# -----------------------------
altura_m_match = re.search(
r"(\d(?:[.,]\d{1,2}))\s*(?:m|metro?s?)\b",
prompt_norm
)
if altura_m_match:
dados["altura"] = float(altura_m_match.group(1).replace(",", ".")) * 100
else:
altura_cm_match = re.search(
r"(\d{2,3})\s*(?:cm|cent[ií]metros?)\b",
prompt_norm
)
if altura_cm_match:
dados["altura"] = float(altura_cm_match.group(1))
# -----------------------------
# Idade
# -----------------------------
idade_match = re.search(
r"(?<!\d)(\d{1,2})\s*(?:anos?|idade)\b",
prompt_norm
)
if idade_match:
dados["idade"] = int(idade_match.group(1))
# -----------------------------
# Sexo / Gênero
# -----------------------------
if re.search(r"\b(homem|masculino|rapaz|menino|garoto|cara|macho)\b", prompt_norm):
dados["sexo"] = "homem"
elif re.search(r"\b(mulher|feminino|garota|menina|moca|moça|mina)\b", prompt_norm):
dados["sexo"] = "mulher"
if "sexo" not in dados:
if re.search(r"\b(o|um)\b", prompt_norm) and not re.search(r"\b(a|uma)\b", prompt_norm):
dados["sexo"] = "homem"
elif re.search(r"\b(a|uma)\b", prompt_norm):
dados["sexo"] = "mulher"
# -----------------------------
# Objetivo (bulking / cutting / manutenção)
# -----------------------------
if re.search(
r"\b(ganhar\s*(massa|peso)|hipertrofia|crescer|aumentar\s*massa|"
r"ficar\s*grande|bulking|ganhar\s*volume|subir\s*de\s*peso)\b",
prompt_norm
):
dados["objetivo"] = "bulking"
elif re.search(
r"\b(emagrecer|perder\s*(peso|gordura)|cutting|definir|secar|"
r"ficar\s*seco|emagrecimento|diminuir\s*peso)\b",
prompt_norm
):
dados["objetivo"] = "cutting"
elif re.search(
r"\b(manter\s*(o|a|meu)?\s*(peso|forma)|manutenc[aã]o|estabilizar|ficar\s*igual)\b",
prompt_norm
):
dados["objetivo"] = "manutenção"
# -----------------------------
# Nível de atividade
# -----------------------------
atividade_map = {
"sedentario": [
r"sedent[áa]rio", r"inativ[ao]", r"parad[ao]", r"n[aã]o\s*(fa[çc]o|pratico)\s*exerc[ií]cio", r"n[aã]o\s*malho",
r"n[aã]o\s*treino", r"n[aã]o\s*vou\s*na\s*academia", r"n[aã]o\s*fa[çc]o\s*atividade", r"sem\s*atividade",
r"sem\s*exerc[ií]cio", r"sem\s*treino", r"sem\s*malha[cç][aã]o", r"sem\s*academia", r"sem\s*movimento",
r"trabalho\s*de\s*escrit[oó]rio", r"trabalho\s*de\s*mesa", r"trabalho\s*sedent[aá]rio",
r"passo\s*o\s*dia\s*sentado", r"passo\s*o\s*dia\s*na\s*frente\s*do\s*computador",
r"pouco\s*tempo\s*livre", r"pouco\s*tempo\s*para\s*exerc[ií]cio",
r"pouco\s*tempo\s*para\s*malha[cç][aã]o", r"pouco\s*tempo\s*para\s*treino",
r"pouco\s*tempo\s*para\s*academia", r"vida\s*ocupada", r"vida\s*agitada", r"vida\s*cheia",
r"vida\s*corrida", r"vida\s*atrapalhada", r"sedent[aá]rio"
],
"leve": [
r"leve", r"pouco\s*ativo", r"atividade\s*leve", r"1\s*(vez|x)", r"uma\s*vez",
r"caminhadas?\s*ocasionais?", r"exerc[ií]cio\s*leve", r"malha[cç][aã]o\s*leve", r"treino\s*leve",
r"academia\s*leve", r"atividade\s*física\s*leve", r"atividade\s*física\s*ocasional",
r"fa[çc]o\s*exerc[ií]cio\s*ocasionalmente", r"fa[çc]o\s*malha[cç][aã]o\s*ocasionalmente",
r"fa[çc]o\s*treino\s*ocasionalmente", r"fa[çc]o\s*academia\s*ocasionalmente"
],
"moderado": [
r"moderado", r"regular", r"atividade\s*moderada",r"treino leve"
r"2\s*(vezes|x)", r"duas\s*vezes", r"3\s*(vezes|x)", r"tr[eê]s\s*vezes",
r"treino\s*3\s*x\s*por\s*semana", r"treino\s*3\s*vezes\s*por\s*semana",
r"fa[çc]o\s*exerc[ií]cio\s*3\s*x\s*por\s*semana",
r"fa[çc]o\s*exerc[ií]cio\s*3\s*vezes\s*por\s*semana",
r"fa[çc]o\s*malha[cç][aã]o\s*3\s*x\s*por\s*semana",
],
"ativo": [
r"\b[aá]?[c]?tiv[ao]\b", r"treino ativo"
r"frequente", r"treino\s*puxado",
r"4\s*(vezes|x)", r"quatro\s*vezes", r"treino\s*pesado",
r"treino\s*regular\s*4\s*x", r"treino\s*regular\s*4\s*vezes",
r"fa[çc]o\s*exerc[ií]cio\s*4\s*x", r"fa[çc]o\s*exerc[ií]cio\s*4\s*vezes",
r"fa[çc]o\s*malha[cç][aã]o\s*4\s*x", r"fa[çc]o\s*malha[cç][aã]o\s*4\s*vezes",
r"fa[çc]o\s*treino\s*4\s*x", r"fa[çc]o\s*treino\s*4\s*vezes",
r"fa[çc]o\s*academia\s*4\s*x", r"fa[çc]o\s*academia\s*4\s*vezes",
r"fa[çc]o\s*exerc[ií]cio\s*regularmente", r"fa[çc]o\s*malha[cç][aã]o\s*regularmente",
r"fa[çc]o\s*treino\s*regularmente", r"fa[çc]o\s*academia\s*regularmente",
],
"muito_ativo": [
r"\bmuito\s+a[c]?tiv[ao]\b", r"treino ativo"
r"5\s*(vezes|x)", r"cinco\s*vezes", r"6\s*(vezes|x)", r"seis\s*vezes",
r"7\s*(vezes|x)", r"sete\s*vezes", r"di[áa]rio", r"todos\s*os\s*dias",
r"atleta", r"competidor", r"fa[çc]o\s*exerc[ií]cio\s*todos\s*os\s*dias",
r"(treino\s*(ha|há)\s*anos|treino\s*intenso)", r"fa[çc]o\s*malha[cç][aã]o\s*todos\s*os\s*dias",
r"fa[çc]o\s*treino\s*todos\s*os\s*dias", r"fa[çc]o\s*academia\s*todos\s*os\s*dias",
r"fa[çc]o\s*exerc[ií]cio\s*5\s*x", r"fa[çc]o\s*exerc[ií]cio\s*5\s*vezes",
r"fa[çc]o\s*exerc[ií]cio\s*6\s*x", r"fa[çc]o\s*exerc[ií]cio\s*6\s*vezes",
r"fa[çc]o\s*exerc[ií]cio\s*7\s*x", r"fa[çc]o\s*exerc[ií]cio\s*7\s*vezes",
r"fa[çc]o\s*malha[cç][aã]o\s*5\s*x", r"fa[çc]o\s*malha[cç][aã]o\s*5\s*vezes",
r"fa[çc]o\s*malha[cç][aã]o\s*6\s*x", r"fa[çc]o\s*malha[cç][aã]o\s*6\s*vezes",
r"fa[çc]o\s*malha[cç][aã]o\s*7\s*x", r"fa[çc]o\s*malha[cç][aã]o\s*7\s*vezes",
r"fa[çc]o\s*treino\s*5\s*x", r"fa[çc]o\s*treino\s*5\s*vezes",
r"fa[çc]o\s*treino\s*6\s*x", r"fa[çc]o\s*treino\s*6\s*vezes",
r"fa[çc]o\s*treino\s*7\s*x", r"fa[çc]o\s*treino\s*7\s*vezes",
r"fa[çc]o\s*academia\s*5\s*x", r"fa[çc]o\s*academia\s*5\s*vezes",
r"fa[çc]o\s*academia\s*6\s*x", r"fa[çc]o\s*academia\s*6\s*vezes",
r"fa[çc]o\s*academia\s*7\s*x", r"fa[çc]o\s*academia\s*7\s*vezes",
],
}
for nivel, padroes in atividade_map.items():
for z1 in padroes:
if re.search(z1, prompt_norm):
dados["atividade"] = nivel
break
if "atividade" in dados:
break
# -----------------------------
# Nível do usuário (iniciante / intermediário / avançado)
# -----------------------------
if re.search(
r"\b(iniciante|come[çc]ando|comecei\s*(agora|recentemente)|"
r"rec[ée]m\s*(comecei|iniciado)|novato|novo\s*(na|no)\s*(academia|treino)|"
r"pouco\s*tempo\s*(de\s*)?(treino|academia)|sem\s*muita\s*exper[iê]ncia|"
r"primeira\s*vez|treino\s*h[aá]\s*pouco\s*tempo)\b",
prompt_norm
):
dados["nivel_usuario"] = "iniciante"
elif re.search(
r"\b(intermedi[áa]rio|m[ée]dio|regular|algum\s*tempo\s*(de\s*)?(treino|academia)|"
r"uns\s*meses\s*de\s*treino|fa[çc]o\s*muscula[cç][aã]o\s*h[aá]\s*meses|"
r"ritmo\s*regular|treino\s*com\s*frequ[êe]ncia\s*m[ée]dia)\b",
prompt_norm
):
dados["nivel_usuario"] = "intermediario"
elif re.search(
r"\b(avancado|avançado|experiente|veterano|antigo\s*(na|no)\s*(academia|treino)|"
r"treino\s*h[aá]\s*(anos|muito\s*tempo)|fa[çc]o\s*muscula[cç][aã]o\s*h[aá]\s*(anos|muito\s*tempo)|"
r"treino\s*puxado|treino\s*pesado|alto\s*n[ií]vel|intenso\s*(h[aá]\s*anos)?|"
r"competidor|fisiculturista|atleta)\b",
prompt_norm
):
dados["nivel_usuario"] = "avancado"
# Se nenhum nível for detectado, definir por padrão com base na atividade
if "nivel_usuario" not in dados:
if dados.get("atividade") in ["sedentario", "leve"]:
dados["nivel_usuario"] = "iniciante"
elif dados.get("atividade") in ["moderado", "ativo"]:
dados["nivel_usuario"] = "intermediario"
elif dados.get("atividade") == "muito_ativo":
dados["nivel_usuario"] = "avancado"
# -----------------------------
# Alergias alimentares
# -----------------------------
# Mapeamento de sinônimos -> alergia base
alergia_map = {
"leite": [
r"leite", r"latic(í|i|e)c?i?ni[ou]s?", r"lactose", r"lact(í|i|e)c?e?os?", r"derivado[s]? de leite"
],
"glúten": [
r"glúten", r"gluten", r"trigo", r"centeio", r"cevada", r"aveia", r"malte"
],
"amendoim": [r"amendoim"],
"castanha": [r"castanh(as?)?", r"nozes?", r"avelã", r"pistache"],
"ovo": [r"ovo[s]?"],
"soja": [r"soja"],
"frutos do mar": [r"fruto[s]? do mar", r"marisco[s]?", r"camarão", r"lagosta", r"caranguejo"],
"chocolate": [r"chocolate"]
}
alergias_encontradas = []
if re.search(r"(alergia|al[eé]rgic[oa]|intoler[âa]ncia|evito|n[aã]o posso|me faz mal|reajo mal|problema com)", prompt_norm):
for alergia_base, padroes in alergia_map.items():
for padrao in padroes:
if re.search(rf"\b{padrao}\b", prompt_norm):
alergias_encontradas.append(alergia_base)
break # evita duplicatas
# Remove duplicados e adiciona ao dicionário
if alergias_encontradas:
dados["alergias"] = list(set(alergias_encontradas))
dados["lesoes"]=detectar_lesoes(prompt_norm)
dados["intencao_treino"]=detectar_objetivos(prompt_norm)
return dados
def coletar_ou_gerar_plano(prompt_norm: str):
dados = extrair_dados_usuario(prompt_norm)
campos_obrigatorios = ["idade", "sexo", "peso", "altura", "atividade", "objetivo"]
faltando = [c for c in campos_obrigatorios if c not in dados]
if faltando:
return {
"status": "incompleto",
"mensagem": f"Preciso que você me diga também: {', '.join(faltando)}."
}
# Se tudo ok → gerar plano
return gerar_plano(
idade=dados["idade"],
sexo=dados["sexo"],
peso=dados["peso"],
altura=dados["altura"],
atividade=dados["atividade"],
objetivo=dados["objetivo"],
intensidade="moderada"
)
def formatar_resposta_humana(resposta_final: dict) -> str:
"""
Usa o Falcon para transformar os dados técnicos em uma resposta natural.
"""
system_prompt = (
"Você é um personal trainer e nutricionista virtual. "
"Explique o resultado abaixo em português, de forma simples, motivadora "
"e prática, como se estivesse conversando com o aluno.\n"
)
dados_json = json.dumps(resposta_final, ensure_ascii=False, indent=2)
entrada = system_prompt + dados_json
inputs = tokenizer(entrada, return_tensors="pt", truncation=True).to("cpu")
output = model.generate(**inputs, max_new_tokens=400)
resposta = tokenizer.decode(output[0], skip_special_tokens=True)
return resposta.strip()
# -------------------------
# Função principal
# -------------------------
def responder(prompt: str):
try:
# Divide a prompt em duas partes: antes e depois da primeira quebra dupla de linha
print("Prompt recebido:", prompt)
partes = prompt.split("# PROMPT_USUARIO", 1)
dados_brutos = partes[0] if len(partes) > 0 else ""
prompt_usuario = partes[1] if len(partes) > 1 else ""
prompt_norm =normalize_text(prompt_usuario)
dados_norm = normalize_text(dados_brutos)
dados_usuario = extrair_dados_usuario(dados_norm)
print("Dados extraídos do usuário:", dados_usuario)
print("Prompt do usuário:", prompt_norm)
campos_obrigatorios = ["idade", "sexo", "peso", "altura", "atividade", "objetivo", "nivel_usuario"]
faltando = [c for c in campos_obrigatorios if c not in dados_usuario]
if faltando:
return f"Preciso que você configure os seguintes dados: {', '.join(faltando)} nas suas definições de perfil."
conceitos = detectar_conceitos(prompt_usuario)
if not conceitos:
return "Desculpe, não entendi o contexto, as suas perguntas só podem estar relacionadas à treino ou nutrição."
print("lesoes:", dados_usuario.get('lesoes'))
print("Alergias:", dados_usuario.get('alergias'))
resposta_final = {}
def conceito_match(tipo):
"""Retorna lista de conceitos relevantes por tipo."""
return [c for c in conceitos if c["tipo"] == tipo and c.get("score", 0) >= 0.5]
# =========================================================
# 🚀 TREINO
# =========================================================
treino_conceitos = conceito_match("treino")
if treino_conceitos:
subtipo = treino_conceitos[0].get("subtipo", "generico")
musculos_alvo = detectar_musculos(prompt_norm)
objetivos = dados_usuario.get('intencao_treino')
# 🔸 Caso 1: Pergunta conceitual (ex: “o que é hipertrofia?”)
if subtipo == "pergunta":
prompt_llm = f"""Você é um treinador experiente.
Responda de forma breve e direta apenas a esta pergunta:
### Pergunta:
{prompt_norm}
### Resposta:
"""
inputs = tokenizer(prompt_llm, return_tensors="pt")
output = model.generate(**inputs, max_new_tokens=256, do_sample=False)
resposta = tokenizer.decode(output[0], skip_special_tokens=True)
if "### Resposta:" in resposta:
resposta = resposta.split("### Resposta:")[-1]
resposta_final["treino"] = resposta.strip()
# 🔸 Caso 2: Treino semanal (split)
elif subtipo == "split":
tipo, dados = detectar_intencao(prompt_norm, musculos_alvo, dados_usuario)
dias = dados if isinstance(dados, int) else 4 # padrão 4 dias
# Budget dinâmico baseado no nível
nivel = dados_usuario.get("nivel_usuario", "").lower()
if nivel == "iniciante":
budget = 40 if dias <= 4 else 50
elif nivel in ["intermediario", "intermedio"]:
budget = 60 if dias < 4 else 50
elif nivel == "avancado":
budget = 75
else:
budget = 50
try:
treino_semana = gerar_split(
sexo=dados_usuario["sexo"],
dias=dias,
budget=budget,
objetivos=objetivos,
lesoes=dados_usuario.get("lesoes", []),
)
resposta_final["treino"] = formatar_treino_humano(treino_semana)
except ValueError:
resposta_final["treino"] = f"Não tenho splits configurados para {dias} dias/semana."
# 🔸 Caso 3: Treino isolado (ex: “treino de pernas”)
elif subtipo == "isolado":
musculos = musculos_alvo
if not musculos_alvo:
musculos = ["quadriceps", "posterior_de_coxa", "gluteo", "panturrilhas",
"core", "peito", "ombro", "triceps", "dorsal", "trapezio",
"biceps", "antebracos", "deltoide_frontal", "deltoide_lateral",
"deltoide_posterior", "romboides", "lombar"]
# Budget dinâmico baseado no nível
nivel = dados_usuario.get("nivel_usuario", "").lower()
if nivel == "iniciante":
budget = 40
elif nivel in ["intermediario", "intermedio"]:
budget = 60
elif nivel == "avancado":
budget = 75
else:
budget = 50
treino, custo = montar_treino(
musculos,
budget=budget,
objetivos=objetivos,
lesoes=dados_usuario.get("lesoes", []),
)
resposta_final["treino"] = formatar_treino_humano({
"split_nome": "Treino Isolado",
"dias": {
"Dia Único": {
"musculos_alvo": musculos,
"treino": treino,
"custo_total": custo
}
}
})
# 🔸 Caso 4: Pedido genérico (“quero melhorar meu treino”)
elif subtipo == "generico":
prompt_llm = f"""Você é um treinador experiente.
Dê uma resposta breve e motivacional para o pedido abaixo.
### Pedido:
{prompt_norm}
### Resposta:
"""
# Tokeniza e gera a resposta
inputs = tokenizer(prompt_llm, return_tensors="pt")
output = model.generate(**inputs, max_new_tokens=256, do_sample=False)
# Decodifica e limpa
resposta = tokenizer.decode(output[0], skip_special_tokens=True)
# Extrai só o trecho após "### Resposta:"
if "### Resposta:" in resposta:
resposta = resposta.split("### Resposta:")[-1]
resposta_final["treino"] = resposta.strip()
# =========================================================
# 🚀 NUTRIÇÃO
# =========================================================
nutricao_conceitos = conceito_match("nutricao")
if nutricao_conceitos:
subtipo = nutricao_conceitos[0].get("subtipo", "generico")
# 🔸 Caso 1: Pergunta conceitual ou alimento específico
if subtipo == "pergunta":
prompt_llm = f"""Você é um nutricionista esportivo.
Responda de forma objetiva, científica e clara à pergunta abaixo.
### Pergunta:
{prompt_norm}
### Resposta:
"""
# Tokeniza e gera
inputs = tokenizer(prompt_llm, return_tensors="pt")
output = model.generate(**inputs, max_new_tokens=256, do_sample=False)
# Decodifica e extrai só o que interessa
resposta = tokenizer.decode(output[0], skip_special_tokens=True)
if "### Resposta:" in resposta:
resposta = resposta.split("### Resposta:")[-1]
resposta_final["nutricao"] = resposta.strip()
# 🔸 Caso 2: Pedido de plano alimentar
elif subtipo == "plano":
resumo, plano = gerar_plano(
idade=dados_usuario["idade"],
sexo=dados_usuario["sexo"],
peso=dados_usuario["peso"],
altura=dados_usuario["altura"],
atividade=dados_usuario["atividade"],
objetivo=dados_usuario["objetivo"],
intensidade="moderada",
alergias=dados_usuario.get("alergias", []),
)
resposta_final["nutricao"] = formatar_plano_nutricional({
"resumo": resumo,
"plano": plano
})
# 🔸 Caso 3: Pedido genérico (“quero melhorar minha dieta”)
elif subtipo == "generico":
prompt_llm = f"Você é um nutricionista esportivo. Dê uma orientação breve sobre:\n\n\"{prompt_norm}\""
inputs = tokenizer(prompt_llm, return_tensors="pt")
output = model.generate(**inputs, max_new_tokens=256, do_sample=False)
resposta = tokenizer.decode(output[0], skip_special_tokens=True)
resposta_final["nutricao"] = resposta.strip()
# =========================================================
# 🚨 Caso nenhum domínio tenha sido acionado
# =========================================================
if not resposta_final:
return "Não consegui identificar se você quer um treino ou nutrição."
# 🧩 Une respostas de treino e nutrição (se existirem)
resposta_texto = ""
if "treino" in resposta_final:
resposta_texto += f"{resposta_final['treino'].strip()}\n\n"
if "nutricao" in resposta_final:
resposta_texto += f"{resposta_final['nutricao'].strip()}\n\n"
# 🔹 Remove espaços extras e retorna só o texto/markdown puro
return resposta_texto.strip()
except Exception as e:
import traceback
print("❌ Erro na função responder:")
traceback.print_exc()
return f"Ocorreu um erro: {str(e)}"
# -------------------------
# Interface Gradio
# -------------------------
demo = gr.Interface(
fn=responder,
inputs=gr.Textbox(lines=3, label="Pergunta"),
outputs=gr.Textbox(label="Resposta"),
title="Personal Trainer AI (com detecção de músculos)"
)
if __name__ == "__main__":
demo.queue()
demo.launch()