Logotipo de Bitdoze

Editor de intros de vídeo con Python: Silero VAD + MoviePy en Mac

Crea un editor de intros de vídeo en Python con Silero VAD, MoviePy y FFmpeg. Detecta automáticamente el habla en tu metraje y genera un montaje de intro con estilo 3D en Mac.

Dragos

Actualizado Publicado 19 min de lectura

Salida de terminal de un editor de intros de vídeo en Python que usa Silero VAD para detectar el habla y renderizar un montaje de intro 3D en Mac

Cortar una intro a mano significa recorrer el metraje, buscar los buenos momentos y reconstruir el mismo montaje en cada subida. Esta guía te muestra cómo crear un editor de intros de vídeo en Python que lo hace por ti: analiza tu metraje, detecta cuándo habla alguien usando detección de actividad de voz (VAD) y renderiza un montaje de intro con estilo 3D.

El stack: Silero VAD encuentra el habla en local, FFmpeg se encarga del desenfoque y los efectos de perspectiva, MoviePy compone los cortes rápidos y uv ejecuta todo como un único script sin gestión manual de dependencias. El tutorial está pensado para Mac; el script en sí es Python puro.

Lo que aprenderás

  • Detectar el habla en local con Silero VAD, sin clave de API
  • Desenfocar y deformar clips en 3D con filtros de FFmpeg
  • Componer cortes rápidos con MoviePy: velocidad 3x, deslizamientos y fundidos
  • Ejecutarlo todo con un solo comando uv run gracias a las dependencias inline de PEP 723

Requisitos previos


  • FFmpeg instalado con Homebrew
  • uv instalado (un comando curl)
  • Un vídeo fuente con gente hablando
  • Unos GB libres en disco para la descarga única de PyTorch

FFmpeg

FFmpeg se encarga de la extracción de audio, el desenfoque y la deformación 3D. Instálalo con Homebrew:

bash
brew install ffmpeg

Comprueba que funciona:

bash
ffmpeg -version

Deberías ver una línea de versión como ffmpeg version 7.x. Si obtienes zsh: command not found: ffmpeg, Homebrew no lo enlazó; ejecuta brew link ffmpeg o reabre el terminal.

uv

uv gestiona la parte de Python, incluido el propio intérprete. Instálalo con:

bash
curl -LsSf https://astral.sh/uv/install.sh | sh

Compruébalo:

bash
uv --version

Sobre Python y el espacio en disco

El script declara requires-python = ">=3.10", y uv descarga y gestiona un Python compatible en la primera ejecución, así que no necesitas instalar ni configurar Python a mano. Si prefieres mantener un intérprete normal con venv en la máquina, sigue esta guía para instalar Python en Mac y configurar un entorno virtual.

Reserva unos gigabytes de espacio libre: la primera ejecución descarga PyTorch y los pesos del modelo Silero VAD. Todo lo demás queda en caché.

¿Nuevo en uv?

Si eres nuevo en uv, Primeros pasos con uv: configuración de proyectos Python en 2026 cubre la configuración de proyectos y los conceptos básicos del entorno. Para este artículo solo necesitas un comando: uv run.

Cómo funciona el editor de intros de vídeo en Python


El script ejecuta un pipeline directo desde el metraje en bruto hasta el montaje final:

  1. Extrae el audio de tu vídeo con FFmpeg como WAV mono de 16 kHz
  2. Ejecuta Silero VAD sobre el audio para encontrar dónde habla alguien
  3. Elige segmentos de voz de más de 2 segundos y toma una muestra de seis
  4. Construye un fondo desenfocado más un primer plano con deformación de perspectiva 3D por clip con FFmpeg
  5. Compone las capas en MoviePy a velocidad 3x con deslizamiento y fundido
  6. Concatena los clips y multiplexa una pista de audio muda en intro_only.mp4

Con los valores por defecto, son seis clips de 1,5 segundos cada uno: un montaje de unos 9 segundos con una pista de audio muda, listo para colocar bajo una base musical.

