Chat, eu uso esse código abaixo

import os
import json
from PIL import Image, ImageDraw, ImageFont
import numpy as np
from moviepy.editor import VideoFileClip, AudioFileClip, ImageClip, concatenate_videoclips, TextClip, CompositeVideoClip, ImageSequenceClip, transfx
from pydub import AudioSegment
from pydub.utils import db_to_float
import tkinter as tk
from tkinter import filedialog, scrolledtext
import pysrt
import shutil
from moviepy.config import change_settings
import datetime
import tempfile
import traceback
import math
import cv2

# Nova função para transição de slide suave
EFFECT_DURATION = 0.4  # Duração do efeito de slide
CLIP_DURATION = 8  # Duração da imagem

def Zoom(clip, mode='in', position='center', speed=1, fps=120):
    duration = clip.duration
    total_frames = int(duration * fps)
    def main(getframe, t):
        frame = getframe(t)
        h, w = frame.shape[:2]
        i = t * fps
        if mode == 'out':
            i = total_frames - i
        zoom = 1 + (i * ((0.1 * speed) / total_frames))
        positions = {'center': [(w - (w * zoom)) / 2, (h - (h * zoom)) / 2],
                     'left': [0, (h - (h * zoom)) / 2],
                     'right': [(w - (w * zoom)), (h - (h * zoom)) / 2],
                     'top': [(w - (w * zoom)) / 2, 0],
                     'topleft': [0, 0],
                     'topright': [(w - (w * zoom)), 0],
                     'bottom': [(w - (w * zoom)) / 2, (h - (h * zoom))],
                     'bottomleft': [0, (h - (h * zoom))],
                     'bottomright': [(w - (w * zoom)), (h - (h * zoom))]}
        tx, ty = positions[position]
        M = np.array([[zoom, 0, tx], [0, zoom, ty]])
        frame = cv2.warpAffine(frame, M, (w, h))
        return frame
    return clip.fl(main)

def apply_zoom_transition(clip, duration, factor=1.0, fps=120):
    return Zoom(clip, mode='in', position='center', speed=factor, fps=fps)

