MarvinRoque commited on
Commit
bc62d05
·
verified ·
1 Parent(s): ef54bed
Files changed (1) hide show
  1. app.py +106 -146
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
- def normalize_text(text):
8
- """Remove acentos e coloca em minúsculas para comparação robusta."""
9
- return "".join(
10
- c for c in unicodedata.normalize("NFD", text.lower())
11
- if unicodedata.category(c) != "Mn"
12
- )
13
-
14
- # =========================================================
15
- # Modelo de linguagem (Falcon 1B Instruct)
16
- # =========================================================
17
- model_id = "tiiuae/Falcon3-1B-Instruct"
18
- tokenizer = AutoTokenizer.from_pretrained(model_id)
19
- model = AutoModelForCausalLM.from_pretrained(
20
- model_id,
21
- torch_dtype="float32",
22
- device_map="auto"
23
- )
24
-
25
- # =========================================================
26
- # Modelo de embeddings (BAAI/bge-m3 ou fallback bge-small)
27
- # =========================================================
28
- try:
29
- embedder = SentenceTransformer("BAAI/bge-m3")
30
- print("✅ Usando embeddings BAAI/bge-m3")
31
- except Exception as e:
32
- print(f"⚠️ Erro ao carregar bge-m3: {e}")
33
- print("➡️ Usando fallback BAAI/bge-small-en-v1.5")
34
- embedder = SentenceTransformer("BAAI/bge-small-en-v1.5")
35
-
36
- # =========================================================
37
- # Domínios fitness
38
- # =========================================================
 
 
39
  fitness_domains = [
40
- # 🏋️‍♂️ Treino
41
- "treino para",
42
- "exercícios para",
43
- "como dividir meu treino",
44
- "tipos de treino: força, resistência, hipertrofia",
45
- "como melhorar hipertrofia",
46
- "periodização de treino",
47
-
48
- # 🍎 Nutrição
49
- "dieta para",
50
- "o que comer para",
51
- "alimentos que ajudam na recuperação muscular",
52
- "suplementos para treino",
53
- "creatina e whey protein",
54
- "hidratação e desempenho físico",
55
- "nutrição esportiva",
56
- "macronutrientes e micronutrientes",
57
- "planejamento alimentar",
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
- # Palavras-chave relevantes (normalizadas depois)
 
 
106
  fitness_keywords = [
107
- "treino", "exercício", "academia", "ginasio", "hipertrofia", "musculação",
108
- "condicionamento", "força", "resistência", "alongamento", "aquecimento",
109
- "nutrição", "alimentacao", "dieta", "suplemento", "suplementacao", "creatina", "whey", "bcaas",
110
- "recuperação", "descanso", "sono", "hidratação",
111
- "lesão", "joelho", "ombro", "lombar", "cotovelo","costas", "peito", "pernas", "trapezio",
112
- "bíceps", "tríceps", "abdômen", "core", "quadriceps", "posterior de coxa", "panturrilha",
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
- # Normalizar keywords
119
- fitness_keywords_norm = [normalize_text(kw) for kw in fitness_keywords]
120
 
121
- # =========================================================
122
- # Pré-calcular embeddings
123
- # =========================================================
124
- fitness_embeddings = embedder.encode(fitness_domains, convert_to_tensor=True, normalize_embeddings=True)
125
- contra_embeddings = embedder.encode(contra_domains, convert_to_tensor=True, normalize_embeddings=True)
 
126
 
127
- # =========================================================
128
- # Função principal
129
- # =========================================================
130
- def responder(prompt):
131
- prompt_norm = normalize_text(prompt)
132
- prompt_embedding = embedder.encode(prompt, convert_to_tensor=True, normalize_embeddings=True)
133
 
134
- # Similaridades
135
- fitness_sim = util.cos_sim(prompt_embedding, fitness_embeddings)
136
- contra_sim = util.cos_sim(prompt_embedding, contra_embeddings)
137
 
138
- max_fitness = torch.max(fitness_sim).item()
139
- max_contra = torch.max(contra_sim).item()
 
140
 
141
- # Palavras-chave
142
- keyword_match = any(kw in prompt_norm for kw in fitness_keywords_norm)
 
143
 
144
- # Score híbrido
145
- score = max_fitness - max_contra
146
- if keyword_match:
147
- score += 0.25 # pequeno bônus se tem keyword fitness
148
 
149
- print(f"Prompt: {prompt}")
150
- print(f"Fitness (max): {max_fitness:.3f} | Contra (max): {max_contra:.3f} | KW matches: {int(keyword_match)} | Score: {score:.3f}")
 
151
 
152
- # Decisão
153
- if score < 0.05:
154
  return "Desculpe, só respondo perguntas sobre treino, nutrição e fitness."
155
 
156
- # === Geração com LLM ===
157
  system_message = (
158
  "Você é um personal trainer virtual. "
159
- "Sempre responda em PORTUGUÊS, de forma clara, curta e prática. "
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=120,
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
- return resposta.split("Assistente:")[-1].strip()
178
-
179
- # =========================================================
180
- # Interface Gradio
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 com Filtro Semântico (BAAI/bge-m3)"
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()