Madras1 commited on
Commit
9869493
·
verified ·
1 Parent(s): 16393f1

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +17 -0
  2. app.py +143 -0
  3. requirements.txt +15 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Passo 1: A Fundação
2
+ FROM python:3.10-slim
3
+
4
+ # Passo 2: O Local de Trabalho
5
+ WORKDIR /code
6
+
7
+ # Passo 3: Preparando o Terreno (Otimização de Cache)
8
+ COPY ./requirements.txt /code/requirements.txt
9
+
10
+ # Passo 4: As Ferramentas
11
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
12
+
13
+ # Passo 5: O Projeto
14
+ COPY ./app.py /code/app.py
15
+
16
+ # Passo 6: A Ignição!
17
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
12
+ 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)}")
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FastAPI e Servidor Web
2
+ fastapi
3
+ uvicorn[standard]
4
+ python-multipart
5
+
6
+ # Machine Learning e Processamento de Dados
7
+ streamlit
8
+ numpy
9
+ pandas
10
+ sentence-transformers
11
+ umap-learn
12
+ hdbscan
13
+ scikit-learn
14
+ torch
15
+ scipy