Transcripción de Audio con OpenAI Whisper y FastAPI: Construyendo una API Inteligente y Escalable
La capacidad de convertir voz a texto es una funcionalidad fundamental en muchas aplicaciones modernas. Desde asistentes virtuales y transcripción de reuniones hasta análisis de contenido multimedia y herramientas de accesibilidad, la demanda de soluciones precisas y eficientes es constante. Tradicionalmente, desarrollar un sistema de transcripción de voz ha sido un desafío complejo, pero modelos avanzados como OpenAI Whisper han democratizado esta capacidad, poniéndola al alcance de los desarrolladores.
Como desarrolladores, necesitamos integrar estas potentes capacidades en nuestras aplicaciones de manera eficiente y robusta. Este artículo te guiará paso a paso para construir una API inteligente con FastAPI que utilice OpenAI Whisper para transcribir archivos de audio, enfocándonos en la escalabilidad y la buena gestión de recursos. Aprenderás a manejar la subida de archivos de forma asíncrona, a interactuar con la API de Whisper y a utilizar las tareas en segundo plano de FastAPI para mantener tu API responsiva.
Contexto del Problema
Imagina que necesitas procesar grabaciones de reuniones, podcasts o notas de voz para extraer texto. Un enfoque síncrono, donde el cliente espera a que la transcripción se complete, puede llevar a tiempos de espera inaceptables, especialmente con archivos de audio largos. Esto no solo degrada la experiencia del usuario, sino que también puede bloquear los recursos del servidor. La solución ideal es una API que acepte el archivo de audio, confirme su recepción y luego procese la transcripción en segundo plano, permitiendo al cliente consultar el resultado más tarde.
Conceptos Clave
- OpenAI Whisper: Es un modelo de IA de código abierto (y disponible a través de la API de OpenAI) entrenado en una vasta cantidad de datos de audio multilingües. Es capaz de transcribir voz a texto con alta precisión y detectar el idioma automáticamente. La API de Whisper soporta varios formatos de audio como
mp3,mp4,mpeg,mpga,m4a,wavywebm, con un límite de tamaño de archivo de 25 MB por defecto. [1, 2, 13, 14] - FastAPI: Un framework web moderno y rápido para construir APIs con Python. Es conocido por su rendimiento, facilidad de uso y soporte nativo para programación asíncrona, lo que lo hace ideal para aplicaciones de IA.
- Programación Asíncrona (AsyncIO): Permite que tu aplicación maneje múltiples operaciones de E/S (entrada/salida), como subir archivos o hacer llamadas a APIs externas, sin bloquear el hilo principal. Esto mejora significativamente la capacidad de respuesta y la concurrencia de tu API.
- Background Tasks en FastAPI: Una característica crucial que permite ejecutar funciones en segundo plano después de que la API ha enviado una respuesta al cliente. Esto es perfecto para tareas que consumen tiempo, como la transcripción de audio, ya que evita que el cliente tenga que esperar a que la operación finalice. [4, 5, 10]
- Manejo de Archivos con
UploadFileyaiofiles: FastAPI utiliza la claseUploadFilepara manejar archivos subidos, la cual es un wrapper alrededor deSpooledTemporaryFile. Esto significa que los archivos se almacenan en memoria hasta un cierto umbral y luego se escriben en disco, optimizando el uso de recursos. Para operaciones de escritura y lectura de archivos de forma asíncrona, utilizaremos la libreríaaiofiles. [17, 20, 23]
Implementación Paso a Paso
1. Configuración del Entorno
Primero, crea un entorno virtual y activa. Luego, instala las dependencias necesarias:
pip install fastapi uvicorn openai python-dotenv aiofiles
Crea un archivo .env en la raíz de tu proyecto para almacenar tu API Key de OpenAI de forma segura:
OPENAI_API_KEY="tu_api_key_de_openai_aqui"
¡Importante! Nunca expongas tu API Key directamente en el código fuente ni la subas a repositorios públicos. Usa siempre variables de entorno. [14, 21]
2. Estructura Básica de la API con FastAPI
Crearemos un archivo main.py. Necesitaremos importar FastAPI, UploadFile, File, BackgroundTasks y otras utilidades.
import os
import uuid
import shutil
from typing import Dict
import aiofiles
from fastapi import FastAPI, UploadFile, File, BackgroundTasks, HTTPException, status
from openai import OpenAI
from dotenv import load_dotenv
# Cargar variables de entorno
load_dotenv()
app = FastAPI()
# Configuración de OpenAI
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
if not OPENAI_API_KEY:
raise ValueError("La variable de entorno OPENAI_API_KEY no está configurada.")
client = OpenAI(api_key=OPENAI_API_KEY)
# Directorio para guardar archivos temporales
UPLOAD_DIR = "./temp_audio_uploads"
os.makedirs(UPLOAD_DIR, exist_ok=True)
# Diccionario para almacenar el estado de las tareas de transcripción
transcription_tasks: Dict[str, Dict] = {}
# Formatos de audio soportados por OpenAI Whisper
SUPPORTED_AUDIO_TYPES = [
"audio/mpeg", # mp3
"audio/mp4", # mp4, m4a
"audio/wav",
"audio/webm",
"audio/ogg",
"video/mp4", # para archivos mp4 que contienen audio
"video/webm" # para archivos webm que contienen audio
]
MAX_FILE_SIZE_MB = 25 # Límite de 25MB para la API de Whisper [1, 2]
async def transcribe_audio_task(task_id: str, file_path: str):
"""Tarea de fondo para transcribir audio con OpenAI Whisper."""
try:
with open(file_path, "rb") as audio_file:
transcription = client.audio.transcriptions.create(
model="whisper-1",
file=audio_file
)
transcription_tasks[task_id]["status"] = "completed"
transcription_tasks[task_id]["result"] = transcription.text
except Exception as e:
transcription_tasks[task_id]["status"] = "failed"
transcription_tasks[task_id]["error"] = str(e)
finally:
# Limpiar el archivo temporal después de la transcripción
if os.path.exists(file_path):
os.remove(file_path)
@app.post("/transcribe/")
async def upload_audio_for_transcription(
file: UploadFile = File(..., description=f"Archivo de audio para transcribir (máx. {MAX_FILE_SIZE_MB}MB)"),
background_tasks: BackgroundTasks = BackgroundTasks()
):
"""Sube un archivo de audio y programa su transcripción con OpenAI Whisper.
Retorna un ID de tarea para consultar el estado de la transcripción.
"""
# 1. Validación del tipo de archivo
if file.content_type not in SUPPORTED_AUDIO_TYPES:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Tipo de archivo no soportado. Tipos permitidos: {', '.join(SUPPORTED_AUDIO_TYPES)}"
)
# 2. Generar un nombre de archivo único y ruta temporal
file_extension = file.filename.split(".")[-1] if "." in file.filename else "tmp"
unique_filename = f"{uuid.uuid4()}.{file_extension}"
temp_file_path = os.path.join(UPLOAD_DIR, unique_filename)
# 3. Guardar el archivo de forma asíncrona y validar tamaño
file_size = 0
try:
async with aiofiles.open(temp_file_path, "wb") as out_file:
while content := await file.read(1024 * 1024): # Leer en chunks de 1MB
file_size += len(content)
if file_size > MAX_FILE_SIZE_MB * 1024 * 1024:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"El archivo excede el límite de {MAX_FILE_SIZE_MB}MB para la API de Whisper."
)
await out_file.write(content)
except HTTPException as e:
# Si hay un error de tamaño, eliminar el archivo parcial
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
raise e
except Exception as e:
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error al guardar el archivo: {str(e)}"
)
# 4. Crear una tarea de transcripción y añadirla a las tareas de fondo
task_id = str(uuid.uuid4())
transcription_tasks[task_id] = {
"status": "pending",
"filename": file.filename,
"file_path": temp_file_path,
"result": None,
"error": None
}
background_tasks.add_task(transcribe_audio_task, task_id, temp_file_path)
return {"message": "Transcripción iniciada", "task_id": task_id}
@app.get("/transcription-status/{task_id}")
async def get_transcription_status(task_id: str):
"""Consulta el estado y el resultado de una tarea de transcripción."""
task = transcription_tasks.get(task_id)
if not task:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ID de tarea no encontrado.")
if task["status"] == "completed":
# Una vez completada, se puede eliminar la tarea del diccionario si no se necesita persistencia
# del historial de tareas en memoria.
# del transcription_tasks[task_id]
return {"status": task["status"], "result": task["result"]}
elif task["status"] == "failed":
return {"status": task["status"], "error": task["error"]}
else:
return {"status": task["status"], "message": "Transcripción en progreso o pendiente."}
@app.get("/health")
async def health_check():
return {"status": "ok", "message": "API is running"}
3. Ejecutar la Aplicación
Guarda el código anterior como main.py y ejecuta tu aplicación FastAPI con Uvicorn:
uvicorn main:app --reload
Ahora puedes acceder a la documentación interactiva de tu API en http://127.0.0.1:8000/docs.
Mini Proyecto / Aplicación Sencilla
El código proporcionado ya implementa una aplicación sencilla con dos endpoints clave:
POST /transcribe/: Permite subir un archivo de audio. La API lo valida, lo guarda temporalmente y luego inicia la transcripción con OpenAI Whisper en segundo plano. Inmediatamente devuelve untask_idal cliente, sin hacerlo esperar por la transcripción completa.GET /transcription-status/{task_id}: Con este endpoint, el cliente puede usar eltask_idrecibido para consultar el estado de su transcripción. Una vez que la tarea se completa, recibirá el texto transcrito.
Probando la API
Puedes usar la interfaz de Swagger UI (http://127.0.0.1:8000/docs) para probar los endpoints. Para el endpoint /transcribe/, selecciona un archivo de audio (.mp3, .wav, etc.) de menos de 25MB. Después de enviarlo, recibirás un task_id. Luego, usa ese task_id en el endpoint /transcription-status/{task_id} para ver el progreso y el resultado.
Alternativamente, puedes usar curl o una librería HTTP como httpx en Python:
import httpx
import asyncio
async def test_transcription_api():
audio_file_path = "./sample_audio.mp3" # Asegúrate de tener un archivo de audio aquí
# Crear un archivo de audio de ejemplo si no existe (solo para demostración)
# En un entorno real, usarías un archivo existente.
if not os.path.exists(audio_file_path):
print(f"Creando archivo de audio de ejemplo: {audio_file_path}")
# Esto es solo un placeholder, en la realidad necesitarías un archivo de audio real.
# Puedes grabar tu voz o descargar un pequeño mp3.
with open(audio_file_path, "wb") as f:
f.write(b"Simulated audio content") # Esto NO es un archivo de audio real
print("¡ADVERTENCIA! El archivo de audio de ejemplo no es un MP3 válido. Por favor, reemplázalo con un archivo MP3 real para probar la transcripción.")
async with httpx.AsyncClient() as client:
print(f"Subiendo archivo {audio_file_path}...")
with open(audio_file_path, "rb") as f:
files = {'file': (os.path.basename(audio_file_path), f, 'audio/mpeg')}
response = await client.post("http://127.0.0.1:8000/transcribe/", files=files, timeout=60.0)
print(f"Respuesta de subida: {response.status_code} - {response.json()}")
if response.status_code == 200:
task_id = response.json().get("task_id")
if task_id:
print(f"Transcripción iniciada. ID de tarea: {task_id}")
while True:
await asyncio.sleep(5) # Esperar 5 segundos antes de consultar de nuevo
status_response = await client.get(f"http://127.0.0.1:8000/transcription-status/{task_id}")
status_data = status_response.json()
print(f"Estado de la tarea {task_id}: {status_data}")
if status_data.get("status") in ["completed", "failed"]:
break
else:
print("No se recibió un task_id.")
else:
print("Error al iniciar la transcripción.")
# Para ejecutar la función de prueba:
# asyncio.run(test_transcription_api())
Nota: Para que el script de prueba funcione correctamente, debes reemplazar "./sample_audio.mp3" con la ruta a un archivo de audio MP3 real y válido en tu sistema. El contenido simulado b"Simulated audio content" no es un MP3 funcional y solo sirve para evitar errores de archivo no encontrado.
Errores Comunes y Depuración
ValueError: OPENAI_API_KEY no está configurada.: Asegúrate de que tu archivo.envesté en la raíz del proyecto y contengaOPENAI_API_KEY="tu_clave", y queload_dotenv()se ejecute correctamente.HTTPException 400: Tipo de archivo no soportado.: Verifica que el archivo que estás subiendo sea uno de los formatos soportados por OpenAI Whisper (mp3,wav,mp4, etc.) y que elContent-Typede la petición sea correcto. [2, 16]HTTPException 413: El archivo excede el límite de 25MB.: La API de Whisper tiene un límite de 25MB por archivo. Si necesitas transcribir archivos más grandes, deberás dividirlos en segmentos más pequeños antes de subirlos. [1, 6]openai.AuthenticationError: Tu API Key de OpenAI es inválida o ha caducado. Revisa tu clave en el panel de OpenAI.- Archivos temporales no eliminados: Aunque la tarea de fondo incluye una limpieza, en caso de fallos inesperados, los archivos podrían persistir. Considera implementar una tarea de limpieza periódica para el directorio
UPLOAD_DIR. - Errores de red/conexión: Las llamadas a APIs externas pueden fallar. Implementa reintentos (retries) o un manejo de errores más sofisticado para la llamada a
client.audio.transcriptions.create.
Aprendizaje Futuro
Esta implementación es un excelente punto de partida, pero hay muchas formas de expandirla y mejorarla para un entorno de producción:
- Streaming de Audio: Explorar cómo transcribir audio en tiempo real a medida que se recibe, aunque la API de Whisper actualmente procesa archivos completos. Esto podría implicar el uso de otras librerías o servicios de streaming de voz. [2]
- Persistencia de Tareas: Actualmente, el estado de las tareas se guarda en un diccionario en memoria (
transcription_tasks). Para una aplicación robusta, deberías persistir esta información en una base de datos (SQL, NoSQL) o un caché (Redis) para que las tareas sobrevivan a reinicios del servidor. - Colas de Mensajes: Para un procesamiento de tareas en segundo plano más robusto y escalable, integra una cola de mensajes como Celery con RabbitMQ o Redis. Esto desacoplará la recepción de la petición del procesamiento de la transcripción, permitiendo manejar picos de carga.
- Procesamiento de Audio Avanzado: Implementar funcionalidades como detección de oradores (diarización), segmentación de audio o eliminación de ruido antes de la transcripción.
- Despliegue en Producción: Dockerizar la aplicación y desplegarla en plataformas en la nube como Railway, Render, Google Cloud Run, AWS ECS o Kubernetes. Asegúrate de configurar correctamente las variables de entorno y los volúmenes para los archivos temporales. [23]
- Manejo de Archivos Grandes: Para archivos de audio que exceden los 25MB, implementa una lógica para dividirlos en segmentos más pequeños, transcribir cada segmento y luego unir las transcripciones. Librerías como
pydubpueden ser útiles para esto. [6]
Con esta guía, tienes las herramientas y el conocimiento para construir tu propia API de transcripción de audio con FastAPI y OpenAI Whisper, abriendo un mundo de posibilidades para tus aplicaciones de IA.