Python · auto-managed ffmpeg · zero setup

clipflow

Trim, compress, and highlight video clips with a clean, typed Python API. FFmpeg binaries auto-downloaded on first use — no manual installation, no configuration.

00:00:00:00
● REC — processing clips
intro 00:00 → 01:00
key_moment ★ 05:00 → 06:30
vertical_short 10:00 → 10:30
outro 58:30 → 60:00
Install $pip install clipflow Requires Python ≥ 3.9  ·  FFmpeg auto-installed ✨

Everything you need.
Nothing you don't.

✨ AUTO
FFmpeg auto-managed
No manual installation. On first use, clipflow detects your platform, downloads the right binaries from trusted sources, and caches them locally. Windows, macOS, Linux — all supported.
TRIM
Lossless stream-copy
Default mode. -ss before -i for a fast keyframe seek. No re-encoding, no quality loss, finished in seconds.
COMPRESS
CRF re-encode
Three named presets — COMPRESS_LOW / MEDIUM / HIGH — or supply any CRF value and codec directly.
HIGHLIGHT ★
Highlight routing
Mark any ClipSpec with highlight=True. Finished clips are copied to output/highlights/ automatically — no second encode.
ASPECT
Aspect ratio crop
Crop and pad to 16:9, 9:16, 1:1, 4:3, or any custom ratio. Centred crop, even-dimension safe, applied as a single ffmpeg filter chain.
INSPECT
Video metadata
Duration, resolution, fps, codec, file size — returned as a typed VideoInfo dataclass via ffprobe. Works on any format ffmpeg supports.
BATCH
Batch processing
Process multiple source files in one call via batch(), or drive it from a JSON spec file with the CLI.

Simple at the surface.
Precise underneath.

import clipflow
from clipflow import ClipSpec, parse_range

# ── lossless stream-copy (default, fastest) ──────────────────────
results = clipflow.trim(
    "documentary.mp4",
    ClipSpec(parse_range("01:00", "02:30")),
    output_dir="clips",
)

# ── multiple ranges in one call ───────────────────────────────────
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")

# ── per-clip progress callback ────────────────────────────────────
def on_progress(idx, total, result):
    print(f"[{idx}/{total}] {result.spec.effective_label()}")

clipflow.trim("video.mp4", clips, on_progress=on_progress)
from clipflow import ClipSpec, CompressOptions, COMPRESS_HIGH, parse_range

# ── named preset (easiest) ────────────────────────────────────────
clip = ClipSpec(
    parse_range("00:00", "01:00"),
    compress=COMPRESS_HIGH,  # CRF 18, slow preset
)

# ── custom CRF + codec ────────────────────────────────────────────
custom = CompressOptions(
    crf=20,
    preset="slower",
    codec="libx265",       # H.265 — smaller files, same quality
    audio_bitrate="192k",
)
clip2 = ClipSpec(parse_range("05:00", "06:30"), compress=custom)

# CRF 0–51: lower = better quality, larger file
# COMPRESS_LOW=28  COMPRESS_MEDIUM=23  COMPRESS_HIGH=18
from clipflow import ClipSpec, COMPRESS_MEDIUM, parse_range
import clipflow

clips = [
    ClipSpec(parse_range("00:00", "01:00"), label="intro"),
    ClipSpec(
        parse_range("05:00", "06:30"),
        label="hero_moment",
        highlight=True,          # ← routed to highlights/
        compress=COMPRESS_MEDIUM,
    ),
]

results = clipflow.trim("concert.mp4", clips, output_dir="out")

print(results[1].output_path)
# out/hero_moment.mp4

print(results[1].highlight_path)
# out/highlights/hero_moment.mp4  ← separate copy, no re-encode
from clipflow import ClipSpec, AR_9_16, AR_16_9, AspectRatio, parse_range

# ── named shortcuts ───────────────────────────────────────────────
vertical = ClipSpec(
    parse_range("00:00", "01:00"),
    aspect_ratio=AR_9_16,    # Reels / Shorts / TikTok
)
landscape = ClipSpec(
    parse_range("00:00", "01:00"),
    aspect_ratio=AR_16_9,    # YouTube
)

# ── custom ratio ──────────────────────────────────────────────────
ultra = ClipSpec(
    parse_range("00:00", "01:00"),
    aspect_ratio=AspectRatio(21, 9),
)

# AR_16_9  AR_9_16  AR_1_1  AR_4_3  — or AspectRatio(W, H)
import clipflow

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.3

# VideoInfo fields: path · duration_s · duration_fmt · width
# height · resolution · fps · video_codec · audio_codec
# size_bytes · size_mb
from 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"), highlight=True),
        ],
    ),
    BatchSpec(
        input_path=Path("ep02.mp4"),
        output_dir=Path("ep02_clips"),
        clips=[ClipSpec(parse_range("00:00", "02:00"))],
    ),
]

all_results = clipflow.batch(specs)

for path, results in all_results.items():
    ok = sum(r.ok for r in results)
    print(f"{path.name}: {ok}/{len(results)} ok")

Three commands.
Complete control.

clipflow trim extract segments
$ clipflow trim lecture.mp4 01:00-02:30

$ clipflow trim lecture.mp4 00:00-01:00 10:30-12:00 \
    --output clips/

$ clipflow trim concert.mp4 05:00-06:30 \
    --compress high \
    --aspect 9:16 \
    --highlight

$ clipflow trim raw.mp4 00:00-30:00 \
    --crf 20 --codec libx265
clipflow inspect video metadata
$ clipflow inspect documentary.mp4

  documentary.mp4
  ────────────────────────────────────
  Duration   01:23:45
  Resolution 1920×1080
  FPS        29.970
  Video      h264
  Audio      aac
  Size       842.3 MB

$ clipflow inspect video.mp4 --json | jq .fps
clipflow batch JSON-driven batch
$ clipflow batch spec.json

# spec.json
[{
  "input": "lecture.mp4",
  "output_dir": "clips",
  "clips": [{
    "start": "00:30", "end": "02:15",
    "label": "intro", "compress": "medium",
    "highlight": true
  }]
}]
time formats accepted everywhere
# All of these are equivalent
01:30       # MM:SS → 90 seconds
00:01:30    # HH:MM:SS → 90 seconds
90          # plain integer seconds
90.5        # fractional seconds

# In Python
parse_range("01:30", "02:00")
parse_range(90, 120)
parse_range("1:30", 120.0)  # mixed ok

Compression presets.

COMPRESS_LOW
CRF28
Presetfast
Codeclibx264
Use casesmallest file
COMPRESS_MEDIUM
CRF23
Presetmedium
Codeclibx264
Use casebalanced (default)
COMPRESS_HIGH
CRF18
Presetslow
Codeclibx264
Use casebest quality

clipflow.trim() parameters

Parameter Type Default Description
input_path str | Path required Source video file path
clips ClipSpec | list[ClipSpec] required One or more segments to extract
output_dir str | Path "output" Destination directory, created automatically
on_progress callable | None None Called after each clip: (idx, total, ClipResult) → None

ClipSpec fields

Field Type Default Description
time_range TimeRange required Segment start and end in seconds
highlight bool False Also copy finished clip to output/highlights/
compress CompressOptions | None None Re-encode settings; None = lossless stream-copy
aspect_ratio AspectRatio | None None Crop and pad to target W:H ratio
label str | None None Output filename stem; auto-generated if omitted