import os import shutil import subprocess import tempfile from pathlib import Path from typing import List, Tuple import gradio as gr # ========================================================= # Batch Cinematic Processor # - Upload many videos # - Interpolate to 60 fps using FFmpeg minterpolate # - Upscale to 1440p # - Return a ZIP with all processed videos # # Requirements: # pip install gradio # ffmpeg installed and available in PATH # # Notes: # - No reverse is applied here. # - This is designed for CPU-friendly, stable batch processing. # ========================================================= def run_ffmpeg(cmd: List[str]) -> None: proc = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) if proc.returncode != 0: raise RuntimeError(proc.stderr[-5000:]) def safe_stem(path: str) -> str: return Path(path).stem.replace(" ", "_") def process_one_video( input_path: str, output_dir: Path, target_width: int, target_height: int, target_fps: int, crf: int, preset: str, ) -> Path: input_path = str(Path(input_path).resolve()) name = safe_stem(input_path) output_path = output_dir / f"{name}_{target_fps}fps_{target_height}p.mp4" # Minterpolate first, then upscale. # This tends to preserve motion continuity better than upscaling first. vf = ( f"minterpolate=fps={target_fps}:mi_mode=mci:mc_mode=aobmc:" f"me_mode=bidir:me=epzs:vsbmc=1:scd=fdiff," f"scale={target_width}:{target_height}:flags=lanczos" ) cmd = [ "ffmpeg", "-y", "-i", input_path, "-vf", vf, "-c:v", "libx264", "-preset", preset, "-crf", str(crf), "-pix_fmt", "yuv420p", "-movflags", "+faststart", str(output_path), ] run_ffmpeg(cmd) return output_path def batch_process( videos: List[str], target_width: int, target_height: int, target_fps: int, crf: int, preset: str, progress=gr.Progress(track_tqdm=False), ) -> Tuple[str, str]: if not videos: raise gr.Error("Envie pelo menos um vídeo.") # Normalize values. target_width = int(target_width) target_height = int(target_height) target_fps = int(target_fps) crf = int(crf) if target_width % 2 != 0: target_width += 1 if target_height % 2 != 0: target_height += 1 workdir = Path(tempfile.mkdtemp(prefix="batch_cinematic_")) outdir = workdir / "outputs" outdir.mkdir(parents=True, exist_ok=True) processed_files: List[Path] = [] total = len(videos) for idx, video in enumerate(videos, start=1): progress((idx - 1) / total, desc=f"Processando {idx}/{total}: {Path(video).name}") processed = process_one_video( video, outdir, target_width=target_width, target_height=target_height, target_fps=target_fps, crf=crf, preset=preset, ) processed_files.append(processed) progress(0.95, desc="Montando ZIP...") zip_base = workdir / "batch_output" zip_path = shutil.make_archive(str(zip_base), "zip", root_dir=outdir) progress(1.0, desc="Concluído") summary = ( f"{len(processed_files)} vídeo(s) processado(s).\n" f"Saída: {target_fps} fps | {target_width}x{target_height}\n" f"Arquivo ZIP pronto." ) return zip_path, summary with gr.Blocks(title="Batch Cinematic Processor") as demo: gr.Markdown( "# Batch Cinematic Processor\n" "Envie vários vídeos e receba um ZIP com tudo processado em 60 fps e 1440p.\n" "Sem reverse automático." ) with gr.Row(): videos = gr.File( label="Vídeos de entrada", file_count="multiple", type="filepath", file_types=[".mp4", ".mov", ".mkv", ".webm", ".avi"], ) with gr.Row(): target_width = gr.Number(label="Largura", value=2560, precision=0) target_height = gr.Number(label="Altura", value=1440, precision=0) target_fps = gr.Number(label="FPS de saída", value=60, precision=0) with gr.Row(): crf = gr.Slider(label="Qualidade (CRF menor = melhor)", minimum=14, maximum=24, value=18, step=1) preset = gr.Dropdown( label="Preset do encoder", choices=["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower"], value="slow", ) process_btn = gr.Button("Processar lote") with gr.Row(): zip_out = gr.File(label="ZIP final") info = gr.Textbox(label="Status", lines=4) process_btn.click( fn=batch_process, inputs=[videos, target_width, target_height, target_fps, crf, preset], outputs=[zip_out, info], ) if __name__ == "__main__": demo.launch()