Diagrama del pipeline del editor de intros de vídeo en Python: FFmpeg extrae el audio, Silero VAD detecta el habla, FFmpeg deforma los clips y MoviePy compone la intro.input.mp4FFmpeg · extraer audio a WAV mono de 16 kHzSilero VAD · buscar marcas de vozsegmentos de voz +2 s · muestra de 6por clip × 6FFmpegfondo desenfocadoFFmpegperspectiva 3DMoviePy · 3x, deslizamientos, fundidosFFmpeg · añadir audio mudointro_only.mp4

El pipeline de la intro: FFmpeg extrae el audio, Silero VAD encuentra el habla, el selector toma una muestra de seis segmentos, FFmpeg construye el fondo desenfocado y el primer plano deformado en 3D por clip, MoviePy compone y concatena, y una pasada final de FFmpeg añade la pista de audio muda.

El script completo


Aquí está el script completo. Guárdalo como intro_generator.py; la cabecera PEP 723 de arriba le dice a uv exactamente qué dependencias fijadas instalar, así que uv run es el único comando que necesitas.

python
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "openai-whisper",
#     "torch",
#     "torchaudio<2.6",
#     "soundfile",
#     "numpy<2.0.0",
#     "moviepy==1.0.3",
#     "packaging",
#     "Pillow<10.0.0",
# ]
# ///

import os
import sys
import warnings

# 1. Suppress warnings BEFORE importing moviepy
# This hides the "invalid escape sequence" text
warnings.filterwarnings("ignore")

import random
import subprocess
from pathlib import Path

import torch
import whisper
from moviepy.editor import (
    CompositeVideoClip,
    VideoFileClip,
    concatenate_videoclips,
    vfx,
)

# --- Configuration ---
MIN_SILENCE = 0.5
MIN_SPEECH = 0.25
PADDING = 0.1

# Intro Style Settings
INTRO_CLIP_COUNT = 6  # Number of fast cuts
INTRO_SPEED = 3.0  # Speed multiplier (3x fast)
CLIP_DURATION = 1.5  # Duration of each clip in seconds
OUTPUT_FILENAME = "intro_only.mp4"


def check_ffmpeg():
    try:
        subprocess.run(["ffmpeg", "-version"], capture_output=True, check=True)
    except:
        print("❌ Error: FFmpeg not found. Run: brew install ffmpeg")
        sys.exit(1)


def extract_audio(video_path, audio_path):
    subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-i",
            video_path,
            "-vn",
            "-acodec",
            "pcm_s16le",
            "-ar",
            "16000",
            "-ac",
            "1",
            "-loglevel",
            "error",
            audio_path,
        ],
        check=True,
    )


def get_good_segments(audio_path):
    """Finds segments where people are actually speaking (Key Moments)."""
    print("🧠 Scanning video for key moments...")

    # Load Silero VAD
    # trust_repo=True fixes the "cache" warning/error
    model, utils = torch.hub.load(
        repo_or_dir="snakers4/silero-vad", model="silero_vad", trust_repo=True
    )
    (get_speech_timestamps, _, read_audio, _, _) = utils

    wav = read_audio(audio_path)
    vad_stamps = get_speech_timestamps(
        wav,
        model,
        threshold=0.5,
        min_speech_duration_ms=int(MIN_SPEECH * 1000),
        min_silence_duration_ms=int(MIN_SILENCE * 1000),
    )

    segments = []
    for v in vad_stamps:
        segments.append((v["start"] / 16000, v["end"] / 16000))

    return segments


def apply_blur_background(input_path, output_path):
    subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-i",
            input_path,
            "-vf",
            "boxblur=40:5,eq=brightness=-0.4",
            "-c:v",
            "libx264",
            "-preset",
            "ultrafast",
            "-an",
            "-loglevel",
            "error",
            output_path,
        ],
        check=True,
    )


def apply_3d_warp(input_path, output_path, direction="left"):
    if direction == "left":
        vf = "perspective=x0=0:y0=0:x1=W:y1=H/5:x2=0:y2=H:x3=W:y3=4*H/5:sense=destination"
    else:
        vf = "perspective=x0=0:y0=H/5:x1=W:y1=0:x2=0:y2=4*H/5:x3=W:y3=H:sense=destination"

    vf += ",pad=w=iw+100:h=ih+100:x=50:y=50:color=black@0"

    subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-i",
            input_path,
            "-vf",
            vf,
            "-c:v",
            "libx264",
            "-preset",
            "ultrafast",
            "-an",
            "-loglevel",
            "error",
            output_path,
        ],
        check=True,
    )


