an#
an — AI-driven structured animation.
Public API surface (curated). See an.ir for the Scene IR, an.adapters
for renderer plumbing, an.audio for TTS/lip-sync protocols, an.verify
for verification, and an.stores for the project mall.
>>> import an
>>> 'SceneIR' in an.__all__
True
Functions
|
Discrete property set at time |
|
Animate a property from |
|
Run children one after the other. |
|
Run all children at once. |
|
Start each action |
|
|
|
A copy of |
|
An empty span that consumes time. |
|
Repeat |
|
Walk a composition tree, emitting leaf actions with absolute times. |
|
Validate that |
|
Cross-field semantic checks. |
|
Parse the structured Markdown form of a scene into a SceneIR. |
|
Render a SceneIR back into the structured Markdown form. |
|
Create a fresh an project at |
|
Load an existing project. |
|
Persist a Project's current scene back to disk (md + json). |
|
Build the standard project mall over |
Return a per-tool status dict. |
Classes
|
Top-level Scene IR document. |
|
Scene metadata. |
|
Reference to an entry in a project store. |
|
A single rendered unit. |
|
One line of spoken dialogue. |
|
Camera state for a shot: a named move, or explicit keys. |
|
Pixel dimensions of the rendered output. |
|
A leaf action with its absolute start and end times. |
|
A loaded an project: directory + mall + current scene. |
- class an.AssetRef(**data)[source]#
Bases:
_IRModelReference to an entry in a project store.
The IR never inlines large assets. Instead it references them by store name + key, so the same character/voice/environment is reusable across scenes.
overrideslets a single shot tweak presentation without forking the asset.>>> AssetRef(kind="character", id="maya", store="characters", ref="maya-v1").id 'maya'
- kind: str#
it selected nothing (the compiler skipped it, nothing read the styles store) and the name belonged to the renderer selector. Art direction arrives as a StylePack (#112).
A
strin the schema, not aLiteral(ADR 0001 decision 2): the values are the REGISTERED entity kinds (an.genres) — the core’senvironment,propandvoice, a genre’scharacter— andan validatechecks it against that registry, naming the genre that provides an unregistered one.- Type:
"style"was retired in an#106
- library: str | None#
The asset-library version this entry was checked out from (ADR 0005 decision 8):
"[<library>:]<asset_id>@<version>", e.g."cutan:character.alice-reiniger@v003"(grammar:an.library.ids.parse_ref(), a pinned version required:vNNNorsha256:<prefix>, neverlatest).None— the default, and every document written before the library existed — means the asset is the project’s own. Today the strategy is check-out, sorefstill names the project-store key the compiler reads and this field is the pin beside it; live reference resolves it instead, later.Additive and omit-when-unset, like
stage: an unsetlibraryleaves no trace in a dump, so no stored scene changes and no schema version moves.
- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- stage: StagePlacement | None#
Where on the stage this entity stands.
None— the default and what every existing document has — means “wherever the layout puts it”, which for characters is the evenly-spaced row the compiler computes.Additive by construction, and hash-free by construction: the contract hashes the COMPILED document, and an
AssetRefnever reaches it. So this field can grow without retiring a single ledger row.
- class an.Camera(**data)[source]#
Bases:
_CameraShakesCamera state for a shot: a named move, or explicit keys.
>>> Camera(move="push_in").move 'push_in' >>> Camera(keys=[CameraKey(at=0.0), CameraKey(at=2.0, x=-200.0)]).keys[1].x -200.0
One code path, two front doors — the shape the dialogue
[emotion]sugar already uses. A namedmovedesugars to a key list;keysis that list written out. Setting both raises, because a scene that says two things about the same camera has no reading that is not a guess.keysdefaults toNone, not[], and that too is load-bearing: the markdown writer dumps the camera withexclude_none=True, which KEEPS empty lists — an empty default would writekeys: []into every camera block it regenerates.position,targetandfocal_lengthwere removed in an#109. They were written into everyscene.mdthis package ever generated and read by nothing; a registered migration drops them.
- class an.Dialogue(**data)[source]#
Bases:
_IRModelOne line of spoken dialogue.
startanddurationare None until the audio pipeline runs (TTS gives us a real duration); the pipeline stamps them then, derivingstartfrom the author’spause/at(planned_start()).- at: float | None#
Where this line starts, in SHOT seconds, whatever came before it —
(at 3.0)inscene.md.startis what the audio pipeline DERIVES fromat/pauseon every pass; these two are what the author wrote (an#187).
- direction: list[str] | None#
How the line is DELIVERED — cues such as
["excited"]or["sighs", "annoyed"],{excited}inscene.md(an#209). A TTS model that takes inline audio tags (ElevenLabs v3/v4) receives them as[excited] Hi!; others ignore them. Never part oftext, so captions and lip-sync alignment never see a cue.
- emotion_intensity: float | None#
maya [angry 0.4]: …inscene.md, a typed parameter on the (b-name) emotion.Noneis full strength, and is omitted from JSON.- Type:
How strongly
emotionshows, 0..1 (an#253)
- leveled: dict | None#
<the synthesized line’s audio_ref>, “gain_db”: <its voice’s gain>}`` when
audio_refis the leveled audio.None— unleveled — is omitted from JSON.- Type:
Stamped by voice leveling (an#315)
- Type:
``{“source”
- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- pause: float | None#
Seconds of silence before this line, after the previous line’s speech ends (the shot start, for the first line) —
(pause 1.5)inscene.md.
- planned_start(cursor)[source]#
Where this line starts, given the previous line ends at
cursor.The one rule the audio pipeline stamps into
startandan validatelays lines out by:atif set, elsecursor + pause. Astarton a line never synthesized (noaudio_ref) was authored — the spelling ofatbefore an#187 — and counts as one; a synthesized line’sstartis the pipeline’s own stamp, re-derived here.- Return type:
>>> Dialogue(speaker="a", text="bye", pause=1.5).planned_start(0.8) 2.3 >>> Dialogue(speaker="a", text="bye", at=4.0).planned_start(0.8) 4.0 >>> Dialogue(speaker="a", text="bye").planned_start(0.8) 0.8
- spoken: float | None#
seconds from the line’s start to the end of its AUDIBLE speech — its take’s own trailing silence left out — stamped with
durationfrom the line’s audio. The next line’spausecounts from here (a line with no pause still follows the whole take).None(silent audio, a line its voice’strim_silencealready cut to the tail the author keeps, a line stamped before an#397) counts from the end ofduration, as before. Never written to JSON: it is read off the line’s stored audio on every pass (the audio pipeline, and a render that skips synthesis), soscene.jsonand its fixtures do not change.- Type:
DERIVED (an#397)
- word_timings: list[WordTimingIR] | None#
The provider’s word timings, line-relative;
Nonewhen the provider has none (offline, Rhubarb) or the line was stamped before an#96.
- class an.FlatAction(start, end, action)[source]#
Bases:
objectA leaf action with its absolute start and end times.
The flat-form list is the canonical representation passed to renderers and verifiers. Composition nodes (sequence/parallel/delay/loop) do not appear in the flat form — they’re collapsed into time offsets.
- class an.Meta(**data)[source]#
Bases:
_IRModelScene metadata.
- captions: Captions | None#
Captions from the dialogue’s word timings (
Captions, an#175);None— the default — is none, omitted from JSON likestyle_pack.
- closing_transition: Transition | None#
a
fadeto itscolorover itsduration, on the last shot’s last frames (the film’s length is unchanged, and its final frame is the colour; the sound fades with it).None(or acut) ends on the last frame as drawn. A dissolve has nothing to dissolve into, so it is refused.- Type:
How the film ENDS (an#389)
- default_easing: str | tuple[float, float, float, float] | list[float] | None#
The easing every authored
tweenthat names none is drawn with (an#166) —"linear"for a snappy South Park cadence, an overshooting cubic-Bezier for a bouncy one. Precedence is **tween > this > the built-in"ease_in_out"``** (:meth:`TweenAction.resolved_easing`). ``None— the default and what every existing document has — changes nothing, and is omitted from JSON likestyle_pack, so no committed scene and no compiled document moves. It reaches authored tweens ONLY: a motion preset writes its own easings, the camera’s named moves supply theirs, and blinks,playclips and swap channels have none to inherit. There is no per-shot override yet — style is a scene’s.
- default_renderer: str#
a registered renderer’s name.
- Type:
Like
Shot.renderer
- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- sounds: list[SoundCue]#
Sound cues in FILM time — a music bed, an ambience under every shot (
SoundCue). Empty, the default, is no sound layer at all.
- step_hz: float | None#
Stepped timing for AUTHORED TWEENS, in pose updates per second;
None(the default) leaves every tween smooth. At 30 fps,15is “on twos” and10“on threes” — the character-animation practice Spider-Verse made famous (characters on twos, simulation on ones). The camera is exempt by construction, as are swap channels (already stepped by format), compiled blinks andplayclips: onlytweencurves are resampled — sample-and-hold of the eased curve on a SHOT-wide grid (every tween in a shot shares it; it restarts at a cut), not a retiming into holds and fast transitions. A tween’s own START and END are always pose changes too, so a tween that begins or ends off-grid changes pose on that frame as well as on the grid (asetat an off-gridatlikewise lands where it was authored). A shot’s ownstep_hzoverrides this. Must be positive (schema) and<= fps(validate + compile), an#89.
- style_pack: str | None#
The
StylePackin the project’sstylesstore this scene is drawn under, by key.None— the default and what every existing document has — leaves every colour exactly where it is, which is why adding this moved no corpus hash (an#112).A pack changes what the COMPILER decides: the character palette, the leg and pupil colours, the environment presets’ sky and ground — and SVG art whose descriptor tags its colours by role (
colour_roles, written byan character new). Untagged art is never inferred (inferring a role from a pixel is what produced an#99’s wrong-tone lid); a rig a pack cannot reach is WARNED about by name at compile.
- voice_loudness: VoiceLoudness | None#
One loudness for every voice (
VoiceLoudness, an#315);None— the default — levels nothing, and is omitted from JSON.
- class an.Project(root, mall, scene)[source]#
Bases:
objectA loaded an project: directory + mall + current scene.
- class an.SceneIR(**data)[source]#
Bases:
_IRModelTop-level Scene IR document. The SSOT.
A document is portable, diffable, and renderer-agnostic. Persisted as JSON at
ir/scene.jsoninside an an project.>>> from an.base import SCHEMA_VERSION >>> doc = SceneIR(meta=Meta(title="Hello")) >>> doc.version == SCHEMA_VERSION True >>> round_tripped = SceneIR.model_validate_json(doc.model_dump_json()) >>> round_tripped.meta.title 'Hello'
- class an.Shot(**data)[source]#
Bases:
_IRModelA single rendered unit. A scene is a sequence of shots.
A shot’s
rendererselects the backend that draws it. Every renderer must accept the same Shot fields; renderer-specific options go underoptions.- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- policy: dict[str, Any] | None#
per aspect, the methods it prefers in order, over the style pack’s (
StylePack.policy) and under an author’s request —{"locomotion": ["loco.glide"]}.None: the style’s.- Type:
This shot’s policy (an#348)
- renderer: str#
Which RENDERER draws this shot — not art direction. The field was called
styleuntil an#106, colliding with the styles store (which holds art direction) and withAssetRef(kind="style"); one word for two meanings is how a scene came to declare a “style” that selected a renderer while the thing that actually styles it went unread. Astrin the schema (ADR 0001 decision 2): any name a renderer registered (an.adapters.register_renderer), checked byan validate.cutoutstays the persisted default (decision 9).
- step_hz: float | None#
Per-shot override of
Meta.step_hz(None= inherit).
- transition: Transition | None#
How this shot is entered (
Transition);Noneis a hard cut.
- an.build_project_mall(project_dir, *, ensure=False, **overrides)[source]#
Build the standard project mall over
project_dir.Pass
ensure=Trueto create the per-store directories on disk if they don’t exist. Pass keyword overrides to swap in alternate stores (e.g. an in-memorydictfor tests).- Return type:
- an.check_requirements()[source]#
Return a per-tool status dict.
The CLI subcommand
an checkpretty-prints this. Programmatic callers can inspect the dict directly.
- an.crowd(id, *, ref, count, kind='prop', store=None, layout='grid', area=(-480.0, -60.0, 480.0, 240.0), scale=1.0, scale_jitter=0.0, jitter=0.0, seed=0)[source]#
countentities of one asset (kind/store/ref) placed inarea.id: the crowd’s name; member
kis<id>_<k>(member_id()) ref: the asset every member draws count: how many (at least 1) kind: the entity kind (prop, or a genre’s, such ascharacter) store: the store the asset lives in (default: the kind’s registered store) layout:grid(rows filled from the back),roworscatterarea:(x0, y0, x1, y1)in scene pixels about the stage centre; amember’s stage point stands inside it
scale: every member’s stage scale scale_jitter: each member’s scale varies by up to this fraction (
0.1: ±10%) jitter: each grid or row member moves by up to this fraction of its cell seed: the scatter and the jitters are drawn from it>>> [m.stage.at for m in crowd("line", ref="dot", count=3, layout="row", area=(0, 0, 300, 100))] [(50.0, 50.0), (150.0, 50.0), (250.0, 50.0)] >>> crowd("x", ref="dot", count=0) Traceback (most recent call last): ... ValueError: a crowd needs at least one member; got count=0
- an.fan_out(action, members, *, crowd_id=None)[source]#
A copy of
actionper member, its targets moved from the crowd to the member.- action: any action, leaf or composite; every target naming the crowd
(
army, or a node of it,army/arm_l) is rewritten
members: the crowd’s members (
AssetRefs or ids), in order crowd_id: the crowd’s name (default: read off the first member’s id,army_0->army)Pair it with
an.ir.compose.stagger()for a ripple through the ranks, orparallel()for the crowd moving as one.>>> from an.ir.compose import tween >>> [a.target for a in fan_out(tween("army/arm_l", "rotation", to=1.0, duration=0.5), ["army_0", "army_1"])] ['army_0/arm_l', 'army_1/arm_l']
- an.flatten(action, *, start=0.0, play_extent=None)[source]#
Walk a composition tree, emitting leaf actions with absolute times.
A
playwithoutdurationadvances asequencebyplay_extent(defaultdefault_play_extent()): its natural length, or zero for a looping animation, which runs to the shot end.Delays are absorbed into the timeline (they don’t appear in the output). Loops are unrolled by simple repetition — appropriate at v0.1; the cutout runtime can re-roll for efficiency later.
Every node is dispatched through its registered kind (
ActionKind), so a genre’s kind flattens without an edit here; a node whose kind nobody registered raises, naming the genre that provides it, and anExtensionActionread before its genre loaded is validated by the registered model on the way through.- Return type:
- an.init(project_dir, *, name=None, force=False)[source]#
Create a fresh an project at
project_dir.Idempotent unless the directory already contains a non-empty
scene.md; passforce=Trueto overwrite. Returns the absolute project root. The project’s.gitignoregainsPROJECT_GITIGNORE(lines it lacks).- Return type:
- an.ir_to_markdown(scene)[source]#
Render a SceneIR back into the structured Markdown form.
- Return type:
>>> from an.ir.schema import SceneIR, Meta, Shot >>> scene = SceneIR(meta=Meta(title="Demo", duration=5.0), ... timeline=[Shot(id="s1", renderer="cutout", duration=5.0)]) >>> md = ir_to_markdown(scene) >>> "# Demo" in md True >>> "## Shot s1 (cutout)" in md True
This writes the WHOLE document in the writer’s own formatting, and keeps no prose but
meta.notes. Updating an existingscene.mdgoes throughmerge_markdown(), which keeps the author’s text wherever the content did not change.
- an.load(project_dir, *, check_kinds=True)[source]#
Load an existing project. Reconciles scene.md / ir/scene.json first.
Registers the installed genres first (
an.genres.load(), ADR 0001 decision 3: discovery is explicit, and loading a project is one of the places it happens), so the scene’s genre kinds — the cut-out genre’splay,expressionandcharacter— read as their own models.Then refuses a scene that names an action kind, entity kind or renderer nothing registered (
an.ir.validate.require_registered_kinds()): the schema holds those asstr(ADR 0001 decision 2), so without this a typo’dkind: enviromentwould load and render silently without its backdrop.check_kinds=Falseis foran validate, which reports them as findings instead.- Return type:
- an.markdown_to_ir(md_text)[source]#
Parse the structured Markdown form of a scene into a SceneIR.
- Return type:
>>> md = '''# Demo ... ... ```yaml meta ... title: Demo ... duration: 5 ... ``` ... ... ## Shot s1 (cutout) ... ... ```yaml shot ... duration: 5 ... ``` ... ... ```dialogue ... charlie: hi ... ``` ... ''' >>> scene = markdown_to_ir(md) >>> scene.meta.title 'Demo' >>> scene.timeline[0].id 's1' >>> scene.timeline[0].dialogue[0].text 'hi'
- an.parallel(*actions)[source]#
Run all children at once. Total duration = max of child durations.
- Return type:
- an.sequence(*actions)[source]#
Run children one after the other. Total duration = sum of child durations.
- Return type:
- an.set_(target, property, value, *, at=0.0)[source]#
Discrete property set at time
at(relative to its enclosing scope).- Return type:
- an.stagger(lag, *actions)[source]#
Start each action
lagseconds after the previous one STARTS.The stagger (Manim’s
LaggedStart,previz’s compose, a crowd entering one by one): the children run in parallel, thei-th delayed byi * lag. It is authoring sugar, not a new kind — it builds theparallelofsequence(delay(i * lag), action)it means, so the scene document,scene.mdand every renderer see only core kinds. Total duration: the latest child’s end.scene.mdholds it verbatim (akind: parallelentry), so it round-trips. (an.stage.text.reveal_units—an.stage.text.staggerbefore an#241 — is the text-block preset: a LIST of per-unit actions with holds, not a combinator.)- Return type:
>>> flat = flatten(stagger(0.25, tween("a", "x", to=1.0, duration=1.0), ... tween("b", "x", to=1.0, duration=1.0), ... tween("c", "x", to=1.0, duration=1.0))) >>> [(f.action.target, f.start, f.end) for f in flat] [('a', 0.0, 1.0), ('b', 0.25, 1.25), ('c', 0.5, 1.5)] >>> duration_of(stagger(0.5, delay(1.0), delay(1.0))) 1.5 >>> stagger(0.1).children [] >>> stagger(-1.0, delay(1.0)) Traceback (most recent call last): ... ValueError: stagger lag must be >= 0, got -1.0
- an.tween(target, property, to, duration, *, from_=None, easing=INHERIT)[source]#
Animate a property from
from_(or its current value) toto.easingleft out inherits the scene’smeta.default_easing(else"ease_in_out"); naming one —"ease_in_out"included — pins it.- Return type:
>>> "easing" in tween("a", "x", to=1.0, duration=1.0).model_fields_set False >>> tween("a", "x", to=1.0, duration=1.0, easing="linear").easing 'linear'
- an.validate_schema(doc)[source]#
Validate that
doc(dict, JSON string, or SceneIR) conforms to the schema.- Return type:
>>> validate_schema({"meta": {"title": "x"}, "timeline": []}).passed True >>> r = validate_schema({"meta": {"title": "x"}, "timeline": [{"id": "s", "duration": "not-a-number"}]}) >>> r.passed False
- an.validate_semantic(scene, *, available_voices=None, available_characters=None, available_props=None, available_environments=None, available_sounds=None, available_library_lock=None, available_styles=None, only=None, fps=None)[source]#
Cross-field semantic checks. Pass live stores in for cross-store checks.
Both
available_voicesandavailable_charactersaccept any mapping;available_propsis the same thing forkind="prop"entities (an#108), andavailable_environmentslets the flat-pan warning read a stage’s planes (an#111) — without it a prop’s swaps are reported as having no descriptor, which is validate refusing what compile accepts. Voices are consulted via__contains__only; characters additionally via__getitem__(the swap-reference andplaychecks read descriptor dicts, an#87 / an#7). PassNoneto skip those checks — and know that skipping them is what it sounds like: aplayor a swap the compiler will refuse passes silently without the store (the CLI,an validate, always passes it).available_stylesis the project’sstylesstore: a check reads the StylePack the scene names (meta.style_pack) fromctx.stores["styles"](a style’spolicy, an#348).available_library_lockis the project’s asset-library lockfile (mall["library_lock"]): with it, every scenelibrary:pin is checked against the lockfile (warningon disagreement) and every pinned check-out against its library version (infowhen it has been edited).The checks are a REGISTRY (
an.genres.registry.register_check()): the core’s own register below, a genre’s when it is loaded (the cut-out genre’splay,expression, turn and view checks), and they run in stages —scene, thenshotonce per shot, thenfinish— each by itsorder. An action or entity kind no loaded genre registered is one error naming the genre that provides it; checks that would trip over it skip that shot rather than crash.onlyruns just the registered checks of those names (whatan renderdoes after synthesis,post_synthesis_findings());fpsis the one the film is assembled at when it is not the scene’s (an render --fps), which decides how long a dissolve’s overlap is.- Return type:
Modules
Renderer adapters — facades over backends (the stage, Manim, Remotion, whiteboard). |
|
Film assembly: rendered shots → one film, with transitions and a sound layer. |
|
Audio pipeline — TTS and lip-sync providers + orchestration. |
|
Core types, constants, and re-exports for an. |
|
|
|
Incremental re-processing: content-addressed build stages (ADR 0004). |
|
Capabilities: what an asset, an engine or the environment affords, and the one matcher. |
|
Captions from the word timings the audio pipeline already computes (an#175). |
|
Collection rules for the package's own doctests. |
|
What a rendered video owes, and to whom. |
|
Bundled non-Python resources (cutout JS runtime, etc.). |
|
The determinism perimeter: what must stay true for a render to be reproducible. |
|
Engines: seekable things the core drives frame by frame, and the renderer that drives them. |
|
Moved to |
|
Numbers as text: a declared subset of d3-format, for counters (an#342). |
|
The frame clock: WHEN each output frame samples scene time. |
|
Genres: what a kind of animation adds to the core, declared as one object. |
|
Scene IR — the single source of truth for a scene. |
|
Iterative edit loop — free-text instruction → IR patch via Claude → re-render. |
|
The asset library: reusable assets that outlive their videos (ADR 0005). |
|
The one switch that says "yes, this run may spend money". |
|
The |
|
Measured durations: shots whose renderer, not their author, decides their length. |
|
Frames to deliverables, engine-independent: the frame stage's resolves and the sinks. |
|
Motion presets: a named vocabulary of moves, as authoring macros. |
|
Orchestrator: validate → audio → render → verify. |
|
Paint: what fills a shape when one flat colour is not enough -- a gradient. |
|
Moved to |
|
Moved to |
|
|
|
Project init/load/save — the on-disk anatomy of an an project. |
|
Moved to |
|
|
|
Moved to |
|
Project-level rendering: per-shot mp4 → final composited mp4 via ffmpeg concat. |
|
The semantic layer: one versioned vocabulary registry, methods, aspects and the matcher. |
|
Sound cues timed by the picture (an#317): |
|
Add a sound from a URL, with its provenance and the cut taken (an#318). |
|
Sound assets: what the sound layer plays, where it came from, and a synthesizer. |
|
|
|
The 2D stage: the default engine, shipped with |
|
Project mall: a dict of dol-backed |
|
StylePack: art direction as a document, and the first reader the styles store has had. |
|
Moved to |
|
The timing kernel: what is on screen at time |
|
User-facing utility functions, plus the SSOT list for CLI dispatch. |
|
Internal helpers: file I/O, hashing, time arithmetic, light path utilities. |
|
Verification protocol — same interface for human, lint, vision-LM, MoVer. |