Spaces:
Sleeping
Sleeping
| """ | |
| 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, | |
| ) | |