многопроцессорность с помощью Moviepy

Недавно я сделал сценарий, который берет 5-минутный видеоклип и сокращает для 5-ти видео по 1 мин. Каждое видео, он работает хорошо, но это занимает слишком много времени для компьютера, такого как мой, и моего компьютера с очень хорошей производительностью деталей:

Intel (R) Core (TM) i7-10700 CPU @ 2,90 ГГц, 2904 МГц, 8 ядер, 16 логических процессоров

Установленная физическая память (ОЗУ) 16,0 ГБ

Итак, я ищу в потоках документов moviepy, я нашел что-то в функции write_videofile, что я могу настроить свои потоки для ускорения, я попробовал, но это не сработало, я имею в виду, что оно сработало, но только оно изменилось, возможно, на более 2 или 3 это.

Также я нашел пример кода с многопоточностью, но похоже, что код не работает, потому что moviepy.multithreading не существует в библиотеке moviepy, пожалуйста, помогите мне ускорить рендеринг, спасибо

вот код, который я нашел:

from moviepy.multithreading import multithread_write_videofile

def concat_clips():
    files = [
        "myclip1.mp4",
        "myclip2.mp4",
        "myclip3.mp4",
        "myclip4.mp4",
    ]
    multithread_write_videofile("output.mp4", get_final_clip, {"files": files})


def get_final_clip(files):
    clips = [VideoFileClip(file) for file in files]
    final = concatenate_videoclips(clips, method="compose")
    return final

это мой код:

from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from moviepy.editor import *
from numpy import array, true_divide
import cv2
import time


# ffmpeg_extract_subclip("full.mp4", start_seconds, end_seconds, targetname="cut.mp4")




def duration_clip(filename):
    clip = VideoFileClip(filename)
    duration = clip.duration
    return duration


current_time = time.strftime("%Y_%m_%d_%H_%M_%S")


def main():
    global duration
    start = 0
    cut_name_num = 1
    end_seconds = start + 60
    video_duration = duration_clip("video.mp4")
    


    txt = input("Enter Your text please: ") [::-1]
    txt_part = 1

    while start < int(video_duration):
        final_text = f"{str(txt_part)} {txt}"



        try:
            try:
                os.makedirs(f"result_{str(current_time)}/result_edit")
            except FileExistsError:
                pass            
            ffmpeg_extract_subclip("video.mp4", start, end_seconds, targetname=f"result_{str(current_time)}/cut_{str(cut_name_num)}.mp4")

            clip = VideoFileClip(f"result_{str(current_time)}/cut_{str(cut_name_num)}.mp4")

            clip = clip.subclip(0, 60)

            clip = clip.volumex(2)

            txt_clip = TextClip(final_text, font="font/VarelaRound-Regular.ttf", fontsize = 50, color = 'white')

            txt_clip = txt_clip.set_pos(("center","top")).set_duration(60) 

            video = CompositeVideoClip([clip, txt_clip])
            
            clip.write_videofile(f"result_{str(current_time)}/result_edit/cut_{str(cut_name_num)}.mp4")

        except:
            try:
                os.makedirs(f"result_{str(current_time)}/result_edit")
            except FileExistsError:
                pass
            
            ffmpeg_extract_subclip("video.mp4", start, video_duration, targetname=f"result_{str(current_time)}/cut_{str(cut_name_num)}.mp4")

            clip_duration = duration_clip(f"result_{str(current_time)}/cut_{str(cut_name_num)}.mp4")

            clip = VideoFileClip(f"result_{str(current_time)}/cut_{str(cut_name_num)}.mp4")

            clip = clip.subclip(0, clip_duration)

            clip = clip.volumex(2)

            txt_clip = TextClip(final_text, font="font/VarelaRound-Regular.ttf", fontsize = 50, color = 'white')

            txt_clip = txt_clip.set_pos(("center","top")).set_duration(60) 

            video = CompositeVideoClip([clip, txt_clip])

            clip.write_videofile(f"result_{str(current_time)}/result_edit/cut_{str(cut_name_num)}.mp4")

        start += 60
        cut_name_num += 1
        end_seconds = start + 60
        txt_part += 1


