import gradio as gr from PIL import Image, ImageDraw import io import numpy as np from math import cos, radians, sin import os import time import json import random import imageio # Load emojis from JSON file with open("emojis.json") as f: emojis = json.load(f)["emojis"] # Directory where emoji PNGs are stored emoji_dir = "emoji_pngs" def preprocess_image(image, emoji_name=None, enable_emoji=True): # Ensure the image is square by padding it size = max(image.size) new_image = Image.new("RGBA", (size, size), (255, 255, 255, 0)) new_image.paste(image, ((size - image.width) // 2, (size - image.height) // 2)) # Create resized images for GIF and Video new_image_gif = new_image.resize((256, 256), Image.LANCZOS) # Resize for GIF new_image_video = new_image.resize((720, 720), Image.LANCZOS) # Resize for Video # Only add emoji if enabled if enable_emoji: # Use the provided emoji or select a random one if not emoji_name: emoji_name = random.choice(list(emojis.keys())) emoji_path = os.path.join(emoji_dir, f"{emoji_name}.png") if os.path.exists(emoji_path): emoji_image = Image.open(emoji_path).convert("RGBA") emoji_image_gif = emoji_image.resize((256, 256), Image.LANCZOS) emoji_image_video = emoji_image.resize((720, 720), Image.LANCZOS) new_image_gif.paste(emoji_image_gif, (0, 0), emoji_image_gif) new_image_video.paste(emoji_image_video, (0, 0), emoji_image_video) else: emoji_name = "none" # Use a placeholder when emojis are disabled return new_image_gif, new_image_video, emoji_name def generate_media(editor1_output, editor2_output, transition_type, fps, transition_speed, color, thickness, enable_emoji): frames = [] gif_duration = 100 # Duration for each frame in the GIF in milliseconds total_frames_gif = 18 # Total number of frames for GIF total_frames_mp4 = int(3 * fps) # Calculate number of frames based on speed try: # Open images img1 = editor1_output["composite"].convert('RGBA') img2 = editor2_output["composite"].convert('RGBA') # Preprocess images to make them square and same size, select emojis once img1_gif, img1_video, random_emoji1 = preprocess_image(img1, enable_emoji=enable_emoji) img2_gif, img2_video, random_emoji2 = preprocess_image(img2, enable_emoji=enable_emoji) # Set size for the GIF gif_size = (256, 256) # Set size for the MP4 video_size = (720, 720) # Locked to 1:1 aspect ratio # Initialize video frames list video_frames = [] # Generate frames for GIF and video if transition_type == "slide": # Calculate step size for consistent speed full_width_gif = gif_size[0] full_width_video = video_size[0] step_gif = full_width_gif // (total_frames_gif // 2) step_video = int(full_width_video / (total_frames_mp4 / transition_speed)) # Generate frames from left to right for GIF for i in range(0, full_width_gif, step_gif): frame = Image.new('RGBA', gif_size) frame.paste(img1_gif, (0, 0)) frame.paste(img2_gif.crop((i, 0, full_width_gif, gif_size[1])), (i, 0), mask=img2_gif.crop((i, 0, full_width_gif, gif_size[1]))) draw = ImageDraw.Draw(frame) draw.line((i, 0, i, gif_size[1]), fill=color, width=thickness // 2) # Reduced line width for GIF frame = frame.convert('P', palette=Image.ADAPTIVE) frames.append(frame) # Generate frames for video from left to right for i in range(0, full_width_video, step_video): frame = Image.new('RGBA', video_size) frame.paste(img1_video, (0, 0)) frame.paste(img2_video.crop((i, 0, full_width_video, video_size[1])), (i, 0), mask=img2_video.crop((i, 0, full_width_video, video_size[1]))) # Add the green divider line draw = ImageDraw.Draw(frame) draw.line((i, 0, i, video_size[1]), fill=color, width=thickness) # Thicker line for video video_frames.append(np.array(frame)) # Generate frames from right to left for GIF for i in range(full_width_gif, step_gif, -step_gif): frame = Image.new('RGBA', gif_size) frame.paste(img1_gif, (0, 0)) frame.paste(img2_gif.crop((i, 0, full_width_gif, gif_size[1])), (i, 0), mask=img2_gif.crop((i, 0, full_width_gif, gif_size[1]))) draw = ImageDraw.Draw(frame) draw.line((i, 0, i, gif_size[1]), fill=color, width=thickness // 2) # Reduced line width for GIF frame = frame.convert('P', palette=Image.ADAPTIVE) frames.append(frame) # Generate frames for video from right to left for i in range(full_width_video, step_video, -step_video): frame = Image.new('RGBA', video_size) frame.paste(img1_video, (0, 0)) frame.paste(img2_video.crop((i, 0, full_width_video, video_size[1])), (i, 0), mask=img2_video.crop((i, 0, full_width_video, video_size[1]))) # Add the green divider line draw = ImageDraw.Draw(frame) draw.line((i, 0, i, video_size[1]), fill=color, width=thickness) # Thicker line for video video_frames.append(np.array(frame)) else: # rotate transition mask_size_gif = (gif_size[0] * 2, gif_size[1] * 2) mask_size_video = (video_size[0] * 2, video_size[1] * 2) # Prepare mask and draw objects mask_gif = Image.new('L', mask_size_gif, 0) draw_gif = ImageDraw.Draw(mask_gif) draw_gif.rectangle([gif_size[0], 0, mask_size_gif[0], mask_size_gif[1]], fill=255) mask_video = Image.new('L', mask_size_video, 0) draw_video = ImageDraw.Draw(mask_video) draw_video.rectangle([video_size[0], 0, mask_size_video[0], mask_size_video[1]], fill=255) # Rotate transition for GIF for angle in range(0, 360, 360 // total_frames_gif): rotated_mask = mask_gif.rotate(angle, center=(mask_size_gif[0] // 2, mask_size_gif[1] // 2), expand=False) cropped_mask = rotated_mask.crop( (gif_size[0] // 2, gif_size[1] // 2, gif_size[0] // 2 + gif_size[0], gif_size[1] // 2 + gif_size[1])) frame = Image.composite(img1_gif, img2_gif, cropped_mask) draw = ImageDraw.Draw(frame) reverse_angle = -angle + 90 center_x, center_y = gif_size[0] // 2, gif_size[1] // 2 end_x1 = center_x + int(gif_size[0] * 1.5 * cos(radians(reverse_angle))) end_y1 = center_y + int(gif_size[1] * 1.5 * sin(radians(reverse_angle))) end_x2 = center_x - int(gif_size[0] * 1.5 * cos(radians(reverse_angle))) end_y2 = center_y - int(gif_size[1] * 1.5 * sin(radians(reverse_angle))) draw.line([center_x, center_y, end_x1, end_y1], fill=color, width=thickness // 2) # Reduced line width for GIF draw.line([center_x, center_y, end_x2, end_y2], fill=color, width=thickness // 2) # Reduced line width for GIF frame = frame.convert('P', palette=Image.ADAPTIVE) frames.append(frame) # Rotate transition for video for angle in range(0, 360, int(360 / (total_frames_mp4 / transition_speed))): rotated_mask = mask_video.rotate(angle, center=(mask_size_video[0] // 2, mask_size_video[1] // 2), expand=False) cropped_mask = rotated_mask.crop( (video_size[0] // 2, video_size[1] // 2, video_size[0] // 2 + video_size[0], video_size[1] // 2 + video_size[1])) frame = Image.composite(img1_video, img2_video, cropped_mask) draw = ImageDraw.Draw(frame) reverse_angle = -angle + 90 center_x, center_y = video_size[0] // 2, video_size[1] // 2 end_x1 = center_x + int(video_size[0] * 1.5 * cos(radians(reverse_angle))) end_y1 = center_y + int(video_size[1] * 1.5 * sin(radians(reverse_angle))) end_x2 = center_x - int(video_size[0] * 1.5 * cos(radians(reverse_angle))) end_y2 = center_y - int(video_size[1] * 1.5 * sin(radians(reverse_angle))) draw.line([center_x, center_y, end_x1, end_y1], fill=color, width=thickness) # Thicker line for video draw.line([center_x, center_y, end_x2, end_y2], fill=color, width=thickness) # Thicker line for video video_frames.append(np.array(frame)) # Save as GIF output_gif = io.BytesIO() frames[0].save(output_gif, format='GIF', save_all=True, append_images=frames[1:], duration=gif_duration, loop=0, optimize=True) output_gif.seek(0) # Save the GIF to a file with a unique name os.makedirs("outputs", exist_ok=True) timestamp = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time())) nonce = int((time.time() * 2) % 2) # create a nonce to the 1/2 second output_gif_path = f"outputs/{random_emoji1}-{random_emoji2}-{timestamp}{nonce}.gif" with open(output_gif_path, "wb") as f: f.write(output_gif.getvalue()) # Save as MP4 output_mp4_path = f"outputs/{random_emoji1}-{random_emoji2}-{timestamp}{nonce}.mp4" imageio.mimsave(output_mp4_path, video_frames, fps=fps, macro_block_size=None) # Frame rate # Return paths for download return output_gif_path, output_mp4_path except Exception as e: print(f"Error creating media: {e}") # Debugging output raise ValueError(f"Error creating media: {e}") # Gradio interface with gr.Blocks() as iface: gr.Markdown("# 2GIF and MP4 Transition Generator") with gr.Row(): with gr.Column(scale=2): image_editor1 = gr.ImageEditor( label="Edit Image 1", brush=gr.Brush(colors=["#ff0000", "#00ff00", "#0000ff"]), eraser=gr.Eraser(default_size=10), height=400, width=400, crop_size=(720, 720), # Specify exact crop size layers=True, type="pil" ) with gr.Column(scale=2): image_editor2 = gr.ImageEditor( label="Edit Image 2", brush=gr.Brush(colors=["#ff0000", "#00ff00", "#0000ff"]), eraser=gr.Eraser(default_size=10), height=400, width=400, crop_size=(720, 720), # Specify exact crop size layers=True, type="pil" ) with gr.Row(): transition_type = gr.Radio(["slide", "rotate"], label="Transition Type", value="slide") enable_emoji = gr.Checkbox(label="Enable Emojis", value=True, info="Toggle emoji overlays on/off") generate_button = gr.Button("Generate GIF & MP4") with gr.Accordion("Advanced Settings", open=False): with gr.Row(): fps = gr.Slider(label="Frame Rate (fps)", minimum=1, maximum=60, value=18, step=1) transition_speed = gr.Slider(label="Transition Speed", minimum=1, maximum=10, value=1.42069, step=0.1) color = gr.ColorPicker(label="Divider Line Color", value="#00ff00") thickness = gr.Slider(label="Line Thickness", minimum=1, maximum=20, value=6, step=1) with gr.Row(): gif_display = gr.Image(label="Generated GIF", elem_id="gif_display", visible=True) gif_download = gr.File(label="Download GIF", elem_id="gif_download", visible=True) mp4_download = gr.File(label="Download MP4", elem_id="mp4_download", visible=True) def handle_generation(editor1_output, editor2_output, transition_type, fps, transition_speed, color, thickness, enable_emoji): try: gif_path, mp4_path = generate_media(editor1_output, editor2_output, transition_type, fps, transition_speed, color, thickness, enable_emoji) return gif_path, gif_path, mp4_path except Exception as e: print(f"Error in handle_generation: {e}") return None, None, None generate_button.click( handle_generation, inputs=[image_editor1, image_editor2, transition_type, fps, transition_speed, color, thickness, enable_emoji], outputs=[gif_display, gif_download, mp4_download] ) # Launch the interface iface.launch(share=False)