Spaces:
Sleeping
Sleeping
File size: 13,462 Bytes
270ccb2 506fd70 eb15f64 506fd70 0321a55 506fd70 0321a55 506fd70 bbf7f6c 067c35a 506fd70 0321a55 eb15f64 506fd70 eb15f64 506fd70 0321a55 506fd70 0321a55 eb15f64 506fd70 0321a55 506fd70 0321a55 506fd70 0321a55 506fd70 0321a55 506fd70 eb15f64 506fd70 eb15f64 506fd70 0321a55 506fd70 eb15f64 506fd70 eb15f64 506fd70 eb15f64 0321a55 eb15f64 0321a55 eb15f64 0321a55 506fd70 0321a55 506fd70 eb15f64 506fd70 eb15f64 506fd70 0321a55 506fd70 eb15f64 0321a55 506fd70 0321a55 506fd70 eb15f64 506fd70 0321a55 eb15f64 0321a55 506fd70 91cc315 506fd70 eb15f64 506fd70 eb15f64 506fd70 0321a55 eb15f64 506fd70 91cc315 506fd70 91cc315 506fd70 eb15f64 506fd70 91cc315 506fd70 0321a55 eb15f64 506fd70 91cc315 0321a55 91cc315 eb15f64 506fd70 eb15f64 506fd70 270ccb2 eb15f64 506fd70 eb15f64 0321a55 eb15f64 506fd70 eb15f64 506fd70 0321a55 eb15f64 506fd70 0321a55 eb15f64 506fd70 270ccb2 91cc315 eb15f64 0321a55 506fd70 91cc315 0321a55 eb15f64 506fd70 eb15f64 0321a55 506fd70 0321a55 270ccb2 0321a55 506fd70 0321a55 e41edf0 f7c2e2a 506fd70 eb15f64 506fd70 270ccb2 91cc315 eb15f64 91cc315 eb15f64 506fd70 0321a55 506fd70 270ccb2 0321a55 270ccb2 eb15f64 506fd70 91cc315 506fd70 91cc315 0321a55 91cc315 506fd70 91cc315 eb15f64 506fd70 91cc315 0321a55 eb15f64 91cc315 eb15f64 91cc315 506fd70 bbf7f6c 68a7c50 2f8f888 3c093d0 68a7c50 3c093d0 506fd70 0321a55 91cc315 506fd70 91cc315 506fd70 91cc315 506fd70 0321a55 506fd70 0321a55 506fd70 eb15f64 0321a55 eb15f64 0321a55 506fd70 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 | # -*- 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
) |