Lukeetah's picture
Update app.py
68a7c50 verified
# -*- 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
)