julianzrmrz commited on
Commit
4e07023
·
1 Parent(s): 0970982

Deploy: Versión inicial con modelos v4

Browse files
main.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ from PIL import Image
4
+ import sys
5
+ import os
6
+
7
+ # Configuración de rutas
8
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '/src')))
9
+ from inference import MemePredictor
10
+
11
+ # --- 1. CONFIGURACIÓN DE PÁGINA (Minimalista) ---
12
+ st.set_page_config(
13
+ page_title="DIME-MEX",
14
+ layout="centered",
15
+ initial_sidebar_state="collapsed"
16
+ )
17
+
18
+ # --- 2. ESTILOS CSS PERSONALIZADOS (Para móvil) ---
19
+ st.markdown("""
20
+ <style>
21
+ /* Ocultar menú hamburguesa y footer para limpieza visual */
22
+ #MainMenu {visibility: hidden;}
23
+ footer {visibility: hidden;}
24
+
25
+ /* Ajustar padding para móviles */
26
+ .block-container {
27
+ padding-top: 2rem;
28
+ padding-bottom: 2rem;
29
+ }
30
+
31
+ /* Botón primario minimalista (Blanco y Negro o color acento del tema) */
32
+ div.stButton > button:first-child {
33
+ width: 100%;
34
+ border-radius: 5px;
35
+ height: 3em;
36
+ font-weight: bold;
37
+ }
38
+ </style>
39
+ """, unsafe_allow_html=True)
40
+
41
+ # --- 3. CARGA DEL MOTOR ---
42
+ @st.cache_resource
43
+ def get_engine():
44
+ return MemePredictor()
45
+
46
+ try:
47
+ predictor = get_engine()
48
+ except Exception as e:
49
+ st.error(f"Error iniciando el sistema: {e}")
50
+ st.stop()
51
+
52
+ # --- 4. INTERFAZ DE USUARIO ---
53
+
54
+ # Título limpio
55
+ st.markdown("## Detector de Contenido")
56
+ st.markdown("Clasificación de memes utilizando visión y lenguaje natural.")
57
+
58
+ # ACORDEÓN DE CONFIGURACIÓN
59
+ with st.expander("Configuración del Modelo"):
60
+ task_mode = st.radio(
61
+ "Nivel de detalle:",
62
+ options=["simple", "complex"],
63
+ format_func=lambda x: "Básico (3 Categorías)" if x == "simple" else "Detallado (6 Categorías)"
64
+ )
65
+
66
+ # ÁREA DE DRAG AND DROP
67
+ uploaded_file = st.file_uploader("Cargar imagen", type=["jpg", "png", "jpeg"], help="Arrastra tu archivo aquí")
68
+
69
+ if uploaded_file is not None:
70
+ # Mostrar imagen centrada y ajustada al ancho del móvil
71
+ image = Image.open(uploaded_file)
72
+ st.image(image, use_column_width=True)
73
+
74
+ # Botón de acción (ocupa todo el ancho por el CSS)
75
+ if st.button("ANALIZAR IMAGEN", type="primary"):
76
+
77
+ with st.spinner("Procesando..."):
78
+ try:
79
+ # Inferencia
80
+ result = predictor.predict(uploaded_file, task=task_mode)
81
+
82
+ if "error" in result:
83
+ st.error(result["error"])
84
+ else:
85
+ st.divider() # Línea separadora sutil
86
+
87
+ # --- RESULTADOS MINIMALISTAS (st.metric) ---
88
+ # Usamos columnas para que se vea bien en celular (uno al lado del otro o apilados)
89
+ col1, col2 = st.columns(2)
90
+
91
+ with col1:
92
+ st.metric(label="Clasificación", value=result['label'])
93
+
94
+ with col2:
95
+ # Formato de porcentaje limpio
96
+ st.metric(label="Confianza", value=f"{result['confidence']:.1%}")
97
+
98
+ # Barra de progreso simple
99
+ st.progress(result['confidence'])
100
+
101
+ # --- DETALLES TÉCNICOS (Ocultos por defecto) ---
102
+ with st.expander("Ver texto extraído"):
103
+ st.caption("Texto detectado por OCR:")
104
+ st.text(result["ocr_text"])
105
+ st.caption("Texto procesado para el modelo:")
106
+ st.code(result["clean_text"], language="text")
107
+
108
+ # --- GRÁFICA LIMPIA ---
109
+ st.caption("Distribución de probabilidades")
110
+ chart_data = pd.DataFrame({
111
+ "Categoría": result['all_labels'],
112
+ "Probabilidad": result['probabilities']
113
+ })
114
+ # Gráfica de barras horizontal es mejor para leer etiquetas largas en móvil
115
+ st.bar_chart(chart_data.set_index("Categoría"), color="#333333")
116
+
117
+ except Exception as e:
118
+ st.error(f"Error interno: {e}")
models/beto_complex_v4.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6041335c6d5f8317d944157ecc99d801d5d069c429923856670275af7fe62073
3
+ size 439506051
models/beto_simple_v4.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dd13bd86b8e9ea6f5f1dcbab9abaa75cf23552c370a6badb509322d0043f19a0
3
+ size 439496628
packages.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ libgl1-mesa-glx
2
+ libglib2.0-0
requeriments.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pandas
2
+ numpy
3
+ scikit-learn
4
+ torch
5
+ huggingface-hub<1.0
6
+ transformers
7
+ nltk
8
+ matplotlib
9
+ seaborn
10
+ tqdm
11
+ streamlit
12
+ easyocr
13
+ opencv-python-headless
requirements.txt DELETED
@@ -1,3 +0,0 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
src/__init__.py ADDED
File without changes
src/__pycache__/inference.cpython-313.pyc ADDED
Binary file (6.2 kB). View file
 
src/__pycache__/nlp_utils.cpython-313.pyc ADDED
Binary file (3.27 kB). View file
 
src/__pycache__/utils.cpython-313.pyc ADDED
Binary file (2.64 kB). View file
 
src/directory.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import json
3
+ import os
4
+
5
+ # Configuracion de rutas dinamicas
6
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
7
+ PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "../"))
8
+
9
+ # Directorios de entrada y salida
10
+ INPUT_DIR = os.path.join(PROJECT_ROOT, "data", "train")
11
+ OUTPUT_DIR = os.path.join(PROJECT_ROOT, "data", "processed", "datasets")
12
+
13
+ TASKS = {
14
+ "simple": {
15
+ "file": "label_simple.csv",
16
+ "cols": ["none", "inappropriate", "hate_speech"]
17
+ },
18
+ "complex": {
19
+ "file": "label_complex.csv",
20
+ "cols": ["none", "inappropriate", "sexism", "racism", "classicism", "other"]
21
+ }
22
+ }
23
+
24
+ def create_dataset_csv(task_name):
25
+ # Validar existencia del directorio de entrada
26
+ if not os.path.exists(INPUT_DIR):
27
+ print(f"Error: No se encuentra el directorio {INPUT_DIR}")
28
+ return
29
+
30
+ # Cargar archivo JSON con metadatos
31
+ json_path = os.path.join(INPUT_DIR, "train_data.json")
32
+ with open(json_path, 'r', encoding='utf-8') as f:
33
+ data_json = json.load(f)
34
+
35
+ df = pd.DataFrame(data_json)
36
+
37
+ # Cargar archivo CSV con etiquetas
38
+ csv_conf = TASKS[task_name]
39
+ labels_path = os.path.join(INPUT_DIR, csv_conf["file"])
40
+ df_labels = pd.read_csv(labels_path, header=None)
41
+
42
+ # Convertir one-hot encoding a etiquetas numericas
43
+ df['label'] = df_labels.values.argmax(axis=1)
44
+
45
+ # Generar rutas relativas de las imagenes
46
+ images_prefix = os.path.join("data", "train", "images")
47
+ df['path'] = df['MEME-ID'].apply(lambda x: os.path.join(images_prefix, x))
48
+
49
+ # Seleccionar columnas finales
50
+ final_df = df[['path', 'text', 'description', 'label']]
51
+
52
+ # Crear directorio de salida si no existe
53
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
54
+
55
+ # Guardar archivo CSV
56
+ output_file = os.path.join(OUTPUT_DIR, f"dataset-{task_name}.csv")
57
+ final_df.to_csv(output_file, index=False)
58
+ print(f"Archivo generado: {output_file}")
59
+
60
+ if __name__ == "__main__":
61
+ create_dataset_csv("simple")
62
+ create_dataset_csv("complex")
src/inference.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import easyocr
3
+ import os
4
+ import sys
5
+ from transformers import AutoTokenizer
6
+
7
+ # Configurar path para importar módulos locales (nlp_utils y utils)
8
+ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
9
+ sys.path.append(CURRENT_DIR)
10
+
11
+ from nlp_utils import BetoClassifier
12
+ from utils import clean_text, preprocess_image_for_ocr
13
+
14
+ # ==========================================
15
+ # ⚙️ CONFIGURACIÓN DEL MODELO
16
+ # ==========================================
17
+ MODEL_VERSION = "v4" # <--- CAMBIA ESTO si entrenas nuevas versiones
18
+ MODEL_NAME = "dccuchile/bert-base-spanish-wwm-cased"
19
+ MAX_LEN = 128
20
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
21
+
22
+ # Construcción de la ruta: .../DIMEMEX/models/v4/
23
+ # Subimos dos niveles desde src/inference.py para llegar a la raiz, luego models, luego v4
24
+ PROJECT_ROOT = os.path.abspath(os.path.join(CURRENT_DIR, "../"))
25
+ MODEL_DIR = os.path.join(PROJECT_ROOT, "models")
26
+
27
+ class MemePredictor:
28
+ def __init__(self):
29
+ print(f"🔧 Inicializando motor en: {DEVICE}")
30
+ print(f"📂 Buscando modelos versión {MODEL_VERSION} en: {MODEL_DIR}")
31
+
32
+ # Cargar OCR (Singleton)
33
+ self.reader = easyocr.Reader(['es', 'en'], gpu=(DEVICE.type == 'cuda'))
34
+ self.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
35
+
36
+ # Cache para no recargar modelos .pth
37
+ self.loaded_models = {}
38
+
39
+ # Mapas de etiquetas
40
+ self.labels_map = {
41
+ "simple": ["None", "Inappropriate", "Hate"],
42
+ "complex": ["None", "Inapp", "Sexism", "Racism", "Classicism", "Other"]
43
+ }
44
+
45
+ def _get_model_instance(self, task):
46
+ # Si ya está en RAM, devolverlo
47
+ if task in self.loaded_models:
48
+ return self.loaded_models[task]
49
+
50
+ # Construir nombre del archivo: beto_simple_v4.pth
51
+ filename = f"beto_{task}_{MODEL_VERSION}.pth"
52
+ path = os.path.join(MODEL_DIR, filename)
53
+
54
+ if not os.path.exists(path):
55
+ raise FileNotFoundError(f"No se encontró el modelo para la tarea '{task}' en {path}")
56
+
57
+ print(f"📥 Cargando modelo desde: {path}")
58
+
59
+ n_classes = len(self.labels_map[task])
60
+
61
+ # Instanciar arquitectura
62
+ model = BetoClassifier(n_classes, MODEL_NAME)
63
+
64
+ # Cargar pesos
65
+ # map_location es vital para evitar errores si entrenaste en GPU y corres en CPU
66
+ try:
67
+ model.load_state_dict(torch.load(path, map_location=DEVICE))
68
+ except Exception as e:
69
+ raise RuntimeError(f"Error al leer el archivo .pth: {e}")
70
+
71
+ model.to(DEVICE)
72
+ model.eval()
73
+
74
+ self.loaded_models[task] = model
75
+ return model
76
+
77
+ def predict(self, image_file, task="simple"):
78
+ # 1. Resetear puntero del archivo
79
+ image_file.seek(0)
80
+
81
+ # 2. Pre-procesamiento de Imagen (Visión)
82
+ proc_img, raw_img = preprocess_image_for_ocr(image_file)
83
+
84
+ if proc_img is None:
85
+ return {"error": "Error procesando la imagen (archivo corrupto o formato inválido)"}
86
+
87
+ # 3. OCR con Fallback
88
+ try:
89
+ # Intento 1: Imagen procesada
90
+ ocr_result = self.reader.readtext(proc_img, detail=0, paragraph=True)
91
+ raw_text = " ".join(ocr_result)
92
+
93
+ # Si leyó muy poco (<3 chars), intentar con la original
94
+ if len(raw_text) < 3:
95
+ ocr_result = self.reader.readtext(raw_img, detail=0, paragraph=True)
96
+ raw_text = " ".join(ocr_result)
97
+ except Exception as e:
98
+ return {"error": f"Fallo en OCR: {e}"}
99
+
100
+ # 4. Limpieza de Texto (NLP)
101
+ text_ready = clean_text(raw_text)
102
+
103
+ # 5. Tokenización
104
+ encoding = self.tokenizer.encode_plus(
105
+ text_ready,
106
+ add_special_tokens=True,
107
+ max_length=MAX_LEN,
108
+ padding='max_length',
109
+ truncation=True,
110
+ return_attention_mask=True,
111
+ return_tensors='pt',
112
+ )
113
+
114
+ input_ids = encoding['input_ids'].to(DEVICE)
115
+ attention_mask = encoding['attention_mask'].to(DEVICE)
116
+
117
+ # 6. Inferencia del Modelo
118
+ try:
119
+ model = self._get_model_instance(task)
120
+
121
+ with torch.no_grad():
122
+ outputs = model(input_ids, attention_mask)
123
+ probs = torch.nn.functional.softmax(outputs, dim=1)
124
+ conf, idx = torch.max(probs, dim=1)
125
+
126
+ label_str = self.labels_map[task][idx.item()]
127
+
128
+ return {
129
+ "ocr_text": raw_text,
130
+ "clean_text": text_ready,
131
+ "label": label_str,
132
+ "confidence": conf.item(),
133
+ "probabilities": probs.cpu().numpy()[0],
134
+ "all_labels": self.labels_map[task]
135
+ }
136
+ except Exception as e:
137
+ return {"error": f"Error en inferencia del modelo: {e}"}
src/nlp_utils.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/nlp_utils.py
2
+ import torch.nn as nn
3
+ from torch.utils.data import Dataset
4
+ import torch
5
+ from transformers import AutoModel
6
+
7
+ class BetoClassifier(nn.Module):
8
+ def __init__(self, n_classes, model_name="dccuchile/bert-base-spanish-wwm-cased"):
9
+ super(BetoClassifier, self).__init__()
10
+ self.bert = AutoModel.from_pretrained(model_name)
11
+ self.drop = nn.Dropout(p=0.3)
12
+ self.out = nn.Linear(self.bert.config.hidden_size, n_classes)
13
+
14
+ def forward(self, input_ids, attention_mask):
15
+ output = self.bert(input_ids=input_ids, attention_mask=attention_mask)
16
+ return self.out(self.drop(output.pooler_output))
17
+
18
+ class MemeDataset(Dataset):
19
+ def __init__(self, df, tokenizer, max_len, label_col):
20
+ self.df = df.reset_index(drop=True)
21
+ self.tokenizer = tokenizer
22
+ self.max_len = max_len
23
+ self.label_col = label_col # Columna dinamica segun la tarea
24
+
25
+ def __len__(self):
26
+ return len(self.df)
27
+
28
+ def __getitem__(self, index):
29
+ text = str(self.df.loc[index, 'text_clean'])
30
+ label = int(self.df.loc[index, self.label_col])
31
+
32
+ encoding = self.tokenizer.encode_plus(
33
+ text,
34
+ add_special_tokens=True,
35
+ max_length=self.max_len,
36
+ padding='max_length',
37
+ truncation=True,
38
+ return_attention_mask=True,
39
+ return_tensors='pt',
40
+ )
41
+
42
+ return {
43
+ 'input_ids': encoding['input_ids'].flatten(),
44
+ 'attention_mask': encoding['attention_mask'].flatten(),
45
+ 'label': torch.tensor(label, dtype=torch.long)
46
+ }
src/split-data.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from sklearn.model_selection import train_test_split
3
+ import os
4
+
5
+ # Configuracion de rutas dinamicas
6
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
7
+ PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, "../"))
8
+
9
+ # Directorios de entrada y salida
10
+ DATA_PROCESSED_DIR = os.path.join(PROJECT_ROOT, "data", "processed","nlp")
11
+ TEXT_FILE_PATH = os.path.join(DATA_PROCESSED_DIR, "cleaned-ocr.csv")
12
+ SIMPLE_FILE_PATH = os.path.join(DATA_PROCESSED_DIR, "datasets", "dataset-simple.csv")
13
+ COMPLEX_FILE_PATH = os.path.join(DATA_PROCESSED_DIR, "datasets", "dataset-complex.csv")
14
+ OUTPUT_DIR = os.path.join(DATA_PROCESSED_DIR, "splits")
15
+
16
+ def create_master_splits():
17
+ # Validar existencia de archivos de entrada
18
+ if not all(os.path.exists(p) for p in [TEXT_FILE_PATH, SIMPLE_FILE_PATH, COMPLEX_FILE_PATH]):
19
+ print("Error: Faltan archivos de entrada en data/processed")
20
+ return
21
+
22
+ # Cargar archivos CSV
23
+ df_text = pd.read_csv(TEXT_FILE_PATH)
24
+ df_simple = pd.read_csv(SIMPLE_FILE_PATH)
25
+ df_complex = pd.read_csv(COMPLEX_FILE_PATH)
26
+
27
+ # Generar columna id basada en el nombre del archivo si no existe
28
+ if 'id' not in df_simple.columns:
29
+ df_simple['id'] = df_simple['path'].apply(os.path.basename)
30
+ if 'id' not in df_complex.columns:
31
+ df_complex['id'] = df_complex['path'].apply(os.path.basename)
32
+
33
+ # Renombrar columnas de etiquetas para evitar conflictos
34
+ df_simple = df_simple.rename(columns={'label': 'label-simple'})
35
+ df_complex = df_complex.rename(columns={'label': 'label-complex'})
36
+
37
+ # Unir etiquetas simples y complejas usando el id
38
+ df_labels_merged = pd.merge(df_simple, df_complex[['id', 'label-complex']], on='id', how='inner')
39
+
40
+ # Unir con el texto limpio para crear el dataset maestro
41
+ df_master = pd.merge(df_labels_merged, df_text[['id', 'text_clean']], on='id', how='inner')
42
+
43
+ # Crear directorio de salida si no existe
44
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
45
+
46
+ # Guardar el dataset completo para entrenamiento final
47
+ complete_data_path = os.path.join(OUTPUT_DIR, "complete-data.csv")
48
+ df_master.to_csv(complete_data_path, index=False)
49
+ print(f"Dataset completo guardado en: {complete_data_path}")
50
+
51
+ # Separar conjunto de prueba manteniendo balance de clases complejas
52
+ df_temp, df_test = train_test_split(
53
+ df_master,
54
+ test_size=0.15,
55
+ random_state=42,
56
+ stratify=df_master['label-complex']
57
+ )
58
+
59
+ # Separar entrenamiento y validacion del conjunto restante
60
+ df_train, df_val = train_test_split(
61
+ df_temp,
62
+ test_size=0.176,
63
+ random_state=42,
64
+ stratify=df_temp['label-complex']
65
+ )
66
+
67
+ # Guardar los archivos de particion
68
+ df_train.to_csv(os.path.join(OUTPUT_DIR, "train.csv"), index=False)
69
+ df_val.to_csv(os.path.join(OUTPUT_DIR, "val.csv"), index=False)
70
+ df_test.to_csv(os.path.join(OUTPUT_DIR, "test.csv"), index=False)
71
+
72
+ print(f"Splits guardados en: {OUTPUT_DIR}")
73
+
74
+ if __name__ == "__main__":
75
+ create_master_splits()
src/streamlit_app.py DELETED
@@ -1,40 +0,0 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
- import streamlit as st
5
-
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/utils.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import cv2
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ def clean_text(text):
7
+ """Limpieza estándar para BERT."""
8
+ if not text or pd.isna(text): return "sin texto"
9
+ text = str(text).lower()
10
+ # Eliminar URLs y usuarios
11
+ text = re.sub(r'http\S+|www\.\S+', '', text)
12
+ text = re.sub(r'@\w+', '', text)
13
+ # Normalizar risas
14
+ text = re.sub(r'(ja|je|ha|he|lo){2,}', 'jaja', text)
15
+ # Eliminar basura de OCR
16
+ text = re.sub(r'[|_~*^>\[\]]', ' ', text)
17
+ # Espacios y saltos
18
+ text = text.replace('\n', ' ').replace('\r', ' ')
19
+ text = re.sub(r'\s+', ' ', text).strip()
20
+ return text if text else "sin texto"
21
+
22
+ def preprocess_image_for_ocr(file_bytes):
23
+ """
24
+ Recibe bytes (desde Streamlit) y aplica filtros de OpenCV.
25
+ Retorna: (imagen_binarizada, imagen_original_cv2)
26
+ """
27
+ # Convertir bytes a array numpy para OpenCV
28
+ file_bytes = np.asarray(bytearray(file_bytes.read()), dtype=np.uint8)
29
+ img = cv2.imdecode(file_bytes, 1) # 1 = Color BGR
30
+
31
+ if img is None: return None, None
32
+
33
+ # Pipeline de Mejora (Igual al benchmark)
34
+ try:
35
+ # 1. Upscaling (2x)
36
+ img_resized = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
37
+
38
+ # 2. Escala de Grises
39
+ gray = cv2.cvtColor(img_resized, cv2.COLOR_BGR2GRAY)
40
+
41
+ # 3. Denoising
42
+ denoised = cv2.fastNlMeansDenoising(gray, None, h=10, templateWindowSize=7, searchWindowSize=21)
43
+
44
+ # 4. Binarización Adaptativa
45
+ binary = cv2.adaptiveThreshold(
46
+ denoised, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2
47
+ )
48
+ return binary, img
49
+ except Exception as e:
50
+ print(f"Error en pre-procesamiento: {e}")
51
+ return None, img