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
    )