FFmpeg's value compounds when you stitch it into automation: agents that trim videos based on transcripts, AI tools that generate frame sequences and need them assembled, batch jobs that detect and fix bad audio levels.
The Pippa example
This codebase calls FFmpeg from Python in three places:
backend/services/tts_normalize.py — runs the EBU R128 + denoise chain on every Pippa voice clip.
backend/services/video_export.py — assembles avatar frames + TTS audio into the WebUI's exportable video.
Telegram bot voice notes — convert Opus → WAV before transcription.
Patterns
Use subprocess.run with the command as a list (not a shell string) — safer, handles spaces in filenames automatically. Pipe stderr for progress parsing. For long jobs, use -progress pipe:1 to get machine-readable progress events.
Code
Python wrapper — basic encode with progress·python
import subprocess, sys, json
def encode_with_progress(src: str, dst: str) -> int:
cmd = [
"ffmpeg", "-y", "-i", src,
"-c:v", "libx264", "-crf", "20", "-preset", "slow",
"-pix_fmt", "yuv420p",
"-c:a", "aac", "-b:a", "192k",
"-progress", "pipe:1",
"-nostats", "-loglevel", "error",
dst,
]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True)
for line in p.stdout:
if line.startswith("out_time_ms="):
ms = int(line.strip().split("=", 1)[1])
print(f"\rProgress: {ms / 1_000_000:.1f}s", end="", flush=True)
return p.wait()
if __name__ == "__main__":
sys.exit(encode_with_progress(sys.argv[1], sys.argv[2]))
Agent-driven trim — LLM picks the timestamps·python
import subprocess, json
from pathlib import Path
def transcribe(src: Path) -> list[dict]:
"""Returns list of segments: [{start, end, text}, ...]
Use whatever transcriber fits your pipeline (faster-whisper, Cloud STT, etc.)
"""
...
def llm_pick_highlights(segments: list[dict]) -> list[tuple[float, float]]:
"""Send segments to an LLM, ask it to return start/end pairs of
the most-quotable 30 seconds total. Return list of (start, end) tuples.
"""
...
def build_highlight_reel(src: Path, ranges: list[tuple[float, float]], dst: Path):
# Build a complex filtergraph that trims and concats the chosen ranges
parts = []
filters = []
for i, (s, e) in enumerate(ranges):
filters.append(f"[0:v]trim=start={s}:end={e},setpts=PTS-STARTPTS[v{i}]")
filters.append(f"[0:a]atrim=start={s}:end={e},asetpts=PTS-STARTPTS[a{i}]")
parts.extend(f"[v{i}][a{i}]" for i in range(len(ranges)))
filtergraph = "; ".join(filters) + "; " + "".join(parts) + f"concat=n={len(ranges)}:v=1:a=1[outv][outa]"
subprocess.run([
"ffmpeg", "-y", "-i", str(src),
"-filter_complex", filtergraph,
"-map", "[outv]", "-map", "[outa]",
"-c:v", "libx264", "-crf", "20", "-preset", "slow",
"-c:a", "aac", "-b:a", "192k",
str(dst),
], check=True)
# Wire it up
src = Path("raw_podcast.mp4")
segs = transcribe(src)
ranges = llm_pick_highlights(segs)
build_highlight_reel(src, ranges, Path("highlights.mp4"))
Write a Python script that takes a video and a list of (start, end) tuples and produces a concatenated highlight reel (use the recipe above). Add progress printing using -progress pipe:1. Bonus: feed the segments through an LLM (Claude API) that picks the 'best' 30 seconds of an audio transcript.
Progress
Progress is local-only — sign in to sync across devices.