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
| Tienes | Usa | Por qué |
|---|---|---|
| Archivos grabados, el audio no puede salir | faster-whisper | Gratis, local y preciso con audio limpio. Solo por lotes |
| Archivos grabados, sin GPU que gestionar | Una API de STT por lotes | Una llamada HTTP por archivo |
| Audio en vivo: un agente, subtítulos, un teléfono | Una API de STT en streaming con detección de turnos | El texto llega mientras la persona habla, y el modelo dice cuándo termina |
| Un script de fin de semana | El paquete SpeechRecognition | Rá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 3.9 o posterior
- Para los ejemplos de Cartesia, una clave de API del playground de Cartesia y el SDK:
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
encodingysample_rateal 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.updatecontiene todo el turno hasta ese momento, no un fragmento nuevo. Concatenar actualizaciones repite palabras. Toma el texto deturn.end, una vez por turno, y únelo tal como llega. El script también guarda el últimoturn.updatey lo usa si alguna vez llega unturn.endcon 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íntoma | Causa probable y solución |
|---|---|
| La transcripción es ininteligible o vacía | encoding 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 veces | Uniste eventos turn.update. Usa solo turn.end |
| Falta el último segundo de voz | Cerraste el socket sin enviar {"type": "close"}, o dejaste de leer eventos antes de que el servidor cerrara |
| Los turnos no terminan con el micro silenciado | Dejaste de enviar audio. Envía silencio (muestras PCM con valor cero) |
| Error del servidor a mitad de un archivo | Enviaste 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 mal | Las 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
- Prueba Ink 2 con tu propia voz en el navegador, sin código, en la página de Ink.
- Pasa las transcripciones a un LLM y di la respuesta con texto a voz en Python.
- Junta las piezas en un framework: crea un agente de voz con Pipecat y Cartesia.
Documentación relacionada
- Guía rápida de voz a texto en tiempo real
- Comparar endpoints de STT: automático, manual y por lotes
- Keyterm prompting para nombres y jerga que el modelo debe escribir bien
- Detección de actividad de voz, y por qué la detección de turnos la sustituye