Upload 3 files
Browse files- Dockerfile +17 -0
- app.py +143 -0
- 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
|