🎬 Tutorial Completo: Generador de SRT con ElevenLabs Forced Alignment

Este tutorial te explica paso a paso cómo usar el script para generar archivos SRT (subtítulos) automáticamente usando la API de Forced Alignment de ElevenLabs.

📋 Requisitos Previos

1. Cuenta de ElevenLabs

2. API Key

3. Python y Dependencias

Para instalar la librería de ElevenLabs en Python, ejecuta el siguiente comando en tu terminal:

pip install elevenlabs

🚀 Preparación de Archivos

Estructura de Carpeta

Crea una carpeta de trabajo con esta estructura:

mi_proyecto/
├── audio.mp3           # Tu archivo de audio
├── texto.txt           # Tu texto línea por línea
└── generar_srt.py      # El script

Formato del archivo texto.txt

Cada línea en este archivo será un segmento de subtítulo separado en el SRT final. Asegúrate de que el texto coincida con lo que se dice en el audio.

Bienvenidos a nuestro tutorial de hoy
En este video aprenderemos sobre inteligencia artificial
La IA está transformando el mundo moderno
Espero que disfruten este contenido educativo

Archivo de audio audio.mp3

⚙️ Configuración del Script

1. Descargar el Script

Copia el código completo del script proporcionado al final de este tutorial y guárdalo como generar_srt.py dentro de tu carpeta mi_proyecto.

2. Configurar API Key

Abre el archivo generar_srt.py con un editor de texto y busca esta línea:

ELEVENLABS_API_KEY = "tu_api_key_aqui"  # ⬅️ REEMPLAZA ESTO CON TU API KEY REAL

Reemplázala con tu API key real de ElevenLabs (la que copiaste en el paso de requisitos):

ELEVENLABS_API_KEY = "sk_1234567890abcdef..."  # ⬅️ Tu API key aquí

🎯 Ejecución del Script

1. Navegar a la Carpeta

Abre tu terminal o símbolo del sistema y navega a la carpeta de tu proyecto:

cd mi_proyecto

2. Verificar Archivos

Asegúrate de tener los tres archivos necesarios en la carpeta mi_proyecto:

3. Ejecutar Script

Ejecuta el script de Python:

python generar_srt.py

4. Salida Esperada

Verás una salida similar a esta en tu terminal:


🎬 Generador de SRT con ElevenLabs Forced Alignment
==================================================
📝 Procesando 4 líneas de texto...
📄 Texto completo: Bienvenidos a nuestro tutorial de hoy En este video aprenderemos...
🎵 Archivo de audio: audio.mp3
🔑 API Key configurada: sk_12345...
🚀 Enviando solicitud a ElevenLabs API...
 ¡Forced alignment completado!
📊 Tipo de respuesta: <class 'elevenlabs.types.ForcedAlignmentResponse'>
📝 Palabras encontradas: 32
 ¡Archivo 'subtitulos.srt' creado exitosamente!
📁 Ubicación: /ruta/completa/mi_proyecto/subtitulos.srt
📝 Se procesaron 4 segmentos de subtítulo

--- Preview del SRT generado ---
1
00:00:00,000 --> 00:00:03,200
Bienvenidos a nuestro tutorial de hoy

2
00:00:03,200 --> 00:00:07,150
En este video aprenderemos sobre inteligencia artificial

3
00:00:07,150 --> 00:00:11,800
La IA está transformando el mundo moderno
...

📁 Resultado Final

Archivo Generado: subtitulos.srt

El script creará automáticamente el archivo subtitulos.srt en la misma carpeta donde se encuentran los otros archivos. Su contenido será similar a esto:

1
00:00:00,000 --> 00:00:03,200
Bienvenidos a nuestro tutorial de hoy

2
00:00:03,200 --> 00:00:07,150
En este video aprenderemos sobre inteligencia artificial

3
00:00:07,150 --> 00:00:11,800
La IA está transformando el mundo moderno

4
00:00:11,800 --> 00:00:15,450
Espero que disfruten este contenido educativo

🔧 Solución de Problemas

Error: API Key no configurada

 ERROR: Debes configurar tu API key en el script

Solución: Edita el script generar_srt.py y configura tu API key real en la línea ELEVENLABS_API_KEY = "tu_api_key_aqui".

Error: Archivos no encontrados

 Error: No se encontró el archivo 'audio.mp3'

Solución: Verifica que los archivos audio.mp3, texto.txt y generar_srt.py estén en la misma carpeta donde ejecutas el script.

Error: Plan insuficiente

 Error: Insufficient credits or plan

Solución: El plan gratuito de ElevenLabs no incluye Forced Alignment. Actualiza tu suscripción a un plan Starter ($5/mes) o superior.