def generate_intro(video_path, segments):
    print("✨ Rendering 3D Intro (No Text)...")

    long_segments = [s for s in segments if (s[1] - s[0]) > 2.0]

    if len(long_segments) < INTRO_CLIP_COUNT:
        print(f"⚠️ Not enough footage found. Need {INTRO_CLIP_COUNT} distinct moments.")
        if not long_segments:
            return
        picks = random.choices(long_segments, k=INTRO_CLIP_COUNT)
    else:
        picks = sorted(random.sample(long_segments, INTRO_CLIP_COUNT))

    intro_clips = []
    temp_files = []

    for i, (start, end) in enumerate(picks):
        raw_dur = CLIP_DURATION * INTRO_SPEED
        mid = start + (end - start) / 2 - (raw_dur / 2)

        raw_clip = f"temp_raw_{i}.mp4"
        bg_clip = f"temp_bg_{i}.mp4"
        fg_clip = f"temp_fg_{i}.mp4"

        # 1. Extract
        subprocess.run(
            [
                "ffmpeg",
                "-y",
                "-ss",
                str(mid),
                "-t",
                str(raw_dur),
                "-i",
                video_path,
                "-c:v",
                "libx264",
                "-an",
                "-loglevel",
                "error",
                raw_clip,
            ],
            check=True,
        )

        # 2. Process
        apply_blur_background(raw_clip, bg_clip)

        direction = "left" if i % 2 == 0 else "right"
        apply_3d_warp(raw_clip, fg_clip, direction)

        # 3. Composite
        try:
            bg = VideoFileClip(bg_clip).fx(vfx.speedx, INTRO_SPEED)
            fg = VideoFileClip(fg_clip).fx(vfx.speedx, INTRO_SPEED)

            if direction == "left":
                fg = fg.set_position(
                    lambda t: (int(-50 + 50 * (t / CLIP_DURATION)), "center")
                )
            else:
                fg = fg.set_position(
                    lambda t: (int(50 - 50 * (t / CLIP_DURATION)), "center")
                )

            comp = CompositeVideoClip([bg, fg]).set_duration(CLIP_DURATION)
            if i > 0:
                comp = comp.crossfadein(0.2)

            intro_clips.append(comp)
            temp_files.extend([raw_clip, bg_clip, fg_clip])

        except Exception as e:
            print(f"   ⚠️ Error processing clip {i}: {e}")

    if not intro_clips:
        print("❌ Failed to generate intro clips.")
        return

    # Concatenate
    full_montage = concatenate_videoclips(intro_clips, method="compose")

    print("   💾 Saving video file...")
    full_montage.write_videofile(
        "temp_visual_intro.mp4", fps=24, codec="libx264", logger=None
    )

    # Add silent audio
    print("   🔊 Adding silent audio track...")
    subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-i",
            "temp_visual_intro.mp4",
            "-f",
            "lavfi",
            "-i",
            "anullsrc=channel_layout=mono:sample_rate=44100",
            "-c:v",
            "copy",
            "-c:a",
            "aac",
            "-shortest",
            "-loglevel",
            "error",
            OUTPUT_FILENAME,
        ],
        check=True,
    )

    # Cleanup
    for f in temp_files:
        if os.path.exists(f):
            os.remove(f)
    if os.path.exists("temp_visual_intro.mp4"):
        os.remove("temp_visual_intro.mp4")

    print(f"✅ Success! Intro saved as: {OUTPUT_FILENAME}")


