Cloné mi voz para el podcast, y descubrí que los TTS se comen frases
ContenidoContents
Durante veinte episodios, la voz de mi podcast no era mía. Era una voz sintética de catálogo, alquilada por una cuota mensual. Funcionaba bien y no tenía queja.
Pero un día leí que había alternativas mucho más baratas y me fui a mirar. La historia de ese paseo tiene tres partes: un catálogo que no me esperaba, una letra pequeña que se cargó el motivo del viaje, y un fallo técnico que creo que casi nadie cuenta y que me parece lo más útil de todo esto.
Aviso desde ya: acabé volviendo al mismo sitio del que salí, pero con la voz cambiada.
Parte 1: el catálogo en español es un campo de minas
El candidato era Fish Audio: mismo tipo de servicio, precio de API muy por debajo. Lo primero que hice fue mirar qué voces en español tiene, porque el problema de siempre es el acento: necesito castellano, no latino.
Filtré su biblioteca por castellano y esto es lo que salió, ordenado por uso:
- un presentador de televisión muy conocido
- el narrador de los documentales de naturaleza
- personajes de doblaje: Bob Esponja, Homer, Rambo, Goku
- un futbolista con nombre y apellidos
Voces clonadas de personas reales, subidas por usuarios, listas para que cualquiera genere audio con ellas. La API te las sirve igual que cualquier otra.
Que estén ahí no significa que puedas usarlas. Si publico un podcast con la voz de alguien reconocible, el problema no es técnico. Y no hablo de la zona gris de “suena parecido”: hablo de voces etiquetadas con el nombre y apellidos de la persona.
Descartando todo eso, lo que queda son voces genéricas sin dueño aparente. Probé cuatro. Todas leían el mismo texto a unas 165 palabras por minuto, cuando mi podcast va a 245. Eso es ritmo de audiolibro: un episodio de cinco minutos y medio se me iba a ocho.
Parte 2: la letra pequeña que se cargó el motivo del viaje
Todo esto lo hacía por precio, así que fui a las cuentas.
Mis guiones pesan entre 4.000 y 7.700 bytes. A quince dólares por millón de bytes, cada episodio cuesta unos ocho céntimos. El consumo es irrelevante, en cualquiera de los dos servicios. Lo que decide es la suscripción.
Y ahí estaba la trampa:
| Plan | Precio | API de pago por uso |
|---|---|---|
| Free | 0 $ | No (y solo 7 minutos al mes) |
| Plus | 5,50 $/mes | No |
| Pro | 37,50 $/mes | Sí |
La API con la que había hecho todas las pruebas solo está disponible del plan Pro en adelante. O sea que migrar me salía por más del triple de lo que ya estaba pagando.
El motivo del viaje se acababa de evaporar. Pero para entonces ya había descubierto lo interesante.
Parte 3: clonar mi propia voz
Si el catálogo no sirve, queda la opción evidente: usar mi voz.
Grabé dos minutos metido en el armario de la ropa, que es la mejor cabina de grabación que hay en una casa normal: la ropa se come el eco, y el eco no se puede quitar después. Con el iPhone, en Notas de Voz, con la calidad puesta en “sin pérdidas”.
Antes de gastar la clonación, medí la toma:
1ffmpeg -i muestra.m4a -af volumedetect -f null - # picos: -6,3 dB, sin saturar
2ffmpeg -i muestra.m4a -af astats -f null - # suelo de ruido: -60 dB
Suelo de ruido a −60 dB y sin saturación: sirve. Si clonas sobre una toma mala, el defecto lo arrastran todos los episodios que hagas.
Y aquí sí hubo sorpresa agradable: la voz clonada copia tu ritmo. Diciendo el mismo texto, mi voz clonada tardó 40,2 segundos y la voz de catálogo que llevaba veinte episodios, 40,0. Clavado. El problema del ritmo de audiolibro desaparecía solo.
Y ahora lo importante: se comen frases
Escuchando una prueba noté algo raro y pensé que el modelo había cambiado de idioma un par de segundos. Fui a mirarlo con calma y no era eso.
Lo que hace es peor. Se salta una frase entera del guión y rellena el hueco con sonidos que no significan nada. Esto es lo que whisper transcribió de un tramo de seis segundos:
diompreinse quintun tianos den timpreinse hana santo de pugenia
Suena a otro idioma porque son fonemas sueltos con entonación de frase. Y donde debería haber dicho “Y una caja en medio, oficial, tiene su comodidad, pero también su riesgo”, no dijo nada parecido. Siguió como si tal cosa, sin pausas raras ni cortes.
Lo medí en serio, generando el mismo texto varias veces y comparando palabra por palabra:
| Configuración | Fidelidad al guión |
|---|---|
Por defecto (temperature 0.7) |
88,4% — se comió una frase |
| Conservador (0.3, chunks de 150) | 91,2% — se comió otra distinta |
| Muy conservador (0.2, chunks de 100) | 91,3% — volvió a fallar |
| Un párrafo por llamada | 100% |
Los parámetros no arreglan nada. El disparador es mandarle varios párrafos de una vez: el modelo pierde el hilo y se inventa el tramo. Troceando por párrafos, clava el texto.
Y el que ya usaba tampoco es inmune, que es la parte que me interesa de verdad. Con mi voz clonada, en un bloque de 227 palabras se saltó frases en cuatro intentos seguidos, cada vez una distinta. Sus números fueron mejores —95,4% y 98,7% frente a 82,2% y 88,4%— pero el fallo es el mismo, solo que más raro.
Lo arreglé de dos maneras, las dos aburridas:
- Partiendo las frases largas. Las que se comía tenían incisos: “Sumar, contar y medir lo hace un programa normal y corriente, de esos de toda la vida, que si suma dos más dos le dan cuatro siempre”. Convertida en cuatro frases cortas, salió limpia a la primera.
- Una llamada por párrafo en los bloques largos.
El método: que lo revise algo sin oído
Esto es lo que me llevo. De oído no lo habría pillado nunca. Un balbuceo de dos segundos en medio de una frase pasa desapercibido cuando llevas media hora escuchando tu propio texto y sabes lo que viene después. El oído completa.
Así que la validación la hace una máquina: se genera el audio, se transcribe con whisper y se compara con el guión palabra por palabra.
1dicho = transcribir(mp3) # whisper-cli, local
2a, b = normalizar(guion), normalizar(dicho)
3ratio = difflib.SequenceMatcher(None, a.split(), b.split()).ratio()
Con eso, cada bloque del episodio te dice si es publicable antes de montarlo. Y si falla, se regenera solo ese trozo.
Un detalle importante si montas algo así: hay que ignorar las diferencias de números, porque whisper escribe “12.345” donde tu guión dice “doce mil trescientos cuarenta y cinco”. Si no las filtras, el validador da falsos positivos todo el rato.
El chiste final: el verificador también miente
Ya montado el episodio, el validador me marcó dos frases perdidas en un bloque. Pero esas mismas frases, generadas por separado, habían salido limpias.
Comparé duraciones: la suma de los trozos daba 72,05 segundos y el archivo montado, 72,05 segundos. No faltaba ni una décima. No faltaba audio: era whisper el que se saltaba trozos al transcribir el archivo largo.
O sea que la herramienta que uso para detectar fallos silenciosos tiene exactamente el mismo fallo silencioso. Por eso la validación se hace por bloques cortos y se contrasta con las duraciones, que no opinan.
Qué me llevo
- En un catálogo de voces clonadas, mira de quién es la voz antes de publicarla. Que la API te la sirva no significa nada.
- Comprueba qué plan da acceso a la API antes de contar el ahorro. El mío estaba tres escalones por encima del que había mirado.
- Ningún TTS es fiable palabra por palabra. Todos se saltan cosas, unos más que otros. Frases cortas y una llamada por párrafo.
- Si publicas audio generado, valídalo con algo que no tenga oído. Y desconfía también del validador.
Al final me quedé donde estaba, pagando lo mismo. Pero ahora la voz que lee mis guiones sale de dos minutos que grabé dentro de un armario, y eso me parece un cambio más grande que el de proveedor.
Lo cuento con esa misma voz en el episodio 21 del podcast, donde además se despide la anterior.