Prompt para construir aplicación de generación y escritura automática de ALTs vacíos en WordPress


### Prompt para construir aplicación de generación y escritura automática de ALTs vacíos en WordPress.

Quiero que generes **una aplicación CLI en Python** (un solo repositorio) que haga lo siguiente de principio a fin:

#### 0) Metas y alcance

* Objetivo: **rellenar de forma segura y masiva el `alt_text` de imágenes en WordPress** usando un modelo multimodal de OpenRouter y **contexto semántico** (H2 más próximo si la imagen está incrustada en un post; H1/título si es imagen destacada).
* Debe **descargar y comprimir** cada imagen para abaratar el análisis, invocar **OpenRouter (modelo `qwen/qwen-2.5-vl-7b-instruct`)**, escribir un ALT SEO conciso en español, **actualizar WordPress** y **emitir un CSV de resultados** con enlace directo para editar la imagen en WP y una **puntuación de fiabilidad**.
* Todo debe poder ejecutarse con una **sola orden CLI** y con **.env** para los secretos.

---

#### 1) Estructura del proyecto

Crea un repo con estos archivos:

* `app.py` (entrypoint CLI con Typer)
* `wp_client.py` (cliente REST de WordPress)
* `vision_client.py` (cliente OpenRouter)
* `image_utils.py` (descarga, normalización y compresión)
* `context_extractor.py` (encontrar H2/H1 de contexto)
* `report.py` (CSV)
* `process_all_images.py` (script para procesamiento masivo con logging detallado)
* `process_post_complete.py` (script para procesar un post específico)
* `delete_alts.py` (script para eliminar ALT texts incorrectos)
* `force_update_alts.py` (script para forzar actualización de ALT texts específicos)
* `test_app.py` (script de pruebas)
* `requirements.txt`
* `.env.example`
* `README.md`
* `PROJECT_SUMMARY.md` (resumen del proyecto)
* `docs/IMPLEMENTATION.md` (documentación técnica)
* `examples/` (ejemplos de uso)

---

#### 2) Dependencias (requirements.txt)

Usa solo librerías mantenidas y comunes:

* `httpx>=0.25.0` (para HTTP con timeouts/concurrency)
* `python-dotenv>=1.0.0`
* `beautifulsoup4>=4.12.0`, `lxml>=4.9.0`
* `Pillow>=10.0.0`
* `tqdm>=4.66.0`
* `tenacity>=8.2.0` (reintentos)
* `pandas>=2.0.0` (generar CSV)
* `cairosvg>=2.7.0` (para rasterizar SVG a PNG antes de comprimir)
* `typer>=0.9.0` (CLI framework)

---

#### 3) Configuración por `.env`

Cargar al inicio con `dotenv` y fallar con mensaje claro si falta algo.

Variables:

* `WP_BASE_URL` 
* `WP_USERNAME`
* `WP_APPLICATION_PASSWORD` (Application Password de WordPress; autenticación Basic)
* `OPENROUTER_API_KEY`
* `OPENROUTER_BASE_URL` = `https://openrouter.ai/api/v1`
* `OPENROUTER_MODEL` = `qwen/qwen-2.5-vl-7b-instruct`
* Opcionales:

  * `MIN_CONFIDENCE` (por defecto `0.55`)
  * `CONCURRENCY` (por defecto `4`)
  * `TIMEOUT_SECONDS` (por defecto `30`)

---

#### 4) CLI (app.py)

Proveer flags con Typer:

* `--report PATH` (por defecto `report_updated.csv`)
* `--dry-run` (por defecto `True`. Si `True`, **no** actualiza alt en WP, solo simula y genera CSV)
* `--limit N` (procesar hasta N imágenes)
* `--include-pages` (incluir páginas además de posts; por defecto `False`)
* `--min-confidence FLOAT` (override de `MIN_CONFIDENCE`)
* `--post-id` (sirve para testear con un solo post, solo recupera las imágenes de ese post y hace el proceso de detección de alts vacíos, reconocimiento de la imagen, generación del texto, y escritura de los nuevos alts. El propósito es testeos)
* `--regenerate-mapping` (solo regenerar el mapeo del sitio)

Progreso con `tqdm`. Logging legible a stdout con colores y emojis usando `typer.secho`.

---

#### 5) Descubrimiento de imágenes sin ALT en WordPress (`wp_client.py`)

* Autenticación: **Basic** con `username:application_password` (Application Passwords nativo de WP ≥5.6).

* Inicialmente haremos un mapeo de todos los posts/pages del blog, guardando la relación de imágenes por post, y almacenando la situación de cada imagen: id, y si el alt está vacío, obtención del contexto... se detalla a continuación.

* Endpoints:

  * **Media**: `GET /wp-json/wp/v2/media?per_page=100&page=…&_fields=id,source_url,alt_text,media_type,mime_type,media_details`

    * Paginar hasta agotar resultados.
    * Filtrar **en cliente** las imágenes con `alt_text` vacío o nulo. Considerar solo mime `image/*`.
  * **Posts**: `GET /wp-json/wp/v2/posts?per_page=100&page=…&_fields=id,title,content,featured_media,link`
  * **Pages** (si `--include-pages=1`): `GET /wp-json/wp/v2/pages?...` con los mismos campos.

