Merlintxu commited on
Commit
a8a2139
·
verified ·
1 Parent(s): efd8ac1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +42 -488
app.py CHANGED
@@ -1,448 +1,51 @@
1
- import os
2
  import json
3
- import logging
4
- import re
5
- import requests
6
- import hashlib
7
- import PyPDF2
8
- import numpy as np
9
- import pandas as pd
10
- from io import BytesIO
11
- from typing import List, Dict, Optional, Tuple, Any
12
- from urllib.parse import urlparse, urljoin
13
- from concurrent.futures import ThreadPoolExecutor, as_completed
14
- from bs4 import BeautifulSoup
15
- from pathlib import Path
16
- from datetime import datetime
17
- from collections import defaultdict
18
- from sklearn.feature_extraction.text import TfidfVectorizer
19
- from requests.adapters import HTTPAdapter
20
- from urllib3.util.retry import Retry
21
- from transformers import pipeline
22
- from sentence_transformers import SentenceTransformer
23
- import torch
24
  import subprocess
25
  import sys
26
- import spacy
27
- import gradio as gr
28
- import matplotlib.pyplot as plt
29
 
30
- # Configuración de logging
31
- logging.basicConfig(
32
- level=logging.INFO,
33
- format='%(asctime)s - %(levelname)s - %(message)s'
34
- )
35
  logger = logging.getLogger(__name__)
36
 
37
-
38
- def sanitize_filename(filename: str) -> str:
39
- """
40
- Sanitiza el nombre de un archivo eliminando o reemplazando caracteres no permitidos.
41
- """
42
- filename = re.sub(r'[<>:"/\\|?*]', '_', filename)
43
- filename = re.sub(r'\s+', '_', filename)
44
- return filename
45
-
46
-
47
- class SEOSpaceAnalyzer:
48
  """
49
- Clase principal que encapsula la lógica para analizar un sitio web a partir de su sitemap.
50
  """
