Spaces:
Sleeping
Sleeping
| # -*- 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(""" | |
| <div class="app-header"> | |
| <p class="app-subtitle">🕸️ Web Scraper Tool</p> | |
| <p class="app-subtitle"> Extraer contenido de páginas web y convertirlo a formatos TXT o PDF</p> | |
| </div> | |
| """) | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=3): | |
| url_input = gr.Textbox( | |
| label="🔗 URL de la página web", | |
| placeholder="Ej: https://www.ejemplo.com/articulo o www.clarin.com.ar", | |
| info="Ingresa la URL completa. Se intentará normalizar (ej. añadir https://).", | |
| lines=1, | |
| elem_id="url-input" | |
| ) | |
| with gr.Column(scale=1, min_width=200): # Asegurar ancho para radios | |
| format_choice = gr.Radio( | |
| choices=["PDF", "TXT"], | |
| value="TXT", # Por defecto TXT | |
| label="📄 Formato de salida", | |
| info="PDF para visual/imágenes, TXT para texto plano." | |
| ) | |
| validation_output_display = gr.Textbox( | |
| label="Estado de la URL", | |
| interactive=False, | |
| placeholder="La URL normalizada o mensajes de validación aparecerán aquí...", | |
| lines=1, # Mantener una línea, pero el CSS puede ajustar la altura | |
| elem_classes=["validation-output-container"] | |
| ) | |
| process_btn = gr.Button("🚀 Extraer y Convertir", variant="primary", size="lg", elem_id="process-button") | |
| with gr.Accordion("📊 Resultado del Procesamiento y Archivo", open=True): | |
| result_output = gr.Markdown( # Usar Markdown para mejor formato | |
| value="Esperando procesamiento...", | |
| elem_id="result-markdown" | |
| ) | |
| file_output = gr.File( | |
| label="📁 Archivo generado (haz clic para descargar)", | |
| interactive=False, | |
| elem_id="file-output" | |
| ) | |
| gr.HTML(""" | |
| <div style="background-color: var(--secondary-color); border: 1px solid var(--border-color); border-radius: 12px; padding: 1.5rem; max-width: 720px; margin: auto; font-family: 'Segoe UI', sans-serif; color: var(--text-primary);"> | |
| <h3 style="color: var(--primary-color); font-weight: 700; margin-bottom: 1rem;">🔷 Información de uso</h3> | |
| <p style="margin-top: 1rem; color: var(--text-secondary)<strong>URLs flexibles:</strong> <span style="color: var(--text-secondary);">Intenta normalizar URLs incompletas (e.g., añadiendo "https://).</span></span> | |
| <p style="margin-top: 1rem; color: var(--text-secondary)<strong>Detección de contenido:</strong> <span style="color: var(--text-secondary);">Identifica si la URL es una imagen o una página web.</span></span> | |
| <p style="margin-top: 1rem; color: var(--text-secondary)<strong>Optimizado para Copilot:</strong> <span style="color: var(--text-secondary);">Los archivos generados buscan compatibilidad.</span></span> | |
| <p style="margin-top: 1rem; color: var(--text-secondary)<strong>Formatos:</strong> <span style="color: var(--text-secondary);">PDF (preserva estructura visual si es texto o imagen directa) y TXT (texto plano).</span><span> | |
| <p style="margin-top: 1rem; color: var(--text-secondary)<strong>Codificación:</strong> <span style="color: var(--text-secondary);">UTF-8 para TXT. Soporte Unicode para PDF con fuentes DejaVu o Arial.</span></span> | |
| <p style="margin-top: 1rem; color: var(--text-secondary)<strong>Fuente PDF:</strong> <span style="color: var(--text-secondary);">Si el archivo DejaVuSansCondensed.ttf está en el servidor o en una carpeta fonts, se usará para mejor soporte Unicode.</span></span></li> | |
| </p> | |
| <p style="margin-top: 1rem; color: var(--text-secondary);">Desarrollado por Lucas Correa para facilitar la integración con herramientas de IA.</p> | |
| </div> | |
| """) | |
| # Configurar eventos | |
| url_input.change( | |
| fn=validate_url_live, | |
| inputs=[url_input], | |
| outputs=[validation_output_display], | |
| ) | |
| process_btn.click( | |
| fn=process_url_and_update_ui, | |
| inputs=[url_input, format_choice], | |
| outputs=[result_output, file_output, validation_output_display] # Actualiza los tres outputs | |
| ) | |
| # Configuración para Hugging Face Spaces (o ejecución local) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=int(os.environ.get('PORT', 7860)), | |
| show_error=True, | |
| debug=True, | |
| # share=True # Descomentar para enlace público temporal si se ejecuta localmente | |
| ) |