rbughao commited on
Commit
9cadd3b
·
verified ·
1 Parent(s): 8376382

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +30 -146
app.py CHANGED
@@ -4,7 +4,6 @@ import subprocess
4
  import uuid
5
  import shutil
6
  import zipfile
7
- import time
8
  from datetime import datetime
9
  import pandas as pd
10
 
@@ -13,16 +12,12 @@ OUTPUT_DIR = "outputs"
13
  os.makedirs(OUTPUT_DIR, exist_ok=True)
14
 
15
  def map_level_to_crf(level: str) -> int:
16
- """
17
- Map user-friendly compression levels to FFmpeg CRF values.
18
- Lower CRF = higher quality, higher file size.
19
- """
20
  level = (level or "Medium").strip().lower()
21
- if level == "low": # Low compression (better quality)
22
  return 22
23
- elif level == "high": # High compression (smaller size)
24
  return 32
25
- else: # Medium (default)
26
  return 28
27
 
28
  def ffmpeg_available() -> bool:
@@ -32,29 +27,13 @@ def ffmpeg_available() -> bool:
32
  except Exception:
33
  return False
34
 
35
- def compress_single_video(
36
- infile_path: str,
37
- crf: int,
38
- preset: str = "fast",
39
- audio_bitrate_kbps: int = 128,
40
- progress: gr.Progress | None = None
41
- ) -> tuple[str, int, int]:
42
- """
43
- Compress a single MP4 using ffmpeg (H.264, AAC).
44
- Returns: (output_path, original_size_bytes, compressed_size_bytes)
45
- """
46
- # Build output filename
47
  base = os.path.basename(infile_path)
48
- name, ext = os.path.splitext(base)
49
- out_name = f"{name}_compressed_{uuid.uuid4().hex[:8]}.mp4"
50
  out_path = os.path.join(OUTPUT_DIR, out_name)
51
 
52
  original_size = os.path.getsize(infile_path)
53
 