if __name__ == "__main__":
    main()

person ZONEX    schedule 15.05.2021    source источник
comment
Мне интересно, как можно объединять файлы в потоки. Что касается меня, вы не можете начать писать второй файл до того, как закончите писать первый файл, поэтому второй файл зависит от первого файла, и нет места, чтобы разделить его на два потока. Вы можете разделить проблему, если вам нужно разрезать файл, потому что созданные файлы не зависят друг от друга. Но, возможно, при многопроцессорности он будет работать быстрее, чем при многопоточности. Но мне интересно, насколько быстро это было бы, если бы вы использовали напрямую ffmpeg или ffmpeg-python.   -  person furas    schedule 16.05.2021
comment
Как я могу это сделать с многопроцессорной обработкой?   -  person ZONEX    schedule 16.05.2021
comment
сначала вам нужно создать функцию с кодом, который вы можете запускать отдельно - т.е. код, который вы запускаете в while-цикле - код после final_text = ... и до start += 60. И сначала попробуйте запустить его как функцию - вы увидите, нужно ли использовать какие-то параметры для запуска этой функции. А позже вы можете попробовать запустить эту функцию с multiprocessing.   -  person furas    schedule 16.05.2021
comment
вы повторяете тот же код, но с разными значениями в try и except. Если бы вы использовали if/else для проверки значений start, video_duration и т. Д., То код был бы короче и читабельнее. Кстати: в более новом Python вы можете использовать os.makedirs(..., exist_ok=True), и тогда вам не нужно использовать try/except   -  person furas    schedule 16.05.2021
comment
если вы используете f-string, вам не нужно str()   -  person furas    schedule 16.05.2021
comment
хорошо, я не знал, давайте рассмотрим, что я сделал, я создал функцию, которая отображает видео, параметры (имя файла, txt), теперь я участвую в многопроцессорной обработке, вы можете мне помочь, пожалуйста?   -  person ZONEX    schedule 16.05.2021
comment
вам нужно нечто большее, чем (filename, txt) - вам нужно (filename, txt, start, end, cut_name_num, txt_part), потому что process не имеет доступа к переменным в основном процессе. Кстати: переменные cut_name_num, txt_part всегда имеют одинаковые значения, поэтому вы можете использовать одну переменную, т.е. number   -  person furas    schedule 16.05.2021
comment
Что значит начало, конец?   -  person ZONEX    schedule 16.05.2021
comment
для создания подклипов вам нужны значения start, end_seconds, и вы также должны отправить их в функцию.   -  person furas    schedule 16.05.2021
comment
вам нужен подклип без текста? Я думаю, это могло бы работать быстрее, если бы вы не написали и не прочитали снова.   -  person furas    schedule 16.05.2021
comment
Текст обязательно. Хорошо, я понимаю тебя   -  person ZONEX    schedule 16.05.2021
comment
Но вопрос в том, как я могу сделать, чтобы каждый процессор работал с другим файлом из другого процессора? Я пытался что-то сделать с этим, но меня это сбивает с толку, я не знаю, как я могу переключить процессор для работы с другим файлом после того, как он закончился его первым файлом   -  person ZONEX    schedule 16.05.2021
comment
если вы используете цикл, который будет использовать Process() для запуска функции с разными аргументами, тогда он будет запускать все процессы одновременно, и вам не нужно ничего переключать. Если вы используете Pool(8) и поместите список с аргументами для всех процессов, тогда он будет запускать 8 процессов одновременно - когда один процесс завершит задание, он автоматически запустит следующий процесс из списка - и вам не нужно ничего переключать. Я тестирую обе версии, но на моем компьютере она сокращается всего на 20 секунд, потому что ffmpeg уже использует полную мощность ЦП для одного процесса, и нет мощности для более быстрой работы других,   -  person furas    schedule 16.05.2021
comment
Не могли бы вы прислать мне пример, я действительно не знаю эту библиотеку.   -  person ZONEX    schedule 16.05.2021
comment
Я начинаю описывать это в ответ. Вы видите это через несколько минут.   -  person furas    schedule 16.05.2021
comment
Хорошо, спасибо, Жду ответа ...   -  person ZONEX    schedule 16.05.2021
comment
Я добавил ответ с несколькими версиями. Мне интересно, планируете ли вы объединить все подклипы в одно видео - потому что, возможно, будет быстрее сделать все это в одном процессе, используя один VideoFileClip со многими TextClip, которые будут иметь разные .set_start(start) и CompositeVideoClip([clip, txt_clip1, txt_clip2, txt_clip3, ...])   -  person furas    schedule 16.05.2021


