Madras1 commited on
Commit
88b1fb5
·
verified ·
1 Parent(s): d21f330

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +159 -220
app.py CHANGED
@@ -1,263 +1,202 @@
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
18
  from sklearn.preprocessing import StandardScaler
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)}")
 
1
  # ==============================================================================
2
+ # API de Análise de Textos com FastAPI (VERSÃO IMPERIAL 2.0)
3
+ # Arquivo: app.py
4
+ # Backend para o AetherMap by Strand DataOps
5
  # ==============================================================================
6
+ import streamlit as st
7
  import numpy as np
8
  import pandas as pd
 
 
 
 
 
 
 
9
  from sentence_transformers import SentenceTransformer
10
  import umap
11
  import hdbscan
12
  from sklearn.preprocessing import StandardScaler
13
  from sklearn.metrics.pairwise import cosine_similarity
14
+ from sklearn.feature_extraction.text import 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
+ from leia import SentimentIntensityAnalyzer # NOVO FEITIÇO: Importando o analisador de sentimento
21
 
22
+ # ================================
23
+ # CONFIGURAÇÕES E CONSTANTES
24
+ # ================================
25
+ DEFAULT_MODEL = 'all-MiniLM-L6-v2'
26
  BATCH_SIZE = 256
27
+ UMAP_N_NEIGHBORS = 15
28
+ HDBSCAN_MIN_SIZE = 20
29
+ HDBSCAN_MIN_SAMPLES = 5
30
+ 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']
31
 
32
+ # ================================
33
+ # LÓGICA DE PROCESSAMENTO
34
+ # ================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ @st.cache_resource
37
  def load_model():
38
  device = "cuda" if torch.cuda.is_available() else "cpu"
39
+ print(f"Carregando modelo para o dispositivo: {device}")
40
+ model = SentenceTransformer(DEFAULT_MODEL, device=device)
41
+ return model
42
+
43
+ @st.cache_data
44
+ def process_data_pipeline(file_bytes, n_samples):
45
+ print("Iniciando o pipeline de processamento de dados...")
46
+ lines = file_bytes.decode('utf-8').splitlines()
47
+ _texts = [s for line in lines if (s := line.strip()) and len(s.split()) > 3][:n_samples]
48
+ if not _texts: return None, None, None
49
+
 
 
 
 
 
 
 
 
 
 
50
  model = load_model()
51
+ print("Gerando embeddings...")
52
+ embeddings = model.encode(_texts, batch_size=BATCH_SIZE, show_progress_bar=False, convert_to_numpy=True)
53
+
54
+ print("Reduzindo dimensionalidade com UMAP...")
55
+ reducer = umap.UMAP(n_components=3, n_neighbors=UMAP_N_NEIGHBORS, min_dist=0.0, metric='cosine', random_state=42)
56
+ embedding_3d = reducer.fit_transform(embeddings)
57
+ embedding_3d = StandardScaler().fit_transform(embedding_3d)
58
+
59
+ print("Clusterizando com HDBSCAN...")
60
+ clusterer = hdbscan.HDBSCAN(min_cluster_size=HDBSCAN_MIN_SIZE, min_samples=HDBSCAN_MIN_SAMPLES, prediction_data=True)
61
+ clusters = clusterer.fit_predict(embedding_3d)
62
+
63
+ print("Montando o DataFrame final...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  df = pd.DataFrame({
65
+ 'x': embedding_3d[:, 0], 'y': embedding_3d[:, 1], 'z': embedding_3d[:, 2],
66
+ 'full_text': _texts, 'cluster': clusters,
67
+ 'probability': clusterer.probabilities_
 
 
68
  })
69
+
70
+ s = SentimentIntensityAnalyzer()
71
+ df['sentiment'] = df['full_text'].apply(lambda text: s.polarity_scores(text)['compound'])
72
 
73
+ del reducer, clusterer, embedding_3d
74
  gc.collect()
75
+ print("Pipeline de processamento de dados concluído.")
76
+ return df, embeddings, _texts
77
 