54
- # FFmpeg command
55
- # -crf controls quality (18-28 typical; 32 more aggressive)
56
- # -preset trades encode speed vs compression efficiency
57
- # Audio is encoded to AAC with specified bitrate
58
  cmd = [
59
  "ffmpeg", "-y",
60
  "-i", infile_path,
@@ -65,97 +44,50 @@ def compress_single_video(
65
  out_path
66
  ]
67
 
68
- # Progress: start
69
- if progress is not None:
70
- progress(0.2, desc=f"Compressing {base} (CRF {crf}, preset {preset})")
71
 
72
- # Run ffmpeg
73
  proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
74
  if proc.returncode != 0:
75
- # Surface ffmpeg error
76
- raise RuntimeError(
77
- f"FFmpeg failed on {base}.\n\nSTDERR:\n{proc.stderr.decode(errors='ignore')}\n\nSTDOUT:\n{proc.stdout.decode(errors='ignore')}"
78
- )
79
 
80
  compressed_size = os.path.getsize(out_path)
81
-
82
- if progress is not None:
83
  progress(0.95, desc=f"Finalizing {base}")
84
 
85
  return out_path, original_size, compressed_size
86
 
87
- def validate_inputs(files: list) -> list[str]:
88
- """
89
- Validate and normalize input files into local paths.
90
- """
91
  if not files or len(files) == 0:
92
  raise gr.Error("Please upload at least one MP4 file.")
93
-
94
- # Gradio returns objects with .name when using gr.Files
95
  paths = []
96
  for f in files:
97
- # f can be a tempfile-like object or a dict depending on Gradio version
98
  path = getattr(f, "name", None) or (isinstance(f, dict) and f.get("name")) or None
99
- if not path:
100
- raise gr.Error("Could not read uploaded file path.")
101
- if not path.lower().endswith(".mp4"):
102
- raise gr.Error(f"Unsupported file type: {os.path.basename(path)}. Please upload .mp4 only.")
103
  paths.append(path)
104
  return paths
105
 
106
- def compress_videos(
107
- files,
108
- level: str,
109
- use_advanced: bool,
110
- crf_value: int,
111
- preset: str,
112
- audio_bitrate_kbps: int,
113
- progress: gr.Progress = gr.Progress()
114
- ):
115
- """
116
- Main handler for multi-file compression with progress and reporting.
117
- Returns:
118
- - zip_path: ZIP containing all compressed videos + CSV report
119
- - summary_df: DataFrame with per-file stats
120
- - out_files: list of compressed file paths (for direct downloads)
121
- """
122
- # Quick check for ffmpeg
123
  if not ffmpeg_available():
124
- raise gr.Error(
125
- "FFmpeg is not available in this environment. "
126
- "Please ensure 'ffmpeg' is installed via packages.txt."
127
- )
128
 
129
  input_paths = validate_inputs(files)
130
-
131
  total_files = len(input_paths)
132
- progress(0, desc=f"Starting compression for {total_files} file(s)")
133
 
134
- # Resolve CRF
135
- crf = crf_value if use_advanced else map_level_to_crf(level or "Medium")
136
- # Resolve preset
137
  preset = preset or "fast"
138
  audio_bitrate_kbps = max(64, int(audio_bitrate_kbps or 128))
139
 
140
- # Prepare report
141
  records = []
142
  compressed_paths = []
143
 
144
- # Process files sequentially with coarse progress
145
  for idx, in_path in enumerate(input_paths, start=1):
146
  base = os.path.basename(in_path)
147
  try:
148
- progress(
149
- (idx - 1) / total_files,
150
- desc=f"[{idx}/{total_files}] Preparing {base}"
151
- )
152
- out_path, orig_size, comp_size = compress_single_video(
153
- infile_path=in_path,
154
- crf=crf,
155
- preset=preset,
156
- audio_bitrate_kbps=audio_bitrate_kbps,
157
- progress=progress
158
- )
159
  reduction_bytes = orig_size - comp_size
160
  reduction_pct = (reduction_bytes / orig_size * 100.0) if orig_size > 0 else 0.0
161
  records.append({
@@ -167,12 +99,8 @@ def compress_videos(
167
  "Output": os.path.basename(out_path)
168
  })
169
  compressed_paths.append(out_path)
170
- progress(
171
- idx / total_files,
172
- desc=f"[{idx}/{total_files}] Completed {base} (↓ {round(reduction_pct,2)}%)"
173
- )
174
  except Exception as e:
175
- # Log failure in report
176
  records.append({
177
  "File": base,
178
  "Original Size (MB)": None,
@@ -182,87 +110,43 @@ def compress_videos(
182
  "Output": f"ERROR: {str(e).splitlines()[0][:180]}"
183
  })
184
 
185
- # Build summary dataframe
186
  df = pd.DataFrame.from_records(records)
187
-
188
- # Save CSV report
189
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
190
  report_name = f"compression_report_{timestamp}.csv"
191
  report_path = os.path.join(OUTPUT_DIR, report_name)
192
  df.to_csv(report_path, index=False)
193
 
194
- # Create ZIP bundle of outputs + report
195
  zip_name = f"compressed_bundle_{timestamp}.zip"
196
  zip_path = os.path.join(OUTPUT_DIR, zip_name)
197
  with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
198
- # Add report
199
  zf.write(report_path, arcname=report_name)
200
- # Add each compressed video
201
  for p in compressed_paths:
202
  zf.write(p, arcname=os.path.basename(p))
203
 
204
- # Final progress
205
  progress(1.0, desc="All done! Download your results below.")
206
-
207
- # Return ZIP (for one-click download), summary table, and individual files (for selective downloads)
208
  return zip_path, df, compressed_paths
209
 
210
- with gr.Blocks() as demo:
211
  gr.Markdown("# 🎥 MP4 Video Compressor (Multi-file)")
212
- gr.Markdown(
213
- "Upload one or more MP4s, choose compression level, and get the compressed files.\n"
214
- "Includes a progress bar and a CSV summary (sizes & reduction)."
215
- )
216
 
217
  with gr.Row():
218
  with gr.Column():
219
- files_in = gr.Files(
220
- label="Upload MP4 Videos",
221
- file_count="multiple",
222
- file_types=[".mp4"],
223
- height=120
224
- )
225
-
226
- # Simple compression level
227
- level = gr.Radio(
228
- label="Compression Level",
229
- choices=["Low", "Medium", "High"],
230
- value="Medium",
231
- info="Low = best quality, High = smallest files"
232
- )
233
-
234
- with gr.Accordion("Advanced Options (optional)", open=False):
235
  use_adv = gr.Checkbox(label="Use advanced settings", value=False)
236
- crf_val = gr.Slider(
237
- minimum=18, maximum=36, step=1, value=28, label="CRF (Quality/Compression)"
238
- )
239
- preset = gr.Dropdown(
240
- label="FFmpeg Preset",
241
- choices=["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"],
242
- value="fast",
243
- allow_custom_value=False
244
- )
245
- audio_bitrate = gr.Slider(
246
- minimum=64, maximum=320, step=16, value=128, label="Audio Bitrate (kbps)"
247
- )
248
-
249
  run_btn = gr.Button("🔄 Compress", variant="primary")
250
 
251
  with gr.Column():
252
- zip_out = gr.File(label="Download ZIP (compressed videos + CSV report)")
253
- summary = gr.Dataframe(
254
- label="Compression Summary",
255
- interactive=False,
256
- wrap=True,
257
- height=260
258
- )
259
  files_out = gr.Files(label="Download Individual Compressed Files")
260
 
261
- run_btn.click(
262
- fn=compress_videos,
263
- inputs=[files_in, level, use_adv, crf_val, preset, audio_bitrate],
264
- outputs=[zip_out, summary, files_out]
265
- )
266
 
267
  if __name__ == "__main__":
268
  demo.launch()
 
4
  import uuid
5
  import shutil
6
  import zipfile
 
7
  from datetime import datetime
8
  import pandas as pd
9
 
 
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:
 
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,
 
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:
62
  raise gr.Error("Please upload at least one MP4 file.")
 
 
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
 
 
83
  records = []
84
  compressed_paths = []
85
 
 
86
  for idx, in_path in enumerate(input_paths, start=1):
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({
 
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,
 
110
  "Output": f"ERROR: {str(e).splitlines()[0][:180]}"
111
  })
112
 
 
113
  df = pd.DataFrame.from_records(records)
 
 
114
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
115
  report_name = f"compression_report_{timestamp}.csv"
116
  report_path = os.path.join(OUTPUT_DIR, report_name)
117
  df.to_csv(report_path, index=False)
118
 
 
119
  zip_name = f"compressed_bundle_{timestamp}.zip"
120
  zip_path = os.path.join(OUTPUT_DIR, zip_name)
121
  with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
 
122
  zf.write(report_path, arcname=report_name)
 
123
  for p in compressed_paths:
124
  zf.write(p, arcname=os.path.basename(p))
125
 
 
126
  progress(1.0, desc="All done! Download your results below.")
 
 
127
  return zip_path, df, compressed_paths
128
 
129
+ with gr.Blocks(css=".summary-table {max-height: 260px; overflow-y: auto;}") as demo:
130
  gr.Markdown("# 🎥 MP4 Video Compressor (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()