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 — версии закреплены по работающему окружению.
This commit is contained in:
parent
7f85340c86
commit
e74873588a
6 changed files with 142 additions and 154 deletions
|
|
@ -1,18 +1,18 @@
|
|||
import asyncio, logging, warnings
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from init import *
|
||||
from init import bot, dp
|
||||
from handlers import router
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
dp.include_router(router)
|
||||
await bot.delete_webhook(drop_pending_updates=True)
|
||||
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types(), skipUpdates=True)
|
||||
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
# warnings.filterwarnings("ignore")
|
||||
|
||||
try: asyncio.run(main())
|
||||
except KeyboardInterrupt: pass
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
botToken = "XXXXXXXXXXXXXXXXXXXXXXXX" # @circlechek_bot
|
||||
import os
|
||||
|
||||
botToken = os.getenv("BOT_TOKEN", "XXXXXXXXXXXXXXXXXXXXXXXX") # @circlechek_bot
|
||||
|
||||
WARN = ("<b>Предупреждение!</b> Кружочек может быть с некорректными метаданными, в этом случае программа выдаст ошибку "
|
||||
"либо произведёт видео с браком! <b>Обязательно перепроверяйте получившиеся видео!</b>\n\n")
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
from aiogram import types, F, Router
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.types import Message, CallbackQuery, ContentType, InlineKeyboardButton, InlineKeyboardMarkup, FSInputFile
|
||||
from aiogram.filters import Command
|
||||
|
||||
import shutil
|
||||
|
||||
from scripts import *
|
||||
from scripts import Movie, process_frames
|
||||
from config import WARN
|
||||
|
||||
router = Router()
|
||||
|
|
@ -15,7 +20,7 @@ router = Router()
|
|||
async def helps(msg: Message) -> None:
|
||||
buttons = [[InlineKeyboardButton(text="ФУНКЦИИ", callback_data="fun"),
|
||||
InlineKeyboardButton(text="АВТОР", callback_data="auth")]]
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons, row_width=2)
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
await msg.answer(text="<b>Кружочичек</b> — бот для обработки видео и кружочков в Телеграме. Для начала работы "
|
||||
"вам достаточно скинуть квадратное видео не дольше минуты в чат, чтобы получить кружок. "
|
||||
|
|
@ -33,7 +38,7 @@ async def author(call: CallbackQuery) -> None:
|
|||
'двух предложенных вариантах.'
|
||||
|
||||
'\n\nЯ же пишу подобные небольшие проекты, о которых вы можете узнать '
|
||||
'больше на моём <a href="https://github.com/IGlek">GitHub</a>.')
|
||||
'больше на моём <a href="https://github.com/EDeev">GitHub</a>.')
|
||||
|
||||
|
||||
@router.callback_query(F.data == "fun")
|
||||
|
|
@ -45,78 +50,84 @@ async def function(call: CallbackQuery) -> None:
|
|||
# ОБРАБОТЧИК ВИДЕО
|
||||
@router.message(F.content_type == ContentType.VIDEO)
|
||||
async def video_to_circle(msg: Message) -> None:
|
||||
video = "../data/circles/" + str(msg.chat.id) + ".mp4"
|
||||
video = f"../data/circles/{msg.chat.id}_{msg.message_id}.mp4"
|
||||
|
||||
await msg.reply("<b>Началась обработка видео!</b> Оно должно быть квадратным и не дольше одной минуты, в ином "
|
||||
"случае бот в ответ вернёт вам изначальное видео, а не кружочек!")
|
||||
try:
|
||||
await msg.bot.download(file=msg.video.file_id, destination=video)
|
||||
await msg.answer_video_note(video_note=FSInputFile(video))
|
||||
except TelegramBadRequest as err:
|
||||
logging.warning("Не удалось сделать кружочек: %s", err)
|
||||
await msg.answer("<b>Не получилось сделать кружочек.</b> Видео должно быть квадратным, не дольше минуты "
|
||||
"и не больше 20 МБ.")
|
||||
finally:
|
||||
if os.path.exists(video):
|
||||
os.remove(video)
|
||||
|
||||
|
||||
# ОБРАБОТЧИК КРУЖОЧКОВ
|
||||
@router.message(F.content_type == ContentType.VIDEO_NOTE)
|
||||
async def video_note(msg: Message) -> None:
|
||||
buttons = [[InlineKeyboardButton(text="Градиент", callback_data="grad"),
|
||||
InlineKeyboardButton(text="Блюр", callback_data="blur")]]
|
||||
keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons, row_width=2)
|
||||
# номер сообщения в кнопках: если прислать несколько кружочков подряд, каждый обработается свой
|
||||
buttons = [[InlineKeyboardButton(text="Градиент", callback_data=f"grad:{msg.message_id}"),
|
||||
InlineKeyboardButton(text="Блюр", callback_data=f"blur:{msg.message_id}")]]
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
msg_answer = await msg.reply("Кружочек загружается...")
|
||||
await msg.bot.download(file=msg.video_note.file_id, destination="../data/video_notes/" + str(msg.chat.id) + ".mp4")
|
||||
await msg.bot.download(file=msg.video_note.file_id,
|
||||
destination=f"../data/video_notes/{msg.chat.id}_{msg.message_id}.mp4")
|
||||
await msg_answer.edit_text(text=WARN + "Какой тип фона в углах вы выберите?", reply_markup=keyboard)
|
||||
|
||||
|
||||
@router.callback_query(lambda call: call.data == "grad" or call.data == "blur")
|
||||
@router.callback_query(F.data.regexp(r"^(grad|blur):\d+$"))
|
||||
async def work_part(call: CallbackQuery) -> None:
|
||||
mode, note_id = call.data.split(":")
|
||||
video_name = f"{call.message.chat.id}_{note_id}"
|
||||
video_file = video_name + ".mp4"
|
||||
source = "../data/video_notes/" + video_file
|
||||
|
||||
if not os.path.exists(source):
|
||||
# кнопку нажали повторно или кружочек уже обработан
|
||||
await call.answer("Этот кружочек уже обработан. Пришлите его ещё раз.", show_alert=True)
|
||||
return
|
||||
|
||||
msg = await call.message.edit_text(WARN + "<b>Начало обработки!</b>")
|
||||
|
||||
video_name = str(msg.chat.id)
|
||||
video_file = video_name + ".mp4"
|
||||
|
||||
path = f"../data/videos/{video_name}"
|
||||
os.mkdir(path)
|
||||
|
||||
# отдельная папка на каждую обработку и уборка в любом случае — иначе после первой же ошибки
|
||||
# повторная обработка в этом чате падала на уже существующей папке
|
||||
path = tempfile.mkdtemp(prefix=video_name + "-", dir="../data/videos")
|
||||
try:
|
||||
path_video = path + "/" + video_file
|
||||
os.replace("../data/video_notes/" + video_file, path_video)
|
||||
os.replace(source, path_video)
|
||||
|
||||
path_frames = path + f"/frames"
|
||||
path_frames = path + "/frames"
|
||||
os.mkdir(path_frames)
|
||||
|
||||
path_background = path + f"/background"
|
||||
path_background = path + "/background"
|
||||
os.mkdir(path_background)
|
||||
|
||||
# тяжёлая обработка — в отдельном потоке, чтобы бот не замирал для остальных пользователей
|
||||
msg = await msg.edit_text(WARN + "<b>Этап:</b> 1 - Обработка видео.")
|
||||
video = Movie(path_video, video_name, path)
|
||||
procces = video.split_into_frames()
|
||||
|
||||
if not procces:
|
||||
if not await asyncio.to_thread(video.split_into_frames):
|
||||
await msg.edit_text("<b>Возникла ошибка!</b> Кружочек невозможно обработать!")
|
||||
shutil.rmtree(path)
|
||||
return
|
||||
|
||||
msg = await msg.edit_text(WARN + "<b>Этап:</b> 2 - Обработка кадров.")
|
||||
frames = list(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 call.data == "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()
|
||||
frames = await asyncio.to_thread(process_frames, path_frames, path_background, mode)
|
||||
|
||||
msg = await msg.edit_text(WARN + "<b>Этап:</b> 3 - Объединение кадров.")
|
||||
video.unity_into_video(frames)
|
||||
result = await asyncio.to_thread(video.unity_into_video, frames)
|
||||
|
||||
msg_final = await msg.edit_text(WARN + "<b>Готово!</b> Видео отправляется...")
|
||||
|
||||
await msg.answer_video(video=FSInputFile(path + "/acs-" + video_file), caption=WARN,
|
||||
reply_to_message_id=call.message.reply_to_message.message_id)
|
||||
reply_to = call.message.reply_to_message
|
||||
await msg.answer_video(video=FSInputFile(result), caption=WARN,
|
||||
reply_to_message_id=reply_to.message_id if reply_to else None)
|
||||
await msg_final.delete()
|
||||
|
||||
shutil.rmtree(path)
|
||||
except Exception:
|
||||
logging.exception("Ошибка обработки кружочка")
|
||||
await msg.edit_text("<b>Возникла ошибка!</b> Кружочек невозможно обработать!")
|
||||
finally:
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
|
|
|
|||
23
code/init.py
23
code/init.py
|
|
@ -7,26 +7,3 @@ from config import botToken
|
|||
|
||||
bot = Bot(token=botToken, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
||||
dp = Dispatcher(storage=MemoryStorage())
|
||||
|
||||
|
||||
import logging, functools, sys
|
||||
|
||||
# logging.basicConfig(level=logging.INFO, filename='../data/info.log',filemode="w", format="%(asctime)s %(levelname)s %(message)s")
|
||||
|
||||
def bug_report(func):
|
||||
@functools.wraps(func)
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
logging.info(f"Successful result")
|
||||
return result
|
||||
except Exception as err:
|
||||
logging.error("Ошибка", exc_info=True)
|
||||
"""with open('../data/log.txt', "a") as f:
|
||||
f.write(repr(e))"""
|
||||
sys.exit()
|
||||
|
||||
return wrapper
|
||||
|
||||
|
|
|
|||
116
code/scripts.py
116
code/scripts.py
|
|
@ -1,8 +1,9 @@
|
|||
from PIL import Image, ImageDraw, ImageFilter
|
||||
from moviepy import *
|
||||
import numpy, os
|
||||
import logging
|
||||
import os
|
||||
|
||||
from init import bug_report
|
||||
import numpy
|
||||
from moviepy import AudioFileClip, ImageSequenceClip, VideoFileClip
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
|
||||
class Movie:
|
||||
|
|
@ -11,48 +12,52 @@ class Movie:
|
|||
self.video_name = video_name
|
||||
self.path = path
|
||||
|
||||
self.path_frames = ""
|
||||
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)
|
||||
video_clip.audio.write_audiofile(self.path + f"/{self.video_name}-audio.mp3", logger=None)
|
||||
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
|
||||
|
||||
self.path_frames = self.path + "/frames"
|
||||
step = 1 / 30.0 if video_clip.fps > 60.0 else 1 / video_clip.fps
|
||||
|
||||
count = 0
|
||||
for current_duration in numpy.arange(0, video_clip.duration, step):
|
||||
count += 1
|
||||
|
||||
str_c = str(count)
|
||||
num = "00000"[:(len(str_c) * -1)] + str_c
|
||||
|
||||
frame_filename = os.path.join(self.path_frames, f"frame-{num}.jpeg")
|
||||
try: video_clip.save_frame(frame_filename, current_duration)
|
||||
except Exception: return False
|
||||
|
||||
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
|
||||
|
||||
@bug_report
|
||||
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()
|
||||
os.remove(self.video_file)
|
||||
|
||||
clip = ImageSequenceClip(list(map(lambda x: self.path_frames + "/" + x, frames)), fps=fps)
|
||||
clip.write_videofile(self.video_file, logger=None)
|
||||
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()
|
||||
|
||||
video_clip = VideoFileClip(self.video_file)
|
||||
audio_clip = AudioFileClip(self.path + f"/{self.video_name}-audio.mp3")
|
||||
|
||||
video_clip_with_audio = video_clip.with_audio(audio_clip)
|
||||
video_clip_with_audio.write_videofile(self.path + "/acs-" + self.video_name + ".mp4", logger=None)
|
||||
|
||||
video_clip.close()
|
||||
audio_clip.close()
|
||||
return result
|
||||
|
||||
|
||||
class Frame:
|
||||
|
|
@ -61,31 +66,14 @@ class Frame:
|
|||
self.background = background
|
||||
|
||||
def size(self):
|
||||
image = Image.open(self.filename)
|
||||
width, height = image.size
|
||||
image.close()
|
||||
|
||||
return width, height
|
||||
with Image.open(self.filename) as image:
|
||||
return image.size
|
||||
|
||||
def medium_color(self):
|
||||
img = Image.open(self.filename)
|
||||
width, height = img.size
|
||||
with Image.open(self.filename) as img:
|
||||
pixels = numpy.asarray(img.convert("RGB"), dtype=numpy.float64)
|
||||
|
||||
pixels = img.load()
|
||||
r, g, b = [], [], []
|
||||
|
||||
for y in range(width):
|
||||
for x in range(height):
|
||||
p = pixels[x, y]
|
||||
r.append(p[0])
|
||||
g.append(p[1])
|
||||
b.append(p[2])
|
||||
|
||||
r = sum(r) // len(r)
|
||||
g = sum(g) // len(g)
|
||||
b = sum(b) // len(b)
|
||||
|
||||
img.close()
|
||||
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):
|
||||
|
|
@ -99,10 +87,11 @@ class Frame:
|
|||
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)
|
||||
|
||||
Image.fromarray(numpy.uint8(result)).save(self.background, quality=95)
|
||||
# без обрезки тёмные и светлые цвета «переворачиваются» при переводе в uint8
|
||||
Image.fromarray(numpy.uint8(numpy.clip(result, 0, 255))).save(self.background, quality=95)
|
||||
|
||||
def blur(self, width, height):
|
||||
image = Image.open(self.filename)
|
||||
with Image.open(self.filename) as image:
|
||||
image = image.filter(ImageFilter.GaussianBlur(6))
|
||||
|
||||
part_width = width // 64
|
||||
|
|
@ -129,3 +118,22 @@ class Frame:
|
|||
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
|
||||
|
|
|
|||
|
|
@ -1,14 +1,4 @@
|
|||
# Core bot framework
|
||||
aiogram>=3.0.0
|
||||
|
||||
# Image processing
|
||||
Pillow>=9.0.0
|
||||
|
||||
# Video processing
|
||||
moviepy>=1.0.3
|
||||
|
||||
# Mathematical operations
|
||||
numpy>=1.21.0
|
||||
|
||||
# Optional: For better video codec support
|
||||
imageio-ffmpeg>=0.4.0
|
||||
aiogram==3.23.0
|
||||
moviepy==2.2.1
|
||||
Pillow==10.3.0
|
||||
numpy==2.2.6
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue