TTS backlog: tamano de tanda adaptativo y reparto de la cuota semanal (#188)
Un tamano fijo de tanda no maximiza: deja la ventana de 5h a medias cuando esta libre y no cabe cuando esta medio usada. Ahora cada corrida mide la cuota y calcula cuantos audios caben. Coste medido (2 tandas de 10 el 2-ago): 4,4 puntos de la ventana de 5h y 0,4 de la semanal por audio. Con la ventana limpia salen 20, no 10. Pero la semanal NO se gasta a tope en cada ventana: se REPARTE entre las que quedan hasta su reset. Llenar cada ventana de 5h al 90% son ~8 puntos de semanal, y hay 20 ventanas activas por semana = 160 puntos para un presupuesto de 85. Sin reparto, domingo y lunes se lo comen y el fin de semana se queda a cero (simulado: se seca en la ventana 12 de 20). Con reparto salen ~10 por ventana, 204 audios/semana, y la semanal acaba justo en el 85% al llegar el reset. En la ultima ventana el reparto vale todo lo que sobre, asi que tampoco queda cuota sin usar: eso sustituye al apano anterior de "apurar en las ultimas 12h", que se retira. El gate binario tambien desaparece: si otro esta usando MiniMax, en vez de saltarse la ventana entera se hace una tanda pequena con lo que quede. Validado en vivo: "reset semanal en 166h, 20 ventanas por delante · Caben: 10 por la de 5h, 10 por el reparto semanal". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+78
-27
@@ -2,9 +2,10 @@
|
|||||||
# Cron del backlog de TTS por autor (issue rafa/feadulta#188).
|
# 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
|
# Corre cada 5 h los lunes, viernes, sábados y domingos, aprovechando la cuota
|
||||||
# ociosa de MiniMax para locutar artículos antiguos. Por ventana:
|
# 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
|
# 1) Mide la cuota y CALCULA el tamaño de la tanda para llenar la ventana hasta
|
||||||
# está usando alguien (la carta semanal, el backfill de summaraise...), se
|
# el objetivo. Un tamaño fijo desaprovecha: deja la ventana a medias cuando
|
||||||
# salta. El trabajo de fondo nunca le come la cuota al trabajo con dueño.
|
# está libre, y no cabe cuando está medio usada. Al dimensionar por hueco
|
||||||
|
# libre, además, deja de importar dónde caiga el cron respecto a la ventana.
|
||||||
# 2) tts_produce.py --autor ... --max N: la cola sale de la BD, así que esto es
|
# 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.
|
# 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
|
# SOLO LOCAL: no toca producción. Publicar en prod es sync_audio_to_prod.py, que
|
||||||
@@ -27,16 +28,29 @@ LOCK="$WORK/cron.lock"
|
|||||||
AUTOR="${FEA_TTS_AUTOR:-382}"
|
AUTOR="${FEA_TTS_AUTOR:-382}"
|
||||||
DESDE="${FEA_TTS_DESDE:-2025}"
|
DESDE="${FEA_TTS_DESDE:-2025}"
|
||||||
HASTA="${FEA_TTS_HASTA:-2026}"
|
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
|
# Coste medido de un audio, en DÉCIMAS de punto porcentual (aritmética entera en
|
||||||
# "¿cabe esta tanda en lo que queda de ventana?": medido, cada audio gasta ~3-4 %
|
# bash). Dos tandas de 10 el 2026-08-02: la ventana de 5 h fue 0→45→88 (~4,4 pts
|
||||||
# de la ventana de 5 h, así que un batch de 10 pide ~40 %. Gate en 55 % deja sitio
|
# por audio) y la semanal 10→14→18 (~0,4 pts). Artículos de Fray Marcos de
|
||||||
# y frena solo cuando la ventana ya la está usando otro (la carta semanal son ~28
|
# 4.000-5.000 caracteres; si se locuta a otro autor con textos mucho más largos,
|
||||||
# audios de golpe) o cuando la está llenando este mismo cron. Si se sube BATCH,
|
# revisar estos números con un par de tandas.
|
||||||
# subir también este número o se saltarán ventanas.
|
COSTE_5H="${FEA_TTS_COSTE_5H:-44}"
|
||||||
MAX_SEMANA="${FEA_TTS_MAX_SEMANA:-85}"
|
COSTE_SEM="${FEA_TTS_COSTE_SEM:-4}"
|
||||||
MAX_5H="${FEA_TTS_MAX_5H:-55}"
|
|
||||||
|
# Objetivos de llenado (%). Dejar cuota sin usar al llegar el reset es tirarla.
|
||||||
|
OBJ_5H="${FEA_TTS_OBJ_5H:-90}"
|
||||||
|
OBJ_SEM="${FEA_TTS_OBJ_SEM:-85}"
|
||||||
|
|
||||||
|
# La semanal NO se gasta a tope en cada ventana: se REPARTE entre las ventanas
|
||||||
|
# que quedan hasta su reset. Llenar cada ventana de 5 h al 90 % son ~8 puntos de
|
||||||
|
# semanal, y hay 20 ventanas activas por semana: 160 puntos para un presupuesto
|
||||||
|
# de 85. Sin reparto, domingo y lunes se lo comen y el fin de semana se queda a
|
||||||
|
# cero. Con reparto sale ~10 audios por ventana, y en la última ventana de la
|
||||||
|
# semana el reparto vale todo lo que sobre, así que tampoco queda cuota sin usar.
|
||||||
|
|
||||||
|
# Tope de seguridad por tanda y override manual (FEA_TTS_BATCH fija el tamaño y
|
||||||
|
# se salta el cálculo).
|
||||||
|
MAX_BATCH="${FEA_TTS_MAX_BATCH:-25}"
|
||||||
|
|
||||||
mkdir -p "$WORK"
|
mkdir -p "$WORK"
|
||||||
cd "$REPO" || exit 1
|
cd "$REPO" || exit 1
|
||||||
@@ -47,39 +61,76 @@ if ! flock -n 9; then
|
|||||||
echo "[$(ts)] otra corrida en curso, salto." >> "$LOG"; exit 0
|
echo "[$(ts)] otra corrida en curso, salto." >> "$LOG"; exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[$(ts)] === cron TTS backlog start (autor=$AUTOR $DESDE-$HASTA batch=$BATCH) ===" >> "$LOG"
|
echo "[$(ts)] === cron TTS backlog start (autor=$AUTOR $DESDE-$HASTA) ===" >> "$LOG"
|
||||||
|
|
||||||
# 1) Gate de cuota MiniMax (ventana de 5 h y semanal)
|
# 1) Medir la cuota, y contar cuántas ventanas del cron quedan hasta que se
|
||||||
read -r PCT5 PCTW <<< "$(python3 "$QUOTA" --json --no-local 2>/dev/null | python3 -c '
|
# reinicie la semanal — es el denominador del reparto.
|
||||||
|
read -r PCT5 PCTW HSEM VENTANAS <<< "$(python3 "$QUOTA" --json --no-local 2>/dev/null | python3 -c '
|
||||||
import json, sys
|
import json, sys
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
# DEBEN COINCIDIR CON EL CRONTAB: 0 */5 * * 1,5,6,0
|
||||||
|
HORAS = {0, 5, 10, 15, 20}
|
||||||
|
DIAS = {0, 4, 5, 6} # lun, vie, sab, dom en datetime.weekday()
|
||||||
|
|
||||||
def pct(v):
|
def pct(v):
|
||||||
# OJO: 0.0 es un valor legítimo (ventana entera libre) y es falsy en Python.
|
# OJO: 0.0 es un valor legítimo (ventana entera libre) y es falsy en Python.
|
||||||
# Un `v or 100` aquí aborta la tanda justo cuando hay toda la cuota disponible.
|
# Un `v or 100` aquí aborta la tanda justo cuando hay toda la cuota disponible.
|
||||||
return int(v) if v is not None else 100
|
return int(v) if v is not None else 100
|
||||||
|
|
||||||
try:
|
try:
|
||||||
d = json.load(sys.stdin)
|
d = json.load(sys.stdin)
|
||||||
m = next(p for p in d["providers"] if p["provider"] == "minimax" and p.get("ok"))
|
m = next(p for p in d["providers"] if p["provider"] == "minimax" and p.get("ok"))
|
||||||
print(pct(m.get("five_h_pct")), pct(m.get("week_pct")))
|
horas, ventanas = 999, 1
|
||||||
|
try:
|
||||||
|
fin = datetime.fromisoformat(m["week_reset"]).astimezone()
|
||||||
|
ahora = datetime.now().astimezone()
|
||||||
|
horas = max(int((fin - ahora).total_seconds() // 3600), 0)
|
||||||
|
# Esta corrida cuenta como una; se suman las que quedan programadas.
|
||||||
|
t = (ahora + timedelta(hours=1)).replace(minute=0, second=0, microsecond=0)
|
||||||
|
while t < fin:
|
||||||
|
if t.hour in HORAS and t.weekday() in DIAS:
|
||||||
|
ventanas += 1
|
||||||
|
t += timedelta(hours=1)
|
||||||
except Exception:
|
except Exception:
|
||||||
print(100, 100) # sin lectura fiable de cuota, no se gasta
|
pass
|
||||||
' 2>/dev/null || echo "100 100")"
|
print(pct(m.get("five_h_pct")), pct(m.get("week_pct")), horas, ventanas)
|
||||||
echo "[$(ts)] MiniMax 5h=${PCT5}% semana=${PCTW}%" >> "$LOG"
|
except Exception:
|
||||||
|
print(100, 100, 999, 1) # sin lectura fiable de cuota, no se gasta
|
||||||
|
' 2>/dev/null || echo "100 100 999 1")"
|
||||||
|
[ "${VENTANAS:-0}" -lt 1 ] && VENTANAS=1
|
||||||
|
|
||||||
if [ "${PCTW:-100}" -ge "$MAX_SEMANA" ]; then
|
# 2) Dimensionar la tanda. Dos límites, manda el más restrictivo:
|
||||||
echo "[$(ts)] ABORT: cuota semanal ${PCTW}% >= ${MAX_SEMANA}%, salto esta ventana." >> "$LOG"
|
# - la ventana de 5 h: se llena hasta OBJ_5H aquí y ahora;
|
||||||
exit 0
|
# - la semanal: solo la parte que le toca a esta ventana de lo que queda.
|
||||||
fi
|
CABE_5H=$(( ((OBJ_5H - PCT5) * 10) / COSTE_5H ))
|
||||||
if [ "${PCT5:-100}" -ge "$MAX_5H" ]; then
|
CABE_SEM=$(( (((OBJ_SEM - PCTW) * 10) / VENTANAS) / COSTE_SEM ))
|
||||||
echo "[$(ts)] ABORT: ventana de 5h al ${PCT5}% >= ${MAX_5H}%, no cabe la tanda; salto." >> "$LOG"
|
[ "$CABE_5H" -lt 0 ] && CABE_5H=0
|
||||||
|
[ "$CABE_SEM" -lt 0 ] && CABE_SEM=0
|
||||||
|
|
||||||
|
BATCH=$CABE_5H
|
||||||
|
[ "$CABE_SEM" -lt "$BATCH" ] && BATCH=$CABE_SEM
|
||||||
|
[ "$BATCH" -gt "$MAX_BATCH" ] && BATCH=$MAX_BATCH
|
||||||
|
# Override manual: fija el tamaño y se salta todo el cálculo.
|
||||||
|
[ -n "${FEA_TTS_BATCH:-}" ] && BATCH="$FEA_TTS_BATCH"
|
||||||
|
|
||||||
|
echo "[$(ts)] MiniMax 5h=${PCT5}% semana=${PCTW}% · reset semanal en ${HSEM}h, ${VENTANAS} ventanas por delante" >> "$LOG"
|
||||||
|
echo "[$(ts)] Caben: ${CABE_5H} por la de 5h, ${CABE_SEM} por el reparto semanal → tanda de ${BATCH}" >> "$LOG"
|
||||||
|
|
||||||
|
if [ "$BATCH" -lt 1 ]; then
|
||||||
|
echo "[$(ts)] ABORT: no cabe ni un audio sin pasarse del objetivo; salto esta ventana." >> "$LOG"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2) Tanda. tts_produce.py ya para solo ante rc 2056/1039 (cuota/rate limit).
|
# 3) Tanda. tts_produce.py ya para solo ante rc 2056/1039 (cuota/rate limit).
|
||||||
|
# Una tanda larga puede desbordar el reset de 5 h (~2,6 min por audio): no pasa
|
||||||
|
# nada, lo que sobra lo absorbe la ventana siguiente y la próxima corrida la
|
||||||
|
# mide y se redimensiona sola. Cortar por tiempo dejaría cuota sin gastar.
|
||||||
echo "[$(ts)] tts_produce --autor $AUTOR --desde $DESDE --hasta $HASTA --max $BATCH ..." >> "$LOG"
|
echo "[$(ts)] tts_produce --autor $AUTOR --desde $DESDE --hasta $HASTA --max $BATCH ..." >> "$LOG"
|
||||||
"$PY" scripts/tts_produce.py --autor "$AUTOR" --desde "$DESDE" --hasta "$HASTA" \
|
"$PY" scripts/tts_produce.py --autor "$AUTOR" --desde "$DESDE" --hasta "$HASTA" \
|
||||||
--max "$BATCH" >> "$LOG" 2>&1
|
--max "$BATCH" >> "$LOG" 2>&1
|
||||||
|
|
||||||
# 3) Cuánto queda tras la tanda (recuento fresco de la BD, barato y sin cuota).
|
# 4) 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" \
|
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)"
|
--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"
|
echo "[$(ts)] === cron TTS backlog done. Pendientes autor $AUTOR $DESDE-$HASTA: ${QUEDAN:-?} ===" >> "$LOG"
|
||||||
|
|||||||
Reference in New Issue
Block a user