File size: 4,754 Bytes
45b6e45
 
 
 
 
 
f64a0f3
 
45b6e45
 
 
 
 
 
09894b2
45b6e45
 
 
 
 
 
 
09894b2
45b6e45
 
 
 
15a5a43
 
45b6e45
347bbc1
 
 
 
 
 
45b6e45
 
09894b2
3d2f8d8
09894b2
 
3d2f8d8
15a5a43
09894b2
 
15a5a43
09894b2
3d2f8d8
15a5a43
8010e39
 
09894b2
 
 
 
8010e39
09894b2
 
 
 
 
 
8010e39
 
15a5a43
09894b2
 
15a5a43
45b6e45
 
09894b2
45b6e45
09894b2
 
 
45b6e45
 
 
3d2f8d8
09894b2
3d2f8d8
09894b2
 
45b6e45
 
09894b2
 
45b6e45
 
 
 
 
 
3d2f8d8
09894b2
45b6e45
 
09894b2
15a5a43
09894b2
 
15a5a43
09894b2
 
 
 
 
45b6e45
09894b2
 
45b6e45
 
 
15a5a43
 
45b6e45
 
3d2f8d8
09894b2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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])