Voz a texto en Python: archivo, streaming y micrófono

Rene, Kabir Goel 
Voz a texto en Python: archivo, streaming y micrófono

Para convertir voz a texto en Python, envías audio a un modelo de reconocimiento de voz y recibes texto. La decisión interesante es qué tipo de modelo, porque un archivo grabado y una conversación en vivo son trabajos distintos. Un archivo puede esperar al clip completo. Quien llama, no.

Esta guía cubre tres formas de hacerlo, cada una con código que funciona tal cual: transcribir un archivo grabado, hacer streaming de audio con detección automática de turnos y transcribir tu micrófono en vivo. Los ejemplos de streaming usan Cartesia Ink 2, el modelo que construimos para agentes de voz. Cuando un modelo local gratuito encaja mejor, lo decimos y también lo mostramos.

Elige un enfoque

TienesUsaPor qué
Archivos grabados, el audio no puede salirfaster-whisperGratis, local y preciso con audio limpio. Solo por lotes
Archivos grabados, sin GPU que gestionarUna API de STT por lotesUna llamada HTTP por archivo
Audio en vivo: un agente, subtítulos, un teléfonoUna API de STT en streaming con detección de turnosEl texto llega mientras la persona habla, y el modelo dice cuándo termina
Un script de fin de semanaEl paquete SpeechRecognitionRápido de probar. Su clave de Google por defecto es solo para pruebas

La mayoría de tutoriales se quedan en SpeechRecognition con recognize_google(). Ese paquete envuelve una docena de motores, pero su backend de Google por defecto usa una clave compartida que Google puede revocar en cualquier momento, y espera a tener una frase completa antes de enviar nada. Sirve para una demo. No para atender llamadas.

Requisitos previos

python -m pip install "cartesia[websockets]"
export CARTESIA_API_KEY="your-api-key"

El extra websockets es necesario para los ejemplos de streaming. Guarda la clave en una variable de entorno o en un gestor de secretos, nunca en código que subas al repositorio.

1. Transcribe un archivo grabado

Si el audio ya está grabado, envía el archivo completo en una sola petición. El endpoint por lotes de Cartesia acepta contenedores como WAV y MP3, y puede devolver una marca de tiempo por palabra. Guarda esto como transcribe_file.py:

import os
import sys

from cartesia import Cartesia

client = Cartesia(api_key=os.environ["CARTESIA_API_KEY"])

with open(sys.argv[1], "rb") as audio_file:
    result = client.stt.transcribe(
        file=audio_file,
        model="ink-whisper",
        language="en",
        timestamp_granularities=["word"],
    )

print(result.text)
for word in result.words or []:
    print(f"{word.start:6.2f}s  {word.word}")

Ejecútalo con python transcribe_file.py meeting.mp3. La transcripción por lotes usa ink-whisper, que cubre unos 100 idiomas; pasa el código del idioma de tu audio. El ejemplo de STT por lotes de la documentación incluye la versión asíncrona.

La alternativa local

Si el audio no puede salir de tu máquina, o tienes mucho y una GPU, ejecuta Whisper tú mismo. faster-whisper reimplementa Whisper de OpenAI sobre CTranslate2 y funciona en la CPU de un portátil:

python -m pip install faster-whisper
from faster_whisper import WhisperModel

model = WhisperModel("base.en", device="cpu", compute_type="int8")
segments, info = model.transcribe("jfk.flac", beam_size=5)

print(f"Detected language: {info.language} ({info.language_probability:.2f})")
for segment in segments:
    print(f"[{segment.start:.2f}s -> {segment.end:.2f}s] {segment.text}")

Con la muestra de JFK de 11 segundos del repositorio de Whisper, el modelo base.en transcribió la cita palabra por palabra en unos 2,5 segundos en una CPU de servidor de 4 núcleos, tras una descarga única del modelo de 140 MB. Los modelos más grandes son más precisos y más lentos. Lo que no tienes es streaming: Whisper trabaja con segmentos terminados, así que no es la herramienta para una conversación.

2. Haz streaming de audio y recibe turnos

Un agente de voz necesita dos cosas que un modelo por lotes no le da: texto mientras la persona sigue hablando y una decisión sobre cuándo ha terminado. Ink 2 hace ambas en un solo WebSocket. Su detección de turnos atiende a lo que se dice además de al silencio, así que a quien hace una pausa a mitad de frase para pensar no se le corta solo porque la línea se quedó callada.

Este script envía un archivo WAV como si fuera audio en vivo. Guárdalo como stream_file.py:

import os
import sys
import time
import wave

from cartesia import Cartesia

with wave.open(sys.argv[1], "rb") as wav:
    if wav.getnchannels() != 1 or wav.getsampwidth() != 2:
        raise SystemExit("Convert the file to 16-bit mono WAV first.")
    sample_rate = wav.getframerate()
    chunks = []
    while data := wav.readframes(sample_rate // 10):  # 100 ms per chunk
        chunks.append(data)

client = Cartesia(api_key=os.environ["CARTESIA_API_KEY"])
transcript = ""

with client.stt.auto_finalize.websocket(
    model="ink-2",
    encoding="pcm_s16le",
    sample_rate=sample_rate,
) as ws:
    for chunk in chunks:
        ws.send_raw(chunk)
        time.sleep(0.1)  # send at the speed of speech
    ws.send({"type": "close"})  # flush buffered audio; the server then closes

    turn_text = ""
    for event in ws:
        if event.type == "turn.update":
            turn_text = event.transcript or turn_text  # the whole turn so far
            print("...", turn_text)
        elif event.type == "turn.end":
            text = event.transcript or turn_text  # fall back if turn.end is empty
            print("turn:", text)
            transcript += text  # join verbatim, no strip()
            turn_text = ""
        elif event.type == "error":
            raise SystemExit(event.message)

print(transcript)

Si tu archivo no es WAV mono de 16 bits, conviértelo antes:

ffmpeg -i input.mp3 -ac 1 -ar 16000 -c:a pcm_s16le input.wav

Tres detalles de ese script son fáciles de hacer mal:

  • Muestras crudas, no un archivo. Los endpoints en tiempo real reciben bytes de audio sin cabecera, por eso el script lee frames del WAV en lugar de enviar el archivo. Declaras encoding y sample_rate al conectarte, y tienen que coincidir con los bytes. La guía de entrada de audio advierte que el servidor puede no devolver un error si no coinciden, así que una frecuencia equivocada aparece como una mala transcripción, no como una excepción.
  • Ritmo de tiempo real. El sleep(0.1) después de cada fragmento de 100 ms no es decorativo. Los endpoints en tiempo real esperan más o menos un segundo de audio por segundo, y volcar un archivo entero en el socket puede saturar el búfer del servidor. Para un archivo que no necesitas en vivo, usa la llamada por lotes del paso 1.
  • Transcripciones acumulativas. Cada turn.update contiene todo el turno hasta ese momento, no un fragmento nuevo. Concatenar actualizaciones repite palabras. Toma el texto de turn.end, una vez por turno, y únelo tal como llega. El script también guarda el último turn.update y lo usa si alguna vez llega un turn.end con la transcripción vacía, un caso que nuestros propios clientes tienen en cuenta.

Para que sea breve, este script lee los eventos después de enviar todo el archivo, así que todo se imprime al final. El ejemplo del micrófono, más abajo, envía y lee a la vez.

Ink 2 transcribe inglés, francés, hindi, japonés y español, y detecta cuál está oyendo. Para otros idiomas en streaming, consulta nuestra comparativa de modelos de voz a texto.

3. Transcribe tu micrófono en vivo

Mismo endpoint, entrada real. Esta versión usa sounddevice para capturar audio mono a 16 kHz y el cliente asíncrono para enviar y recibir a la vez:

python -m pip install sounddevice

En Linux puede que también necesites PortAudio (sudo apt install libportaudio2). Guarda esto como microphone.py:

import asyncio
import os

import sounddevice as sd
from cartesia import AsyncCartesia

SAMPLE_RATE = 16000
BLOCK = SAMPLE_RATE // 10  # 100 ms of audio per message


async def main():
    loop = asyncio.get_running_loop()
    audio = asyncio.Queue(maxsize=50)  # about 5 s of audio

    def enqueue(block):
        if audio.full():  # the network fell behind: drop the oldest block
            audio.get_nowait()
        audio.put_nowait(block)

    def on_audio(indata, frames, time_info, status):
        loop.call_soon_threadsafe(enqueue, bytes(indata))

    client = AsyncCartesia(api_key=os.environ["CARTESIA_API_KEY"])
    async with client.stt.auto_finalize.websocket(
        model="ink-2",
        encoding="pcm_s16le",
        sample_rate=SAMPLE_RATE,
    ) as ws:

        async def send():
            with sd.RawInputStream(
                samplerate=SAMPLE_RATE,
                blocksize=BLOCK,
                channels=1,
                dtype="int16",
                callback=on_audio,
            ):
                while True:
                    await ws.send_raw(await audio.get())

        async def receive():
            turn_text = ""
            async for event in ws:
                if event.type == "turn.start":
                    print("(listening)")
                elif event.type == "turn.update":
                    turn_text = event.transcript or turn_text
                elif event.type == "turn.end":
                    print("You said:", event.transcript or turn_text)
                    turn_text = ""
                elif event.type == "error":
                    raise RuntimeError(event.message)

        await asyncio.gather(send(), receive())


asyncio.run(main())

Ejecuta python microphone.py, di algo, haz una pausa y se imprime la transcripción de ese turno. Pulsa Ctrl+C para parar.

El callback de audio se ejecuta en otro hilo, así que pasa cada bloque al bucle de eventos con call_soon_threadsafe en lugar de tocar el socket directamente. El micrófono sigue enviando mientras estás callado, y eso importa: el servidor interpreta la falta de audio como un retraso de red, no como silencio, así que una entrada silenciada o en pausa debe enviar ceros en lugar de nada. La cola tiene un límite de unos cinco segundos de audio: si la conexión se retrasa, se descartan los bloques más antiguos en lugar de acumularse en memoria, porque la transcripción de algo dicho hace un minuto no le sirve a una conversación en vivo.

En un agente de voz, turn.start es donde haces que el bot deje de hablar, y turn.end es donde envías el texto a tu LLM. Para empezar la respuesta antes, gestiona también turn.eager_end: empieza a generar cuando llegue y descarta el borrador si le sigue turn.resume. La documentación de detección de turnos recoge los umbrales por si tus usuarios hacen muchas pausas (o casi ninguna).

Problemas habituales

SíntomaCausa probable y solución
La transcripción es ininteligible o vacíaencoding o sample_rate no coinciden con el audio. Guarda los bytes que envías y escúchalos con ffplay -f s16le -ar 16000 -ac 1 audio.raw
Las palabras aparecen dos o tres vecesUniste eventos turn.update. Usa solo turn.end
Falta el último segundo de vozCerraste el socket sin enviar {"type": "close"}, o dejaste de leer eventos antes de que el servidor cerrara
Los turnos no terminan con el micro silenciadoDejaste de enviar audio. Envía silencio (muestras PCM con valor cero)
Error del servidor a mitad de un archivoEnviaste audio más rápido que en tiempo real. Ajusta el ritmo de los fragmentos o usa el modo por lotes
”You need to install cartesia[websockets]”Instala el extra: python -m pip install "cartesia[websockets]"
El audio telefónico se transcribe malLas líneas telefónicas suelen llevar μ-law a 8 kHz. Declara encoding="pcm_mulaw" y sample_rate=8000 en lugar de convertir; consulta G.711 explicado

Cuánto cuesta

Los modelos locales cuestan cómputo y nada más. En Cartesia, Ink 2 usa 3 créditos por segundo de audio, y una hora de audio cuesta 0,39 $ en el plan Scale; los créditos del plan gratuito cubren aproximadamente una hora y 50 minutos de Ink 2. Las cifras actuales están en la página de precios.

Si prefieres no montar el pipeline tú mismo, Cartesia Managed Agents conecta Ink, un LLM y el texto a voz de Sonic a un número de teléfono por ti. Y si solo necesitas transcribir unas cuantas grabaciones, probablemente te baste con faster-whisper en tu portátil.

Próximos pasos

  1. Prueba Ink 2 con tu propia voz en el navegador, sin código, en la página de Ink.
  2. Pasa las transcripciones a un LLM y di la respuesta con texto a voz en Python.
  3. Junta las piezas en un framework: crea un agente de voz con Pipecat y Cartesia.

Documentación relacionada

Preguntas frecuentes

¿Cómo convierto voz a texto en Python?

Elige un modelo de reconocimiento de voz, envíale audio y recibe texto. Para un archivo grabado, un modelo local como faster-whisper o una API por lotes devuelve la transcripción completa en una sola llamada. Para audio en vivo, abre un WebSocket con un modelo de streaming como Cartesia Ink 2, envía fragmentos pequeños de PCM de 16 bits a medida que llegan y procesa los eventos de transcripción según vuelven.

¿Cuál es la mejor biblioteca de Python para reconocimiento de voz?

Depende del audio. Para archivos grabados en tu propia máquina, faster-whisper es una buena opción gratuita. Para un agente de voz o cualquier cosa en vivo, usa una API de streaming con detección de turnos, porque los modelos por lotes como Whisper esperan al clip completo. El paquete SpeechRecognition envuelve varios motores; su backend de Google por defecto usa una clave de prueba compartida que Google puede revocar, así que no lo lleves a producción.

¿Puede Python hacer voz a texto en tiempo real?

Sí, con un modelo de streaming. Captura el micrófono con una biblioteca como sounddevice, envía fragmentos de unos 100 milisegundos por un WebSocket y lee las transcripciones parciales y finales según llegan. Envía el audio a la velocidad a la que se habló; mandar un archivo entero de golpe puede saturar un endpoint en tiempo real.

¿Puedo hacer voz a texto en Python sin conexión?

Sí. faster-whisper y Vosk se ejecutan en local tras descargar el modelo una vez, así que el audio no sale de tu máquina. Tendrás que gestionar el tamaño del modelo, la velocidad de CPU o GPU y tu propia lógica de streaming. Las API alojadas como la de Cartesia necesitan conexión y envían el audio al proveedor.

¿Qué formato de audio necesita la conversión de voz a texto?

Los endpoints por lotes suelen aceptar archivos comunes como WAV y MP3. Los endpoints en tiempo real suelen pedir muestras crudas sin cabecera de archivo, y debes declarar la codificación y la frecuencia de muestreo. El PCM mono de 16 bits a 16 kHz es la opción segura para voz. El audio telefónico suele ser μ-law a 8 kHz.