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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -74
app.py CHANGED
@@ -1,12 +1,13 @@
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
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
@@ -21,14 +22,18 @@ 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
- # 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','à',
@@ -54,8 +59,9 @@ STOP_WORDS_PT = [
54
  'teremos','terão','teria','teríamos','teriam','dá','pergunta','resposta'
55
  ]
56
 
 
57
  # ==============================================================================
58
- # MODELO Carregado uma vez e reaproveitado
59
  # ==============================================================================
60
  @lru_cache(maxsize=1)
61
  def load_model():
@@ -63,114 +69,77 @@ def load_model():
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)
@@ -180,21 +149,18 @@ def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
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)
@@ -202,7 +168,11 @@ async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)
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)
@@ -210,26 +180,57 @@ async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)
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
 
231
  except Exception as e:
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)}")
 
1
  # ==============================================================================
2
+ # API do AetherMap — VERSÃO SEMÂNTICA 4.0
3
+ # Backend com busca semântica verdadeira via endpoint /search/
4
  # ==============================================================================
5
 
6
  import numpy as np
7
  import pandas as pd
8
  import torch
9
  import gc
10
+ import uuid
11
 
12
  from fastapi import FastAPI, UploadFile, File, Form, HTTPException
13
  from typing import List, Dict, Any
 
22
  from scipy.stats import entropy
23
 
24
  # ==============================================================================
25
+ # CONFIGURAÇÕES GERAIS E CACHE
26
  # ==============================================================================
27
  DEFAULT_MODEL = "all-MiniLM-L6-v2"
28
  BATCH_SIZE = 256
29
  UMAP_N_NEIGHBORS = 30
30
  HDBSCAN_MIN_SIZE = 50
31
 
32
+ # <<< NOVA CÂMARA DO TESOURO >>>
33
+ # Guarda os embeddings e textos do último processamento.
34
+ # Em produção real com múltiplos usuários, usaríamos Redis ou um DB vetorial.
35
+ cache: Dict[str, Any] = {}
36
+
37
  STOP_WORDS_PT = [
38
  'de','a','o','que','e','do','da','em','um','para','é','com','não','uma','os','no',
39
  'se','na','por','mais','as','dos','como','mas','foi','ao','ele','das','tem','à',
 
59
  'teremos','terão','teria','teríamos','teriam','dá','pergunta','resposta'
60
  ]
61
 
62
+
63
  # ==============================================================================
64
+ # MODELO E FUNÇÕES DE ANÁLISE (como antes)
65
  # ==============================================================================
66
  @lru_cache(maxsize=1)
67
  def load_model():
 
69
  print(f"[MODEL] Carregando modelo '{DEFAULT_MODEL}' em: {device}")
70
  return SentenceTransformer(DEFAULT_MODEL, device=device)
71
 
72
+ # ... (Todas as funções de análise: preparar_textos, processar_pipeline, calcular_metricas, etc. permanecem exatamente as mesmas da versão anterior)
 
 
73
  def preparar_textos(file_bytes: bytes, n_samples: int) -> List[str]:
74
+ # ... (código inalterado)
75
  linhas = file_bytes.decode("utf-8", errors="ignore").splitlines()
76
  textos = [s for line in linhas if (s := line.strip()) and len(s.split()) > 3]
77
  return textos[:n_samples]
78
 
79
  def processar_pipeline(textos: List[str]) -> (pd.DataFrame, np.ndarray):
80
+ # ... (código inalterado)
81
  print(f"[PIPELINE] Iniciando pipeline para {len(textos)} textos...")
82
  model = load_model()
83
  embeddings = model.encode(textos, batch_size=BATCH_SIZE, show_progress_bar=False, convert_to_numpy=True)
 
84
  reducer = umap.UMAP(n_components=3, n_neighbors=UMAP_N_NEIGHBORS, min_dist=0.0, metric="cosine", random_state=42)
85
  emb_3d = reducer.fit_transform(embeddings)
86
  emb_3d = StandardScaler().fit_transform(emb_3d)
 
87
  clusterer = hdbscan.HDBSCAN(min_cluster_size=HDBSCAN_MIN_SIZE)
88
  clusters = clusterer.fit_predict(emb_3d)
89
+ df = pd.DataFrame({"x": emb_3d[:, 0], "y": emb_3d[:, 1], "z": emb_3d[:, 2], "full_text": textos, "cluster": clusters.astype(str)})
 
 
 
 
 
