Spaces:
Sleeping
Sleeping
app.py
CHANGED
|
@@ -1,172 +1,129 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 3 |
from sentence_transformers import SentenceTransformer, util
|
| 4 |
-
import unicodedata
|
| 5 |
import torch
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
#
|
| 15 |
-
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
#
|
|
|
|
|
|
|
| 39 |
fitness_domains = [
|
| 40 |
-
|
| 41 |
-
"
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
"
|
| 45 |
-
"
|
| 46 |
-
"
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
"
|
| 50 |
-
"
|
| 51 |
-
"
|
| 52 |
-
"
|
| 53 |
-
"
|
| 54 |
-
"
|
| 55 |
-
"
|
| 56 |
-
"
|
| 57 |
-
"
|
| 58 |
-
"proteínas, carboidratos e gorduras",
|
| 59 |
-
"creatina, bcaas, whey protein",
|
| 60 |
-
"o que ajuda na hipertrofia",
|
| 61 |
-
|
| 62 |
-
# 🛌 Recuperação
|
| 63 |
-
"descanso e recuperação muscular",
|
| 64 |
-
"descanso entre séries",
|
| 65 |
-
"alongamento e aquecimento",
|
| 66 |
-
"prevenção de lesões no treino",
|
| 67 |
-
"sono e hidratação no desempenho físico",
|
| 68 |
-
|
| 69 |
-
# 🩺 Lesões e reabilitação
|
| 70 |
-
"treino adaptado para lesões",
|
| 71 |
-
"reabilitação e fisioterapia esportiva",
|
| 72 |
-
|
| 73 |
-
# 🎯 Objetivos gerais
|
| 74 |
-
"como ganhar massa muscular",
|
| 75 |
-
"como perder peso",
|
| 76 |
-
"como ganhar massa",
|
| 77 |
-
"melhor forma de melhorar condicionamento físico",
|
| 78 |
-
"plano de treino para iniciantes",
|
| 79 |
-
"estratégias para motivação e metas fitness"
|
| 80 |
-
]
|
| 81 |
-
|
| 82 |
-
# Contra-domínios mais específicos
|
| 83 |
-
contra_domains = [
|
| 84 |
-
# Finanças
|
| 85 |
-
"como ganhar dinheiro",
|
| 86 |
-
"investir em ações e bolsa de valores",
|
| 87 |
-
"criptomoedas e bitcoin",
|
| 88 |
-
"finanças pessoais e poupança",
|
| 89 |
-
|
| 90 |
-
# Tecnologia
|
| 91 |
-
"melhores celulares android",
|
| 92 |
-
"como deixar o computador mais rápido",
|
| 93 |
-
"programação em python",
|
| 94 |
-
"jogos online e consoles",
|
| 95 |
-
"reviews de gadgets e eletrônicos",
|
| 96 |
-
|
| 97 |
-
# Outros
|
| 98 |
-
"viagens e turismo",
|
| 99 |
-
"política e governo no brasil",
|
| 100 |
-
"história da segunda guerra mundial",
|
| 101 |
-
"astrologia e signos",
|
| 102 |
-
"religião e espiritualidade"
|
| 103 |
]
|
| 104 |
|
| 105 |
-
#
|
|
|
|
|
|
|
| 106 |
fitness_keywords = [
|
| 107 |
-
"treino", "
|
| 108 |
-
"
|
| 109 |
-
"
|
| 110 |
-
"
|
| 111 |
-
"
|
| 112 |
-
"
|
| 113 |
-
"reabilitação", "fisioterapia", "explosividade", "mobilidade", "flexibilidade", "plano de treino", "plano alimentar",
|
| 114 |
-
"perder peso", "emagrecer", "ganhar massa", "ganhar músculo", "definição muscular", "motivação", "metas fitness",
|
| 115 |
-
"fitness", "personal trainer", "personal", "treinador"
|
| 116 |
]
|
| 117 |
|
| 118 |
-
#
|
| 119 |
-
fitness_keywords_norm = [normalize_text(
|
| 120 |
|
| 121 |
-
#
|
| 122 |
-
# Pré-calcular embeddings
|
| 123 |
-
#
|
| 124 |
-
fitness_embeddings = embedder.encode(fitness_domains,
|
| 125 |
-
|
|
|
|
| 126 |
|
| 127 |
-
#
|
| 128 |
-
# Função
|
| 129 |
-
#
|
| 130 |
-
def responder(prompt):
|
| 131 |
-
|
| 132 |
-
|
| 133 |
|
| 134 |
-
#
|
| 135 |
-
|
| 136 |
-
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
|
|
|
| 140 |
|
| 141 |
-
#
|
| 142 |
-
|
|
|
|
| 143 |
|
| 144 |
-
#
|
| 145 |
-
score = max_fitness
|
| 146 |
-
if keyword_match:
|
| 147 |
-
score += 0.25 # pequeno bônus se tem keyword fitness
|
| 148 |
|
| 149 |
-
|
| 150 |
-
print(f"
|
|
|
|
| 151 |
|
| 152 |
-
#
|
| 153 |
-
if score <
|
| 154 |
return "Desculpe, só respondo perguntas sobre treino, nutrição e fitness."
|
| 155 |
|
| 156 |
-
#
|
| 157 |
system_message = (
|
| 158 |
"Você é um personal trainer virtual. "
|
| 159 |
-
"
|
| 160 |
"Se o usuário pedir treino, forneça uma lista numerada de exercícios físicos reais."
|
| 161 |
)
|
| 162 |
-
|
| 163 |
-
return
|
| 164 |
-
entrada = f"{system_message}\n\nUsuário: {prompt}\nAssistente:"
|
| 165 |
inputs = tokenizer(entrada, return_tensors="pt").to(model.device)
|
| 166 |
|
| 167 |
outputs = model.generate(
|
| 168 |
**inputs,
|
| 169 |
-
max_new_tokens=
|
| 170 |
temperature=0.7,
|
| 171 |
do_sample=True,
|
| 172 |
top_p=0.9,
|
|
@@ -174,16 +131,19 @@ def responder(prompt):
|
|
| 174 |
)
|
| 175 |
|
| 176 |
resposta = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
| 182 |
demo = gr.Interface(
|
| 183 |
fn=responder,
|
| 184 |
inputs=gr.Textbox(lines=3, label="Pergunta"),
|
| 185 |
outputs=gr.Textbox(label="Resposta"),
|
| 186 |
-
title="Personal Trainer AI
|
| 187 |
)
|
| 188 |
|
| 189 |
demo.queue().launch()
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 3 |
from sentence_transformers import SentenceTransformer, util
|
|
|
|
| 4 |
import torch
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
import unicodedata
|
| 7 |
|
| 8 |
+
# -------------------------
|
| 9 |
+
# Config
|
| 10 |
+
# -------------------------
|
| 11 |
+
EMBEDDING_MODEL = "rufimelo/bert-large-portuguese-cased-sts"
|
| 12 |
+
LLM_MODEL = "tiiuae/Falcon3-1B-Instruct" # ajuste se precisar outro LLM
|
| 13 |
+
|
| 14 |
+
# score params (ajustáveis)
|
| 15 |
+
THRESHOLD = 0.60 # cutoff para aceitar prompt como fitness
|
| 16 |
+
KEYWORD_WEIGHT = 0.12 # peso por keyword encontrada
|
| 17 |
+
MAX_KEYWORD_BONUS = 0.50 # máximo bônus de keyword
|
| 18 |
+
|
| 19 |
+
# -------------------------
|
| 20 |
+
# Normalização
|
| 21 |
+
# -------------------------
|
| 22 |
+
def normalize_text(text: str) -> str:
|
| 23 |
+
if text is None:
|
| 24 |
+
return ""
|
| 25 |
+
text = unicodedata.normalize("NFD", text)
|
| 26 |
+
text = "".join(ch for ch in text if unicodedata.category(ch) != "Mn")
|
| 27 |
+
return text.lower().strip()
|
| 28 |
+
|
| 29 |
+
# -------------------------
|
| 30 |
+
# Carregamento de modelos
|
| 31 |
+
# -------------------------
|
| 32 |
+
print("Carregando embedder...", EMBEDDING_MODEL)
|
| 33 |
+
embedder = SentenceTransformer(EMBEDDING_MODEL)
|
| 34 |
+
|
| 35 |
+
print("Carregando LLM...", LLM_MODEL)
|
| 36 |
+
tokenizer = AutoTokenizer.from_pretrained(LLM_MODEL, use_fast=True)
|
| 37 |
+
model = AutoModelForCausalLM.from_pretrained(LLM_MODEL, device_map="auto", torch_dtype=torch.float32)
|
| 38 |
+
|
| 39 |
+
# -------------------------
|
| 40 |
+
# Domínio fitness (frases representativas)
|
| 41 |
+
# -------------------------
|
| 42 |
fitness_domains = [
|
| 43 |
+
"treino para pernas e gluteos",
|
| 44 |
+
"exercícios para hipertrofia muscular",
|
| 45 |
+
"como dividir meu treino semanal",
|
| 46 |
+
"periodização para ganho de força",
|
| 47 |
+
"programa de hipertrofia de 12 semanas",
|
| 48 |
+
"dieta para ganho de massa muscular",
|
| 49 |
+
"o que comer antes e depois do treino",
|
| 50 |
+
"suplementação para hipertrofia",
|
| 51 |
+
"recuperação e descanso muscular",
|
| 52 |
+
"alongamento e aquecimento antes do treino",
|
| 53 |
+
"prevenção de lesões para corredores",
|
| 54 |
+
"treino adaptado para lesão no joelho",
|
| 55 |
+
"reabilitação esportiva e fisioterapia",
|
| 56 |
+
"treino para condicionamento e resistência",
|
| 57 |
+
"exercícios explosivos para membros inferiores",
|
| 58 |
+
"divisão de treino abc para hipertrofia",
|
| 59 |
+
"planejamento alimentar para atletas",
|
| 60 |
+
"estratégias para perder gordura mantendo massa muscular"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
]
|
| 62 |
|
| 63 |
+
# -------------------------
|
| 64 |
+
# Keywords (fallback)
|
| 65 |
+
# -------------------------
|
| 66 |
fitness_keywords = [
|
| 67 |
+
"treino", "treinar", "exercicio", "exercícios", "academia", "ginasio",
|
| 68 |
+
"hipertrofia", "musculacao", "musculação", "dieta", "alimentacao", "alimentação",
|
| 69 |
+
"suplemento", "creatina", "whey", "bcaas", "recuperacao", "recuperação",
|
| 70 |
+
"lesao", "lesão", "joelho", "ombro", "lombar", "fisioterapia", "reabilitacao",
|
| 71 |
+
"forca", "resistencia", "resistência", "explosividade", "aquecimento", "alongamento",
|
| 72 |
+
"plano de treino", "plano alimentar", "ganhar massa", "perder peso", "condicionamento"
|
|
|
|
|
|
|
|
|
|
| 73 |
]
|
| 74 |
|
| 75 |
+
# pré-normaliza keywords
|
| 76 |
+
fitness_keywords_norm = [normalize_text(k) for k in fitness_keywords]
|
| 77 |
|
| 78 |
+
# -------------------------
|
| 79 |
+
# Pré-calcular embeddings do domínio fitness (e normalizar)
|
| 80 |
+
# -------------------------
|
| 81 |
+
fitness_embeddings = embedder.encode([normalize_text(s) for s in fitness_domains],
|
| 82 |
+
convert_to_tensor=True)
|
| 83 |
+
fitness_embeddings = F.normalize(fitness_embeddings, p=2, dim=1) # vetores unitários
|
| 84 |
|
| 85 |
+
# -------------------------
|
| 86 |
+
# Função de filtragem + geração
|
| 87 |
+
# -------------------------
|
| 88 |
+
def responder(prompt: str):
|
| 89 |
+
prompt_text = prompt or ""
|
| 90 |
+
prompt_norm = normalize_text(prompt_text)
|
| 91 |
|
| 92 |
+
# embedding do prompt e normalização
|
| 93 |
+
prompt_emb = embedder.encode(prompt_norm, convert_to_tensor=True)
|
| 94 |
+
prompt_emb = F.normalize(prompt_emb, p=2, dim=0).unsqueeze(0) # shape (1, dim)
|
| 95 |
|
| 96 |
+
# similaridade com domínio fitness (max)
|
| 97 |
+
sims = util.cos_sim(prompt_emb, fitness_embeddings)[0] # shape (N_domains,)
|
| 98 |
+
max_fitness = float(torch.max(sims).item())
|
| 99 |
|
| 100 |
+
# keywords matches (adaptativo)
|
| 101 |
+
kw_matches = sum(1 for kw in fitness_keywords_norm if kw in prompt_norm)
|
| 102 |
+
keyword_bonus = min(kw_matches * KEYWORD_WEIGHT, MAX_KEYWORD_BONUS)
|
| 103 |
|
| 104 |
+
# score final (somente fitness + bonus)
|
| 105 |
+
score = max_fitness + keyword_bonus
|
|
|
|
|
|
|
| 106 |
|
| 107 |
+
# logs para debug
|
| 108 |
+
print(f"Prompt: {prompt_text}")
|
| 109 |
+
print(f"max_fitness: {max_fitness:.3f} | kw_matches: {kw_matches} | bonus: {keyword_bonus:.3f} | score: {score:.3f}")
|
| 110 |
|
| 111 |
+
# decisão
|
| 112 |
+
if score < THRESHOLD:
|
| 113 |
return "Desculpe, só respondo perguntas sobre treino, nutrição e fitness."
|
| 114 |
|
| 115 |
+
# se passou: gerar resposta com LLM
|
| 116 |
system_message = (
|
| 117 |
"Você é um personal trainer virtual. "
|
| 118 |
+
"Responda em português, de forma clara, curta e prática. "
|
| 119 |
"Se o usuário pedir treino, forneça uma lista numerada de exercícios físicos reais."
|
| 120 |
)
|
| 121 |
+
entrada = f"{system_message}\n\nUsuário: {prompt_text}\nAssistente:"
|
|
|
|
|
|
|
| 122 |
inputs = tokenizer(entrada, return_tensors="pt").to(model.device)
|
| 123 |
|
| 124 |
outputs = model.generate(
|
| 125 |
**inputs,
|
| 126 |
+
max_new_tokens=200,
|
| 127 |
temperature=0.7,
|
| 128 |
do_sample=True,
|
| 129 |
top_p=0.9,
|
|
|
|
| 131 |
)
|
| 132 |
|
| 133 |
resposta = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 134 |
+
# retorna apenas a parte após "Assistente:" se existir
|
| 135 |
+
if "Assistente:" in resposta:
|
| 136 |
+
return resposta.split("Assistente:")[-1].strip()
|
| 137 |
+
return resposta.strip()
|
| 138 |
+
|
| 139 |
+
# -------------------------
|
| 140 |
+
# Gradio
|
| 141 |
+
# -------------------------
|
| 142 |
demo = gr.Interface(
|
| 143 |
fn=responder,
|
| 144 |
inputs=gr.Textbox(lines=3, label="Pergunta"),
|
| 145 |
outputs=gr.Textbox(label="Resposta"),
|
| 146 |
+
title="Personal Trainer AI (PT-BR embeddings)"
|
| 147 |
)
|
| 148 |
|
| 149 |
demo.queue().launch()
|