Ответы (1)


Используя процессы, я сократил время всего на 15-20 секунд, потому что ffmpeg даже в одном процессе использовала почти полную мощность процессора, а у моего компьютера не было мощности для более быстрого запуска других процессов.


Сначала я сократил код, чтобы сделать его короче.

В коде try и except были похожие элементы, поэтому я переместил их в сторону try/except.

Далее я использовал

        if end > video_duration:
            end = video_duration

а мне вообще не нужен был try/except.

При использовании os.makedirs(..., exist_ok=True) мне не нужно запускать его в try/except

Тем временем я сократил время на 20 секунд, используя

        clip = VideoFileClip(filename).subclip(start, end)

вместо того

        temp_filename  = f"{base_folder}/cut_{number}.mp4"
        
        fmpeg_extract_subclip(filename, start, end, targetname=temp_filename)
        
        clip = VideoFileClip(temp_filename)

Таким образом, я не записываю подклип на диск, и мне не нужно читать его снова с диска.


from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from moviepy.editor import *
import time

def main():

    text = input("Enter Your text please: ") [::-1]
    #text = 'Hello World'
    
    base_folder = time.strftime("result_%Y_%m_%d_%H_%M_%S")    
    os.makedirs(f"{base_folder}/result_edit", exist_ok=True)
    
    filename = "video.mp4"
    #filename = "BigBuckBunny.mp4"
    
    video_duration = VideoFileClip(filename).duration

    number = 0 # instead of `cut_name_num` and `txt_part` because both had the same value

    time_start = time.time()

    for start in range(0, int(video_duration), 60):

        end = start + 60
        
        if end > video_duration:
            end = video_duration

        number += 1

        clip_duration = end - start
        print(f'[DEBUG] number: {number:2} | start: {start:6.2f} | end: {end:6.2f} | duration: {clip_duration:.2f}')
       
        final_text = f"{number} {text}"
    
        temp_filename  = f"{base_folder}/cut_{number}.mp4"
        final_filename = f"{base_folder}/result_edit/cut_{number}.mp4"
        
        #ffmpeg_extract_subclip(filename, start, end, targetname=temp_filename)
        
        #clip = VideoFileClip(temp_filename)
        clip = VideoFileClip(filename).subclip(start, end)
        clip = clip.volumex(2)
    
        txt_clip = TextClip(final_text, font="font/VarelaRound-Regular.ttf", fontsize=50, color='white')
        txt_clip = txt_clip.set_pos(("center","top")).set_duration(60)
        
        video = CompositeVideoClip([clip, txt_clip])
    
        video.write_videofile(final_filename)

    # - after loop -
    
    # because I use `number += 1` before loop so now `number` has number of subclips
    print('number of subclips:', number)

    time_end = time.time()
    
    diff = time_end - time_start
    print(f'time: {diff:.2f}s ({diff//60:02.0f}:{diff%60:02.2f})')
    
    
if __name__ == "__main__":
    main()