90
  del reducer, clusterer, emb_3d; gc.collect()
91
  return df, embeddings
92
 
 
93
  def calcular_metricas(textos: List[str]) -> Dict[str, Any]:
94
+ # ... (código inalterado)
95
  print("[METRICAS] Calculando métricas globais...")
96
  if not textos: return {}
 
97
  vectorizer_count = CountVectorizer(stop_words=STOP_WORDS_PT, max_features=20000)
98
  vectorizer_tfidf = TfidfVectorizer(stop_words=STOP_WORDS_PT, max_features=20000)
 
99
  try:
100
  counts_matrix = vectorizer_count.fit_transform(textos)
101
  tfidf_matrix = vectorizer_tfidf.fit_transform(textos)
102
+ except ValueError:
103
  return {"riqueza_lexical": 0, "top_tfidf_palavras": [], "top_frequencia_palavras": [], "entropia": 0.0}
 
 
104
  vocab_count = vectorizer_count.get_feature_names_out()
105
  contagens = counts_matrix.sum(axis=0).A1
106
  top_idx_freq = np.argsort(contagens)[-10:][::-1]
107
  top_frequencia = [{"palavra": vocab_count[i], "contagem": int(contagens[i])} for i in top_idx_freq]
 
 
108
  vocab_tfidf = vectorizer_tfidf.get_feature_names_out()
109
  soma_tfidf = tfidf_matrix.sum(axis=0).A1
110
  top_idx_tfidf = np.argsort(soma_tfidf)[-10:][::-1]
111
  top_tfidf = [{"palavra": vocab_tfidf[i], "score": round(float(soma_tfidf[i]), 4)} for i in top_idx_tfidf]
112
+ return {"riqueza_lexical": len(vocab_count), "top_tfidf_palavras": top_tfidf, "top_frequencia_palavras": top_frequencia, "entropia": float(entropy(contagens / contagens.sum(), base=2))}
113
+
 
 
 
 
 
 
 
114
  def encontrar_duplicados(df: pd.DataFrame, embeddings: np.ndarray) -> Dict[str, Any]:
115
+ # ... (código inalterado)
116
  print("[DUPLICADOS] Detectando duplicados...")
 
117
  mask = df["full_text"].duplicated(keep=False)
118
  grupos_exatos = {t: [int(i) for i in idxs] for t, idxs in df[mask].groupby("full_text").groups.items()}
 
119
  pares_semanticos = []
120
  histograma = {"bins": [], "contagens": []}
 
121
  if 2 < len(embeddings) < 5000:
122
  sim = cosine_similarity(embeddings)
123
  triu_indices = np.triu_indices_from(sim, k=1)
124
  sim_vetor = sim[triu_indices]
 
 
125
  pares_idx = np.where(sim_vetor > 0.98)[0]
126
  for i in pares_idx:
127
  idx1, idx2 = triu_indices[0][i], triu_indices[1][i]
128
  if df["full_text"].iloc[idx1] != df["full_text"].iloc[idx2]:
129
+ pares_semanticos.append({"doc1_idx": int(idx1), "doc2_idx": int(idx2), "similaridade": float(sim[idx1, idx2]), "texto1": df["full_text"].iloc[idx1], "texto2": df["full_text"].iloc[idx2],})
 
 
 
 
 
 
130
  contagens, bin_edges = np.histogram(sim_vetor, bins=np.arange(0.8, 1.01, 0.05))
131
  histograma["bins"] = [f"{b:.2f}-{e:.2f}" for b, e in zip(bin_edges[:-1], bin_edges[1:])]
132
  histograma["contagens"] = contagens.tolist()
133
+ return {"grupos_exatos": grupos_exatos, "pares_semanticos": pares_semanticos, "similaridade_histograma": histograma}
134
 
 
 
 
 
 
 
 
135
  def analisar_clusters(df: pd.DataFrame) -> Dict[str, Any]:
136
+ # ... (código inalterado)
137
  print("[CLUSTERS] Analisando clusters individualmente...")
138
  analise = {}
139
  ids_clusters_validos = sorted([c for c in df["cluster"].unique() if c != "-1"], key=int)
 
140
  for cid in ids_clusters_validos:
141
  textos_cluster = df[df["cluster"] == cid]["full_text"].tolist()
142
  if len(textos_cluster) < 2: continue
 
143
  try:
144
  vectorizer = TfidfVectorizer(stop_words=STOP_WORDS_PT, max_features=1000)
