vid_2_gif / app.py
broadfield-dev's picture
Update app.py
53bb891 verified
Raw
History Blame Contribute Delete
9.72 kB
import gradio as gr
import yt_dlp
import os
import tempfile
import math
import logging
# Imports for moviepy version 1.0.3
from moviepy.editor import VideoFileClip
import moviepy.video.fx.all as vfx
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
# Configure logging to see more details from moviepy in the console
logging.basicConfig(level=logging.INFO)
LOGGER = logging.getLogger(__name__)
def download_video(url, uploaded_file, state):
video_path = None
if uploaded_file is not None:
video_path = uploaded_file
elif url and url.strip():
try:
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_video_file:
video_path = temp_video_file.name
ydl_opts = {
'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
'outtmpl': video_path,
'overwrites': True,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
except Exception as e:
raise gr.Error(f"Failed to download video: {str(e)}")
else:
return None, None, state, "0", "0", "0:00:00", gr.update(maximum=1, value=1)
if not video_path or not os.path.exists(video_path):
raise gr.Error("Video file could not be created or found.")
try:
with VideoFileClip(video_path) as clip:
fps = clip.fps
duration = clip.duration
frame_count = int(duration * fps)
hours, rem = divmod(duration, 3600)
minutes, seconds = divmod(rem, 60)
vid_len_str = f"{int(hours):01}:{int(minutes):02}:{math.floor(seconds):02}"
state['video_path'] = video_path
state['active_path'] = video_path
state['fps'] = fps
gif_fps_update = gr.update(maximum=math.ceil(fps), value=min(15, math.ceil(fps)))
return video_path, video_path, state, str(frame_count), str(fps), vid_len_str, gif_fps_update
except Exception as e:
if video_path and os.path.exists(video_path):
os.remove(video_path)
raise gr.Error(f"Failed to process video: {str(e)}")
def parse_time(time_str):
parts = time_str.split(':')
try:
if len(parts) == 3:
h, m, s = map(float, parts)
return h * 3600 + m * 60 + s
elif len(parts) == 2:
m, s = map(float, parts)
return m * 60 + s
elif len(parts) == 1:
return float(parts[0])
else:
raise ValueError("Invalid time format")
except ValueError:
raise gr.Error("Invalid time format. Please use HH:MM:SS, MM:SS, or seconds.")
def trim_video(state, start_time, end_time):
video_path = state.get('video_path')
if not video_path or not os.path.exists(video_path):
raise gr.Error("No video loaded to trim.")
try:
start_s = parse_time(start_time)
end_s = parse_time(end_time)
if start_s >= end_s:
raise gr.Error("Start time must be before end time.")
with VideoFileClip(video_path) as clip:
if end_s > clip.duration:
end_s = clip.duration
gr.Warning(f"End time exceeded video duration. It has been adjusted to {clip.duration:.2f}s.")
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_clip_file:
clipped_path = temp_clip_file.name
ffmpeg_extract_subclip(video_path, start_s, end_s, targetname=clipped_path)
with VideoFileClip(clipped_path) as clip:
frame_count = int(clip.duration * clip.fps)
state['active_path'] = clipped_path
return clipped_path, state, str(frame_count)
except Exception as e:
raise gr.Error(f"Failed to trim video: {str(e)}")
def update_speed(state, clip_speed):
video_path = state.get('active_path')
if not video_path or not os.path.exists(video_path):
raise gr.Error("No video loaded to change speed. Please load or trim a video first.")
try:
with VideoFileClip(video_path) as clip:
# Using the .fx() method, standard for moviepy v1.0.3
sped_up_clip = clip.fx(vfx.speedx, clip_speed)
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_speed_file:
sped_up_path = temp_speed_file.name
new_fps = clip.fps * clip_speed
sped_up_clip.write_videofile(sped_up_path, fps=new_fps)
state['active_path'] = sped_up_path
state['fps'] = new_fps
gif_fps_update = gr.update(maximum=math.ceil(new_fps), value=min(15, math.ceil(new_fps)))
return sped_up_path, state, f"{new_fps:.2f}", gif_fps_update
except Exception as e:
raise gr.Error(f"Failed to update speed: {str(e)}")
def make_gif(state, resize_percent, gif_fps):
yield None, None, "Gathering video data..."
video_path = state.get('active_path')
if not video_path or not os.path.exists(video_path):
raise gr.Error("No video available to create a GIF. Please load or trim a video first.")
try:
yield None, None, "Processing... Check console for ffmpeg progress bar."
with VideoFileClip(video_path) as clip:
# Using the .fx() method, standard for moviepy v1.0.3
if resize_percent != 100:
clip = clip.fx(vfx.resize, resize_percent / 100.0)
with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as temp_gif_file:
gif_path = temp_gif_file.name
clip.write_gif(gif_path, fps=gif_fps, program='ffmpeg', logger='bar')
yield gif_path, gr.File(value=gif_path, visible=True), "GIF creation complete."
except Exception as e:
detailed_error = f"Failed to create GIF: {str(e)}. This often means 'ffmpeg' is not installed or not found in your system's PATH. Please ensure ffmpeg is correctly installed and check the console output for more details."
LOGGER.error(detailed_error, exc_info=True)
raise gr.Error(detailed_error)
css = '''
.padded.svelte-90oupt { background: cornflowerblue; }
.dark .gr-box { background-color: #344d74; }
.p-2 { background-color: #344d74; }
.gap-4 { background-color: #6681ab; }
.dark .gr-padded { background-color: #21314a; }
'''
with gr.Blocks() as app:
state = gr.State(value={'video_path': None, 'active_path': None, 'fps': None})
gr.Markdown("# Video to GIF Converter")
with gr.Row():
gr.Column(scale=1)
with gr.Column(scale=2):
with gr.Group():
gr.Markdown("### 1. Load Video")
inp_url = gr.Textbox(label="Enter Video URL")
inp_upload = gr.File(label="Or Upload Video File", type="filepath")
load_btn = gr.Button("Load Video", variant="primary")
with gr.Accordion("Loaded Video Preview & Details", open=False) as preview_accordion:
outp_vid = gr.Video(label="Current Video Preview")
with gr.Row():
frame_count = gr.Textbox(label="Frame Count", interactive=False)
fps = gr.Textbox(label="Video FPS", interactive=False)
vid_len = gr.Textbox(label="Video Length", interactive=False)
with gr.Group():
gr.Markdown("### 2. Modify Video (Optional)")
gr.Markdown("#### Trim Video")
with gr.Row():
start_f = gr.Textbox(label="Start Time (HH:MM:SS)", value="0:00:00")
end_f = gr.Textbox(label="End Time (HH:MM:SS)", value="0:00:05")
trim_btn = gr.Button("Trim Video")
trim_count = gr.Textbox(label="Trimmed Frame Count", interactive=False)
gr.Markdown("#### Adjust Speed")
clip_speed = gr.Slider(label="Playback Speed Multiplier", minimum=0.1, maximum=4, value=1, step=0.1)
speed_btn = gr.Button("Update Speed")
with gr.Group():
gr.Markdown("### 3. Generate GIF")
resize_slider = gr.Slider(label="Resize Percentage", minimum=10, maximum=100, value=100, step=1)
gif_fps_slider = gr.Slider(label="GIF Frame Rate (FPS)", minimum=1, maximum=60, value=15, step=1,
info="Lower FPS results in a smaller file size.")
gif_btn = gr.Button("Make GIF", variant="primary")
gif_stat = gr.Textbox(label="Status", interactive=False)
with gr.Row():
gif_show = gr.Image(label="Output GIF", interactive=False)
gif_file = gr.File(label="Download GIF", visible=False)
gr.Column(scale=1)
load_btn.click(
fn=download_video,
inputs=[inp_url, inp_upload, state],
outputs=[outp_vid, inp_upload, state, frame_count, fps, vid_len, gif_fps_slider]
).then(lambda: gr.update(open=True), None, preview_accordion)
trim_btn.click(
fn=trim_video,
inputs=[state, start_f, end_f],
outputs=[outp_vid, state, trim_count]
).then(lambda: gr.update(open=True), None, preview_accordion)
speed_btn.click(
fn=update_speed,
inputs=[state, clip_speed],
outputs=[outp_vid, state, fps, gif_fps_slider]
)
gif_btn.click(
fn=make_gif,
inputs=[state, resize_slider, gif_fps_slider],
outputs=[gif_show, gif_file, gif_stat]
)
app.queue().launch(debug=True)