def main():
    if len(sys.argv) < 2:
        print("Usage: uv run intro_generator.py <video.mp4>")
        sys.exit(1)

    input_video = sys.argv[1]
    check_ffmpeg()

    temp_wav = "temp_analysis.wav"

    try:
        extract_audio(input_video, temp_wav)

        good_parts = get_good_segments(temp_wav)

        if not good_parts:
            print("⚠️ No speech detected. Picking random segments...")
            duration = float(
                subprocess.check_output(
                    [
                        "ffprobe",
                        "-v",
                        "error",
                        "-show_entries",
                        "format=duration",
                        "-of",
                        "default=noprint_wrappers=1:nokey=1",
                        input_video,
                    ]
                )
            )
            good_parts = [(t, t + 5) for t in range(0, int(duration), 10)]

        generate_intro(input_video, good_parts)

    finally:
        if os.path.exists(temp_wav):
            os.remove(temp_wav)


if __name__ == "__main__":
    main()

Entendiendo el script


El script usa metadatos inline de PEP 723 para declarar sus dependencias:

python
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "openai-whisper",
#     "torch",
#     "torchaudio<2.6",
#     "soundfile",
#     "numpy<2.0.0",
#     "moviepy==1.0.3",
#     "packaging",
#     "Pillow<10.0.0",
# ]
# ///

Cuando lo ejecutas, uv crea un entorno aislado, instala exactamente estas versiones y no toca nada de tu Python global. El patrón se cubre con más detalle en uv run: ejecuta scripts de Python sin gestión de dependencias.

Variables de configuración

Variable Por defecto Descripción
MIN_SILENCE 0.5 Duración mínima de silencio en segundos
MIN_SPEECH 0.25 Duración mínima de voz en segundos
INTRO_CLIP_COUNT 6 Número de clips de la intro
INTRO_SPEED 3.0 Multiplicador de velocidad de los clips
CLIP_DURATION 1.5 Duración de cada clip en segundos
OUTPUT_FILENAME “intro_only.mp4” Nombre del archivo de salida

Dos de ellas controlan la duración de la intro: el tiempo total siempre es INTRO_CLIP_COUNT × CLIP_DURATION, así que los valores por defecto producen un montaje de 9 segundos.

Detección de actividad de voz (VAD)

La detección de voz es lo que convierte esto en un editor en lugar de un muestreador aleatorio de clips. get_good_segments() carga Silero VAD a través de torch.hub y le pide todas las ventanas donde alguien está hablando:

python
def get_good_segments(audio_path):
    """Finds segments where people are actually speaking (Key Moments)."""
    model, utils = torch.hub.load(
        repo_or_dir="snakers4/silero-vad", model="silero_vad", trust_repo=True
    )
    (get_speech_timestamps, _, read_audio, _, _) = utils

    wav = read_audio(audio_path)
    vad_stamps = get_speech_timestamps(
        wav,
        model,
        threshold=0.5,
        min_speech_duration_ms=int(MIN_SPEECH * 1000),
        min_silence_duration_ms=int(MIN_SILENCE * 1000),
    )

    return [(v["start"] / 16000, v["end"] / 16000) for v in vad_stamps]

Silero VAD es la opción habitual para la detección de actividad de voz en Python: el modelo ocupa unos dos megabytes, funciona en CPU y corre completamente offline tras la descarga inicial. No necesitas clave de API y no hay coste por minuto. Devuelve posiciones de inicio y fin en muestras a 16 kHz, que la función convierte a segundos. El argumento threshold=0.5 es el dial de sensibilidad: bájalo si el habla baja se escapa, súbelo si el ruido de fondo produce falsos positivos.

Efectos de perspectiva 3D con FFmpeg

El aspecto 3D viene del filtro perspective de FFmpeg, aplicado a cada clip con direcciones alternas:

python
def apply_3d_warp(input_path, output_path, direction="left"):
    if direction == "left":
        vf = "perspective=x0=0:y0=0:x1=W:y1=H/5:x2=0:y2=H:x3=W:y3=4*H/5:sense=destination"
    else:
        vf = "perspective=x0=0:y0=H/5:x1=W:y1=0:x2=0:y2=4*H/5:x3=W:y3=H:sense=destination"

sense=destination le dice a FFmpeg que mapee los cuatro puntos de esquina de salida sobre el origen. Eso comprime el fotograma en un trapecio en vez de estirarlo: un borde se inclina hacia dentro mientras el opuesto queda fijo. El filtro pad añadido tras la cadena de perspectiva agrega 50 píxeles de margen transparente en cada lado, para que el clip deformado pueda deslizarse sobre el fondo desenfocado después sin bordes duros.

