██████╗██╗ ██╗██████╗ ███████╗██╗ ██████╗ ██╗ ██╗
██╔════╝██║ ██║██╔══██╗██╔════╝██║ ██╔═══██╗██║ ██║
██║ ██║ ██║██████╔╝█████╗ ██║ ██║ ██║██║ █╗ ██║
██║ ██║ ██║██╔═══╝ ██╔══╝ ██║ ██║ ██║██║███╗██║
╚██████╗███████╗██║██║ ██║ ███████╗╚██████╔╝╚███╔███╔╝
╚═════╝╚══════╝╚═╝╚═╝ ╚═╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝
Trim · Compress · Highlight
Typed Python API and CLI for video clipping — auto-managed ffmpeg, zero setup. Zero runtime dependencies. Docker-ready for instant development.
Most Python video libraries convert frames to NumPy arrays — slow, memory-heavy, and unnecessary for trimming. clipflow builds and runs ffmpeg commands directly as subprocess calls:
- Lossless stream-copy — trimming a 10 GB file takes seconds with zero quality loss
- Frame-accurate re-encode when you need compression
- Highlight routing — a first-class concept missing from every other package
- Clean typed API — all inputs and outputs are typed dataclasses, no dicts
- Zero runtime dependencies — stdlib + system ffmpeg, nothing else
pip install clipflowRequires: Python ≥ 3.9
docker pull ronaldgosso/clipflow:latest
docker run --rm -v /path/to/videos:/data ronaldgosso/clipflow:latest trim video.mp4 00:00-01:00See README_DOCKER.md or Docker Hub for complete Docker documentation.
FFmpeg: Automatically downloaded and managed on first use — no manual installation required! 🎉
On first use, clipflow automatically:
- Detects your platform (Windows, macOS, or Linux)
- Downloads the appropriate FFmpeg binaries from trusted sources
- Caches them locally for future use
- Falls back to system PATH if you already have FFmpeg installed
You can also pre-download FFmpeg explicitly:
import clipflow
# Optional: Pre-download FFmpeg (happens automatically on first use anyway)
clipflow.setup_ffmpeg()
# Check where FFmpeg is located
print(clipflow.get_ffmpeg_path())
print(clipflow.get_ffprobe_path())Cache location:
- Windows:
%LOCALAPPDATA%\clipflow\ffmpeg\ - macOS:
~/Library/Caches/clipflow/ffmpeg/ - Linux:
~/.cache/clipflow/ffmpeg/
If you prefer to use your system FFmpeg, ensure it's on your PATH — clipflow will use it automatically.
If you want to install FFmpeg manually or use a specific version:
# Windows — via Chocolatey
choco install ffmpeg
# Windows — via winget
winget install Gyan.FFmpeg
# macOS — via Homebrew
brew install ffmpeg
# Ubuntu/Debian
sudo apt install ffmpeg
# Fedora
sudo dnf install ffmpegimport clipflow
results = clipflow.trim(
"documentary.mp4",
clipflow.ClipSpec(clipflow.parse_range("01:00", "02:30")),
)
print(results[0])
# ClipResult(✓ 'clip_01' → output/clip_01.mp4 [0.43s])clipflow trim documentary.mp4 01:00-02:30import clipflow
from clipflow import ClipSpec, parse_range, COMPRESS_HIGH, AR_9_16
# Lossless stream-copy
results = clipflow.trim(
"input.mp4",
ClipSpec(parse_range("00:30", "02:15")),
output_dir="clips",
)
# Multiple ranges
clips = [
ClipSpec(parse_range("00:00", "01:00"), label="intro"),
ClipSpec(parse_range("10:30", "12:00"), label="climax"),
ClipSpec(parse_range("58:00", "60:00"), label="outro"),
]
results = clipflow.trim("lecture.mp4", clips, output_dir="out")
# Compress + aspect ratio + highlight
results = clipflow.trim(
"raw_footage.mp4",
ClipSpec(
parse_range("05:00", "06:30"),
highlight=True, # copied to out/highlights/ automatically
compress=COMPRESS_HIGH, # CRF 18, slow preset
aspect_ratio=AR_9_16, # crop/pad to 9:16 for Reels/Shorts
label="hero_moment",
),
output_dir="out",
)
print(results[0].highlight_path) # out/highlights/hero_moment.mp4
# Progress callback
def on_progress(idx, total, result):
print(f"[{idx}/{total}] {'✓' if result.ok else '✗'} {result.spec.effective_label()}")
clipflow.trim("video.mp4", clips, output_dir="out", on_progress=on_progress)ClipResult fields:
| Field | Type | Description |
|---|---|---|
ok |
bool |
True if clip was produced without error |
output_path |
Path | None |
Absolute path to the output file |
highlight_path |
Path | None |
Path to highlight copy, or None |
duration_s |
float |
Wall-clock seconds the ffmpeg call took |
error |
str | None |
Error message if ok is False |
info = clipflow.inspect("documentary.mp4")
print(info.resolution) # '1920×1080'
print(info.duration_fmt) # '01:23:45'
print(info.fps) # 29.97
print(info.video_codec) # 'h264'
print(info.audio_codec) # 'aac'
print(info.size_mb) # 842.3from pathlib import Path
from clipflow import BatchSpec, ClipSpec, parse_range
import clipflow
specs = [
BatchSpec(
input_path=Path("ep01.mp4"),
output_dir=Path("ep01_clips"),
clips=[
ClipSpec(parse_range("00:30", "01:30"), label="cold_open"),
ClipSpec(parse_range("20:00", "21:00"), label="twist", highlight=True),
],
),
]
all_results = clipflow.batch(specs)# Lossless trim
clipflow trim lecture.mp4 01:00-02:30
# Multiple ranges
clipflow trim lecture.mp4 00:00-01:00 10:30-12:00 --output clips/
# Compress + crop + highlight
clipflow trim concert.mp4 05:00-06:30 --compress high --aspect 9:16 --highlight
# Custom CRF + H.265
clipflow trim raw.mp4 00:00-30:00 --crf 20 --codec libx265
# Inspect metadata
clipflow inspect documentary.mp4
clipflow inspect documentary.mp4 --json | jq .fps
# Batch from JSON spec
clipflow batch spec.jsonspec.json format:
[
{
"input": "lecture.mp4",
"output_dir": "clips",
"clips": [
{ "start": "00:30", "end": "02:15", "label": "intro", "compress": "medium" },
{ "start": "10:00", "end": "11:30", "highlight": true, "aspect": "16:9" }
]
}
]All time inputs accept:
| Format | Example | Seconds |
|---|---|---|
"MM:SS" |
"01:30" |
90.0 |
"HH:MM:SS" |
"01:02:03" |
3723.0 |
"SS" |
"90" |
90.0 |
int / float |
90 / 90.5 |
90.0 / 90.5 |
| Constant | CRF | Speed | Use case |
|---|---|---|---|
COMPRESS_LOW |
28 | fast | Smallest file |
COMPRESS_MEDIUM |
23 | medium | Balanced (ffmpeg default) |
COMPRESS_HIGH |
18 | slow | Best quality |
Custom:
from clipflow import CompressOptions
custom = CompressOptions(crf=20, preset="slower", codec="libx265", audio_bitrate="192k")| Constant | Ratio | Use case |
|---|---|---|
AR_16_9 |
16:9 | YouTube, landscape |
AR_9_16 |
9:16 | Reels, Shorts, TikTok |
AR_1_1 |
1:1 | Instagram square |
AR_4_3 |
4:3 | Classic TV |
from clipflow import AspectRatio
ultra_wide = AspectRatio(21, 9)clipflow builds ffmpeg commands as list[str] and runs them with subprocess.run.
FFmpeg Management: On first use, clipflow automatically downloads and caches FFmpeg binaries for your platform. The binaries are stored in a local cache directory and reused on subsequent runs. If you have FFmpeg on your PATH, clipflow will use your system installation instead.
Stream-copy (default — lossless, fast):
ffmpeg -y -ss <start> -t <duration> -i input.mp4 -c copy output.mp4
-ss before -i = keyframe seek. Bytes are copied directly, no re-encoding.
Re-encode (with compress=):
ffmpeg -y -i input.mp4 -ss <start> -t <duration> -c:v libx264 -crf 23 -preset medium -c:a copy output.mp4
-ss after -i = frame-accurate cut. Required when the encoder needs full frame data.
Highlights: shutil.copy2 of the finished clip to output/highlights/. No second ffmpeg call.
The entire subprocess layer is isolated in clipflow/_ffmpeg.py — no other module touches subprocess.
git clone https://github.com/ronaldgosso/clipflow.git
cd clipflow
pip install -e ".[dev]"
pytest # 82 tests
pytest --cov=clipflow --cov-report=term # coverage (>90%)
ruff check clipflow/ tests/ && black clipflow/ # lint + format
mypy clipflow/ --ignore-missing-imports # strict typesUse Docker for instant development with zero Python installation:
# Pull pre-built image
docker pull ronaldgosso/clipflow:latest
# Run CLI
docker run --rm -v /path/to/videos:/data ronaldgosso/clipflow:latest trim video.mp4 00:00-01:00For complete Docker documentation including Docker Compose, CI/CD integration, and production deployment, see README_DOCKER.md or Docker Hub.
Note: FFmpeg binaries are automatically managed. Tests mock the FFmpeg manager to avoid actual downloads.
Release:
# Bump version in pyproject.toml + clipflow/__init__.py, then:
git tag v0.3.1 && git push origin v0.3.1
# publish.yml triggers → PyPI via OIDC + Docker image to Docker HubSee CONTRIBUTING.md for detailed guidelines.
MIT © Ronald Isack Gosso