Capítulo 14 de 22 · intermedio
Audio: de voz a texto y de regreso
Qué cubre esta sesión
La última modalidad, y la única que corre en ambas direcciones. De voz a texto (STT) convierte audio en un string — transcripción, comandos de voz, notas de reunión. De texto a voz (TTS) convierte un string en audio hablado — narración, asistentes de voz, accesibilidad. Entre las dos cierran el círculo: un usuario puede hablarle a tu app y tu app puede responderle. Ninguna es difícil; ambas son una llamada a la API. Las partes interesantes son los detalles de formato de los que nadie te advierte, y el patrón que la mayoría de las features de audio en realidad sigue.
Para probar las dos mitades en un solo script autocontenido, hacemos un viaje redondo: sintetizamos una oración a audio, luego transcribimos ese audio de vuelta a texto y lo vemos regresar intacto. Sin archivos de muestra que buscar — el script hace los suyos.
OpenAI
De texto a voz: elige una voz, pasa el texto, recibe bytes MP3. MP3 es un contenedor que todo navegador y reproductor entiende, así que no hay nada que convertir.
# Text to speech: pick a voice, pass the text, get audio bytes back. The model
# returns MP3, which every browser and player understands.
def speak(text: str, path: str) -> str:
response = client.audio.speech.create(model=_TTS_MODEL, voice="alloy", input=text)
with open(path, "wb") as f:
f.write(response.content)
return path
De voz a texto es la imagen espejo — entrégale el archivo de audio al modelo de transcripción y lee el texto del resultado.
# Speech to text: hand the audio file to the transcription model, get the text.
def transcribe(path: str) -> str:
with open(path, "rb") as f:
return client.audio.transcriptions.create(model=_STT_MODEL, file=f).text
El viaje redondo es un arnés de prueba genuinamente útil: si "the quick brown fox" regresa como "the quick brown fox," las dos mitades de tu pipeline de audio funcionan. Cuando construyas una feature de voz, ten a la mano una verificación de viaje redondo como esta — los bugs de audio son miserables de depurar de oído, y un diff de transcripción te dice al instante cuál mitad se rompió.
Gemini
Dos diferencias de cableado que vale la pena conocer de entrada. El TTS de
Gemini regresa muestras PCM crudas, no un contenedor, así que nada puede
reproducirlas hasta que las envuelvas en un header WAV — unas cuantas líneas con
el módulo wave de la biblioteca estándar. Y la transcripción es solo la
llamada multimodal de la semana 10 con una parte de audio en lugar de una de
imagen.
# Gemini TTS returns 24kHz 16-bit mono PCM in inline_data. PCM is just raw
# samples with no header, so nothing can play it until we wrap it in a WAV
# container — a few lines with the standard-library `wave` module.
def _pcm_to_wav(pcm: bytes, path: str, rate: int = 24000) -> None:
with wave.open(path, "wb") as w:
w.setnchannels(1)
w.setsampwidth(2) # 16-bit
w.setframerate(rate)
w.writeframes(pcm)
def speak(text: str, path: str) -> str:
response = client.models.generate_content(
model=_TTS_MODEL,
contents=text,
config=types.GenerateContentConfig(
response_modalities=["AUDIO"],
speech_config=types.SpeechConfig(
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Kore"),
),
),
),
)
pcm = response.candidates[0].content.parts[0].inline_data.data
_pcm_to_wav(pcm, path)
return path
Esa trampa de PCM-versus-contenedor es el bug de audio más común de todos:
recibes los bytes de vuelta, los guardas como .wav, y nada se reproduce,
porque las muestras crudas no tienen header que describa el sample rate y el
ancho. Envuélvelas y funciona. No es un problema del modelo; es un problema de
formato, y ahora conoces el arreglo.
Ponlo a trabajar
Tres apps de docker-compose bajo code/showcase/<slug>/. Dos reciben una URL de
audio y regresan texto; la de texto a voz regresa audio que la página
reproduce. Mismo procedimiento: bash bootstrap-secrets.sh,
docker compose up --build, http://localhost:3000.
Showcase 1 — Transcribe
Entra una URL de audio, sale una transcripción. La base sobre la que todo lo demás se construye. Nota la división entre proveedores: OpenAI tiene un endpoint de transcripción dedicado, Gemini trata el audio como una parte multimodal más — dos filosofías, el mismo resultado.
Showcase 2 — Texto a voz
Escribe una línea, escúchala. Esta es la que muestra la arruga de formato en la
práctica: el MP3 de OpenAI cae directo en un elemento <audio>, mientras que el
PCM de Gemini primero se envuelve en WAV. La página renderiza lo que venga de
vuelta.
Showcase 3 — Notas de reunión
De audio a un resumen con puntos de acción — el patrón que la mayoría de los productos de audio en realidad publica. OpenAI transcribe y luego resume (dos pasos); Gemini lo hace en una sola llamada porque entiende audio de forma nativa. De cualquier modo, el valor son las palabras, y un modelo de texto hace el razonamiento sobre ellas.
Las tres son STT o TTS con una capa delgada encima. La modalidad es nueva; la ingeniería — una llamada, algo de manejo de formato, quizá un segundo paso de texto — es familiar.
Córrelo
El README de esta carpeta tiene la versión de Python, la línea de instalación,
las dos variables de entorno y los comandos exactos. Las keys vienen del .env
sin trackear en la raíz del curso. El envoltorio WAV usa la biblioteca estándar;
solo las llamadas al SDK necesitan keys.
Lo que te llevas
El audio son dos direcciones de una llamada cada una — STT para traer la voz adentro, TTS para mandarla de vuelta — y juntas dejan que un usuario le hable a tu app y la escuche responder. La lección que te ahorra tiempo real no es la API, es el formato: el PCM crudo no se reproduce hasta que se envuelve en un contenedor, así que cuando el audio regrese en silencio, sospecha del header antes que del modelo. Y nota la forma recurrente — la mayoría de las features de audio son STT seguido de un paso de texto ordinario, lo que significa que todo lo de las semanas anteriores (prompting, salida estructurada, grounding) aplica sobre la transcripción. Eso cierra el arco multimodal: texto, imágenes adentro, imágenes afuera, y ahora audio en ambos sentidos. La próxima semana pasamos de capacidades a oficio — contexto largo y preguntas y respuestas sobre documentos, y qué pasa de verdad cuando metes un millón de tokens en un prompt.