Composición de vídeo con MoviePy

MoviePy apila cada clip como un fondo desenfocado con un primer plano deformado deslizándose por encima:

python
bg = VideoFileClip(bg_clip).fx(vfx.speedx, INTRO_SPEED)
fg = VideoFileClip(fg_clip).fx(vfx.speedx, INTRO_SPEED)

if direction == "left":
    fg = fg.set_position(
        lambda t: (int(-50 + 50 * (t / CLIP_DURATION)), "center")
    )

comp = CompositeVideoClip([bg, fg]).set_duration(CLIP_DURATION)

Ambas capas se aceleran 3x con vfx.speedx, el primer plano se mueve 50 píxeles hacia el centro durante los 1,5 segundos del clip y CompositeVideoClip aplana la pila. Cada clip terminado recibe un crossfadein de 0,2 segundos, y luego concatenate_videoclips apila los seis en el montaje.

Por qué MoviePy está fijado a 1.0.3

MoviePy 2.0 se lanzó a finales de 2024 con una reescritura incompatible: el módulo moviepy.editor ya no existe, set_position pasó a ser with_position y el encadenamiento de .fx() cambió. Este script importa directamente de moviepy.editor, así que cualquier instalación 2.x falla en el import.

Ese pin es lo que mantiene el script reproducible. uv lee la cabecera, instala MoviePy 1.0.3 en un entorno limitado a este archivo y nada más en tu máquina cambia. Migrar a la API 2.x es un proyecto razonable (el estilo with_position es más limpio), pero es una reescritura del código de composición, no un cambio de versión. Si el error de import aparece de todos modos, la solución está en la sección de solución de problemas más abajo.

Ejecutar el script


Con uv y FFmpeg instalados:

bash
uv run intro_generator.py your_video.mp4

La primera ejecución es más lenta: uv resuelve e instala las dependencias fijadas (PyTorch es la grande) y torch.hub descarga el modelo Silero VAD una vez. Después todo queda en caché y las ejecuciones posteriores solo pagan el render.

Como todo es uv más FFmpeg, el script también viaja bien: si quieres un entorno reproducible en un servidor en vez de tu Mac, mira cómo ejecutar cualquier app Python en Docker.

Salida esperada

text
🧠 Scanning video for key moments...
✨ Rendering 3D Intro (No Text)...
   💾 Saving video file...
   🔊 Adding silent audio track...
✅ Success! Intro saved as: intro_only.mp4

Verificar el resultado

Tres comprobaciones antes de darlo por terminado.

La duración debería acercarse a los 9 segundos con los valores por defecto (6 clips × 1,5 s):

bash
ffprobe -v error -show_entries format=duration \
  -of default=noprint_wrappers=1:nokey=1 intro_only.mp4

El stream de audio debe existir y ser aac, que es la pista muda que el script multiplexa:

bash
ffprobe -v error -select_streams a -show_entries stream=codec_name \
  -of default=noprint_wrappers=1:nokey=1 intro_only.mp4

Salida esperada: aac. Si no imprime nada, el paso del audio mudo falló y los editores se quejarán de una pista ausente.

Por último, la comprobación visual: open intro_only.mp4. Deberías ver seis cortes rápidos alternando deformaciones 3D a izquierda y derecha sobre un fondo desenfocado y oscurecido, con fundidos rápidos entre clips. Cuando falla: una duración lejos de 9 segundos significa que algunos clips dieron error durante el procesado (busca las líneas con ⚠️ en el terminal), y una imagen plana sin deformar significa que la cadena del filtro de perspectiva se rompió en una edición.

Personalizar el resultado


Edita el bloque de configuración al inicio del script para cambiar el estilo.

Cambiar el número de clips

python
INTRO_CLIP_COUNT = 8  # More clips

Ajustar la velocidad

python
INTRO_SPEED = 2.0  # Slower
INTRO_SPEED = 4.0  # Faster

Cambiar la duración de los clips

