Madras1 commited on
Commit
135faae
·
verified ·
1 Parent(s): 42a8ae6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +104 -132
app.py CHANGED
@@ -1,6 +1,6 @@
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
@@ -28,6 +28,7 @@ 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','à',
@@ -54,205 +55,176 @@ STOP_WORDS_PT = [
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
 
@@ -260,4 +232,4 @@ async def process_api(
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)}")
 
1
  # ==============================================================================
2
+ # API do AetherMap — VERSÃO IMPERIAL 3.0
3
+ # Backend aprimorado com análises por cluster e dados para gráficos detalhados.
4
  # ==============================================================================
5
 
6
  import numpy as np
 
28
  UMAP_N_NEIGHBORS = 30
29
  HDBSCAN_MIN_SIZE = 50
30
 
31
+ # Lista de stopwords expandida e mantida
32
  STOP_WORDS_PT = [
33
  'de','a','o','que','e','do','da','em','um','para','é','com','não','uma','os','no',
34
  'se','na','por','mais','as','dos','como','mas','foi','ao','ele','das','tem','à',
 
55
  ]
56
 
57
  # ==============================================================================
58
+ # MODELO — Carregado uma vez e reaproveitado
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
+ # FUNÇÕES AUXILIARES DE ANÁLISE (Isoladas e reutilizáveis)
68
  # ==============================================================================
 
69
  def preparar_textos(file_bytes: bytes, n_samples: int) -> List[str]:
70
  linhas = file_bytes.decode("utf-8", errors="ignore").splitlines()
71
  textos = [s for line in linhas if (s := line.strip()) and len(s.split()) > 3]
72
  return textos[:n_samples]
73
 
 
 
 
 
 
74
  def processar_pipeline(textos: List[str]) -> (pd.DataFrame, np.ndarray):
75
  print(f"[PIPELINE] Iniciando pipeline para {len(textos)} textos...")
 
76
  model = load_model()
77
+ embeddings = model.encode(textos, batch_size=BATCH_SIZE, show_progress_bar=False, convert_to_numpy=True)
78
+
79
+ reducer = umap.UMAP(n_components=3, n_neighbors=UMAP_N_NEIGHBORS, min_dist=0.0, metric="cosine", random_state=42)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  emb_3d = reducer.fit_transform(embeddings)
 
 
81
  emb_3d = StandardScaler().fit_transform(emb_3d)
82
+
 
 
83
  clusterer = hdbscan.HDBSCAN(min_cluster_size=HDBSCAN_MIN_SIZE)
84
  clusters = clusterer.fit_predict(emb_3d)
85
+
86
  df = pd.DataFrame({
87
+ "x": emb_3d[:, 0], "y": emb_3d[:, 1], "z": emb_3d[:, 2],
88
+ "full_text": textos, "cluster": clusters.astype(str)
 
 
 
89
  })
90
+
91
+ del reducer, clusterer, emb_3d; gc.collect()
 
 
92
  return df, embeddings
93
 
94
+ # <<< MODIFICAÇÃO >>> Retorna dados para gráficos
 
 
 
 
95
  def calcular_metricas(textos: List[str]) -> Dict[str, Any]:
96
  print("[METRICAS] Calculando métricas globais...")
97
+ if not textos: return {}
98
 
99
+ vectorizer_count = CountVectorizer(stop_words=STOP_WORDS_PT, max_features=20000)
100
+ vectorizer_tfidf = TfidfVectorizer(stop_words=STOP_WORDS_PT, max_features=20000)
101
+
102
  try:
103
+ counts_matrix = vectorizer_count.fit_transform(textos)
 
 
 
 
 
 
 
 
104
  tfidf_matrix = vectorizer_tfidf.fit_transform(textos)
105
+ except ValueError: # Corpus vazio ou só com stopwords
106
+ return {"riqueza_lexical": 0, "top_tfidf_palavras": [], "top_frequencia_palavras": [], "entropia": 0.0}
107
+
108
+ # Métricas de Frequência
109
+ vocab_count = vectorizer_count.get_feature_names_out()
110
+ contagens = counts_matrix.sum(axis=0).A1
111
+ top_idx_freq = np.argsort(contagens)[-10:][::-1]
112
+ top_frequencia = [{"palavra": vocab_count[i], "contagem": int(contagens[i])} for i in top_idx_freq]
113
+
114
+ # Métricas de TF-IDF
115
+ vocab_tfidf = vectorizer_tfidf.get_feature_names_out()
116
+ soma_tfidf = tfidf_matrix.sum(axis=0).A1
117
+ top_idx_tfidf = np.argsort(soma_tfidf)[-10:][::-1]
118
+ top_tfidf = [{"palavra": vocab_tfidf[i], "score": round(float(soma_tfidf[i]), 4)} for i in top_idx_tfidf]
119
+
120
  return {
121
+ "riqueza_lexical": len(vocab_count),
122
+ "top_tfidf_palavras": top_tfidf,
123
+ "top_frequencia_palavras": top_frequencia,
124
+ "entropia": float(entropy(contagens / contagens.sum(), base=2))
125
  }
126
 
127
+ # <<< MODIFICAÇÃO >>> Retorna dados para o histograma
 
 
 
 
128
  def encontrar_duplicados(df: pd.DataFrame, embeddings: np.ndarray) -> Dict[str, Any]:
129
  print("[DUPLICADOS] Detectando duplicados...")
130
+
 
131
  mask = df["full_text"].duplicated(keep=False)
132
+ grupos_exatos = {t: [int(i) for i in idxs] for t, idxs in df[mask].groupby("full_text").groups.items()}
133
+
 
 
 
 
 
 
 
 
134
  pares_semanticos = []
135
+ histograma = {"bins": [], "contagens": []}
136
+
137
+ if 2 < len(embeddings) < 5000:
138
  sim = cosine_similarity(embeddings)
139
+ triu_indices = np.triu_indices_from(sim, k=1)
140
+ sim_vetor = sim[triu_indices]
141
 
142
+ # Pares para a lista de duplicados
143
+ pares_idx = np.where(sim_vetor > 0.98)[0]
144
+ for i in pares_idx:
145
+ idx1, idx2 = triu_indices[0][i], triu_indices[1][i]
146
  if df["full_text"].iloc[idx1] != df["full_text"].iloc[idx2]:
147
  pares_semanticos.append({
148
+ "doc1_idx": int(idx1), "doc2_idx": int(idx2),
 
149
  "similaridade": float(sim[idx1, idx2]),
150
+ "texto1": df["full_text"].iloc[idx1], "texto2": df["full_text"].iloc[idx2],
 
151
  })
152
+
153
+ # Histograma para o gráfico
154
+ contagens, bin_edges = np.histogram(sim_vetor, bins=np.arange(0.8, 1.01, 0.05))
155
+ histograma["bins"] = [f"{b:.2f}-{e:.2f}" for b, e in zip(bin_edges[:-1], bin_edges[1:])]
156
+ histograma["contagens"] = contagens.tolist()
157
 
158
  return {
159
  "grupos_exatos": grupos_exatos,
160
+ "pares_semanticos": pares_semanticos,
161
+ "similaridade_histograma": histograma
162
  }
163
 
164
+ # <<< NOVA FUNÇÃO >>> Cria o brasão de armas de cada cluster
165
+ def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
166
+ print("[CLUSTERS] Analisando clusters individualmente...")
167
+ analise = {}
168
+ ids_clusters_validos = sorted([c for c in df["cluster"].unique() if c != "-1"], key=int)
169
+
170
+ for cid in ids_clusters_validos:
171
+ textos_cluster = df[df["cluster"] == cid]["full_text"].tolist()
172
+ if len(textos_cluster) < 2: continue
173
+
174
+ try:
175
+ vectorizer = TfidfVectorizer(stop_words=STOP_WORDS_PT, max_features=1000)
176
+ tfidf_matrix = vectorizer.fit_transform(textos_cluster)
177
+ vocab = vectorizer.get_feature_names_out()
178
+ soma = tfidf_matrix.sum(axis=0).A1
179
+ top_idx = np.argsort(soma)[-5:][::-1]
180
+ top_palavras = [{"palavra": vocab[i], "score": round(float(soma[i]), 4)} for i in top_idx]
181
+ except ValueError:
182
+ top_palavras = []
183
+
184
+ analise[cid] = {
185
+ "num_documentos": len(textos_cluster),
186
+ "top_palavras": top_palavras
187
+ }
188
+ return analise
189
 
190
  # ==============================================================================
191
  # FASTAPI — DEFINIÇÃO DA API
192
  # ==============================================================================
193
+ app = FastAPI(title="API do AetherMap (Versão Imperial)", version="3.0.0")
 
 
 
 
194
 
195
  @app.post("/process/")
196
+ async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)):
 
 
 
197
  print(f"[API] Requisição recebida para {file.filename} ({n_samples} amostras).")
 
198
  try:
199
  file_bytes = await file.read()
 
200
  textos = preparar_textos(file_bytes, n_samples)
201
+ if not textos: raise HTTPException(status_code=400, detail="Nenhum texto válido encontrado.")
 
202
 
203
  df, embeddings = processar_pipeline(textos)
204
+
205
+ # <<< MODIFICAÇÃO >>> Chamando todas as novas e aprimoradas funções de análise
206
+ metricas_globais = calcular_metricas(df["full_text"].tolist())
207
+ analise_de_duplicados = encontrar_duplicados(df, embeddings)
208
+ analise_por_cluster = analisar_clusters(df)
209
 
210
  n_clusters = len(df["cluster"].unique()) - (1 if "-1" in df["cluster"].unique() else 0)
211
+ n_ruido = int((df["cluster"] == "-1").sum())
212
 
213
+ # <<< MODIFICAÇÃO >>> Montando a resposta imperial, rica em dados
214
  resposta = {
215
  "metadata": {
216
  "filename": file.filename,
217
+ "num_documents_processed": len(df),
218
  "n_samples_requested": n_samples,
219
+ "num_clusters_found": n_clusters,
220
+ "num_noise_points": n_ruido,
221
  },
222
+ "metrics": metricas_globais,
223
+ "duplicates": analise_de_duplicados,
224
+ "cluster_analysis": analise_por_cluster, # NOVO
225
  "plot_data": df[["x", "y", "z", "cluster", "full_text"]].to_dict("records"),
226
  }
227
+
228
  print("[API] Processamento finalizado com sucesso.")
229
  return resposta
230
 
 
232
  import traceback
233
  print("[ERRO] ERRO CRÍTICO NA REQUISIÇÃO:", e)
234
  traceback.print_exc()
235
+ raise HTTPException(status_code=500, detail=f"Erro interno do servidor: {str(e)}")