File size: 11,693 Bytes
e720154
 
 
 
8376382
 
 
e720154
8376382
 
 
 
7b99ffe
 
 
 
 
 
 
 
 
 
8376382
7b99ffe
 
 
 
 
 
8376382
 
 
 
 
 
 
 
7b99ffe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8376382
9cadd3b
8376382
 
 
 
7b99ffe
 
 
 
 
 
 
 
 
 
8376382
 
 
7b99ffe
 
8376382
 
7b99ffe
 
 
8376382
7b99ffe
 
e720154
 
7b99ffe
 
 
 
 
 
9cadd3b
7b99ffe
8376382
 
7b99ffe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8376382
 
9cadd3b
8376382
 
7b99ffe
8376382
9cadd3b
8376382
 
 
 
 
7b99ffe
 
 
 
8376382
 
 
7b99ffe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8376382
9cadd3b
8376382
 
 
7b99ffe
8376382
7b99ffe
 
8376382
 
 
 
 
 
 
 
 
9cadd3b
7b99ffe
 
 
 
 
 
 
 
 
8376382
 
 
 
7b99ffe
 
8376382
 
 
 
 
 
 
7b99ffe
8376382
 
 
7b99ffe
 
8376382
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e720154
7b99ffe
 
 
 
 
 
 
 
 
 
e720154
 
 
7b99ffe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15fc0ca
7b99ffe
 
 
 
 
 
 
 
 
 
 
 
 
8376382
7b99ffe
 
 
 
 
 
 
 
 
 
 
8376382
 
e720154
7b99ffe
9cadd3b
8376382
e720154
7b99ffe
 
 
 
 
e720154
8376382
 
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
import gradio as gr
import os
import subprocess
import uuid
import zipfile
from datetime import datetime
import pandas as pd

# Ensure output directory exists
OUTPUT_DIR = "outputs"
os.makedirs(OUTPUT_DIR, exist_ok=True)

# ---------------------------
# Helpers & Configuration
# ---------------------------

def map_level_to_crf(level: str, hevc: bool) -> int:
    """
    Map user-friendly compression levels to CRF values.
    HEVC often achieves similar quality at slightly higher CRF than H.264,
    but we keep scales aligned for simplicity.
    """
    level = (level or "Medium").strip().lower()
    if level == "low":        # Best quality, larger files
        return 22 if not hevc else 24
    elif level == "high":     # Smallest files, lower quality
        return 32 if not hevc else 34
    else:                     # Medium
        return 28 if not hevc else 30

