Skip to main content

core-flux

A fast, layer-based video editing library for Python, built on native FFmpeg filtergraphs.

Every edit you chain — resize, crop, fade, overlay, mix — appends a node to a filter graph. Nothing executes until you call render(), which compiles the whole timeline into a single FFmpeg process. Frames never cross into Python, so a composite runs at FFmpeg's own speed instead of Python's.

from core_flux import VideoLayer, AudioLayer, Composition

Composition(
    layers=[
        VideoLayer("gameplay.mp4").resize(1920, 1080).fade_out(),
        VideoLayer("facecam.mp4").resize(400, 300).set_position(50, 50).mute(),
    ],
    audio_tracks=[AudioLayer("lofi.mp3").with_volume_scaled_to(0.2)],
).render("edit.mp4")

Renamed in 0.4.0: the import name is now core_flux, matching the package name. import fastvideo still works but emits a DeprecationWarning, and will be removed in 1.0.


Installation

pip install core-flux

FFmpeg must be installed separately and available on your PATH:

Platform Command
macOS brew install ffmpeg
Debian/Ubuntu sudo apt install ffmpeg
Windows winget install ffmpeg

Requirements: Python 3.8+, FFmpeg 4.0+.


Core concepts

Layers stack, first one is the canvas. layers[0] is the background; everything after is composited on top of it in order, at the position you give it.

Output length follows the base video layer. A music bed longer than your video is cut off at the end of the picture; a shorter one is padded with silence. Overlays never extend the render, and a short overlay disappears when it ends rather than freezing on its last frame.

Track volumes are absolute. with_volume_scaled_to(0.2) means 20% of the original, regardless of how many other tracks are in the mix.

Nothing runs until render(). Layers are cheap to build and rearrange. Use get_command() to see the FFmpeg invocation without running it.


Quick start

Composite a facecam over gameplay with background music

from core_flux import VideoLayer, AudioLayer, Composition

background = VideoLayer("gameplay.mp4").resize(1920, 1080).fade_out(duration=1.5)

facecam = (VideoLayer("facecam.mp4")
           .resize(400, 300)
           .set_position(x=50, y=50)
           .mute())

music = AudioLayer("lofi.mp3").with_volume_scaled_to(0.2).fade_out()

Composition(layers=[background, facecam], audio_tracks=[music]).render("edit.mp4")

Join clips end to end

from core_flux import VideoLayer, Composition, concatenate

reel = concatenate([
    VideoLayer("intro.mp4"),
    VideoLayer("main.mp4").trim(5, 45),
    VideoLayer("outro.mp4"),
], width=1920, height=1080)

Composition(layers=[reel.fade_in().fade_out()]).render("reel.mp4")

Time an overlay to appear partway through

watermark = (VideoLayer("logo_anim.mp4")
             .resize(200, 200)
             .set_position(x=1650, y=50)
             .set_start(8.0)       # appears 8 seconds in
             .set_opacity(0.6))

Composition(layers=[VideoLayer("talk.mp4"), watermark]).render("talk.mp4")

Export a GIF

Composition(layers=[VideoLayer("clip.mp4").trim(0, 4)]).render(
    "clip.gif", gif_fps=15, gif_width=480
)

API reference

VideoLayer(input_path)

A video clip. The file is probed on construction, so a missing or unreadable path fails immediately rather than deep inside FFmpeg later. Every method returns self, so calls chain.

Read-only properties: .duration, .width, .height, .fps, .has_audio, .end (start_time + duration). .duration, .width and .height track the edits you apply — after .trim(0, 5).speed(2.0), .duration is 2.5.

Geometry

Method Description
.set_position(x, y) Place the layer's top-left corner on the canvas.
.resize(width, height) Scale to an exact pixel size.
.scale_by(factor) Scale by a multiplier, rounded to even dimensions for H.264.
.crop(x1, y1, width, height) Keep a rectangle starting at top-left (x1, y1).
.rotate(degrees) Rotate clockwise by 90, 180 or 270.
.flip(axis) Mirror across "horizontal" or "vertical".

Appearance

Method Description
.adjust_colors(contrast=1.0, brightness=0.0, saturation=1.0) Tune the picture.
.blackwhite() Remove all colour.
.set_opacity(alpha) Make the layer semi-transparent when overlaid (0.01.0).
.add_text(text, x=10, y=10, size=48, color="white", font=None, box=False, box_color="black@0.5", start=None, end=None) Burn in text. Requires an FFmpeg built with libfreetype.

Time

