File size: 8,614 Bytes
dc37acf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# ==============================================================================
#  داشبورد_3d_magic_streamlit.py (VERSÃO PARA HUGGING FACE SPACES)
# Arquivo: app.py
# ==============================================================================
import streamlit as st
import numpy as np
import pandas as pd
from sentence_transformers import SentenceTransformer
import umap
import plotly.express as px
import hdbscan
from sklearn.preprocessing import StandardScaler
from sklearn.metrics.pairwise import cosine_similarity
import torch
import gc

# ================================
# CONFIGURAÇÕES DA PÁGINA E DO MODELO
# ================================
st.set_page_config(
    page_title="Dashboard 3D Mágico",
    page_icon="🌌",
    layout="wide",
    initial_sidebar_state="expanded"
)

# Constantes do seu script original
DEFAULT_MODEL = 'all-MiniLM-L6-v2'
BATCH_SIZE = 256
UMAP_N_NEIGHBORS = 30
HDBSCAN_MIN_SIZE = 50

# ================================
# FUNÇÕES CACHEADAS (O CORAÇÃO DA PERFORMANCE)
# ================================

# @st.cache_resource: Carrega o modelo de embedding UMA ÚNICA VEZ e o mantém na memória.
# É o nosso "superpoder" para não ter que esperar o modelo carregar a cada interação.
@st.cache_resource
def load_model():
    """Carrega o modelo SentenceTransformer e o coloca no dispositivo correto."""
    device = "cuda" if torch.cuda.is_available() else "cpu"
    print(f"Carregando modelo para o dispositivo: {device}")
    model = SentenceTransformer(DEFAULT_MODEL, device=device)
    return model

# @st.cache_data: Executa o processamento pesado e guarda o resultado.
# Se o usuário subir o mesmo arquivo com os mesmos parâmetros, o Streamlit usa o resultado
# guardado em vez de reprocessar tudo. Magia!
@st.cache_data
def process_data(texts, n_samples):
    """
    Função que encapsula todo o pipeline de processamento:
    Embeddings -> UMAP -> HDBSCAN -> DataFrame
    """
    model = load_model() # Pega o modelo já carregado da função cacheada

    st.info(f"Processando {len(texts):,} textos... Isso pode levar alguns minutos.")
    
    # --- 2. EMBEDDINGS ---
    progress_bar = st.progress(0, text="Gerando embeddings...")
    embeddings = model.encode(texts, batch_size=BATCH_SIZE, show_progress_bar=False, convert_to_numpy=True)
    progress_bar.progress(30, text="Reduzindo dimensionalidade com UMAP 3D...")

    # --- 3. UMAP 3D ---
    reducer = umap.UMAP(n_components=3, n_neighbors=UMAP_N_NEIGHBORS, min_dist=0.0, metric='cosine', random_state=42)
    embedding_3d = reducer.fit_transform(embeddings)
    embedding_3d = StandardScaler().fit_transform(embedding_3d)
    progress_bar.progress(70, text="Clusterizando com HDBSCAN...")

    # --- 4. HDBSCAN ---
    clusterer = hdbscan.HDBSCAN(min_cluster_size=HDBSCAN_MIN_SIZE)
    clusters = clusterer.fit_predict(embedding_3d)
    n_clusters = len(set(clusters)) - (1 if -1 in clusters else 0)
    st.success(f"Clusterização concluída! Encontrados {n_clusters} clusters.")
    progress_bar.progress(90, text="Montando o DataFrame final...")

    # --- 5 & 6. DATAFRAME ---
    df = pd.DataFrame({
        'x': embedding_3d[:, 0], 'y': embedding_3d[:, 1], 'z': embedding_3d[:, 2],
        'text_hover': [t[:300] + "..." if len(t) > 300 else t for t in texts],
        'length': [len(t) for t in texts],
        'cluster': clusters.astype(str)
    })
    df['color'] = df['cluster'].astype(int)
    # Ajuste de tamanho para melhor visualização
    df['size'] = np.log1p(df['length']) * 1.5 + 1 
    
    progress_bar.progress(100, text="Processamento concluído!")
    progress_bar.empty()
    
    # Limpeza de memória
    del reducer, clusterer, embedding_3d
    gc.collect()

    return df, embeddings, np.array(texts, dtype=object)

# ================================
# LAYOUT DA APLICAÇÃO (A INTERFACE)
# ================================

