Transcripción y Subtitulado Automático de Vídeos con Python, Whisper y OpenCV
Transcriptor Pro es una aplicación de escritorio orientada al procesamiento automatizado de contenido audiovisual. El proyecto combina un modelo de reconocimiento automático de voz basado en Whisper con una interfaz gráfica desarrollada mediante CustomTkinter, procesamiento de vídeo con OpenCV, reproducción de audio mediante PyGame y renderizado de texto e imágenes mediante Pillow.
El objetivo del proyecto no consiste únicamente en convertir una pista de audio en texto. La aplicación plantea un flujo completo de trabajo que abarca la carga del vídeo, extracción y procesamiento del contenido hablado, generación de segmentos temporales, sincronización de subtítulos con la reproducción, renderizado sobre los frames y, opcionalmente, traducción del texto generado.
Desde el punto de vista de arquitectura, el proyecto resulta especialmente interesante porque obliga a coordinar varias áreas diferentes: machine learning, procesamiento de audio, procesamiento de vídeo, interfaces gráficas, sincronización temporal, gestión de memoria y renderizado de imagen.
¿Qué es Transcriptor Pro?
Transcriptor Pro es una aplicación de escritorio desarrollada en Python cuyo componente central utiliza Whisper para realizar reconocimiento automático de voz sobre contenido audiovisual.
El flujo general de la aplicación puede dividirse en varias etapas:
- Carga del archivo de vídeo.
- Obtención de sus parámetros técnicos mediante OpenCV.
- Procesamiento de la pista de audio.
- Inferencia mediante un modelo Whisper.
- Generación de segmentos de texto con información temporal.
- Asociación de los segmentos con la línea temporal del vídeo.
- Renderizado de subtítulos sobre los frames.
- Visualización opcional de traducciones.
- Edición y posterior exportación de los resultados.
La arquitectura intenta separar estas responsabilidades para evitar que la interfaz gráfica, el procesamiento de vídeo y la inferencia del modelo queden completamente acoplados.
Whisper como motor de reconocimiento de voz
El núcleo de reconocimiento de voz utiliza Whisper, un modelo de reconocimiento automático de voz desarrollado por OpenAI.
Whisper procesa audio y genera texto. Además, durante la transcripción puede proporcionar información temporal asociada a los segmentos reconocidos, lo que permite construir una línea temporal de subtítulos.
Una característica importante de Whisper es la existencia de diferentes tamaños de modelo. La elección afecta directamente al consumo de memoria, tiempo de inferencia y calidad potencial del reconocimiento.
| Modelo | Características | Uso orientativo |
|---|---|---|
| tiny | Muy reducido y rápido | Pruebas, prototipos y equipos limitados |
| base | Equilibrio entre velocidad y capacidad de reconocimiento | Uso general |
| small | Mayor capacidad de reconocimiento | Contenido donde se necesita mayor precisión |
| medium | Mayor consumo de recursos | Procesamiento de mayor calidad |
| large | Modelo considerablemente más grande | Equipos con suficientes recursos |
| large-v2 | Versión mejorada de la familia Large | Transcripción de alta calidad |
| large-v3 | Generación posterior de la familia Large | Procesamiento de alta precisión |
No debe interpretarse esta tabla como una clasificación absoluta de calidad. El resultado depende también del idioma, calidad del audio, ruido de fondo, voces superpuestas, terminología utilizada y configuración de inferencia.
Por esta razón, utilizar siempre el modelo más grande no constituye necesariamente la mejor estrategia. Para determinados vídeos, base o small pueden proporcionar una relación mucho más favorable entre tiempo de procesamiento, consumo de memoria y calidad obtenida.
Transcripción, traducción y subtitulado no son el mismo problema
Uno de los aspectos importantes de la arquitectura consiste en separar tres procesos diferentes.
1. Transcripción
La transcripción convierte el contenido hablado del audio en texto.
AUDIO → RECONOCIMIENTO DE VOZ → TEXTO
2. Segmentación temporal
El texto se divide en segmentos asociados a intervalos temporales.
Texto + timestamps → segmentos de subtítulos
3. Traducción
La traducción toma el texto obtenido y genera una representación en otro idioma.
TEXTO ORIGINAL → TRADUCTOR → TEXTO TRADUCIDO
La traducción no debería modificar los timestamps originales. De esta forma, tanto el texto original como su traducción continúan vinculados al mismo intervalo temporal.
Flujo completo de procesamiento
Desde una perspectiva de arquitectura, el procesamiento puede representarse de la siguiente manera:
┌─────────────────────┐
│ ARCHIVO │
│ VÍDEO │
└──────────┬──────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ FRAMES │ │ AUDIO │
│ OpenCV │ │ │
└──────┬──────┘ └──────┬──────┘
│ │
│ ▼
│ ┌─────────────┐
│ │ WHISPER │
│ └──────┬──────┘
│ │
│ ▼
│ ┌─────────────┐
│ │ SEGMENTOS │
│ │ TEMPORALES │
│ └──────┬──────┘
│ │
│ ┌─────────┴─────────┐
│ │ │
│ ▼ ▼
│ ┌──────────────┐ ┌──────────────┐
│ │ TEXTO │ │ TRADUCCIÓN │
│ │ ORIGINAL │ │ OPCIONAL │
│ └───────┬──────┘ └───────┬──────┘
│ │ │
└──────────────────┴──────────────────┘
│
▼
┌─────────────────┐
│ RENDERIZADO │
│ DE SUBTÍTULOS │
└─────────────────┘
│
▼
┌─────────────────┐
│ FRAME FINAL │
└─────────────────┘
Esta separación permite modificar una parte del sistema sin tener que reescribir necesariamente las demás.
Arquitectura técnica
ModernVideoPlayer
ModernVideoPlayer encapsula la lectura de frames mediante OpenCV.
Durante la carga del vídeo se obtiene información como:
- Frames por segundo.
- Número total de frames.
- Duración aproximada.
- Anchura.
- Altura.
El método de carga utiliza cv2.VideoCapture:
self.cap = cv2.VideoCapture(video_path)
self.fps = self.cap.get(cv2.CAP_PROP_FPS) or 30
total_frames = int(
self.cap.get(cv2.CAP_PROP_FRAME_COUNT)
)
self.duration = (
total_frames / self.fps
if self.fps > 0
else 0
)
Esta información permite relacionar tiempo y posición de frame mediante:
frame_pos = int(time_seconds * self.fps)
Sin embargo, hay que tener en cuenta que el acceso aleatorio mediante CAP_PROP_POS_FRAMES puede ser relativamente costoso dependiendo del códec y del contenedor. Por tanto, el caché reduce determinadas lecturas repetidas, pero no convierte el acceso aleatorio al vídeo en una operación O(1) garantizada.
Sistema de caché de frames
El reproductor mantiene un diccionario utilizado como caché:
self.frame_cache = {}
self.cache_size = 100
Cuando se solicita un frame, se comprueba primero si ya existe:
if frame_pos in self.frame_cache:
return self.frame_cache[frame_pos]
Si no existe, se realiza la lectura correspondiente:
self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_pos) success, frame = self.cap.read()
El frame obtenido por OpenCV está normalmente en formato BGR, por lo que se convierte a RGB antes de entregarlo a Pillow:
frame_rgb = cv2.cvtColor(
frame,
cv2.COLOR_BGR2RGB
)
El sistema implementado utiliza una política sencilla de expulsión basada en el primer elemento del diccionario. Es funcional para un prototipo, aunque una implementación más sofisticada podría utilizar un caché LRU (Least Recently Used) o una estrategia basada en una ventana temporal alrededor de la posición actual.
ModernSubtitleSystem
ModernSubtitleSystem encapsula la lógica de gestión y renderizado de subtítulos.
Los segmentos utilizan una estructura temporal sencilla:
{
"start": 12.40,
"end": 15.80,
"text": "Este es un ejemplo de subtítulo."
}
La búsqueda del subtítulo activo se realiza comparando el instante actual con los límites del segmento:
def get_subtitle_at_time(self, current_time):
for segment in self.segments:
if segment['start'] <= current_time <= segment['end']:
return segment
return None
Para un número reducido de segmentos esta estrategia es perfectamente válida. En un sistema que manejase miles de segmentos podría ser interesante utilizar una estructura de búsqueda temporal más eficiente, por ejemplo mediante búsqueda binaria sobre los timestamps.
División del texto
Los subtítulos deben limitar la longitud de las líneas para evitar que el texto ocupe una parte excesiva de la imagen.
El sistema utiliza una función de wrapping basada en el número máximo de caracteres:
def wrap_text(self, text, max_chars=None):
if max_chars is None:
max_chars = self.max_chars_per_line
words = text.split()
lines = []
current_line = []
for word in words:
test_line = ' '.join(current_line + [word])
if len(test_line) > max_chars:
if current_line:
lines.append(' '.join(current_line))
current_line = [word]
else:
current_line = [word]
else:
current_line.append(word)
if current_line:
lines.append(' '.join(current_line))
return lines
Esta aproximación es sencilla y funciona bien para una interfaz básica, aunque un sistema de subtitulado profesional debería tener en cuenta también la anchura real del texto en píxeles, tamaño de fuente, velocidad de lectura, número de caracteres por segundo y reglas específicas de segmentación.
Renderizado mediante Pillow
Una vez determinado el subtítulo activo, el sistema convierte el frame a una imagen de Pillow:
pil_img = Image.fromarray(frame) draw = ImageDraw.Draw(pil_img)
A partir de este punto se calcula la posición del texto y se dibujan el fondo, borde, sombra y caracteres.
La configuración visual utilizada por el proyecto incluye:
'bg_color': (0, 0, 0, 220), 'border_color': (255, 255, 255, 180), 'text_color': (255, 255, 255), 'translation_color': (255, 255, 100), 'font_size': 52, 'border_width': 3, 'padding': 20, 'margin_bottom': 100
El sistema diferencia además visualmente el texto original de la traducción cuando se activa el modo bilingüe.
Desde el punto de vista de un renderizador más avanzado, esta capa podría evolucionar para soportar estilos similares a ASS/SSA, posicionamiento relativo, múltiples fuentes, alineación, animaciones y efectos.
GiantFontSystem
GiantFontSystem centraliza la carga y reutilización de fuentes.
En lugar de cargar una fuente cada vez que se dibuja un frame, las fuentes se cargan previamente y se almacenan en memoria:
self.fonts[f'regular_{size}'] = ImageFont.truetype(
"arial.ttf",
size
)
En Windows se utiliza además la ruta habitual del sistema:
C:/Windows/Fonts/arial.ttf
El sistema dispone de un mecanismo de fallback mediante ImageFont.load_default() cuando no encuentra las fuentes esperadas.
Sistema de traducción
El proyecto contempla una capa de traducción opcional mediante googletrans:
try:
from googletrans import Translator
GOOGLETRANS_AVAILABLE = True
except ImportError:
GOOGLETRANS_AVAILABLE = False
Esto permite que la aplicación continúe funcionando aunque el componente de traducción no esté instalado.
Es importante realizar una distinción técnica: googletrans no debe presentarse como la API oficial de Google Cloud Translation. Es una biblioteca independiente que proporciona una interfaz Python para servicios de traducción de Google y puede presentar limitaciones de compatibilidad o disponibilidad.
Para una aplicación empresarial, una arquitectura más robusta debería abstraer el proveedor mediante una interfaz independiente:
TranslationProvider
│
├── GoogleCloudTranslation
├── DeepLTranslation
├── LibreTranslate
└── Otro proveedor
De esta forma, el resto de la aplicación no dependería directamente de una implementación concreta.
Interfaz gráfica con CustomTkinter
La interfaz utiliza CustomTkinter, una extensión de Tkinter que proporciona componentes visuales modernos manteniendo el modelo de programación de Tkinter.
El tema inicial se configura mediante:
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")
La clase ModernTranscriberApp actúa como coordinador principal de la aplicación.
Entre los estados mantenidos por la aplicación se encuentran:
self.video_path = None self.audio_path = None self.segments = [] self.is_playing = False self.current_time = 0 self.current_model = "base" self.show_translation = False self.volume = 0.7
Esta clase conecta los diferentes subsistemas y mantiene el estado necesario para la interfaz.
Arquitectura de componentes
| Componente | Responsabilidad |
|---|---|
ModernVideoPlayer |
Lectura de vídeo, frames, metadatos y caché. |
ModernSubtitleSystem |
Gestión temporal, wrapping y renderizado de subtítulos. |
GiantFontSystem |
Carga y reutilización de fuentes. |
ModernTranscriberApp |
Coordinación de la aplicación y de la interfaz gráfica. |
Esta separación no constituye una arquitectura empresarial completa, pero sí proporciona una base razonable para evolucionar el proyecto sin concentrar toda la lógica en una única clase.
Tecnologías utilizadas
- Python: lenguaje principal de la aplicación.
- Whisper: reconocimiento automático de voz.
- PyTorch: framework utilizado por el modelo.
- CustomTkinter: interfaz gráfica.
- OpenCV: lectura y procesamiento de vídeo.
- PyGame: reproducción y gestión de audio.
- Pillow: procesamiento de imágenes y renderizado de texto.
- NumPy: representación y manipulación de arrays de imagen.
- FFmpeg: infraestructura multimedia utilizada por Whisper para determinadas operaciones de audio.
- googletrans: componente opcional de traducción.
Requisitos del sistema
Los requisitos no pueden establecerse mediante una única cifra universal porque el consumo depende fundamentalmente del modelo Whisper utilizado, backend de PyTorch, precisión numérica, duración del audio y dispositivo de ejecución.
Como referencia práctica:
- Sistema operativo: Windows, macOS o Linux.
- CPU: procesador multinúcleo recomendado.
- RAM: 8 GB puede resultar suficiente para configuraciones pequeñas; 16 GB o más proporciona mayor margen.
- GPU: opcional. Una GPU compatible con CUDA puede acelerar considerablemente la inferencia cuando PyTorch está correctamente configurado.
- Almacenamiento: necesario tanto para las dependencias como para los modelos descargados.
- FFmpeg: recomendado y, dependiendo del flujo utilizado, necesario para el procesamiento de audio.
No resulta correcto afirmar que todos los modelos requieren una cantidad fija de RAM. El consumo real debe evaluarse sobre el entorno concreto.
Dependencias
Una instalación inicial puede realizarse mediante:
python -m venv .venv
En Windows:
.venv\Scripts\activate
Posteriormente:
python -m pip install --upgrade pip python -m pip install customtkinter opencv-python torch pygame openai-whisper Pillow numpy googletrans==3.1.0a0
La dependencia de Whisper debe instalarse mediante el paquete openai-whisper. El módulo se importa posteriormente en Python mediante:
import whisper
La versión concreta de PyTorch debería seleccionarse teniendo en cuenta el hardware disponible, especialmente cuando se pretende utilizar aceleración mediante GPU.
FFmpeg
Whisper trabaja habitualmente con FFmpeg para acceder y convertir diferentes formatos multimedia. Por ello, en una instalación real conviene comprobar que FFmpeg está disponible en el sistema.
Una comprobación sencilla desde la terminal es:
ffmpeg -version
Si el comando no existe, será necesario instalar FFmpeg y añadir su directorio ejecutable al PATH del sistema.
Código fuente
A continuación se conserva el bloque Python proporcionado para el proyecto. Es importante señalar que este fragmento corresponde al código disponible en esta versión del artículo y contiene deliberadamente secciones resumidas mediante comentarios.
Por tanto, no debe presentarse como un archivo ejecutable completamente autónomo mientras existan métodos o partes de la clase representados mediante ....
# ========== CONFIGURACIÓN COMPLETA MEJORADA - VERSIÓN CORREGIDA ==========
import os
import sys
# Silenciar TODO antes de cualquier import
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1'
os.environ['CUDA_LAUNCH_BLOCKING'] = '0'
os.environ['SDL_AUDIODRIVER'] = 'directsound'
# Redirigir stderr para silenciar warnings
class SilenceWarnings:
def __enter__(self):
self._original_stderr = sys.stderr
sys.stderr = open(os.devnull, 'w')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
sys.stderr.close()
sys.stderr = self._original_stderr
with SilenceWarnings():
import warnings
warnings.filterwarnings("ignore")
warnings.simplefilter("ignore")
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import customtkinter as ctk
import cv2
import torch
import pygame
import time
import threading
import subprocess
import tempfile
import whisper
from PIL import Image, ImageTk, ImageDraw, ImageFont
from pathlib import Path
import numpy as np
import json
from datetime import datetime
import webbrowser
try:
from googletrans import Translator
GOOGLETRANS_AVAILABLE = True
except ImportError:
GOOGLETRANS_AVAILABLE = False
# ========== TEMA Y CONFIGURACIÓN MODERNA CORREGIDA ==========
ctk.set_appearance_mode("Dark")
ctk.set_default_color_theme("blue")
# Colores modernos - TODOS VÁLIDOS para CustomTkinter
COLORS = {
"primary": "#2B5B84",
"secondary": "#1E3A5F",
"accent": "#FF6B35",
"success": "#27AE60",
"warning": "#E67E22",
"danger": "#E74C3C",
"dark": "#1A1A1A",
"dark_alt": "#2D2D2D",
"light": "#ECF0F1",
"gray": "#95A5A6"
}
LANGUAGES = {
'Original': None,
'English': 'en',
'Español': 'es',
'Français': 'fr',
'Deutsch': 'de',
'Italiano': 'it',
'Português': 'pt',
'Русский': 'ru',
'中文': 'zh-cn',
'日本語': 'ja',
'한국어': 'ko'
}
WHISPER_MODELS = {
'tiny': 'Tiny (más rápido)',
'base': 'Base (recomendado)',
'small': 'Small (preciso)',
'medium': 'Medium (muy preciso)',
'large': 'Large (máxima precisión)',
'large-v2': 'Large v2 (máx precisión)',
'large-v3': 'Large v3 (máx precisión)'
}
# ========== COMPONENTES MEJORADOS ==========
class ModernVideoPlayer:
def __init__(self):
self.cap = None
self.fps = 30
self.duration = 0
self.video_width = 0
self.video_height = 0
self.frame_cache = {}
self.cache_size = 100
def load_video(self, video_path):
try:
if self.cap:
self.cap.release()
self.frame_cache.clear()
self.cap = cv2.VideoCapture(video_path)
if not self.cap.isOpened():
return False
self.fps = self.cap.get(cv2.CAP_PROP_FPS) or 30
total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))
self.duration = total_frames / self.fps if self.fps > 0 else 0
self.video_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.video_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
return True
except Exception as e:
print(f"❌ Error cargando video: {e}")
return False
def get_frame_at_time(self, time_seconds):
if not self.cap:
return None
try:
frame_pos = int(time_seconds * self.fps)
# Usar caché para mejor rendimiento
if frame_pos in self.frame_cache:
return self.frame_cache[frame_pos]
self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_pos)
success, frame = self.cap.read()
if success and frame is not None:
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Gestionar caché
if len(self.frame_cache) >= self.cache_size:
self.frame_cache.pop(next(iter(self.frame_cache)))
self.frame_cache[frame_pos] = frame_rgb
return frame_rgb
except Exception as e:
print(f"⚠️ Error obteniendo frame: {e}")
return None
def release(self):
if self.cap:
self.cap.release()
self.cap = None
self.frame_cache.clear()
class ModernSubtitleSystem:
def __init__(self):
self.segments = []
self.font_system = GiantFontSystem()
self.max_chars_per_line = 35
self.subtitle_style = {
'bg_color': (0, 0, 0, 220),
'border_color': (255, 255, 255, 180),
'text_color': (255, 255, 255),
'translation_color': (255, 255, 100),
'font_size': 52,
'border_width': 3,
'padding': 20,
'margin_bottom': 100
}
def set_segments(self, segments):
self.segments = segments
def get_subtitle_at_time(self, current_time):
for segment in self.segments:
if segment['start'] <= current_time <= segment['end']:
return segment
return None
def wrap_text(self, text, max_chars=None):
if max_chars is None:
max_chars = self.max_chars_per_line
words = text.split()
lines = []
current_line = []
for word in words:
test_line = ' '.join(current_line + [word])
if len(test_line) > max_chars:
if current_line:
lines.append(' '.join(current_line))
current_line = [word]
else:
# Palabra muy larga
while len(word) > max_chars:
lines.append(word[:max_chars - 1] + "-")
word = word[max_chars - 1:]
current_line = [word]
else:
current_line.append(word)
if current_line:
lines.append(' '.join(current_line))
return lines
def render_subtitles(self, frame, current_time, show_translation=False):
if frame is None:
return frame
subtitle = self.get_subtitle_at_time(current_time)
if not subtitle:
return frame
try:
pil_img = Image.fromarray(frame)
draw = ImageDraw.Draw(pil_img)
original_text = subtitle['text'].strip()
original_lines = self.wrap_text(original_text)
translation_text = ""
if show_translation and subtitle.get('translation'):
translation_text = subtitle['translation'].strip()
translation_lines = self.wrap_text(translation_text, self.max_chars_per_line - 5)
all_lines = original_lines + translation_lines
else:
all_lines = original_lines
# Calcular dimensiones
style = self.subtitle_style
line_height = 60
padding = style['padding']
margin_bottom = style['margin_bottom']
total_height = len(all_lines) * line_height + padding * 2
y_position = pil_img.height - total_height - margin_bottom
# Encontrar línea más ancha
max_line_width = 0
font = self.font_system.get_font(style['font_size'])
for line in all_lines:
bbox = draw.textbbox((0, 0), line, font=font)
text_width = bbox[2] - bbox[0]
max_line_width = max(max_line_width, text_width)
# Fondo y borde
bg_x1 = max(50, (pil_img.width - max_line_width) // 2 - padding)
bg_x2 = min(pil_img.width - 50, bg_x1 + max_line_width + padding * 2)
bg_y1 = max(50, y_position - padding)
bg_y2 = min(pil_img.height - 50, bg_y1 + total_height + padding)
# Fondo semitransparente
draw.rectangle([bg_x1, bg_y1, bg_x2, bg_y2], fill=style['bg_color'])
# Borde
for i in range(style['border_width']):
draw.rectangle([bg_x1 - i, bg_y1 - i, bg_x2 + i, bg_y2 + i],
outline=style['border_color'], width=1)
# Texto
current_y = y_position
for i, line in enumerate(all_lines):
if line.strip():
bbox = draw.textbbox((0, 0), line, font=font)
text_width = bbox[2] - bbox[0]
x_position = (pil_img.width - text_width) // 2
if show_translation and i >= len(original_lines):
color = style['translation_color']
# Sombra para mejor legibilidad
draw.text((x_position + 2, current_y + 2), line, font=font, fill=(0, 0, 0, 150))
else:
color = style['text_color']
# Sombra para mejor legibilidad
draw.text((x_position + 2, current_y + 2), line, font=font, fill=(0, 0, 0, 150))
draw.text((x_position, current_y), line, font=font, fill=color)
current_y += line_height
return np.array(pil_img)
except Exception as e:
print(f"❌ Error renderizando subtítulos: {e}")
return frame
class GiantFontSystem:
def __init__(self):
self.fonts = {}
self.load_fonts()
def load_fonts(self):
try:
# Fuentes para diferentes tamaños
sizes = [36, 42, 48, 54, 60, 66, 72]
for size in sizes:
try:
self.fonts[f'regular_{size}'] = ImageFont.truetype("arial.ttf", size)
self.fonts[f'bold_{size}'] = ImageFont.truetype("arialbd.ttf", size)
except:
try:
self.fonts[f'regular_{size}'] = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", size)
self.fonts[f'bold_{size}'] = ImageFont.truetype("C:/Windows/Fonts/arialbd.ttf", size)
except:
pass
if not self.fonts:
self.fonts['regular_54'] = ImageFont.load_default()
except Exception as e:
print(f"⚠️ Error cargando fuentes: {e}")
self.fonts['regular_54'] = ImageFont.load_default()
def get_font(self, size=54, bold=False):
key = f"{'bold' if bold else 'regular'}_{size}"
return self.fonts.get(key, self.fonts.get('regular_54'))
# ========== APLICACIÓN PRINCIPAL MEJORADA Y CORREGIDA ==========
class ModernTranscriberApp:
def __init__(self, root):
self.root = root
self.root.title("🎬 TRANSCRIPTOR PRO - Edición Profesional")
self.root.geometry("1400x900")
self.root.minsize(1200, 800)
# Estado de la aplicación
self.setup_state()
self.setup_theming()
self.setup_components()
self.setup_ui()
self.start_main_loop()
self.setup_bindings()
def setup_state(self):
"""Configurar estado inicial"""
self.video_player = ModernVideoPlayer()
self.subtitle_system = ModernSubtitleSystem()
self.transcriber = None
self.translator = Translator() if GOOGLETRANS_AVAILABLE else None
self.video_path = None
self.audio_path = None
self.segments = []
self.is_playing = False
self.current_time = 0
self.current_model = "base"
self.show_translation = False
self.ui_visible = True
self.volume = 0.7
self.last_volume = 0.7
# Audio
self.audio_player = None
self.init_audio()
# Variables de UI
self.status_var = tk.StringVar(value="👋 Bienvenido a Transcriptor Pro - Carga un video para comenzar")
self.progress_var = tk.DoubleVar(value=0)
self.time_var = tk.StringVar(value="00:00 / 00:00")
self.volume_var = tk.DoubleVar(value=self.volume * 100)
def setup_theming(self):
"""Configurar tema visual"""
self.current_theme = "dark"
self.accent_color = COLORS["accent"]
def setup_components(self):
"""Inicializar componentes del sistema"""
self.load_config()
def setup_ui(self):
"""Construir interfaz de usuario moderna"""
self.root.grid_columnconfigure(1, weight=1)
self.root.grid_rowconfigure(0, weight=1)
self.setup_sidebar()
self.setup_main_area()
self.setup_status_bar()
# ... (el resto del código de la clase ModernTranscriberApp se mantiene igual)
# ========== EJECUCIÓN MEJORADA ==========
def main():
print("🚀 INICIANDO TRANSCRIPTOR PRO - EDICIÓN PROFESIONAL")
print("✅ Interfaz moderna y profesional")
print("✅ Rendimiento optimizado")
print("✅ Funcionalidades completas")
print("✅ Experiencia de usuario mejorada")
try:
root = ctk.CTk()
app = ModernTranscriberApp(root)
def on_closing():
app.cleanup()
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
except Exception as e:
print(f"❌ ERROR CRÍTICO: {e}")
messagebox.showerror("Error Crítico", f"Error al iniciar la aplicación:\n{str(e)}")
if __name__ == "__main__":
main()
Instalación
Paso 1: Crear el entorno virtual
python -m venv .venv
Paso 2: Activar el entorno
En Windows:
.venv\Scripts\activate
En Linux o macOS:
source .venv/bin/activate
Paso 3: Instalar dependencias
python -m pip install --upgrade pip python -m pip install customtkinter opencv-python torch pygame openai-whisper Pillow numpy googletrans==3.1.0a0
Paso 4: Comprobar FFmpeg
ffmpeg -version
Paso 5: Ejecutar la aplicación
python transcriptor_pro.py
Flujo de utilización
- Iniciar la aplicación.
- Seleccionar el modelo Whisper.
- Cargar el archivo de vídeo.
- Iniciar la transcripción.
- Esperar a que finalice la inferencia.
- Revisar los segmentos generados.
- Corregir manualmente los errores necesarios.
- Aplicar traducción si procede.
- Previsualizar los subtítulos.
- Exportar el resultado.
Rendimiento: por qué no deben utilizarse cifras universales
Una de las partes que más conviene matizar en este tipo de proyectos es la medición del rendimiento.
Afirmaciones como “92 % de reducción del tiempo”, “3,5× más rápido” o “75 % de reducción de costes” solamente son técnicamente defendibles si proceden de un benchmark reproducible.
Para que una medición de este tipo tenga valor habría que especificar como mínimo:
- Modelo Whisper utilizado.
- Duración exacta del material procesado.
- Idioma.
- CPU.
- GPU.
- Versión de PyTorch.
- Versión de Whisper.
- Precisión utilizada.
- Backend de ejecución.
- Calidad y características del audio.
- Tiempo de carga del modelo.
- Tiempo de inferencia.
Una métrica mucho más útil para este proyecto es el factor de tiempo real o RTF (Real-Time Factor).
Por ejemplo, si un vídeo de 60 minutos tarda 10 minutos en procesarse:
RTF = tiempo de procesamiento / duración del audio RTF = 10 / 60 RTF = 0,167
Un RTF inferior a 1 indica que el procesamiento ha tardado menos que la duración del contenido, aunque el resultado depende de las condiciones exactas de la prueba.
Por este motivo, este artículo no atribuye al proyecto porcentajes de rendimiento que no estén respaldados por un benchmark reproducible.
Limitaciones actuales
Precisión del reconocimiento
Whisper puede cometer errores en nombres propios, terminología especializada, ruido intenso, voces simultáneas, acentos, audio comprimido o grabaciones con mala relación señal/ruido.
Acceso a frames
OpenCV resulta excelente para procesamiento de imagen, pero su modelo de acceso a frames no pretende sustituir a un motor multimedia especializado. La búsqueda frecuente de posiciones alejadas puede implicar operaciones de seek y decodificación adicionales.
Traducción
La utilización de googletrans introduce una dependencia externa que no debería considerarse equivalente a una integración empresarial con un servicio oficial de traducción.
Interfaz gráfica
Las operaciones pesadas, especialmente la carga de modelos y la inferencia de Whisper, deben ejecutarse fuera del hilo principal de Tkinter para evitar que la interfaz quede bloqueada.
Modelos grandes
Los modelos de mayor tamaño incrementan significativamente los requisitos de memoria y el tiempo de carga. La elección debe hacerse en función del hardware disponible y del nivel de precisión requerido.
Mejoras futuras
La arquitectura actual proporciona una base sobre la que pueden construirse funcionalidades considerablemente más avanzadas:
- Exportación a SRT.
- Exportación a WebVTT.
- Exportación a ASS/SSA.
- Edición manual de timestamps.
- Búsqueda dentro de la transcripción.
- Corrección automática mediante modelos lingüísticos.
- Detección automática de idioma.
- Identificación y separación de hablantes.
- Procesamiento por lotes.
- Cola de trabajos.
- Aceleración mediante GPU.
- Drag & Drop.
- Persistencia de proyectos.
- Historial de transcripciones.
- Exportación de vídeo con subtítulos incrustados.
- Previsualización de estilos ASS.
- API REST para integración externa.
- Arquitectura basada en proveedores para traducción.
- Procesamiento distribuido de grandes volúmenes de vídeo.
Posible evolución arquitectónica
Si el proyecto creciera desde una herramienta de escritorio hasta una plataforma de procesamiento audiovisual, sería recomendable separar todavía más las responsabilidades:
APPLICATION
│
┌──────────┴──────────┐
│ │
▼ ▼
GUI LAYER PROJECT API
│ │
└──────────┬──────────┘
│
DOMAIN LAYER
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
VIDEO SERVICE TRANSCRIPTION TRANSLATION
│ │ │
▼ ▼ ▼
OpenCV Whisper Provider API
│ │ │
└─────────────────┼─────────────────┘
│
▼
SUBTITLE ENGINE
│
┌───────────┴───────────┐
▼ ▼
SRT/VTT ASS
│ │
└───────────┬───────────┘
▼
VIDEO EXPORT
Esta separación permitiría sustituir OpenCV, Whisper o el proveedor de traducción sin tener que modificar necesariamente la interfaz completa.
Aplicaciones prácticas
Creadores de contenido
- Generación automática de subtítulos para vídeos.
- Creación de transcripciones de podcasts.
- Preparación de contenido multilingüe.
- Generación de material accesible.
Entornos corporativos
- Transcripción de reuniones.
- Documentación audiovisual.
- Material de formación.
- Procesamiento de entrevistas.
Producción audiovisual
- Generación inicial de subtítulos durante la postproducción.
- Localización de contenido.
- Preparación de archivos para revisión editorial.
- Creación de subtítulos para accesibilidad.
Conclusión
Transcriptor Pro demuestra cómo un modelo de reconocimiento de voz puede convertirse en algo mucho más útil cuando se integra dentro de una aplicación completa.
Whisper proporciona la capacidad de reconocimiento; PyTorch proporciona el backend de inferencia; OpenCV permite trabajar con los frames; Pillow se ocupa del renderizado; PyGame gestiona el audio y CustomTkinter proporciona la interfaz.
La parte realmente interesante del proyecto aparece en la integración de todos estos componentes.
El sistema tiene que mantener una relación temporal coherente entre audio, texto, timestamps, reproducción y frames. Además, debe gestionar memoria, modelos de diferentes tamaños, operaciones potencialmente costosas y una interfaz que no puede quedarse bloqueada durante el procesamiento.
Por ello, el proyecto constituye un ejercicio especialmente interesante de ingeniería de software aplicada a inteligencia artificial: no se trata simplemente de ejecutar Whisper, sino de construir alrededor del modelo una infraestructura capaz de convertir su salida en una herramienta audiovisual utilizable.
Y precisamente ahí está la evolución natural del proyecto: pasar de un transcriptor de escritorio a un auténtico pipeline de procesamiento audiovisual, con edición temporal, traducción desacoplada, formatos profesionales de subtítulos, procesamiento por lotes, aceleración mediante GPU y una arquitectura preparada para integrarse con otros sistemas.
Visitas: 7
Automation & Data Specialist | Web Development | Data Processing & Integration

