rbughao commited on
Commit
7b99ffe
·
verified ·
1 Parent(s): 2387415

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -33
app.py CHANGED
@@ -2,7 +2,6 @@ import gradio as gr
2
  import os
3
  import subprocess
4
  import uuid
5
- import shutil
6
  import zipfile
7
  from datetime import datetime
8
  import pandas as pd
@@ -11,14 +10,23 @@ import pandas as pd
11
  OUTPUT_DIR = "outputs"
12
  os.makedirs(OUTPUT_DIR, exist_ok=True)
13
 
14
- def map_level_to_crf(level: str) -> int:
 
 
 
 
 
 
 
 
 
15
  level = (level or "Medium").strip().lower()
16
- if level == "low":
17
- return 22
18
- elif level == "high":
19
- return 32
20
- else:
21
- return 28
22
 
23
  def ffmpeg_available() -> bool:
24
  try:
@@ -27,35 +35,117 @@ def ffmpeg_available() -> bool:
27
  except Exception:
28
  return False
29
 
30
- def compress_single_video(infile_path: str, crf: int, preset: str = "fast", audio_bitrate_kbps: int = 128, progress=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  base = os.path.basename(infile_path)
32
  out_name = f"{os.path.splitext(base)[0]}_compressed_{uuid.uuid4().hex[:8]}.mp4"
33
  out_path = os.path.join(OUTPUT_DIR, out_name)
34
 
35
  original_size = os.path.getsize(infile_path)
36
 
 
 
 
 
 
 
 
 
 
 
37
  cmd = [
38
  "ffmpeg", "-y",
39
  "-i", infile_path,
40
- "-vcodec", "libx264",
 
41
  "-crf", str(crf),
42
  "-preset", preset,
 
 
 
43
  "-acodec", "aac", "-b:a", f"{audio_bitrate_kbps}k",
44
- out_path
 
45
  ]
46
 
 
 
 
 
 
 
47
  if progress:
48
- progress(0.2, desc=f"Compressing {base}...")
49
 
50
  proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
51
- if proc.returncode != 0:
52
- raise RuntimeError(f"FFmpeg failed on {base}:\n{proc.stderr.decode(errors='ignore')}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  compressed_size = os.path.getsize(out_path)
55
  if progress:
56
  progress(0.95, desc=f"Finalizing {base}")
57
 
58
- return out_path, original_size, compressed_size
59
 
60
  def validate_inputs(files):
61
  if not files or len(files) == 0:
@@ -63,20 +153,39 @@ def validate_inputs(files):
63
  paths = []
64
  for f in files:
65
  path = getattr(f, "name", None) or (isinstance(f, dict) and f.get("name")) or None
66
- if not path or not path.lower().endswith(".mp4"):
67
- raise gr.Error(f"Invalid file: {path}")
 
 
68
  paths.append(path)
69
  return paths
70
 
71
- def compress_videos(files, level, use_advanced, crf_value, preset, audio_bitrate_kbps, progress=gr.Progress()):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  if not ffmpeg_available():
73
  raise gr.Error("FFmpeg is not installed. Add it in packages.txt.")
74
 
75
  input_paths = validate_inputs(files)
76
  total_files = len(input_paths)
77
- progress(0, desc=f"Starting compression for {total_files} file(s)...")
78
 
79
- crf = crf_value if use_advanced else map_level_to_crf(level)
 
80
  preset = preset or "fast"
81
  audio_bitrate_kbps = max(64, int(audio_bitrate_kbps or 128))
82
 
@@ -87,11 +196,21 @@ def compress_videos(files, level, use_advanced, crf_value, preset, audio_bitrate
87
  base = os.path.basename(in_path)
88
  try:
89
  progress((idx - 1) / total_files, desc=f"[{idx}/{total_files}] Processing {base}")
90
- out_path, orig_size, comp_size = compress_single_video(in_path, crf, preset, audio_bitrate_kbps, progress)
 
 
 
 
 
 
 
 
91
  reduction_bytes = orig_size - comp_size
92
  reduction_pct = (reduction_bytes / orig_size * 100.0) if orig_size > 0 else 0.0
93
  records.append({
94
  "File": base,
 
 
95
  "Original Size (MB)": round(orig_size / (1024 * 1024), 3),
96
  "Compressed Size (MB)": round(comp_size / (1024 * 1024), 3),
97
  "Size Reduced (MB)": round(reduction_bytes / (1024 * 1024), 3),
@@ -99,10 +218,12 @@ def compress_videos(files, level, use_advanced, crf_value, preset, audio_bitrate
99
  "Output": os.path.basename(out_path)
100
  })
101
  compressed_paths.append(out_path)
102
- progress(idx / total_files, desc=f"[{idx}/{total_files}] Done {base}")
103
  except Exception as e:
104
  records.append({
105
  "File": base,
 
 
106
  "Original Size (MB)": None,
107
  "Compressed Size (MB)": None,
108
  "Size Reduced (MB)": None,
@@ -126,27 +247,72 @@ def compress_videos(files, level, use_advanced, crf_value, preset, audio_bitrate
126
  progress(1.0, desc="All done! Download your results below.")
127
  return zip_path, df, compressed_paths
128
 
129
- with gr.Blocks(title="BG CoRe.Vi MP4" , css=".summary-table {max-height: 260px; overflow-y: auto;}") as demo:
130
- gr.Markdown("# BG CoRe.Vi MP4 (Multi-file)")
131
- gr.Markdown("Upload MP4 videos, choose compression level, and download compressed files with a summary report.")
 
 
 
 
 
 
 
132
 
133
  with gr.Row():
134
  with gr.Column():
135
- files_in = gr.Files(label="Upload MP4 Videos", file_count="multiple", file_types=[".mp4"])
136
- level = gr.Radio(label="Compression Level", choices=["Low", "Medium", "High"], value="Medium")
137
- with gr.Accordion("Advanced Options", open=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  use_adv = gr.Checkbox(label="Use advanced settings", value=False)
139
- crf_val = gr.Slider(minimum=18, maximum=36, step=1, value=28, label="CRF")
140
- preset = gr.Dropdown(label="FFmpeg Preset", choices=["ultrafast","superfast","veryfast","faster","fast","medium","slow","slower","veryslow"], value="fast")
141
- audio_bitrate = gr.Slider(minimum=64, maximum=320, step=16, value=128, label="Audio Bitrate (kbps)")
 
 
 
 
 
 
 
 
142
  run_btn = gr.Button("🔄 Compress", variant="primary")
143
 
144
  with gr.Column():
145
- zip_out = gr.File(label="Download ZIP (videos + report)")
146
  summary = gr.Dataframe(label="Compression Summary", interactive=False, wrap=True, elem_classes="summary-table")
147
  files_out = gr.Files(label="Download Individual Compressed Files")
148
 
149
- run_btn.click(fn=compress_videos, inputs=[files_in, level, use_adv, crf_val, preset, audio_bitrate], outputs=[zip_out, summary, files_out])
 
 
 
 
150
 
151
  if __name__ == "__main__":
152
  demo.launch()
 
2
  import os
3
  import subprocess
4
  import uuid
 
5
  import zipfile
6
  from datetime import datetime
7
  import pandas as pd
 
10
  OUTPUT_DIR = "outputs"
11
  os.makedirs(OUTPUT_DIR, exist_ok=True)
12
 
13
+ # ---------------------------
14
+ # Helpers & Configuration
15
+ # ---------------------------
16
+
17
+ def map_level_to_crf(level: str, hevc: bool) -> int:
18
+ """
19
+ Map user-friendly compression levels to CRF values.
20
+ HEVC often achieves similar quality at slightly higher CRF than H.264,
21
+ but we keep scales aligned for simplicity.
22
+ """
23
  level = (level or "Medium").strip().lower()
24
+ if level == "low": # Best quality, larger files
25
+ return 22 if not hevc else 24
26
+ elif level == "high": # Smallest files, lower quality
27
+ return 32 if not hevc else 34
28
+ else: # Medium
29
+ return 28 if not hevc else 30
30
 
31
  def ffmpeg_available() -> bool:
32
  try:
 
35
  except Exception:
36
  return False
37
 
38
+ def resolution_to_height(res_choice: str) -> int | None:
39
+ """
40
+ Map UI selection to target height for downscaling.
41
+ Using '-vf scale=-2:HEIGHT' ensures even width while preserving aspect.
42
+ """
43
+ if not res_choice or "No downscaling" in res_choice:
44
+ return None
45
+ table = {
46
+ "720p (1280x720)": 720,
47
+ "1080p (1920x1080)": 1080,
48
+ "1440p (2560x1440)": 1440,
49
+ "4K (3840x2160)": 2160,
50
+ }
51
+ return table.get(res_choice)
52
+
53
+ # ---------------------------
54
+ # Core compression
55
+ # ---------------------------
56
+
57
+ def compress_single_video(
58
+ infile_path: str,
59
+ use_hevc: bool,
60
+ crf: int,
61
+ preset: str = "fast",
62
+ audio_bitrate_kbps: int = 128,
63
+ target_height: int | None = None,
64
+ progress: gr.Progress | None = None,
65
+ fallback_to_h264_on_error: bool = True
66
+ ):
67
+ """
68
+ Compress a single MP4 using FFmpeg.
69
+ - Video codec: H.264 (libx264) or H.265 (libx265)
70
+ - Optional downscale using '-vf scale=-2:HEIGHT'
71
+ Returns: (output_path, original_size_bytes, compressed_size_bytes, codec_used)
72
+ """
73
  base = os.path.basename(infile_path)
74
  out_name = f"{os.path.splitext(base)[0]}_compressed_{uuid.uuid4().hex[:8]}.mp4"
75
  out_path = os.path.join(OUTPUT_DIR, out_name)
76
 
77
  original_size = os.path.getsize(infile_path)
78
 
79
+ # Build common parts
80
+ vf_args = []
81
+ if target_height:
82
+ # Limit maximal height; width auto-calculated as even number to maintain aspect ratio
83
+ vf_args = ["-vf", f"scale=-2:{target_height}"]
84
+
85
+ vcodec = "libx265" if use_hevc else "libx264"
86
+ codec_used = "H.265 (libx265)" if use_hevc else "H.264 (libx264)"
87
+
88
+ # Base ffmpeg command
89
  cmd = [
90
  "ffmpeg", "-y",
91
  "-i", infile_path,
92
+ *(vf_args),
93
+ "-vcodec", vcodec,
94
  "-crf", str(crf),
95
  "-preset", preset,
96
+ # Improve playback compatibility
97
+ "-pix_fmt", "yuv420p",
98
+ # AAC audio with selected bitrate
99
  "-acodec", "aac", "-b:a", f"{audio_bitrate_kbps}k",
100
+ # Better streaming behavior
101
+ "-movflags", "+faststart",
102
  ]
103
 
104
+ # Tag HEVC properly for Apple players
105
+ if use_hevc:
106
+ cmd += ["-tag:v", "hvc1"]
107
+
108
+ cmd += [out_path]
109
+
110
  if progress:
111
+ progress(0.2, desc=f"Compressing {base} with {codec_used} (CRF {crf}, preset {preset})…")
112
 
113
  proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
114
+ if proc.returncode != 0 and use_hevc and fallback_to_h264_on_error:
115
+ # Fallback to H.264 if HEVC encoder isn't available or failed
116
+ vcodec = "libx264"
117
+ codec_used = "H.264 (libx264)"
118
+ fallback_cmd = [
119
+ "ffmpeg", "-y",
120
+ "-i", infile_path,
121
+ *(vf_args),
122
+ "-vcodec", vcodec,
123
+ "-crf", str(crf),
124
+ "-preset", preset,
125
+ "-pix_fmt", "yuv420p",
126
+ "-acodec", "aac", "-b:a", f"{audio_bitrate_kbps}k",
127
+ "-movflags", "+faststart",
128
+ out_path
129
+ ]
130
+ proc2 = subprocess.run(fallback_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
131
+ if proc2.returncode != 0:
132
+ raise RuntimeError(
133
+ f"FFmpeg failed on {base}.\n\n"
134
+ f"HEVC STDERR:\n{proc.stderr.decode(errors='ignore')}\n\n"
135
+ f"H.264 STDERR:\n{proc2.stderr.decode(errors='ignore')}"
136
+ )
137
+
138
+ elif proc.returncode != 0:
139
+ raise RuntimeError(
140
+ f"FFmpeg failed on {base}.\n\nSTDERR:\n{proc.stderr.decode(errors='ignore')}\n\n"
141
+ f"STDOUT:\n{proc.stdout.decode(errors='ignore')}"
142
+ )
143
 
144
  compressed_size = os.path.getsize(out_path)
145
  if progress:
146
  progress(0.95, desc=f"Finalizing {base}")
147
 
148
+ return out_path, original_size, compressed_size, codec_used
149
 
150
  def validate_inputs(files):
151
  if not files or len(files) == 0:
 
153
  paths = []
154
  for f in files:
155
  path = getattr(f, "name", None) or (isinstance(f, dict) and f.get("name")) or None
156
+ if not path:
157
+ raise gr.Error("Could not read uploaded file path.")
158
+ if not path.lower().endswith(".mp4"):
159
+ raise gr.Error(f"Unsupported file type: {os.path.basename(path)}. Please upload .mp4 only.")
160
  paths.append(path)
161
  return paths
162
 
163
+ def compress_videos(
164
+ files,
165
+ level: str,
166
+ use_advanced: bool,
167
+ crf_value: int,
168
+ preset: str,
169
+ audio_bitrate_kbps: int,
170
+ use_hevc: bool,
171
+ max_resolution_choice: str,
172
+ progress: gr.Progress = gr.Progress()
173
+ ):
174
+ """
175
+ Multi-file compression with:
176
+ - H.264 vs H.265 toggle
177
+ - Optional downscaling
178
+ - Progress & summary report
179
+ """
180
  if not ffmpeg_available():
181
  raise gr.Error("FFmpeg is not installed. Add it in packages.txt.")
182
 
183
  input_paths = validate_inputs(files)
184
  total_files = len(input_paths)
185
+ progress(0, desc=f"Starting compression for {total_files} file(s)")
186
 
187
+ target_height = resolution_to_height(max_resolution_choice)
188
+ crf = crf_value if use_advanced else map_level_to_crf(level, hevc=use_hevc)
189
  preset = preset or "fast"
190
  audio_bitrate_kbps = max(64, int(audio_bitrate_kbps or 128))
191
 
 
196
  base = os.path.basename(in_path)
197
  try:
198
  progress((idx - 1) / total_files, desc=f"[{idx}/{total_files}] Processing {base}")
199
+ out_path, orig_size, comp_size, codec_used = compress_single_video(
200
+ infile_path=in_path,
201
+ use_hevc=use_hevc,
202
+ crf=crf,
203
+ preset=preset,
204
+ audio_bitrate_kbps=audio_bitrate_kbps,
205
+ target_height=target_height,
206
+ progress=progress
207
+ )
208
  reduction_bytes = orig_size - comp_size
209
  reduction_pct = (reduction_bytes / orig_size * 100.0) if orig_size > 0 else 0.0
210
  records.append({
211
  "File": base,
212
+ "Codec": codec_used,
213
+ "Downscale": max_resolution_choice if target_height else "None",
214
  "Original Size (MB)": round(orig_size / (1024 * 1024), 3),
215
  "Compressed Size (MB)": round(comp_size / (1024 * 1024), 3),
216
  "Size Reduced (MB)": round(reduction_bytes / (1024 * 1024), 3),
 
218
  "Output": os.path.basename(out_path)
219
  })
220
  compressed_paths.append(out_path)
221
+ progress(idx / total_files, desc=f"[{idx}/{total_files}] Done {base} (↓ {round(reduction_pct,2)}%)")
222
  except Exception as e:
223
  records.append({
224
  "File": base,
225
+ "Codec": "—",
226
+ "Downscale": max_resolution_choice if target_height else "None",
227
  "Original Size (MB)": None,
228
  "Compressed Size (MB)": None,
229
  "Size Reduced (MB)": None,
 
247
  progress(1.0, desc="All done! Download your results below.")
248
  return zip_path, df, compressed_paths
249
 
250
+ # ---------------------------
251
+ # Gradio UI
252
+ # ---------------------------
253
+
254
+ with gr.Blocks(title = "BG CoRe.Vi MP4 (Multi-file)", css=""".summary-table {max-height: 280px; overflow-y: auto;}""") as demo:
255
+ gr.Markdown("# BG CoRe.Vi MP4 (H.264 / H.265, Downscaling, Multi-file)")
256
+ gr.Markdown(
257
+ "Upload one or more MP4s, choose compression level or advanced settings, optionally downscale, and download the compressed files.\n"
258
+ "Includes a progress bar and a CSV summary (sizes & reduction)."
259
+ )
260
 
261
  with gr.Row():
262
  with gr.Column():
263
+ files_in = gr.Files(
264
+ label="Upload MP4 Videos",
265
+ file_count="multiple",
266
+ file_types=[".mp4"]
267
+ )
268
+
269
+ level = gr.Radio(
270
+ label="Compression Level",
271
+ choices=["Low", "Medium", "High"],
272
+ value="Medium",
273
+ info="Low = best quality, High = smallest files"
274
+ )
275
+
276
+ # Codec & downscaling
277
+ use_hevc = gr.Checkbox(label="Use HEVC (H.265)", value=False,
278
+ info="Smaller files, slower to encode; add '-tag:v hvc1' for Apple compatibility.")
279
+ max_res = gr.Dropdown(
280
+ label="Optional Downscaling",
281
+ choices=[
282
+ "No downscaling",
283
+ "720p (1280x720)",
284
+ "1080p (1920x1080)",
285
+ "1440p (2560x1440)",
286
+ "4K (3840x2160)"
287
+ ],
288
+ value="No downscaling"
289
+ )
290
+
291
+ with gr.Accordion("Advanced Options (optional)", open=False):
292
  use_adv = gr.Checkbox(label="Use advanced settings", value=False)
293
+ crf_val = gr.Slider(minimum=18, maximum=36, step=1, value=28, label="CRF (Quality/Compression)")
294
+ preset = gr.Dropdown(
295
+ label="FFmpeg Preset",
296
+ choices=["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"],
297
+ value="fast",
298
+ allow_custom_value=False
299
+ )
300
+ audio_bitrate = gr.Slider(
301
+ minimum=64, maximum=320, step=16, value=128, label="Audio Bitrate (kbps)"
302
+ )
303
+
304
  run_btn = gr.Button("🔄 Compress", variant="primary")
305
 
306
  with gr.Column():
307
+ zip_out = gr.File(label="Download ZIP (compressed videos + CSV report)")
308
  summary = gr.Dataframe(label="Compression Summary", interactive=False, wrap=True, elem_classes="summary-table")
309
  files_out = gr.Files(label="Download Individual Compressed Files")
310
 
311
+ run_btn.click(
312
+ fn=compress_videos,
313
+ inputs=[files_in, level, use_adv, crf_val, preset, audio_bitrate, use_hevc, max_res],
314
+ outputs=[zip_out, summary, files_out]
315
+ )
316
 
317
  if __name__ == "__main__":
318
  demo.launch()