51
- def __init__(self, max_urls: int = 20, max_workers: int = 4) -> None:
52
- """
53
- Inicializa la sesión, carga los modelos y configura parámetros.
54
- :param max_urls: Número máximo de URLs a procesar en un análisis.
55
- :param max_workers: Número de hilos para la ejecución concurrente.
56
- """
57
- self.max_urls = max_urls
58
- self.max_workers = max_workers
59
- self.session = self._configure_session()
60
- self.models = self._load_models()
61
- self.base_dir = Path("content_storage")
62
- self.base_dir.mkdir(parents=True, exist_ok=True)
63
- self.current_analysis: Dict[str, Any] = {}
64
-
65
- def _load_models(self) -> Dict[str, Any]:
66
- """Carga modelos optimizados para Hugging Face y spaCy."""
67
- try:
68
- device = 0 if torch.cuda.is_available() else -1
69
- logger.info("Cargando modelos NLP...")
70
- models = {
71
- 'summarizer': pipeline("summarization", model="facebook/bart-large-cnn", device=device),
72
- 'ner': pipeline("ner", model="dslim/bert-base-NER", aggregation_strategy="simple", device=device),
73
- 'semantic': SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2'),
74
- 'spacy': spacy.load("es_core_news_lg")
75
- }
76
- logger.info("Modelos cargados correctamente.")
77
- return models
78
- except Exception as e:
79
- logger.error(f"Error cargando modelos: {e}")
80
- raise
81
-
82
- def _configure_session(self) -> requests.Session:
83
- """Configura una sesión HTTP con reintentos y headers personalizados."""
84
- session = requests.Session()
85
- retry = Retry(
86
- total=3,
87
- backoff_factor=1,
88
- status_forcelist=[500, 502, 503, 504],
89
- allowed_methods=['GET', 'HEAD']
90
- )
91
- adapter = HTTPAdapter(max_retries=retry)
92
- session.mount('http://', adapter)
93
- session.mount('https://', adapter)
94
- session.headers.update({
95
- 'User-Agent': 'Mozilla/5.0 (compatible; SEOBot/1.0)',
96
- 'Accept-Language': 'es-ES,es;q=0.9'
97
- })
98
- return session
99
-
100
- def analyze_sitemap(self, sitemap_url: str) -> Tuple[Dict, List[str], Dict, Dict]:
101
- """
102
- Analiza un sitemap completo, procesando URLs en paralelo y generando estadísticas, análisis de contenido, enlaces y recomendaciones SEO.
103
- :param sitemap_url: URL del sitemap XML.
104
- :return: Tuple con estadísticas, recomendaciones, análisis de contenido y análisis de enlaces.
105
- """
106
- try:
107
- logger.info(f"Parseando sitemap: {sitemap_url}")
108
- urls = self._parse_sitemap(sitemap_url)
109
- if not urls:
110
- logger.warning("No se pudieron extraer URLs del sitemap.")
111
- return {"error": "No se pudieron extraer URLs del sitemap"}, [], {}, {}
112
-
113
- results: List[Dict] = []
114
- with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
115
- futures = {executor.submit(self._process_url, url): url for url in urls[:self.max_urls]}
116
- for future in as_completed(futures):
117
- url = futures[future]
118
- try:
119
- res = future.result()
120
- results.append(res)
121
- logger.info(f"Procesado: {url}")
122
- except Exception as e:
123
- logger.error(f"Error procesando {url}: {e}")
124
- results.append({'url': url, 'status': 'error', 'error': str(e)})
125
-
126
- self.current_analysis = {
127
- 'stats': self._calculate_stats(results),
128
- 'content_analysis': self._analyze_content(results),
129
- 'links': self._analyze_links(results),
130
- 'recommendations': self._generate_seo_recommendations(results),
131
- 'details': results, # <-- Aquí se incluyen todos los detalles individuales
132
- 'timestamp': datetime.now().isoformat()
133
- }
134
- return (self.current_analysis['stats'],
135
- self.current_analysis['recommendations'],
136
- self.current_analysis['content_analysis'],
137
- self.current_analysis['links']),
138
- except Exception as e:
139
- logger.error(f"Error en análisis: {e}")
140
- return {"error": str(e)}, [], {}, {}
141
-
142
- def _process_url(self, url: str) -> Dict:
143
- """Procesa una URL individual y decide el método de procesamiento según el tipo de contenido."""
144
- try:
145
- response = self.session.get(url, timeout=15)
146
- response.raise_for_status()
147
- content_type = response.headers.get('Content-Type', '')
148
- result: Dict[str, Any] = {'url': url, 'status': 'success'}
149
-
150
- if 'application/pdf' in content_type:
151
- result.update(self._process_pdf(response.content))
152
- elif 'text/html' in content_type:
153
- result.update(self._process_html(response.text, url))
154
- else:
155
- result.update({'type': 'unknown', 'content': '', 'word_count': 0})
156
-
157
- self._save_content(url, response.content)
158
- return result
159
- except requests.exceptions.RequestException as e:
160
- logger.warning(f"Error procesando {url}: {str(e)}")
161
- return {'url': url, 'status': 'error', 'error': str(e)}
162
- except Exception as e:
163
- logger.error(f"Error inesperado en {url}: {str(e)}")
164
- return {'url': url, 'status': 'error', 'error': str(e)}
165
-
166
- def _process_html(self, html: str, base_url: str) -> Dict:
167
- """Procesa contenido HTML: extrae y limpia el texto, enlaces y metadatos."""
168
- soup = BeautifulSoup(html, 'html.parser')
169
- clean_text = self._clean_text(soup.get_text())
170
- return {
171
- 'type': 'html',
172
- 'content': clean_text,
173
- 'word_count': len(clean_text.split()),
174
- 'links': self._extract_links(soup, base_url),
175
- 'metadata': self._extract_metadata(soup)
176
- }
177
-
178
- def _process_pdf(self, content: bytes) -> Dict:
179
- """Procesa documentos PDF extrayendo texto de cada página."""
180
- try:
181
- text = ""
182
- with BytesIO(content) as pdf_file:
183
- reader = PyPDF2.PdfReader(pdf_file)
184
- for page in reader.pages:
185
- extracted = page.extract_text()
186
- text += extracted if extracted else ""
187
- clean_text = self._clean_text(text)
188
- return {
189
- 'type': 'pdf',
190
- 'content': clean_text,
191
- 'word_count': len(clean_text.split()),
192
- 'page_count': len(reader.pages)
193
- }
194
- except PyPDF2.PdfReadError as e:
195
- logger.error(f"Error leyendo PDF: {e}")
196
- return {'type': 'pdf', 'error': str(e)}
197
-
198
- def _clean_text(self, text: str) -> str:
199
- """Realiza la limpieza y normalización del texto."""
200
- if not text:
201
- return ""
202
- text = re.sub(r'\s+', ' ', text)
203
- return re.sub(r'[^\w\sáéíóúñÁÉÍÓÚÑ]', ' ', text).strip()
204
-
205
- def _extract_links(self, soup: BeautifulSoup, base_url: str) -> List[Dict]:
206
- """Extrae y clasifica enlaces presentes en el HTML."""
207
- links: List[Dict] = []
208
- base_netloc = urlparse(base_url).netloc
209
-
210
- for tag in soup.find_all('a', href=True):
211
- try:
212
- href = tag['href'].strip()
213
- if not href or href.startswith('javascript:'):
214
- continue
215
- full_url = urljoin(base_url, href)
216
- parsed = urlparse(full_url)
217
- links.append({
218
- 'url': full_url,
219
- 'type': 'internal' if parsed.netloc == base_netloc else 'external',
220
- 'anchor': self._clean_text(tag.get_text())[:100],
221
- 'file_type': self._get_file_type(parsed.path)
222
- })
223
- except Exception as e:
224
- logger.warning(f"Error procesando enlace {tag.get('href')}: {e}")
225
- continue
226
- return links
227
-
228
- def _get_file_type(self, path: str) -> str:
229
- """Determina el tipo de archivo según la extensión encontrada en la URL."""
230
- ext = Path(path).suffix.lower()
231
- return ext[1:] if ext else 'html'
232
-
233
- def _extract_metadata(self, soup: BeautifulSoup) -> Dict:
234
- """Extrae metadatos relevantes para SEO (título, descripción, keywords y etiquetas OpenGraph)."""
235
- metadata: Dict[str, Any] = {
236
- 'title': '',
237
- 'description': '',
238
- 'keywords': [],
239
- 'og': {}
240
- }
241
- if soup.title and soup.title.string:
242
- metadata['title'] = soup.title.string.strip()[:200]
243
-
244
- for meta in soup.find_all('meta'):
245
- name = meta.get('name', '').lower()
246
- property_ = meta.get('property', '').lower()
247
- content = meta.get('content', '')
248
- if name == 'description':
249
- metadata['description'] = content[:300]
250
- elif name == 'keywords':
251
- metadata['keywords'] = [kw.strip() for kw in content.split(',') if kw.strip()]
252
- elif property_.startswith('og:'):
253
- metadata['og'][property_[3:]] = content
254
- return metadata
255
-
256
- def _parse_sitemap(self, sitemap_url: str) -> List[str]:
257
- """
258
- Parsea un sitemap XML e incluso maneja índices de sitemaps.
259
- :return: Lista de URLs encontradas en el sitemap.
260
- """
261
- try:
262
- response = self.session.get(sitemap_url, timeout=10)
263
- response.raise_for_status()
264
-
265
- if 'xml' not in response.headers.get('Content-Type', ''):
266
- logger.warning(f"El sitemap no parece ser XML: {sitemap_url}")
267
- return []
268
-
269
- soup = BeautifulSoup(response.text, 'lxml-xml')
270
- urls: List[str] = []
271
- # Manejo de sitemap index
272
- if soup.find('sitemapindex'):
273
- for sitemap in soup.find_all('loc'):
274
- url = sitemap.text.strip()
275
- if url.endswith('.xml'):
276
- urls.extend(self._parse_sitemap(url))
277
- else:
278
- urls = [loc.text.strip() for loc in soup.find_all('loc')]
279
- # Filtrar URLs que empiezan por http y eliminar duplicados
280
- filtered_urls = list({url for url in urls if url.startswith('http')})
281
- return filtered_urls
282
- except Exception as e:
283
- logger.error(f"Error al parsear el sitemap {sitemap_url}: {e}")
284
- return []
285
-
286
- def _save_content(self, url: str, content: bytes) -> None:
287
- """
288
- Almacena el contenido descargado en una estructura organizada. Antes de escribir, verifica si ya existe el archivo.
289
- """
290
- try:
291
- parsed = urlparse(url)
292
- domain_dir = self.base_dir / parsed.netloc
293
- # Construir ruta a partir de la ruta URL
294
- path = parsed.path.lstrip('/')
295
- if not path or path.endswith('/'):
296
- path = os.path.join(path, 'index.html')
297
- safe_path = sanitize_filename(path)
298
- save_path = domain_dir / safe_path
299
- save_path.parent.mkdir(parents=True, exist_ok=True)
300
-
301
- # Calcula hash del contenido y evita re-escribir si el archivo existe y es idéntico
302
- new_hash = hashlib.md5(content).hexdigest()
303
- if save_path.exists():
304
- with open(save_path, 'rb') as f:
305
- existing_content = f.read()
306
- existing_hash = hashlib.md5(existing_content).hexdigest()
307
- if new_hash == existing_hash:
308
- logger.debug(f"El contenido de {url} ya está guardado y es idéntico.")
309
- return
310
-
311
- with open(save_path, 'wb') as f:
312
- f.write(content)
313
- logger.info(f"Contenido guardado en: {save_path}")
314
- except Exception as e:
315
- logger.error(f"Error al guardar contenido para {url}: {e}")
316
-
317
- def _calculate_stats(self, results: List[Dict]) -> Dict:
318
- """Calcula estadísticas básicas sobre el conjunto de resultados procesados."""
319
- successful = [r for r in results if r.get('status') == 'success']
320
- content_types = [r.get('type', 'unknown') for r in successful]
321
- avg_word_count = round(np.mean([r.get('word_count', 0) for r in successful]) if successful else 0, 1)
322
- return {
323
- 'total_urls': len(results),
324
- 'successful': len(successful),
325
- 'failed': len(results) - len(successful),
326
- 'content_types': pd.Series(content_types).value_counts().to_dict(),
327
- 'avg_word_count': avg_word_count,
328
- 'failed_urls': [r['url'] for r in results if r.get('status') != 'success']
329
- }
330
-
331
- def _analyze_content(self, results: List[Dict]) -> Dict:
332
- """
333
- Analiza el contenido extraído usando TF-IDF y muestra algunas muestras.
334
- :return: Diccionario con keywords y ejemplos de contenido.
335
- """
336
- successful = [r for r in results if r.get('status') == 'success' and r.get('content')]
337
- texts = [r['content'] for r in successful if len(r['content'].split()) > 10]
338
- if not texts:
339
- return {'top_keywords': [], 'content_samples': []}
340
  try:
