TTS: cola de backlog por autor con gate de cuota MiniMax (#188)

Aprovechar la cuota ociosa de MiniMax para locutar el backlog historico,
empezando por Fray Marcos (2026 pendientes + 2025 = 86 articulos).

- fea_post_io.php: accion `listpending <autor> <desde> <hasta> <limite> [voz]`.
  Devuelve los posts ES publicados de un autor que no tienen audio, mas
  recientes primero. Si se pasa la voz clonada del autor, tambien salen los
  locutados con otra voz, para rehacerlos.
  Esa consulta ES la idempotencia: no hay fichero de estado que mantener, lo ya
  locutado deja de salir solo. Relanzar o solapar no duplica ni gasta cuota.

- tts_produce.py: flags --autor/--desde/--hasta/--max/--dry-run. Sin flags, el
  modo cartas de la carta semanal no cambia (retrocompatible).
  --max acota la tanda a la ventana de 5h.

- tts_backlog_cron.sh: wrapper de cron con flock y gate de cuota MiniMax
  (semanal >=85% o ventana de 5h >=55% -> se salta la ventana, para no comerle
  la cuota a la carta semanal). Cola configurable por entorno.
  Crontab: 0 */5 * * 1,5,6,0 (lunes, viernes, sabado y domingo).

SOLO LOCAL: no toca produccion. Publicar el backlog en prod
(sync_audio_to_prod.py) queda bloqueado hasta despues del cutover a
Hetzner (#180).

De paso entran al historial los cambios de tts_produce.py que estaban sin
commitear desde el 24-jul (argparse con --ids/--cartas y QUOTA_OR_RATE_ERRORS
para no reintentar ante rc 2056/1039).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 07:56:16 -04:00
parent 39256c0f9b
commit 7c5330a528
3 changed files with 209 additions and 8 deletions
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Cron del backlog de TTS por autor (issue rafa/feadulta#188).
# Corre cada 5 h los lunes, viernes, sábados y domingos, aprovechando la cuota
# ociosa de MiniMax para locutar artículos antiguos. Por ventana:
# 1) Gate de cuota MiniMax: si la semana va alta, o si la ventana de 5 h ya la
# está usando alguien (la carta semanal, el backfill de summaraise...), se
# salta. El trabajo de fondo nunca le come la cuota al trabajo con dueño.
# 2) tts_produce.py --autor ... --max N: la cola sale de la BD, así que esto es
# idempotente por construcción — lo ya locutado no vuelve a salir.
# SOLO LOCAL: no toca producción. Publicar en prod es sync_audio_to_prod.py, que
# está bloqueado hasta después del cutover a Hetzner (#180).
# flock evita solapes si una ventana se alargara. Log por día.
set -uo pipefail
export PATH="/home/rafa/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
export HOME="/home/rafa"
REPO="/home/rafa/joomla-migration"
PY="/home/rafa/tts-local/xtts-venv/bin/python"
QUOTA="/home/rafa/ytsummaries/scripts/quota.py"
WORK="/tmp/fea-tts-backlog"
LOG="$WORK/cron-$(date +%F).log"
LOCK="$WORK/cron.lock"
# Cola: cambiar aquí (o por entorno) para pasar de un autor a otro.
# 382 Fray Marcos · 383 Pagola · 774 Sicre · 386 Arregi
AUTOR="${FEA_TTS_AUTOR:-382}"
DESDE="${FEA_TTS_DESDE:-2025}"
HASTA="${FEA_TTS_HASTA:-2026}"
BATCH="${FEA_TTS_BATCH:-10}"
# Gates de cuota (%). El semanal es el techo real del backlog. El de 5 h es
# "¿cabe esta tanda en lo que queda de ventana?": medido, cada audio gasta ~3-4 %
# de la ventana de 5 h, así que un batch de 10 pide ~40 %. Gate en 55 % deja sitio
# y frena solo cuando la ventana ya la está usando otro (la carta semanal son ~28
# audios de golpe) o cuando la está llenando este mismo cron. Si se sube BATCH,
# subir también este número o se saltarán ventanas.
MAX_SEMANA="${FEA_TTS_MAX_SEMANA:-85}"
MAX_5H="${FEA_TTS_MAX_5H:-55}"
mkdir -p "$WORK"
cd "$REPO" || exit 1
ts() { date +'%F %T'; }
exec 9>"$LOCK"
if ! flock -n 9; then
echo "[$(ts)] otra corrida en curso, salto." >> "$LOG"; exit 0
fi
echo "[$(ts)] === cron TTS backlog start (autor=$AUTOR $DESDE-$HASTA batch=$BATCH) ===" >> "$LOG"
# 1) Gate de cuota MiniMax (ventana de 5 h y semanal)
read -r PCT5 PCTW <<< "$(python3 "$QUOTA" --json --no-local 2>/dev/null | python3 -c '
import json, sys
try:
d = json.load(sys.stdin)
m = next(p for p in d["providers"] if p["provider"] == "minimax" and p.get("ok"))
print(int(m.get("five_h_pct") or 100), int(m.get("week_pct") or 100))
except Exception:
print(100, 100) # sin lectura fiable de cuota, no se gasta
' 2>/dev/null || echo "100 100")"
echo "[$(ts)] MiniMax 5h=${PCT5}% semana=${PCTW}%" >> "$LOG"
if [ "${PCTW:-100}" -ge "$MAX_SEMANA" ]; then
echo "[$(ts)] ABORT: cuota semanal ${PCTW}% >= ${MAX_SEMANA}%, salto esta ventana." >> "$LOG"
exit 0
fi
if [ "${PCT5:-100}" -ge "$MAX_5H" ]; then
echo "[$(ts)] ABORT: ventana de 5h al ${PCT5}% >= ${MAX_5H}%, no cabe la tanda; salto." >> "$LOG"
exit 0
fi
# 2) Tanda. tts_produce.py ya para solo ante rc 2056/1039 (cuota/rate limit).
echo "[$(ts)] tts_produce --autor $AUTOR --desde $DESDE --hasta $HASTA --max $BATCH ..." >> "$LOG"
"$PY" scripts/tts_produce.py --autor "$AUTOR" --desde "$DESDE" --hasta "$HASTA" \
--max "$BATCH" >> "$LOG" 2>&1
# 3) Cuánto queda tras la tanda (recuento fresco de la BD, barato y sin cuota).
QUEDAN="$("$PY" scripts/tts_produce.py --autor "$AUTOR" --desde "$DESDE" --hasta "$HASTA" \
--dry-run 2>/dev/null | sed -n 's/.*Cola: \([0-9]*\) posts.*/\1/p' | tail -1)"
echo "[$(ts)] === cron TTS backlog done. Pendientes autor $AUTOR $DESDE-$HASTA: ${QUEDAN:-?} ===" >> "$LOG"