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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +253 -26
app.py CHANGED
@@ -2,40 +2,267 @@ import gradio as gr
2
  import os
3
  import subprocess
4
  import uuid
 
 
 
 
 
5
 
6
- # Function to compress video using ffmpeg
7
- def compress_video(video):
8
- input_path = f"input_{uuid.uuid4()}.mp4"
9
- output_path = f"compressed_{uuid.uuid4()}.mp4"
10
-
11
- # Save uploaded video
12
- with open(input_path, "wb") as f:
13
- f.write(video.read())
14
-
15
- # FFmpeg command for compression
16
- command = [
17
- "ffmpeg", "-i", input_path,
18
- "-vcodec", "libx264", "-crf", "28", # CRF controls quality (lower = better quality)
19
- "-preset", "fast",
20
- output_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  ]
22
- subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
23
 
24
- os.remove(input_path) # Clean up original file
25
- return output_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- # Gradio UI
28
  with gr.Blocks() as demo:
29
- gr.Markdown("# 🎥 MP4 Video Compressor")
30
- gr.Markdown("Upload an MP4 video, compress it, and download the result.")
 
 
 
31
 
32
  with gr.Row():
33
  with gr.Column():
34
- video_input = gr.File(label="Upload MP4 Video", file_types=[".mp4"])
35
- compress_button = gr.Button("Compress Video")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  with gr.Column():
37
- output_video = gr.File(label="Download Compressed Video")
 
 
 
 
 
 
 
38
 
39
- compress_button.click(fn=compress_video, inputs=video_input, outputs=output_video)
 
 
 
 
40
 
41
- demo.launch()
 
 
2
  import os
3
  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
 
11
+ # Ensure output directory exists
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:
29
+ try:
30
+ subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
31
+ return True
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,
61
+ "-vcodec", "libx264",
62
+ "-crf", str(crf),
63
+ "-preset", preset,
64
+ "-acodec", "aac", "-b:a", f"{audio_bitrate_kbps}k",
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({
162
+ "File": base,
163
+ "Original Size (MB)": round(orig_size / (1024 * 1024), 3),
164
+ "Compressed Size (MB)": round(comp_size / (1024 * 1024), 3),
165
+ "Size Reduced (MB)": round(reduction_bytes / (1024 * 1024), 3),
166
+ "Reduction (%)": round(reduction_pct, 2),
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,
179
+ "Compressed Size (MB)": None,
180
+ "Size Reduced (MB)": None,
181
+ "Reduction (%)": None,
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()