import os import subprocess import pathlib import hashlib import requests import gradio as gr # ---------------------------------------------------------------------- # 1️⃣ Download a static FFmpeg build (Linux x86_64, CPU only) # ---------------------------------------------------------------------- FFMPEG_URL = ( "https://github.com/BtbN/FFmpeg-Builds/releases/download" "/latest/ffmpeg-master-latest-linux64-gpl.tar.xz" ) FFMPEG_DIR = pathlib.Path("ffmpeg") BINARY_PATH = FFMPEG_DIR / "ffmpeg" def _download_and_extract(): """Download and extract FFmpeg if not already present.""" if BINARY_PATH.is_file(): return # Create directory FFMPEG_DIR.mkdir(parents=True, exist_ok=True) tar_path = FFMPEG_DIR / "ffmpeg.tar.xz" # Stream download (avoids loading whole file into memory) with requests.get(FFMPEG_URL, stream=True, timeout=30) as r: r.raise_for_status() with open(tar_path, "wb") as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) # Extract only the binary (no need for docs, etc.) import tarfile with tarfile.open(tar_path, "r:xz") as tar: # Find the member that ends with '/ffmpeg' for member in tar.getmembers(): if member.name.endswith("/ffmpeg"): member.path = os.path.basename(member.name) # flatten tar.extract(member, path=FFMPEG_DIR) break # Clean up archive tar_path.unlink() # Make binary executable BINARY_PATH.chmod(0o755) _download_and_extract() # ---------------------------------------------------------------------- # 2️⃣ Helper: run FFmpeg safely # ---------------------------------------------------------------------- def run_ffmpeg(input_bytes: bytes, args: str) -> bytes: """ Execute a very limited FFmpeg command. Parameters ---------- input_bytes : bytes Raw input file (e.g., video or audio) uploaded by the user. args : str Command‑line arguments *after* the input and before the output. Example: "-c:a aac -b:a 128k" (convert audio to AAC). Returns ------- bytes Output file produced by FFmpeg. """ # Validate arguments – allow only a whitelist of safe flags allowed_flags = { "-c:a", "-c:v", "-b:a", "-b:v", "-ar", "-ac", "-vn", "-an", "-map", "-f", "-y", "-loglevel", "quiet" } tokenised = args.split() if any(tok not in allowed_flags and not tok.startswith("-") for tok in tokenised): raise ValueError("Unsupported FFmpeg flag detected.") # Write input to a temporary file input_path = pathlib.Path("input.tmp") output_path = pathlib.Path("output.tmp") input_path.write_bytes(input_bytes) # Build the command cmd = [ str(BINARY_PATH), "-loglevel", "quiet", # suppress noisy output "-i", str(input_path), # input file *tokenised, # user‑provided args str(output_path) # output file ] # Run FFmpeg; enforce a short timeout (e.g., 30 s) to keep the Space alive try: subprocess.run( cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30, ) finally: # Clean up input regardless of success/failure if input_path.exists(): input_path.unlink() if not output_path.is_file(): raise RuntimeError("FFmpeg did not produce an output file.") # Return output bytes and clean up out_bytes = output_path.read_bytes() output_path.unlink() return out_bytes # ---------------------------------------------------------------------- # 3️⃣ Gradio UI # ---------------------------------------------------------------------- def ffmpeg_interface(file: gr.File, args: str) -> gr.File: """ Gradio wrapper that takes an uploaded file and FFmpeg args, returns the processed file. """ output_bytes = run_ffmpeg(file.read(), args) # Heuristic MIME type based on args; fallback to generic binary mime = "application/octet-stream" if "-c:a aac" in args or ".aac" in args: mime = "audio/aac" elif "-c:v libx264" in args or ".mp4" in args: mime = "video/mp4" return gr.File.update(value=output_bytes, mime_type=mime) iface = gr.Interface( fn=ffmpeg_interface, inputs=[ gr.File(label="Upload video/audio file"), gr.Textbox( label="FFmpeg arguments (after input, before output)", placeholder="-c:a aac -b:a 128k", ), ], outputs=gr.File(label="Processed file"), title=" FFmpeg on Hugging Face Spaces (CPU‑only)", description=( "Upload a media file and supply a limited set of FFmpeg flags. " "The app runs on a free‑tier CPU space, so keep jobs short." ), allow_flagging="never", analytics_enabled=False, ) if __name__ == "__main__": iface.launch()