Course EN

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.