def ffmpeg_available() -> bool:
    try:
        subprocess.run(["ffmpeg", "-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
        return True
    except Exception:
        return False

def resolution_to_height(res_choice: str) -> int | None:
    """
    Map UI selection to target height for downscaling.
    Using '-vf scale=-2:HEIGHT' ensures even width while preserving aspect.
    """
    if not res_choice or "No downscaling" in res_choice:
        return None
    table = {
        "720p (1280x720)": 720,
        "1080p (1920x1080)": 1080,
        "1440p (2560x1440)": 1440,
        "4K (3840x2160)": 2160,
    }
    return table.get(res_choice)

# ---------------------------
# Core compression
# ---------------------------

def compress_single_video(
    infile_path: str,
    use_hevc: bool,
    crf: int,
    preset: str = "fast",
    audio_bitrate_kbps: int = 128,
    target_height: int | None = None,
    progress: gr.Progress | None = None,
    fallback_to_h264_on_error: bool = True
):
    """
    Compress a single MP4 using FFmpeg.
    - Video codec: H.264 (libx264) or H.265 (libx265)
    - Optional downscale using '-vf scale=-2:HEIGHT'
    Returns: (output_path, original_size_bytes, compressed_size_bytes, codec_used)
    """
    base = os.path.basename(infile_path)
    out_name = f"{os.path.splitext(base)[0]}_compressed_{uuid.uuid4().hex[:8]}.mp4"
    out_path = os.path.join(OUTPUT_DIR, out_name)

    original_size = os.path.getsize(infile_path)

    # Build common parts
    vf_args = []
    if target_height:
        # Limit maximal height; width auto-calculated as even number to maintain aspect ratio
        vf_args = ["-vf", f"scale=-2:{target_height}"]

    vcodec = "libx265" if use_hevc else "libx264"
    codec_used = "H.265 (libx265)" if use_hevc else "H.264 (libx264)"

    # Base ffmpeg command
    cmd = [
        "ffmpeg", "-y",
        "-i", infile_path,
        *(vf_args),
        "-vcodec", vcodec,
        "-crf", str(crf),
        "-preset", preset,
        # Improve playback compatibility
        "-pix_fmt", "yuv420p",
        # AAC audio with selected bitrate
        "-acodec", "aac", "-b:a", f"{audio_bitrate_kbps}k",
        # Better streaming behavior
        "-movflags", "+faststart",
    ]

    # Tag HEVC properly for Apple players
    if use_hevc:
        cmd += ["-tag:v", "hvc1"]

    cmd += [out_path]

    if progress:
        progress(0.2, desc=f"Compressing {base} with {codec_used} (CRF {crf}, preset {preset})…")

    proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    if proc.returncode != 0 and use_hevc and fallback_to_h264_on_error:
        # Fallback to H.264 if HEVC encoder isn't available or failed
        vcodec = "libx264"
        codec_used = "H.264 (libx264)"
        fallback_cmd = [
            "ffmpeg", "-y",
            "-i", infile_path,
            *(vf_args),
            "-vcodec", vcodec,
            "-crf", str(crf),
            "-preset", preset,
            "-pix_fmt", "yuv420p",
            "-acodec", "aac", "-b:a", f"{audio_bitrate_kbps}k",
            "-movflags", "+faststart",
            out_path
        ]
        proc2 = subprocess.run(fallback_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        if proc2.returncode != 0:
            raise RuntimeError(
                f"FFmpeg failed on {base}.\n\n"
                f"HEVC STDERR:\n{proc.stderr.decode(errors='ignore')}\n\n"
                f"H.264 STDERR:\n{proc2.stderr.decode(errors='ignore')}"
            )

    elif proc.returncode != 0:
        raise RuntimeError(
            f"FFmpeg failed on {base}.\n\nSTDERR:\n{proc.stderr.decode(errors='ignore')}\n\n"
            f"STDOUT:\n{proc.stdout.decode(errors='ignore')}"
        )

    compressed_size = os.path.getsize(out_path)
    if progress:
        progress(0.95, desc=f"Finalizing {base}")

    return out_path, original_size, compressed_size, codec_used

def validate_inputs(files):
    if not files or len(files) == 0:
        raise gr.Error("Please upload at least one MP4 file.")
    paths = []
    for f in files:
        path = getattr(f, "name", None) or (isinstance(f, dict) and f.get("name")) or None
        if not path:
            raise gr.Error("Could not read uploaded file path.")
        if not path.lower().endswith(".mp4"):
            raise gr.Error(f"Unsupported file type: {os.path.basename(path)}. Please upload .mp4 only.")
        paths.append(path)
    return paths

def compress_videos(
    files,
    level: str,
    use_advanced: bool,
    crf_value: int,
    preset: str,
    audio_bitrate_kbps: int,
    use_hevc: bool,
    max_resolution_choice: str,
    progress: gr.Progress = gr.Progress()
):
    """
    Multi-file compression with:
    - H.264 vs H.265 toggle
    - Optional downscaling
    - Progress & summary report
    """
    if not ffmpeg_available():
        raise gr.Error("FFmpeg is not installed. Add it in packages.txt.")

    input_paths = validate_inputs(files)
    total_files = len(input_paths)
    progress(0, desc=f"Starting compression for {total_files} file(s)…")

    target_height = resolution_to_height(max_resolution_choice)
    crf = crf_value if use_advanced else map_level_to_crf(level, hevc=use_hevc)
    preset = preset or "fast"
    audio_bitrate_kbps = max(64, int(audio_bitrate_kbps or 128))

    records = []
    compressed_paths = []

    for idx, in_path in enumerate(input_paths, start=1):
        base = os.path.basename(in_path)
        try:
            progress((idx - 1) / total_files, desc=f"[{idx}/{total_files}] Processing {base}")
            out_path, orig_size, comp_size, codec_used = compress_single_video(
                infile_path=in_path,
                use_hevc=use_hevc,
                crf=crf,
                preset=preset,
                audio_bitrate_kbps=audio_bitrate_kbps,
                target_height=target_height,
                progress=progress
            )
            reduction_bytes = orig_size - comp_size
            reduction_pct = (reduction_bytes / orig_size * 100.0) if orig_size > 0 else 0.0
            records.append({
                "File": base,
                "Codec": codec_used,
                "Downscale": max_resolution_choice if target_height else "None",
                "Original Size (MB)": round(orig_size / (1024 * 1024), 3),
                "Compressed Size (MB)": round(comp_size / (1024 * 1024), 3),
                "Size Reduced (MB)": round(reduction_bytes / (1024 * 1024), 3),
                "Reduction (%)": round(reduction_pct, 2),
                "Output": os.path.basename(out_path)
            })
            compressed_paths.append(out_path)
            progress(idx / total_files, desc=f"[{idx}/{total_files}] Done {base} (↓ {round(reduction_pct,2)}%)")
        except Exception as e:
            records.append({
                "File": base,
                "Codec": "—",
                "Downscale": max_resolution_choice if target_height else "None",
                "Original Size (MB)": None,
                "Compressed Size (MB)": None,
                "Size Reduced (MB)": None,
                "Reduction (%)": None,
                "Output": f"ERROR: {str(e).splitlines()[0][:180]}"
            })

    df = pd.DataFrame.from_records(records)
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    report_name = f"compression_report_{timestamp}.csv"
    report_path = os.path.join(OUTPUT_DIR, report_name)
    df.to_csv(report_path, index=False)

    zip_name = f"compressed_bundle_{timestamp}.zip"
    zip_path = os.path.join(OUTPUT_DIR, zip_name)
    with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
        zf.write(report_path, arcname=report_name)
        for p in compressed_paths:
            zf.write(p, arcname=os.path.basename(p))

    progress(1.0, desc="All done! Download your results below.")
    return zip_path, df, compressed_paths

# ---------------------------
# Gradio UI
# ---------------------------

with gr.Blocks(title = "BG CoRe.Vi MP4 (Multi-file)", css=""".summary-table {max-height: 280px; overflow-y: auto;}""") as demo:
    gr.Markdown("# BG CoRe.Vi MP4 (H.264 / H.265, Downscaling, Multi-file)")
    gr.Markdown(
        "Upload one or more MP4s, choose compression level or advanced settings, optionally downscale, and download the compressed files.\n"
        "Includes a progress bar and a CSV summary (sizes & reduction)."
    )

    with gr.Row():
        with gr.Column():
            files_in = gr.Files(
                label="Upload MP4 Videos",
                file_count="multiple",
                file_types=[".mp4"]
            )

            level = gr.Radio(
                label="Compression Level",
                choices=["Low", "Medium", "High"],
                value="Medium",
                info="Low = best quality, High = smallest files"
            )

            # Codec & downscaling
            use_hevc = gr.Checkbox(label="Use HEVC (H.265)", value=False,
                                   info="Smaller files, slower to encode")
            max_res = gr.Dropdown(
                label="Optional Downscaling",
                choices=[
                    "No downscaling",
                    "720p (1280x720)",
                    "1080p (1920x1080)",
                    "1440p (2560x1440)",
                    "4K (3840x2160)"
                ],
                value="No downscaling"
            )

            with gr.Accordion("Advanced Options (optional)", open=False):
                use_adv = gr.Checkbox(label="Use advanced settings", value=False)
                crf_val = gr.Slider(minimum=18, maximum=36, step=1, value=28, label="CRF (Quality/Compression)")
                preset = gr.Dropdown(
                    label="FFmpeg Preset",
                    choices=["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"],
                    value="fast",
                    allow_custom_value=False
                )
                audio_bitrate = gr.Slider(
                    minimum=64, maximum=320, step=16, value=128, label="Audio Bitrate (kbps)"
                )

            run_btn = gr.Button("🔄 Compress", variant="primary")

        with gr.Column():
            zip_out = gr.File(label="Download ZIP (compressed videos + CSV report)")
            summary = gr.Dataframe(label="Compression Summary", interactive=False, wrap=True, elem_classes="summary-table")
            files_out = gr.Files(label="Download Individual Compressed Files")

    run_btn.click(
        fn=compress_videos,
        inputs=[files_in, level, use_adv, crf_val, preset, audio_bitrate, use_hevc, max_res],
        outputs=[zip_out, summary, files_out]
    )

if __name__ == "__main__":
    demo.launch()