TTS masivo del backlog histórico por autor (Fray Marcos primero): cron cada 5 h con gate de cuota MiniMax + reporte diario #188
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Aprovechar la cuota ociosa de MiniMax para locutar el backlog histórico de artículos con voz clonada, empezando por Fray Marcos (lo que falta de 2026 + todo 2025) y siguiendo por el resto de años y por los otros autores ya clonados (Pagola, Sicre, Arregi — ver #152).
Hoy la cuota está prácticamente sin usar (MiniMax 5h 0 %, semanal 7 %), así que es capacidad que se pierde cada semana.
1. Inventario medido (WP local, 2026-08-01)
Solo posts ES (
term_taxonomy_id1404 de polylang) en estadopublish. Losdraftytrashquedan fuera a propósito.Lote 1 pedido por Rafa = Fray Marcos 2026 + 2025 = 86 artículos.
Rango de Fray Marcos: 2008–2026, ~60 artículos/año salvo 2013 (58) y 2009/2008 (9 y 11). Solo 1 artículo en todo el inventario baja de 400 caracteres (Fray Marcos 2021) → prácticamente nada se va a descartar por «sin contenido».
2. Diseño
Se reutiliza el pipeline que ya existe (
tts_produce.py+minimax_tts.py+fea_post_io.php, #152/#163). Lo único que falta es de dónde sale la cola: hoy se construye a partir de una lista de cartas (FEA_TTS_CARTAS) o de un CSV de IDs a mano, y eso no sirve para un backlog de 2.481.2.1 La cola se deriva de la BD, no de un fichero de estado
Nueva acción en
scripts/fea_post_io.php:que devuelve los IDs de posts que cumplen todo:
post_author = <author_id>,post_type = post,post_status = publishwp_term_relationships, tt_id 1404 — no hardcodear: resolver por slug)CHAR_LENGTH(post_content) >= 400fea_audio_done = 1y sin metafea_audio_skip = 1ordenados por
post_date DESC(lo más reciente primero, que es lo que más se lee).Esa consulta es la idempotencia. No hay fichero de estado que se pueda desincronizar: si un post ya tiene audio, sencillamente no vuelve a salir en la cola. Relanzar el script, solaparlo o reiniciar la máquina no duplica nada ni gasta cuota de más.
2.2
tts_produce.pygana 4 flags--autor/--desde/--hasta→ construyen la cola conlistpendingen vez de con cartas.--max N→ para después de N audios OK en esta ejecución (acota la ventana de 5 h).El resto ya está y no se toca: voz por autor vía
AUTHOR_VOICES(#152), pausas dinámicas,setaudioconfea_audio_voice, freno anterc2056/1039 (cuota/rate limit) yINTERVAL=180 sentre audios.2.3 Cron de ventana:
scripts/tts_backlog_cron.shMismo patrón que el backfill de ytsummaries (
scripts/backfill_cron.sh), que ya está probado:flock -n→ si una ventana se alarga, la siguiente se salta en vez de solaparse.~/ytsummaries/scripts/quota.py --json --no-local:tts_produce.py --autor $AUTOR --desde $DESDE --hasta $HASTA --max $BATCH/tmp/fea-tts-backlog/cron-YYYY-MM-DD.log.Cola configurable por variables (
FEA_TTS_AUTOR,FEA_TTS_DESDE,FEA_TTS_HASTA,FEA_TTS_BATCH) para poder pasar de Fray Marcos a Pagola cambiando una línea, sin tocar código.Crontab:
Cada 5 h lunes, viernes, sábado y domingo (fuera martes, miércoles y jueves, como pidió Rafa). Son 5 disparos/día × 4 días = 20 ventanas/semana.
2.4 Dimensionar el lote (
BATCH)Dato real del 2026-07-08: 22 audios llevaron la ventana de 5 h del 0 % al 89 %. Con eso,
BATCH=15deja ~35 % de margen para que un TTS de la carta semanal quepa encima sin chocar. A 15/ventana × 20 ventanas = 300/semana teóricos, pero el techo real lo pondrá la cuota semanal, no las ventanas — por eso el gate semanal es el que manda.Con ese ritmo: el lote 1 (86) cae en el primer fin de semana; los 2.481 de los 4 autores, del orden de 2–3 meses de fondo. Empezar con
BATCH=10la primera semana y subirlo cuando se vea el consumo semanal real.3. Reporte diario
Job de Hermes en modo
no-agent(stdout se entrega directo a Rafa), igual quefeadulta-feedback-daily:~/.hermes/scripts/fea_tts_backlog_report.py— solo lectura, no genera nada:post_meta+mtimedeuploads/tts/), desglosados por autor y voz;listpending, contando);fea_audio_errornuevos, si los hay.4. ⚠️ Interacción con el cutover a Hetzner (#180, lunes 03-ago)
wordpress/wp-content/uploads/tts/. No toca producción. El cron puede arrancar ya.sync_audio_to_prod.pyapunta a CDMON (134.0.10.170) y lleva el workaround dessh 'cat > ruta'por la glibc rota de ese servidor (#163). Después del cutover el destino es Hetzner, donde ese workaround sobra. No lanzar el sync antes del 03-ago — el delta deuploadsdel lunes está medido en 0 ficheros y meter cientos de mp3 nuevos ahí dentro invalida esa medición y alarga la ventana.sync_audio_to_prod.pyesté repuntado a Hetzner y revalidado. Los audios se van acumulando en local mientras tanto, sin prisa.5. Fases
listpending+ flags detts_produce.py+tts_backlog_cron.sh+ entrada de crontab. Validar la primera ventana a mano antes de dejarla sola.fea_tts_backlog_report.py+ job de Hermes).sync_audio_to_prod.pya Hetzner y revalidar.FEA_TTS_AUTOR.6. Decisiones abiertas
draftde 2026 entran? Ahora mismo no. Son 4 por autor; si son cartas en preparación, ya se locutan por el flujo semanal.Medición y diseño: sesión Claude Code 2026-08-01. El inventario del §1 sale de una consulta de solo lectura contra el WP local.
F1 implementada y corriendo (2026-08-01)
Rama
feat/tts-backlog-autor, commit7c5330a, sin mergear. Tres ficheros:scripts/fea_post_io.php→ acciónlistpending <autor> <desde> <hasta> <limite> [voz].scripts/tts_produce.py→ flags--autor/--desde/--hasta/--max/--dry-run. Sin flags, el modo cartas de la carta semanal es idéntico al de antes (verificado: la cola de--cartas 45018sigue dando los mismos 15 IDs).scripts/tts_backlog_cron.sh→ wrapper conflock+ gate de cuota.Entrada de crontab instalada:
De paso entran al historial los cambios de
tts_produce.pyque estaban sin commitear desde el 24-jul (argparse--ids/--cartasyQUOTA_OR_RATE_ERRORSpara no reintentar ante rc 2056/1039).Validado end-to-end
php -l·py_compile·bash -nlistpending 382 2025 2026post_date DESCreal: #44204 (2026-05-21) → #44184 (05-14) → #44164 (05-07)FrayMarcosFeadulta2026, mp3 enuploads/tts/, metasfea_audio_url/voice/donecorrectastts_backlog_cron.sh5h=9% semana=8%, generó #44164, log dejóPendientes: 83Quedan 83 de los 86 del lote 1.
Corrección al §6.3: no hay nada que regenerar
Escribí que había 33 audios viejos locutados con Nico antes de tener las voces clonadas. Es falso. Los 37 audios existentes de los 4 autores tienen todos ya su voz clonada correcta, y los mp3 están fechados el 9-jul por la mañana — o sea que se regeneraron enteros al día siguiente de #152, no solo el artículo de la carta 734 por autor como decía mi nota.
listpendingmantiene igualmente la comprobación de voz (sale en la cola lo locutado con una voz que no es la del autor). Hoy devuelve 0 por ese motivo; queda como red de seguridad si en el futuro se clona una voz nueva o se cambia una existente.Ajuste sobre el diseño del §2.4
El gate de la ventana de 5 h sube de 25 % a 55 %. Medido en las pruebas de hoy: 3 audios = 9 % de la ventana, o sea ~3 % por audio, coherente con el dato del 8-jul (22 audios = 89 %). Con
BATCH=10una tanda pide ~35 %, así que un gate al 25 % se bloquearía a sí mismo casi siempre. A 55 % cabe la tanda y sigue frenando cuando la ventana la está usando otro — la carta semanal son ~28 audios de golpe. Si se subeBATCH, hay que subir tambiénFEA_TTS_MAX_5H.BATCHarranca en 10 (no 15) hasta ver una semana de consumo real.Ritmo real medido
La síntesis de un artículo de ~4.000 caracteres tarda ~10 s; el grueso del tiempo es el
INTERVAL=180 sentre audios. Una tanda de 10 son ~30 min, muy holgada dentro de la ventana de 5 h.Limitación conocida
El cron es del crontab de la WSL: si Windows está apagado o la WSL no ha arrancado, la ventana no se dispara. No se pierde nada (la cola se recalcula sola), simplemente el backlog avanza más despacio.
Siguiente
F2, el reporte diario. Sin empezar.
F2 implementada — y dos fallos del F1 cazados en la primera ventana real (2026-08-02)
Rama
feat/tts-backlog-autorsubida a gitea (c5dcbdb), sin mergear. Tres commits.🔴 Las 4 ventanas del 2-ago no corrieron: el script se quedó sin bit
+xtts_backlog_cron.shacabó en-rw-r--r--. Lo dejé ejecutable al probarlo, pero dos ediciones posteriores del fichero reescribieron el modo y se llevaron el+xpor delante. Cron lo invocaba directamente → permission denied → ni log, ni aviso (no hay MTA en la WSL, así que el correo de cron se pierde). Confirmado en/var/log/syslog: las 4 ventanas de hoy (00:00, 05:00, 10:00, 15:00) sí se dispararon, y ninguna dejó rastro en/tmp/fea-tts-backlog/.Arreglado por partida triple, porque un fallo mudo no puede depender de acordarse de un
chmod:chmod +xy modo 100755 en el índice de git (git update-index --chmod=+x), que en el primer commit había entrado como 100644.**/bin/bash** /home/rafa/.../tts_backlog_cron.sh→ deja de depender del modo del fichero.🔴 El gate de cuota abortaba justo cuando había toda la cuota libre
Al relanzar una ventana de recuperación:
Pero la API decía
five_h_pct: 0.0. El parser hacíaint(m.get("five_h_pct") or 100)y0.0es falsy en Python, así que la ventana entera libre se leía como ventana entera llena. El caso peor posible: el gate bloqueaba precisamente las ventanas más aprovechables. Ahora distingue0.0deNonecon una comprobación explícita, y el comentario del código lo deja escrito para que no vuelva.Verificado tras el arreglo — misma ventana, lectura correcta y tanda en marcha:
Lección para el §2.3: un gate de cuota que falla en cerrado es invisible. Aborta, escribe una línea de log que parece normal y nadie se entera. Los dos fallos de hoy tenían el mismo síntoma —no pasa nada— y por eso el aviso de "ninguna ventana dejó rastro" del reporte es parte del arreglo, no un adorno.
F2 — Reporte diario
scripts/fea_tts_backlog_report.py, solo lectura: no genera audio ni escribe en la BD.Vive en el repo y
~/.hermes/scripts/lo ve por symlink, no por copia — el problema de la skill del webmaster duplicada y desincronizada no se repite.Job
9bcb2f913cbd, primera entrega el 3-ago a las 07:30. Salida real de hoy:Los audios se atribuyen a su autor por
fea_audio_voice(cada autor clonado tiene la suya), y el recuento de las 24 h sale del mtime de los mp3, no de las metas: es lo que se acaba de escribir de verdad, sin que lo enturbie una sincronización antigua.Estado
Cierre de sesión 2026-08-02: reparto semanal + respuesta al §6.1 (ordenar por tráfico)
Rama
feat/tts-backlog-autoral día en gitea, 4 commits, sin mergear (e1a14ec).El tamaño de tanda fijo era un error, y el reparto semanal también hacía falta
Rafa señaló dos cosas seguidas, las dos correctas.
1. Con la ventana de 5 h libre caben 20, no 10. Coste medido con dos tandas reales: 4,4 puntos de la ventana de 5 h y 0,4 de la semanal por audio. El
BATCH=10escrito a mano dejaba media ventana sin usar. Ahora cada corrida mide y calcula.2. Pero maximizar cada ventana de 5 h agota la semana antes del fin de semana. 20 audios por ventana × 20 ventanas activas = 160 puntos de semanal para un presupuesto de 85. Simulado, el tope duro del 85 % se seca en la ventana 12 de 20 — o sea el viernes, dejando sábado y domingo a cero.
La solución no es un tope, es repartir: cada ventana gasta la parte proporcional de lo que quede de semana, dividido entre las ventanas que faltan hasta el reset.
El total es idéntico porque la cuota semanal es el único cuello de botella real: caben 400 audios/semana por las ventanas de 5 h y solo ~205 por la semanal. Sobra capacidad de 5 h por todas partes.
Consecuencias que conviene no olvidar:
OBJ_SEM: 85 % → ~205 audios/semana, 95 % → ~230. El colchón que deja el 85 % es justo para el TTS de una carta (~28 audios ≈ 11 puntos).Validado en vivo:
reset semanal en 166h, 20 ventanas por delante · Caben: 10 por la de 5h, 10 por el reparto semanal.§6.1 RESUELTO a medias: ordenar por tráfico gana, pero menos de lo esperado
Cruzado GA4 (14-oct-2025 → 02-ago-2026) con la BD, vía
pagePath→ id de K2 → meta_fgj2wp_old_k2_id.El archivo antiguo se consume mucho: 1,02 M de páginas vistas en URLs de artículo (49 % del tráfico del portal), 14.217 artículos antiguos distintos con visitas. Y desde que el archivo vive aparte en
antiguo.feadulta.com, tiene más sesiones que la web viva (20.337 vs 12.757 en 28 días).Filtrar por "vigente" no sirve: de los ~2.458 pendientes de los cuatro autores clonados, 2.311 tienen tráfico medible. No hay un subconjunto muerto que descartar; la palanca es el orden, no la selección.
Ordenar por tráfico vs por fecha, con los primeros 200 audios:
Mejora real de 16 puntos, no un cambio de juego: la fecha ya es buen proxy porque lo reciente es lo más leído. Decisión aplazada — el orden por fecha se queda de momento; cambiarlo es sustituir el
ORDER BYdelistpendingpor una tabla de tráfico precalculada.Tráfico muy repartido: top 50 = 22 % de las vistas, top 250 = 46 %, top 1.000 = 72 %.
⚠️ Gotcha del cruce, por si alguien lo repite: las URLs de listado de K2 tienen la misma forma que las de artículo (
/es/buscadoravanzado/itemlist/user/43-fraymarcos.html). Sin excluir/itemlist/, la página de autor de Fray Marcos (24.117 vistas) se atribuye al artículo con k2 id 43 («LA PASCUA», 33 vistas reales) y encabeza la cola. Hay que filtrarlas.El hallazgo más valioso no era de este issue y se ha llevado a #189: los autores sin voz clonada tienen tanto tráfico como los clonados. Enrique Martínez Lozano, 60.442 vistas y 827 artículos sin locutar, sin voz. Arregi, que sí tiene voz, es el 14º por tráfico.
Estado al cierre
0 */5 * * 1,5,6,0con/bin/bashexplícito. Reporte diario a las 07:30 por Hermes.Fallo del informe diario en su primera ejecución real (2026-08-03 07:30)
Causa. Puse
~/.hermes/scripts/fea_tts_backlog_report.pycomo symlink al script del repo, para no tener dos copias que se desincronicen. Hermes resuelve los symlinks antes de comprobar que el path caiga dentro de su directorio de scripts, así que lo rechaza: para él el script está en/home/rafa/joomla-migration/.Arreglo. Symlink sustituido por un wrapper fino que llama al script del repo por subproceso — el mismo patrón que ya seguían
feadulta_ga4_daily.pyyfeadulta_ga4_weekly.pyen ese directorio, que apuntan a/mnt/c/Users/Chia/feadulta-git/. Sigue habiendo un único sitio donde se edita la lógica (el repo); el wrapper solo reenvía stdout/stderr y el código de salida.Verificado:
hermes cron run 9bcb2f913cbd→Last run: 2026-08-03T07:36 ok, con el informe correcto.Regla para futuros jobs de Hermes que ejecuten código del repo: wrapper, nunca symlink.
Nota documentada en el docstring de
scripts/fea_tts_backlog_report.py(commit enfeat/tts-backlog-autor).De paso, primer dato real del backlog
49 audios en 24 h con 12% de cuota semanal consumida — coherente con los 0,4 puntos/audio medidos en §3. La ventana saltada lo fue por cuota (5h al 100% en ese momento), que es el comportamiento correcto, no un fallo.
8-ago-2026 — el cron llevaba 3 días caído, y ya no depende de la rama
Qué pasó. El 5-ago el checkout principal (
~/joomla-migration) pasó a la ramadocs/rescate-gap-194. Los scripts del backlog solo viven enfeat/tts-backlog-autor,así que
scripts/tts_backlog_cron.shdejó de existir en el working tree y el cron pasóa invocar un fichero inexistente: fallo mudo, sin log, sin aviso. 7 ventanas perdidas
(las 5 del viernes 7 y las 2 de la madrugada del sábado 8). El informe diario de Hermes
estaba roto por lo mismo, y por la misma razón que el symlink de agosto: el script
estaba bien y la instalación mal.
Arreglo (
2fa50a8). El cron ya no depende de qué rama tenga puesta el repo:~/worktrees/fea-tts-backlogclavado afeat/tts-backlog-autor.Checkout directo no era opción: esa rama versiona
wordpress/y habría machacadola instalación local de WordPress.
uploads/ttsytts-samplesson symlinks a los directorios reales del repoprincipal, así que todos los mp3 siguen en un único sitio.
tts_backlog_cron.shdeduce su repo de la ubicación del propio script(
FEA_TTS_REPOsigue disponible como override) en vez de llevarlo a fuego.Fray Marcos 2025-2026: cerrado. Los 4 que quedaban (#16620, #16614, #16613, #16612),
locutados. El lote queda a 0.
Nuevo lote: Pagola (383), todos los años — 389 pendientes. Primera tanda de 12 hecha,
quedan 377.
Recalibrado el coste por audio. Los textos de Pagola son bastante más cortos que los
de Fray Marcos (media 2.516 car — min 2.275, max 2.885 — frente a ~7.800), y el gasto
por audio lo refleja:
Con la calibración vieja el cron creía que cabía la mitad de lo que cabe y dejaba cuota
sin gastar. Los nuevos valores van en el crontab, junto al lote — el script conserva los
de Fray Marcos por defecto, así que la calibración viaja con el autor, no con el código:
(28 y 3 en décimas, un pelo por encima de lo medido para no pasarse del objetivo.)
Pendiente de decidir: al cambiar de autor conviene volver a medir un par de tandas.
Sicre (627) y Arregi (443) siguen sin tocar.
Corrección del informe con ceros falsos (2026-08-28)
El informe diario convertía silenciosamente cualquier error de
docker exec wordpress-web php /tmp/fea_post_io.php listpending …en cadena vacía y la contaba como0 pendientes. Por eso pudo informar 0/0 pese a existir cola: no era un estado del backlog, era una lectura fallida camuflada.Fix publicado: commit
bda4a75(feat/tts-backlog-autor). Ahora una lectura WP fallida entrega explícitamente⚠️ informe TTS inválidoy sale con código 1; nunca comunica cero. También queda ajustado el informe para sumar todas las líneas activas de crontab y señalar Sicre774, rango2000–2026, como autor/lote actual.Verificación real posterior: