akra35567 commited on
Commit
73830e7
·
1 Parent(s): 0f2b311

Update modules/treinamento.py

Browse files
Files changed (1) hide show
  1. modules/treinamento.py +315 -417
modules/treinamento.py CHANGED
@@ -1,417 +1,315 @@
1
- # treinamento.py
2
- import threading
3
- import time
4
- import logging
5
- import sqlite3
6
- import re
7
- import json
8
- from typing import Optional, Any, List, Dict, Tuple
9
- import collections
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
- try:
14
- from sentence_transformers import SentenceTransformer
15
- except Exception:
16
- SentenceTransformer = None
17
-
18
- # Listas para análise de emoções, gírias e tom
19
- PALAVRAS_POSITIVAS = ['bom', 'ótimo', 'incrível', 'maravilhoso', 'feliz', 'alegre', 'amor', 'gostar', 'adorei', 'top', 'show', 'legal', 'bacana']
20
- PALAVRAS_NEGATIVAS = ['ruim', 'péssimo', 'horrível', 'triste', 'ódio', 'raiva', 'chateado', 'detesto', 'odeio', 'merda', 'porra', 'foda-se']
21
- GIRIAS_ANGOLANAS = ['mano', 'puto', 'kkk', 'rsrs', 'lol', 'tô', 'cê', 'num', 'tipo', 'né', 'bah', 'uai', 'oxe', 'eita', 'caramba', 'pqp', 'fdp', 'vsf', 'mlk', 'arrombado', 'viado', 'bicha', 'cu', 'buceta', 'rola', 'pau', 'bunda', 'peito', 'teta', 'bct', 'pnc', 'pnctl', 'fuder', 'foder', 'transar', 'comer', 'chupar', 'mamada', 'boquete', 'punheta', 'gozar']
22
- PALAVRAS_RUDES = ['puto', 'merda', 'porra', 'caralho', 'cacete', 'fdp', 'vsf', 'mlk', 'arrombado', 'viado', 'bicha', 'cu', 'buceta', 'rola', 'pau', 'bunda', 'peito', 'teta', 'bct', 'pnc', 'pnctl', 'fuder', 'foder', 'transar', 'comer', 'chupar', 'mamada', 'boquete', 'punheta', 'gozar', 'pqp', 'kkk', 'vai tomar no cu', 'vai se foder', 'vai pra puta que pariu', 'seu filho da puta', 'sua puta', 'sua vadia', 'sua piranha', 'sua cachorra', 'sua puta barata', 'sua vagabunda', 'sua ordinária', 'sua desgraçada', 'sua infeliz', 'sua imbecil', 'sua idiota', 'sua burra', 'sua retardada', 'sua mongoloide', 'sua deficiente', 'sua aleijada', 'sua gorda', 'sua magrela', 'sua feia', 'sua bonita de merda', 'sua gostosa de bosta']
23
-
24
-
25
- class Treinamento:
26
- """Classe responsável por treinar/ajustar o comportamento de Akira a partir dos dados no banco.
27
-
28
- Funcionalidades principais:
29
- - Agregar aprendizados e mensagens do banco
30
- - Gerar embeddings (se disponível) e armazenar via Database.salvar_embedding
31
- - Atualizar aprendizados agregados (interesses, limites, persona) no DB
32
- - Rodar em background periodicamente (por padrão 24h)
33
- """
34
-
35
- def __init__(self, db, contexto: Optional[Any] = None, interval_hours: int = 24):
36
- self.db = db
37
- self.contexto = contexto
38
- self.interval_hours = interval_hours
39
- self._thread = None
40
- self._running = False
41
- self._model = None
42
- # Usuários privilegiados que devem ter tom formal por padrão
43
- self.privileged_users = ['244937035662', 'isaac', 'isaac quarenta', 'ceo', 'fundador']
44
-
45
- def registrar_interacao(self, usuario: str, mensagem: str, resposta: str, numero: str = '', is_reply: bool = False, mensagem_original: str = ''):
46
- """Registra uma interação para treinamento futuro."""
47
- try:
48
- conn = sqlite3.connect(self.db.db_path)
49
- c = conn.cursor()
50
- c.execute('INSERT INTO mensagens (usuario, mensagem, resposta, numero, is_reply, mensagem_original) VALUES (?, ?, ?, ?, ?, ?)',
51
- (usuario, mensagem, resposta, numero, is_reply, mensagem_original))
52
- conn.commit()
53
- conn.close()
54
- logger.info('Interação registrada para treinamento')
55
- except Exception as e:
56
- logger.warning(f'Erro ao registrar interação: {e}')
57
-
58
- def _ensure_model(self):
59
- # tenta usar o model carregado no contexto, ou carrega localmente
60
- if self._model is not None:
61
- return
62
- if self.contexto and hasattr(self.contexto, 'model') and getattr(self.contexto, 'model', None):
63
- self._model = self.contexto.model
64
- return
65
- if SentenceTransformer is None:
66
- logger.debug('SentenceTransformer não disponível; embeddings desativados.')
67
- return
68
- try:
69
- self._model = SentenceTransformer('all-MiniLM-L6-v2')
70
- logger.info('Treinamento: modelo de embeddings carregado localmente.')
71
- except Exception as e:
72
- logger.warning(f'Treinamento: falha ao carregar SentenceTransformer: {e}')
73
- self._model = None
74
-
75
- def _fetch_recent_data(self, limit=1000) -> List[tuple]:
76
- """Lê mensagens e aprendizados do banco diretamente via sqlite para agregação.
77
- Filtra apenas conversas reais onde Akira respondeu e números válidos."""
78
- rows = []
79
- try:
80
- conn = sqlite3.connect(self.db.db_path)
81
- c = conn.cursor()
82
- # Filtrar apenas linhas com resposta não vazia, número válido e usuário não vazio
83
- c.execute('''
84
- SELECT usuario, numero, mensagem, resposta FROM mensagens
85
- WHERE resposta IS NOT NULL AND resposta != ''
86
- AND numero IS NOT NULL AND numero != '' AND numero != 'unknown'
87
- AND usuario IS NOT NULL AND usuario != '' AND usuario != 'unknown'
88
- AND LENGTH(numero) >= 10 AND numero LIKE '244%'
89
- ORDER BY id DESC LIMIT ?
90
- ''', (limit,))
91
- rows = c.fetchall()
92
- conn.close()
93
- logger.info(f'Treinamento: filtrados {len(rows)} registros válidos de conversas reais')
94
- except Exception as e:
95
- logger.error(f'Erro ao buscar dados para treinamento: {e}')
96
- return rows
97
-
98
- def train_once(self):
99
- """Executa um ciclo de treinamento simples/heurístico: agrega mensagens, cria embeddings e atualiza aprendizados."""
100
- logger.info('Iniciando ciclo de treinamento (train_once)')
101
- data = self._fetch_recent_data(limit=1000)
102
- if not data:
103
- logger.info('Nenhuma mensagem encontrada para treinar.')
104
- # registra horário do último treino
105
- try:
106
- self.db.salvar_info_geral('ultimo_treino', str(time.time()))
107
- except Exception:
108
- pass
109
- return
110
-
111
- # Extrair palavras-chave simples por frequência para adaptar 'interesses' gerais
112
- token_counter = collections.Counter()
113
- sentences_for_embeddings = []
114
- for usuario, numero, mensagem, resposta in data:
115
- texto = (mensagem or '') + ' ' + (resposta or '')
116
- # tokenização simples
117
- tokens = [t for t in re.split(r'\W+', texto.lower()) if len(t) > 2]
118
- token_counter.update(tokens)
119
- sentences_for_embeddings.append(mensagem or '')
120
-
121
- # top keywords
122
- top_keywords = [w for w, _ in token_counter.most_common(20)]
123
- logger.info({'event': 'top_keywords', 'keywords': top_keywords[:10]})
124
-
125
- # Análise de emoções, gírias e tom
126
- emocoes_positivas = 0
127
- emocoes_negativas = 0
128
- girias_counter = collections.Counter()
129
- tom_rude = 0
130
- for usuario, numero, mensagem, resposta in data:
131
- texto = (mensagem or '') + ' ' + (resposta or '')
132
- tokens = [t for t in re.split(r'\W+', texto.lower()) if len(t) > 2]
133
- for token in tokens:
134
- if token in PALAVRAS_POSITIVAS:
135
- emocoes_positivas += 1
136
- if token in PALAVRAS_NEGATIVAS:
137
- emocoes_negativas += 1
138
- if token in GIRIAS_ANGOLANAS:
139
- girias_counter[token] += 1
140
- if token in PALAVRAS_RUDES:
141
- tom_rude += 1
142
-
143
- # top girias
144
- top_girias = [w for w, c in girias_counter.most_common(10)]
145
- logger.info({'event': 'top_girias', 'girias': top_girias})
146
- logger.info({'event': 'analise', 'positivas': emocoes_positivas, 'negativas': emocoes_negativas, 'rude': tom_rude})
147
-
148
- # salvar agregados
149
- try:
150
- self.db.salvar_info_geral('interesses_geral', ','.join(top_keywords))
151
- except Exception as e:
152
- logger.warning(f'Não foi possível salvar interesses_geral: {e}')
153
-
154
- try:
155
- self.db.salvar_info_geral('emocoes_positivas', str(emocoes_positivas))
156
- except Exception as e:
157
- logger.warning(f'Não foi possível salvar emocoes_positivas: {e}')
158
-
159
- try:
160
- self.db.salvar_info_geral('emocoes_negativas', str(emocoes_negativas))
161
- except Exception as e:
162
- logger.warning(f'Não foi possível salvar emocoes_negativas: {e}')
163
-
164
- try:
165
- self.db.salvar_info_geral('girias_geral', ','.join(top_girias))
166
- except Exception as e:
167
- logger.warning(f'Não foi possível salvar girias_geral: {e}')
168
-
169
- try:
170
- self.db.salvar_info_geral('tom_rude', str(tom_rude))
171
- except Exception as e:
172
- logger.warning(f'Não foi possível salvar tom_rude: {e}')
173
-
174
- # gerar embeddings se possível
175
- self._ensure_model()
176
- if self._model:
177
- try:
178
- # remove vazios
179
- sentences = [s for s in sentences_for_embeddings if s and s.strip()]
180
- # limitar para evitar uso excessivo de memória
181
- sentences = sentences[:512]
182
- embeddings = self._model.encode(sentences)
183
- for s, emb in zip(sentences, embeddings):
184
- try:
185
- self.db.salvar_embedding(s, emb.tobytes())
186
- except Exception:
187
- # salvar_embedding deve existir no Database
188
- pass
189
- logger.info('Embeddings gerados e salvos (parciais).')
190
- except Exception as e:
191
- logger.warning(f'Erro ao gerar embeddings no treino: {e}')
192
-
193
- # NOVO: Análise avançada de aprendizado por usuário
194
- self._analisar_aprendizado_por_usuario(data)
195
-
196
- # marcar último treino
197
- try:
198
- self.db.salvar_info_geral('ultimo_treino', str(time.time()))
199
- except Exception:
200
- pass
201
- logger.info('Ciclo de treinamento finalizado.')
202
-
203
- def _analisar_aprendizado_por_usuario(self, data: List[tuple]):
204
- """Analisa aprendizado específico por usuário para adaptação dinâmica"""
205
- usuarios_unicos = set()
206
- for _, numero, _, _ in data:
207
- usuarios_unicos.add(numero)
208
-
209
- for numero_usuario in usuarios_unicos:
210
- # Buscar mensagens recentes do usuário
211
- mensagens_usuario = self._fetch_user_messages(numero_usuario, limit=50)
212
-
213
- if not mensagens_usuario:
214
- continue
215
-
216
- # Analisar emoções e tom
217
- analise_emocional = self._analisar_emocoes_usuario(mensagens_usuario)
218
- tom_predominante = self._detectar_tom_usuario(mensagens_usuario, numero_usuario)
219
-
220
- # Aprender gírias específicas do usuário
221
- girias_aprendidas = self._aprender_girias_usuario(numero_usuario, mensagens_usuario)
222
-
223
- # Salvar aprendizados específicos
224
- self._salvar_aprendizados_usuario(numero_usuario, analise_emocional, tom_predominante, girias_aprendidas)
225
-
226
- def _fetch_user_messages(self, numero_usuario: str, limit: int = 50) -> List[tuple]:
227
- """Busca mensagens recentes de um usuário específico"""
228
- rows = []
229
- try:
230
- conn = sqlite3.connect(self.db.db_path)
231
- c = conn.cursor()
232
- c.execute('SELECT mensagem, resposta FROM mensagens WHERE numero=? ORDER BY id DESC LIMIT ?',
233
- (numero_usuario, limit))
234
- rows = c.fetchall()
235
- conn.close()
236
- except Exception as e:
237
- logger.error(f'Erro ao buscar mensagens do usuário {numero_usuario}: {e}')
238
- return rows
239
-
240
- def _analisar_emocoes_usuario(self, mensagens: List[tuple]) -> Dict[str, Any]:
241
- """Analisa emoções nas mensagens de um usuário"""
242
- emocao_counter = collections.Counter()
243
- intensidade_total = 0
244
- total_mensagens = len(mensagens)
245
-
246
- for mensagem, resposta in mensagens:
247
- texto = (mensagem or '') + ' ' + (resposta or '')
248
- analise = self.db.analisar_emocoes_mensagem(texto)
249
- emocao_counter[analise['emocao']] += 1
250
- intensidade_total += analise['intensidade']
251
-
252
- emocao_predominante = emocao_counter.most_common(1)[0][0] if emocao_counter else 'neutro'
253
- intensidade_media = intensidade_total / total_mensagens if total_mensagens > 0 else 0
254
-
255
- return {
256
- 'emocao_predominante': emocao_predominante,
257
- 'intensidade_media': intensidade_media,
258
- 'distribuicao_emocoes': dict(emocao_counter)
259
- }
260
-
261
- def _detectar_tom_usuario(self, mensagens: List[tuple], numero_usuario: str = '') -> str:
262
- """Detecta o tom predominante do usuário"""
263
- # Usuários privilegiados sempre têm tom formal
264
- if numero_usuario in self.privileged_users:
265
- return 'formal'
266
-
267
- tom_counter = collections.Counter()
268
-
269
- for mensagem, _ in mensagens:
270
- mensagem_lower = (mensagem or '').lower()
271
-
272
- # Detectar tom rude
273
- if any(palavra in mensagem_lower for palavra in PALAVRAS_RUDES):
274
- tom_counter['rude'] += 1
275
- # Detectar tom formal
276
- elif any(palavra in mensagem_lower for palavra in ['por favor', 'obrigado', 'desculpe', 'com licença', 'senhor', 'senhora', 'prezado', 'estimado']):
277
- tom_counter['formal'] += 1
278
- # Detectar tom casual
279
- elif any(palavra in mensagem_lower for palavra in GIRIAS_ANGOLANAS):
280
- tom_counter['casual'] += 1
281
- else:
282
- tom_counter['neutro'] += 1
283
-
284
- return tom_counter.most_common(1)[0][0] if tom_counter else 'neutro'
285
-
286
- def _aprender_girias_usuario(self, numero_usuario: str, mensagens: List[tuple]) -> List[Dict[str, Any]]:
287
- """Aprende gírias específicas do usuário"""
288
- girias_novas = []
289
-
290
- for mensagem, resposta in mensagens:
291
- texto = (mensagem or '') + ' ' + (resposta or '')
292
- tokens = [t for t in re.split(r'\W+', texto.lower()) if len(t) > 2]
293
-
294
- for token in tokens:
295
- # Verificar se é uma possível gíria (não está em dicionário comum)
296
- if (token not in PALAVRAS_POSITIVAS and
297
- token not in PALAVRAS_NEGATIVAS and
298
- len(token) > 2 and
299
- not token.isdigit()):
300
-
301
- # Verificar frequência e contexto
302
- if self._eh_giria_potencial(token, mensagens):
303
- significado_inferido = self._inferir_significado_giria(token, mensagens)
304
-
305
- if significado_inferido:
306
- girias_novas.append({
307
- 'giria': token,
308
- 'significado': significado_inferido,
309
- 'contexto': self._extrair_contexto_giria(token, mensagens)
310
- })
311
-
312
- # Salvar no banco
313
- self.db.salvar_giria_aprendida(numero_usuario, token, significado_inferido,
314
- self._extrair_contexto_giria(token, mensagens))
315
-
316
- return girias_novas
317
-
318
- def _eh_giria_potencial(self, palavra: str, mensagens: List[tuple], threshold: int = 3) -> bool:
319
- """Verifica se uma palavra é uma gíria potencial baseada na frequência"""
320
- count = 0
321
- for mensagem, resposta in mensagens:
322
- texto = (mensagem or '') + ' ' + (resposta or '')
323
- count += texto.lower().count(palavra.lower())
324
-
325
- return count >= threshold
326
-
327
- def _inferir_significado_giria(self, giria: str, mensagens: List[tuple]) -> Optional[str]:
328
- """Tenta inferir o significado de uma gíria baseada no contexto"""
329
- contextos = []
330
-
331
- for mensagem, resposta in mensagens:
332
- texto = (mensagem or '') + ' ' + (resposta or '')
333
- if giria.lower() in texto.lower():
334
- # Extrair contexto ao redor da gíria
335
- palavras = texto.split()
336
- try:
337
- idx = next(i for i, p in enumerate(palavras) if p.lower() == giria.lower())
338
- contexto = ' '.join(palavras[max(0, idx-3):idx+4])
339
- contextos.append(contexto)
340
- except (StopIteration, ValueError):
341
- continue
342
-
343
- if not contextos:
344
- return None
345
-
346
- # Análise simples: se aparece com exclamação, pode ser admiração
347
- if any('!' in ctx for ctx in contextos):
348
- return "expressão de admiração ou surpresa"
349
-
350
- # Se aparece com interrogação, pode ser dúvida
351
- if any('?' in ctx for ctx in contextos):
352
- return "expressão de dúvida ou confusão"
353
-
354
- # Default: expressão emocional
355
- return "expressão emocional ou gíria local"
356
-
357
- def _extrair_contexto_giria(self, giria: str, mensagens: List[tuple]) -> str:
358
- """Extrai contexto de uso da gíria"""
359
- contextos = []
360
- for mensagem, resposta in mensagens:
361
- texto = (mensagem or '') + ' ' + (resposta or '')
362
- if giria.lower() in texto.lower():
363
- contextos.append(texto[:100] + '...' if len(texto) > 100 else texto)
364
-
365
- return '; '.join(contextos[:3]) # Limitar a 3 exemplos
366
-
367
- def _salvar_aprendizados_usuario(self, numero_usuario: str, analise_emocional: Dict[str, Any],
368
- tom_predominante: str, girias_aprendidas: List[Dict[str, Any]]):
369
- """Salva os aprendizados específicos do usuário"""
370
- try:
371
- # Salvar emoção predominante
372
- self.db.salvar_aprendizado_detalhado(numero_usuario, 'emocao_predominante',
373
- analise_emocional['emocao_predominante'])
374
-
375
- # Salvar intensidade emocional
376
- self.db.salvar_aprendizado_detalhado(numero_usuario, 'intensidade_emocional',
377
- str(analise_emocional['intensidade_media']))
378
-
379
- # Salvar tom predominante
380
- self.db.registrar_tom_usuario(numero_usuario, tom_predominante,
381
- analise_emocional['intensidade_media'])
382
-
383
- # Salvar distribuição de emoções
384
- self.db.salvar_aprendizado_detalhado(numero_usuario, 'distribuicao_emocoes',
385
- json.dumps(analise_emocional['distribuicao_emocoes']))
386
-
387
- logger.info(f'Aprendizados salvos para usuário {numero_usuario}: emoção={analise_emocional["emocao_predominante"]}, tom={tom_predominante}')
388
-
389
- except Exception as e:
390
- logger.warning(f'Erro ao salvar aprendizados do usuário {numero_usuario}: {e}')
391
-
392
- def _run_loop(self):
393
- interval = max(1, self.interval_hours) * 3600
394
- logger.info(f'Treinamento periódico iniciado (interval_hours={self.interval_hours})')
395
- while self._running:
396
- try:
397
- self.train_once()
398
- except Exception as e:
399
- logger.exception(f'Erro no loop de treinamento: {e}')
400
- # dormir pelo intervalo configurado
401
- for _ in range(int(interval)):
402
- if not self._running:
403
- break
404
- time.sleep(1)
405
- logger.info('Treinamento periódico finalizado.')
406
-
407
- def start_periodic_training(self):
408
- if self._running:
409
- return
410
- self._running = True
411
- self._thread = threading.Thread(target=self._run_loop, daemon=True)
412
- self._thread.start()
413
-
414
- def stop(self):
415
- self._running = False
416
- if self._thread:
417
- self._thread.join(timeout=5)
 
