Python API¶
Every CLI command is a thin wrapper around the package modules, so anything the CLI does you can do from Python.
| Module | Contents |
|---|---|
vexy_stax.scene |
Pydantic v2 scene model and load_scene() |
vexy_stax.geometry |
Camera poses, gaps, opacities, caption layout, frame plans |
vexy_stax.engines |
Engine protocol and registry |
vexy_stax.images |
Pillow image reading and flat compositing |
vexy_stax.juicy |
Per-channel colour matching between renders |
vexy_stax.showcase |
The showcase runner behind example.py |
vexy_stax.cli |
The Stax Fire CLI and main() |
vexy_stax.__version__ holds the installed version.
Render a scene¶
from pathlib import Path
from vexy_stax.engines import available_engines, get_engine
from vexy_stax.scene import load_scene
scene = load_scene("scene.json")
print("Available:", available_engines())
pygfx = get_engine("pygfx")
pygfx.render_image(scene, "expanded", Path("beauty.png"))
pygfx.render_image(scene, "compact", Path("stack.png"))
get_engine("blender").render_video(scene, Path("morph.mp4"))
vexy_stax.scene¶
load_scene(path)¶
Reads a JSON file, validates it against the Scene model and resolves every slide src against the scene file's directory (absolute paths and data: URIs are kept). Invalid input raises pydantic.ValidationError.
from vexy_stax.scene import load_scene
scene = load_scene("scene.json")
scene.slides[0].src # absolute path string
scene.camera.gap # float
Model classes¶
from vexy_stax.scene import (
Scene, Slide, Caption, CaptionStyle, CaptionFade,
Camera, Transition, Video, Floor, Edge, Size, OpacityPerView,
View, TransitionKind, ShowIn, Easing,
)
Every model sets extra="forbid". The type aliases are:
| Alias | Values |
|---|---|
View |
"expanded", "compact" |
TransitionKind |
"expand", "collapse", "expand_collapse", "collapse_expand" |
ShowIn |
"expanded", "compact", "both", "none" |
Easing |
"linear", "easeInOutCubic", "easeOutCubic", "easeInCubic" |
Slide.resolved_opacity(view) returns the slide's opacity in one view, whether opacity is a number or an OpacityPerView.
Build a scene in code¶
from vexy_stax.scene import Camera, Caption, Scene, Size, Slide, Transition
scene = Scene(
size=Size(width=1920, height=1080),
camera=Camera(gap=1200, angle=50),
transition=Transition(kind="expand_collapse", easing="linear"),
slides=[
Slide(src="/abs/layer-0.png", caption=Caption(text="Background")),
Slide(src="/abs/layer-1.png", caption=Caption(text="Content")),
Slide(src="/abs/layer-2.png", opacity={"expanded": 1.0, "compact": 0.0}),
],
)
Paths in a scene built this way are not resolved; use absolute paths. Leave gap unset on a slide to inherit camera.gap; gap=0 gives MIN_GAP.
Save a scene¶
from pathlib import Path
Path("scene.json").write_text(scene.model_dump_json(by_alias=True, indent=2, exclude_none=True))
by_alias=True writes the $schema key under its JSON name. exclude_none=True leaves out unset optional fields, including per-slide gap: null, which would otherwise read back as "minimal gap".
vexy_stax.geometry¶
All functions take a Scene and return plain values. See Geometry for the formulas.
from vexy_stax import geometry as g
g.compact_camera(scene) # CameraPose
g.expanded_camera(scene) # CameraPose
g.compact_camera(scene, viewport_aspect=2.0)
g.plate_gaps(scene) # list[float]
g.stack_depth(scene, "expanded") # float
g.slide_lift(scene) # float
g.frame_plan(scene) # list[FrameState]
g.frame_plan(scene, first_hold=0, last_hold=0)
g.transition_frames(scene) # frames per leg
g.video_fps(scene) # int
g.video_dimensions(scene) # (width, height)
g.ease("easeInOutCubic", 0.5) # 0.5
g.interpolate_opacity(scene.slides[0], 0.5)
g.caption_opacities(scene, 0.75) # list[float]
g.caption_size(scene) # 1 em in points
g.caption_plate_height(scene)
g.caption_plate_center_y(scene)
g.caption_baseline_y(scene)
g.caption_anchor_x(scene)
g.caption_fill_color(scene) # hex string
g.caption_border_color(scene) # hex string
g.plate_edge_width(scene) # border thickness in points
g.default_font_path() # Path to vexy-stax.ttf, or None
vexy_stax.engines¶
from vexy_stax.engines import Engine, all_engines, available_engines, get_engine, is_available, register
| Function | Returns |
|---|---|
all_engines() |
every registered name: ['blender', 'playwright', 'pygfx'] |
available_engines() |
registered names whose dependency probe succeeds |
is_available(name) |
True if the probe for name succeeds |
get_engine(name) |
the engine instance; KeyError for an unknown name |
register(engine) |
registers an engine instance under engine.name |
render_image(scene, view, out) writes a PNG; render_video(scene, out) writes an MP4 and raises ValueError when the scene has no transition. See Engines.
vexy_stax.images¶
from vexy_stax.images import overlay_images, read_images
infos = read_images(["/abs/layer-0.png", "/abs/layer-1.png"])
# [{'path': ..., 'width': ..., 'height': ...}, ...]
overlay_images(infos, "flat.png")
overlay_images builds a canvas as large as the widest and tallest image, centres each image horizontally, aligns it to the bottom, and alpha-composites from index 0 (back) to the last (front). It is what vexy-stax overlay runs.
vexy_stax.juicy¶
Per-channel linear colour correction. It compares a flat Pillow overlay with a 3D stack render and corrects the render toward the overlay's colours.
from vexy_stax.juicy import apply_still, apply_video, compute_corrections
corrections = compute_corrections("flat.png", "stack.png") # (scale, offset) per channel, B, G, R order
apply_still("beauty.png", corrections) # rewrites the PNG in place
apply_video("morph.mp4", corrections) # re-encodes the video with ffmpeg
compute_corrections matches the mean and standard deviation of each channel inside the overlay's opaque area. apply_still also pushes near-white pixels to pure white.
The scene's juicy flag records the intent, but the render and video commands don't call these functions; call them yourself.
vexy_stax.showcase¶
run_showcase(...) renders a compact still, an expanded still and a short transition video of testdata/airbl.scene.json with each available engine into outputs/<engine>/, and checks that each still is not blank (pixel standard deviation above STD_GATE = 20.0). example.py in the repository root calls it. It is a development tool and expects a repository checkout.