341
- stop_words = list(self.models['spacy'].Defaults.stop_words)
342
- vectorizer = TfidfVectorizer(stop_words=stop_words, max_features=50, ngram_range=(1, 2))
343
- tfidf = vectorizer.fit_transform(texts)
344
- feature_names = vectorizer.get_feature_names_out()
345
- sorted_indices = np.argsort(np.asarray(tfidf.sum(axis=0)).ravel())[-10:]
346
- top_keywords = feature_names[sorted_indices][::-1].tolist()
347
- except Exception as e:
348
- logger.error(f"Error en análisis TF-IDF: {e}")
349
- top_keywords = []
350
- return {
351
- 'top_keywords': top_keywords,
352
- 'content_samples': [{'url': r['url'], 'sample': (r['content'][:500] + '...') if len(r['content']) > 500 else r['content']}
353
- for r in successful[:3]]
354
- }
355
-
356
- def _analyze_links(self, results: List[Dict]) -> Dict:
357
- """
358
- Analiza la estructura de enlaces en el contenido procesado.
359
- :return: Estadísticas de enlaces internos, dominios externos, anclas y tipos de archivos.
360
- """
361
- all_links = []
362
- for result in results:
363
- if result.get('links'):
364
- all_links.extend(result['links'])
365
- if not all_links:
366
- return {
367
- 'internal_links': {},
368
- 'external_domains': {},
369
- 'common_anchors': {},
370
- 'file_types': {}
371
- }
372
- df = pd.DataFrame(all_links)
373
- return {
374
- 'internal_links': df[df['type'] == 'internal']['url'].value_counts().head(20).to_dict(),
375
- 'external_domains': df[df['type'] == 'external']['url']
376
- .apply(lambda x: urlparse(x).netloc)
377
- .value_counts().head(10).to_dict(),
378
- 'common_anchors': df['anchor'].value_counts().head(10).to_dict(),
379
- 'file_types': df['file_type'].value_counts().to_dict()
380
- }
381
-
382
- def _generate_seo_recommendations(self, results: List[Dict]) -> List[str]:
383
- """
384
- Genera recomendaciones SEO basadas en metadatos, cantidad de contenido y estructura de enlaces.
385
- :return: Lista de recomendaciones.
386
- """
387
- successful = [r for r in results if r.get('status') == 'success']
388
- if not successful:
389
- return ["No se pudo analizar ningún contenido exitosamente"]
390
-
391
- recs = []
392
- missing_titles = sum(1 for r in successful if not r.get('metadata', {}).get('title'))
393
- if missing_titles:
394
- recs.append(f"📌 Añadir títulos a {missing_titles} páginas")
395
- short_descriptions = sum(1 for r in successful if not r.get('metadata', {}).get('description'))
396
- if short_descriptions:
397
- recs.append(f"📌 Añadir meta descripciones a {short_descriptions} páginas")
398
- short_content = sum(1 for r in successful if r.get('word_count', 0) < 300)
399
- if short_content:
400
- recs.append(f"📝 Ampliar contenido en {short_content} páginas (menos de 300 palabras)")
401
-
402
- all_links = [link for r in results for link in r.get('links', [])]
403
- if all_links:
404
- df_links = pd.DataFrame(all_links)
405
- internal_links = df_links[df_links['type'] == 'internal']
406
- if len(internal_links) > 100:
407
- recs.append(f"🔗 Optimizar estructura de enlaces internos ({len(internal_links)} enlaces)")
408
- return recs if recs else ["✅ No se detectaron problemas críticos de SEO"]
409
-
410
- def _plot_internal_links(self, links_data: Dict) -> Optional[plt.Figure]:
411
- """
412
- Genera un gráfico de barras para la distribución de enlaces internos.
413
- :param links_data: Diccionario con los enlaces internos.
414
- :return: Figura de matplotlib o None si no hay datos.
415
- """
416
- internal_links = links_data.get('internal_links', {})
417
- if not internal_links:
418
- return None
419
- fig, ax = plt.subplots()
420
- names = list(internal_links.keys())
421
- counts = list(internal_links.values())
422
- ax.barh(names, counts)
423
- ax.set_xlabel("Cantidad de enlaces")
424
- ax.set_title("Top 20 Enlaces Internos")
425
- plt.tight_layout()
426
- return fig
427
-
428
 
