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 | |
| # 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 = "tiiuae/Falcon3-1B-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 | |
| # ------------------------- | |
| print("Carregando embedder:", EMBEDDING_MODEL) | |
| embedder = SentenceTransformer(EMBEDDING_MODEL) | |
| print("Carregando LLM:", LLM_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 e subgrupos | |
| "pernas": ["perna", "pernas", "inferiores", "lower body", "treino inferior"], | |
| "quadriceps": ["quadriceps", "quads", "coxa da frente", "frontal da coxa"], | |
| "posterior_de_coxa": ["posterior", "posterior de coxa", "isquiotibiais", "hamstrings"], | |
| "gluteo": ["gluteo", "glúteos", "bumbum", "gluteus"], | |
| "panturrilhas": ["panturrilha", "panturrilhas", "batata da perna", "gastrocnêmio", "soleo"], | |
| # 🔹 Costas e subgrupos | |
| "costas": ["costas", "dorsal", "latissimo", "lats", "dorso", "costa"], | |
| "lombar": ["lombar", "parte baixa das costas", "erectores", "eretores da espinha"], | |
| "trapezio": ["trapezio", "trapézio", "pescoço largo"], | |
| # 🔹 Peito | |
| "peito": ["peito", "peitoral", "chest"], | |
| # 🔹 Braços (grupo e subgrupos) | |
| "bracos": ["braco", "braço", "bracos", "braços", "arm", "arms", "treino de bracos", "treino de braços"], | |
| "biceps": ["biceps", "bíceps"], | |
| "triceps": ["triceps", "tríceps"], | |
| "antebraco": ["antebraco", "antebraço", "antebracos", "antebraços", "forearm"], | |
| # 🔹 Ombros | |
| "ombro": ["ombro", "ombros", "deltoide", "deltoides", "shoulder"], | |
| # 🔹 Abdômen e core | |
| "abdomen": ["abdomen", "abdominal", "reto abdominal", "abs"], | |
| "oblíquos": ["oblíquos", "obliquo", "oblíquo"], | |
| "core": ["core", "centro do corpo", "estabilizadores"], | |
| # 🔹 Superiores | |
| "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", "costas", "lombar", "trapézio", "antebraço"], | |
| "empurrar": ["triceps", "peito", "ombro", "deltoides"] | |
| } | |
| # ------------------------- | |
| # Lesões (keywords) | |
| # ------------------------- | |
| lesao_context_keywords = [ | |
| "dor", "dói","doi", "doe", "machucado", "lesão", "lesoes", "lesões", "rompido", "lesionado" | |
| "inflamado", "inflamação", "luxação", "ruptura", "tendinite", "entorse", | |
| "condromalacia", "bursite", "hernia", "hérnia" | |
| ] | |
| lesao_keywords = { | |
| "joelho": ["joelho", "ligamento cruzado", "lca", "menisco", "condromalacia"], | |
| "ombro": ["ombro", "manguito rotador", "luxação de ombro", "tendinite no ombro"], | |
| "lombar": ["lombar", "coluna", "hernia de disco", "hérnia de disco", "ciática"], | |
| "quadril": ["quadril", "artrose no quadril", "bursite no quadril"], | |
| "tornozelo": ["tornozelo", "entorse de tornozelo", "lesão no tornozelo"], | |
| "cotovelo": ["cotovelo", "epicondilite", "tennis elbow", "golfista"], | |
| "punho": ["punho", "síndrome do túnel do carpo"], | |
| } | |
| 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 | |
| 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_emb, prompt_norm): | |
| matches = [] | |
| for concept, c_emb in concept_embeddings.items(): | |
| sim = float(util.cos_sim(prompt_emb, c_emb).item()) | |
| if sim >= KW_SIM_THRESHOLD: | |
| matches.append((concept, sim)) | |
| # fallback por regex | |
| if any(k in prompt_norm for k in ["emagrecer", "perder peso", "cutting", "bulking", "secar", "ganhar massa", "ganhara peso"]): | |
| matches.append(("dieta", 1.0)) | |
| return matches | |
| def detectar_musculos(texto: str) -> list[str]: | |
| texto = texto.lower() | |
| detectados = set() | |
| # 1️⃣ Identificar palavras-chave | |
| for musculo, termos in muscle_keywords.items(): | |
| for termo in termos: | |
| if termo in texto: | |
| detectados.add(musculo) | |
| # 2️⃣ Expansão genérica de todos os grupos compostos | |
| expansoes = set() | |
| for grupo, subgrupos in group_hierarchy.items(): | |
| if grupo in detectados: | |
| detectados.remove(grupo) | |
| expansoes.update(subgrupos) | |
| detectados.update(expansoes) | |
| # 3️⃣ Hierarquia reversa: se um grupo e seus subgrupos aparecerem, priorizar os subgrupos | |
| for grupo, subgrupos in group_hierarchy.items(): | |
| if grupo in detectados and any(s in detectados for s in subgrupos): | |
| detectados.remove(grupo) | |
| return list(detectados) | |
| # ------------------------- | |
| # Objetivos (keywords) | |
| # ------------------------- | |
| objetivo_keywords = { | |
| "hipertrofia": ["hipertrofia", "massa", "crescimento muscular", "ganhar tamanho", "volume"], | |
| "forca": ["força", "forca", "powerlifting", "power", "pesado", "ganhar força"], | |
| "condicionamento": ["resistência", "resistencia", "condicionamento", "endurance", "cardio", "alta repetiçao", "repetições altas"], | |
| "explosividade": ["explosivo", "explosividade", "pliometria", "saltar", "sprints", "potência", "potencia"] | |
| } | |
| def detectar_objetivos(texto: str) -> list[str]: | |
| texto = texto.lower() | |
| objetivos_detectados = [] | |
| for objetivo, termos in objetivo_keywords.items(): | |
| for termo in termos: | |
| if termo in texto: | |
| objetivos_detectados.append(objetivo) | |
| break | |
| if not objetivos_detectados: | |
| return ["hipertrofia"] # padrão | |
| return objetivos_detectados | |
| texto = texto.lower() | |
| for objetivo, termos in objetivo_keywords.items(): | |
| for termo in termos: | |
| if termo in texto: | |
| return objetivo | |
| return "hipertrofia" # padrão se nada for detectado | |
| def detectar_intencao(prompt_norm: str, musculos_detectados: list[str]): | |
| """ | |
| Retorna: | |
| ("split", dias) -> se detectar pedido de divisão semanal | |
| ("isolado", musculos) -> se detectar treino de músculos específicos | |
| """ | |
| # 🔹 Detectar split (nº de dias por semana) | |
| padrao_split = re.search(r"(\d+)\s*(x|vezes|dias)\s*(por\s*semana)?", prompt_norm) | |
| if padrao_split: | |
| dias = int(padrao_split.group(1)) | |
| return "split", dias | |
| # 🔹 Detectar treino isolado (músculos) | |
| if musculos_detectados: | |
| return "isolado", musculos_detectados | |
| # 🔹 Default → treino full body isolado | |
| return "isolado", ["peito", "costas", "ombro", "braços", "pernas", "core"] | |
| 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 | |
| # 🔹 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): | |
| 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) | |
| resumo = ( | |
| f"📊 **Plano Nutricional**\n\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\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 = {} | |
| # ----------------------------- | |
| # Peso (em kg) | |
| # ----------------------------- | |
| peso_match = re.search(r"(\d{2,3}(?:[.,]\d{1,2})?)\s*(kg|quilo|quilos)", prompt_norm) | |
| if peso_match: | |
| dados["peso"] = float(peso_match.group(1).replace(",", ".")) | |
| # ----------------------------- | |
| # Altura (cm ou metros) | |
| # ----------------------------- | |
| altura_m_match = re.search(r"(\d(?:[.,]\d{1,2})?)\s*(m|metro|metros)", 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|centimetros|centímetros)", prompt_norm) | |
| if altura_cm_match: | |
| dados["altura"] = float(altura_cm_match.group(1)) | |
| # ----------------------------- | |
| # Idade | |
| # ----------------------------- | |
| idade_match = re.search(r"(\d{1,2})\s*(anos|idade|ano)", prompt_norm) | |
| if idade_match: | |
| dados["idade"] = int(idade_match.group(1)) | |
| # ----------------------------- | |
| # Sexo / Gênero | |
| # ----------------------------- | |
| if re.search(r"\b(homem|masculino|rapaz|menino)\b", prompt_norm): | |
| dados["sexo"] = "homem" | |
| elif re.search(r"\b(mulher|feminino|garota|menina)\b", prompt_norm): | |
| dados["sexo"] = "mulher" | |
| # ----------------------------- | |
| # Objetivo (bulking/cutting) | |
| # ----------------------------- | |
| if any(word in prompt_norm for word in ["ganhar massa", "ganhar peso", "bulking", "hipertrofia", "aumentar massa", "crescer"]): | |
| dados["objetivo"] = "bulking" | |
| elif any(word in prompt_norm for word in ["perder peso", "emagrecer", "cutting", "definir", "secar", "perder gordura"]): | |
| dados["objetivo"] = "cutting" | |
| else: | |
| dados["objetivo"] = "manutenção" | |
| # ----------------------------- | |
| # Nível de atividade | |
| # ----------------------------- | |
| atividade_map = { | |
| "sedentário": [r"sedent[áa]rio", r"inativo", r"parado"], | |
| "leve": [r"leve", r"pouco ativo", r"atividade leve", r"caminhadas ocasionais", r"1\s*(vez|x)", r"uma vez"], | |
| "moderado": [r"moderado", r"regular", r"atividade moderada", r"2\s*(vezes|x)", r"duas vezes", r"3\s*(vezes|x)", r"tr[eê]s vezes"], | |
| "ativo": [r"alto", r"intenso", r"treino pesado", r"4\s*(vezes|x)", r"quatro vezes", r"5\s*(vezes|x)", r"cinco vezes"], | |
| "muito_ativo": [r"muito alto", r"atleta", r"competidor", r"6\s*(vezes|x)", r"seis vezes", r"7\s*(vezes|x)", r"sete vezes", r"di[áa]rio"], | |
| } | |
| 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 | |
| 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\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: | |
| prompt_text = prompt or "" | |
| prompt_norm = normalize_text(prompt_text) | |
| # 1️⃣ Extrair dados do usuário primeiro | |
| dados_usuario = 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_usuario] | |
| if faltando: | |
| return f"Preciso que você me diga também: {', '.join(faltando)}." | |
| # 2️⃣ Só depois segue para embeddings e intenções | |
| prompt_emb = embedder.encode(prompt_norm, convert_to_tensor=True) | |
| if prompt_emb.dim() == 1: | |
| prompt_emb = prompt_emb.unsqueeze(0) | |
| prompt_emb = F.normalize(prompt_emb, p=2, dim=1) | |
| sims = util.cos_sim(prompt_emb, fitness_embeddings)[0] | |
| max_fitness = float(torch.max(sims).item()) | |
| concept_matches = detectar_conceitos(prompt_emb, prompt_norm) | |
| keyword_bonus = min(len(concept_matches) * KEYWORD_WEIGHT, MAX_KEYWORD_BONUS) | |
| score = max_fitness + keyword_bonus | |
| conceitos_detectados = [c for c, _ in concept_matches] | |
| intenções = { | |
| "treino": any(c in conceitos_detectados for c in ["treino", "hipertrofia", "força", "resistência", "estratégias", "cardio"]), | |
| "nutricao": any(c in conceitos_detectados for c in ["dieta", "suplementos", "ganhar peso", "perder peso", "cutting", "bulking", "ganhar massa", "definir"]), | |
| "recuperacao": any(c in conceitos_detectados for c in ["recuperação", "lesões"]), | |
| } | |
| resposta_final = {} | |
| print(dados_usuario["sexo"]) | |
| # 🚀 TREINO | |
| if intenções["treino"]: | |
| musculos_alvo = detectar_musculos(prompt_norm) | |
| lesoes = detectar_lesoes(prompt_norm) | |
| objetivos = detectar_objetivos(prompt_norm) | |
| tipo, dados = detectar_intencao(prompt_norm, musculos_alvo) | |
| if tipo == "split": | |
| dias = dados | |
| try: | |
| treino_semana = gerar_split(dias=dias, budget=50, objetivos=objetivos, lesoes=lesoes) | |
| resposta_final["treino"] = treino_semana | |
| except ValueError: | |
| resposta_final["treino"] = f"Não tenho splits configurados para {dias} dias/semana." | |
| elif tipo == "isolado": | |
| musculos = dados | |
| treino, custo = montar_treino(musculos, budget=50, objetivos=objetivos, lesoes=lesoes) | |
| resposta_final["treino"] = { | |
| "split_nome": "Treino isolado", | |
| "musculos_alvo": musculos, | |
| "custo_total": custo, | |
| "treino": treino | |
| } | |
| # 🚀 NUTRIÇÃO | |
| if intenções["nutricao"]: | |
| 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" | |
| ) | |
| resposta_final["nutricao"] = {"resumo": resumo, "plano": plano} | |
| # 🚀 RECUPERAÇÃO | |
| if intenções["recuperacao"]: | |
| resposta_final["recuperacao"] = { | |
| "dica": "Lembre-se de alongar e priorizar o sono para melhor recuperação." | |
| } | |
| if not resposta_final: | |
| return "Não consegui identificar se você quer um treino, nutrição ou recuperação." | |
| return resposta_final | |
| 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().launch() | |