Method Description
.trim(start, end) Keep the section between two timestamps, in seconds.
.subclip(start=0, end=None) Same as trim, but end defaults to the end of the clip.
.speed(factor) 2.0 is twice as fast, 0.5 half speed. Audio pitch is preserved.
.set_start(seconds) Delay the layer so it begins partway into the timeline.
.fade_in(start_time=0.0, duration=1.0) Fade picture and native audio up together.
.fade_out(start_time=None, duration=1.0) Fade both down. With no start_time, lands on the clip's final duration seconds.

Audio

Method Description
.with_volume_scaled_to(factor) Scale the embedded audio. 0.5 halves, 2.0 doubles.
.mute() Drop the embedded audio entirely.

AudioLayer(input_path)

A standalone audio track. Supports .with_volume_scaled_to(factor), .mute(), .trim(start, end), .subclip(start, end), .speed(factor), .set_start(seconds), .fade_in(start_time=0.0, duration=1.0) and .fade_out(start_time=None, duration=1.0), plus the .duration and .end properties.

AudioLayer("podcast.wav").trim(30, 90).fade_in().fade_out()

ImageLayer(input_path, duration=5.0, fps=30)

A still image held on screen — title cards, logos, watermarks. Accepts every VideoLayer method.

ImageLayer("logo.png", duration=3).set_position(20, 20).set_opacity(0.7)

ColorLayer(width, height, duration, color="black", fps=30)

A solid colour canvas to composite onto, when you don't want a video as the background.

Composition(layers=[
    ColorLayer(1920, 1080, duration=10, color="#101014"),
    VideoLayer("clip.mp4").resize(1280, 720).set_position(320, 180),
]).render("framed.mp4")

concatenate(layers, width=None, height=None, fps=None, audio=True)

Join clips end to end. FFmpeg's concat requires matching resolution, pixel aspect and frame rate, so each segment is normalised first; segments with no audio contribute matching silence to keep sound aligned across the joins.

Returns an ordinary VideoLayer, so the result can be trimmed, faded and overlaid like any other clip. width/height/fps default to the first clip's.


Composition(layers=None, audio_tracks=None)

The timeline.

Member Description
.add_layer(layer) Stack another visual layer on top. Chainable.
.add_audio(track) Add another track to the mix. Chainable.
.duration Expected output length.
.get_command(output_path, **kwargs) The FFmpeg command that would run, as a string.
.render(output_path, ...) Compile and write the file. Returns output_path.

render(output_path, format_type=None, quiet=False, verbose=False, overwrite=True, duration=None, **encoder_options)

Argument Description
format_type 'video', 'gif' or 'audio'. Inferred from the file extension when omitted.
quiet Suppress core-flux's own progress lines.
verbose Stream FFmpeg's raw output to the console instead of capturing it.
overwrite Overwrite an existing file (default True).
duration Hard-cap the output length in seconds.
gif_fps, gif_width GIF only. Default to 15 and the source width.
**encoder_options Passed to FFmpeg as output options, overriding the defaults — crf=18, preset="slow", r=60.

Defaults for MP4-family containers are H.264 + AAC, yuv420p, with +faststart for web playback. Other containers are left to FFmpeg's own codec defaults, so .webm correctly gets VP9/Opus.

timeline.render("out.mp4", crf=18, preset="slow")
timeline.render("out.gif", gif_fps=20, gif_width=600)
timeline.render("mix.wav")
print(timeline.get_command("out.mp4"))

Errors

Everything raised on purpose inherits from CoreFluxError, so one except clause covers the library:

Exception Raised when
FFmpegNotFoundError ffmpeg/ffprobe are not on PATH.
MediaNotFoundError An input file does not exist.
UnsupportedMediaError The file is unreadable, or lacks the stream the layer needs.
FilterUnavailableError Your FFmpeg build lacks a required filter (e.g. drawtext).
RenderError FFmpeg exited non-zero. Carries .command and .stderr.
from core_flux import CoreFluxError, RenderError

try:
    timeline.render("out.mp4")
except RenderError as e:
    print(e.stderr)   # what FFmpeg actually complained about
    print(e.command)  # the exact command, to re-run by hand
except CoreFluxError as e:
    print(e)

Performance

Two workloads, three engines, on an Apple M1 (8-core, 16 GB, native arm64, Python 3.10). Three runs each after a warmup. The input is a 15-second 1080p clip at a realistic ~8 Mbps.

Workload: transcode (1080p -> 720p, H.264/AAC)
  Engine       | Avg        | Min        | Max        | Std dev
  ---------------------------------------------------------------
  ffmpeg       |     4.24s  |     4.24s  |     4.25s  |   0.00s
  core-flux    |     4.35s  |     4.32s  |     4.40s  |   0.04s
  moviepy      |    16.31s  |    16.18s  |    16.56s  |   0.21s
  -> core-flux is 3.75x the speed of MoviePy

