Madras1 commited on
Commit
16f73f1
·
verified ·
1 Parent(s): 9869493

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +221 -101
app.py CHANGED
@@ -1,11 +1,17 @@
1
  # ==============================================================================
2
- # API de Análise de Textos com FastAPI (VERSÃO DEFINITIVA)
3
- # Arquivo: app.py
4
- # Backend para o AetherMap by Strand DataOps
5
  # ==============================================================================
6
- import streamlit as st # Usado apenas por seu excelente e conveniente mecanismo de cache!
7
  import numpy as np
8
  import pandas as pd
 
 
 
 
 
 
 
9
  from sentence_transformers import SentenceTransformer
10
  import umap
11
  import hdbscan
@@ -13,131 +19,245 @@ from sklearn.preprocessing import StandardScaler
13
  from sklearn.metrics.pairwise import cosine_similarity
14
  from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
15
  from scipy.stats import entropy
16
- import torch
17
- import gc
18
- from fastapi import FastAPI, UploadFile, File, Form, HTTPException
19
- from typing import Dict, Any, List
20
 
21
- # ================================
22
- # CONFIGURAÇÕES E CONSTANTES
23
- # ================================
24
- DEFAULT_MODEL = 'all-MiniLM-L6-v2'
25
  BATCH_SIZE = 256
26
  UMAP_N_NEIGHBORS = 30
27
  HDBSCAN_MIN_SIZE = 50
28
- STOP_WORDS_PT = ['de', 'a', 'o', 'que', 'e', 'do', 'da', 'em', 'um', 'para', 'é', 'com', 'não', 'uma', 'os', 'no', 'se', 'na', 'por', 'mais', 'as', 'dos', 'como', 'mas', 'foi', 'ao', 'ele', 'das', 'tem', 'à', 'seu', 'sua', 'ou', 'ser', 'quando', 'muito', 'há', 'nos', 'já', 'está', 'eu', 'também', 'só', 'pelo', 'pela', 'até', 'isso', 'ela', 'entre', 'era', 'depois', 'sem', 'mesmo', 'aos', 'ter', 'seus', 'quem', 'nas', 'me', 'esse', 'eles', 'estão', 'você', 'tinha', 'foram', 'essa', 'num', 'nem', 'suas', 'meu', 'às', 'minha', 'numa', 'pelos', 'elas', 'havia', 'seja', 'qual', 'será', 'nós', 'tenho', 'lhe', 'deles', 'essas', 'esses', 'pelas', 'este', 'fosse', 'dele', 'tu', 'te', 'vocês', 'vos', 'lhes', 'meus', 'minhas', 'teu', 'tua', 'teus', 'tuas', 'nosso', 'nossa', 'nossos', 'nossas', 'dela', 'delas', 'esta', 'estes', 'estas', 'aquele', 'aquela', 'aqueles', 'aquelas', 'isto', 'aquilo', 'estou', 'está', 'estamos', 'estão', 'estive', 'esteve', 'estivemos', 'estiveram', 'estava', 'estávamos', 'estavam', 'estivera', 'estivéramos', 'esteja', 'estejamos', 'estejam', 'estivesse', 'estivéssemos', 'estivessem', 'estiver', 'estivermos', 'estiverem', 'hei', 'há', 'havemos', 'hão', 'houve', 'houvemos', 'houveram', 'houvera', 'houvéramos', 'haja', 'hajamos', 'hajam', 'houvesse', 'houvéssemos', 'houvessem', 'houver', 'houvermos', 'houverem', 'houverei', 'houverá', 'houveremos', 'houverão', 'houveria', 'houveríamos', 'houveriam', 'sou', 'somos', 'são', 'era', 'éramos', 'eram', 'fui', 'foi', 'fomos', 'foram', 'fora', 'fôramos', 'seja', 'sejamos', 'sejam', 'fosse', 'fôssemos', 'fossem', 'for', 'formos', 'forem', 'serei', 'será', 'seremos', 'serão', 'seria', 'seríamos', 'seriam', 'tenho', 'tem', 'temos', 'tém', 'tinha', 'tínhamos', 'tinham', 'tive', 'teve', 'tivemos', 'tiveram', 'tivera', 'tivéramos', 'tenha', 'tenhamos', 'tenham', 'tivesse', 'tivéssemos', 'tivessem', 'tiver', 'tivermos', 'tiverem', 'terei', 'terá', 'teremos', 'terão', 'teria', 'teríamos', 'teriam', 'dá', 'pergunta', 'resposta']
29
 
