Spaces:
Running
Running
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) | |