2gif-maker / app.py
Jeffgold's picture
Update app.py
3f3ab80 verified
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)