429
  def create_interface() -> gr.Blocks:
430
- """
431
- Crea la interfaz de usuario utilizando Gradio.
432
- """
433
  analyzer = SEOSpaceAnalyzer()
434
  with gr.Blocks(title="SEO Analyzer Pro", theme=gr.themes.Soft()) as interface:
435
  gr.Markdown("""
436
  # 🕵️ SEO Analyzer Pro
437
  **Analizador SEO avanzado con modelos de lenguaje**
438
 
439
- Sube la URL de un sitemap.xml para analizar todo el sitio web.
440
  """)
441
  with gr.Row():
442
  with gr.Column():
443
- sitemap_input = gr.Textbox(label="URL del Sitemap",
444
- placeholder="https://ejemplo.com/sitemap.xml",
445
- interactive=True)
 
 
446
  analyze_btn = gr.Button("Analizar Sitio", variant="primary")
447
  with gr.Row():
448
  clear_btn = gr.Button("Limpiar")
@@ -450,97 +53,48 @@ def create_interface() -> gr.Blocks:
450
  plot_btn = gr.Button("Visualizar Enlaces Internos", variant="secondary")
451
  with gr.Column():
452
  status_output = gr.Textbox(label="Estado del Análisis", interactive=False)
453
- progress_bar = gr.Progress()
454
-
455
  with gr.Tabs():