Затем я переместил код в функцию с аргументами my_process(filename, text, start, end, number, base_folder)

from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from moviepy.editor import *
import time

def my_process(filename, text, start, end, number, base_folder):

    clip_duration = end - start
    print(f'[DEBUG] number: {number:2} | start: {start:6.2f} | end: {end:6.2f} | duration: {clip_duration:.2f}')
    
    final_text = f"{number} {text}"

    temp_filename  = f"{base_folder}/cut_{number}.mp4"
    final_filename = f"{base_folder}/result_edit/cut_{number}.mp4"
    
    #print('[DEBUG] ffmpeg_extract_subclip')
    #ffmpeg_extract_subclip(filename, start, end, targetname=temp_filename)
    
    #print('[DEBUG] VideoClip')
    #clip = VideoFileClip(temp_filename)
    clip = VideoFileClip(filename).subclip(start, end)
    clip = clip.volumex(2)

    #print('[DEBUG] TextClip')
    txt_clip = TextClip(final_text, font="font/VarelaRound-Regular.ttf", fontsize=50, color='white')
    txt_clip = txt_clip.set_pos(("center","top")).set_duration(60)
    
    #print('[DEBUG] CompositeVideoClip')
    video = CompositeVideoClip([clip, txt_clip])

    #print('[DEBUG] CompositeVideoClip write')
    video.write_videofile(final_filename)
    #print('[DEBUG] CompositeVideoClip end')
    

def main():

    text = input("Enter Your text please: ") [::-1]
    #text = 'Hello World'
    
    base_folder = time.strftime("result_%Y_%m_%d_%H_%M_%S")    
    os.makedirs(f"{base_folder}/result_edit", exist_ok=True)
    
    filename = "video.mp4"
    #filename = "BigBuckBunny.mp4"
    
    video_duration = VideoFileClip(filename).duration

    number = 0 # instead of `cut_name_num` and `txt_part` because both had the same value

    time_start = time.time()
    
    for start in range(0, int(video_duration), 60):

        end = start + 60
        
        if end > video_duration:
            end = video_duration
        
        number += 1
        
        my_process(filename, text, start, end, number, base_folder)
        
    # - after loop -
    
    # because I use `number += 1` before loop so now `number` has number of subclips
    print('number of subclips:', number)

    time_end = time.time()
    
    diff = time_end - time_start
    print(f'time: {diff:.2f}s ({diff//60:02.0f}:{diff%60:02.2f})')


if __name__ == "__main__":
    main()

И теперь я могу запускать функцию в отдельных процессах, используя стандартный модуль multiprocessing.

(или стандартные модули threading, concurrent.futures или внешние модули Joblib, Ray и т. д. .).

Начинает единый процесс

# it has to use named arguments`target=`, `args=`

p = multiprocessing.Process(target=my_process, args=(filename, text, start, end, number, base_folder))
p.start()  # start it

но если я использую его в цикле, я запускаю много процессов одновременно.


from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from moviepy.editor import *
import time
import multiprocessing

def my_process(filename, text, start, end, number, base_folder):

    clip_duration = end - start
    print(f'[DEBUG] number: {number:2} | start: {start:6.2f} | end: {end:6.2f} | duration: {clip_duration:.2f}')
    
    final_text = f"{number} {text}"

    temp_filename  = f"{base_folder}/cut_{number}.mp4"
    final_filename = f"{base_folder}/result_edit/cut_{number}.mp4"
    
    #print('[DEBUG] ffmpeg_extract_subclip')
    #ffmpeg_extract_subclip(filename, start, end, targetname=temp_filename)
    
    #print('[DEBUG] VideoClip')
    #clip = VideoFileClip(temp_filename)
    clip = VideoFileClip(filename).subclip(start, end)
    clip = clip.volumex(2)

    #print('[DEBUG] TextClip')
    txt_clip = TextClip(final_text, font="font/VarelaRound-Regular.ttf", fontsize=50, color='white')
    txt_clip = txt_clip.set_pos(("center","top")).set_duration(60)
    
    #print('[DEBUG] CompositeVideoClip')
    video = CompositeVideoClip([clip, txt_clip])

    #print('[DEBUG] CompositeVideoClip write')
    video.write_videofile(final_filename)
    #print('[DEBUG] CompositeVideoClip end')
    

