#!/usr/bin/env python3 """Orquestador nocturno: locuta cartas ES del gap con MiniMax (voz Nico), una a una, repartido en el tiempo. Reanudable (meta fea_audio_done) y con freno ante la cuota (para tras N fallos seguidos). NO toca el front; solo genera el mp3 y asocia la URL al post (meta fea_audio_url). Lanzar: nohup ~/tts-local/xtts-venv/bin/python scripts/tts_produce.py > /tmp/feadulta-tts-prod.out 2>&1 & Log: /tmp/feadulta-tts-prod.log """ import os import shutil import subprocess import sys import time from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import minimax_tts as mm # get_post_text, add_pauses, t2a, OUT import translate_post as tp # carta_article_ids VOICE = "NicoFeadulta2026" MODEL = "speech-2.8-hd" CONTAINER = "wordpress-web" PROD = Path(__file__).resolve().parent.parent / "wordpress/wp-content/uploads/tts" LOG = Path("/tmp/feadulta-tts-prod.log") INTERVAL = 180 # s entre cartas exitosas (reparte el ritmo) BACKOFF = 1800 # s de espera ante fallo de cuota antes de reintentar MAX_CONSEC_FAIL = 3 # fallos seguidos → parar (cuota probablemente agotada) MIN_CHARS = 200 # por debajo, se considera sin contenido locutable # Cola de cartas a locutar. Override por entorno (FEA_TTS_CARTAS) para priorizar # la carta nueva de la semana; si no, cae al orden del gap histórico. _DEFAULT_CARTAS = "45018 44997 44975 44230 44229 44228 44090 44089 44088 44087 44086 44085 44084 44083 42590" CARTAS = os.environ.get("FEA_TTS_CARTAS", _DEFAULT_CARTAS).replace(",", " ").split() def log(msg): line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}" print(line, flush=True) with LOG.open("a") as f: f.write(line + "\n") def php(*args): return subprocess.run(["docker", "exec", CONTAINER, "php", "/tmp/fea_post_io.php", *args], capture_output=True, text=True) def meta(pid, key): return php("getmeta", str(pid), key).stdout.strip() def build_queue(): # Cola literal de IDs (ya filtrada/ordenada) para priorizar la carta nueva. ids_override = os.environ.get("FEA_TTS_IDS", "").replace(",", " ").split() if ids_override: return [int(x) for x in ids_override if x.strip().isdigit()] q = [] for c in CARTAS: cid = int(c) for pid in tp.carta_article_ids(cid): if pid not in q: q.append(pid) return q def main(): PROD.mkdir(parents=True, exist_ok=True) subprocess.run(["docker", "cp", "scripts/fea_post_io.php", f"{CONTAINER}:/tmp/fea_post_io.php"], capture_output=True) queue = build_queue() log(f"=== INICIO orquestador TTS. Cola: {len(queue)} posts ES del gap ===") i = consec = ok = 0 while i < len(queue): pid = queue[i] if meta(pid, "fea_audio_done") == "1" or meta(pid, "fea_audio_skip") == "1": i += 1 continue try: title, text = mm.get_post_text(pid) except Exception as e: # noqa: BLE001 log(f"#{pid}: error leyendo ({e}); skip") php("setflag", str(pid), "fea_audio_skip", "1") i += 1 continue if len(text) < MIN_CHARS: log(f"#{pid}: sin contenido ({len(text)} car); skip") php("setflag", str(pid), "fea_audio_skip", "1") i += 1 continue rc = mm.t2a(mm.add_pauses(text), VOICE, MODEL, f"prod-{pid}") if rc == 0: src = mm.OUT / f"prod-{pid}.mp3" dst = PROD / f"{pid}.mp3" shutil.move(str(src), str(dst)) php("setaudio", str(pid), f"/wp-content/uploads/tts/{pid}.mp3") ok += 1 consec = 0 log(f"#{pid} OK «{title[:45]}» → tts/{pid}.mp3 (total {ok})") i += 1 time.sleep(INTERVAL) else: consec += 1 log(f"#{pid} FALLO rc={rc} (fallo seguido {consec}/{MAX_CONSEC_FAIL})") php("setflag", str(pid), "fea_audio_error", str(rc)) if consec >= MAX_CONSEC_FAIL: log("Demasiados fallos seguidos → cuota agotada probablemente. PARO. " "Reanudable: relanzar el script más tarde (salta lo ya hecho).") break time.sleep(BACKOFF) # reintenta el mismo post tras esperar log(f"=== FIN tanda. {ok} audios generados esta ejecución. ===") if __name__ == "__main__": main()