Workload: composite (overlay + audio mix + fade)
  Engine       | Avg        | Min        | Max        | Std dev
  ---------------------------------------------------------------
  ffmpeg       |     4.36s  |     4.29s  |     4.46s  |   0.09s
  core-flux    |     4.40s  |     4.36s  |     4.46s  |   0.05s
  moviepy      |    28.38s  |    28.19s  |    28.53s  |   0.17s
  -> core-flux is 6.45x the speed of MoviePy

Reading these honestly:

  • core-flux tracks raw FFmpeg within ~2%, which is the point — it adds a graph builder and one ffprobe call, not a processing layer.
  • The gap over MoviePy widens on the composite workload (3.75x to 6.45x) because MoviePy composites frames in Python/NumPy, while core-flux hands the overlay to FFmpeg. The more layers you add, the wider it gets.
  • These ratios depend heavily on your source. Encoding is the floor for every engine; the harder your footage is to encode, the more that floor dominates and the smaller the ratio. On easily-compressed footage the same benchmark shows over 12x, which flatters core-flux. Treat 3–6x as the realistic range for ordinary video.

Reproduce it yourself — the script generates its own inputs, or drop in your own benchmark/sample.mp4:

pip install moviepy
python benchmark/benchmark.py

Upgrading from 0.3.x

0.4.0 changes several behaviours that were producing wrong output. Existing code keeps working, but the results change:

Change Before Now
Output duration The longest audio track set the length, so a 10s music bed over a 6s video produced a 10s file. The base video layer sets the length. Long tracks are cut, short ones padded with silence.
Track volume amix divided every track by the number of inputs, so with_volume_scaled_to(0.2) became 0.1 once a second track existed. Volumes are absolute, whatever the track count.
Short overlays Froze on their last frame for the rest of the render. Disappear when they end.
GIF export Crashed with a split filter error on any composition with a filter or a second layer. Works.
Audio containers Every non-MP3 audio output was forced to AAC, producing unplayable .wav files. The container's correct default codec is used.
Missing files Surfaced later as an opaque FFmpeg error. MediaNotFoundError, at construction.
mute() Scaled volume to zero, leaving a silent stream in the mix. Drops the stream.
Import name import fastvideo import core_flux. The old name still works with a DeprecationWarning.

fade_out(start_fade=...) still works but is deprecated; use start_time=, or omit it to fade the final seconds.


Limitations

Worth knowing before you pick this over MoviePy:

  • No per-frame Python access. Everything is an FFmpeg filter, which is exactly why it's fast — but you cannot write a custom NumPy effect over raw frames. MoviePy can. If you need that, use MoviePy.
  • add_text() needs libfreetype. Many FFmpeg builds omit it. Check with ffmpeg -filters | grep drawtext; you'll get a clear FilterUnavailableError if it's missing.
  • No clip looping, and no crossfade transition between concatenated clips (each cut is hard).
  • Built on ffmpeg-python, which has not seen a release since 2019. It works, but it is not actively maintained.

Development

git clone https://github.com/Faaris/core-flux
cd core-flux
pip install -e ".[dev]"
pytest

The test suite generates its own media with FFmpeg's lavfi sources, so no fixture files are needed. Tests that assert durations or volumes render real files; tests that assert graph shape inspect get_command().


License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

core_flux-0.4.0.tar.gz (24.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

core_flux-0.4.0-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

File details

Details for the file core_flux-0.4.0.tar.gz.

File metadata

  • Download URL: core_flux-0.4.0.tar.gz
  • Upload date:
  • Size: 24.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.10.13

File hashes

Hashes for core_flux-0.4.0.tar.gz
Algorithm Hash digest
SHA256 cce60bd7d13084d6d75e5f291dd70a43b987e783ab11c392a73d1a7dd2d7c3ad
MD5 64b40f44026dac898e2297c73fc0f06a
BLAKE2b-256 c67df46963dd440a9376fc32eeb0a285dbf2f86239be17c6cc2c149287289860

See more details on using hashes here.

File details

Details for the file core_flux-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: core_flux-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 19.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.10.13

File hashes

Hashes for core_flux-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 aa9acb52fa7ab41d798c7001ac242a8b5f7ba732db9efa8b7e330fdc25f01536
MD5 c7e9296aa9619795ac9916fc907d8c9a
BLAKE2b-256 6e8ec50e62d521746a0209ffa5d0f194a1c79c719dea28c5667a9cd47a60e9b4

See more details on using hashes here.

Release history Release notifications | RSS feed

0.6.0

2 files

This release

0.4.0 This release

2 files

0.3.4

2 files

0.3.3

2 files

0.2.5

2 files

0.2.4

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page