En este momento estás viendo [Python] Automatiza la transcripción y subtítulos de tus vídeos con Python y Whisper AI

[Python] Automatiza la transcripción y subtítulos de tus vídeos con Python y Whisper AI

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

  1. Iniciar la aplicación.
  2. Seleccionar el modelo Whisper.
  3. Cargar el archivo de vídeo.
  4. Iniciar la transcripción.
  5. Esperar a que finalice la inferencia.
  6. Revisar los segmentos generados.
  7. Corregir manualmente los errores necesarios.
  8. Aplicar traducción si procede.
  9. Previsualizar los subtítulos.
  10. 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

Website |  + posts

Automation & Data Specialist | Web Development | Data Processing & Integration

Serna Studio

Automation & Data Specialist | Web Development | Data Processing & Integration

Deja una respuesta