Spaces:
Sleeping
Sleeping
| 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]) |