
Hay posts que son largos. Y cuando digo largos me refiero a esos artículos de tres mil palabras donde explicas un concepto técnico paso a paso, con bloques de código intercalados y algún que otro diagrama mental. Son útiles, pero reconozcámoslo, no siempre tienes la energía de sentarte a leer con la atención que merecen.
A veces estás en el autobús, preparando la cena o simplemente con los ojos cansados después de una jornada de código. En esos momentos pensé que estaría bien poder escuchar los artículos en vez de leerlos. Como un podcast, pero sin tener que grabar nada manualmente.
Así que me puse a investigar y acabé montando un sistema de narración por voz con inteligencia artificial integrado directamente en el blog. En este post te cuento cómo funciona, qué decisiones técnicas tomé y por qué creo que es una funcionalidad que muchos blogs técnicos deberían plantearse.
El concepto es bastante directo. Cada post tiene un reproductor de audio en la parte superior de la página. Cuando un visitante pulsa "Generar audio", el servidor convierte el contenido del post en voz usando un modelo de IA, guarda el fichero de audio en almacenamiento S3 y lo sirve a todos los visitantes que lleguen después. El primer usuario espera un poco mientras se genera, pero a partir de ahí el audio está cacheado y se reproduce al instante.
Es un enfoque lazy, bajo demanda. No genero audio de todos los posts automáticamente porque cada generación tiene un coste (pequeño, pero coste al fin y al cabo). Solo se genera lo que alguien realmente quiere escuchar.
Investigué bastante antes de decidirme. Las opciones principales eran OpenAI TTS, Google Cloud TTS, ElevenLabs y Qwen3-TTS de Alibaba. Cada una tiene sus puntos fuertes.
ElevenLabs tiene probablemente la mejor calidad de voz del mercado, pero su modelo de suscripción mensual no encajaba con el volumen bajo de un blog personal. Google Cloud tiene un tier gratuito generoso, pero configurar el proyecto GCP con service accounts y SDKs era más complejidad de la que necesitaba. Qwen3-TTS es open source y se puede autoalojar, lo cual mola, pero requiere una GPU dedicada o usar un proveedor como Replicate.
Al final me quedé con OpenAI y su modelo gpt-4o-mini-tts. La API es simple, un solo POST que devuelve el audio en binario. La calidad en español es muy buena con la voz coral, que suena cálida y conversacional, justo lo que buscaba para narrar artículos técnicos sin que suene a robot leyendo un manual.
Lo interesante es que el sistema es agnóstico al proveedor. Todo se configura con variables de entorno, así que cambiar de OpenAI a Qwen3-TTS o cualquier otro servicio que exponga un endpoint compatible es cuestión de modificar cinco variables, sin tocar una línea de código.
Convertir un post de blog en audio no es tan directo como pasarle el HTML al modelo de TTS. Hay varios pasos intermedios que son necesarios para que el resultado suene bien.
Lo primero es extraer texto limpio del HTML. Los bloques de código se eliminan porque escuchar a una IA leer const express = require('express') no aporta nada. Lo mismo con tablas, imágenes y elementos embebidos. Los headings se convierten en pausas naturales y las citas se prefijan con "Cita" para dar contexto al oyente. El código inline como nombres de funciones o variables sí se mantiene porque forma parte de la explicación.
Después viene el chunking. La API de OpenAI acepta un máximo de 4096 caracteres por petición, así que hay que dividir el texto en trozos. El truco está en cortar por párrafos siempre que sea posible, cayendo a cortes por oraciones si un párrafo es muy largo. Así cada fragmento mantiene coherencia semántica y la voz no se corta a mitad de una idea.
Cada chunk se envía secuencialmente a la API, que devuelve un buffer MP3. Como el formato MP3 es frame-based, los buffers se pueden concatenar directamente sin necesidad de librerías de audio. El resultado final se sube a S3 y la URL se guarda en la base de datos junto con un hash del contenido.
Este es probablemente el aspecto más importante del diseño. Cada vez que se genera audio para un post, se calcula un hash SHA-256 del texto procesado (no del HTML en bruto, sino del texto que realmente se narra). Ese hash se guarda junto con la URL del audio.
Cuando alguien visita el post, el reproductor hace una petición GET que compara el hash actual del contenido con el hash almacenado. Si coinciden, devuelve la URL del audio cacheado y la reproducción empieza inmediatamente. Si el hash es diferente (porque el post se ha editado), el reproductor muestra un badge de "Actualizar" que permite regenerar el audio con el contenido nuevo. El audio antiguo sigue siendo reproducible mientras tanto, así que nunca hay una experiencia rota.
Este enfoque tiene una ventaja clave para el control de costes. El audio solo se regenera si el contenido cambia y alguien pide explícitamente la regeneración. Nada se genera automáticamente. Si editas una coma en un post y nadie pulsa regenerar, no pagas nada.
El reproductor es un componente React client-side que usa el elemento <audio> nativo del navegador. Sin librerías de audio externas. Los controles son los típicos de cualquier reproductor de podcast, play y pausa, barra de progreso donde puedes hacer click para saltar a cualquier punto, botones para avanzar o retroceder 15 segundos, control de velocidad (desde 0.75x hasta 2x) y un botón para descargar el MP3.
Algo que quería desde el principio era un reproductor flotante que acompañase la lectura. Cuando el reproductor principal sale de pantalla al hacer scroll (porque estás leyendo el artículo mientras lo escuchas), aparece una barra compacta fija en la parte inferior con los controles básicos. Solo aparece si el audio está reproduciéndose o en pausa, nunca si no has iniciado la reproducción. Un botón con una flecha te lleva de vuelta al reproductor principal si quieres ver los controles completos.
En móvil los controles se adaptan, ocultando los botones de skip y velocidad en el reproductor flotante para no saturar una pantalla pequeña, pero manteniéndolos en el reproductor principal.
Dado que cada generación de audio consume créditos de la API de OpenAI, la protección contra abusos era fundamental. Implementé un sistema de rate limiting en dos capas.
La primera es un límite por IP que restringe el número de generaciones que un mismo visitante puede hacer por hora. La segunda es un límite global que actúa como techo absoluto para todo el servidor, independientemente de cuántas IPs diferentes hagan peticiones. Así, aunque alguien intente atacar con IPs rotadas, hay un máximo de gasto por hora que no se puede superar.
Además, la idempotencia del sistema actúa como protección natural. Si alguien pide generar audio para un post que ya tiene audio cacheado con el mismo contenido, la respuesta es inmediata y no se hace ninguna llamada a la API de OpenAI. Solo pagas por generaciones realmente nuevas.
Montar esto me enseñó varias cosas que no esperaba. La primera es que la calidad de la conversión HTML a texto importa más de lo que parece. Si no eliminas los bloques de código correctamente o no manejas bien las entidades HTML, el modelo de voz acaba diciendo cosas como "ampersand" o leyendo nombres de clases CSS, lo cual rompe completamente la experiencia.
La segunda es que el chunking es más sutil de lo que parece. Si cortas una oración a la mitad, la entonación del final de un chunk y el principio del siguiente no casan bien. Cortar por párrafos siempre que sea posible produce un resultado mucho más natural.
Y la tercera es que un sistema multi-proveedor bien diseñado vale la pena desde el principio. Empecé con OpenAI, probé Qwen3-TTS a través de Replicate, volví a OpenAI porque la voz me convenció más, y todo sin cambiar una sola línea de código. Solo variables de entorno.
Para un blog personal con publicaciones esporádicas, el coste es prácticamente irrelevante. Cada post de unas cinco mil palabras cuesta unos céntimos en generar. Y como el audio se cachea indefinidamente, solo pagas una vez por post. El almacenamiento en S3 es autoalojado, así que no hay coste adicional por servir el audio.
El mes pasado generé audio para varios posts y el coste total no llegó al euro. Para lo que aporta en accesibilidad y comodidad para los lectores, me parece una inversión ridículamente baja.
De momento el sistema funciona bien tal y como está, pero hay algunas mejoras que me gustaría explorar. Una es añadir un botón en el panel de administración para pre-generar audio de posts concretos sin tener que esperar a que un visitante lo pida. Otra es experimentar con diferentes voces según el tipo de contenido, una voz más seria para posts sobre seguridad y una más informal para reflexiones personales.
También me interesa explorar la posibilidad de self-hostear Qwen3-TTS cuando tenga acceso a una GPU, lo que eliminaría por completo el coste por generación.
Si tienes un blog técnico y te planteas añadir algo similar, mi consejo es que empieces con lo más simple posible. Un modelo de TTS, una voz, cache en S3 y un rate limit. Ya habrá tiempo de sofisticar.