class VideoCreatorApp:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Video Creator")

        self.images_paths = []
        self.voice_path = ""
        self.random_sounds_paths = []
        self.subtitle_path = ""
        self.output_video_path = ""

        self.error_log = []

        # Set the paths to the ImageMagick and FFmpeg executables
        imagemagick_path = r"C:\Program Files\ImageMagick-7.1.1-Q16-HDRI\magick.exe"
        ffmpeg_path = r"C:\ProgramData\chocolatey\bin\ffmpeg.exe"
        change_settings({"IMAGEMAGICK_BINARY": imagemagick_path, "FFMPEG_BINARY": ffmpeg_path})

        # Define functions before creating buttons
        self.select_images = self.select_images
        self.select_voice = self.select_voice
        self.select_random_sounds = self.select_random_sounds
        self.select_subtitle = self.select_subtitle
        self.select_output_video = self.select_output_video
        self.render_video = self.render_video

        # Upload Files Section
        self.upload_label = tk.Label(self.root, text="Upload Files:")
        self.upload_label.pack()

        self.select_images_button = tk.Button(
            self.root, text="Select Images", command=self.select_images
        )
        self.select_images_button.pack()

        self.select_voice_button = tk.Button(
            self.root, text="Select Voice", command=self.select_voice
        )
        self.select_voice_button.pack()

        self.select_random_sounds_button = tk.Button(
            self.root, text="Select Random Sounds", command=self.select_random_sounds
        )
        self.select_random_sounds_button.pack()

        self.select_subtitle_button = tk.Button(
            self.root, text="Select Subtitle (SRT)", command=self.select_subtitle
        )
        self.select_subtitle_button.pack()

        # Output Video Section
        self.output_video_label = tk.Label(self.root, text="Output Video:")
        self.output_video_label.pack()
        self.output_video_button = tk.Button(
            self.root, text="Select Output Video Path", command=self.select_output_video
        )
        self.output_video_button.pack()

        # JSON Configuration Section
        self.config_label = tk.Label(self.root, text="JSON Configuration:")
        self.config_label.pack()
        self.config_text = tk.Text(self.root, height=10, width=40)
        self.config_text.pack()

        # Render Video Section
        self.render_video_button = tk.Button(
            self.root, text="Render Video", command=self.render_video
        )
        self.render_video_button.pack()

        # Backlog Section
        self.backlog_label = tk.Label(self.root, text="Backlog:")
        self.backlog_label.pack()
        self.backlog_text = scrolledtext.ScrolledText(self.root, height=10, width=40)
        self.backlog_text.pack()

    def select_images(self) -> None:
        try:
            images_paths = filedialog.askopenfilenames(
                title="Select Images", filetypes=[("Images", "*.jpg;*.png")]
            )
            self.images_paths = list(images_paths)
            self.backlog_text.insert(tk.END, "Images selected\n")
        except Exception as e:
            self.error_log.append(f"Error selecting images: {str(e)}")
            self.backlog_text.insert(tk.END, f"Error selecting images: {str(e)}\n")

    def select_voice(self) -> None:
        try:
            voice_path = filedialog.askopenfilename(
                title="Select Voice", filetypes=[("Audio files", "*.mp3;*.wav")]
            )
            self.voice_path = voice_path
            self.backlog_text.insert(tk.END, "Voice selected\n")
        except Exception as e:
            self.error_log.append(f"Error selecting voice: {str(e)}")
            self.backlog_text.insert(tk.END, f"Error selecting voice: {str(e)}\n")

    def select_random_sounds(self) -> None:
        try:
            random_sounds_paths = filedialog.askopenfilenames(
                title="Select Random Sounds", filetypes=[("Audio Files", "*.mp3;*.wav")]
            )
            self.random_sounds_paths = list(random_sounds_paths)
            self.backlog_text.insert(tk.END, "Random sounds selected\n")
        except Exception as e:
            self.error_log.append(f"Error selecting random sounds: {str(e)}")
            self.backlog_text.insert(tk.END, f"Error selecting random sounds: {str(e)}\n")

    def select_subtitle(self) -> None:
        try:
            subtitle_path = filedialog.askopenfilename(
                title="Select Subtitle (SRT)", filetypes=[("SRT Files", "*.srt")]
            )
            self.subtitle_path = subtitle_path
            self.backlog_text.insert(tk.END, "Subtitle selected\n")
        except Exception as e:
            self.error_log.append(f"Error selecting subtitle: {str(e)}")
            self.backlog_text.insert(tk.END, f"Error selecting subtitle: {str(e)}\n")

    def select_output_video(self) -> None:
        try:
            output_video_path = filedialog.asksaveasfilename(
                title="Select Output Video Path",
                defaultextension=".mp4",
                filetypes=[("MP4 Files", "*.mp4")]
            )
            if output_video_path:
                self.output_video_path = output_video_path
                self.backlog_text.insert(tk.END, f"Output video path selected: {self.output_video_path}\n")
            else:
                self.error_log.append("No output video path selected")
                self.backlog_text.insert(tk.END, "No output video path selected\n")
        except Exception as e:
            self.error_log.append(f"Error selecting output video path: {str(e)}")
            self.backlog_text.insert(tk.END, f"Error selecting output video path: {str(e)}\n")

    def render_video(self) -> None:
        try:
            config_text = self.config_text.get("1.0", "end-1c")
            if not config_text.strip():
                raise ValueError("Configuration is empty")

            try:
                config = json.loads(config_text)
            except json.JSONDecodeError as e:
                self.error_log.append(f"JSON Error: {str(e)}")
                self.backlog_text.insert(tk.END, f"JSON Error: {str(e)}\n")
                return

            if 'images' not in config or 'output' not in config:
                self.error_log.append("Invalid configuration: missing 'images' or 'output'")
                self.backlog_text.insert(tk.END, "Invalid configuration: missing 'images' or 'output'\n")
                return

            if 'width' not in config['output'] or 'height' not in config['output']:
                self.error_log.append("Invalid configuration: missing 'width' or 'height' in 'output'")
                self.backlog_text.insert(tk.END, "Invalid configuration: missing 'width' or 'height' in 'output'\n")
                return

            if not self.images_paths:
                self.error_log.append("No images selected")
                self.backlog_text.insert(tk.END, "No images selected\n")
                return

            width, height = config['output']['width'], config['output']['height']
            images = [cv2.resize(cv2.imread(path), (width, height)) for path in self.images_paths if os.path.isfile(path)]
            if len(images) != len(self.images_paths):
                self.error_log.append("Some selected images do not exist")
                self.backlog_text.insert(tk.END, "Some selected images do not exist\n")
                return

            # Verifica se todas as imagens têm o mesmo tamanho
            sizes = [image.shape[:2] for image in images]
            if len(set(sizes)) > 1:
                raise ValueError("Todas as imagens devem ter o mesmo tamanho")

            # Cria clipes de vídeo para as imagens
            image_clips = []
            for i, image_config in enumerate(config['images']):
                image_path = self.images_paths[i]
                image_duration = image_config['duration']
                image_effect = image_config.get('effect', None)
                image_transition = image_config.get('transition', None)

                if isinstance(image_path, str):
                    image_clip = ImageClip(image_path).set_duration(image_duration)
                else:
                    continue

                # Aplica os efeitos
                if image_effect == 'zoom':
                    image_clip = apply_zoom_transition(image_clip, duration=image_duration, factor=image_config.get('zoom', 1.0), fps=120)

                # Adiciona os clipes de imagem a uma lista
                image_clips.append(image_clip)

            # Cria os clipes de transição de slide
            first_clip = CompositeVideoClip(
                [image_clips[0].fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")]
            ).set_start((CLIP_DURATION - EFFECT_DURATION) * 0)

            last_clip = CompositeVideoClip(
                [image_clips[-1].fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
            ).set_start((CLIP_DURATION - EFFECT_DURATION) * (len(image_clips) - 1))

            middle_clips = [
                (
                    CompositeVideoClip(
                        [clip.fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
                    )
                    .set_start((CLIP_DURATION - EFFECT_DURATION) * idx)
                    .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
                )
                for idx, clip in enumerate(image_clips[1:-1], start=1)
            ]

            videos = [first_clip] + middle_clips + [last_clip]

            # Cria um único CompositeVideoClip com todos os clipes de imagem
            video_clip = CompositeVideoClip(videos)

            # Cria clipes de texto para as legendas
            if self.subtitle_path:
                subtitles = pysrt.open(self.subtitle_path)
                text_clips = []
                for subtitle in subtitles:
                    start_time = subtitle.start
                    start_delta = datetime.timedelta(
                        hours=start_time.hours,
                        minutes=start_time.minutes,
                        seconds=start_time.seconds,
                        milliseconds=start_time.milliseconds
                    )
                    duration = datetime.timedelta(
                        hours=subtitle.duration.hours,
                        minutes=subtitle.duration.minutes,
                        seconds=subtitle.duration.seconds,
                        milliseconds=subtitle.duration.milliseconds
                    )
                    text_clip = TextClip(subtitle.text, fontsize=24, color='white')
                    text_clip = text_clip.set_start(start_delta.total_seconds())
                    text_clip = text_clip.set_duration(duration.total_seconds())
                    text_clips.append(text_clip)

            # Cria um clipe de vídeo composto com o vídeo e os clipes de texto
            if text_clips:
                final_clip = CompositeVideoClip([video_clip] + text_clips)
                final_clip = final_clip.fx(transfx.fadein, duration=3, initial_color=(0, 0, 0))
            else:
                final_clip = video_clip

            # Adiciona a voz e o áudio ao clipe final
            voice = AudioSegment.from_file(self.voice_path)
            if self.random_sounds_paths:
                mixer = AudioSegment.empty()
                for sound_path in self.random_sounds_paths:
                    sound = AudioSegment.from_file(sound_path)
                    mixer = mixer.overlay(sound)
                audio = mixer
            else:
                audio = voice

            # Cria um caminho temporário para o arquivo de áudio
            temp_audio_path = tempfile.mktemp(suffix=".wav")
            audio.export(temp_audio_path, format="wav")

            # Cria um clipe de áudio a partir do arquivo de áudio temporário
            audio_clip = AudioFileClip(temp_audio_path)

            # Adiciona o clipe de áudio ao clipe final
            final_clip = final_clip.set_audio(audio_clip)

            # Escreve o clipe de vídeo final no caminho do vídeo de saída
            if not self.output_video_path:
                self.error_log.append("No output video path selected")
                self.backlog_text.insert(tk.END, "No output video path selected\n")
                return

            final_clip.write_videofile(self.output_video_path, fps=120, bitrate='10000k', ffmpeg_params=['-vcodec', 'libx264', '-acodec', 'aac'])

            self.backlog_text.insert(tk.END, "Video rendered successfully and saved to " + self.output_video_path + "\n")

        except Exception as e:
            self.error_log.append(f"Error rendering video: {str(e)}")
            self.backlog_text.insert(tk.END, f"Error rendering video: {str(e)}\n")
            traceback.print_exc()

    def print_error_log(self):
        for error in self.error_log:
            print(error)

if __name__ == "__main__":
    app = VideoCreatorApp()
    app.root.mainloop()
    app.print_error_log()

Tenho esse código acima, porém a transição de slide está sendo feita em todas as imagens, e era para apenas ser feita quando for chamada na imagem que estiver determinada no config_text. 


Esse abaixo é o codigo que usei para criar essa função no meu código, ele pode conter alguma resposta que nos ajude a resolver

# Create image clips and resize them to same size so no black borders gets added

EFFECT_DURATION = 0.4
CLIP_DURATION = 1

clip1 = ImageClip("../experiments/1.jpeg").set_duration(CLIP_DURATION)
clip2 = (
    ImageClip("../experiments/2.jpeg").resize(clip1.size).set_duration(CLIP_DURATION)
)
clip3 = (
    ImageClip("../experiments/3.jpeg").resize(clip1.size).set_duration(CLIP_DURATION)
)
clips = [clip1, clip2, clip3]

# For the first clip we will need it to start from the beginning and only add
# slide out effect to the end of it
first_clip = CompositeVideoClip(
    [clips[0].fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")]
).set_start((CLIP_DURATION - EFFECT_DURATION) * 0)

# For the last video we only need it to start entring the screen from the left going right
# but not slide out at the end so the end clip exits on a full image not a partial image or black screen
last_clip = CompositeVideoClip(
    [clips[-1].fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
    # -1 because we start with index 0 so we go all the way up to array length - 1
).set_start((CLIP_DURATION - EFFECT_DURATION) * (len(clips) - 1))

videos = (
    [first_clip]
    # For all other clips in the middle, we need them to slide in to the previous clip and out for the next one
    + [
        (
            CompositeVideoClip(
                [clip.fx(transfx.slide_in, duration=EFFECT_DURATION, side="right")]
            )
            .set_start((CLIP_DURATION - EFFECT_DURATION) * idx)
            .fx(transfx.slide_out, duration=EFFECT_DURATION, side="left")
        )
            # set start to 1 since we start from second clip in the original array
        for idx, clip in enumerate(clips[1:-1], start=1)
    ]
    + [last_clip]
)

video = CompositeVideoClip(videos)
video.write_videofile(
    "final_clip.mp4",
    codec="libx264",
    audio_codec="aac",
    preset="ultrafast",
    fps=24,
    threads=24,
    ffmpeg_params=["-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", "-pix_fmt", "yuv420p"],
)

Outra coisa que achei foi uma possível forma de separar as transições do clipe normal, o que poderia ajudar nessa questão do frame preto pois fariamos de uma forma mais parecida com a função original, chamando ela quando necessário

How to add transitions between clips in moviepy?
Asked 2 years, 5 months ago
Modified 1 year, 10 months ago
Viewed 8k times
3

My current attempt:
This is my current code:

from moviepy.editor import *

clips = [VideoFileClip('a.mp4'), VideoFileClip('b.mp4'), VideoFileClip('c.mp4')]
transitioned_clips = [demo_clip.crossfadein(2) for demo_clip in clips]
for_delivery = concatenate_videoclips(transitioned_clips)
for_delivery.write_videofile(target_path, fps=clip.fps, bitrate='%dK' % (bitrate), threads=50, verbose=False, logger=None, preset='ultrafast')
I also tried using CompositeVideoClip, but:

It resulted in a completely black video.

Even for the completely black video it took 50 times longer to write the video file than for without transitions.

My current output:
My current output is a video with the 3 videos concatenated (which is good), but no transitions between the clips (which is not good).

My goal:
My goal is to add the crossfadein transition for 2 seconds between the clips and concatenate the clips into one video and output it.

In other words, I want it like (in order from left to right):

|        |      +       |        |      +       |        |
| clip 1 | transition 1 | clip 2 | transition 2 | clip 3 |
|        |      +       |        |      +       |        |
Is there anyway to have transitions? Any help appreciated.

pythonvideoconcatenationtransitionmoviepy
Share
Follow
edited May 22, 2022 at 8:00
asked May 18, 2022 at 7:42
U13-Forward's user avatar
U13-Forward
71.4k1414 gold badges9797 silver badges120120 bronze badges
Add a comment
1 Answer
Sorted by:

Highest score (default)
4

+50
You could try this approach of manually setting the start time to handle the transitions.

padding = 2

video_clips = [VideoFileClip('a.mp4'), VideoFileClip('b.mp4'), VideoFileClip('c.mp4')]

video_fx_list = [video_clips[0]]

idx = video_clips[0].duration - padding
for video in video_clips[1:]:
    video_fx_list.append(video.set_start(idx).crossfadein(padding))
    idx += video.duration - padding

final_video = CompositeVideoClip(video_fx_list)
final_video.write_videofile(target_path, fps=clip.fps) # add any remaining params
Edit:
Here's an attempt using concatenate:

custom_padding = 2
final_video = concatenate(
    [
        clip1,
        clip2.crossfadein(custom_padding),
        clip3.crossfadein(custom_padding)
    ],
    padding=-custom_padding,
    method="chain"
)
final_video.write_videofile(target_path, fps=clip.fps) # add any remaining params

O config_text que utilizamos é esse abaixo

{
  "duration": 40,
  "images": [
    {
      "name": "imagem-1.jpg",
      "duration": 8,
      "effect": "zoom",
      "transition": "slide",
      "zoom": 1.05
    },
    {
      "name": "imagem-2.jpg",
      "duration": 8,
      "effect": "none",
      "transition": "slide",
      "zoom": 1.0
    },
    {
      "name": "imagem-3.jpg",
      "duration": 8,
      "effect": "none",
      "transition": "slide",
      "zoom": 1.0
    },
    {
      "name": "imagem-4.jpg",
      "duration": 8,
      "effect": "zoom",
      "transition": "slide",
      "zoom": 1.05
    },
    {
      "name": "imagem-5.jpg",
      "duration": 8,
      "effect": "zoom",
      "transition": "slide",
      "zoom": 1.05
    }
  ],
  "voice": {
    "name": "voz",
    "duration": 49
  },
  "sounds": [],
  "output": {
    "width": 1920,
    "height": 1080,
    "fps": 120,
    "filename": "output.mp4"
  },
  "legend": {
    "text": "legenda",
    "size": 24,
    "color": "#FFFFFF",
    "position": [10, 10]
  }
}

Passo a passo:
1)Entenda meu código
2)Entenda o código da função que funciona
3)Pesquise na web
4)Entenda como conseguimos como a logica compoe (composite) do slide para que ele funcione.
5)Entenda o trecho de uma resposta em ingles sobre transições entre clipes e use para talvez concertar algo em nossa transição de slide
6)Entenda meu config_text
7) Crie uma solução para que consigamos ter slide apenas nas imagens que eu determine no config_text mas mantenha a forma de criar o slide da mesma forma (Composite) e ache uma solução para colocar isso dentro do render video, pois quando não tiver slide, será apenas o clipe de imagem com efeitos. Te darei uma bela gorjeta.

Lembre-se que a transição não é um efeito, e sim algo que acontece entre o fim de um clipe e o começo de outro.