xcrop's picture
Update app.py
03bd538 verified
"""
Upload β†’ Shorts (Free, Phone-only) – Hugging Face Space App
"""
import os
import tempfile
from typing import List
import gradio as gr
from moviepy.editor import VideoFileClip
def make_916(clip: VideoFileClip, target_h: int = 1920, target_w: int = 1080) -> VideoFileClip:
"""Resize + crop to 9:16 vertical format."""
resized = clip.resize(height=target_h)
w, h = resized.size
if w == target_w:
return resized
x1 = max(0, (w - target_w) // 2)
x2 = x1 + target_w
return resized.crop(x1=x1, y1=0, x2=x2, y2=target_h)
def slice_evenly(duration: float, clip_len: float, n_clips: int) -> List[float]:
"""Get evenly spaced start times for subclips."""
starts = []
if duration <= 0 or clip_len <= 0 or n_clips <= 0:
return starts
last_valid = max(0.0, duration - clip_len)
for i in range(n_clips):
t = (last_valid * i) / max(1, n_clips - 1)
starts.append(float(t))
return starts
def video_to_shorts(video_path: str, clip_len: int, n_clips: int, fps: int, quality: str):
temp_root = tempfile.mkdtemp(prefix="shorts_")
outputs = []
error_msg = None
try:
base_name = os.path.splitext(os.path.basename(video_path))[0]
with VideoFileClip(video_path) as vid:
duration = float(vid.duration or 0)
if duration <= 1:
raise ValueError("Video too short or failed to load.")
max_clips = max(1, int(duration // max(1, clip_len)))
n_use = min(n_clips, max_clips)
starts = slice_evenly(duration, clip_len, n_use)
if quality == "High":
bitrate = "5000k"
elif quality == "Medium":
bitrate = "3500k"
else:
bitrate = "2000k"
for idx, st in enumerate(starts, start=1):
sub = vid.subclip(st, min(st + clip_len, duration))
sub916 = make_916(sub)
out_path = os.path.join(temp_root, f"{base_name}_short_{idx:02d}.mp4")
sub916.write_videofile(
out_path,
codec="libx264",
audio_codec="aac",
fps=fps,
bitrate=bitrate,
threads=2,
verbose=False,
logger=None,
)
outputs.append(out_path)
if not outputs:
error_msg = "No clips generated. Try shorter length or another video."
except Exception as e:
error_msg = f"Error: {str(e)}"
if error_msg:
return None, error_msg
return outputs, None
def run_app():
with gr.Blocks(title="Upload β†’ Shorts (Free, Phone)") as demo:
gr.Markdown(
"""
# 🎬 Upload β†’ Shorts Converter
Upload a long video and get vertical (9:16) Shorts.
βœ… Works on phone (no PC needed).
"""
)
video_in = gr.File(label="Upload your video", type="filepath")
clip_len = gr.Slider(10, 60, value=25, step=1, label="Clip length (sec)")
n_clips = gr.Slider(1, 10, value=3, step=1, label="Number of clips")
fps = gr.Slider(24, 60, value=30, step=1, label="FPS")
quality = gr.Radio(["High", "Medium", "Low"], value="Medium", label="Quality")
btn = gr.Button("πŸŽ₯ Make Shorts")
gallery = gr.Gallery(label="Your Shorts", columns=3)
status = gr.Markdown()
def submit(video_in, clip_len, n_clips, fps, quality):
if not video_in:
return [], "❌ Please upload a video first."
files, err = video_to_shorts(video_in, int(clip_len), int(n_clips), int(fps), quality)
if err:
return [], f"❌ {err}"
return files, f"βœ… Done! {len(files)} short(s) generated."
btn.click(submit, [video_in, clip_len, n_clips, fps, quality], [gallery, status])
return demo
if __name__ == "__main__":
app = run_app()
app.launch()