loop / app.py
digidau's picture
Update app.py
09894b2 verified
Raw
History Blame Contribute Delete
4.75 kB
import os
import uuid
import shutil
import subprocess
import gradio as gr
TEMP_DIR = os.path.abspath("temp")
OUTPUT_DIR = os.path.abspath("outputs")
os.makedirs(TEMP_DIR, exist_ok=True)
os.makedirs(OUTPUT_DIR, exist_ok=True)
def get_video_duration(video_path):
cmd = [
"ffprobe", "-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
video_path
]
result = subprocess.check_output(cmd).decode().strip()
return float(result)
def create_loop(input_video, loop_method, fade_duration, transition, fps, crf):
if input_video is None:
raise gr.Error("Please upload a video.")
uid = str(uuid.uuid4())[:8]
input_path = os.path.join(TEMP_DIR, f"{uid}_input.mp4")
output_path = os.path.join(OUTPUT_DIR, f"{uid}_loop.mp4")
if isinstance(input_video, dict):
video_file = input_video["path"]
else:
video_file = input_video
shutil.copy(video_file, input_path)
duration = get_video_duration(input_path)
# Filter Anti-Aliasing & Standarisasi
sws_flags = "flags=lanczos+accurate_rnd+full_chroma_int"
# Filter dasar tanpa spasi/newline yang merusak sintaks FFmpeg
base = f"format=yuv420p,fps={fps}:round=near,scale=trunc(iw/2)*2:trunc(ih/2)*2:{sws_flags}"
if loop_method == "Crossfade (Standard)":
if duration <= fade_duration:
fade_duration = duration / 2
offset = duration - fade_duration
filter_complex = f"[0:v]{base},split[v1][v2];[v1][v2]xfade=transition={transition}:duration={fade_duration}:offset={offset},format=yuv420p[v]"
target_duration = str(duration - fade_duration)
elif loop_method == "Split & Swap (Premiere Style)":
midpoint = duration / 2.0
# Beri margin 0.2 detik agar tidak error exit status 234
if fade_duration >= midpoint:
fade_duration = midpoint - 0.2
offset = midpoint - fade_duration
filter_complex = (
f"[0:v]{base},split[v_src1][v_src2];"
f"[v_src1]trim=start=0:end={midpoint},setpts=PTS-STARTPTS[partA];"
f"[v_src2]trim=start={midpoint}:end={duration},setpts=PTS-STARTPTS[partB];"
f"[partB][partA]xfade=transition={transition}:duration={fade_duration}:offset={offset},format=yuv420p[v]"
)
target_duration = str(duration - fade_duration)
else:
# PING-PONG
filter_complex = f"[0:v]{base},split[fwd][rev_src];[rev_src]reverse[rev];[fwd][rev]concat=n=2:v=1:a=0,format=yuv420p[v]"
target_duration = str(duration * 2)
cmd = [
"ffmpeg", "-y", "-i", input_path,
"-filter_complex", filter_complex,
"-map", "[v]", "-t", target_duration, "-an",
"-c:v", "libx264", "-preset", "slower", "-crf", str(crf),
"-pix_fmt", "yuv420p", "-movflags", "+faststart",
output_path
]
try:
subprocess.run(cmd, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
error_msg = e.stderr.decode()
raise gr.Error(f"FFmpeg Error: {error_msg}")
preview_html = f"""
<div style="border-radius:12px; overflow:hidden; background-color: #000;">
<video autoplay loop muted controls playsinline width="100%">
<source src="/file={output_path}" type="video/mp4">
</video>
</div>
"""
return output_path, preview_html
with gr.Blocks(theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🎬 Seamless Loop Maker Pro")
with gr.Row():
with gr.Column():
input_video = gr.Video(label="Upload Video")
loop_method = gr.Radio(
choices=["Split & Swap (Premiere Style)", "Ping-Pong (No Ghosting)", "Crossfade (Standard)"],
value="Split & Swap (Premiere Style)", label="Loop Method"
)
fade_duration = gr.Slider(minimum=0.1, maximum=5.0, value=1.0, step=0.1, label="Fade Duration (sec)")
fps = gr.Dropdown(choices=[24, 30, 60], value=24, label="FPS")
transition = gr.Dropdown(choices=["dissolve", "fade", "smoothleft", "fadeblack"], value="dissolve", label="Transition")
crf = gr.Slider(minimum=12, maximum=28, value=18, step=1, label="Quality (CRF)")
generate_btn = gr.Button("πŸš€ Generate Loop", variant="primary")
with gr.Column():
preview = gr.HTML()
output_video = gr.Video(label="Download MP4")
generate_btn.click(
fn=create_loop,
inputs=[input_video, loop_method, fade_duration, transition, fps, crf],
outputs=[output_video, preview]
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, allowed_paths=[OUTPUT_DIR])