devenirfantasma's picture
Update app.py
b6eb924 verified
"""
MVP: Comparador de Modelos de Análisis de Sentimiento
Aplicación Gradio que compara dos modelos de análisis de sentimiento en español:
- pysentimiento/robertuito-sentiment-analysis (entrenado en Twitter)
- finiteautomata/beto-sentiment-analysis (BETO clásico)
Características:
- ✅ Comparación lado a lado de modelos
- ✅ Análisis directo de texto
- ✅ Análisis web desde URLs
- ✅ Interfaz con pestañas
- ✅ Ejemplos en español argentino
Arquitectura:
- Carga eficiente: modelos cargados una sola vez al inicio
- Procesamiento paralelo: ambos modelos ejecutados simultáneamente
- Extracción web: BeautifulSoup para contenido de páginas web
Autor: Desarrollado para IFTS - Procesamiento de Lenguaje Natural
Fecha: 2025
"""
import gradio as gr
from transformers import pipeline
import time
import requests
from bs4 import BeautifulSoup
from typing import Dict, Tuple, Optional
from urllib.parse import urlparse
# Modelos a comparar
MODELOS = {
"RoBERTuito": "pysentimiento/robertuito-sentiment-analysis",
"BETO": "finiteautomata/beto-sentiment-analysis",
}
# Mapeo de etiquetas a español
ETIQUETAS_ES = {"POS": "Positivo", "NEG": "Negativo", "NEU": "Neutral"}
class ComparadorSentimientos:
"""Clase para manejar la comparación de modelos de sentimiento."""
def __init__(self):
self.modelos = {}
self._cargar_modelos()
def _cargar_modelos(self):
"""Carga ambos modelos una sola vez al inicio."""
print("Cargando modelos de analisis de sentimiento...")
for nombre, modelo_path in MODELOS.items():
print(f" Cargando {nombre}: {modelo_path}")
try:
self.modelos[nombre] = pipeline(
"sentiment-analysis", model=modelo_path, return_all_scores=False
)
print(f" {nombre} cargado exitosamente")
except Exception as e:
print(f" Error cargando {nombre}: {str(e)}")
self.modelos[nombre] = None
print("Modelos cargados!\n")
def extraer_texto_web(self, url: str) -> str:
"""
Extrae texto de una página web.
Args:
url: URL de la página web
Returns:
Texto extraído o mensaje de error
"""
if not url.strip():
return "❌ Por favor ingresa una URL válida"
# Validar URL
try:
parsed = urlparse(url)
if not parsed.scheme or not parsed.netloc:
return "❌ URL inválida. Asegúrate de incluir http:// o https://"
except:
return "❌ URL inválida"
try:
print(f"Extrayendo texto de: {url}")
# Headers para evitar bloqueos
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
# Request con timeout
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
# Parsear HTML
soup = BeautifulSoup(response.content, "html.parser")
# Remover scripts y estilos
for script in soup(["script", "style"]):
script.decompose()
# Extraer texto
texto = soup.get_text()
# Limpiar texto
lineas = (linea.strip() for linea in texto.splitlines())
chunks = (frase.strip() for frase in lineas if frase)
texto_limpio = " ".join(chunks)
# Limitar longitud
if len(texto_limpio) > 5000:
texto_limpio = texto_limpio[:5000] + "..."
print(f"Texto extraido: {len(texto_limpio)} caracteres")
return texto_limpio
except requests.exceptions.Timeout:
return "ERROR: Timeout - La pagina tardo demasiado en responder"
except requests.exceptions.ConnectionError:
return "ERROR: Error de conexion - Verifica la URL"
except requests.exceptions.HTTPError as e:
return f"ERROR: Error HTTP: {e}"
except Exception as e:
return f"ERROR: Error inesperado: {str(e)}"
def _chunk_text(self, texto: str, max_tokens: int = 400) -> list:
"""
Divide texto largo en chunks más pequeños.
Args:
texto: Texto a dividir
max_tokens: Máximo tokens por chunk (aproximado)
Returns:
Lista de chunks de texto
"""
# Aproximación: ~4 caracteres por token
max_chars = max_tokens * 4
if len(texto) <= max_chars:
return [texto]
# Dividir por oraciones para mantener contexto
oraciones = texto.split(".")
chunks = []
chunk_actual = ""
for oracion in oraciones:
oracion = oracion.strip() + "."
# Si agregar esta oración supera el límite, guardar chunk actual
if len(chunk_actual) + len(oracion) > max_chars and chunk_actual:
chunks.append(chunk_actual.strip())
chunk_actual = oracion
else:
chunk_actual += oracion
# Agregar último chunk
if chunk_actual:
chunks.append(chunk_actual.strip())
return chunks
def _analizar_chunk(self, modelo, chunk: str) -> Optional[Dict]:
"""
Analiza un chunk individual de texto.
Args:
modelo: Pipeline de transformers
chunk: Chunk de texto a analizar
Returns:
Resultado del análisis o None si hay error
"""
try:
resultado = modelo(chunk)[0]
return {"label": resultado["label"], "score": resultado["score"]}
except Exception as e:
print(f"Error analizando chunk: {str(e)}")
return None
def analizar_texto(self, texto: str) -> Dict[str, Dict[str, float]]:
"""
Analiza el sentimiento usando ambos modelos con chunking para textos largos.
Args:
texto: Texto a analizar
Returns:
Diccionario con resultados de ambos modelos
"""
if not texto.strip():
return {
"error": "Por favor ingresa un texto válido",
"RoBERTuito": {"Error": 1.0},
"BETO": {"Error": 1.0},
}
resultados = {}
# Límites de tokens para cada modelo
limites_tokens = {
"RoBERTuito": 400, # Más conservador que 512
"BETO": 100, # Más conservador que 128
}
for nombre_modelo, modelo in self.modelos.items():
if modelo is None:
resultados[nombre_modelo] = {"Error": 1.0}
continue
try:
inicio = time.time()
# Dividir texto en chunks según límite del modelo
chunks = self._chunk_text(texto, limites_tokens[nombre_modelo])
print(f"Procesando {len(chunks)} chunks con {nombre_modelo}")
# Analizar cada chunk
resultados_chunks = []
for i, chunk in enumerate(chunks):
resultado = self._analizar_chunk(modelo, chunk)
if resultado:
resultados_chunks.append(resultado)
if not resultados_chunks:
resultados[nombre_modelo] = {
"Error": 1.0,
"_error": "No se pudo procesar ningún chunk",
}
continue
# Combinar resultados (votación mayoritaria + promedio de confianza)
votos = {"POS": 0, "NEG": 0, "NEU": 0}
suma_confianza = 0
for resultado in resultados_chunks:
votos[resultado["label"]] += 1
suma_confianza += resultado["score"]
# Determinar etiqueta ganadora
etiqueta_ganadora = max(votos, key=votos.get)
confianza_promedio = suma_confianza / len(resultados_chunks)
tiempo = time.time() - inicio
# Convertir etiqueta a español
etiqueta_es = ETIQUETAS_ES.get(etiqueta_ganadora, etiqueta_ganadora)
resultados[nombre_modelo] = {
etiqueta_es: round(confianza_promedio, 4),
"_tiempo": round(tiempo, 3),
"_confianza": round(confianza_promedio, 4),
"_chunks_procesados": len(resultados_chunks),
"_total_chunks": len(chunks),
}
except Exception as e:
resultados[nombre_modelo] = {"Error": 1.0, "_error": str(e)}
return resultados
# Inicializar comparador global
comparador = ComparadorSentimientos()
def analizar_sentimiento(texto: str) -> Tuple[str, str]:
"""
Función principal para la interfaz Gradio.
Args:
texto: Texto ingresado por el usuario
Returns:
Tupla con resultados formateados para ambos modelos
"""
resultados = comparador.analizar_texto(texto)
# Formatear resultados para mostrar en Gradio
robertuito_text = ""
beto_text = ""
if "error" in resultados:
robertuito_text = f"ERROR: {resultados['error']}"
beto_text = f"ERROR: {resultados['error']}"
else:
# RoBERTuito
robertuito = resultados.get("RoBERTuito", {"Error": 1.0})
if "Error" in robertuito:
robertuito_text = "ERROR en RoBERTuito"
else:
etiqueta = list(robertuito.keys())[0]
confianza = robertuito[etiqueta]
tiempo = robertuito.get("_tiempo", 0)
chunks = robertuito.get("_chunks_procesados", 1)
total_chunks = robertuito.get("_total_chunks", 1)
robertuito_text = f"-> {etiqueta}: {confianza:.1%} ({tiempo:.2f}s, {chunks}/{total_chunks} chunks)"
# BETO
beto = resultados.get("BETO", {"Error": 1.0})
if "Error" in beto:
beto_text = "ERROR en BETO"
else:
etiqueta = list(beto.keys())[0]
confianza = beto[etiqueta]
tiempo = beto.get("_tiempo", 0)
chunks = beto.get("_chunks_procesados", 1)
total_chunks = beto.get("_total_chunks", 1)
beto_text = f"-> {etiqueta}: {confianza:.1%} ({tiempo:.2f}s, {chunks}/{total_chunks} chunks)"
return robertuito_text, beto_text
# Ejemplos representativos en español argentino
EJEMPLOS = [
["La verdad, este lugar está bárbaro. Muy recomendable."],
["Qué buena onda la atención, volvería sin dudarlo."],
["Me encantó la comida, aunque la música estaba muy fuerte."],
["Una porquería de servicio, nunca más vuelvo."],
["Qué garrón, tardaron una banda en traer el pedido."],
["Re copado todo, la rompieron con el ambiente."],
["Zafa, pero nada especial el lugar."],
["Está piola el lugar, volvería."],
]
# Crear interfaz Gradio con pestañas
with gr.Blocks(
title="Comparador de Modelos de Sentimiento",
theme=gr.themes.Soft(),
css="""
.gradio-container {
max-width: 1200px;
margin: auto;
}
.title {
text-align: center;
color: #2563eb;
font-size: 2.5em;
margin-bottom: 1em;
}
.subtitle {
text-align: center;
color: #64748b;
font-size: 1.1em;
margin-bottom: 2em;
}
.tab-content {
padding: 1em;
}
""",
) as demo:
# Header
gr.HTML("""
<div class="title">🆚 Comparador de Modelos de Sentimiento</div>
<div class="subtitle">
Compara RoBERTuito vs BETO en análisis de sentimiento para español<br>
<strong>RoBERTuito:</strong> Especializado en lenguaje coloquial y redes sociales<br>
<strong>BETO:</strong> Modelo clásico entrenado en Wikipedia
</div>
""")
# Pestañas principales
with gr.Tabs():
# === PESTAÑA 1: ANÁLISIS DIRECTO ===
with gr.TabItem("📝 Análisis Directo", id="directo"):
with gr.Row():
with gr.Column(scale=2):
texto_input = gr.Textbox(
label="📝 Texto para analizar",
placeholder="Ingresa aquí el texto que quieres analizar...",
lines=4,
show_copy_button=True,
)
# Botón de análisis
analizar_btn = gr.Button(
"🔍 Analizar Sentimiento", variant="primary", size="lg"
)
# Resultados
with gr.Row():
with gr.Column():
gr.HTML(
"<h3 style='text-align: center; color: #059669;'>🤖 RoBERTuito</h3>"
)
robertuito_output = gr.Textbox(
label="Resultado RoBERTuito", interactive=False, lines=2
)
with gr.Column():
gr.HTML(
"<h3 style='text-align: center; color: #dc2626;'>📚 BETO</h3>"
)
beto_output = gr.Textbox(
label="Resultado BETO", interactive=False, lines=2
)
# Ejemplos
gr.Examples(
examples=EJEMPLOS,
inputs=texto_input,
label="💡 Ejemplos en español argentino (clickea para probar)",
examples_per_page=4,
)
# Conectar eventos para análisis directo
analizar_btn.click(
fn=analizar_sentimiento,
inputs=[texto_input],
outputs=[robertuito_output, beto_output],
)
texto_input.change(
fn=analizar_sentimiento,
inputs=[texto_input],
outputs=[robertuito_output, beto_output],
)
# === PESTAÑA 2: ANÁLISIS WEB ===
with gr.TabItem("🌐 Análisis Web", id="web"):
with gr.Row():
with gr.Column(scale=2):
url_input = gr.Textbox(
label="🔗 URL de la página web",
placeholder="https://ejemplo.com/noticia...",
lines=2,
show_copy_button=True,
)
# Botón de extracción y análisis
extraer_btn = gr.Button(
"🌐 Extraer y Analizar", variant="primary", size="lg"
)
# Área de texto extraído
with gr.Row():
texto_web_output = gr.Textbox(
label="📄 Texto extraído de la web",
lines=6,
interactive=False,
placeholder="El texto extraído aparecerá aquí...",
)
# Resultados web
with gr.Row():
with gr.Column():
gr.HTML(
"<h3 style='text-align: center; color: #059669;'>🤖 RoBERTuito</h3>"
)
robertuito_web_output = gr.Textbox(
label="Resultado RoBERTuito (Web)", interactive=False, lines=2
)
with gr.Column():
gr.HTML(
"<h3 style='text-align: center; color: #dc2626;'>📚 BETO</h3>"
)
beto_web_output = gr.Textbox(
label="Resultado BETO (Web)", interactive=False, lines=2
)
# Ejemplos de URLs
ejemplos_urls = [
["https://www.pagina12.com.ar/"],
["https://www.clarin.com/"],
["https://www.lanacion.com.ar/"],
["https://www.infobae.com/"],
]
gr.Examples(
examples=ejemplos_urls,
inputs=url_input,
label="📰 Ejemplos de sitios de noticias argentinas",
examples_per_page=4,
)
# Conectar eventos para análisis web
def analizar_texto_web(url):
"""Función que combina extracción web y análisis."""
texto_extraido = comparador.extraer_texto_web(url)
if texto_extraido.startswith("❌"):
return texto_extraido, "❌ Error", "❌ Error"
# Analizar sentimiento del texto extraído
robertuito_result, beto_result = analizar_sentimiento(texto_extraido)
return texto_extraido, robertuito_result, beto_result
extraer_btn.click(
fn=analizar_texto_web,
inputs=[url_input],
outputs=[texto_web_output, robertuito_web_output, beto_web_output],
)
# === PESTAÑA 3: INFORMACIÓN ===
with gr.TabItem("ℹ️ Información", id="info"):
gr.Markdown("""
## 🏗️ Arquitectura de la Aplicación
Esta aplicación utiliza una arquitectura modular para comparar modelos de análisis de sentimiento:
```mermaid
graph TB
A[Usuario] --> B{Interfaz Gradio}
B --> C[Pestaña: Análisis Directo]
B --> D[Pestaña: Análisis Web]
C --> E[Texto Input]
D --> F[URL Input]
F --> G[Extracción Web]
E --> H[Análisis Modelos]
G --> H
H --> I[RoBERTuito]
H --> J[BETO]
I --> K[Resultado]
J --> K
K --> L[Usuario]
```
## 🤖 Modelos Utilizados
### RoBERTuito (`pysentimiento/robertuito-sentiment-analysis`)
- ✅ **Especializado** en lenguaje coloquial y redes sociales
- ✅ **Entrenado** en tweets en español
- ✅ **Ideal** para lenguaje informal y argentino
### BETO (`finiteautomata/beto-sentiment-analysis`)
- ✅ **Clásico** basado en BERT entrenado en Wikipedia
- ✅ **Formal** y generalista
- ✅ **Bueno** para textos periodísticos y formales
## ✨ Características
- **Carga eficiente**: Modelos cargados una sola vez al inicio
- **Procesamiento paralelo**: Ambos modelos ejecutados simultáneamente
- **Extracción web**: BeautifulSoup para contenido de páginas
- **Manejo de errores**: Validación robusta de URLs y contenido
- **Interfaz intuitiva**: Pestañas organizadas por funcionalidad
## 🚀 Uso Recomendado
### Para análisis directo:
- Textos cortos y específicos
- Mensajes de redes sociales
- Comentarios y reseñas
### Para análisis web:
- Artículos de noticias completos
- Páginas web con contenido extenso
- Análisis de sentimiento periodístico
## 📊 Métricas Incluidas
- **Confianza**: Probabilidad asignada por cada modelo
- **Tiempo de respuesta**: Latencia de cada análisis
- **Longitud del texto**: Caracteres procesados
""")
# Información técnica global (fuera de pestañas)
with gr.Accordion("🔧 Configuración Técnica", open=False):
gr.Markdown(f"""
**Estado de modelos:**
- RoBERTuito: {"✅ Cargado" if comparador.modelos.get("RoBERTuito") else "❌ Error"}
- BETO: {"✅ Cargado" if comparador.modelos.get("BETO") else "❌ Error"}
**Versión de librerías:**
- Transformers: {comparador.modelos["RoBERTuito"].__class__.__module__.split(".")[0] if comparador.modelos.get("RoBERTuito") else "N/A"}
- Gradio: {gr.__version__}
**Características técnicas:**
- Modelos pre-cargados para mejor rendimiento
- Procesamiento paralelo activado
- Extracción web con timeout de 10 segundos
- Límite de texto: 5000 caracteres por análisis
""")
# Punto de entrada
if __name__ == "__main__":
print("Iniciando aplicacion de comparacion de modelos...")
demo.launch(
server_name="0.0.0.0", # Para Hugging Face Spaces
server_port=7860,
show_api=False,
)