CoRe.Vi / app.py
rbughao's picture
Update app.py
15fc0ca verified
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()