praxinoscope
Storyboard-driven animated explainer videos in pure Python.
A storyboard is a plain YAML file: ordered scenes, each with a scene type, typed fields, captions in one or more languages, a duration and source references. praxinoscope renders it deterministically, with Cairo drawing, Pango text and ffmpeg encoding, into MP4, WebM or GIF at 16:9, 9:16 or 1:1. The same storyboard and theme always produce the same frames.
Status: milestones 1 and 2 of the project brief are done, plus the rest of the starter scene types and six chart scenes. That covers the engine core, seventeen scene types (title, end, counter, merge/split, timeline, journey, grid, compare, stages, quote, place, ranking, line, donut, seats, waterfall, diverging), the Bauhaus and Muted themes, the storyboard schema and loader, an end-to-end render, and a small CLI. Ingest, outline and the AI adapter come later.
AI agents: start with llms.txt, or run praxinoscope guide.
Install
System libraries (not on PyPI): Pango with GObject introspection, and ffmpeg.
# Debian/Ubuntu
sudo apt install ffmpeg gir1.2-pango-1.0 python3-gi python3-gi-cairo
# macOS
brew install ffmpeg pango pygobject3
pip install praxinoscope # pycairo, PyGObject, numpy, pydantic, PyYAML
python -c "import praxinoscope; praxinoscope.fetch_fonts()" # Noto Sans SC for Chinese (~17 MB)
PyGObject and pycairo build against the system libraries. If the build fails,
create the virtual environment with --system-site-packages so it uses the
distribution's python3-gi and python3-cairo instead.
Jost ships in the package. See FONTS.md for font licenses.
Quick start
praxinoscope example board.yaml # starter storyboard
praxinoscope check board.yaml # every problem at once, with fix hints
praxinoscope preview board.yaml # sheet.png: one still per scene
praxinoscope render board.yaml out.mp4 --aspect 9:16
From Python, each step is one call and takes a path, YAML text, a dict or a list:
import praxinoscope as px
px.check("board.yaml") # [] when it will render
px.render("board.yaml", "out.mp4", aspect="9:16", scale=0.5)
Project gives the same with more control:
from praxinoscope import Project
p = Project.load("examples/sail/storyboard.yaml")
p.check() # field validation, caption overflow, glyph coverage
p.preview("sheet.png") # contact sheet: one still per scene
p.render("sail.mp4") # 1080p, with music and sound cues
p.render("sail-vertical.mp4", aspect="9:16")
p.render("sail.gif", scale=0.5)
Storyboard
version: 1
meta:
title: "SAIL"
languages: [en] # caption languages; with two, e.g. [en, fr], *_alt fields use the second
aspect: "16:9" # 16:9 | 9:16 | 1:1
theme: bauhaus
fps: 30
eyebrow: "Chapter 1"
scenes:
- id: red-storm # stable slug
type: merge
fields: {count: 27, unit: nations, result: RED STORM}
captions:
en: "2021: 27 nations launch Red Storm, the first crewed Mars mission."
year: "2021" # shown in the year chip; omit to hide it
sources: [p3.s1, p3.s2] # traceability back to the input text
duration: auto # reading-time based, or seconds
pinned: false # protect from regeneration (used by later stages)
Validation reports every problem at once, with paths such as
scenes[1] (red-storm).fields.count, and suggests fixes for typos
(unknown field 'valeu' (did you mean 'value'?)). praxinoscope.json_schema()
returns the JSON Schema, with each scene type's fields spelled out, for editors
and structured AI output.
The same storyboard in the compact form, which loads identically:
title: "SAIL"
scenes:
- {type: merge, id: red-storm, count: 27, unit: nations, result: RED STORM, year: "2021",
caption: "2021: 27 nations launch Red Storm, the first crewed Mars mission."}
Scene fields sit on the scene, id defaults to <type>-<n>, caption: "text"
is the first-language caption, and meta keys may sit at the top level.
praxinoscope expand prints the canonical form.
Scene types
praxinoscope scenes prints the full reference, generated from the code.
| type | fields |
|---|---|
title |
title, title_alt, subtitle, kicker |
end |
title, title_alt, lines (up to 6) |
counter |
value, start, decimals, prefix, suffix, unit, unit_alt, label, label_alt |
merge |
count, unit, unit_alt, result, result_alt, mode (merge or split) |
timeline |
events (2 to 8 of date, label, label_alt), highlight |
journey |
origin, origin_alt, destination, destination_alt, via (up to 3), distance, distance_alt |
grid |
count or items (up to 12 names), highlight, label, label_alt |
compare |
left, right (each title, title_alt, value, prefix, suffix, decimals, note, note_alt), mode (versus or before-after) |
stages |
steps (2 to 6 of title, title_alt, note), current |
quote |
text, text_alt, attribution, attribution_alt, marks |
place |
name, name_alt, detail, detail_alt, coordinates, nearby (up to 4) |
ranking |
items (2 to 10 of label, label_alt, value), order, highlight |
line |
values, start_label, end_label, callout (at, text, text_alt), from_zero |
donut |
parts (2 to 6 of label, label_alt, value), center, center_alt, show, highlight |
seats |
parties (1 to 8 of label, label_alt, seats), majority, majority_label |
waterfall |
steps (2 to 9 of label, label_alt, value, total) |
diverging |
items (2 to 10, values may be negative), baseline, baseline_alt, sort |
The chart scenes (ranking to diverging) also take label, label_alt (a
heading) and prefix, suffix, decimals (how numbers print), except seats,
which takes only the heading. grid takes shape: square, person, house or
circle. Their designs follow the
FT Visual Vocabulary.
examples/tour/storyboard.yaml uses every scene
type once, and examples/charts/storyboard.yaml
shows the chart scenes with invented data; render either with theme="bauhaus"
or theme="muted" to compare themes.
Architecture
praxinoscope/
schema.py storyboard model (pydantic v2), versioned
loader.py YAML/JSON load + save, scene field resolution, error collection
timing.py auto durations (reading time per script), frame-exact timeline
render.py composition: background, scene, chrome, transition; video, stills, contact sheet, audio
project.py high-level Project API
api.py one-call check/render/preview, generated scene reference
cli.py praxinoscope command
plugins.py scene/theme registries + entry points
engine/ tween, layout (Frame/Rect/anchors/grids), Pango text, color, ffmpeg encoder, numpy audio
scenes/ SceneType contract + built-in scenes
themes/ Theme contract + Bauhaus
Scenes draw only through the theme (ctx.theme.entity(...), ctx.text(..., role),
ctx.ease(t, start, dur, "emphasis")), and they position things with the
stage rect and frame units, never with pixels. A theme supplies the palette,
text roles, shape vocabulary, easings, the chrome (header, eyebrow, year chip, caption band), the
transition and the sound palette.
The shape vocabulary every theme provides (the base Theme has plain
fallbacks for all of them):
| shape | is | used by |
|---|---|---|
entity |
something that acts; emphasis=True for "the one" |
title, end, merge, compare |
item |
a thing in a collection; emphasis / dim for highlighting |
grid |
figure |
a pictogram: person, house or circle |
grid |
place |
a location | journey, place |
connector |
a link from A toward B | stages, compare |
marker |
a node on a line, active or still ahead; also a seat or a legend swatch |
timeline, stages, compare, seats, donut |
rule |
a plain line: axis, divider, attribution dash | timeline, quote, place |
route |
a travelled path (an engine.path.Polyline) |
journey |
traveller |
what moves along a route, pointing along its heading | journey |
pin |
a map pin | journey, place |
halo |
a ring for pulses and "you are here" | timeline, grid, stages, place |
panel |
a card grouping content | compare |
bar |
a quantity bar; emphasis for a total or the one pointed at |
compare, ranking, waterfall, diverging |
slice |
a ring or pie segment | donut |
series |
a data line (a Polyline) |
line |
axis |
a chart baseline or zero line | line, waterfall, diverging |
callout |
an annotation box with a pointer | line |
quote_mark |
an opening quotation mark | quote |
on_accent(i) |
the text color that reads on accent(i) |
stages, compare |
Themes may also define the text roles quote, quote_alt and value; a theme
without them falls back to display_alt, subtitle and display.
Writing a scene type
from praxinoscope import register_scene
from praxinoscope.scenes import Fields, SceneType
class DotsFields(Fields):
n: int = 3
@register_scene
class Dots(SceneType[DotsFields]):
name = "dots"
Fields = DotsFields
min_duration = 3.0
def draw(self, ctx, t):
for i, cell in enumerate(ctx.stage.cols(*[1] * self.f.n)):
k = ctx.ease(t, 0.2 * i, 0.6, "emphasis")
ctx.theme.entity(ctx.cr, cell.center, cell.short * 0.5 * k, i)
Writing a theme
Subclass praxinoscope.themes.Theme (or an existing theme), set name, and
override palette, roles, regions, chrome, transition, shapes and
sounds as needed. Register it with @register_theme, or in another package with
an entry point:
[project.entry-points."praxinoscope.themes"]
swiss = "mypkg.themes:Swiss"
Development
git clone https://github.com/emptymalei/praxinoscope && cd praxinoscope
pip install -e '.[dev]'
pytest
Documentation
The docs site is built with Zensical from docs/ and
zensical.toml; the API reference is generated from docstrings.
pip install -e '.[docs]'
zensical serve # http://localhost:8000, rebuilds on save
zensical build # static site in site/
Releasing
Set __version__ in src/praxinoscope/__init__.py, merge, then push a matching
tag (git tag v0.1.0 && git push origin v0.1.0). The Release workflow builds the
sdist and wheel, renders a test video from the installed wheel, and publishes to
PyPI through trusted publishing. Running the workflow by hand publishes to
TestPyPI instead.
Decisions taken as defaults
These are open questions from the brief, settled for now and easy to revisit:
- Name:
praxinoscope, after Émile Reynaud's 1877 animation device. It was free on PyPI on 2026-10-03. - License: Apache-2.0.
- Pango binding: PyGObject PangoCairo.
- Schema: pydantic v2,
version: 1.
License
Apache-2.0. Bundled fonts keep their own licenses (see FONTS.md).
Metadata
Release files for praxinoscope 0.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 | |
|---|---|---|---|
| praxinoscope-0.1.0.tar.gz | 1.2 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| praxinoscope-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.4 MB
Release files / praxinoscope-0.1.0.tar.gz
| Download URL | praxinoscope-0.1.0.tar.gz |
|---|---|
| Size | 1.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6457a8b5240bb8d562ba836524de789a148c5539fd733391297595299621af7b
|
|
BLAKE2b-256 checksum How to use checksums |
c5bf89e68c894c1efe1cbcc5e62c66b77db24242928d11cd15e1f936cc09b5db
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency logRelease files / praxinoscope-0.1.0-py3-none-any.whl
| Download URL | praxinoscope-0.1.0-py3-none-any.whl |
|---|---|
| Size | 169.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
af0fed317300b778c3711698859045bdfee397d439ee28c3121fd96281febb2a
|
|
BLAKE2b-256 checksum How to use checksums |
8f3187efc84a874c5be3c47da392095fcb635e692b6374c84da35c9d1b254e97
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.
Transparency log