digidau commited on
Commit
09894b2
·
verified ·
1 Parent(s): 3d2f8d8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +41 -108
app.py CHANGED
@@ -12,8 +12,7 @@ os.makedirs(OUTPUT_DIR, exist_ok=True)
12
 
13
  def get_video_duration(video_path):
14
  cmd = [
15
- "ffprobe",
16
- "-v", "error",
17
  "-show_entries", "format=duration",
18
  "-of", "default=noprint_wrappers=1:nokey=1",
19
  video_path
@@ -21,14 +20,7 @@ def get_video_duration(video_path):
21
  result = subprocess.check_output(cmd).decode().strip()
22
  return float(result)
23
 
24
- def create_loop(
25
- input_video,
26
- loop_method,
27
- fade_duration,
28
- transition,
29
- fps,
30
- crf
31
- ):
32
  if input_video is None:
33
  raise gr.Error("Please upload a video.")
34
 
@@ -36,7 +28,6 @@ def create_loop(
36
  input_path = os.path.join(TEMP_DIR, f"{uid}_input.mp4")
37
  output_path = os.path.join(OUTPUT_DIR, f"{uid}_loop.mp4")
38
 
39
- # Handling Gradio video input
40
  if isinstance(input_video, dict):
41
  video_file = input_video["path"]
42
  else:
@@ -45,133 +36,79 @@ def create_loop(
45
  shutil.copy(video_file, input_path)
46
  duration = get_video_duration(input_path)
47
 
48
- # Flags untuk High-Quality Scaling & Anti-Aliasing
49
- # Lanczos mencegah gambar "bergerigi" (aliasing)
50
  sws_flags = "flags=lanczos+accurate_rnd+full_chroma_int"
51
-
52
- # Filter dasar: Perbaiki resolusi (harus genap), set FPS, dan format pixel
53
- base_filter = f"format=yuv420p,fps={fps}:round=near,scale=trunc(iw/2)*2:trunc(ih/2)*2:{sws_flags}"
54
 
55
  if loop_method == "Crossfade (Standard)":
56
- if duration <= fade_duration * 2:
57
- raise gr.Error(f"Video too short. Min: {fade_duration * 2:.1f}s")
58
-
59
  offset = duration - fade_duration
60
- filter_complex = f"""
61
- [0:v]{base_filter},split[v1][v2];
62
- [v1][v2]xfade=transition={transition}:duration={fade_duration}:offset={offset},
63
- format=yuv420p[v]
64
- """
65
  target_duration = str(duration - fade_duration)
66
 
67
  elif loop_method == "Split & Swap (Premiere Style)":
68
  midpoint = duration / 2.0
 
 
 
 
69
  offset = midpoint - fade_duration
70
-
71
- if offset <= 0:
72
- raise gr.Error("Fade duration is too long for this video.")
73
-
74
- filter_complex = f"""
75
- [0:v]{base_filter},split[v_src1][v_src2];
76
- [v_src1]trim=start=0:end={midpoint},setpts=PTS-STARTPTS[partA];
77
- [v_src2]trim=start={midpoint}:end={duration},setpts=PTS-STARTPTS[partB];
78
- [partB][partA]xfade=transition={transition}:duration={fade_duration}:offset={offset},
79
- format=yuv420p[v]
80
- """
81
  target_duration = str(duration - fade_duration)
82
 
83
  else:
84
- # PING-PONG / BOOMERANG METHOD
85
- filter_complex = f"""
86
- [0:v]{base_filter},split[fwd][rev_src];
87
- [rev_src]reverse[rev];
88
- [fwd][rev]concat=n=2:v=1:a=0,
89
- format=yuv420p[v]
90
- """
91
  target_duration = str(duration * 2)
92
 
93
- # PENGATURAN ENCODER (Optimasi Kualitas Tinggi)
94
  cmd = [
95
- "ffmpeg", "-y",
96
- "-i", input_path,
97
  "-filter_complex", filter_complex,
98
- "-map", "[v]",
99
- "-t", target_duration,
100
- "-an", # Hapus audio agar loop lebih fokus ke visual
101
- "-c:v", "libx264",
102
- "-preset", "slower", # Lebih lambat = kualitas kompresi lebih halus
103
- "-profile:v", "high",
104
- "-tune", "film", # Menjaga tekstur halus (grass/flowers)
105
- "-crf", str(crf),
106
- "-pix_fmt", "yuv420p",
107
- "-movflags", "+faststart", # Optimal untuk playback browser
108
  output_path
109
  ]
110
 
111
  try:
112
- subprocess.run(cmd, check=True)
113
  except subprocess.CalledProcessError as e:
114
- raise gr.Error(f"FFmpeg Error: {e}")
 
115
 
116
  preview_html = f"""
117
- <div style="border-radius:12px; overflow:hidden; background-color: #000; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.3);">
118
- <video autoplay loop muted controls playsinline width="100%" style="display: block;">
119
  <source src="/file={output_path}" type="video/mp4">
120
  </video>
121
  </div>
122
- <p style="margin-top:10px; opacity:0.7; font-size:14px; text-align:center;">
123
- ✨ Seamless loop preview
124
- </p>
125
  """
126
-
127
  return output_path, preview_html
128
 
129
- # UI GRADIO
130
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
131
- gr.Markdown(
132
- """
133
- # 🎬 Seamless Loop Video Maker Pro
134
- Membuat loop video tanpa patah dengan kualitas sinematik (Anti-Aliasing).
135
- """
136
- )
137
-
138
  with gr.Row():
139
  with gr.Column():
140
- input_video = gr.Video(label="Upload Video", sources=["upload"])
141
-
142
  loop_method = gr.Radio(
143
- choices=[
144
- "Split & Swap (Premiere Style)",
145
- "Ping-Pong (No Ghosting)",
146
- "Crossfade (Standard)"
147
- ],
148
- value="Split & Swap (Premiere Style)",
149
- label="Loop Method"
150
  )
151
-
152
- with gr.Row():
153
- fade_duration = gr.Slider(
154
- minimum=0.1, maximum=5.0, value=1.0, step=0.1,
155
- label="Fade Duration (sec)"
156
- )
157
- fps = gr.Dropdown(choices=[24, 30, 60], value=24, label="FPS")
158
-
159
- with gr.Row():
160
- transition = gr.Dropdown(
161
- choices=["dissolve", "fade", "smoothleft", "fadeblack", "pixelize"],
162
- value="dissolve",
163
- label="Transition Type"
164
- )
165
- crf = gr.Slider(
166
- minimum=12, maximum=28, value=18, step=1,
167
- label="Quality (Lower is Better)"
168
- )
169
-
170
- generate_btn = gr.Button("🚀 Generate Seamless Loop", variant="primary")
171
-
172
  with gr.Column():
173
- preview = gr.HTML(label="Loop Preview")
174
- output_video = gr.Video(label="Download Rendered MP4")
175
 
176
  generate_btn.click(
177
  fn=create_loop,
@@ -180,8 +117,4 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
180
  )
181
 
182
  if __name__ == "__main__":
183
- demo.launch(
184
- server_name="0.0.0.0",
185
- server_port=7860,
186
- allowed_paths=[OUTPUT_DIR]
187
- )
 
12
 
13
  def get_video_duration(video_path):
14
  cmd = [
15
+ "ffprobe", "-v", "error",
 
16
  "-show_entries", "format=duration",
17
  "-of", "default=noprint_wrappers=1:nokey=1",
18
  video_path
 
20
  result = subprocess.check_output(cmd).decode().strip()
21
  return float(result)
22
 
23
+ def create_loop(input_video, loop_method, fade_duration, transition, fps, crf):
 
 
 
 
 
 
 
24
  if input_video is None:
25
  raise gr.Error("Please upload a video.")
26
 
 
28
  input_path = os.path.join(TEMP_DIR, f"{uid}_input.mp4")
29
  output_path = os.path.join(OUTPUT_DIR, f"{uid}_loop.mp4")
30
 
 
31
  if isinstance(input_video, dict):
32
  video_file = input_video["path"]
33
  else:
 
36
  shutil.copy(video_file, input_path)
37
  duration = get_video_duration(input_path)
38
 
39
+ # Filter Anti-Aliasing & Standarisasi
 
40
  sws_flags = "flags=lanczos+accurate_rnd+full_chroma_int"
41
+ # Filter dasar tanpa spasi/newline yang merusak sintaks FFmpeg
42
+ base = f"format=yuv420p,fps={fps}:round=near,scale=trunc(iw/2)*2:trunc(ih/2)*2:{sws_flags}"
 
43
 
44
  if loop_method == "Crossfade (Standard)":
45
+ if duration <= fade_duration:
46
+ fade_duration = duration / 2
 
47
  offset = duration - fade_duration
48
+ filter_complex = f"[0:v]{base},split[v1][v2];[v1][v2]xfade=transition={transition}:duration={fade_duration}:offset={offset},format=yuv420p[v]"
 
 
 
 
49
  target_duration = str(duration - fade_duration)
50
 
51
  elif loop_method == "Split & Swap (Premiere Style)":
52
  midpoint = duration / 2.0
53
+ # Beri margin 0.2 detik agar tidak error exit status 234
54
+ if fade_duration >= midpoint:
55
+ fade_duration = midpoint - 0.2
56
+
57
  offset = midpoint - fade_duration
58
+ filter_complex = (
59
+ f"[0:v]{base},split[v_src1][v_src2];"
60
+ f"[v_src1]trim=start=0:end={midpoint},setpts=PTS-STARTPTS[partA];"
61
+ f"[v_src2]trim=start={midpoint}:end={duration},setpts=PTS-STARTPTS[partB];"
62
+ f"[partB][partA]xfade=transition={transition}:duration={fade_duration}:offset={offset},format=yuv420p[v]"
63
+ )
 
 
 
 
 
64
  target_duration = str(duration - fade_duration)
65
 
66
  else:
67
+ # PING-PONG
68
+ 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]"
 
 
 
 
 
69
  target_duration = str(duration * 2)
70
 
 
71
  cmd = [
72
+ "ffmpeg", "-y", "-i", input_path,
 
73
  "-filter_complex", filter_complex,
74
+ "-map", "[v]", "-t", target_duration, "-an",
75
+ "-c:v", "libx264", "-preset", "slower", "-crf", str(crf),
76
+ "-pix_fmt", "yuv420p", "-movflags", "+faststart",
 
 
 
 
 
 
 
77
  output_path
78
  ]
79
 
80
  try:
81
+ subprocess.run(cmd, check=True, capture_output=True)
82
  except subprocess.CalledProcessError as e:
83
+ error_msg = e.stderr.decode()
84
+ raise gr.Error(f"FFmpeg Error: {error_msg}")
85
 
86
  preview_html = f"""
87
+ <div style="border-radius:12px; overflow:hidden; background-color: #000;">
88
+ <video autoplay loop muted controls playsinline width="100%">
89
  <source src="/file={output_path}" type="video/mp4">
90
  </video>
91
  </div>
 
 
 
92
  """
 
93
  return output_path, preview_html
94
 
 
95
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
96
+ gr.Markdown("# 🎬 Seamless Loop Maker Pro")
 
 
 
 
 
 
97
  with gr.Row():
98
  with gr.Column():
99
+ input_video = gr.Video(label="Upload Video")
 
100
  loop_method = gr.Radio(
101
+ choices=["Split & Swap (Premiere Style)", "Ping-Pong (No Ghosting)", "Crossfade (Standard)"],
102
+ value="Split & Swap (Premiere Style)", label="Loop Method"
 
 
 
 
 
103
  )
104
+ fade_duration = gr.Slider(minimum=0.1, maximum=5.0, value=1.0, step=0.1, label="Fade Duration (sec)")
105
+ fps = gr.Dropdown(choices=[24, 30, 60], value=24, label="FPS")
106
+ transition = gr.Dropdown(choices=["dissolve", "fade", "smoothleft", "fadeblack"], value="dissolve", label="Transition")
107
+ crf = gr.Slider(minimum=12, maximum=28, value=18, step=1, label="Quality (CRF)")
108
+ generate_btn = gr.Button("🚀 Generate Loop", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  with gr.Column():
110
+ preview = gr.HTML()
111
+ output_video = gr.Video(label="Download MP4")
112
 
113
  generate_btn.click(
114
  fn=create_loop,
 
117
  )
118
 
119
  if __name__ == "__main__":
120
+ demo.launch(server_name="0.0.0.0", server_port=7860, allowed_paths=[OUTPUT_DIR])