456
  with gr.Tab("📊 Resumen"):
457
  stats_output = gr.JSON(label="Estadísticas Generales")
458
  recommendations_output = gr.JSON(label="Recomendaciones SEO")
459
  with gr.Tab("📝 Contenido"):
460
  content_output = gr.JSON(label="Análisis de Contenido")
461
- gr.Examples(
462
- examples=[{"content": "Ejemplo de análisis de contenido..."}],
463
- inputs=[content_output],
464
- label="Ejemplos de Salida"
465
- )
466
  with gr.Tab("🔗 Enlaces"):
467
  links_output = gr.JSON(label="Análisis de Enlaces")
468
  links_plot = gr.Plot(label="Visualización de Enlaces Internos")
469
- with gr.Tab("📂 Documentos"):
470
- gr.Markdown("""
471
- ### Documentos Encontrados
472
- Los documentos descargados se guardan en la carpeta `content_storage/`
473
- """)
474
-
475
- # Función que genera el reporte y lo guarda en disco
476
- def generate_report() -> Optional[str]:
477
  if analyzer.current_analysis:
478
  report_path = "content_storage/seo_report.json"
479
- try:
480
- with open(report_path, 'w', encoding='utf-8') as f:
481
- json.dump(analyzer.current_analysis, f, indent=2, ensure_ascii=False)
482
- return report_path
483
- except Exception as e:
484
- logger.error(f"Error generando reporte: {e}")
485
- return None
486
- return None
487
-
488
- # Callback para generar gráfico de enlaces internos a partir del análisis almacenado
489
- def generate_internal_links_plot(links_json: Dict) -> Any:
490
- fig = analyzer._plot_internal_links(links_json)
491
- return fig if fig is not None else {}
492
-
493
- # Asignación de acciones a botones y otros eventos
494
  analyze_btn.click(
495
  fn=analyzer.analyze_sitemap,
496
  inputs=sitemap_input,
497
- outputs=[stats_output, recommendations_output, content_output, links_output],
498
  show_progress=True
499
  )