* Para cada **attachment** encontrado sin ALT:

  * `attachment.id`, `attachment.source_url`, `attachment.mime_type`.
  * **Contexto**:

    * **Como destacada**: si existe un post/página cuyo `featured_media == attachment.id`, entonces **contexto = H1 = `title.rendered`** de ese post/página (limpiar HTML).
    * **Como incrustada**: escanear `content.rendered` HTML de posts/páginas; localizar `<img>` cuyo `src` o `srcset` contenga la `source_url` (o cuyo `class` contenga `wp-image-{id}`). Con BeautifulSoup:

      * obtener el `h2` más cercano **anterior** (o el heading más cercano en el mismo bloque) como **contexto**; si no hay H2, usar el heading más próximo (H3/H4…); si no hay, fallback al **H1 (título)** del post.
    * Permitir múltiples usos; priorizar **H2** si existe, si no H1 del post/página destacada. Guarda `context_type` = `featured|embedded|unknown` y `context_heading` (texto limpio).

---

* el mapeo se archivará en el fichero `{url_base}_mapeo.json`
* una vez completado el mapeo ya podemos proceder con los siguientes pasos
* No se procesarán imágenes duplicadas
* Incluir estadísticas detalladas del mapeo (total posts, imágenes con/sin ALT, etc.)

---

#### 6) Descarga y preprocesado de imagen (`image_utils.py`)

* **Descargar** por `source_url` con timeouts y reintentos (tenacity).
* **Normalizar**:

  * Si `SVG`: intentar rasterizar con `cairosvg` → PNG → abrir con Pillow; si falla, **omitir** con aviso y registrar en CSV (status=`skipped_svg`).
  * Abrir con Pillow; si tiene alpha, **componer sobre fondo blanco**.
  * **Redimensionar a ancho 500px** manteniendo aspecto **solo si** la imagen es más ancha de 500px.
  * Convertir a **JPEG** con calidad **60** (o similar), `optimize=True`, modo RGB.
* Retornar **bytes** del JPEG comprimido + dimensiones resultantes.

---

#### 7) Cliente OpenRouter (`vision_client.py`)

* Consumir OpenRouter usando API **OpenAI-compatible** mediante `httpx` directo (no dependas de SDKs externos).
* Endpoint: `POST {OPENROUTER_BASE_URL}/chat/completions`

  * Headers:

    * `Authorization: Bearer {OPENROUTER_API_KEY}`
    * `HTTP-Referer: {WP_BASE_URL}` (si procede)
    * `X-Title: GuruWalk Alt Text Generator`
* **Modelo**: `{OPENROUTER_MODEL}` = `qwen/qwen-2.5-vl-7b-instruct`.
* **Mensaje** con contenido multimodal:

  * Pasa **la imagen como data URL** `data:image/jpeg;base64,...` **y** el **contexto textual** (H2/H1, tipo de contexto, URL pública, nombre del post) en el mismo turno de usuario.
  * Si el modelo no acepta data URL, usa la **URL pública** de la imagen (fallback).
* **Instrucciones del sistema (ES)** (inclúyelas como primer mensaje `role=system`):

  ```
  Eres un experto en accesibilidad y SEO. Escribe texto alternativo (atributo alt) en español neutro, conciso (ideal 8–16 palabras, máx. ~125–140 caracteres), sin comillas, sin "imagen de", sin stuffing de keywords, sin marcas salvo que aparezcan visualmente. Incluye texto legible en la imagen (pósters/carteles) cuando sea importante. 
  Si la imagen es un logo: "Logo de {marca}".
  Si es claramente decorativa o un icono genérico sin valor informativo, responde con skip=true.
  Devuelve SIEMPRE JSON estricto con esta forma:
  {
    "alt_text": "<string en español>",
    "confidence": <float 0..1>,
    "reason": "<máx. 15 palabras justificando la confianza>",
    "skip": <true|false>
  }
  Criterios de confianza: nitidez, ausencia de ambigüedad, coincidencia con el contexto (H2/H1), ausencia de alucinaciones, identificación correcta de escena/objeto, inclusión de texto visible relevante.
  ```
* **Mensaje de usuario** (plantilla):

  ```
  CONTEXTO:
  - Tipo: {context_type}
  - Heading: {context_heading}   // cadena limpia, puede estar vacía
  - Post/Página: {post_title} | ID: {post_id}
  - URL pública imagen: {source_url}
  - Idioma: es

  TAREA: Genera un alt_text SEO-friendly siguiendo las instrucciones del sistema. Responde SOLO el JSON.

  [IMAGEN ADJUNTA]
  ```
* Parsear respuesta; **validar JSON**; si falla, reintentar con prompt "solo JSON".

---

#### 8) Lógica de decisión y actualización (`app.py`)

Para cada attachment sin ALT:

1. **Descargar + comprimir** (500px) → bytes JPEG.
2. **Construir contexto** (H2/H1) como arriba.
3. **Llamar OpenRouter** con la imagen + contexto.
4. Si `skip==true` → registrar fila `status=skipped_decorative` y continuar.
5. Si `confidence < MIN_CONFIDENCE` → registrar `status=low_confidence` y **no** actualizar salvo que el usuario pase `--min-confidence` menor.
6. Si OK:

   * Si `--dry-run=False`: **POST** a `WP /wp-json/wp/v2/media/{id}` con body `{"alt_text": "<nuevo alt>"}` y Basic Auth. Manejar 401/403/429/5xx con **reintentos exponenciales**.
   * Considerar éxito si HTTP 200 y `alt_text` coincide.
7. Añadir fila a **CSV** con:

   * `image_url` (source\_url)
   * `attachment_id`
   * `context_type` (`featured|embedded|unknown`)
   * `context_heading`
   * `post_id` (si aplica)
   * `post_title` (si aplica)
   * `edit_media_url` = `{WP_BASE_URL}/wp-admin/post.php?post={attachment_id}&action=edit`
   * `old_alt_text` (vacío o existente)
   * `new_alt_text`
   * `confidence`
   * `model` (qwen/qwen-2.5-vl-7b-instruct)
   * `status` (`updated|dry_run|low_confidence|skipped_decorative|skipped_svg|error`)
   * `error_message` (si hay)

> Importante: El CSV **principal** debe listar **solo las imágenes cuyo ALT fue cambiado** (`status=updated` o `dry_run` si se hubiera actualizado). Crea además `report_all.csv` con todas las filas para auditoría.

---

#### 9) CSV y reporting (`report.py`)

* Escribir **`report_updated.csv`** con las columnas anteriores filtradas por `status in {updated,dry_run}`.
* Escribir **`report_all.csv`** con todo.
* Mostrar resumen final en consola: total candidatos, saltos, actualizados, tiempo total.

---

#### 10) Scripts adicionales

**`process_all_images.py`**: Script para procesamiento masivo con logging detallado completo, incluyendo información de cada imagen procesada, tiempos, y estadísticas.

**`process_post_complete.py`**: Script para procesar un post específico con análisis completo.

**`delete_alts.py`**: Script para eliminar ALT texts incorrectos de una lista predefinida de IDs.

**`force_update_alts.py`**: Script para forzar la actualización de ALT texts específicos con valores predefinidos.

**`test_app.py`**: Script de pruebas para verificar la funcionalidad básica.

---

#### 11) Robustez

* Timeouts configurables, reintentos (`tenacity`) para HTTP WP y OpenRouter.
* Sanitizar HTML de headings (strip tags, normalizar espacios).
* Manejar formatos: jpg/jpeg/png/webp/gif; gif animado → usa solo primer frame. svg → intentar rasterizar; si no, saltar.
* No procesar **duplicados**: de-dup por `source_url` y por `attachment_id`.
* Respeta `--limit N`.
* Logging colorido con emojis usando `typer.secho`.
* Manejo de errores con mensajes informativos en colores.

---

#### 12) README.md (instrucciones de uso)

Incluye:

* Cómo crear `Application Password` en WP.
* Cómo rellenar `.env` (proveer `.env.example`).
* Instalación: `python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt`
* Ejemplos:

  * **Simulación**:
    `python app.py --report report_updated.csv --dry-run --limit 200 --min-confidence 0.6`
  * **Aplicar cambios**:
    `python app.py --dry-run=False --report report_updated.csv`
  * **Procesar solo un post**:
    `python app.py --post-id 123 --dry-run`
  * **Regenerar mapeo**:
    `python app.py --regenerate-mapping`
  * **Procesamiento masivo**:
    `python process_all_images.py`
* Notas de privacidad: solo se envían a OpenRouter **la imagen comprimida** (o URL si data URL no aceptada) + contexto mínimo.

---

#### 13) Criterios de aceptación (tests manuales)

* La app detecta imágenes con `alt_text` vacío (al menos 10 ejemplos) y **genera** un `report_all.csv`.
* La app **actualiza** el `alt_text` de al menos un attachment cuando `--dry-run=False`.
* El enlace `edit_media_url` abre la pantalla de edición del adjunto en WP.
* El ALT generado:

  * está en español,
  * no supera \~140 caracteres,
  * no usa "imagen de",
  * incorpora texto visible si lo hay,
  * se ajusta al contexto H2/H1.
* El CSV principal **solo** contiene filas actualizadas (o que se actualizarían en dry-run).
* Los scripts adicionales funcionan correctamente para casos específicos.

---

**Genera todo el código y archivos descritos arriba.**
El lenguaje del ALT debe ser **español**.
Usa `qwen/qwen-2.5-vl-7b-instruct` en OpenRouter.
No incluyas credenciales en el repo.
El resultado final debe ser ejecutable con los pasos del README.
Usa `typer` para la CLI con colores y emojis.
Incluye logging detallado y manejo de errores robusto.
Daniel Pajuelo
Daniel Pajuelo es ingeniero informático y SEO Senior, actualmente trabajando en Guruwalk. En su blog personal escribe sobre Inteligencia Artificial, SEO, Programación... Ver más

Continua leyendo

Leer más sobre: SEO, Programación