2025, Nov 16 09:01
Почему распаковка dict в списке ломает concatenate_videoclips в MoviePy
Разбираем ошибку AttributeError в MoviePy при склейке: распаковка словаря (*dict) в список передается как целые числа. Покажем воспроизведение и два исправления.
Незаметная ловушка Python в MoviePy: при распаковке словаря в список, который затем передаётся в concatenate_videoclips, в него могут попасть целые числа вместо клипов. В результате во время выполнения появляется AttributeError, указывающий на MoviePy, хотя истинная причина кроется в том, как формируется последовательность.
Как воспроизвести проблему
Ниже приведён фрагмент, который создаёт титульную карточку, подхватывает изображения из каталога, добавляет простые затемнения и пытается склеить всё в единое видео. Ошибка возникает в момент распаковки словаря в список клипов.
from moviepy.editor import *
from moviepy.config import change_settings
from pathlib import Path
import os
import traceback
change_settings({"IMAGEMAGICK_BINARY": r"C:\\Program Files\\ImageMagick-7.1.2-Q16-HDRI\\magick.exe"})
def looks_like_image(fp):
"""
Checks if a file is likely an image based on its extension.
"""
valid_ext = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
_, ext = os.path.splitext(fp)
return ext.lower() in valid_ext
clip_map = {}
user_tag = "user_"
assets_dir = Path(r"C:/Users/User/Development/moviepy/images")
idx = 0
for p in assets_dir.iterdir():
if p.is_file():
if os.path.isfile(p) and looks_like_image(p):
clip_map[idx] = ImageClip(str(p)).set_duration(2)
clip_map[idx] = clip_map[idx].fx(vfx.fadeout, 1)
clip_map[idx] = clip_map[idx].fx(vfx.fadein, 1)
idx += 1
else:
print(f"'{p}' is not an image file (or doesn't exist).")
audio_track = AudioFileClip(r'C:\\Users\\User\\Development\\moviepy\\audio\\KYJellyBabies-AccessDenied.mp3')
title_clip = TextClip(
"In Loving memory of \nJoe Blocks",
fontsize=70,
color='white',
font='Arial',
bg_color='transparent'
).set_pos('center').set_duration(5)
combined_clips = [
title_clip,
*clip_map, # <-- проблема: разворачивает КЛЮЧИ словаря
]
try:
final_cut = concatenate_videoclips(combined_clips, method='compose')
final_cut.audio = audio_track.subclip(0, final_cut.duration)
final_cut.write_videofile('final_video.mp4', fps=30)
except Exception as err:
traceback.print_exc()
Во время выполнения это проявляется так:
AttributeError: 'int' object has no attribute 'duration'
Что на самом деле происходит
Оператор распаковки * ведёт себя по-разному для разных контейнеров. В Python запись *dict_obj возвращает не значения, а ключи. В нашем случае ключи — это целые числа, добавленные в процессе нумерации. Эти числа распаковываются в список клипов и передаются в MoviePy. Функция concatenate_videoclips ожидает, что каждый элемент — объект, похожий на клип, с атрибутом duration. У целого числа такого атрибута нет, отсюда и AttributeError.
Два простых исправления
Если вам действительно нужен словарь, распаковывайте его значения, а не ключи. Это минимальное изменение, которое сохраняет остальной процесс без изменений.
combined_clips = [
title_clip,
*clip_map.values(), # разворачивает объекты ImageClip, а не ключи словаря
]
Либо стройте список с самого начала. Так семантика согласуется с тем, как результат будет использоваться, и вы избегаете случайного разворачивания ключей.
from moviepy.editor import *
from moviepy.config import change_settings
from pathlib import Path
import os
import traceback
change_settings({"IMAGEMAGICK_BINARY": r"C:\\Program Files\\ImageMagick-7.1.2-Q16-HDRI\\magick.exe"})
def looks_like_image(fp):
valid_ext = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
_, ext = os.path.splitext(fp)
return ext.lower() in valid_ext
clip_list = [] # используйте список и добавляйте клипы по мере работы
user_tag = "user_"
assets_dir = Path(r"C:/Users/User/Development/moviepy/images")
for p in assets_dir.iterdir():
if p.is_file():
if os.path.isfile(p) and looks_like_image(p):
clip_list.append(
ImageClip(str(p))
.set_duration(2)
.fx(vfx.fadeout, 1)
.fx(vfx.fadein, 1)
)
else:
print(f"'{p}' is not an image file (or doesn't exist).")
audio_track = AudioFileClip(r'C:\\Users\\User\\Development\\moviepy\\audio\\KYJellyBabies-AccessDenied.mp3')
title_clip = TextClip(
"In Loving memory of \nJoe Blocks",
fontsize=70,
color='white',
font='Arial',
bg_color='transparent'
).set_pos('center').set_duration(5)
combined_clips = [
title_clip,
*clip_list,
]
try:
final_cut = concatenate_videoclips(combined_clips, method='compose')
final_cut.audio = audio_track.subclip(0, final_cut.duration)
final_cut.write_videofile('final_video.mp4', fps=30)
except Exception as err:
traceback.print_exc()
Почему эта деталь важна
API вроде MoviePy предполагают, что в передаваемых последовательностях лежат медиа-объекты с ожидаемыми атрибутами и методами. Случайная примесь скаляров из-за распаковки словаря через * ломает эти ожидания и переносит сбой в код библиотеки, из-за чего ошибка выглядит не по адресу. Понимание того, как Python распаковывает итерируемые объекты, помогает избежать таких скрытых несоответствий типов, особенно в пайплайнах, где элементы последовательно компонуются и трансформируются.
Итоги
Перед склейкой явно определяйте, какую последовательность вы собираете. Если используете словарь, там, где нужны объекты, распаковывайте .values(). Если доступ по ключам не требуется, лучше сразу используйте список и добавляйте в него ImageClip по мере генерации. И если что-то пошло не так, полный traceback поможет быстро найти место, где в конвейер попал не-клип. Контроль над тем, что именно вы распаковываете, сэкономит время при следующей сборке клипов в MoviePy.