Convert-Videos / app.py
Aliwan's picture
Update app.py
e30142e verified
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()