From 1df01686b3371be0e4aa03f48140d0954486cc0b Mon Sep 17 00:00:00 2001 From: Syed Tahseen Date: Fri, 31 Oct 2025 16:11:03 +0500 Subject: [PATCH] Update circle.py --- fun/circle.py | 245 ++++++++++++++++++++++---------------------------- 1 file changed, 109 insertions(+), 136 deletions(-) diff --git a/fun/circle.py b/fun/circle.py index 45eea7b..8da8139 100644 --- a/fun/circle.py +++ b/fun/circle.py @@ -1,146 +1,119 @@ -import asyncio import os -from io import BytesIO - -from PIL import Image, ImageDraw, ImageFilter, ImageOps -from pyrogram import Client, filters, enums +import tempfile +import json +import asyncio +import time +from pyrogram import Client, filters from pyrogram.types import Message - -# noinspection PyUnresolvedReferences from utils.misc import modules_help, prefix -# noinspection PyUnresolvedReferences -from utils.scripts import import_library, format_exc - - -VideoFileClip = import_library("moviepy", "moviepy==2.2.1").VideoFileClip - -im = None - - -def process_img(filename): - global im - im = Image.open(f"downloads/{filename}") - w, h = im.size - img = Image.new("RGBA", (w, h), (0, 0, 0, 0)) - img.paste(im, (0, 0)) - m = min(w, h) - img = img.crop(((w - m) // 2, (h - m) // 2, (w + m) // 2, (h + m) // 2)) - w, h = img.size - mask = Image.new("L", (w, h), 0) - draw = ImageDraw.Draw(mask) - draw.ellipse((10, 10, w - 10, h - 10), fill=255) - mask = mask.filter(ImageFilter.GaussianBlur(2)) - img = ImageOps.fit(img, (w, h)) - img.putalpha(mask) - im = BytesIO() - im.name = "img.webp" - img.save(im) - im.seek(0) - - -video = None - - -def process_vid(filename): - global video - video = VideoFileClip(f"downloads/{filename}") - w, h = video.size - m = min(w, h) - box = { - "x1": (w - m) // 2, - "y1": (h - m) // 2, - "x2": (w + m) // 2, - "y2": (h + m) // 2, - } - video = video.cropped(**box) - - -@Client.on_message(filters.command(["circle", "round"], prefix) & filters.me) -async def circle(_, message: Message): +async def run_subprocess(cmd, timeout=None): + proc = await asyncio.create_subprocess_exec( + *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout) + return proc.returncode, stdout.decode(), stderr.decode() + except asyncio.TimeoutError: + proc.kill() + await proc.communicate() + raise + +async def get_video_duration(video_path): + cmd = [ + "ffprobe", "-v", "error", "-show_entries", "format=duration", + "-of", "json", video_path + ] try: - if not message.reply_to_message: - return await message.reply( - "Reply is required for this command", - parse_mode=enums.ParseMode.HTML, - ) - if message.reply_to_message.photo: - filename = "circle.jpg" - typ = "photo" - elif message.reply_to_message.sticker: - if message.reply_to_message.sticker.is_video: - return await message.reply( - "Video stickers is not supported", - parse_mode=enums.ParseMode.HTML, - ) - filename = "circle.webp" - typ = "photo" - elif message.reply_to_message.video: - filename = "circle.mp4" - typ = "video" - elif message.reply_to_message.document: - _filename = message.reply_to_message.document.file_name.casefold() - if _filename.endswith(".png"): - filename = "circle.png" - typ = "photo" - elif _filename.endswith(".jpg"): - filename = "circle.jpg" - typ = "photo" - elif _filename.endswith(".jpeg"): - filename = "circle.jpeg" - typ = "photo" - elif _filename.endswith(".webp"): - filename = "circle.webp" - typ = "photo" - elif _filename.endswith(".mp4"): - filename = "circle.mp4" - typ = "video" + returncode, stdout, _ = await run_subprocess(cmd, timeout=20) + if returncode != 0 or not stdout: + return 10 + return min(float(json.loads(stdout)["format"]["duration"]), 60) + except Exception: + return 10 + +@Client.on_message(filters.command("circle", prefix) & filters.me) +async def send_circle_video(client: Client, message: Message): + reply = getattr(message, "reply_to_message", None) + if not reply or not (reply.video or reply.animation or reply.document): + await message.edit("Reply to a video.") + return + + parts = message.text.strip().split() + target_username = parts[1] if len(parts) >= 2 and parts[1].startswith("@") else None + send_to_current_chat = target_username is None + + if len(parts) >= 2 and not parts[1].startswith("@"): + await message.edit("`.circle` or `.circle @user`") + return + + await message.edit("Downloading...") + + video_path = circle_path = None + try: + video_path = await client.download_media(reply) + if not video_path: + await message.edit("Download failed.") + return + + video_duration = await get_video_duration(video_path) + + with tempfile.NamedTemporaryFile(suffix='_circle.mp4', delete=False) as temp_file: + circle_path = temp_file.name + + await message.edit("Processing...") + + # Use higher bitrate and better scaling algorithm to reduce blurriness + cmd = [ + "ffmpeg", "-y", "-i", video_path, + "-filter_complex", + ( + "[0:v]scale=640:640:force_original_aspect_ratio=increase:flags=lanczos," + "crop=640:640[scaled];" + "color=white:size=640x640[c];" + "[scaled]format=rgba," + "geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':" + "a='if(gte(hypot(X-320,Y-320),320),0,255)'[masked];" + "[c][masked]overlay=0:0" + ), + "-c:v", "libx264", "-pix_fmt", "yuv420p", + "-c:a", "aac", "-b:v", "2000k", "-b:a", "128k", + "-r", "30", "-t", str(video_duration), + "-preset", "ultrafast", "-movflags", "+faststart", + circle_path + ] + + returncode, _, stderr = await run_subprocess(cmd, timeout=300) + if returncode != 0 or not os.path.exists(circle_path) or os.path.getsize(circle_path) == 0: + await message.edit("FFmpeg error." if returncode != 0 else "Convert failed.") + return + + await message.edit("Sending...") + send_kwargs = dict( + video_note=circle_path, duration=int(video_duration), length=640 + ) + try: + if send_to_current_chat: + await client.send_video_note(chat_id=message.chat.id, **send_kwargs) + await message.edit("Done!") else: - return await message.reply( - "Invalid file type", parse_mode=enums.ParseMode.HTML - ) - else: - return await message.reply( - "Invalid file type", parse_mode=enums.ParseMode.HTML - ) - - if typ == "photo": - await message.edit( - "Processing image📷", parse_mode=enums.ParseMode.HTML - ) - await message.reply_to_message.download(f"downloads/{filename}") - await asyncio.get_event_loop().run_in_executor(None, process_img, filename) - await message.delete() - return await message.reply_sticker( - sticker=im, reply_to_message_id=message.reply_to_message.id - ) - else: - await message.edit( - "Processing video🎥", parse_mode=enums.ParseMode.HTML - ) - await message.reply_to_message.download(f"downloads/{filename}") - await asyncio.get_event_loop().run_in_executor(None, process_vid, filename) - - await message.edit("Saving video📼", parse_mode=enums.ParseMode.HTML) - await asyncio.get_event_loop().run_in_executor( - None, video.write_videofile, "downloads/result.mp4" - ) + await client.send_video_note(chat_id=target_username, **send_kwargs) + await message.edit(f"Sent to {target_username}") + except Exception as e: + await message.edit("Send failed.") - await message.delete() - await message.reply_video_note( - video_note="downloads/result.mp4", - duration=int(video.duration), - reply_to_message_id=message.reply_to_message.id, - ) - if isinstance(video, VideoFileClip): - video.close() - os.remove(f"downloads/{filename}") - os.remove("downloads/result.mp4") + except asyncio.TimeoutError: + await message.edit("Timeout.") except Exception as e: - await message.reply(format_exc(e), parse_mode=enums.ParseMode.HTML) - - + await message.edit("Error.") + finally: + for path in (video_path, circle_path): + try: + if path and os.path.exists(path): + os.remove(path) + except Exception: + pass + modules_help["circle"] = { - "round": "Round a photo or video.", - "circle": "Circle a photo or video.", + "circle": "Convert video to circle. High quality and less blurry." }