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"""
""" 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])