1
+ # treinamento.py
2
+ import threading
3
+ import time
4
+ import logging
5
+ import sqlite3
6
+ import re
7
+ import json
8
+ import collections
9
+ from typing import Optional, Any, List, Dict, Tuple
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # MODELO MAIS PESADO E ROBUSTO: paraphrase-multilingual-mpnet-base-v2
14
+ # - 110M parâmetros
15
+ # - Suporta 50+ idiomas (inclui português, gírias, sotaques)
16
+ # - Excelente em: semântica, intenção, emoção, ironia, contexto
17
+ # - Ideal para bots com "alma humana"
18
+ try:
19
+ from sentence_transformers import SentenceTransformer
20
+ # Força o modelo mais poderoso
21
+ MODEL_NAME = "sentence-transformers/paraphrase-multilingual-mpnet-base-v2"
22
+ logger.info(f"Carregando modelo pesado: {MODEL_NAME}")
23
+ except Exception as e:
24
+ logger.warning(f"sentence_transformers não disponível: {e}")
25
+ SentenceTransformer = None
26
+ MODEL_NAME = None
27
+
28
+ # Listas expandidas para análise emocional + gírias angolanas
29
+ PALAVRAS_POSITIVAS = [
30
+ 'bom', 'ótimo', 'incrível', 'maravilhoso', 'feliz', 'alegre', 'amor', 'gostar', 'adorei',
31
+ 'top', 'show', 'legal', 'bacana', 'fixe', 'bué', 'oroh', 'máximo', 'perfeito', 'genial',
32
+ 'divertido', 'hilário', 'gargalhada', 'rsrs', 'kkk', 'lol', 'haha', 'amo', 'adoro'
33
+ ]
34
+
35
+ PALAVRAS_NEGATIVAS = [
36
+ 'ruim', 'péssimo', 'horrível', 'triste', 'ódio', 'raiva', 'chateado', 'detesto', 'odeio',
37
+ 'merda', 'porra', 'caralho', 'puto', 'foda-se', 'tristeza', 'depressão', 'sofrimento',
38
+ 'choro', 'lágrima', 'dor', 'sofrer', 'fracasso', 'perdi', 'derrota'
39
+ ]
40
+
41
+ GIRIAS_ANGOLANAS = [
42
+ 'mano', 'puto', 'kkk', 'rsrs', 'lol', 'tô', 'cê', 'num', 'tipo', 'né', 'bah', 'uai',
43
+ 'oxe', 'eita', 'caramba', 'pqp', 'fdp', 'vsf', 'mlk', 'mwangolé', 'kota', 'mané',
44
+ 'oroh', 'bué', 'fixe', 'kota', 'baza', 'bazar', 'bazar fora', 'está fixe', 'está bué'
45
+ ]
46
+
47
+ PALAVRAS_RUDES = [
48
+ 'puto', 'merda', 'porra', 'caralho', 'cacete', 'fdp', 'vsf', 'mlk', 'arrombado',
49
+ 'viado', 'bicha', 'cu', 'buceta', 'rola', 'pau', 'bunda', 'peito', 'teta', 'bct',
50
+ 'pnc', 'pnctl', 'fuder', 'foder', 'transar', 'comer', 'chupar', 'mamada', 'boquete',
51
+ 'punheta', 'gozar', 'pqp', 'vai tomar no cu', 'vai se foder', 'seu filho da puta'
52
+ ]
53
+
54
+
55
+ class Treinamento:
56
+ """
57
+ Treinamento com o modelo MAIS PESADO E HUMANO:
58
+ - paraphrase-multilingual-mpnet-base-v2
59
+ - Aprendizado em tempo real + periódico
60
+ - Detecta: intenção, emoção, ironia, gírias, tom, contexto
61
+ """
62
+
63
+ def __init__(self, db, contexto: Optional[Any] = None, interval_hours: int = 1):
64
+ self.db = db
65
+ self.contexto = contexto
66
+ self.interval_hours = interval_hours
67
+ self._thread = None
68
+ self._running = False
69
+ self._model = None
70
+ self.privileged_users = ['244937035662', 'isaac', 'isaac quarenta', 'ceo', 'fundador']
71
+
72
+ # ================================================================
73
+ # CARREGAMENTO DO MODELO PESADO (com fallback)
74
+ # ================================================================
75
+
76
+ def _ensure_model(self):
77
+ if self._model is not None:
78
+ return
79
+ if self.contexto and hasattr(self.contexto, 'model') and self.contexto.model:
80
+ self._model = self.contexto.model
81
+ return
82
+ if SentenceTransformer is None or MODEL_NAME is None:
83
+ logger.warning("Modelo pesado não disponível. Usando análise heurística.")
84
+ return
85
+ try:
86
+ logger.info(f"Carregando modelo pesado: {MODEL_NAME} (pode demorar 10-20s)...")
87
+ self._model = SentenceTransformer(MODEL_NAME)
88
+ logger.info("Modelo pesado carregado com sucesso! Akira agora é mais humana.")
89
+ except Exception as e:
90
+ logger.error(f"Falha ao carregar modelo pesado: {e}")
91
+ self._model = None
92
+
93
+ # ================================================================
94
+ # APRENDIZADO EM TEMPO REAL
95
+ # ================================================================
96
+
97
+ def registrar_interacao(self, usuario: str, mensagem: str, resposta: str, numero: str = '', is_reply: bool = False, mensagem_original: str = ''):
98
+ """Registra + aprende na hora com modelo pesado"""
99
+ try:
100
+ self.db.salvar_mensagem(usuario, mensagem, resposta, numero, is_reply, mensagem_original)
101
+ self._aprender_em_tempo_real(numero, mensagem, resposta)
102
+ logger.info(f"Interação aprendida em tempo real: {numero}")
103
+ except Exception as e:
104
+ logger.warning(f'Erro ao registrar: {e}')
105
+
106
+ def _aprender_em_tempo_real(self, numero: str, msg: str, resp: str):
107
+ if not numero or numero == 'unknown':
108
+ return
109
+
110
+ texto = f"{msg} {resp}".lower()
111
+
112
+ # === ANÁLISE COM MODELO PESADO (se disponível) ===
113
+ self._ensure_model()
114
+ if self._model:
115
+ try:
116
+ # Embedding da mensagem completa
117
+ emb = self._model.encode(texto).tobytes()
118
+ self.db.salvar_embedding(texto, emb)
119
+
120
+ # Similaridade com frases emocionais (exemplo)
121
+ frases_emocao = {
122
+ "feliz": "estou muito feliz hoje",
123
+ "triste": "estou muito triste e sozinho",
124
+ "raiva": "estou puto com tudo",
125
+ "amor": "eu te amo muito"
126
+ }
127
+ embs_ref = self._model.encode(list(frases_emocao.values()))
128
+ sims = self._model.encode(texto) @ embs_ref.T
129
+ emocao_pred = list(frases_emocao.keys())[sims.argmax()]
130
+ intensidade = float(sims.max())
131
+ self.db.salvar_aprendizado_detalhado(numero, "emocao_ia", json.dumps({
132
+ "emocao": emocao_pred,
133
+ "intensidade": intensidade,
134
+ "fonte": "mpnet"
135
+ }))
136
+ except Exception as e:
137
+ logger.warning(f"Erro no modelo pesado: {e}")
138
+
139
+ # === ANÁLISE HEURÍSTICA (sempre) ===
140
+ rude = any(p in texto for p in PALAVRAS_RUDES)
141
+ tom = 'rude' if rude else 'casual'
142
+
143
+ palavras = [p for p in re.findall(r'\b\w{4,}\b', texto)
144
+ if p not in {'não', 'que', 'com', 'pra', 'pro', 'uma', 'ele', 'ela', 'isso'}]
145
+ contador = collections.Counter(palavras)
146
+ top_girias = [w for w, c in contador.most_common(5) if c > 1]
147
+
148
+ # Salvar tom
149
+ intensidade_tom = 0.8 if rude else 0.5
150
+ self.db.registrar_tom_usuario(numero, tom, intensidade_tom, texto[:100])
151
+
152
+ # Salvar gírias
153
+ for giria in top_girias:
154
+ significado = "gíria agressiva" if rude else "gíria local"
155
+ self.db.salvar_giria_aprendida(numero, giria, significado, texto[:100])
156
+
157
+ # ================================================================
158
+ # TREINAMENTO PERIÓDICO (a cada hora)
159
+ # ================================================================
160
+
161
+ def train_once(self):
162
+ logger.info("Iniciando treinamento periódico com modelo pesado...")
163
+ data = self._fetch_recent_data(limit=1000)
164
+ if not data:
165
+ logger.info("Nenhum dado para treinar.")
166
+ self._salvar_ultimo_treino()
167
+ return
168
+
169
+ usuarios = set(row[1] for row in data if row[1] and row[1].startswith('244'))
170
+ for numero in usuarios:
171
+ msgs = self._fetch_user_messages(numero, limit=50)
172
+ if len(msgs) < 3:
173
+ continue
174
+
175
+ analise = self._analisar_com_mpnet(msgs) if self._model else self._analisar_heuristica(msgs)
176
+ tom = self._detectar_tom_usuario(msgs, numero)
177
+
178
+ self.db.salvar_aprendizado_detalhado(numero, 'emocao_predominante', analise['emocao_predominante'])
179
+ self.db.salvar_aprendizado_detalhado(numero, 'intensidade_emocional', str(analise['intensidade_media']))
180
+ self.db.registrar_tom_usuario(numero, tom, analise['intensidade_media'])
181
+
182
+ self._gerar_embeddings_globais(data)
183
+ self._salvar_ultimo_treino()
184
+ logger.info("Treinamento concluído com sucesso.")
185
+
186
+ def _analisar_com_mpnet(self, mensagens: List[Tuple]) -> Dict:
187
+ """Análise emocional com modelo pesado"""
188
+ textos = [f"{m} {r}" for m, r in mensagens]
189
+ embs = self._model.encode(textos)
190
+
191
+ # Frases de referência
192
+ refs = {
193
+ "feliz": "estou muito feliz e animado",
194
+ "triste": "estou triste e deprimido",
195
+ "raiva": "estou com raiva e irritado",
196
+ "amor": "eu amo e adoro essa pessoa"
197
+ }
198
+ ref_embs = self._model.encode(list(refs.values()))
199
+ sims = embs @ ref_embs.T
200
+ emocoes = [list(refs.keys())[i] for i in sims.argmax(axis=1)]
201
+ intensidades = sims.max(axis=1)
202
+
203
+ counter = collections.Counter(emocoes)
204
+ return {
205
+ 'emocao_predominante': counter.most_common(1)[0][0],
206
+ 'intensidade_media': float(intensidades.mean())
207
+ }
208
+
209
+ def _analisar_heuristica(self, mensagens: List[Tuple]) -> Dict:
210
+ counter = collections.Counter()
211
+ intensidade = 0
212
+ total = len(mensagens)
213
+ for msg, resp in mensagens:
214
+ texto = (msg or '') + ' ' + (resp or '')
215
+ analise = self.db.analisar_emocoes_mensagem(texto)
216
+ counter[analise['emocao']] += 1
217
+ intensidade += analise['intensidade']
218
+ return {
219
+ 'emocao_predominante': counter.most_common(1)[0][0] if counter else 'neutro',
220
+ 'intensidade_media': intensidade / total if total > 0 else 0
221
+ }
222
+
223
+ def _detectar_tom_usuario(self, mensagens: List[Tuple], numero: str) -> str:
224
+ if numero in self.privileged_users:
225
+ return 'formal'
226
+ counter = collections.Counter()
227
+ for msg, _ in mensagens:
228
+ msg_lower = (msg or '').lower()
229
+ if any(p in msg_lower for p in PALAVRAS_RUDES):
230
+ counter['rude'] += 1
231
+ elif any(p in msg_lower for p in ['por favor', 'obrigado', 'senhor']):
232
+ counter['formal'] += 1
233
+ elif any(p in msg_lower for p in GIRIAS_ANGOLANAS):
234
+ counter['casual'] += 1
235
+ else:
236
+ counter['neutro'] += 1
237
+ return counter.most_common(1)[0][0] if counter else 'neutro'
238
+
239
+ def _gerar_embeddings_globais(self, data: List[Tuple]):
240
+ if not self._model:
241
+ return
242
+ sentences = [row[2] for row in data if row[2] and len(row[2]) > 10][:256]
243
+ try:
244
+ embeddings = self._model.encode(sentences)
245
+ for s, emb in zip(sentences, embeddings):
246
+ self.db.salvar_embedding(s, emb.tobytes())
247
+ except Exception as e:
248
+ logger.warning(f'Erro ao gerar embeddings globais: {e}')
249
+
250
+ def _fetch_recent_data(self, limit=1000) -> List[Tuple]:
251
+ rows = []
252
+ try:
253
+ conn = sqlite3.connect(self.db.db_path)
254
+ c = conn.cursor()
255
+ c.execute('''
256
+ SELECT usuario, numero, mensagem, resposta
257
+ FROM mensagens
258
+ WHERE resposta IS NOT NULL AND resposta != ''
259
+ AND numero IS NOT NULL AND numero != '' AND numero != 'unknown'
260
+ AND LENGTH(numero) >= 10 AND numero LIKE '244%'
261
+ ORDER BY id DESC LIMIT ?
262
+ ''', (limit,))
263
+ rows = c.fetchall()
264
+ conn.close()
265
+ except Exception as e:
266
+ logger.error(f'Erro ao buscar dados: {e}')
267
+ return rows
268
+
269
+ def _fetch_user_messages(self, numero: str, limit: int = 50) -> List[Tuple]:
270
+ rows = []
271
+ try:
272
+ conn = sqlite3.connect(self.db.db_path)
273
+ c = conn.cursor()
274
+ c.execute('SELECT mensagem, resposta FROM mensagens WHERE numero=? ORDER BY id DESC LIMIT ?', (numero, limit))
275
+ rows = c.fetchall()
276
+ conn.close()
277
+ except Exception as e:
278
+ logger.error(f'Erro ao buscar mensagens do usuário {numero}: {e}')
279
+ return rows
280
+
281
+ def _salvar_ultimo_treino(self):
282
+ try:
283
+ self.db.salvar_info_geral('ultimo_treino', str(time.time()))
284
+ except:
285
+ pass
286
+
287
+ # ================================================================
288
+ # LOOP DE TREINAMENTO
289
+ # ================================================================
290
+
291
+ def _run_loop(self):
292
+ interval = max(1, self.interval_hours) * 3600
293
+ logger.info(f"Treinamento periódico iniciado (a cada {self.interval_hours}h)")
294
+ while self._running:
295
+ try:
296
+ self.train_once()
297
+ except Exception as e:
298
+ logger.exception(f"Erro no treinamento: {e}")
299
+ for _ in range(int(interval)):
300
+ if not self._running:
301
+ break
302
+ time.sleep(1)
303
+ logger.info("Treinamento periódico parado.")
304
+
305
+ def start_periodic_training(self):
306
+ if self._running:
307
+ return
308
+ self._running = True
309
+ self._thread = threading.Thread(target=self._run_loop, daemon=True)
310
+ self._thread.start()
311
+
312
+ def stop(self):
313
+ self._running = False
314
+ if self._thread:
315
+ self._thread.join(timeout=5)