def main():

    text = input("Enter Your text please: ") [::-1]
    #text = 'Hello World'
    
    base_folder = time.strftime("result_%Y_%m_%d_%H_%M_%S")    
    os.makedirs(f"{base_folder}/result_edit", exist_ok=True)
    
    filename = "video.mp4"
    #filename = "BigBuckBunny.mp4"
    
    video_duration = VideoFileClip(filename).duration

    number = 0 # instead of `cut_name_num` and `txt_part` because both had the same value

    time_start = time.time()
    
    all_processes = []
    for start in range(0, int(video_duration), 60):

        end = start + 60
        
        if end > video_duration:
            end = video_duration
        
        number += 1
        
        print("add process:", number)
        p = multiprocessing.Process(target=my_process, args=(filename, text, start, end, number, base_folder)) # it has to use `target=`, `args=`
        p.start()  # start it
        all_processes.append(p)  # keep it to use `join()`
            
    # - after loop -
    
    for p in all_processes:
        p.join()  # wait for the end of process
        
    # because I use `number += 1` before loop so now `number` has number of subclips
    print('number of subclips:', number)

    time_end = time.time()
    
    diff = time_end - time_start
    print(f'time: {diff:.2f}s ({diff//60:02.0f}:{diff%60:02.2f})')


if __name__ == "__main__":
    main()

Предыдущая версия для 11 подклипов запускает 11 процессов. Используя Pool(4), вы можете объединить все процессы в пул, и он будет запускать 4 процесса одновременно. Когда один процесс завершит задачу, он запустит следующий процесс с новыми аргументами.

На этот раз я использую цикл для создания списка с аргументами для всех процессов.

args_for_all_processes = []

for start in range(0, int(video_duration), 60):

    end = start + 60
    
    if end > video_duration:
        end = video_duration
    
    number += 1
    print("add process:", number)
    
    args_for_all_processes.append( (filename, text, start, end, number, base_folder) )

и я использую этот список с Pool, и он сделает все остальное.

# I have 4 CPU so I use Pool(4) - but without value it should automatically use `os.cpu_count()`
with multiprocessing.Pool(4) as pool:      
    results = pool.starmap(my_process, args_for_all_processes)
    #print(results)

Pool может запускать процессы в другом порядке, но если они используют return для отправки какого-либо результата, то Pool выдаст результаты в правильном порядке.

from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
from moviepy.editor import *
import time
import multiprocessing

def my_process(filename, text, start, end, number, base_folder):

    clip_duration = end - start
    print(f'[DEBUG] number: {number:2} | start: {start:6.2f} | end: {end:6.2f} | duration: {clip_duration:.2f}')
    
    final_text = f"{number} {text}"

    temp_filename  = f"{base_folder}/cut_{number}.mp4"
    final_filename = f"{base_folder}/result_edit/cut_{number}.mp4"
    
    #print('[DEBUG] ffmpeg_extract_subclip')
    #ffmpeg_extract_subclip(filename, start, end, targetname=temp_filename)
    
    #print('[DEBUG] VideoClip')
    #clip = VideoFileClip(temp_filename)
    clip = VideoFileClip(filename).subclip(start, end)
    clip = clip.volumex(2)

    #print('[DEBUG] TextClip')
    txt_clip = TextClip(final_text, font="font/VarelaRound-Regular.ttf", fontsize=50, color='white')
    txt_clip = txt_clip.set_pos(("center","top")).set_duration(60)
    
    #print('[DEBUG] CompositeVideoClip')
    video = CompositeVideoClip([clip, txt_clip])

    #print('[DEBUG] CompositeVideoClip write')
    video.write_videofile(final_filename)
    #print('[DEBUG] CompositeVideoClip end')
    
    # return "OK"  # you can use `return` to send result/information to main process.
    