78
+ @st.cache_data
79
+ def calcular_metricas_globais_api(_texts: List[str]) -> Dict[str, Any]:
80
+ print("Calculando métricas globais...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  try:
82
  vectorizer_tfidf = TfidfVectorizer(stop_words=STOP_WORDS_PT, max_features=20000)
83
+ tfidf_matrix = vectorizer_tfidf.fit_transform(_texts)
84
  vocab = vectorizer_tfidf.get_feature_names_out()
85
 
86
+ riqueza_lexical = len(vocab)
87
+
88
+ soma_tfidf = tfidf_matrix.sum(axis=0).A1
89
+ indices_top_tfidf = np.argsort(soma_tfidf)[-10:][::-1]
90
+ palavras_relevantes = [vocab[i] for i in indices_top_tfidf]
91
+
92
+ contagens_palavras = np.array(tfidf_matrix.sum(axis=0)).flatten()
93
+ entropia_corpus = entropy(contagens_palavras / np.sum(contagens_palavras), base=2)
94
+
95
+ except (ValueError, ZeroDivisionError):
96
+ riqueza_lexical = 0
97
  palavras_relevantes = []
98
+ entropia_corpus = 0.0
99
+
100
+ return {"riqueza_lexical": int(riqueza_lexical), "palavras_relevantes": palavras_relevantes, "entropia": float(entropia_corpus)}
101
+
102
+ @st.cache_data
103
+ def analisar_clusters_api(_df: pd.DataFrame, _embeddings: np.ndarray, _texts: List[str]) -> Dict[str, Any]:
104
+ print("Analisando detalhes de cada cluster...")
105
+ analise = {}
106
+ text_df = pd.DataFrame({'full_text': _texts, 'cluster': _df['cluster']})
107
+
108
+ for cluster_id in sorted(_df['cluster'].unique()):
109
+ if cluster_id == -1: continue
110
+
111
+ cluster_indices = _df[_df['cluster'] == cluster_id].index
112
+ cluster_texts = text_df.loc[cluster_indices, 'full_text'].tolist()
113
+ cluster_embeddings = _embeddings[cluster_indices]
114
+
115
+ try:
116
+ vectorizer = TfidfVectorizer(stop_words=STOP_WORDS_PT, max_features=100, ngram_range=(1,2))
117
+ vectorizer.fit(cluster_texts)
118
+ keywords = vectorizer.get_feature_names_out()[:10]
119
+ except ValueError:
120
+ keywords = []
121
+
122
+ centroid = np.mean(cluster_embeddings, axis=0).reshape(1, -1)
123
+ similarities = cosine_similarity(cluster_embeddings, centroid)
124
+ representative_idx_in_cluster = np.argmax(similarities)
125
+ original_idx = cluster_indices[representative_idx_in_cluster]
126
+ representative_doc = _texts[original_idx]
127
+
128
+ cohesion = _df.loc[cluster_indices, 'probability'].mean()
129
+
130
+ analise[str(cluster_id)] = {
131
+ "size": len(cluster_texts),
132
+ "keywords": list(keywords),
133
+ "representative_doc": representative_doc,
134
+ "cohesion_score": float(cohesion),
135
+ "avg_sentiment": float(_df.loc[cluster_indices, 'sentiment'].mean())
136
  }
137
+ return analise
138
 
139
+ @st.cache_data
140
+ def encontrar_duplicados_api(_df: pd.DataFrame, _embeddings: np.ndarray, similaridade_minima: float = 0.98) -> Dict[str, Any]:
141
+ print("Procurando por duplicados...")
142
+ duplicados_exatos_mask = _df['full_text'].duplicated(keep=False)
143
+ df_duplicados_exatos = _df[duplicados_exatos_mask].copy()
144
+ grupos_exatos = {}
145
+ if not df_duplicados_exatos.empty:
146
+ grupos_exatos = {text: [int(i) for i in list(indices)] for text, indices in df_duplicados_exatos.groupby('full_text').groups.items()}
147
+
148
  pares_semanticos = []
149
+ limite_semantico = 5000
150
+ if len(_embeddings) < limite_semantico:
151
+ sim_matrix = cosine_similarity(_embeddings)
152
+ indices_superiores = np.triu_indices_from(sim_matrix, k=1)
153
+ pares_altamente_similares = sim_matrix[indices_superiores] > similaridade_minima
154
+ indices_pares = np.where(pares_altamente_similares)[0]
155
+ for i in indices_pares:
156
+ idx1, idx2 = indices_superiores[0][i], indices_superiores[1][i]
157
+ if _df['full_text'].iloc[int(idx1)] != _df['full_text'].iloc[int(idx2)]:
158
+ 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)]})
159
+
160
+ return {"grupos_exatos": grupos_exatos, "pares_semanticos": pares_semanticos}
161
+
162
+ # ================================
163
+ # DEFINIÇÃO DA API COM FASTAPI
164
+ # ================================
165
+ app = FastAPI(title="API do AetherMap by Strand DataOps", version="2.0.0")
166
+
167
+ @app.post("/process/", summary="Processa e Analisa um Arquivo de Texto")
168
+ async def process_text_file(n_samples: int = Form(10000), file: UploadFile = File(...)):
169
+ print(f"Recebida requisição para processar {n_samples} amostras do arquivo: {file.filename}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  try:
171
  file_bytes = await file.read()
172
+ df, embeddings, texts = process_data_pipeline(file_bytes, n_samples)
173
+ if df is None:
174
+ raise HTTPException(status_code=400, detail="Nenhum texto válido encontrado no arquivo.")
175
+
176
+ metricas = calcular_metricas_globais_api(texts)
177
+ analise_duplicidade = encontrar_duplicados_api(df, embeddings)
178
+ analise_clusters = analisar_clusters_api(df, embeddings, texts)
179
+
180
+ plot_data = df[['x', 'y', 'z', 'cluster', 'full_text', 'sentiment']].to_dict('records')
181
+ n_clusters = len([c for c in df['cluster'].unique() if c != -1])
182
+ n_ruido = int((df['cluster'] == -1).sum())
183
+
184
+ response = {
185
  "metadata": {
186
  "filename": file.filename,
187
  "num_documents_processed": int(len(df)),
 
188
  "num_clusters_found": int(n_clusters),
189
  "num_noise_points": int(n_ruido),
190
  },
191
  "metrics": metricas,
192
+ "duplicates": analise_duplicidade,
193
+ "plot_data": plot_data,
194
+ "cluster_analysis": analise_clusters,
195
  }
196
+ print("Processamento concluído com sucesso. Retornando resposta.")
197
+ return response
 
 
198
  except Exception as e:
199
  import traceback
200
+ print(f"Erro CRÍTICO durante o processamento: {e}")
201
  traceback.print_exc()
202
+ raise HTTPException(status_code=500, detail=f"Ocorreu um erro interno no servidor: {str(e)}")