mirror of
https://github.com/EDeev/circlechek.git
synced 2026-10-07 20:49:51 +03:00
- папка обработки была одна на чат: после первой же ошибки она оставалась, и все следующие кружочки в этом чате падали на os.mkdir — теперь своя временная папка на каждую обработку и уборка в любом случае; - кнопки фона знают свой кружочек: если прислать несколько подряд, обработается нужный; повторное нажатие отвечает подсказкой вместо ошибки; - покадровая обработка шла прямо в обработчике и блокировала бота для всех — вынесена в отдельный поток; - кружочек без звука падал на .audio.write_audiofile; - градиент по тёмным и светлым кадрам «переворачивался» (−10 в uint8 = 246): значения ограничены 0…255; средний цвет кадра считается NumPy, а не циклом по пикселям; - декоратор bug_report вызывал sys.exit() внутри обработчика — убран, ошибки логируются, пользователь получает сообщение; - видео, которое не удалось превратить в кружок, больше не остаётся на диске; - токен — из переменной BOT_TOKEN; ссылка «АВТОР» вела на чужой теперь профиль IGlek — заменена на EDeev; - requirements.txt: moviepy>=1.0.3 не подходил к коду под moviepy 2 — версии закреплены по работающему окружению.
139 lines
5 KiB
Python
139 lines
5 KiB
Python
import logging
|
||
import os
|
||
|
||
import numpy
|
||
from moviepy import AudioFileClip, ImageSequenceClip, VideoFileClip
|
||
from PIL import Image, ImageDraw, ImageFilter
|
||
|
||
|
||
class Movie:
|
||
def __init__(self, video_file, video_name, path):
|
||
self.video_file = video_file
|
||
self.video_name = video_name
|
||
self.path = path
|
||
|
||
self.path_frames = self.path + "/frames"
|
||
self.path_audio = self.path + f"/{self.video_name}-audio.mp3"
|
||
self.has_audio = False
|
||
|
||
def split_into_frames(self):
|
||
try:
|
||
video_clip = VideoFileClip(self.video_file)
|
||
except Exception:
|
||
logging.exception("Не удалось открыть кружочек")
|
||
return False
|
||
|
||
try:
|
||
# у кружочка может не быть звуковой дорожки
|
||
if video_clip.audio is not None:
|
||
video_clip.audio.write_audiofile(self.path_audio, logger=None)
|
||
self.has_audio = True
|
||
|
||
step = 1 / 30.0 if video_clip.fps > 60.0 else 1 / video_clip.fps
|
||
|
||
for count, current_duration in enumerate(numpy.arange(0, video_clip.duration, step), start=1):
|
||
frame_filename = os.path.join(self.path_frames, f"frame-{count:05}.jpeg")
|
||
video_clip.save_frame(frame_filename, current_duration)
|
||
except Exception:
|
||
logging.exception("Не удалось разобрать кружочек на кадры")
|
||
return False
|
||
finally:
|
||
video_clip.close()
|
||
|
||
return True
|
||
|
||
def unity_into_video(self, frames):
|
||
video_clip = VideoFileClip(self.video_file)
|
||
fps = 30.0 if video_clip.fps > 60.0 else video_clip.fps
|
||
video_clip.close()
|
||
|
||
clip = ImageSequenceClip([self.path_frames + "/" + x for x in frames], fps=fps)
|
||
if self.has_audio:
|
||
audio_clip = AudioFileClip(self.path_audio)
|
||
# звук не длиннее видео, иначе последний кадр «замирает»
|
||
clip = clip.with_audio(audio_clip.subclipped(0, min(audio_clip.duration, clip.duration)))
|
||
|
||
result = self.path + "/acs-" + self.video_name + ".mp4"
|
||
clip.write_videofile(result, codec="libx264", audio_codec="aac", logger=None)
|
||
clip.close()
|
||
|
||
return result
|
||
|
||
|
||
class Frame:
|
||
def __init__(self, filename, background):
|
||
self.filename = filename
|
||
self.background = background
|
||
|
||
def size(self):
|
||
with Image.open(self.filename) as image:
|
||
return image.size
|
||
|
||
def medium_color(self):
|
||
with Image.open(self.filename) as img:
|
||
pixels = numpy.asarray(img.convert("RGB"), dtype=numpy.float64)
|
||
|
||
r, g, b = (int(c) for c in pixels.reshape(-1, 3).mean(axis=0))
|
||
return r, g, b
|
||
|
||
def gradient(self, width, height, start_list, stop_list, is_horizontal_list):
|
||
def get_gradient(start, stop, width, height, is_horizontal):
|
||
if is_horizontal:
|
||
return numpy.tile(numpy.linspace(start, stop, width), (height, 1))
|
||
else:
|
||
return numpy.tile(numpy.linspace(start, stop, height), (width, 1)).T
|
||
|
||
result = numpy.zeros((height, width, len(start_list)))
|
||
for i, (start, stop, is_horizontal) in enumerate(zip(start_list, stop_list, is_horizontal_list)):
|
||
result[:, :, i] = get_gradient(start, stop, width, height, is_horizontal)
|
||
|
||
# без обрезки тёмные и светлые цвета «переворачиваются» при переводе в uint8
|
||
Image.fromarray(numpy.uint8(numpy.clip(result, 0, 255))).save(self.background, quality=95)
|
||
|
||
def blur(self, width, height):
|
||
with Image.open(self.filename) as image:
|
||
image = image.filter(ImageFilter.GaussianBlur(6))
|
||
|
||
part_width = width // 64
|
||
part_height = height // 64
|
||
|
||
image = image.crop((part_width * 10, part_height * 10, part_width * 54, part_height * 54))
|
||
image = image.resize((width, height))
|
||
image.save(self.background, quality=95)
|
||
|
||
image.close()
|
||
|
||
def unity_image(self):
|
||
im1 = Image.open(self.background)
|
||
im2 = Image.open(self.filename)
|
||
|
||
mask = Image.new("L", im2.size, 0)
|
||
draw = ImageDraw.Draw(mask)
|
||
draw.ellipse(((1, 1), (im2.size[0] - 1, im2.size[1] - 1)), fill=255)
|
||
mask_blur = mask.filter(ImageFilter.GaussianBlur(1))
|
||
|
||
im1.paste(im2, (0, 0), mask_blur)
|
||
im1.save(self.filename)
|
||
|
||
im1.close()
|
||
im2.close()
|
||
mask.close()
|
||
|
||
|
||
def process_frames(path_frames, path_background, mode):
|
||
frames = sorted(os.listdir(path_frames))
|
||
|
||
for frame in frames:
|
||
img = Frame(path_frames + "/" + frame, path_background + "/" + f"{frame[:-5]}-g.jpeg")
|
||
width, height = img.size()
|
||
|
||
if mode == "blur":
|
||
img.blur(width, height)
|
||
else:
|
||
r, g, b = img.medium_color(); nearly = 10
|
||
img.gradient(width, height, (r - nearly, g - nearly, b - nearly),
|
||
(r + nearly, g + nearly, b + nearly), (True, False, False))
|
||
|
||
img.unity_image()
|
||
|
||
return frames
|