def main():

    text = input("Enter Your text please: ") [::-1]
    #text = 'Hello World'
    
    base_folder = time.strftime("result_%Y_%m_%d_%H_%M_%S")    
    os.makedirs(f"{base_folder}/result_edit", exist_ok=True)
    
    filename = "video.mp4"
    #filename = "BigBuckBunny.mp4"
    
    video_duration = VideoFileClip(filename).duration

    number = 0 # instead of `cut_name_num` and `txt_part` because both had the same value

    time_start = time.time()
    
    # first create list with arguments for all processes
    
    args_for_all_processes = []
    
    for start in range(0, int(video_duration), 60):

        end = start + 60
        
        if end > video_duration:
            end = video_duration
        
        number += 1
        print("add process:", number)
        
        args_for_all_processes.append( (filename, text, start, end, number, base_folder) )

    # - after loop -            
        
    # next put all processes to pool
        
    with multiprocessing.Pool(4) as pool:  # I have 4 CPU so I use Pool(4) - but it should use `os.cpu_count()` in `Pool()
    
        results = pool.starmap(my_process, args_for_all_processes)
        #print(results)
            
    # - after loop -
    
    # because I use `number += 1` before loop so now `number` has number of subclips
    print('number of subclips:', number)

    time_end = time.time()
    
    diff = time_end - time_start
    print(f'time: {diff:.2f}s ({diff//60:02.0f}:{diff%60:02.2f})')


if __name__ == "__main__":
    main()
person furas    schedule 16.05.2021
comment
Вы можете мне объяснить, почему вы это сделали? if end ›video_duration: end = video_duration Я немного сбиваю с толку, потому что если я беру видео, которое позволяет сказать 5:01, то его конец будет через 5:00, а не через 5:01 - person ZONEX; 16.05.2021
comment
последний клип может быть короче 60 секунд - если вы попытаетесь использовать .subclip(start, end) с end больше, чем video_duration, возникнет ошибка. Используя if, я получаю правильное значение end, и мне не нужно использовать try/except - person furas; 16.05.2021
comment
О, теперь я понимаю, я знал это, но я забыл, почему я попробовал, кроме того, что твой способ лучше и меньше кода, спасибо - person ZONEX; 16.05.2021
comment
Я знаю, что код работает хорошо, но я пробовал кодировать как свой, так и ваш, и код без многопроцессорной обработки был короче, чем другой код. и у меня довольно хороший компьютер, самый короткий - 330 секунд, а другой - 425 секунд, знаете почему? - person ZONEX; 16.05.2021
comment
в многопроцессорной / многопоточности требуется дополнительное время для запуска процесса - если процессы не могут выполняться одновременно (т. е. один процесс получает всю мощность процессора), вы получаете больше времени. И ffmpeg (который используется в MoviPy) может получить всю мощность процессора за один процесс, и это может создать все проблемы. И у этого может не быть решения. - person furas; 16.05.2021
comment
Как я могу разделить мощность для каждого процессора? - person ZONEX; 17.05.2021
comment
или мне просто нужно использовать файл большего размера, 40 минут вместо 5 минут, он будет работать быстрее? - person ZONEX; 17.05.2021
comment
Я тестировал 10-минутный файл и сократил время всего на 20 секунд. Я думаю, ffmpeg уже может использовать потоки на всех ядрах, и это не может работать быстрее. Сколько потоков использует ffmpeg по умолчанию? - person furas; 17.05.2021
comment
Я попробовал его на 49-минутном файле, и он работал очень быстро, для коротких видео я просто должен использовать один процесс, я думаю, может быть, он сделал это быстрее, чем 5-минутный видеофайл. - person ZONEX; 17.05.2021