¿Qué hace moviepy (Composición de video en Python)?
Ideal para videos cortos y guionizados, y para colocar texto exacto sobre metraje. Para videos largos, con mucho diseño y reutilizables, prefiere Remotion.
| Usa moviepy cuando | Usa Remotion cuando |
|---|---|
| El proyecto es un solo script de Python | Necesitas componentes de React y un sistema de diseño |
| Superponer texto sobre clips existentes o generados por IA | Gráficos animados complejos y gráficas |
| Ediciones por lotes de muchos archivos | La vista previa con recarga en caliente es importante |
| No hay cadena de herramientas de Node disponible | Videos largos y plantillados |
Configuración
pip install "moviepy>=2" pillow
# o: uv add moviepy pillow
Este skill está orientado a moviepy 2.x. Su API difiere de la 1.x: las importaciones vienen directamente de moviepy, y los métodos son with_*/subclipped/resized, no set_*/subclip/resize.
Conceptos básicos
from moviepy import (VideoFileClip, AudioFileClip, ImageClip, TextClip,
CompositeVideoClip, CompositeAudioClip, concatenate_videoclips, vfx, afx)
clip = VideoFileClip("in.mp4").subclipped(5, 20) # extracto de 15 s
clip = clip.resized(height=1080) # mantener aspecto
clip = clip.with_effects([vfx.FadeIn(0.5), vfx.FadeOut(0.5)])
joined = concatenate_videoclips([clip, VideoFileClip("b.mp4")], method="compose")
joined.write_videofile("out/resultado.mp4", fps=30, codec="libx264", audio_codec="aac")
Siempre llama a .close() en los clips en scripts largos, o úsalos en un bloque with, para liberar los manejadores de archivos.
Superposición de texto (texto determinista sobre metraje)
titulo = (TextClip(font="C:/Windows/Fonts/arial.ttf", # ruta real a un archivo de fuente
text="Dra. Jane Doe\nJefa de Ciencias",
font_size=56, color="white",
stroke_color="black", stroke_width=2,
method="caption", size=(900, None))
.with_start(1.0).with_duration(4.0)
.with_position(("left", 860))
.with_effects([vfx.CrossFadeIn(0.3), vfx.CrossFadeOut(0.3)]))
final = CompositeVideoClip([clip, titulo])
Notas:
TextClipnecesita una ruta de archivo de fuente (TTF/OTF). Incluye la fuente de la marca en el proyecto.- Los tiempos de los clips están en segundos, relativos al inicio del compuesto.
- Para texto con mucho estilo (píldoras redondeadas, degradados), renderiza un PNG con Pillow y usa
ImageClip. - Mantén el texto dentro del área segura: al menos 5% desde cada borde.
Audio
voz = AudioFileClip("voz_en_off.mp3")
musica = AudioFileClip("musica.mp3").with_effects([afx.MultiplyVolume(0.12), afx.AudioFadeOut(2)])
musica = musica.with_duration(voz.duration + 1)
final = clip.with_audio(CompositeAudioClip([musica, voz.with_start(0.5)]))
Deja que la voz en off decida la línea de tiempo: establece la duración del clip a voz.duration + padding, no al revés.
Patrón anclado al audio
escenas = [("escena1.mp3", "intro.png"), ("escena2.mp3", "grafica.png")]
partes = []
for audio_path, imagen_path in escenas:
a = AudioFileClip(audio_path)
v = ImageClip(imagen_path).with_duration(a.duration + 0.6).with_audio(a)
partes.append(v)
concatenate_videoclips(partes).write_videofile("out/explicativo.mp4", fps=30)
Ken Burns en imágenes fijas
img = ImageClip("foto.jpg").with_duration(6)
img = img.resized(lambda t: 1.0 + 0.04 * t).with_position("center")
CompositeVideoClip([img], size=(1920, 1080)).write_videofile("out/kb.mp4", fps=30)
Rendimiento
- Previsualiza con
fps=12ypreset="ultrafast"; renderiza la versión final a 30 fps. write_videofile(..., threads=4).- Evita lambdas de Python por fotograma en clips largos. Pre-procesa con FFmpeg cuando sea posible.
Problemas comunes
| Problema | Causa y solución |
|---|---|
ImportError: cannot import name 'editor' | moviepy 2 eliminó moviepy.editor; importa desde moviepy |
AttributeError: set_duration | Usa with_duration; igual para with_start, with_position |
| El clip de texto falla | Pasa una ruta real a un archivo de fuente |
| Audio desincronizado después de unir | Asegúrate de que todos los clips compartan el mismo fps; aplica el audio después de la concatenación |
| Renderizado lento | Reduce la resolución para borradores; evita lambdas grandes en resized |