Metadata-Version: 2.5
Name: praxinoscope
Version: 0.1.0
Summary: Storyboard-driven animated explainer videos in pure Python
Project-URL: Homepage, https://github.com/emptymalei/praxinoscope
Project-URL: Documentation, https://emptymalei.github.io/praxinoscope/
Project-URL: Source, https://github.com/emptymalei/praxinoscope
Project-URL: Issues, https://github.com/emptymalei/praxinoscope/issues
Author: LM
License-Expression: Apache-2.0
License-File: LICENSE
License-File: src/praxinoscope/fonts/OFL-Jost.txt
Keywords: animation,cairo,data visualization,explainer,pango,storyboard,video
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.10
Requires-Dist: numpy>=1.24
Requires-Dist: pycairo>=1.20
Requires-Dist: pydantic>=2.5
Requires-Dist: pygobject>=3.42
Requires-Dist: pyyaml>=6.0
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocstrings-python>=1.18; extra == 'docs'
Requires-Dist: zensical>=0.0.67; extra == 'docs'
Description-Content-Type: text/markdown

# 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](https://github.com/emptymalei/praxinoscope/blob/main/llms.txt), or run `praxinoscope guide`.

## Install

System libraries (not on PyPI): Pango with GObject introspection, and ffmpeg.

```bash
# Debian/Ubuntu
sudo apt install ffmpeg gir1.2-pango-1.0 python3-gi python3-gi-cairo
# macOS
brew install ffmpeg pango pygobject3
```

```bash
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](https://github.com/emptymalei/praxinoscope/blob/main/FONTS.md) for font licenses.

## Quick start

```bash
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:

```python
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:

```python
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

```yaml
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:

```yaml
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](https://github.com/Financial-Times/chart-doctor/tree/main/visual-vocabulary).

[examples/tour/storyboard.yaml](https://github.com/emptymalei/praxinoscope/blob/main/examples/tour/storyboard.yaml) uses every scene
type once, and [examples/charts/storyboard.yaml](https://github.com/emptymalei/praxinoscope/blob/main/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

```python
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:

```toml
[project.entry-points."praxinoscope.themes"]
swiss = "mypkg.themes:Swiss"
```

## Development

```bash
git clone https://github.com/emptymalei/praxinoscope && cd praxinoscope
pip install -e '.[dev]'
pytest
```

### Documentation

The docs site is built with [Zensical](https://zensical.org) from `docs/` and
`zensical.toml`; the API reference is generated from docstrings.

```bash
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).