500
  clear_btn.click(
501
- fn=lambda: [None] * 4,
502
- outputs=[stats_output, recommendations_output, content_output, links_output]
503
  )
504
  download_btn.click(
505
  fn=generate_report,
506
  outputs=gr.File(label="Descargar Reporte")
507
  )
508
  plot_btn.click(
509
- fn=generate_internal_links_plot,
510
  inputs=links_output,
511
  outputs=links_plot
512
  )
513
  return interface
514
 
515
-
516
- def setup_spacy_model() -> None:
517
- """
518
- Verifica y descarga el modelo de spaCy 'es_core_news_lg' si no está instalado.
519
- """
520
- try:
521
- spacy.load("es_core_news_lg")
522
- logger.info("Modelo spaCy 'es_core_news_lg' cargado correctamente.")
523
- except OSError:
524
- logger.info("Descargando modelo spaCy 'es_core_news_lg'...")
525
- try:
526
- subprocess.run(
527
- [sys.executable, "-m", "spacy", "download", "es_core_news_lg"],
528
- check=True,
529
- stdout=subprocess.PIPE,
530
- stderr=subprocess.PIPE
531
- )
532
- logger.info("Modelo descargado exitosamente.")
533
- except subprocess.CalledProcessError as e:
534
- logger.error(f"Error al descargar modelo: {e.stderr.decode()}")
535
- raise RuntimeError("No se pudo descargar el modelo spaCy") from e
536
-
537
-
538
  if __name__ == "__main__":
539
  setup_spacy_model()
540
  app = create_interface()
541
- app.launch(
542
- server_name="0.0.0.0",
543
- server_port=7860,
544
- show_error=True,
545
- share=False
546
- )
 
1
+ import gradio as gr
2
  import json
3
+ from seo_analyzer import SEOSpaceAnalyzer
4
+ import spacy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import subprocess
6
  import sys
7
+ import logging
 
 
8
 
9
+ logging.basicConfig(level=logging.INFO)
 
 
 
 
10
  logger = logging.getLogger(__name__)
11
 
12
+ def setup_spacy_model() -> None:
 
 
 
 
 
 
 
 
 
 
13
  """
14
+ Verifica y descarga el modelo de spaCy 'es_core_news_lg' si no está instalado.
15
  """