# Título e descrição
st.title("🌌 Dashboard 3D Mágico de Textos")
st.markdown("""
Bem-vindo, meu príncipe Gabriel Yogi! 
Esta é a nossa oficina mágica. Faça o upload de um arquivo `.txt` e veja seus textos ganharem vida em um universo 3D. 
Depois, use a busca semântica para encontrar constelações de ideias.
""")

# --- BARRA LATERAL PARA CONTROLES ---
with st.sidebar:
    st.header("⚙️ Controles")
    uploaded_file = st.file_uploader("1. Escolha seu arquivo .txt", type="txt")
    
    # O slider só aparece se um arquivo for carregado
    if uploaded_file:
        max_samples_default = 10000
        n_samples = st.slider(
            "2. Selecione o número máximo de amostras",
            min_value=500,
            max_value=50000,
            value=max_samples_default,
            step=500,
            help="Valores mais altos exigem mais tempo de processamento."
        )

    process_button = st.button("✨ Gerar Dashboard ✨", disabled=not uploaded_file, type="primary")

# --- ÁREA PRINCIPAL PARA VISUALIZAÇÃO ---

# Inicializa o 'session_state' para guardar nossos dados
if 'processed' not in st.session_state:
    st.session_state.processed = False

if process_button:
    if uploaded_file is not None:
        with st.spinner('Lendo e validando o arquivo...'):
            # Decodifica o arquivo e lê as linhas
            lines = uploaded_file.getvalue().decode('utf-8').splitlines()
            texts = []
            for line in lines:
                s = line.strip()
                if s and len(s.split()) > 3:
                    texts.append(s)
            
            # Limita ao número de amostras
            texts = texts[:n_samples]

        if len(texts) > 0:
            # Chama a função de processamento pesado e guarda os resultados no 'session_state'
            df, embeddings, full_texts = process_data(texts, n_samples)
            st.session_state.df = df
            st.session_state.embeddings = embeddings
            st.session_state.full_texts = full_texts
            st.session_state.processed = True
        else:
            st.error("Nenhum texto válido encontrado no arquivo! Verifique se ele não está vazio e se as linhas têm mais de 3 palavras.")
            st.session_state.processed = False
    else:
        st.warning("Por favor, faça o upload de um arquivo primeiro.")

# Se os dados já foram processados, mostramos o dashboard
if st.session_state.processed:
    st.header("🔎 Explore seu Universo de Textos")
    
    query = st.text_input("Busca Semântica", placeholder="Digite algo para destacar no gráfico...")
    
    df_display = st.session_state.df.copy()
    
    if query:
        model = load_model()
        q_embedding = model.encode([query])
        sims = cosine_similarity(q_embedding, st.session_state.embeddings)[0]
        
        top_k = 50
        idx = np.argsort(sims)[-top_k:][::-1]
        
        # Cria uma coluna para destaque e ajusta o tamanho e a cor
        df_display['highlight'] = '0' # Começa como string para ser categoria
        df_display.loc[idx, 'highlight'] = '1'
        df_display['display_size'] = np.where(df_display['highlight'] == '1', 10, df_display['size'] * 0.6)
        
        fig = px.scatter_3d(
            df_display, x='x', y='y', z='z',
            color='highlight', size='display_size',
            hover_data={'text_hover': True, 'cluster': True, 'length': True},
            title=f"Busca por: '{query}'",
            color_discrete_map={'0': 'grey', '1': 'yellow'},
            labels={'highlight': 'Resultado'}
        )
        fig.update_traces(marker=dict(opacity=0.9))

        # Mostra os top 5 resultados
        st.subheader("Top 5 Resultados da Busca:")
        for i, (r, s) in enumerate(zip(idx[:5], sims[idx[:5]])):
            st.markdown(f"**{i+1}.** *Similaridade: {s:.3f}*  \n> {st.session_state.full_texts[r][:350]}...")
            
    else:
        # Se não há busca, mostra o gráfico original por cluster
        fig = px.scatter_3d(
            df_display, x='x', y='y', z='z',
            color='cluster', size='size',
            hover_data={'text_hover': True, 'cluster': True, 'length': True},
            title=f"Visualização 3D de {len(df_display):,} Textos",
            color_continuous_scale='Turbo'
        )
        fig.update_traces(marker=dict(opacity=0.8))

    # Configurações finais do layout do gráfico
    fig.update_layout(
        template='plotly_dark',
        height=800,
        margin=dict(l=0, r=0, b=0, t=40),
        scene_camera=dict(eye=dict(x=1.5, y=1.5, z=1.5))
    )
    st.plotly_chart(fig, use_container_width=True)

else:
    st.info("Aguardando um arquivo para começar a mágica... ✨")