From 5714d60c10b644c8e6b408676399dca4972de443 Mon Sep 17 00:00:00 2001
From: rafa
Date: Mon, 31 Aug 2026 07:06:52 -0400
Subject: [PATCH] =?UTF-8?q?docs:=20gu=C3=ADa=20de=20autoservicio=20TTS=20p?=
=?UTF-8?q?ara=20Inma/Mixbot=20(#222)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Documenta el proceso exacto de preparación de texto y pausas para MiniMax
que hasta ahora solo vivía en scripts/minimax_tts.py, para que Inma/Mixbot
puedan generar audio sin pasar por Hermes/PC de Rafa. Señala el hueco de
REST (fea/v1/subir-audio) que sigue bloqueando el paso de publicación.
---
...guia-tts-audio-autoservicio-inma-mixbot.md | 230 ++++++++++++++++++
docs/guia-tts-traduccion-inma.md | 8 +
2 files changed, 238 insertions(+)
create mode 100644 docs/guia-tts-audio-autoservicio-inma-mixbot.md
diff --git a/docs/guia-tts-audio-autoservicio-inma-mixbot.md b/docs/guia-tts-audio-autoservicio-inma-mixbot.md
new file mode 100644
index 0000000..e512da0
--- /dev/null
+++ b/docs/guia-tts-audio-autoservicio-inma-mixbot.md
@@ -0,0 +1,230 @@
+# Guía de generación de audio (TTS) en autoservicio — para Inma / Mixbot
+
+> **Para quién es este documento:** para Inma y Mixbot, a partir del traspaso pedido por
+> Rafa (issue [#222](https://gitea.feadulta.com/rafa/feadulta/issues/222)): dejar de pedirle
+> el audio a Hermes y generarlo y subirlo vosotros mismos, con vuestro propio token de MiniMax.
+>
+> **Estado a 2026-08-31:** el proceso de generación (este documento) está completo y probado.
+> **Subir el resultado al artículo sigue bloqueado** — falta un endpoint REST (`fea/v1/subir-audio`)
+> para enganchar el mp3 al post, tal como pedía el punto 1 del #222. Ese endpoint lo está
+> construyendo Codix (ver aviso al final). Hasta que esté desplegado podéis generar y guardar
+> los mp3, pero no publicarlos en el sitio.
+>
+> Este documento sustituye, para el caso de uso "Inma/Mixbot generan directamente", a la
+> sección 4 de `docs/guia-tts-traduccion-inma.md` (que describe el flujo antiguo vía Hermes
+> ejecutando `scripts/tts_produce.py` en el PC de Rafa). Ese flujo sigue existiendo como
+> alternativa si alguna vez hace falta que Hermes lo dispare, pero deja de ser el camino
+> principal.
+
+---
+
+## 1. Por qué este documento existe
+
+La generación de audio con MiniMax no es "coger el texto del artículo y pedirle un mp3".
+El resultado suena mal (entonación cortada, pausas artificiales o inexistentes, sección de
+cabecera/firma leída en voz alta) si no se prepara el texto antes. Todo lo de abajo salió de
+prueba y error real sobre cartas ya publicadas — no es una recomendación genérica de MiniMax,
+es la receta que ya suena bien en producción. Seguidla tal cual, no la simplifiquéis.
+
+## 2. La cuenta y las voces
+
+- **Endpoint:** `https://api.minimax.io/v1/t2a_v2` (modelo de síntesis).
+- **Modelo:** `speech-2.8-hd` (calidad) o `speech-2.8-turbo` (más barato, para pruebas).
+- **Voces clonadas por autor** (issue #152 — solo estos autores usan su propia voz; el resto
+ cae a la voz por defecto):
+
+ | Autor | WP user_id | `voice_id` |
+ |---|---:|---|
+ | Fray Marcos | 382 | `FrayMarcosFeadulta2026` |
+ | José Antonio Pagola | 383 | `PagolaFeadulta2026` |
+ | José Luis Sicre | 774 | `SicreFeadulta2026` |
+ | José Arregi | 386 | `ArregiFeadulta2026` |
+
+- **Voz por defecto (resto de autores):** confirmar con Rafa cuál es la vigente antes de
+ arrancar — hay más de una en la cuenta (`NicoFeadulta2026` y una más reciente,
+ `nico_feadulta_es_20260802`) y usar la que no toca es un error fácil de cometer sin verlo
+ (issue #189, todavía sin cerrar del todo). **No deis por hecho ninguna de las dos sin
+ preguntar primero.**
+- Un autor nuevo sin voz clonada = voz por defecto. Clonar una voz nueva requiere el
+ procedimiento del #152 (audio limpio 2-5 min, sin música de fondo) — no es autoservicio
+ desde este documento, es un paso aparte.
+
+## 3. Preparar el texto (la parte que importa)
+
+Partiendo del **HTML del artículo en español** (el campo `content` que devuelve
+`GET /wp-json/wp/v2/posts/{id}?context=edit`), en este orden exacto:
+
+### 3.1 Limpiar el HTML a texto plano por párrafos
+
+1. Sustituir `
`, `
`/`
` y ``…`` por un salto de línea.
+2. Quitar el resto de etiquetas HTML.
+3. Quitar shortcodes `[...]` (multimedia, cajas, etc. — no se locutan).
+4. Decodificar entidades HTML (`á` → `á`, etc.).
+5. Separar en párrafos por línea, colapsar espacios múltiples, descartar líneas de 1
+ carácter o menos (restos de saltos vacíos).
+
+### 3.2 Cortar la firma final del autor
+
+Los artículos suelen terminar con una línea corta tipo `José Antonio Pagola` o
+`Fray Marcos` — hay que **conservar esa línea y cortar todo lo que venga después**
+(enlaces, notas, anexos), porque eso sí sobra en el audio.
+
+Cómo detectarla (mirando desde el final del artículo hacia atrás, no desde el principio):
+un párrafo cuenta como firma si tiene entre 2 y 6 palabras, ninguna palabra supera lo
+razonable para un nombre propio, no contiene dígitos ni `:`, `;`, `http`, `www.`, `@`, y
+cada palabra empieza en mayúscula (se permiten minúsculas para conectores como "de", "del",
+"la", "las", "los", "y", "e"). **Importante:** solo cuenta como firma si hay al menos 200
+caracteres de contenido real *antes* de ese párrafo — si no, podéis confundir un encabezado
+inicial corto (p. ej. `"CORPUS (A)"`, `"DOMINGO XI (A)"`) con la firma y cortar el artículo
+entero por error. Buscad la firma desde el final, quedaos con el primer párrafo (contando
+desde atrás) que cumpla la condición, y descartad todo lo posterior.
+
+### 3.3 Expandir abreviaturas bíblicas
+
+MiniMax lee mal `Mt 5, 1-12`. Antes de generar el audio, expandir el nombre del libro
+cuando la abreviatura va seguida de un número (cita bíblica): `Mt` → `Mateo`, `Mc` → `Marcos`,
+`Lc` → `Lucas`, `Jn` → `Juan`, `Rom`/`Rm` → `Romanos`, etc. Hay un listado largo de ~70
+abreviaturas en `scripts/minimax_tts.py::expand_bible_abbreviations` (repo `joomla-migration`)
+— reutilizadlo tal cual en vez de rehacer la lista a mano, es fácil dejarse alguna.
+
+### 3.4 Insertar pausas — el hallazgo central de este documento
+
+MiniMax soporta marcas de pausa literales dentro del texto: `<#0.3#>` inserta 0,3 s de
+silencio en ese punto exacto. Sin estas marcas el audio suena corrido y sin respiración;
+poniendo una pausa fija igual en todos los puntos suena robótico. La receta que sí funciona
+es **pausa proporcional a la longitud de la frase que se acaba de cerrar**:
+
+- Tras cada fin de frase (`.`, `!`, `?`, `…`), calcular el nº de palabras de esa frase:
+ - frase corta (< 6 palabras) → pausa **0,1 s**
+ - frase media (6-12 palabras) → pausa **0,2 s**
+ - frase larga (> 12 palabras) → pausa **0,3 s**
+ (umbrales configurables, pero estos son los que ya suenan bien; no hace falta tocarlos)
+- Entre párrafos (allí donde había una línea en blanco en el original) → pausa fija de
+ **0,7 s**, más larga que cualquier pausa de frase — es lo que hace que el oído perciba el
+ cambio de idea/tema.
+- La última pausa de frase de cada párrafo se descarta (queda sustituida por la del
+ párrafo, que ya es más larga — poner las dos seguidas suena a silencio muerto).
+- **Todo bloque (título, párrafo) que no termine en signo de puntuación se cierra con un
+ punto antes de generar el audio.** Sin esto, MiniMax deja la entonación "abierta" en el
+ aire al final de un título o frase cortada — se nota mucho y es fácil no darse cuenta en
+ texto pero sí al oído.
+
+Ejemplo de transformación (simplificado):
+
+```
+Entrada: "Jesús les dijo esto. Y añadió una frase mucho más larga sobre lo que significaba
+ seguirle de verdad en el día a día.\n\nOtro párrafo distinto."
+
+Salida: "Jesús les dijo esto. <#0.1#> Y añadió una frase mucho más larga sobre lo que
+ significaba seguirle de verdad en el día a día. <#0.7#> Otro párrafo distinto."
+```
+
+(la frase corta cerró con 0,1s pero esa marca se sustituye por la de párrafo 0,7s al ser la
+última del bloque; la larga, al no ser la última del párrafo, habría llevado 0,3s si hubiera
+seguido más texto en el mismo párrafo).
+
+Referencia de implementación exacta (por si hay dudas de un caso concreto):
+`scripts/minimax_tts.py::add_pauses` + `_sent_pause` + `ensure_terminal_punctuation`.
+
+### 3.5 Trocear si el texto es largo
+
+MiniMax limita cada petición a 10.000 caracteres; dejamos margen porque las marcas de pausa
+también cuentan, así que **el límite práctico es 8.000 caracteres por petición**. Cartas
+largas hay que trocearlas:
+
+- Cortar **solo por los puntos donde ya hay una marca de pausa** (nunca a mitad de frase).
+- Entre petición y petición, **esperar 35 segundos** — es el límite de ritmo (TPM) de la
+ cuenta MiniMax; sin esta espera, la segunda petición puede fallar aunque quede cuota.
+- Los fragmentos de audio resultantes se concatenan con `ffmpeg` (`concat` a nivel de audio,
+ sin recodificar más de lo necesario) para producir un único mp3 final.
+
+## 4. Llamar a la API
+
+```
+POST https://api.minimax.io/v1/t2a_v2
+Authorization: Bearer
+Content-Type: application/json
+
+{
+ "model": "speech-2.8-hd",
+ "text": " ya insertadas>",
+ "voice_setting": {"voice_id": "", "speed": 1.0, "vol": 1.0, "pitch": 0},
+ "audio_setting": {"sample_rate": 32000, "bitrate": 128000, "format": "mp3", "channel": 1},
+ "language_boost": "Spanish"
+}
+```
+
+La respuesta trae el audio en `data.audio` como **hexadecimal**, no base64 — decodificarlo a
+bytes antes de escribir el mp3.
+
+### Errores de cuota — no reintentéis en bucle
+
+Si `data.audio` viene vacío, mirad `base_resp.status_code`:
+
+- **`2056`** = cuota del plan agotada (se resetea con el tiempo, horas).
+- **`1039`** = límite de ritmo por minuto (TPM) — bajad la cadencia, no lo repitáis al
+ segundo siguiente.
+
+Un HTTP 200 **no significa que haya audio** — hay que mirar siempre el `base_resp` del
+cuerpo. Ante cualquiera de estos dos códigos: **parad y reintentad más tarde**, no
+machaquéis la API en bucle esperando que cambie.
+
+## 5. Acabado del audio (opcional pero es lo que suena en producción)
+
+El pipeline actual aplica dos retoques más al mp3 antes de darlo por bueno — no son
+obligatorios para que el audio "funcione", pero sí para que suene igual que el resto del
+catálogo ya publicado:
+
+1. **Ruido de confort:** una capa de ruido marrón muy suave (`amplitude=0.004`, filtrado
+ paso-alto 120 Hz / paso-bajo 3800 Hz) mezclada con el audio entero. Sin esto, los
+ silencios de las pausas `<#…#>` contrastan de forma audible con el suelo de ruido del
+ habla generada — se nota como un "hueco" en vez de una pausa natural.
+2. **Fade in/out:** entrada suave de 0,08 s y salida de 0,5 s en los últimos 0,5 s del
+ audio, para evitar un corte brusco ("bump") al final.
+
+Receta `ffmpeg` exacta usada hoy (referencia: `scripts/minimax_tts.py::t2a`, líneas finales):
+
+```bash
+ffmpeg -y -i entrada.mp3 -filter_complex \
+ "anoisesrc=color=brown:amplitude=0.004:sample_rate=32000[n];\
+ [n]highpass=f=120,lowpass=f=3800[nf];\
+ [0:a][nf]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[m];\
+ [m]afade=t=in:st=0:d=0.08,afade=t=out:st=:d=0.5[a]" \
+ -map "[a]" -b:a 128k salida.mp3
+```
+
+Si vuestro entorno de generación no tiene `ffmpeg` disponible, generad el audio igual sin
+este paso y avisad a Rafa/Claudix — es preferible subir un audio sin el acabado a no subir
+nada, pero que quede constancia de que ese paso se saltó (para que quien revise sepa por qué
+suena ligeramente distinto al resto).
+
+## 6. Subir el resultado al artículo — BLOQUEADO hoy
+
+Aquí es donde entra el hueco real que reportó el #222: **no hay forma de enganchar el mp3 al
+post por REST todavía.** Los campos que necesita el sitio para reproducir el audio
+(`fea_audio_url`, `fea_audio_done`, más `fea_audio_voice` para saber qué voz sonó) no están
+expuestos en `/wp/v2/posts/{id}`, y el namespace `fea/v1` no tiene endpoint de audio.
+
+**Esto se resuelve con un endpoint `fea/v1/subir-audio`**, con el mismo patrón que
+`fea/v1/subir-avatar` (#175) que ya usáis: multipart con `post_id` + fichero `audio` (mp3),
+autenticación igual que el resto de endpoints `fea/v1` (vuestro usuario Editor/Administrator),
+y como respuesta el valor anterior de esos metas para poder deshacer si algo suena mal.
+**Codix lo está construyendo** a partir de esta misma información — en cuanto esté
+desplegado, se actualiza este documento con el ejemplo de llamada real y se cierra el punto 1
+del #222.
+
+Mientras tanto: generad y guardad los mp3 con normalidad (pasos 1-5), pero no hay manera de
+publicarlos todavía — no es un fallo vuestro, es infraestructura pendiente.
+
+## 7. Lo que NO cambia con este traspaso
+
+- Las traducciones (EN/FR/IT/PT) siguen, de momento, por el camino de siempre
+ (`docs/guia-tts-traduccion-inma.md`, motor Gemma/MiniMax vía Hermes) — este documento
+ cubre solo audio. Si el traspaso de traducciones también avanza, hará falta un documento
+ equivalente a este (y probablemente el mismo tipo de hueco de REST para enlazar las
+ traducciones vía Polylang) — no asumáis que ya está resuelto por analogía con el audio.
+- El backlog histórico de TTS (issue #188, ~1.700 audios pendientes) es un problema aparte
+ de mucho más volumen que la carta semanal (~15-20/semana). Si el traspaso del #222 incluye
+ también ese backlog, es una decisión de Rafa que hay que confirmar explícitamente antes de
+ empezar — no arranquéis el backlog solo porque ya tengáis el mecanismo funcionando para la
+ carta semanal.
diff --git a/docs/guia-tts-traduccion-inma.md b/docs/guia-tts-traduccion-inma.md
index dfdaa18..d06ed3e 100644
--- a/docs/guia-tts-traduccion-inma.md
+++ b/docs/guia-tts-traduccion-inma.md
@@ -8,6 +8,14 @@
> 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.
+>
+> **Actualización 2026-08-31 (issue [#222](https://gitea.feadulta.com/rafa/feadulta/issues/222)):**
+> el **audio (TTS)** deja de depender de Hermes — Inma/Mixbot lo generan y suben con su propio
+> token de MiniMax. La §4 de abajo describe el flujo viejo (Hermes ejecutando
+> `scripts/tts_produce.py` en el PC de Rafa); para el flujo nuevo de autoservicio, con la receta
+> exacta de preparación de texto y pausas, usar
+> **`docs/guia-tts-audio-autoservicio-inma-mixbot.md`**. Las **traducciones** siguen por ahora
+> como se describe en §3 de este documento — no incluidas todavía en el traspaso.
---