# -*- coding: utf-8 -*- import gradio as gr import os from web_scraper_tool import WebScrapperTool # CSS personalizado con estética minimalista profesional custom_css = """ /* Importar fuente Inter */ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); /* Variables globales */ :root { --primary-color: #7c3aed; --primary-hover: #6d28d9; --secondary-color: #f1f5f9; --text-primary: #0f172a; /* texto principal fuerte */ --text-secondary: #475569; /* texto gris oscuro */ --border-color: #cbd5e1; --success-color: #10b981; --error-color: #ef4444; --warning-color: #f59e0b; --gradient-bg: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%); } /* Reset y configuración base */ * { box-sizing: border-box; } body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--gradient-bg); margin: 0; padding: 0; min-height: 100vh; } /* Contenedor principal */ .gradio-container { max-width: 800px !important; margin: 2rem auto !important; padding: 2rem 1rem !important; background: rgba(255, 255, 255, 0.95) !important; /* Fondo claro para el contenedor */ backdrop-filter: blur(10px); border-radius: 24px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); } /* Estilos para el encabezado personalizado */ .app-header { text-align: center; margin-bottom: 2rem; } .app-title { color: var(--text-primary); /* Color base oscuro */ font-size: 2.5rem !important; /* Aumentar tamaño */ font-weight: 700 !important; margin-bottom: 0.5rem; background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; padding: 0.2em 0; /* Añadir un poco de padding por si el clip es muy ajustado */ display: block; /* Asegurar que se muestre como bloque */ } .app-subtitle { color: var(--text-secondary) !important; /* Asegurar que se aplique el color */ font-size: 1.125rem !important; line-height: 1.6; margin-bottom: 2rem; } /* Campos de entrada */ .gr-textbox { border: 2px solid var(--border-color) !important; border-radius: 12px !important; padding: 12px 16px !important; font-size: 1rem !important; transition: all 0.3s ease !important; background: white !important; } .gr-textbox:focus { border-color: var(--primary-color) !important; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1) !important; outline: none !important; } /* Botones */ .gr-button { background: var(--primary-color) !important; color: white !important; border: none !important; border-radius: 12px !important; padding: 12px 24px !important; font-size: 1rem !important; font-weight: 600 !important; cursor: pointer !important; transition: all 0.3s ease !important; text-transform: none !important; letter-spacing: 0.025em !important; } .gr-button:hover { background: var(--primary-hover) !important; transform: translateY(-2px) !important; box-shadow: 0 10px 25px -5px rgba(139, 92, 246, 0.4) !important; } .gr-button:active { transform: translateY(0) !important; } /* Radio buttons */ .gr-radio { margin: 1rem 0 !important; } .gr-radio label span { /* Gradio envuelve el texto del label en un span */ font-weight: 500 !important; color: var(--text-primary) !important; font-size: 1rem !important; } /* Estado de la URL (validation_output_display) */ .validation-output-container .gr-textbox textarea { /* Específico para el output de validación */ font-family: 'Inter', monospace !important; font-size: 0.9rem !important; padding: 8px 12px !important; min-height: 40px !important; line-height: 1.4 !important; } /* Área de descarga */ .gr-file { border: 2px dashed var(--border-color) !important; border-radius: 12px !important; padding: 2rem !important; text-align: center !important; background: var(--secondary-color) !important; transition: all 0.3s ease !important; } .gr-file:hover { border-color: var(--primary-color) !important; background: rgba(139, 92, 246, 0.05) !important; } /* Responsive design */ @media (max-width: 768px) { .gradio-container { margin: 1rem !important; padding: 1.5rem 1rem !important; border-radius: 16px !important; } .app-title { font-size: 2rem !important; } .app-subtitle { font-size: 1rem !important; } } /* Footer */ .footer { text-align: center; margin-top: 2rem; padding-top: 2rem; border-top: 1px solid var(--border-color); color: var(--text-secondary); font-size: 0.875rem; } /* Animaciones sutiles (opcional) */ @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } /* Aplicar animación a elementos principales si se desea (puede requerir selectores más específicos) */ /* .gradio-container > div > .gr-form > * { animation: fadeIn 0.6s ease forwards; } */ """ scraper = WebScrapperTool() def validate_url_live(url: str): if not url or not url.strip(): return gr.Textbox(value="", placeholder="Estado de la URL", label="Estado de la URL") try: normalized_url = scraper.normalize_url(url.strip()) return gr.Textbox(value=f"✅ URL normalizada: {normalized_url}", interactive=False, label="Estado de la URL") except Exception as e: # Aquí normalizar podría fallar si la URL es muy malformada. return gr.Textbox(value=f"⚠️ Formato de URL inválido o necesita ajuste: {str(e)}", interactive=False, label="Estado de la URL") def process_url_and_update_ui(url: str, format_choice: str, progress=gr.Progress(track_tqdm=True)): if not url or not url.strip(): return "❌ Por favor ingresa una URL.", None, "Ingresa una URL válida primero." normalized_url_display_val = "" try: progress(0.1, desc="Normalizando URL...") normalized_url = scraper.normalize_url(url.strip()) # Actualiza el estado de la URL procesada para mostrarla incluso si hay error después normalized_url_display_val = f"URL procesada: {normalized_url}" progress(0.2, desc="Detectando tipo de contenido...") # La detección real del tipo de contenido ocurre dentro de _get_content # is_image_url es solo una suposición basada en la extensión. content_type_detected = "🖼️ Imagen (suposición inicial)" if scraper.is_image_url(normalized_url) else "📄 Página web (suposición inicial)" progress(0.4, desc=f"Extrayendo contenido como {format_choice}...") if format_choice == "PDF": result = scraper.scrape_to_pdf(normalized_url) else: result = scraper.scrape_to_text(normalized_url) progress(0.9, desc="Finalizando...") if result['status'] == 'success': progress(1.0, desc="¡Completado!") success_msg = f"""## ✅ **Procesamiento exitoso** **🔗 URL original:** `{url}` **⚙️ URL procesada:** `{result['url']}` **📁 Archivo generado:** `{os.path.basename(result['file'])}` **📊 Tipo detectado (suposición):** `{content_type_detected}` **📄 Formato de salida:** `{format_choice}` 💡 **Listo para Copilot:** El archivo está optimizado para ser procesado. """ return success_msg, result['file'], f"✅ {normalized_url_display_val}" else: # En caso de error, el mensaje de validación también debe reflejar la URL que se intentó procesar. error_msg_for_validation = f"⚠️ Error con {url}. {normalized_url_display_val if normalized_url_display_val else 'URL no pudo ser normalizada.'}" error_msg = f"""## ❌ **Error en el procesamiento** **🔗 URL intentada:** `{result.get('url', url)}` ({normalized_url_display_val}) **⚠️ Error:** `{result['message']}` 💡 **Sugerencias:** - Verifica que la URL sea accesible y correcta. - Intenta con una URL diferente. - Algunos sitios pueden bloquear el scraping. - Si es PDF y el error es por caracteres, intenta TXT. - Asegúrate que la URL apunte a contenido textual para TXT, o imagen/texto para PDF. """ return error_msg, None, error_msg_for_validation except Exception as e: import traceback tb_str = traceback.format_exc() # Mensaje para el campo de validación en caso de error crítico critical_error_validation_msg = f"💥 Error crítico procesando {url}. {normalized_url_display_val if normalized_url_display_val else 'URL no pudo ser normalizada.'}" error_msg = f"""## ❌ **Error inesperado en la aplicación** **⚠️ Error:** `{str(e)}` **📄 Detalles:** {tb_str[:400]}... 💡 **Intenta nuevamente o revisa la URL. Si el problema persiste, revisa los logs del servidor.** """ return error_msg, None, critical_error_validation_msg # Crear interfaz Gradio with gr.Blocks(css=custom_css, theme=gr.themes.Soft(), title="🕸️ Web Scraper Tool") as demo: # HTML para el encabezado con clases específicas gr.HTML("""
🕸️ Web Scraper Tool
Extraer contenido de páginas web y convertirlo a formatos TXT o PDF
Intenta normalizar URLs incompletas (e.g., añadiendo "https://).
Identifica si la URL es una imagen o una página web.
Los archivos generados buscan compatibilidad.
PDF (preserva estructura visual si es texto o imagen directa) y TXT (texto plano).
UTF-8 para TXT. Soporte Unicode para PDF con fuentes DejaVu o Arial.
Si el archivo DejaVuSansCondensed.ttf está en el servidor o en una carpeta fonts, se usará para mejor soporte Unicode.
Desarrollado por Lucas Correa para facilitar la integración con herramientas de IA.