Sync: guardar en el historial el trabajo de las ultimas semanas que solo vivia en el disco

Este repo local tenia origin apuntando al Gitea local (localhost:3000), que Rafa
declaro archivado el 2026-06-28 (commit 962f33a, desarrollo movido a
gitea.feadulta.com). Ese memo nunca llego a este checkout: main quedo congelado
y todo el trabajo real de las ultimas 3+ semanas se fue commiteando solo en la
rama fix/multiidioma-portada-132 (ya fusionada a main sin perdida, commit
2504666), mientras que ademas se acumulaban 78 cambios sin commitear en el
working tree que nunca llegaron a NINGUN historial de git.

Este commit consolida esos cambios sueltos: TTS multi-voz (tts_*.py), scripts
de traduccion (translate_haiku.py, pretranslate_en_haiku.py, sync_translations_to_prod.py),
mu-plugins nuevos desplegados a prod (fea-beta-feedback, fea-cloudflare-realip,
fea-legacy-redirect, fea-gsc-verification, fea-support-campaign, fea-ui, etc.),
scripts de mantenimiento de enlaces/cartas, capturas E2E (tools/e2e/shot_*.cjs)
y documentacion de sesiones recientes.

Excluido deliberadamente (no es codigo versionable): tts-voices/ (3.3GB de
muestras de audio para clonacion de voz, anadido a .gitignore), logs/ (logs de
ejecucion, anadido a .gitignore), y 2 ficheros vacios accidentales + 2 copias
duplicadas sueltas en la raiz que ya existen en su ubicacion correcta.
This commit is contained in:
2026-07-15 20:03:21 -04:00
parent 250466696b
commit 69e849d38e
72 changed files with 5413 additions and 220 deletions
+130 -38
View File
@@ -19,11 +19,13 @@
| Entorno | URL | Estado |
|---------|-----|--------|
| **Beta (actual)** | `https://wp-nuevo.feadulta.com` | Es donde se trabaja **ahora** |
| Producción final | `https://feadulta.com` | Tras el "cutover" de DNS (cambiará la URL base) |
| **Producción** | `https://www.feadulta.com` | El sitio en vivo, WordPress. Cutover completado el 2026-07-08 |
| Legado Joomla | `https://antiguo.feadulta.com` | Solo contenido histórico no migrado. No se publica aquí |
Mientras no se avise, **todo va a `wp-nuevo.feadulta.com`**. Cuando se haga el cambio de
dominio, solo hay que sustituir la URL base en los ejemplos de abajo.
**El cutover de dominio ya se hizo (2026-07-08):** `www.feadulta.com` es WordPress y es donde
se publica siempre. `wp-nuevo.feadulta.com` (el subdominio de Beta) ha dejado de servir nada
(el directorio que usaba se vació al mover los ficheros a la raíz del hosting) — si algún
enlace o script antiguo todavía lo menciona, hay que cambiarlo a `www.feadulta.com`.
### 1.2 Acceso recomendado: API REST de WordPress + contraseña de aplicación
@@ -33,7 +35,7 @@ No hace falta SSH ni tocar la base de datos. WordPress trae una API REST y un si
**Cómo obtener la credencial (lo hace Inma una sola vez):**
Inma **ya tiene usuario con rol Editor** en el sitio (suficiente para crear, editar,
publicar y programar entradas). Con ese usuario:
1. Entrar en `https://wp-nuevo.feadulta.com/wp-admin`.
1. Entrar en `https://www.feadulta.com/wp-admin`.
2. Ir a **Usuarios → Perfil** → bajar hasta **"Contraseñas de aplicación"**.
3. Escribir un nombre (p. ej. `cowork-cartas`) y pulsar **Añadir**.
4. WordPress muestra una contraseña de 24 caracteres con espacios
@@ -45,10 +47,10 @@ usuario):
```bash
curl -s -u "USUARIO:xxxx xxxx xxxx xxxx xxxx xxxx" \
https://wp-nuevo.feadulta.com/wp-json/wp/v2/users/me
https://www.feadulta.com/wp-json/wp/v2/users/me
```
Base de la API para todo lo demás: `https://wp-nuevo.feadulta.com/wp-json/wp/v2/`
Base de la API para todo lo demás: `https://www.feadulta.com/wp-json/wp/v2/`
> **⚠️ Pendiente de configurar en Cloudflare (bloqueante).** Comprobado el **27/06/2026**: el
> sitio está tras Cloudflare y devuelve **403 "Attention Required"** a **cualquier** petición
@@ -96,8 +98,8 @@ Las categorías se asignan por su **ID numérico** vía API (campo `categories:
| ID | Nombre | Slug | Significado |
|----|--------|------|-------------|
| **6** | Carta de la semana | `cartasemana` | La carta **vigente**. Debe estar solo la actual. |
| **22** | La semana pasada | `carta-semana-pasada` | La carta de la semana anterior. |
| **21** | Cartas de otras semanas | `cartas-de-otras-semanas` | Histórico (todas las anteriores). |
| **22** | La semana pasada | `carta-semana-pasada` | La carta de la semana anterior. Debe estar solo una. |
| **21** | Cartas de otras semanas | `cartas-de-otras-semanas` | Histórico **acumulativo**: TODAS las cartas publicadas alguna vez, incluidas la vigente (6) y la anterior (22). No se quita nunca, solo se añade. Así el listado de "Otras Semanas" sirve para navegar por cualquier carta pasada sin tener que ir carta por carta. |
### 3.2 Categorías temáticas (para los artículos de dentro de la carta)
@@ -128,7 +130,7 @@ estén listos, publicarlos.
```bash
curl -s -u "USUARIO:APP_PASSWORD" \
-X POST https://wp-nuevo.feadulta.com/wp-json/wp/v2/posts \
-X POST https://www.feadulta.com/wp-json/wp/v2/posts \
-H "Content-Type: application/json" \
-d '{
"title": "Título del artículo",
@@ -145,6 +147,56 @@ De la respuesta JSON interesan dos campos:
Repetir para cada artículo, usando la categoría temática que corresponda (§3.2).
> ⚠️ **No adivinéis la URL a partir del título — usad siempre el `link` real de la API,
> y solo DESPUÉS de publicar.** En la carta 734 los artículos se quedaron en `draft` con el
> título prefijado `[PRUEBA]`, y la carta se compuso enlazando slugs *adivinados* a mano
> (`.../prueba-silencio/`, etc.) a partir de ese título provisional. Al publicar de verdad
> con el título limpio, WordPress genera el slug desde el título final y **le añade un
> sufijo (`-2`, `-4`…) si ya existe otro post con ese mismo slug** — algo frecuente en
> temas recurrentes ("Silencio", "La palabra", "Te encontré"…). Resultado: 21 de 22
> enlaces de la carta apuntaban a una URL que nunca existió. Regla: **publicad cada
> artículo (quitando `[PRUEBA]` del título) en cuanto esté listo**, y componed los
> enlaces de la carta con el `link` que devuelve la API **tras ese publish**, no antes.
### Paso 1.5 — Asignar `_carta_id` a cada artículo de la semana (IMPORTANTE)
Cada artículo de la semana debe llevar el meta `_carta_id` = ID del post de la carta a la
que pertenece. **Sin esto, el sistema no puede localizar "todos los artículos de esta
carta" para publicarlos/traducirlos/locutarlos en bloque.**
`_carta_id` es un meta interno y **no se puede asignar por el endpoint estándar**
`/wp/v2/posts/<id>` (WordPress lo bloquea por empezar por `_`). Para esto hay un
**endpoint REST propio**, desplegado el 2026-07-07:
```bash
# Leer el _carta_id actual de un artículo
curl -s -u "USUARIO:APP_PASSWORD" \
https://www.feadulta.com/wp-json/fea/v1/carta-id/ID_DEL_ARTICULO
# Asignar (o corregir) el _carta_id de un artículo
curl -s -u "USUARIO:APP_PASSWORD" \
-X POST https://www.feadulta.com/wp-json/fea/v1/carta-id/ID_DEL_ARTICULO \
-H "Content-Type: application/json" \
-d '{"carta_id": ID_DE_LA_CARTA}'
# Borrar el _carta_id (si os habéis equivocado de artículo)
curl -s -u "USUARIO:APP_PASSWORD" \
-X DELETE https://www.feadulta.com/wp-json/fea/v1/carta-id/ID_DEL_ARTICULO
```
Respuesta en los tres casos: `{"post_id": ID, "carta_id": ID|null}`.
Notas:
- Solo podéis asignar `_carta_id` a artículos **que ya podéis editar** (mismo criterio que
el resto de la API: vuestro usuario Editor).
- `carta_id` tiene que ser el ID de **un post que exista** (normalmente el de la carta,
aunque en el momento de llamar a este endpoint la carta puede que aún esté en borrador —
eso no importa, solo tiene que existir el post).
- Podéis llamarlo justo después de crear cada artículo (Paso 1) o al final, después de tener
el ID de la carta (Paso 3) — el orden entre pasos 1 y 1.5 y 3 no importa mientras al acabar
todos los artículos de la semana tengan el `_carta_id` correcto.
- Código fuente: `wp-content/mu-plugins/fea-carta-id-api.php` (mu-plugin, activo siempre).
### Paso 2 — Componer el artículo-carta
La carta es un post HTML con:
@@ -156,7 +208,9 @@ Ver §5 para el formato exacto de los encabezados y un ejemplo completo.
### Paso 3 — Publicar (o programar) la carta
La carta va en la categoría **6** (`cartasemana`).
La carta va en la categoría **6** (`cartasemana`) **y también en la 21** (`cartas-de-otras-semanas`)
desde el primer momento — ver §3.1: la 21 es un histórico acumulativo que no se quita nunca, así
que toda carta la lleva ya desde que se crea, no solo cuando se "degrada" en el Paso 4.
- **Publicar ya:** `"status": "publish"`.
- **Programar:** `"status": "future"` + `"date"` con la fecha/hora local del sitio en
@@ -164,35 +218,53 @@ La carta va en la categoría **6** (`cartasemana`).
```bash
curl -s -u "USUARIO:APP_PASSWORD" \
-X POST https://wp-nuevo.feadulta.com/wp-json/wp/v2/posts \
-X POST https://www.feadulta.com/wp-json/wp/v2/posts \
-H "Content-Type: application/json" \
-d '{
"title": "Carta de la semana — 29 de junio",
"content": "<p>…cuerpo de la carta con sus secciones y enlaces…</p>",
"status": "future",
"date": "2026-06-29T08:00:00",
"categories": [6]
"categories": [6, 21]
}'
```
### Paso 4 — Rotar la carta anterior (IMPORTANTE, manual)
Al entrar una carta nueva en la categoría **6**, hay que **degradar la anterior** para que la
categoría "Carta de la semana" contenga **solo una** carta:
categoría "Carta de la semana" contenga **solo una** carta. Esto es solo mover el marcador de
**estado** (6 → 22 → ninguno): la categoría **21** ("Otras semanas") **no se toca en la
rotación**, porque es acumulativa y ya la lleva cada carta desde que se publicó (Paso 3). Nunca
se quita la 21 a una carta.
1. A la carta que **dejaba** de ser actual: quitarle la categoría **6** y ponerle la **22**
("La semana pasada").
2. A la que estaba en **22**: pasarla a la **21** ("Cartas de otras semanas").
("La semana pasada"). Mantiene la **21** que ya tenía.
2. A la que estaba en **22**: quitarle la **22** sin más. Mantiene la **21** que ya tenía —
así queda solo en el histórico "Otras semanas", visible igual que todas las demás.
Vía API se actualiza el array `categories` del post (sustituye al anterior):
Vía API se actualiza el array `categories` del post (sustituye al anterior, así que hay que
**incluir siempre la 21** en el array nuevo o se perdería):
```bash
# 1. La carta que deja de ser actual: 6 → 22 (conserva 21)
curl -s -u "USUARIO:APP_PASSWORD" \
-X POST https://wp-nuevo.feadulta.com/wp-json/wp/v2/posts/ID_DE_LA_CARTA_VIEJA \
-X POST https://www.feadulta.com/wp-json/wp/v2/posts/ID_DE_LA_CARTA_VIEJA \
-H "Content-Type: application/json" \
-d '{"categories": [22]}'
-d '{"categories": [22, 21]}'
# 2. La que estaba en 22: se queda solo con 21 (y las que no sean de estado, p.ej. 71)
curl -s -u "USUARIO:APP_PASSWORD" \
-X POST https://www.feadulta.com/wp-json/wp/v2/posts/ID_DE_LA_CARTA_MAS_VIEJA \
-H "Content-Type: application/json" \
-d '{"categories": [21]}'
```
> ⚠️ El endpoint `POST /wp/v2/posts/ID` con `categories` **sustituye** el array completo, no
> añade. Antes de rotar, comprobar con `GET /wp-json/wp/v2/posts/ID_DE_LA_CARTA?_fields=categories`
> qué categorías tiene ya el post (p. ej. si tiene además la 71 "Feadulta") e incluirlas todas
> en el array nuevo — si no, se pierden categorías sin querer (esto pasó en la carta 733/734,
> ver issue #159 y #161).
>
> Si se programa la carta nueva con `future`, esta rotación puede hacerse el mismo día en que
> se publique. Si surge duda sobre qué carta está en qué categoría, consultar:
> `GET /wp-json/wp/v2/posts?categories=6` (debe devolver solo una).
@@ -220,6 +292,12 @@ de portada indicado:
- Los enlaces dentro de cada sección deben apuntar a **artículos que existan** en el sitio
(las URLs del Paso 1). Enlaces a páginas externas se ignoran.
- Si una sección no tiene enlaces, ese bloque de la portada queda vacío.
- **"Noticias de alcance" NO se enlaza a mano en el cuerpo de la carta.** Si la semana
incluye una noticia de este tipo, el artículo va categorizado en la categoría **41**
("Noticias de alcance") y **eso basta**: la portada tiene un bloque de footer aparte que
se alimenta solo de esa categoría. Si además se enlaza esa noticia dentro de "Artículos
seleccionados para la semana", **sale duplicada** en la portada (pasó en la carta 734,
ver issue #159).
**Ejemplo mínimo de cuerpo de carta:**
@@ -228,23 +306,23 @@ de portada indicado:
<p><strong style="color:red">Evangelio y comentarios al Evangelio</strong></p>
<ul>
<li><a href="https://wp-nuevo.feadulta.com/comentario-evangelio-domingo/">Comentario al evangelio</a></li>
<li><a href="https://www.feadulta.com/comentario-evangelio-domingo/">Comentario al evangelio</a></li>
</ul>
<p><strong style="color:red">Artículos seleccionados para la semana</strong></p>
<ul>
<li><a href="https://wp-nuevo.feadulta.com/articulo-uno/">Primer artículo</a></li>
<li><a href="https://wp-nuevo.feadulta.com/articulo-dos/">Segundo artículo</a></li>
<li><a href="https://www.feadulta.com/articulo-uno/">Primer artículo</a></li>
<li><a href="https://www.feadulta.com/articulo-dos/">Segundo artículo</a></li>
</ul>
<p><strong style="color:red">Para unas eucaristías más participativas y actuales</strong></p>
<ul>
<li><a href="https://wp-nuevo.feadulta.com/eucaristia-domingo/">Material para la eucaristía</a></li>
<li><a href="https://www.feadulta.com/eucaristia-domingo/">Material para la eucaristía</a></li>
</ul>
<p><strong style="color:red">Material multimedia</strong></p>
<ul>
<li><a href="https://wp-nuevo.feadulta.com/video-semana/">Vídeo de la semana</a></li>
<li><a href="https://www.feadulta.com/video-semana/">Vídeo de la semana</a></li>
</ul>
```
@@ -276,31 +354,42 @@ de portada indicado:
---
## 7. Traducción y audio automáticos (lado servidor — informativo)
## 7. Traducción y audio (lado servidor — ⚠️ hoy es MANUAL, no automático)
**Inma no tiene que traducir ni generar audio.** En el servidor hay (o habrá) un proceso
programado (cron) que:
**Corregido 2026-07-12: esto todavía NO es automático.** El cron que traduciría y generaría
audio solo al publicar (issue [#23](https://gitea.feadulta.com/rafa/feadulta/issues/23)) sigue
sin implementar — es una propuesta abierta, no algo que ya corra. Versiones anteriores de esta
guía decían "hay (o habrá) un proceso programado" dando a entender que ya estaba activo o a
punto; no lo está. **Inma NO tiene que traducir ni generar audio ella misma**, pero sí tiene que
**pedirlo** — no llega solo.
1. Detecta cartas y artículos nuevos publicados **en español** sin traducción.
2. Los **traduce** a EN/FR/IT/PT y los enlaza como traducciones (Polylang).
3. Genera el **audio TTS** (voz, MiniMax) de la carta y sus artículos.
**Cómo pedirlo hoy:** escribir al grupo de WhatsApp mencionando "Hermes" (o por Telegram),
indicando la carta/artículo. Hermes ejecuta los scripts correspondientes en el servidor de Rafa.
Ver el runbook completo (motores de traducción, voces TTS por autor, tiempos, qué hacer si
Hermes no responde) en `docs/guia-tts-traduccion-inma.md`.
Por eso es importante: **subir siempre el contenido en español** y dejar que el proceso haga
el resto. Si una carta urgente necesita traducción inmediata, avisar a Rafa.
**Por eso sigue siendo importante subir siempre el contenido en español** — la traducción parte
siempre del ES, pero hay que pedirla, no asumir que "ya llegará". Si una carta urgente necesita
traducción inmediata, avisar a Rafa directamente además de pedírselo a Hermes.
> *Este punto es responsabilidad de Rafa (infraestructura). Se incluye aquí solo para que el
> asistente de Inma sepa que no debe duplicar ese trabajo.*
> *Este punto es responsabilidad de Rafa (infraestructura). Se incluye aquí para que el
> asistente de Inma sepa que el trabajo de traducir/locutar no lo tiene que hacer él mismo,
> pero sí que tiene que solicitarlo activamente.*
---
## 8. Resumen rápido (checklist por carta)
- [ ] Crear cada artículo de la semana (borrador → publicado). Guardar su URL.
- [ ] Crear cada artículo de la semana y **publicarlo** (no dejarlo en `draft`/`[PRUEBA]`).
- [ ] Guardar la URL (`link`) de cada artículo **después** de publicarlo, no antes (el slug
puede cambiar por colisión con otro post del mismo título).
- [ ] Asignar `_carta_id` a cada artículo con el endpoint de §1.5 (`fea/v1/carta-id/{id}`).
- [ ] Componer la carta en HTML con los **encabezados exactos** de §5 y los enlaces a esos artículos.
- [ ] Publicar o programar la carta en la categoría **6**.
- [ ] Rotar la carta anterior: 6 → 22, y la de 22 → 21.
- [ ] Publicar o programar la carta en las categorías **6 y 21** (§3.1, §4 Paso 3).
- [ ] Rotar la carta anterior: quitar 6, poner 22 (conservando su 21). A la que estaba en 22,
quitarle solo la 22 (conservando su 21 — nunca se quita la 21 a nadie, §4 Paso 4).
- [ ] Comprobar que la portada muestra las secciones (esperar hasta 15 min si hace falta).
- [ ] No traducir ni generar audio: lo hace el servidor.
- [ ] No traducir ni generar audio a mano — pero SÍ pedirlo a Hermes (WhatsApp/Telegram, ver §7 y `docs/guia-tts-traduccion-inma.md`). No es automático todavía.
---
@@ -314,6 +403,9 @@ el resto. Si una carta urgente necesita traducción inmediata, avisar a Rafa.
| Editar artículo/carta | `POST /wp-json/wp/v2/posts/{id}` |
| Ver carta vigente | `GET /wp-json/wp/v2/posts?categories=6` |
| Subir imagen | `POST /wp-json/wp/v2/media` (cabecera `Content-Disposition`) |
| Leer `_carta_id` de un artículo | `GET /wp-json/fea/v1/carta-id/{id}` |
| Asignar `_carta_id` a un artículo | `POST /wp-json/fea/v1/carta-id/{id}` con body `{"carta_id": N}` |
| Borrar `_carta_id` de un artículo | `DELETE /wp-json/fea/v1/carta-id/{id}` |
Campos útiles del post: `title`, `content` (HTML), `status` (`draft`/`publish`/`future`),
`date` (ISO 8601 para programar), `categories` (array de IDs), `featured_media` (ID de adjunto).
+121
View File
@@ -0,0 +1,121 @@
# Guía de traducción y audio (TTS) para Inma / Mixbot — feadulta.com
> **Para quién es este documento:** para Inma y su asistente (Mixbot / Cowork), y como
> referencia para Hermes cuando se le pide que traduzca o locute un artículo/carta.
>
> **Estado real a 2026-07-12 (importante, corrige la guía de publicación §7 de versiones
> anteriores): esto NO es automático.** No hay ningún cron corriendo hoy que traduzca o genere
> audio solo al publicar — esa automatización es la propuesta abierta
> [issue #23](https://gitea.feadulta.com/rafa/feadulta/issues/23), sin implementar. Todo lo de
> abajo es un proceso que **hay que pedir**, hoy solo ejecutable en el servidor/PC de Rafa.
---
## 1. Quién puede hacer qué, hoy
| Tarea | ¿Quién puede hacerla sin Rafa presente? |
|---|---|
| Publicar carta/artículos en español | **Sí, Inma/Mixbot solos** — API REST ya funciona (ver `docs/guia-publicacion-carta-inma.md`). No depende del PC de Rafa. |
| Pedir traducción o TTS | **Solo indirectamente**: hay que pedírselo a **Hermes** (WhatsApp/Telegram). Los scripts que traducen y locutan viven únicamente en el PC/servidor de Rafa (Docker local + credenciales locales) — Mixbot no tiene acceso directo a ellos. |
| Traducir/locutar si Hermes tampoco está disponible | **Hoy, no.** Es la limitación real que hay que conocer: si el PC de Rafa está apagado o Hermes está caído, ni Inma ni Mixbot pueden disparar esto por su cuenta. Ver §5 (qué falta para que esto no dependa de Hermes). |
## 2. Cómo pedir una traducción o un audio (mientras Hermes esté disponible)
Escribir al grupo de WhatsApp `Feadulta_webmaster` mencionando "Hermes" (o por Telegram),
indicando qué carta/artículo (ID de WordPress o título+fecha si no se tiene el ID) y qué se
necesita: traducción, audio, o ambos. Ejemplos:
> "Hermes, tradúceme la carta 54XXX a los 4 idiomas"
> "Hermes, genera el audio de los artículos de la carta de esta semana"
Hermes ejecuta los scripts de abajo en el servidor de Rafa. No hace falta que Inma/Mixbot sepan
los nombres de los scripts ni los IDs internos — es información para cuando Hermes (o Rafa)
necesite el detalle técnico.
## 3. Traducción — motores disponibles
`scripts/translate_post.py` (repo `joomla-migration`) soporta tres motores via `FEA_ENGINE`:
| Motor | Coste | Cuándo usarlo |
|---|---|---|
| **`gemma`** (por defecto) | Gratis (modelo local, LM Studio en el PC de Rafa) | Opción por defecto. Requiere que el PC/GPU de Rafa esté encendido. |
| **`minimax`** | De pago, acotado (misma cuenta que el TTS) | Alternativa cuando Gemma no está disponible o la calidad no basta. |
| **`haiku`** | ⚠️ **De pago vía API directa de Anthropic** (no es cuota de sesión) | **No usar por defecto ni de forma autónoma.** Choca con la política de no gastar API de pago sin que Rafa confirme cada vez. Reservado para cuando Rafa lo ejecuta él mismo o da autorización puntual. |
Comando (lo ejecuta Hermes o Rafa, no Inma/Mixbot directamente):
```bash
cd /home/rafa/joomla-migration
python3 scripts/translate_post.py --carta <ID_CARTA> --langs en,fr,it,pt --status draft
# o para un solo artículo:
python3 scripts/translate_post.py --post-id <ID_POST> --langs en,fr,it,pt --status draft
```
`--status draft` dejar en borrador para revisión; `--status publish` publica directo. Tras
traducir, hace falta el paso de enlaces internos (`scripts/fix_carta_joomla_links.php`) y, si se
publica, degradar la carta anterior (`scripts/demote_old_cartasemana.php`) — Hermes ya conoce
este flujo (ver skill `feadulta-webmaster`, `references/procedures.md`).
## 4. Audio (TTS) — voces por autor
`scripts/minimax_tts.py` + `scripts/tts_produce.py` (genera y escribe en WP local) +
`scripts/sync_audio_to_prod.py` (sube a prod, soporta `--rollback` para deshacer). Modelo MiniMax
`speech-2.8-hd`.
**Voz por defecto:** `NicoFeadulta2026` (todos los autores sin voz clonada).
**Voces clonadas por autor** (issue #152 — solo estos 4 autores usan su propia voz, el resto cae
a Nico):
| Autor | WP user_id | voice_id |
|---|---|---|
| Fray Marcos | 382 | `FrayMarcosFeadulta2026` |
| José Antonio Pagola | 383 | `PagolaFeadulta2026` |
| José Luis Sicre | 774 | `SicreFeadulta2026` |
| José Arregi | 386 | `ArregiFeadulta2026` |
Añadir un autor nuevo a esta lista requiere clonar su voz primero (grabación limpia 2-5 min, sin
música/ruido de fondo — verificar con espectrograma antes de clonar, ver memoria
`feadulta-tts-voz-fraymarcos-202607` para el procedimiento y los descartes por música colada) y
añadirlo a `AUTHOR_VOICES` en `scripts/minimax_tts.py`. Esto sí requiere que Rafa (o alguien con
acceso al repo y a MiniMax) lo haga — no es autoservicio para Inma/Mixbot hoy.
**Generar audio de una carta concreta** (por defecto `tts_produce.py` procesa una cola larga de
cartas pendientes — para priorizar una carta concreta, sobreescribir la cola):
```bash
cd /home/rafa/joomla-migration
FEA_TTS_CARTAS="<ID_CARTA>" python3 scripts/tts_produce.py
```
Reanudable (no repite lo ya hecho, meta `fea_audio_done`) y con freno automático si la cuota de
MiniMax se agota (para tras fallos seguidos, no se queda colgado).
**Publicar el audio en prod** (el paso anterior solo escribe en el WordPress local):
```bash
python3 scripts/sync_audio_to_prod.py --carta <ID_CARTA>
# deshacer si algo suena mal:
python3 scripts/sync_audio_to_prod.py --rollback --carta <ID_CARTA>
```
Runbook de rollback ya documentado para que Hermes lo ejecute sin Rafa presente: issue #163.
## 5. La API key de MiniMax — decisión pendiente (de Rafa, no resuelta en este documento)
Hoy la key vive en un fichero local de Rafa, usada para TTS (y podría usarse para traducción
`FEA_ENGINE=minimax`). Para que Inma/Mixbot puedan disparar esto sin pasar por Hermes, harían
falta tanto acceso a esta key como acceso al entorno donde corren los scripts (Docker local del
PC de Rafa) — hoy ninguna de las dos cosas es cierta. Ver el issue maestro
[#172](https://gitea.feadulta.com/rafa/feadulta/issues/172) para las opciones que se están
valorando (key separada para Inma, gestor de secretos compartido, o mantener todo detrás de
Hermes). Mientras no se decida, la vía real es §2: pedírselo a Hermes.
## 6. Qué falta para que esto sea de verdad independiente de Hermes/Rafa
Siendo honestos: hoy, si el PC de Rafa está apagado (viaje, avería, lo que sea) y Hermes no
responde, **no hay forma de que Inma/Mixbot generen traducción o audio por su cuenta** — los
scripts y el WordPress local que usan como paso intermedio solo existen ahí. Para que esto
cambiara de verdad haría falta uno de:
- Mover el pipeline de traducción/TTS a un sitio alcanzable por Mixbot directamente (ej. correr
contra prod en vez de contra el WordPress local, y alojar los scripts en un servidor
accesible, no en el PC personal de Rafa).
- O implementar de una vez el cron automático (issue #23) para que ni siquiera haga falta
pedirlo — se dispara solo al publicar en español.
Ninguna de las dos está hecha. Documentado aquí para que la decisión de priorizarlo (o no) sea
consciente, no un descuido.
@@ -0,0 +1,180 @@
# Handoff técnico para Rafa — issues #174 y #175
## Alcance
Preparación técnica solamente. **No aplicar en producción desde aquí.**
---
## Issue #174 — cierre autónomo de la carta (publicar + rotar)
### Regla funcional confirmada por Inma/Mixbot
La rotación que ya funcionó hoy manualmente por WP REST API para la carta 735 es esta, y **solo para el post ES**:
1. **nueva carta** → añadir cat **6** (`carta actual`)
2. **la que estaba en cat 6** → quitar cat 6, añadir cat **22** (`semana pasada`), manteniendo cat **21** (`archivo acumulativo`)
3. **la que estaba en cat 22** → quitar cat 22, dejar solo cat **21**
4. **no tocar traducciones** en este paso
### Decisión pendiente para Rafa
Hay dos caminos válidos:
#### Opción A — adaptar `rotate_cartas.php` a ES-only
Pros:
- reutiliza lógica ya existente y conocida en el repo
- más coherente con el toolkit actual de scripts
- fácil dejar `DRY-RUN` + `APPLY=1`
Contras:
- hay que modificar el script actual porque hoy rota todos los idiomas
- si Mixbot publica por REST y la rotación la hace otro script aparte, el flujo queda repartido en dos piezas
#### Opción B — que Mixbot haga la rotación por WP REST API
Pros:
- ya está validado en vivo hoy
- mantiene toda la operación editorial en el mismo sitio donde ya se publica la carta
- evita acoplar la decisión editorial a un script server-side adicional
Contras:
- la lógica queda fuera del repo de scripts PHP si no se documenta bien
- conviene dejarla muy explícita para no divergirse del comportamiento esperado
### Recomendación técnica
Mi lectura: **cualquiera vale**, pero elegiría según quién vaya a mantenerlo:
- si Rafa quiere la lógica **versionada y centralizada en el repo**, mejor **A: `rotate_cartas.php` ES-only**
- si Inma/Mixbot ya tienen el flujo sólido y prefieren autonomía total desde su lado, mejor **B: REST API desde Mixbot**, con la lógica documentada en el issue/runbook
### Si Rafa elige A (script ES-only), propuesta concreta
Crear una variante mínima o adaptar `rotate_cartas.php` con estas reglas:
- entrada: `CARTA=<es_id>`
- lookup solo en idioma `es`
- localizar:
- `actual_es` = post con cat 6
- `pasada_es` = post con cat 22
- `nueva_es` = `CARTA`
- operaciones:
- `pasada_es` pierde 22, conserva 21
- `actual_es` pierde 6, gana 22, conserva 21
- `nueva_es` gana 6 y conserva/gana 21
- **nunca tocar** posts EN/FR/IT/PT
- mantener modo `DRY-RUN`
### Dry-run deseable para Rafa
Salida ideal del dry-run:
- `nueva_es=#54495 -> +6 (+21 si faltaba)`
- `actual_es=#54254 -> -6 +22 (mantiene 21)`
- `pasada_es=#53984 -> -22 (mantiene 21)`
- `translations untouched`
### Decisión funcional que conviene dejar escrita
Para evitar ambigüedad futura, dejar explícito en el issue o en el script:
- `21` es acumulativa y la conservan todas las cartas archivables
- `6` y `22` son mutuamente excluyentes
- la rotación editorial inicial afecta **solo al ES**
- las traducciones rotan/publican en un paso posterior independiente
---
## Issue #175 — endpoint `subir-avatar`
### Base ya existente
El sistema actual ya tiene casi todo:
- frontend lee `user_meta('foto_perfil')`
- `face_crop_avatar.py` ya resuelve recorte/centrado de cara
- `regen_avatars.php` ya existe para regeneración/ajustes
- caso real listo para probar: **Silvia Martínez Cano**, `user_id=1112`, `slug=silvia-mtnz`
### Contrato recomendado
Yo le propondría a Rafa este contrato por simplicidad operativa:
#### Endpoint
`POST /wp-json/fea/v1/subir-avatar`
#### Auth
La misma Application Password que ya usa `crear-autor` (`#166`), con el mismo criterio de permisos.
#### Identificador
**`user_id`** mejor que `slug`.
Motivo:
- Mixbot ya lo tiene en el roster
- evita ambigüedades por slug cambiado / transliteraciones / colisiones
- simplifica el lookup
#### Formato de entrada
**multipart/form-data** mejor que base64.
Motivo:
- más natural para subir imágenes
- menos overhead
- encaja mejor con `media_handle_sideload` / manejo típico WP
- más fácil de depurar que base64
#### Campo esperado
- `user_id` (obligatorio)
- `file` (obligatorio)
Opcionalmente:
- `crop=server|client` si Rafa quiere dejar ambas puertas abiertas, aunque probablemente es overkill de entrada
### Recomendación de procesamiento
Mi recomendación clara: **que el endpoint NO haga face-crop complejo** en la primera versión.
Mejor v1:
- Mixbot/Inma mandan la imagen **ya cuadrada**
- estándar objetivo: **1254x1254** (el que ya usáis)
- el endpoint solo:
1. valida usuario
2. guarda attachment
3. actualiza `foto_perfil`
4. devuelve `attachment_id`, `user_id`, `url`
Pros:
- mucho menos riesgo y menos dependencias server-side
- evita meter OpenCV/Pillow/lógica pesada dentro del endpoint
- más fácil de testear e idempotente
- si mañana queréis face-crop automático, se añade en v2
### Comportamiento recomendado del endpoint
- requiere login + permiso tipo `edit_others_posts` (igual filosofía que `crear-autor`)
- valida que `user_id` exista
- acepta solo imagen (`jpg`, `jpeg`, `png`, `webp` si WP lo admite ahí)
- crea attachment en Media Library
- hace `update_user_meta($user_id, 'foto_perfil', $attachment_id)`
- respuesta JSON:
- `user_id`
- `display_name`
- `attachment_id`
- `url`
- `updated: true`
### Idempotencia mínima útil
No hace falta obsesionarse en v1, pero sí conviene:
- si se vuelve a subir otra foto para el mismo `user_id`, simplemente reemplazar el `foto_perfil` al nuevo attachment
- opcional: si el hash del fichero coincide con el ya asignado, devolver `updated:false`
### Caso piloto sugerido
Primera prueba con:
- `user_id=1112`
- `slug=silvia-mtnz`
- imagen ya recortada por Inma/Mixbot a **1254x1254**
Así la prueba valida solo el endpoint, no el pipeline de crop.
---
## Cierres administrativos
Por estado funcional, salvo sorpresa:
- `#166` → cerrable
- `#168` → cerrable
- `#170` → cerrable
---
## Recomendación final para Rafa
Si quiere minimizar trabajo y riesgo:
- **#174**: usar la lógica que ya validó Mixbot hoy, y solo portar al repo si él prefiere centralizar
- **#175**: endpoint mínimo con `user_id` + multipart + imagen ya cuadrada a 1254x1254
Eso deja la autonomía mucho más cerca sin meterse en una refactor rara.
+94
View File
@@ -0,0 +1,94 @@
# Revisión rápida de issues mencionados por Inma/Rafa — 2026-07-15
## Alcance
Revisión en modo **diagnóstico + preparación**. **Sin cambios en producción**.
## Observación importante sobre numeración
En el repo actual de Gitea `rafa/feadulta` los issues llegan hoy hasta **#146**. Por tanto, las referencias del mensaje (`#166`, `#168`, `#170`, `#174`, `#175`) no existen en el tracker actual y probablemente pertenecen a una numeración anterior o a notas habladas.
Para no quedarnos bloqueados, revisé los issues **actuales y más cercanos al tema de autonomía / operación**:
- `#144` Cron servidor: auto-traducción (Haiku) + TTS (MiniMax)
- `#145` Habilitar acceso a la REST API para publicar cartas
- `#146` Propuesta: arquitectura multiagente con Ringer, Goose, OB1, Hermes y OpenClaw
- además `#126` y el histórico `#121` / `#125` por dependencia operativa
## Hallazgos verificados
### 1) Issue #126 — seguridad login post-cutover
Verificación server-side en prod (`/web`) hecha por SSH + `wp eval`:
- `llar_active=1`
- `mu_exists=1`
- mu-plugin presente: `fea-cloudflare-realip.php`
Conclusión:
- **La parte operativa del checklist parece ya presente en prod.**
- El issue probablemente necesita **actualización/cierre**, no trabajo técnico urgente.
### 2) Gap antiguo de repo sobre `fea-cloudflare-realip.php`
El comentario viejo de `#126` decía que el mu-plugin no estaba trackeado en git.
Estado actual en el repo local:
- Sí existe: `wordpress/wp-content/mu-plugins/fea-cloudflare-realip.php`
Conclusión:
- Ese comentario ya quedó **desactualizado**.
### 3) Issue #144 — base técnica para autonomía traducción/TTS
Comprobado en el repo: existen los bloques principales mencionados por el issue:
- `scripts/detect_untranslated.php`
- `scripts/translate_post.py`
- `scripts/fix_carta_joomla_links.php`
- `scripts/demote_old_cartasemana.php`
- `scripts/minimax_tts.py`
- `scripts/sync_audio_to_prod.py`
Además ejecuté el detector en el **WordPress local Docker** (no en prod):
#### Ejecución local
`docker exec wordpress-web php /tmp/detect_untranslated.php 0.12 draft`
- Resultado: **0 ofensores draft**
`docker exec wordpress-web php /tmp/detect_untranslated.php 0.12 any`
- Resultado: **12 ofensores sospechosos** sobre publicados/cualquier estado
- Resumen por idioma:
- `en: 4/1148`
- `fr: 3/1148`
- `it: 3/1147`
- `pt: 2/1147`
IDs señalados por el detector:
- `47978, 47981, 47980, 47979`
- `47756, 47153`
- `54304, 47285, 54307, 43278, 54306, 54305`
Notas útiles:
- El script **no corre bien desde host** porque exige `/var/www/html/wp-load.php`; hay que lanzarlo dentro del contenedor.
- Esto es buena pista para futura automatización en `#144`: ya hay piezas, pero conviene empaquetarlas en un wrapper reproducible y con contexto de ejecución claro.
### 4) Issue #145 — REST API para Inma
No hice verificación externa definitiva porque eso requiere:
- credencial real de aplicación, y
- prueba end-to-end frente a Cloudflare
Estado documental actual:
- la skill y la documentación operativa indican que la REST API **ya fue habilitada para Inma**, pero no he revalidado hoy ese punto desde fuera.
Conclusión:
- Antes del viaje a Madrid conviene hacer un **smoke test real** (`/wp-json/wp/v2/users/me`) desde fuera del server.
### 5) Issue #146 — autonomía / multiagente
Lo revisado aquí es principalmente discusión/arquitectura; no detecté una acción local obvia de código en este repo que mereciera tocar hoy sin alinear primero el objetivo.
## Recomendación práctica
1. **Aclarar la numeración** de `#166/#168/#170/#174/#175` para no revisar los issues equivocados.
2. Si los “nuevos de autonomía” eran realmente los del tracker actual, yo priorizaría así:
- `#145`: verificar de verdad que Inma puede publicar sin Rafa delante.
- `#144`: encapsular el flujo traducción/TTS en un wrapper/cron operativo.
- `#146`: dejarlo como diseño/roadmap, no como siguiente cambio técnico directo.
3. `#126` huele a **issue de limpieza/cierre** si nadie ve un fleco pendiente.
## Qué NO hice
- No apliqué cambios en producción.
- No toqué Joomla legacy.
- No abrí/cerré/edité issues en Gitea.
- No modifiqué código del repo en esta revisión.
@@ -0,0 +1,155 @@
# Segunda pasada — issues correctos en Gitea vivo (`gitea.feadulta.com`) — 2026-07-15
## Alcance
Revisión en modo **diagnóstico + preparación** sobre la instancia viva:
- Repo: `https://gitea.feadulta.com/rafa/feadulta`
- Issues revisados: `#166`, `#168`, `#170`, `#174`, `#175`
- **Sin cambios en producción**
- **Sin edición de issues**
## Corrección del desajuste anterior
La pasada previa consultó el Gitea local archivado (`localhost:3000`), que se queda en `#146`. La fuente correcta para el trabajo actual es **`gitea.feadulta.com`**, tal como ya reflejan:
- la skill `feadulta-editorial-workflows`
- `feadulta/webmaster/references/environment.md`
## Estado por issue
### `#166` — Alta de autores / `crear-autor`
**Estado funcional real:** resuelto e integrado.
Verificado en el issue vivo:
- Rafa comentó que decidió la **opción B** (endpoint acotado).
- Rafa comentó después: **"Desplegado en prod y verificado en vivo"**.
- Inma comentó el 14-jul que ya está **integrado en Mixbot**.
Verificado además en el repo local:
- existe `wordpress/wp-content/mu-plugins/fea-crear-autor-api.php`
- registra `POST /wp-json/fea/v1/crear-autor`
- fuerza rol fijo `author`
- es **idempotente** si el slug ya existe
Conclusión:
- **No hay trabajo técnico pendiente aquí**.
- Si queréis limpieza de tablero, este issue ya está para **cerrar** cuando Rafa quiera.
---
### `#168` — Bots en Joomla viejo / Cloudflare
**Estado funcional real:** resuelto lado Cloudflare, aunque el issue sigue abierto.
Verificado en el issue vivo:
- el body ya documenta la parte técnica previa (cache Joomla activada y diagnóstico del fatal)
- Inma añadió comentario 14-jul: **"HECHO y verificado"** ajustando una regla existente de Cloudflare por el límite de 5 custom rules del plan free
Conclusión:
- Operativamente está **resuelto**.
- Si no queda ningún fleco de observabilidad, este issue también está para **cerrar**.
---
### `#170` — Crawlers sociales / preview Facebook
**Estado funcional real:** resuelto lado Cloudflare, aunque el issue sigue abierto.
Verificado en el issue vivo:
- Inma comentó 14-jul que ya existía una regla **"Permitir Facebook"** y que el crawler de Facebook ya pasa con **200**
Conclusión:
- Operativamente está **resuelto**.
- Igual que `#168`, parece issue de **cierre administrativo**, no técnico.
---
### `#174` — Cierre autónomo de la carta (publicar + rotar)
**Estado funcional real:** abierto, esperando OK de Rafa. Aquí sí hay trabajo útil preparado, pero no conviene aplicar nada aún.
Lo importante que he verificado localmente:
#### 1) Ya existe lógica de rotación en scripts
En el repo local existe:
- `scripts/rotate_cartas.php`
- `scripts/demote_old_cartasemana.php`
`rotate_cartas.php` implementa esta cascada:
1. la que estaba en "semana pasada" → queda solo en "otras semanas"
2. la que estaba en "semana actual" → pasa a "semana pasada"
3. la nueva → pasa a "semana actual"
#### 2) Pero la implementación actual rota **todos los idiomas**, no solo ES
Esto es importante porque en `#174` justo se pregunta si:
- **solo se rota ES al cerrar la carta**, y
- las traducciones **no se rotan** hasta que estén publicadas
El script actual **no sigue ese criterio**: deriva y actúa sobre `es/en/fr/it/pt`.
#### 3) Dry-run local ejecutado
He ejecutado `rotate_cartas.php` en el WordPress local Docker en modo dry-run.
Resultado:
- el script **funciona** como dry-run
- pero el espejo local **no está alineado con el caso exacto de la carta 735 en ES** (el mirror local va por otro estado / otra numeración viva en esa parte)
- por tanto, **sirve para validar la mecánica del script**, pero **no para afirmar que ya resuelva `#174` tal cual**
Conclusión técnica:
- `#174` **no es greenfield**: ya hay base.
- Pero **hay que adaptar** la lógica si la decisión final es "rotar solo ES y dejar derivados/traducciones para después".
- No he preparado parche porque todavía falta el **OK funcional de Rafa**, y sin eso sería fácil codificar la lógica equivocada.
Recomendación:
- cuando Rafa confirme la regla exacta, el siguiente paso bueno es preparar un **wrapper ES-only + dry-run** en local, en vez de reaprovechar `rotate_cartas.php` tal cual.
---
### `#175` — Endpoint `subir-avatar`
**Estado funcional real:** abierto, esperando OK de Rafa. También tiene muy buena base técnica ya hecha.
Lo verificado localmente:
#### 1) El frontend ya usa `foto_perfil`
En `wordpress/wp-content/mu-plugins/fea-homepage.php`:
- el avatar del autor usa el meta de usuario **`foto_perfil`**
- ese meta guarda un attachment ID
#### 2) Ya existen las piezas de procesamiento
En el repo hay:
- `scripts/face_crop_avatar.py`
- `scripts/regen_avatars.php`
O sea: el flujo de recorte / regeneración **ya existe**; faltaría encapsularlo detrás de un endpoint seguro.
#### 3) El caso concreto mencionado en el issue existe y está sin foto en local
Verificado en el WP local:
- usuario `1112` existe
- login: `silvia-mtnz`
- display: `Silvia Martínez Cano`
- `foto_perfil` está vacío
Conclusión técnica:
- `#175` tampoco es greenfield.
- El endpoint encaja bien como hermano de `fea/v1/crear-autor` (`#166`).
- La decisión pendiente no es "si se puede", sino **qué contrato exacto quiere Rafa**:
- `user_id` vs `slug`
- multipart vs base64
- si el endpoint recorta o exige imagen ya preparada
Recomendación:
- en cuanto Rafa dé el OK de contrato, el trabajo lógico es clonar el patrón de `fea-crear-autor-api.php` y dejar un endpoint **mínimo, acotado e idempotente**.
## Resumen ejecutivo
- `#166`: **hecho**
- `#168`: **hecho** ✅ pero sigue abierto
- `#170`: **hecho** ✅ pero sigue abierto
- `#174`: **esperando OK funcional de Rafa**; hay base técnica, pero la actual rota todos los idiomas
- `#175`: **esperando OK funcional de Rafa**; hay base técnica muy clara y el caso Silvia está identificado
## Qué hice en esta pasada
- Reapunté la revisión a `gitea.feadulta.com`
- Leí los issues correctos por API pública
- Verifiqué localmente la existencia del endpoint `crear-autor`
- Ejecuté un **dry-run** de la lógica de rotación existente
- Verifiqué en local el caso `Silvia Martínez Cano / 1112 / foto_perfil vacío`
## Qué NO hice
- No toqué producción
- No cerré issues
- No comenté en Gitea
- No modifiqué código del repo