16
+ try:
17
+ spacy.load("es_core_news_lg")
18
+ logger.info("Modelo spaCy 'es_core_news_lg' cargado correctamente.")
19
+ except OSError:
20
+ logger.info("Descargando modelo spaCy 'es_core_news_lg'...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  try:
22
+ subprocess.run(
23
+ [sys.executable, "-m", "spacy", "download", "es_core_news_lg"],
24
+ check=True,
25
+ stdout=subprocess.PIPE,
26
+ stderr=subprocess.PIPE
27
+ )
28
+ logger.info("Modelo descargado exitosamente.")
29
+ except subprocess.CalledProcessError as e:
30
+ logger.error(f"Error al descargar modelo: {e.stderr.decode()}")
31
+ raise RuntimeError("No se pudo descargar el modelo spaCy") from e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  def create_interface() -> gr.Blocks:
 
 
 
34
  analyzer = SEOSpaceAnalyzer()
35
  with gr.Blocks(title="SEO Analyzer Pro", theme=gr.themes.Soft()) as interface:
36
  gr.Markdown("""
37
  # 🕵️ SEO Analyzer Pro
38
  **Analizador SEO avanzado con modelos de lenguaje**
39
 
40
+ Ingresa la URL de un sitemap.xml para analizar el sitio web.
41
  """)
42
  with gr.Row():
43
  with gr.Column():
44
+ sitemap_input = gr.Textbox(
45
+ label="URL del Sitemap",
46
+ placeholder="https://ejemplo.com/sitemap.xml",
47
+ interactive=True
48
+ )
49
  analyze_btn = gr.Button("Analizar Sitio", variant="primary")
50
  with gr.Row():
51
  clear_btn = gr.Button("Limpiar")
 
53
  plot_btn = gr.Button("Visualizar Enlaces Internos", variant="secondary")
54
  with gr.Column():
55
  status_output = gr.Textbox(label="Estado del Análisis", interactive=False)
 
 
56
  with gr.Tabs():
57
  with gr.Tab("📊 Resumen"):
58
  stats_output = gr.JSON(label="Estadísticas Generales")
59
  recommendations_output = gr.JSON(label="Recomendaciones SEO")
60
  with gr.Tab("📝 Contenido"):
61
  content_output = gr.JSON(label="Análisis de Contenido")
 
 
 
 
 
62
  with gr.Tab("🔗 Enlaces"):
63
  links_output = gr.JSON(label="Análisis de Enlaces")
64
  links_plot = gr.Plot(label="Visualización de Enlaces Internos")
65
+ with gr.Tab("📄 Detalles"):
66
+ details_output = gr.JSON(label="Detalles Individuales")
67
+ def generate_report() -> str:
 
 
 
 
 
68
  if analyzer.current_analysis:
69
  report_path = "content_storage/seo_report.json"
70
+ with open(report_path, 'w', encoding='utf-8') as f:
71
+ json.dump(analyzer.current_analysis, f, indent=2, ensure_ascii=False)
72
+ return report_path
73
+ return ""
74
+ def plot_internal_links(links_json: dict) -> any:
75
+ return analyzer.plot_internal_links(links_json)
 
 
 
 
 
 
 
 
 
76
  analyze_btn.click(
77
  fn=analyzer.analyze_sitemap,
78
  inputs=sitemap_input,
79
+ outputs=[stats_output, recommendations_output, content_output, links_output, details_output],
80
  show_progress=True
81
  )
82
  clear_btn.click(
83
+ fn=lambda: [None, None, None, None, None],
84
+ outputs=[stats_output, recommendations_output, content_output, links_output, details_output]
85
  )
86
  download_btn.click(
87
  fn=generate_report,
88
  outputs=gr.File(label="Descargar Reporte")
89
  )
90
  plot_btn.click(
91
+ fn=plot_internal_links,
92
  inputs=links_output,
93
  outputs=links_plot
94
  )
95
  return interface
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  if __name__ == "__main__":
98
  setup_spacy_model()
99
  app = create_interface()
100
+ app.launch(server_name="0.0.0.0", server_port=7860, show_error=True, share=False)