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"(? 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()