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