Error: Texto muy largo

 Error: Text too long

Solución: El texto combinado en texto.txt no puede exceder los 675k caracteres.

Error: Audio muy grande

 Error: File too large

Solución: El archivo de audio no puede exceder los 3GB de tamaño o las 10 horas de duración.

💡 Consejos y Mejores Prácticas

1. Calidad del Audio

2. Formato del Texto

3. Segmentación Inteligente

4. Verificación

🌍 Idiomas Soportados

El script funciona con los 29 idiomas soportados por ElevenLabs, incluyendo:

💰 Costos

🔒 Seguridad


📋 Código Completo del Script

Guarda este código como generar_srt.py en tu carpeta de proyecto:


#!/usr/bin/env python3
"""
Script completo para generar un archivo SRT usando la API de Forced Alignment de ElevenLabs.
Coloca este script en la misma carpeta que audio.mp3 y texto.txt
El archivo SRT resultante se guardará como 'subtitulos.srt' en la misma carpeta.
"""
import os
import json
import base64
from elevenlabs.client import ElevenLabs

# ========================================
# 🔑 CONFIGURACIÓN - COLOCA TU API KEY AQUÍ
# ========================================
ELEVENLABS_API_KEY = "tu_api_key_aqui"  # ⬅️ REEMPLAZA ESTO CON TU API KEY REAL
# ========================================