python
CLIP_DURATION = 2.0  # Longer clips
CLIP_DURATION = 1.0  # Shorter clips

Modificar la intensidad del desenfoque

Edita la cadena de filtro en apply_blur_background():

python
# Stronger blur
"-vf", "boxblur=60:10,eq=brightness=-0.5"

# Lighter blur
"-vf", "boxblur=20:3,eq=brightness=-0.2"

Preajustes de estilo

Tres puntos de partida si no quieres ajustar cuatro mandos desde cero. Cada uno cambia el bloque de configuración, y los preajustes que mencionan desenfoque también tocan la cadena de filtro en apply_blur_background().

Por defecto (rápida)

python
INTRO_CLIP_COUNT = 6
INTRO_SPEED = 3.0
CLIP_DURATION = 1.5
python
# in apply_blur_background()
"boxblur=40:5,eq=brightness=-0.4"

Total: 6 × 1.5 = 9 segundos. El comportamiento original.

Relajada

python
INTRO_CLIP_COUNT = 4
INTRO_SPEED = 2.0
CLIP_DURATION = 2.0
python
# in apply_blur_background()
"boxblur=20:3,eq=brightness=-0.2"

Total: 8 segundos. Cortes más lentos, fondo más limpio, más fácil de seguir.

Cinematográfica

python
INTRO_CLIP_COUNT = 5
INTRO_SPEED = 1.5
CLIP_DURATION = 2.5
python
# in apply_blur_background()
"boxblur=40:5,eq=brightness=-0.5"

Total: 12.5 segundos. Movimiento más lento, fondo más oscuro, más espacio para un título superpuesto.

Son puntos de partida, no datos de calibración. El montaje se renderiza con pista de audio muda a propósito, así que superpón música o una voz en off en tu editor.

Dependencias explicadas


Paquete Propósito
openai-whisper Kit de reconocimiento de voz; se importa arriba pero nunca se llama. Silero VAD hace la detección, así que puedes quitar esta línea y el import para una instalación más ligera
torch PyTorch para ejecutar el modelo VAD
torchaudio Procesamiento de audio con PyTorch; proporciona el helper read_audio que usa Silero VAD
soundfile Backend de decodificación de audio que torchaudio puede usar al cargar el WAV
numpy Operaciones numéricas
moviepy Edición y composición de vídeo
Pillow Procesamiento de imágenes (requerido por MoviePy)
packaging Utilidades de gestión de versiones

Restricciones de versión

Los pins de la cabecera PEP 723 son estructurales, no decoración:

  • moviepy==1.0.3: MoviePy 2.x eliminó el módulo moviepy.editor, así que el import falla en cualquier instalación 2.x. Mira «Por qué MoviePy está fijado a 1.0.3» más arriba.
  • torchaudio<2.6: evita problemas de emparejamiento torch/torchaudio en Apple Silicon. La sección de solución de problemas de abajo cubre instalaciones lentas o fallidas.
  • Pillow<10.0.0: MoviePy 1.0.3 aún referencia Image.ANTIALIAS, que Pillow 10 eliminó.
  • numpy<2.0.0: mantiene el stack de audio en la ABI de numpy 1.x que esperan esas builds de torch.

Solución de problemas


Error: FFmpeg no encontrado

Instala FFmpeg con Homebrew:

bash
brew install ffmpeg

Comprueba la instalación:

bash
ffmpeg -version
Advertencia: no hay metraje suficiente

Esto ocurre cuando el vídeo no tiene suficientes segmentos de habla de más de 2 segundos. El script reutiliza segmentos en ese caso. Prueba a:

  • Usar un vídeo fuente más largo
  • Reducir INTRO_CLIP_COUNT
  • Bajar la duración mínima de segmento en el código
Errores de memoria con vídeos grandes

Para vídeos grandes, prueba a:

  • Procesar clips más cortos
  • Reducir la resolución del vídeo fuente
  • Cerrar otras aplicaciones
La primera ejecución es lenta

La primera ejecución descarga el modelo Silero VAD y las dependencias de Python. Las ejecuciones posteriores son más rápidas porque uv y PyTorch cachean estos archivos.

