1
0
Fork 0
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:
Деев Егор Викторович 2026-10-05 19:16:13 +00:00
parent 7f85340c86
commit e74873588a
6 changed files with 142 additions and 154 deletions

View file

@ -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

View file

@ -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")

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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