def seconds_to_srt_time(seconds):
    """Convierte segundos a formato SRT (HH:MM:SS,mmm)"""
    hours = int(seconds // 3600)
    minutes = int((seconds % 3600) // 60)
    secs = int(seconds % 60)
    milliseconds = int((seconds % 1) * 1000)
    return f"{hours:02d}:{minutes:02d}:{secs:02d},{milliseconds:03d}"

def create_srt_from_alignment(alignment_result, text_lines):
    """
    Crea contenido SRT basado en el resultado de forced alignment
    y las líneas de texto originales
    """
    srt_content = []
    subtitle_index = 1
    
    # Procesar la respuesta de forced alignment
    if hasattr(alignment_result, 'words') and alignment_result.words:
        words = alignment_result.words
        current_line_index = 0
        current_line_text = text_lines[current_line_index] if current_line_index < len(text_lines) else ""
        current_line_words = []
        
        # Procesar palabra por palabra
        for word_info in words:
            word = word_info.word if hasattr(word_info, 'word') else str(word_info.get('word', ''))
            start_time = word_info.start if hasattr(word_info, 'start') else float(word_info.get('start', 0))
            end_time = word_info.end if hasattr(word_info, 'end') else float(word_info.get('end', 0))
            
            # Si una palabra tiene un final de tiempo antes de su inicio, la ignoramos o ajustamos
            if start_time > end_time:
                # print(f"⚠️  Advertencia: Palabra '{word}' tiene tiempo final menor que inicial. Ajustando o ignorando.")
                if not current_line_words: # Si es la primera palabra de un segmento, ajustamos
                    end_time = start_time + 0.1 # Darle una duración mínima
                else: # Si no, podemos ignorarla o dejarla como está si el siguiente word_info corregirá
                    pass 
            
            current_line_words.append({
                'word': word.strip(),
                'start': start_time,
                'end': end_time
            })
            
            # Construir texto de las palabras actuales para comparación
            current_words_text = ' '.join([w['word'] for w in current_line_words]).strip()
            
            # Convertir el texto de la línea original a un formato comparable (minúsculas, sin puntuación)
            line_clean = current_line_text.lower()
            line_clean = ''.join(char for char in line_clean if char.isalnum() or char.isspace()).strip()

            # Convertir el texto de las palabras actuales a un formato comparable
            words_clean = current_words_text.lower()
            words_clean = ''.join(char for char in words_clean if char.isalnum() or char.isspace()).strip()
            
            # Verificar si hemos completado la línea actual (comparación más flexible)
            # La lógica es que si el texto limpio de las palabras actuales contiene o es suficientemente
            # cercano al texto limpio de la línea original, consideramos la línea completa.
            # También si el número de palabras en el audio ya supera las de la línea original.
            
            # Añado un umbral de coincidencia o longitud para mayor robustez
            min_match_ratio = 0.75 # Porcentaje de palabras de la línea que deben estar en el texto actual
            
            original_words_count = len(current_line_text.split())
            current_aligned_words_count = len(current_line_words)
            
            is_line_match = (
                line_clean in words_clean or 
                (len(words_clean) > 0 and line_clean.startswith(words_clean) and current_aligned_words_count >= original_words_count * min_match_ratio) or
                (current_aligned_words_count >= original_words_count and words_clean.startswith(line_clean))
            )
            
            if is_line_match or (current_aligned_words_count > 0 and word_info == words[-1]):
                if current_line_words:
                    start_subtitle = current_line_words[0]['start']
                    end_subtitle = current_line_words[-1]['end']
                    
                    # Asegurar que el tiempo final no sea menor que el inicial
                    if end_subtitle < start_subtitle:
                        end_subtitle = start_subtitle + 0.1 # Asignar duración mínima
                    
                    srt_content.append(f"{subtitle_index}")
                    srt_content.append(f"{seconds_to_srt_time(start_subtitle)} --> {seconds_to_srt_time(end_subtitle)}")
                    srt_content.append(current_line_text)
                    srt_content.append("")  # Línea vacía entre subtítulos
                    
                    subtitle_index += 1
                    current_line_index += 1
                    current_line_words = []
                    
                    if current_line_index < len(text_lines):
                        current_line_text = text_lines[current_line_index]
                    else:
                        break # No hay más líneas de texto para procesar
    
    # Si no hay palabras procesadas, o si algo falla, crear una versión básica de fallback
    elif hasattr(alignment_result, 'characters') and alignment_result.characters:
        # Fallback usando información de caracteres
        # Esto es menos preciso, pero asegura que algo se genera
        total_duration = 0
        if alignment_result.characters:
            last_char_end = alignment_result.characters[-1].end if hasattr(alignment_result.characters[-1], 'end') else 0
            if last_char_end > 0:
                total_duration = last_char_end
            elif alignment_result.words: # Si hay palabras pero sin tiempos, usar el último tiempo de palabra
                total_duration = alignment_result.words[-1].end if hasattr(alignment_result.words[-1], 'end') else 0

        # Si aún no tenemos duración, intentar con una heurística
        if total_duration == 0 and text_lines:
            # Asumiendo una velocidad de habla promedio de 3 palabras por segundo
            # y 5 caracteres por palabra
            estimated_duration_per_char = 1 / (3 * 5) # ~0.066 segundos por caracter
            total_chars = sum(len(line) for line in text_lines)
            total_duration = total_chars * estimated_duration_per_char
            print(f"<span class="icon-warning">⚠️</span>  Advertencia: No se encontraron tiempos de palabras/caracteres. Usando duración estimada: {total_duration:.2f}s")
        
        # Distribuir tiempo uniformemente entre líneas
        num_lines = len(text_lines)
        time_per_line = total_duration / num_lines if num_lines > 0 else 0
        
        for i, line in enumerate(text_lines):
            start_time = i * time_per_line
            end_time = (i + 1) * time_per_line
            
            srt_content.append(f"{i + 1}")
            srt_content.append(f"{seconds_to_srt_time(start_time)} --> {seconds_to_srt_time(end_time)}")
            srt_content.append(line)
            srt_content.append("")
    else:
        print("<span class="icon-warning">⚠️</span>  Advertencia: No se encontraron datos de palabras o caracteres en la respuesta de alignment.")
        # Último fallback: cada línea dura 3 segundos por defecto si no hay información de tiempo
        default_duration_per_line = 3.0
        for i, line in enumerate(text_lines):
            start_time = i * default_duration_per_line
            end_time = (i + 1) * default_duration_per_line
            
            srt_content.append(f"{i + 1}")
            srt_content.append(f"{seconds_to_srt_time(start_time)} --> {seconds_to_srt_time(end_time)}")
            srt_content.append(line)
            srt_content.append("")
        print("<span class="icon-tip">🔍</span> Se generaron subtítulos con duración estimada (3s por línea). Revisa la precisión.")


    return '\n'.join(srt_content)

def main():
    print("<span class="icon-title">🎬</span> Generador de SRT con ElevenLabs Forced Alignment")
    print("=" * 50)
    
    # Verificar API key
    if not ELEVENLABS_API_KEY or ELEVENLABS_API_KEY == "tu_api_key_aqui":
        print("<span class="icon-error">❌</span> ERROR: Debes configurar tu API key en el script")
        print("<span class="icon-tip">🔍</span> Abre el archivo y reemplaza 'tu_api_key_aqui' con tu API key real")
        print("<span class="icon-key">🔑</span> Encuentra tu API key en: https://elevenlabs.io/app/settings/api-keys")
        return
    
    # Verificar que existen los archivos necesarios
    if not os.path.exists('audio.mp3'):
        print("<span class="icon-error">❌</span> Error: No se encontró el archivo 'audio.mp3' en la carpeta actual")
        print("<span class="icon-folder">📂</span> Carpeta actual: ", os.getcwd())
        return
    
    if not os.path.exists('texto.txt'):
        print("<span class="icon-error">❌</span> Error: No se encontró el archivo 'texto.txt' en la carpeta actual")
        print("<span class="icon-folder">📂</span> Carpeta actual: ", os.getcwd())
        return
    
    # Leer el archivo de texto
    try:
        with open('texto.txt', 'r', encoding='utf-8') as f:
            text_lines = [line.strip() for line in f.readlines() if line.strip()]
    except Exception as e:
        print(f"<span class="icon-error">❌</span> Error al leer texto.txt: {e}")
        return
    
    if not text_lines:
        print("<span class="icon-error">❌</span> Error: El archivo texto.txt está vacío o solo contiene saltos de línea")
        print("<span class="icon-tip">🔍</span> Asegúrate de que texto.txt contenga el texto del audio, una frase por línea.")
        return
    
    # Combinar todas las líneas en un solo texto para el forced alignment
    full_text = ' '.join(text_lines)
    
    print(f"<span class="icon-info">📝</span> Procesando {len(text_lines)} líneas de texto...")
    print(f"<span class="icon-file">📄</span> Texto completo: {full_text[:100]}..." if len(full_text) > 100 else f"<span class="icon-file">📄</span> Texto completo: {full_text}")
    print(f"<span class="icon-info">🎵</span> Archivo de audio: audio.mp3")
    print(f"<span class="icon-key">🔑</span> API Key configurada: {ELEVENLABS_API_KEY[:8]}...")
    
    # Crear cliente de ElevenLabs
    try:
        elevenlabs = ElevenLabs(api_key=ELEVENLABS_API_KEY)
    except Exception as e:
        print(f"<span class="icon-error">❌</span> Error al crear cliente ElevenLabs: {e}")
        print("<span class="icon-tip">🔍</span> Verifica que tu API key sea correcta y que la librería 'elevenlabs' esté instalada.")
        return
    
    try:
        # Realizar forced alignment
        print("\n<span class="icon-rocket">🚀</span> Enviando solicitud a ElevenLabs API...")
        with open('audio.mp3', 'rb') as audio_file:
            alignment_result = elevenlabs.forced_alignment.create(
                file=audio_file,
                text=full_text
            )
        
        print("<span class="icon-success">✅</span> ¡Forced alignment completado!")
        
        # Debug: Mostrar estructura de la respuesta
        print(f"<span class="icon-info">📊</span> Tipo de respuesta: {type(alignment_result)}")
        if hasattr(alignment_result, 'words') and alignment_result.words:
            print(f"<span class="icon-info">📝</span> Palabras encontradas: {len(alignment_result.words)}")
        elif hasattr(alignment_result, 'characters') and alignment_result.characters:
            print(f"<span class="icon-info">📝</span> Caracteres encontrados: {len(alignment_result.characters)}")
        
        # Crear archivo SRT
        srt_content = create_srt_from_alignment(alignment_result, text_lines)
        
        if not srt_content.strip():
            print("<span class="icon-warning">⚠️</span>  Advertencia: No se pudo generar contenido SRT válido")
            print("<span class="icon-tip">🔍</span> Revisa que el texto coincida con el audio y que el audio sea claro.")
            return
        
        # Guardar archivo SRT en la misma carpeta
        output_file = 'subtitulos.srt'
        with open(output_file, 'w', encoding='utf-8') as f:
            f.write(srt_content)
        
        print(f"<span class="icon-success">✅</span> ¡Archivo '{output_file}' creado exitosamente!")
        print(f"<span class="icon-folder">📁</span> Ubicación: {os.path.abspath(output_file)}")
        print(f"<span class="icon-info">📝</span> Se procesaron {len(text_lines)} segmentos de subtítulo")
        
        # Mostrar un preview del SRT
        print("\n--- Preview del SRT generado ---")
        preview = srt_content[:500] + "..." if len(srt_content) > 500 else srt_content
        print(preview)
        
    except Exception as e:
        print(f"<span class="icon-error">❌</span> Error al procesar: {e}")
        if "insufficient credits" in str(e).lower() or "plan" in str(e).lower():
            print("<span class="icon-bulb">💡</span> Sugerencia: Verifica tu plan de ElevenLabs. El Forced Alignment requiere un plan de pago (Starter o superior).")
        elif "api key" in str(e).lower() or "authentication" in str(e).lower():
            print("<span class="icon-bulb">💡</span> Sugerencia: Verifica que tu API key sea correcta y esté bien configurada en el script.")
        elif "too large" in str(e).lower() or "text too long" in str(e).lower():
            print("<span class="icon-bulb">💡</span> Sugerencia: Revisa los límites de tamaño de audio y texto de ElevenLabs.")
        
if __name__ == "__main__":
    main()