145
  tfidf_matrix = vectorizer.fit_transform(textos_cluster)
 
149
  top_palavras = [{"palavra": vocab[i], "score": round(float(soma[i]), 4)} for i in top_idx]
150
  except ValueError:
151
  top_palavras = []
152
+ analise[cid] = {"num_documentos": len(textos_cluster), "top_palavras": top_palavras}
 
 
 
 
153
  return analise
154
 
155
+
156
  # ==============================================================================
157
  # FASTAPI — DEFINIÇÃO DA API
158
  # ==============================================================================
159
+ app = FastAPI(title="API do AetherMap (Versão Semântica)", version="4.0.0")
160
 
161
  @app.post("/process/")
162
  async def process_api(n_samples: int = Form(10000), file: UploadFile = File(...)):
163
+ print(f"[API /process] Requisição recebida para {file.filename}.")
164
  try:
165
  file_bytes = await file.read()
166
  textos = preparar_textos(file_bytes, n_samples)
 
168
 
169
  df, embeddings = processar_pipeline(textos)
170
 
171
+ # <<< MODIFICAÇÃO >>> Guardando o resultado na Câmara do Tesouro
172
+ job_id = str(uuid.uuid4()) # Gera um ID único para esta análise
173
+ cache[job_id] = {"embeddings": embeddings, "df": df}
174
+ print(f"[CACHE] Resultados salvos no cache com o ID: {job_id}")
175
+
176
  metricas_globais = calcular_metricas(df["full_text"].tolist())
177
  analise_de_duplicados = encontrar_duplicados(df, embeddings)
178
  analise_por_cluster = analisar_clusters(df)
 
180
  n_clusters = len(df["cluster"].unique()) - (1 if "-1" in df["cluster"].unique() else 0)
181
  n_ruido = int((df["cluster"] == "-1").sum())
182
 
 
183
  resposta = {
184
+ "job_id": job_id, # Enviamos o ID para o frontend
185
+ "metadata": {"filename": file.filename, "num_documents_processed": len(df), "num_clusters_found": n_clusters, "num_noise_points": n_ruido},
 
 
 
 
 
186
  "metrics": metricas_globais,
187
  "duplicates": analise_de_duplicados,
188
+ "cluster_analysis": analise_por_cluster,
189
  "plot_data": df[["x", "y", "z", "cluster", "full_text"]].to_dict("records"),
190
  }
191
 
192
+ print("[API /process] Processamento finalizado com sucesso.")
193
  return resposta
194
 
195
  except Exception as e:
196
  import traceback
197
+ print("[ERRO] ERRO CRÍTICO EM /process:", e)
198
+ traceback.print_exc()
199
+ raise HTTPException(status_code=500, detail=f"Erro interno do servidor: {str(e)}")
200
+
201
+
202
+ # <<< NOVO FEITIÇO IMPERIAL >>>
203
+ @app.post("/search/")
204
+ async def search_api(query: str = Form(...), job_id: str = Form(...)):
205
+ print(f"[API /search] Busca recebida para a query '{query}' no job '{job_id}'.")
206
+
207
+ if job_id not in cache:
208
+ raise HTTPException(status_code=404, detail="Job ID não encontrado ou expirado. Por favor, processe um novo arquivo.")
209
+
210
+ try:
211
+ model = load_model()
212
+ cached_data = cache[job_id]
213
+ corpus_embeddings = cached_data["embeddings"]
214
+
215
+ # 1. Cria o embedding da busca
216
+ query_embedding = model.encode([query], convert_to_numpy=True)
217
+
218
+ # 2. Calcula a similaridade de cosseno
219
+ similarities = cosine_similarity(query_embedding, corpus_embeddings)[0]
220
+
221
+ # 3. Encontra os top 100 resultados com score acima de um limiar
222
+ top_indices = np.argsort(similarities)[-100:][::-1]
223
+
224
+ results = [
225
+ {"index": int(i), "score": float(similarities[i])}
226
+ for i in top_indices if similarities[i] > 0.3
227
+ ]
228
+
229
+ print(f"[API /search] Encontrados {len(results)} resultados semanticamente relevantes.")
230
+ return {"results": results}
231
+
232
+ except Exception as e:
233
+ import traceback
234
+ print("[ERRO] ERRO CRÍTICO EM /search:", e)
235
  traceback.print_exc()
236
+ raise HTTPException(status_code=500, detail=f"Erro interno na busca semântica: {str(e)}")