ImportError: cannot import name 'editor' from 'moviepy'

Estás ejecutando MoviePy 2.x. Esto ocurre cuando el script se ejecuta con un intérprete distinto de uv run (un moviepy 2.x instalado con pip ensombrece al fijado) o cuando la cabecera PEP 723 perdió el pin moviepy==1.0.3 en una edición.

Soluciones:

  • Ejecuta el script exactamente como se documenta: uv run intro_generator.py your_video.mp4
  • Confirma que la cabecera aún contiene moviepy==1.0.3
  • Si migraste a 2.x a propósito, actualiza la API: moviepy.editor ya no existe (importa los clips desde moviepy directamente), set_position pasó a with_position y la sintaxis .fx(vfx.speedx, ...) cambió
La instalación de torch o torchaudio falla o va lenta en Apple Silicon

Ve siguiendo estos pasos en orden:

  • Confirma que el pin torchaudio<2.6 sigue en la cabecera. Un torchaudio sin pin puede traer una build que no coincide con tu versión de torch.
  • Limpia la caché y reintenta: uv cache clean, y luego repite el mismo comando uv run. Una descarga a medias es la causa más común.
  • Comprueba que uv eligió un Python arm64 nativo: uv python list. Un intérprete x86_64 corriendo bajo Rosetta produce instalaciones lentas y fallos aleatorios de wheels.
No se detecta voz en metraje solo con música o B-roll

Comportamiento esperado, no un bug: Silero VAD escucha voces, así que el metraje solo con música no produce marcas de tiempo y el script recurre a segmentos aleatorios espaciados cada 10 segundos.

Si quieres mejores elecciones en metraje silencioso o mixto:

  • Baja el threshold del VAD de 0.5 a unos 0.4 en get_good_segments(). Más sensible, pero más falsos positivos.
  • Baja el filtro de segmento mínimo de 2.0 segundos en generate_intro() para que califiquen ráfagas de voz más cortas.
  • Dale metraje de gente hablando. Para eso está hecho el detector.

Preguntas frecuentes


¿Silero VAD necesita clave de API o acceso a internet?

No. torch.hub descarga el modelo una vez en la primera ejecución; después, la detección de actividad de voz con Silero VAD corre completamente offline en tu CPU. No necesitas cuenta ni clave de API, y no hay facturación por minuto. El modelo ocupa unos dos megabytes, que es también por qué el arranque es rápido.

¿Funciona este editor de intros de vídeo en Python en Linux o Windows?

El script es Python puro, FFmpeg y paquetes pip, todo multiplataforma. Solo cambia el comando de instalación de FFmpeg: apt install ffmpeg en Debian/Ubuntu, winget install ffmpeg en Windows. Advertencia honesta: este tutorial está escrito en un Mac y no lo he vuelto a ejecutar en otros sistemas, así que trata Linux y Windows como «debería funcionar» hasta que lo ejecutes una vez tú mismo.

¿Por qué la intro renderizada es muda?

Por diseño. El script elimina el audio fuente y multiplexa una pista AAC muda para que el montaje encaje limpiamente bajo música o narración en tu editor sin pelear con el sonido original. Si quieres audio en el archivo, añádelo en postproducción o genera primero una voz en off.

¿Cuánto dura la intro?

INTRO_CLIP_COUNT × CLIP_DURATION. Los valores por defecto dan 6 × 1.5 = 9 segundos. Los preajustes de estilo de arriba producen 8 segundos (Relajada) y 12,5 segundos (Cinematográfica). Para una intro de YouTube, quedarse por debajo de 10 segundos es una buena regla; más que eso y los espectadores empiezan a saltársela.

Conclusión


Ese es todo el pipeline: un editor de intros de vídeo en Python donde Silero VAD encuentra los momentos hablados, FFmpeg hace el trabajo de desenfoque y perspectiva 3D, MoviePy compone los cortes y uv fija cada dependencia al propio archivo del script. Apúntalo a cualquier vídeo en bruto con gente hablando y obtienes un montaje de nueve segundos listo para colocar bajo una base musical.

Formas de llevarlo más lejos: