En la era de la nube y los servicios de suscripción costosos, la filosofía del software libre y el desarrollo local nos invita a recuperar el control de nuestras herramientas. Bajo esta premisa , me propuse un reto técnico: montar un estudio de locución y síntesis de voz por inteligencia artificial completamente local, privado y automatizado, sin depender de APIs de pago ni de servidores externos.
Después de experimentar con diversas configuraciones, hoy comparto la guía definitiva que me funcionó de manera infalible en producción.
1. El Banco de Pruebas (Especificaciones de Hardware y Entorno)
Para replicar este entorno con éxito, es fundamental conocer la arquitectura sobre la cual testeamos y validamos este pipeline:
- Sistema Host: Windows 11.
- Capa de Virtualización: Subsistema de Windows para Linux (WSL2).
- Entorno Invitado (Guest): Ubuntu 22.04 LTS (que provee nativamente Python 3.10).
- Aceleración Gráfica (GPU): Tarjeta gráfica NVIDIA GeForce GTX 1060 con 6 GB de VRAM y controladores actualizados con soporte WDDM.
Ajuste preventivo de memoria en WSL2
Para evitar que la IA desborde los recursos de tu equipo durante la inferencia en la GPU, es altamente recomendable limitar la memoria de WSL. Crea un archivo llamado .wslconfig en tu ruta de usuario de Windows (%userprofile%) con estos valores:
[wsl2]
memory=6GB
swap=4GB
Aplica los cambios abriendo tu terminal en Windows y ejecutando: wsl --shutdown
2. Preparando el Entorno de Trabajo en Ubuntu (WSL2)
Inicia tu terminal de Ubuntu 22.04 y actualiza los paquetes base del sistema, asegurando las herramientas de compilación y manipulación de audio:
sudo apt update && sudo apt upgrade -y
sudo apt install -y python3-pip python3-venv git ffmpeg
Crea un directorio exclusivo para tu proyecto y aísla las dependencias en un entorno virtual (venv):
mkdir ~/podcast_ia
cd ~/podcast_ia
python3 -m venv podcast_env
¿Cómo iniciar sesión en cada nueva sesión de trabajo?
Cada vez que enciendas tu computadora o cierres la terminal y quieras volver a trabajar en tus pódcasts, recuerda seguir estos pasos para activar tu entorno virtual:
1. Entra a la carpeta del proyecto:
cd ~/podcast_ia
2. Activa el entorno (env):
source podcast_env/bin/activate
(Verás que la terminal antepone (podcast_env) en la línea de comandos).
3. Una vez terminado tu trabajo, puedes salir del entorno simplemente escribiendo: deactivate.
3. Instalación de Dependencias de IA (PyTorch y Coqui TTS)
Dentro de tu entorno virtual activo, instala PyTorch configurado específicamente para el soporte de CUDA (versión 11.8, óptima para la arquitectura de la GTX 1060), seguido por el motor de Coqui y utilidades de audio:
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install TTS==0.22.0 transformers==4.33.0 pydub
Nota técnica previa: Para que el motor neuronal capture con absoluta fidelidad los matices de tu voz y evite distorsiones, artefactos o lecturas erróneas de puntuación, es indispensable alimentar el sistema con una muestra de audio perfectamente calibrada. Si deseas profundizar en el proceso de captura, el tiempo de duración ideal y las especificaciones exactas para exportar tu muestra limpia en Audacity, puedes consultar mi guía detallada en el post sobre la Generación de archivo .WAV de audio para entrenamiento de voz TTS.
4. El Script Básico: Síntesis Lineal con Archivo de Referencia .wav
Para nuestra primera versión funcional del script, mantendremos las cosas simples. Este programa básico toma un archivo de texto plano (guion.txt) y un archivo de audio con tu propia voz (referencia.wav), aplica el parche de seguridad indispensable para versiones recientes de PyTorch, procesa el texto eliminando los puntos finales problemáticos y une los fragmentos inyectando silencios limpios mediante Pydub.
Crea un archivo llamado estudio_basico.py y pega el siguiente código:
"""
=============================================================================
ESTUDIO BÁSICO DE PÓDCAST IA - CLONACIÓN ZERO-SHOT CON ARCHIVO WAV
Descripción: Script lineal y directo para generar locuciones a partir de
un guion en texto plano y una muestra de voz personalizada.
=============================================================================
"""
import os
import torch
import re
from TTS.api import TTS
from pydub import AudioSegment
# --- 1. PARCHE DE SEGURIDAD PARA PYTORCH 2.6+ ---
# Permite cargar de forma segura los pesos locales del modelo de Coqui.
original_load = torch.load
def parche_load(*args, **kwargs):
kwargs['weights_only'] = False
return original_load(*args, **kwargs)
torch.load = parche_load
os.environ["COQUI_TOS_AGREED"] = "1"
def cargar_guion_simple(ruta_archivo):
"""
Lee el guion línea por línea, eliminando puntos finales para evitar
que la IA verbalice la palabra 'punto'.
"""
if not os.path.exists(ruta_archivo):
return None
with open(ruta_archivo, 'r', encoding='utf-8') as f:
lineas = f.readlines()
parrafos_limpios = []
for linea in lineas:
texto = linea.strip()
if not texto or texto.startswith(('#', '[')):
continue
# Purgado de caracteres raros y eliminación del punto final
texto = texto.replace('\xa0', ' ').replace('\t', ' ')
texto = texto.rstrip('.')
texto = re.sub(r'\s+', ' ', texto).strip()
if texto:
parrafos_limpios.append(texto)
return parrafos_limpios
def main():
print("\n=== ESTUDIO BÁSICO DE PÓDCAST (LOCAL) ===")
# Detección de hardware (GPU NVIDIA)
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Hardware en uso: {device.upper()}")
# 1. Carga del guion
archivo_entrada = input("Ingresa el nombre de tu archivo de guion (ej. guion.txt): ").strip()
parrafos = cargar_guion_simple(archivo_entrada)
if parrafos is None:
print(f"❌ Error: No se encontró el archivo '{archivo_entrada}'.")
return
print(f"✅ Guion cargado: {len(parrafos)} bloques listos.")
# 2. Verificación del archivo de voz de referencia
audio_referencia = "referencia.wav"
if not os.path.exists(audio_referencia):
print(f"❌ Error crítico: No se encuentra el archivo '{audio_referencia}' en este directorio.")
print("Coloca tu muestra de voz en formato .wav (Mono, 22050Hz) antes de continuar.")
return
# 3. Carga del motor XTTSv2 en la GPU
print("\nCargando motor XTTSv2 en la GPU...")
tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(device)
# --- PARÁMETROS DE CALIBRACIÓN ACÚSTICA ---
CONFIG_VELOCIDAD = 0.85
CONFIG_TEMPERATURA = 0.65
CONFIG_PENALIZACION = 2.0
print("\n🚀 Iniciando síntesis de audio...")
audio_maestro = AudioSegment.empty()
for i, p in enumerate(parrafos):
print(f"[{i+1}/{len(parrafos)}] Procesando fragmento...")
temp_file = f"temp_chunk_{i}.wav"
# Inferencia utilizando tu archivo de voz personalizado
tts.tts_to_file(
text=p,
language="es",
speaker_wav=audio_referencia,
file_path=temp_file,
split_sentences=False,
speed=CONFIG_VELOCIDAD,
temperature=CONFIG_TEMPERATURA,
repetition_penalty=CONFIG_PENALIZACION
)
# Ensamblaje con pausa natural de medio segundo entre frases
chunk_audio = AudioSegment.from_wav(temp_file)
silencio = AudioSegment.silent(duration=500)
audio_maestro += chunk_audio + silencio
os.remove(temp_file)
archivo_final = "episodio_maestro.wav"
audio_maestro.export(archivo_final, format="wav")
print(f"\n🎉 ¡Proceso finalizado! Audio guardado con éxito como: '{archivo_final}'")
if __name__ == "__main__":
main()
5. Evolucionando al Siguiente Nivel: El Script Avanzado (estudio_voces.py)
Una vez que dominas la síntesis básica con tu archivo de referencia, es probable que quieras un flujo de trabajo más versátil: poder elegir entre distintas voces oficiales integradas en el modelo, calibrar los parámetros de velocidad y temperatura al vuelo, y nombrar tus episodios personalizados sin el riesgo de perder producciones anteriores por sobrescritura accidental.
Para lograrlo, creamos estudio_voces.py. Este script interactivo incluye un gestor de nombres seguro y un menú de selección múltiple.
Crea este archivo en tu directorio de trabajo y pega el código completo:
"""
=============================================================================
ESTUDIO AVANZADO DE PÓDCAST IA - SELECTOR DE VOCES Y GESTIÓN INTELIGENTE
Descripción: Script interactivo para la generación automatizada de locuciones
con opciones de voces oficiales, purga de puntuación y protección
de archivos.
=============================================================================
"""
import os
import torch
import re
from TTS.api import TTS
from pydub import AudioSegment
# --- 1. PARCHE DE SEGURIDAD PARA PYTORCH 2.6+ ---
original_load = torch.load
def parche_load(*args, **kwargs):
kwargs['weights_only'] = False
return original_load(*args, **kwargs)
torch.load = parche_load
os.environ["COQUI_TOS_AGREED"] = "1"
# --- 2. INGESTA Y LIMPIEZA QUIRÚRGICA DEL GUION ---
def cargar_guion_por_parrafos(ruta_archivo):
"""
Lee el guion línea por línea, limpiando caracteres especiales y
eliminando puntos finales para evitar la lectura fonética de 'punto'.
"""
if not os.path.exists(ruta_archivo):
return None
with open(ruta_archivo, 'r', encoding='utf-8') as f:
lineas = f.readlines()
parrafos_limpios = []
for linea in lineas:
texto = linea.strip()
if not texto or texto.startswith(('#', '[')):
continue
texto = texto.replace('\xa0', ' ').replace('\t', ' ')
texto = re.sub(r'\[.*?\]', '', texto)
texto = texto.rstrip('.') # Truco antipunto final
texto = re.sub(r'\.{2,}', ',', texto)
texto = re.sub(r'\s+', ' ', texto).strip()
if texto:
parrafos_limpios.append(texto)
return parrafos_limpios
# --- 3. GESTOR INTELIGENTE DE NOMBRES DE ARCHIVO ---
def obtener_nombre_archivo_seguro(nombre_base):
"""
Previene la sobrescritura de archivos agregando un sufijo numérico
autoincremental si el nombre de salida ya existe.
"""
if not nombre_base.endswith(".wav"):
nombre_base += ".wav"
if not os.path.exists(nombre_base):
return nombre_base
base, ext = os.path.splitext(nombre_base)
contador = 1
while True:
nuevo_nombre = f"{base}_{contador}{ext}"
if not os.path.exists(nuevo_nombre):
return nuevo_nombre
contador += 1
# --- 4. FLUJO PRINCIPAL AVANZADO ---
def main():
print("\n==========================================================")
print("=== ESTUDIO DE PÓDCAST IA - SELECTOR DE VOZ & AVANZADO ===")
print("==========================================================\n")
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Hardware detectado: {device.upper()}")
if device == "cuda":
print(f"Tarjeta gráfica en uso: {torch.cuda.get_device_name(0)}")
# Paso 1: Carga del texto
archivo_entrada = input("\n[1/5] Ingresa el nombre de tu archivo de guion (ej. guion.txt): ").strip()
parrafos = cargar_guion_por_parrafos(archivo_entrada)
if parrafos is None:
print(f"❌ Error: No se encontró el archivo '{archivo_entrada}'.")
return
print(f"✅ Guion cargado: {len(parrafos)} bloques purgados y listos.")
# Paso 2: Elección de fuente de voz (Oficiales del sistema vs. Archivo WAV)
print("\n[2/5] Selecciona la fuente de voz para la locución:")
print("[1] Voces oficiales del sistema (Luis Moray, Marcos Rudaski, Alma María)")
print("[2] Usar mi propio archivo de audio de referencia (.wav personalizado)")
tipo_voz = input("Elige una opción (1 o 2) [Por defecto 1]: ").strip()
if tipo_voz == "":
tipo_voz = "1"
speaker_id = None
speaker_wav_path = None
if tipo_voz == '1':
print("\n--- Voces Oficiales del Sistema ---")
print("[A] Luis Moray (Masculino - Predeterminado)")
print("[B] Marcos Rudaski (Masculino alternativo)")
print("[C] Alma María (Femenino)")
eleccion = input("Elige tu locutor (A, B o C) [Por defecto A]: ").strip().upper()
if eleccion == 'B':
speaker_id = "Marcos Rudaski"
elif eleccion == 'C':
speaker_id = "Alma María"
else:
speaker_id = "Luis Moray"
print(f"🎙️ Locutor seleccionado: {speaker_id}")
else:
nombre_wav = input("Ingresa el nombre de tu archivo de referencia [Por defecto 'referencia.wav']: ").strip()
if nombre_wav == "":
nombre_wav = "referencia.wav"
if not os.path.exists(nombre_wav):
print(f"❌ Error: No se encuentra el archivo '{nombre_wav}'.")
return
speaker_wav_path = nombre_wav
print(f"🎙️ Archivo de referencia seleccionado: {speaker_wav_path}")
# Paso 3: Calibración acústica interactiva
print("\n[3/5] Calibración acústica (Presiona Enter para usar los valores por defecto):")
vel_input = input(" -> Velocidad [Por defecto 0.85]: ").strip()
CONFIG_VELOCIDAD = float(vel_input) if vel_input else 0.85
temp_input = input(" -> Temperatura [Por defecto 0.65]: ").strip()
CONFIG_TEMPERATURA = float(temp_input) if temp_input else 0.65
pen_input = input(" -> Penalización de repetición [Por defecto 2.0]: ").strip()
CONFIG_PENALIZACION = float(pen_input) if pen_input else 2.0
# Paso 4: Modo de exportación
print("\n[4/5] ¿Cómo deseas exportar el audio final?")
print("[1] Un solo archivo maestro continuo (con pausas automáticas)")
print("[2] Múltiples archivos independientes (un .wav por cada párrafo)")
opcion_exp = input("Elige una opción (1 o 2) [Por defecto 1]: ").strip()
if opcion_exp == "":
opcion_exp = "1"
# Paso 5: Nombramiento inteligente del archivo
nombre_personalizado = input("\n[5/5] ¿Cómo quieres nombrar tu archivo de salida? [Por defecto 'episodio_maestro.wav']: ").strip()
if not nombre_personalizado:
nombre_personalizado = "episodio_maestro.wav"
archivo_final = obtener_nombre_archivo_seguro(nombre_personalizado)
if archivo_final != nombre_personalizado:
print(f"ℹ️ El archivo '{nombre_personalizado}' ya existía. Se guardará de forma segura como: '{archivo_final}'")
# Carga del motor en la GPU
print("\nCargando motor XTTSv2 en la GPU (esto tomará unos segundos)...")
tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(device)
print(f"\n🚀 Iniciando síntesis por lotes...\n")
if opcion_exp == '1':
audio_maestro = AudioSegment.empty()
for i, p in enumerate(parrafos):
print(f"[{i+1}/{len(parrafos)}] Procesando párrafo: {p[:50]}...")
temp_file = f"temp_chunk_{i}.wav"
if speaker_id:
tts.tts_to_file(
text=p, language="es", speaker=speaker_id, file_path=temp_file,
split_sentences=False, speed=CONFIG_VELOCIDAD, temperature=CONFIG_TEMPERATURA, repetition_penalty=CONFIG_PENALIZACION
)
else:
tts.tts_to_file(
text=p, language="es", speaker_wav=speaker_wav_path, file_path=temp_file,
split_sentences=False, speed=CONFIG_VELOCIDAD, temperature=CONFIG_TEMPERATURA, repetition_penalty=CONFIG_PENALIZACION
)
chunk_audio = AudioSegment.from_wav(temp_file)
silencio = AudioSegment.silent(duration=500) # Pausa natural de respiración
audio_maestro += chunk_audio + silencio
os.remove(temp_file)
audio_maestro.export(archivo_final, format="wav")
print(f"\n🎉 ¡Proceso finalizado! Tu pódcast maestro está guardado como: '{archivo_final}'")
elif opcion_exp == '2':
carpeta_salida = os.path.splitext(archivo_final)[0] + "_fragmentos"
os.makedirs(carpeta_salida, exist_ok=True)
for i, p in enumerate(parrafos):
print(f"[{i+1}/{len(parrafos)}] Procesando párrafo: {p[:50]}...")
out_file = f"{carpeta_salida}/bloque_{i+1:03d}.wav"
if speaker_id:
tts.tts_to_file(
text=p, language="es", speaker=speaker_id, file_path=out_file,
split_sentences=False, speed=CONFIG_VELOCIDAD, temperature=CONFIG_TEMPERATURA, repetition_penalty=CONFIG_PENALIZACION
)
else:
tts.tts_to_file(
text=p, language="es", speaker_wav=speaker_wav_path, file_path=out_file,
split_sentences=False, speed=CONFIG_VELOCIDAD, temperature=CONFIG_TEMPERATURA, repetition_penalty=CONFIG_PENALIZACION
)
print(f"\n🎉 ¡Proceso finalizado! Fragmentos independientes guardados en: '{carpeta_salida}/'")
if __name__ == "__main__":
main()
Conclusión
Con esta infraestructura montada en tu equipo, cuentas con un estudio de locución automatizado de nivel profesional. Al correr localmente sobre tu propia GPU, garantizas la privacidad absoluta de tus contenidos, controlas los tiempos de renderizado y eliminas por completo los costos recurrentes de servicios en la nube.
Aquí demostramos que, combinando código abierto, herramientas de Linux en Windows (me habría gustado haber trabajado nativamente sobre GNU/Linux pero, no podía, ya que hice esto en una laptop prestada) y un procesamiento inteligente de textos, podemos crear soluciones potentes y elegantes. ¡Es momento de exportar tus guiones y comenzar a producir tus propios podcasts!
Deja un comentario