30
- # ================================
31
- # LÓGICA DE PROCESSAMENTO (Nossas funções, agora sem interface)
32
- # ================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- @st.cache_resource
35
  def load_model():
36
  device = "cuda" if torch.cuda.is_available() else "cpu"
37
- print(f"Carregando modelo para o dispositivo: {device}")
38
- model = SentenceTransformer(DEFAULT_MODEL, device=device)
39
- return model
40
-
41
- @st.cache_data
42
- def process_data_pipeline(file_bytes, n_samples):
43
- print("Iniciando o pipeline de processamento de dados...")
44
- lines = file_bytes.decode('utf-8').splitlines()
45
- _texts = [s for line in lines if (s := line.strip()) and len(s.split()) > 3][:n_samples]
46
- if not _texts: return None, None
 
 
 
 
 
 
 
 
 
 
 
47
  model = load_model()
48
- print("Gerando embeddings...")
49
- embeddings = model.encode(_texts, batch_size=BATCH_SIZE, show_progress_bar=False, convert_to_numpy=True)
50
- print("Reduzindo dimensionalidade com UMAP...")
51
- reducer = umap.UMAP(n_components=3, n_neighbors=UMAP_N_NEIGHBORS, min_dist=0.0, metric='cosine', random_state=42)
52
- embedding_3d = reducer.fit_transform(embeddings)
53
- embedding_3d = StandardScaler().fit_transform(embedding_3d)
54
- print("Clusterizando com HDBSCAN...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  clusterer = hdbscan.HDBSCAN(min_cluster_size=HDBSCAN_MIN_SIZE)
56
- clusters = clusterer.fit_predict(embedding_3d)
57
- print("Montando o DataFrame final...")
58
- df = pd.DataFrame({'x': embedding_3d[:, 0], 'y': embedding_3d[:, 1], 'z': embedding_3d[:, 2], 'full_text': _texts, 'cluster': clusters.astype(str)})
59
- del reducer, clusterer, embedding_3d
 
 
 
 
 
 
 
60
  gc.collect()
61
- print("Pipeline de processamento de dados concluído.")
62
  return df, embeddings
63
 
64
- @st.cache_data
65
- def calcular_metricas_globais_api(_texts: List[str]) -> Dict[str, Any]:
66
- print("Calculando métricas globais...")
 
 
 
 
 
 
67
  try:
68
- vectorizer_count = CountVectorizer(stop_words=STOP_WORDS_PT, max_features=20000).fit(_texts)
69
- riqueza_lexical = len(vectorizer_count.get_feature_names_out())
70
- except ValueError: riqueza_lexical = 0
 
 
 
 
71
  try:
72
- vectorizer_tfidf = TfidfVectorizer(stop_words=STOP_WORDS_PT, max_features=20000).fit(_texts)
73
- tfidf_matrix, vocab = vectorizer_tfidf.transform(_texts), vectorizer_tfidf.get_feature_names_out()
74
- soma_tfidf, indices_top_tfidf = tfidf_matrix.sum(axis=0).A1, np.argsort(tfidf_matrix.sum(axis=0).A1)[-10:][::-1]
75
- palavras_relevantes = [vocab[i] for i in indices_top_tfidf]
76
- except ValueError: palavras_relevantes = []
 
 
 
 
 
 
77
  try:
78
- contagens_palavras = np.array(vectorizer_count.transform(_texts).sum(axis=0)).flatten()
79
- entropia_corpus = entropy(contagens_palavras / np.sum(contagens_palavras), base=2)
80
- except (ValueError, ZeroDivisionError): entropia_corpus = 0.0
81
- return {"riqueza_lexical": int(riqueza_lexical), "palavras_relevantes": palavras_relevantes, "entropia": float(entropia_corpus)}
82
-
83
- @st.cache_data
84
- def encontrar_duplicados_api(_df: pd.DataFrame, _embeddings: np.ndarray, similaridade_minima: float = 0.98) -> Dict[str, Any]:
85
- print("Procurando por duplicados...")
86
- duplicados_exatos_mask = _df['full_text'].duplicated(keep=False)
87
- df_duplicados_exatos = _df[duplicados_exatos_mask].copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  grupos_exatos = {}
89
- if not df_duplicados_exatos.empty:
90
- grupos_exatos = {text: [int(i) for i in indices] for text, indices in df_duplicados_exatos.groupby('full_text').groups.items()}
 
 
 
 
 
91
  pares_semanticos = []
92
- limite_semantico = 5000
93
- if len(_embeddings) < limite_semantico:
94
- sim_matrix = cosine_similarity(_embeddings)
95
- indices_superiores = np.triu_indices_from(sim_matrix, k=1)
96
- pares_altamente_similares = sim_matrix[indices_superiores] > similaridade_minima
97
- indices_pares = np.where(pares_altamente_similares)[0]
98
- for i in indices_pares:
99
- idx1, idx2 = indices_superiores[0][i], indices_superiores[1][i]
100
- if _df['full_text'].iloc[int(idx1)] != _df['full_text'].iloc[int(idx2)]:
101
- pares_semanticos.append({'doc1_idx': int(idx1), 'doc2_idx': int(idx2), 'similaridade': float(sim_matrix[int(idx1), int(idx2)]), 'texto1': _df['full_text'].iloc[int(idx1)], 'texto2': _df['full_text'].iloc[int(idx2)]})
102
- return {"grupos_exatos": grupos_exatos, "pares_semanticos": pares_semanticos}
103
-
104
- # ================================
105
- # DEFINIÇÃO DA API COM FASTAPI
106
- # ================================
107
- app = FastAPI(title="API do AetherMap by Strand DataOps", version="1.1.0")
108
-
109
- @app.post("/process/", summary="Processa e Analisa um Arquivo de Texto")
110
- async def process_text_file(n_samples: int = Form(10000), file: UploadFile = File(...)):
111
- print(f"Recebida requisição para processar {n_samples} amostras do arquivo: {file.filename}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  try:
113
  file_bytes = await file.read()
114
- df, embeddings = process_data_pipeline(file_bytes, n_samples)
115
- if df is None:
116
- raise HTTPException(status_code=400, detail="Nenhum texto válido encontrado no arquivo.")
117
-
118
- metricas = calcular_metricas_globais_api(df['full_text'].tolist())
119
- analise_duplicidade = encontrar_duplicados_api(df, embeddings)
120
-
121
- plot_data = df[['x', 'y', 'z', 'cluster', 'full_text']].to_dict('records')
122
- n_clusters = len(df['cluster'].unique()) - (1 if '-1' in df['cluster'].unique() else 0)
123
- n_ruido = (df['cluster'] == '-1').sum()
124
-
125
- response = {
 
126
  "metadata": {
127
  "filename": file.filename,
128
  "num_documents_processed": int(len(df)),
129
- "n_samples_requested": int(n_samples),
130
  "num_clusters_found": int(n_clusters),
131
  "num_noise_points": int(n_ruido),
132
  },
133
  "metrics": metricas,
134
- "duplicates": analise_duplicidade,
135
- "plot_data": plot_data,
136
  }
137
- print("Processamento concluído com sucesso. Retornando resposta.")
138
- return response
 
 
139
  except Exception as e:
140
  import traceback
141
- print(f"Erro CRÍTICO durante o processamento: {e}")
142
  traceback.print_exc()
143
- raise HTTPException(status_code=500, detail=f"Ocorreu um erro interno no servidor: {str(e)}")
 
1
  # ==============================================================================
2
+ # API de Análise de Textos com FastAPI VERSÃO PROFISSIONAL
3
+ # Totalmente reescrita para estabilidade, paralelismo seguro e isolamento.
 
4
  # ==============================================================================
5
+
6
  import numpy as np
7
  import pandas as pd
8
+ import torch
9
+ import gc
10
+
11
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
12
+ from typing import List, Dict, Any
13
+ from functools import lru_cache
14
+
15
  from sentence_transformers import SentenceTransformer
16
  import umap
17
  import hdbscan
 
19
  from sklearn.metrics.pairwise import cosine_similarity
20
  from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
21
  from scipy.stats import entropy
 
 
 
 
22
 
23
+ # ==============================================================================
24
+ # CONFIGURAÇÕES GERAIS
25
+ # ==============================================================================
26
+ DEFAULT_MODEL = "all-MiniLM-L6-v2"
27
  BATCH_SIZE = 256
28
  UMAP_N_NEIGHBORS = 30
29
  HDBSCAN_MIN_SIZE = 50
 
30
 
31
+ STOP_WORDS_PT = [
32
+ 'de','a','o','que','e','do','da','em','um','para','é','com','não','uma','os','no',
33
+ 'se','na','por','mais','as','dos','como','mas','foi','ao','ele','das','tem','à',
34
+ 'seu','sua','ou','ser','quando','muito','há','nos','já','está','eu','também','só',
35
+ 'pelo','pela','até','isso','ela','entre','era','depois','sem','mesmo','aos','ter',
36
+ 'seus','quem','nas','me','esse','eles','estão','você','tinha','foram','essa','num',
37
+ 'nem','suas','meu','às','minha','numa','pelos','elas','havia','seja','qual','será',
38
+ 'nós','tenho','lhe','deles','essas','esses','pelas','este','fosse','dele','tu','te',
39
+ 'vocês','vos','lhes','meus','minhas','teu','tua','teus','tuas','nosso','nossa',
40
+ 'nossos','nossas','dela','delas','esta','estes','estas','aquele','aquela','aqueles',
41
+ 'aquelas','isto','aquilo','estou','está','estamos','estão','estive','esteve',
42
+ 'estivemos','estiveram','estava','estávamos','estavam','estivera','estivéramos',
43
+ 'esteja','estejamos','estejam','estivesse','estivéssemos','estivessem','estiver',
44
+ 'estivermos','estiverem','hei','há','havemos','hão','houve','houvemos','houveram',
45
+ 'houvera','houvéramos','haja','hajamos','hajam','houvesse','houvéssemos','houvessem',
46
+ 'houver','houvermos','houverem','houverei','houverá','houveremos','houverão',
47
+ 'houveria','houveríamos','houveriam','sou','somos','são','era','éramos','eram',
48
+ 'fui','foi','fomos','foram','fora','fôramos','seja','sejamos','sejam','fosse',
49
+ 'fôssemos','fossem','for','formos','forem','serei','será','seremos','serão','seria',
50
+ 'seríamos','seriam','tenho','tem','temos','tém','tinha','tínhamos','tinham','tive',
51
+ 'teve','tivemos','tiveram','tivera','tivéramos','tenha','tenhamos','tenham',
52
+ 'tivesse','tivéssemos','tivessem','tiver','tivermos','tiverem','terei','terá',
53
+ 'teremos','terão','teria','teríamos','teriam','dá','pergunta','resposta'
54
+ ]
55
+
56
+ # ==============================================================================
57
+ # MODELO — Carregado uma vez e reaproveitado (seguro e imutável)
58
+ # ==============================================================================
59
 
60
+ @lru_cache(maxsize=1)
61
  def load_model():
62
  device = "cuda" if torch.cuda.is_available() else "cpu"
63
+ print(f"[MODEL] Carregando modelo '{DEFAULT_MODEL}' em: {device}")
64
+ return SentenceTransformer(DEFAULT_MODEL, device=device)
65
+
66
+
67
+ # ==============================================================================
68
+ # FUNÇÃO: Preparar textos
69
+ # ==============================================================================
70
+
71
+ def preparar_textos(file_bytes: bytes, n_samples: int) -> List[str]:
72
+ linhas = file_bytes.decode("utf-8", errors="ignore").splitlines()
73
+ textos = [s for line in linhas if (s := line.strip()) and len(s.split()) > 3]
74
+ return textos[:n_samples]
75
+
76
+
77
+ # ==============================================================================
78
+ # FUNÇÃO: Pipeline principal (um por requisição, seguro)
79
+ # ==============================================================================
80
+
81
+ def processar_pipeline(textos: List[str]) -> (pd.DataFrame, np.ndarray):
82
+ print(f"[PIPELINE] Iniciando pipeline para {len(textos)} textos...")
83
+
84
  model = load_model()
85
+
86
+ # Embeddings
87
+ print("[PIPELINE] Gerando embeddings...")
88
+ embeddings = model.encode(
89
+ textos,
90
+ batch_size=BATCH_SIZE,
91
+ show_progress_bar=False,
92
+ convert_to_numpy=True
93
+ )
94
+
95
+ # UMAP
96
+ print("[PIPELINE] Reduzindo dimensionalidade com UMAP...")
97
+ reducer = umap.UMAP(
98
+ n_components=3,
99
+ n_neighbors=UMAP_N_NEIGHBORS,
100
+ min_dist=0.0,
101
+ metric="cosine",
102
+ random_state=42
103
+ )
104
+ emb_3d = reducer.fit_transform(embeddings)
105
+
106
+ # Normalize
107
+ emb_3d = StandardScaler().fit_transform(emb_3d)
108
+
109
+ # HDBSCAN
110
+ print("[PIPELINE] Clusterizando com HDBSCAN...")
111
  clusterer = hdbscan.HDBSCAN(min_cluster_size=HDBSCAN_MIN_SIZE)
112
+ clusters = clusterer.fit_predict(emb_3d)
113
+
114
+ df = pd.DataFrame({
115
+ "x": emb_3d[:, 0],
116
+ "y": emb_3d[:, 1],
117
+ "z": emb_3d[:, 2],
118
+ "full_text": textos,
119
+ "cluster": clusters.astype(str)
120
+ })
121
+
122
+ del reducer, clusterer, emb_3d
123
  gc.collect()
124
+
125
  return df, embeddings
126
 
127
+
128
+ # ==============================================================================
129
+ # FUNÇÃO: Métricas globais do corpus
130
+ # ==============================================================================
131
+
132
+ def calcular_metricas(textos: List[str]) -> Dict[str, Any]:
133
+ print("[METRICAS] Calculando métricas globais...")
134
+
135
+ # Riqueza lexical
136
  try:
137
+ vectorizer_count = CountVectorizer(stop_words=STOP_WORDS_PT, max_features=20000)
138
+ vectorizer_count.fit(textos)
139
+ riqueza = len(vectorizer_count.get_feature_names_out())
140
+ except ValueError:
141
+ riqueza = 0
142
+
143
+ # TF-IDF
144
  try:
145
+ vectorizer_tfidf = TfidfVectorizer(stop_words=STOP_WORDS_PT, max_features=20000)
146
+ tfidf_matrix = vectorizer_tfidf.fit_transform(textos)
147
+ vocab = vectorizer_tfidf.get_feature_names_out()
148
+
149
+ soma = tfidf_matrix.sum(axis=0).A1
150
+ top_idx = np.argsort(soma)[-10:][::-1]
151
+ palavras_relevantes = [vocab[i] for i in top_idx]
152
+ except ValueError:
153
+ palavras_relevantes = []
154
+
155
+ # Entropia
156
  try:
157
+ contagens = vectorizer_count.transform(textos).sum(axis=0).A1
158
+ ent = entropy(contagens / contagens.sum(), base=2)
159
+ except Exception:
160
+ ent = 0.0
161
+
162
+ return {
163
+ "riqueza_lexical": int(riqueza),
164
+ "palavras_relevantes": palavras_relevantes,
165
+ "entropia": float(ent)
166
+ }
167
+
168
+
169
+ # ==============================================================================
170
+ # FUNÇÃO: Duplicados exatos e semânticos
171
+ # ==============================================================================
172
+
173
+ def encontrar_duplicados(df: pd.DataFrame, embeddings: np.ndarray) -> Dict[str, Any]:
174
+ print("[DUPLICADOS] Detectando duplicados...")
175
+
176
+ # Duplicados exatos
177
+ mask = df["full_text"].duplicated(keep=False)
178
+ df_dup = df[mask]
179
+
180
  grupos_exatos = {}
181
+ if not df_dup.empty:
182
+ grupos_exatos = {
183
+ texto: [int(i) for i in indices]
184
+ for texto, indices in df_dup.groupby("full_text").groups.items()
185
+ }
186
+
187
+ # Duplicados semânticos (limite)
188
  pares_semanticos = []
189
+ limite = 5000
190
+
191
+ if len(embeddings) < limite:
192
+ sim = cosine_similarity(embeddings)
193
+ triu = np.triu_indices_from(sim, k=1)
194
+ pares = np.where(sim[triu] > 0.98)[0]
195
+
196
+ for i in pares:
197
+ idx1, idx2 = triu[0][i], triu[1][i]
198
+ if df["full_text"].iloc[idx1] != df["full_text"].iloc[idx2]:
199
+ pares_semanticos.append({
200
+ "doc1_idx": int(idx1),
201
+ "doc2_idx": int(idx2),
202
+ "similaridade": float(sim[idx1, idx2]),
203
+ "texto1": df["full_text"].iloc[idx1],
204
+ "texto2": df["full_text"].iloc[idx2],
205
+ })
206
+
207
+ return {
208
+ "grupos_exatos": grupos_exatos,
209
+ "pares_semanticos": pares_semanticos
210
+ }
211
+
212
+
213
+ # ==============================================================================
214
+ # FASTAPI — DEFINIÇÃO DA API
215
+ # ==============================================================================
216
+
217
+ app = FastAPI(
218
+ title="API do AetherMap (Versão Profissional)",
219
+ version="2.0.0",
220
+ )
221
+
222
+ @app.post("/process/")
223
+ async def process_api(
224
+ n_samples: int = Form(10000),
225
+ file: UploadFile = File(...)
226
+ ):
227
+ print(f"[API] Requisição recebida para {file.filename} ({n_samples} amostras).")
228
+
229
  try:
230
  file_bytes = await file.read()
231
+
232
+ textos = preparar_textos(file_bytes, n_samples)
233
+ if not textos:
234
+ raise HTTPException(status_code=400, detail="Nenhum texto válido encontrado.")
235
+
236
+ df, embeddings = processar_pipeline(textos)
237
+ metricas = calcular_metricas(df["full_text"].tolist())
238
+ duplicados = encontrar_duplicados(df, embeddings)
239
+
240
+ n_clusters = len(df["cluster"].unique()) - (1 if "-1" in df["cluster"].unique() else 0)
241
+ n_ruido = (df["cluster"] == "-1").sum()
242
+
243
+ resposta = {
244
  "metadata": {
245
  "filename": file.filename,
246
  "num_documents_processed": int(len(df)),
247
+ "n_samples_requested": n_samples,
248
  "num_clusters_found": int(n_clusters),
249
  "num_noise_points": int(n_ruido),
250
  },
251
  "metrics": metricas,
252
+ "duplicates": duplicados,
253
+ "plot_data": df[["x", "y", "z", "cluster", "full_text"]].to_dict("records"),
254
  }
255
+
256
+ print("[API] Processamento finalizado com sucesso.")
257
+ return resposta
258
+
259
  except Exception as e:
260
  import traceback
261
+ print("[ERRO] ERRO CRÍTICO NA REQUISIÇÃO:", e)
262
  traceback.print_exc()
263
+ raise HTTPException(status_code=500, detail=f"Erro interno do servidor: {str(e)}")