VisualKit
A Python engine for programmatically building and exporting video timelines —
media clips, text, audio, reusable "compound" clip templates, and
code-rendered visuals (infographics/animations built with HTML/CSS/JS),
composed on a Timeline and exported to a standalone video or to a project
file for import into DaVinci Resolve.
Status: phase 1. Core timeline/clip model, coded-visual compilation, and export are implemented. The FFmpeg exporter (and single-clip previews) also support: keyframed transforms (position, scale, rotation, zoom, opacity), chroma key, rectangle/ellipse masks (with keyframeable geometry),
fade/slide/pop/wipeanimation presets (ClipAnimation, which also covers text -- aTextClipis aVisualCliplike any other), extended text styling (outline, drop shadow, letter spacing, line height, gradient fill), and lossless alpha video (render.mkv) for animated coded visuals. The DaVinci Resolve export ignores all of the above (keyframes, chroma key, masks, animation presets, and the extra text styling) and keeps rendering the flat H.264render.mp4. Transitions and per-character text animation are on the roadmap (seenotes/) but not yet built.
Documentation
This README is the quick pitch and quick start. For the full guide —
core concepts, a complete clip/field reference, coded-visual authoring,
templates, keyframes/effects, exporting, error handling, a dedicated guide
for code-generating agents, and a list of known pitfalls — see
VisualKit Docs.
Requirements
- Python >= 3.12
- FFmpeg on your
PATH, for standalone video export and for encoding animated coded visuals - A Chrome or Chromium install, for
CodedVisualClips and forTextClips exported withFFmpegVideoExporter. VisualKit never bundles or downloads a browser. It looks, in order, at theVISUALKIT_CHROMEenvironment variable, yourPATH, the usual install locations, and browsers cached by Playwright/Puppeteer.chrome-headless-shellis preferred when present (smaller and faster to start). - Optional:
playwright(pip install "visualkit[render]"), needed only to render animated coded visuals. It is used purely as a driver for the Chrome above. Still visuals, text, and everything else work without it.
Installation
pip install -e . # core
pip install -e ".[render]" # + animated coded visuals
(This project isn't yet published to PyPI; install from a local checkout.)
Quick start
import visualkit as vk
timeline = vk.Timeline()
video = vk.MediaClip(
id="main_video",
source=vk.Source(source="footage.mp4"),
duration=vk.Time.from_seconds(10),
)
timeline.add_clip(video, track_index=0)
title = vk.TextClip(
id="title",
text="Hello, World!",
timeline_start=vk.Time.from_seconds(1),
duration=vk.Time.from_seconds(3),
)
timeline.add_clip(title, track_index=1)
# Export to a standalone MP4 (requires ffmpeg, and Chrome for the TextClip)
timeline.export_to_video("output/hello.mp4")
# ...or to a DaVinci Resolve project
timeline.export_to_resolve("output/hello_resolve.xml")
See examples/ for runnable walkthroughs of each feature area:
| Example | Covers |
|---|---|
01_basic_timeline.py |
Building a timeline from media/text/audio clips, multi-track layout, exporting |
02_coded_visuals.py |
CodedVisualClip: a bundle with a manifest, typed variables, a still preview, and an animated render |
03_compound_clips_and_templates.py |
CompoundClip as a grouping/template mechanism with exposed Variables |
04_asset_resolvers.py |
Resolving abstract asset references (asset://...) to real files at export time |
Core concepts
Timeline & Tracks. A Timeline holds a list of video tracks and a list
of audio tracks (Track[Clip]). timeline.add_clip(clip, track_index=N)
routes a clip to the right lane automatically based on its type, creating
tracks up to N as needed. Clips can be inserted in OVERLAP (default) or
RIPPLE mode; ripple-inserting into a point that falls inside an existing
clip's span raises InvalidTrackOperationError rather than silently
producing an overlapping timeline.
Clips. MediaClip (video/image files), AudioClip, TextClip, and
CodedVisualClip all share a common BaseClip (id, timing, speed). Visual
clips (MediaClip, TextClip, CodedVisualClip) additionally carry a
Transform (position, scale, rotation, opacity).
CompoundClip. Groups a nested Timeline (its inner_timeline) into a
single clip on the parent timeline — useful for building reusable templates.
Exposed Variables let a compound clip's inner text/parameters be
overridden per-instance (compound.set_parameter(name, value)) without
touching the inner timeline's structure. A compound's own speed compresses
its entire inner timeline proportionally when flattened.
CodedVisualClip. References a small HTML/CSS/JS project (a "coded visual"),
such as an animated infographic, either a single .html file or a directory
bundle (index.html + manifest.json + local images/SVGs/CSS/JS). It is not
itself media: CodedVisualCompiler renders it in headless Chrome into a real
file, a PNG for a still visual or an MP4 for an animated one, and that file
is what the timeline and exporters use. See Coded visuals below.
Flattening. timeline.flatten() (used internally by both exporters)
applies template parameters, renders any CodedVisualClips, and expands
CompoundClips into plain clips with absolute timeline coordinates, giving a
timeline made only of MediaClip/TextClip/AudioClip. It never modifies the
timeline you call it on, so exporting cannot change your templates. A compound's
own transform is composed onto its children, and its content is trimmed to the
compound's duration.
Coded visuals
clip = vk.CodedVisualClip(id="card", source="assets/stat_card", duration=vk.Time(4))
clip.load_manifest() # canvas size + variable schema from the bundle
clip.set_variable("title", "Active users")
clip.set_variable("accent", "#22c55e") # typed: a bad color raises immediately
timeline.add_clip(clip)
timeline.export_to_video("out.mp4") # renders, then composites like any media clip
Design canvas and scaling. A visual is authored against a fixed design canvas
(<meta name="canvas-size" content="1920x1080">, or canvas_size in manifest.json,
or set on the clip; clip.design_size reports the effective one; canvas_size itself
is None until set explicitly). Put your content inside .visualkit-canvas. It is
always rendered at exactly that size and scaled, aspect ratio preserved, to fit
whatever frame it lands in, so the layout never reflows. The 9:16 (vertical), 1:1 and
16:9 cases are all just a different design size.
Variables and templates. {{ name }} in the HTML is replaced by the
HTML-escaped value. Use {{{ name }}} for trusted raw HTML. The same values are
available to script as window.__VARIABLES__.name, already typed. Variable types
(string, number, boolean, color, asset, json) are enforced. Declare them
in manifest.json with labels and descriptions to make a bundle a reusable scene
template. Wrap a visual in a CompoundClip and expose_parameter(...) to drive it
per instance. Placeholders inside <style> are HTML-escaped as well, which is safe
for validated color/number variables but not for arbitrary strings.
Local assets. Images, SVGs, fonts, CSS and JS beside index.html load normally.
Editing any of them invalidates the cached render.
Still vs. animated. render_mode is auto by default: a page with motion becomes a
video, a static one a PNG. Detection uses the manifest's animated flag if set, else
renders the page at two times and compares. Animation is stepped deterministically
(CSS/Web Animations are seeked; Date, setTimeout, setInterval and
requestAnimationFrame run on a virtual clock), so output is identical on every
machine. Frames stream straight into FFmpeg. Pass render_video=True/False to
flatten()/export_*() to override.
Limits. Determinism is guaranteed for CSS/Web Animations and for the JavaScript timing
APIs listed above. Anything else that depends on real time (<video>/<audio> elements,
WebGL or canvas code that reads other clocks, network fetches) is not guaranteed to be
frame-exact. Pre-render such media and reference it as a normal clip instead. Only
HTML/CSS/JS visuals exist today; other compilers are a possible future extension.
Exporting
-
timeline.export_to_video(path, **kwargs): renders a standalone video viaFFmpegVideoExporter. Text size is relative to a 1080p reference frame (font_size=48is 48px at 1080p) so a 360p preview matches the 1080p render. A failed encode raisesExportErrorcarrying FFmpeg's own output, and the destination file is only replaced on success. -
timeline.export_to_resolve(path, **kwargs): writes an NLE project viaDaVinciResolveExporter..xmlproduces FCP7 XML (XMEML), the more established path for DaVinci Resolve..fcpxmlproduces Apple FCPXML 1.10. NTSC rates (29.97, 23.976, 59.94) use their exact rational frame rate, and FCPXML times are frame-aligned rationals.Verify in your NLE. XMEML and FCPXML position conventions were implemented from the format specifications and are covered by structural tests, but have not been checked against a live DaVinci Resolve import. Test a positioned clip before relying on it. Scale, opacity and timing are the well-trodden parts.
Both exporters accept an asset_resolver, a callable or an object with a
.resolve(str) -> str method, for mapping abstract source references
(e.g. asset://b_roll) to real paths or URLs at export time. See
visualkit.engine.asset_resolver.DictAssetResolver or examples/04_asset_resolvers.py.
Errors
All library errors derive from visualkit.VisualKitError: InvalidTimeError,
TimelineValidationError, TemplateParameterError (an exposed parameter targets a
missing clip/property, or a protected field such as id), MissingSourceError,
ExportError, and the coded-visual family CodedVisualError >
CodedVisualCompileError > BrowserNotFoundError. Timeline edits (split_clip,
move_clip_track, and add_clip, including a batch of several clips) are atomic: a rejected
edit leaves the timeline exactly as it was.
Development
uv sync --group dev # includes playwright + pillow so the render tests run
pytest
ruff check . && ruff format --check .
License
MIT — see LICENSE.
Release files for visualkit 2.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| visualkit-2.1.0.tar.gz | 180.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| visualkit-2.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 307.0 kB
Release files / visualkit-2.1.0.tar.gz
| Download URL | visualkit-2.1.0.tar.gz |
|---|---|
| Size | 180.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7583b632dea52a930920c036a7ae89f095bbddd6c81110434e4afa8046242529
|
|
BLAKE2b-256 checksum How to use checksums |
c5a19a90e9566571a0dad0e447204792881ec310b829139a10b333d209839863
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / visualkit-2.1.0-py3-none-any.whl
| Download URL | visualkit-2.1.0-py3-none-any.whl |
|---|---|
| Size | 126.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b0b6cd13b632faed0c551f27d2a2029333b5d9c2f071b0e7a95426470d032507
|
|
BLAKE2b-256 checksum How to use checksums |
5e777b76d855221e32b10376bcb81686ef203a17af89a4d768fe56621bd602fc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|