an.ir.schema#
Pydantic v2 models for the Scene IR.
Design principles (locked in from the architectural plan):
Renderer-agnostic. No cutout-specific or Manim-specific fields here. Backend adapters compile shots into their own internal formats. This module only knows what every shot has in common.
Versioned envelope. Every IR document carries
versionandcompatible_version. Migrations live inan.ir.migrate.Forward-compatible reads. Top-level model has
extra="allow"so a future field doesn’t crash an older reader.**Open
Actionunion** (ADR 0001 decision 2). The union holds the core kinds (set,tween,sequence,parallel,delay,loop) and ONE open member,ExtensionAction, which a callable discriminator selects for any otherkind. A genre registers its kinds (an.genres); a document’skind: playthen validates to the registered model, and before registration it stays anExtensionActionthat round-trips untouched and that validate, flatten and the compiler refuse by name. The union is never rebuilt at registration.Time in seconds (float). Always.
Doctest:
>>> from an.ir.schema import SceneIR, Meta, Shot
>>> scene = SceneIR(
... meta=Meta(title="Park Bench", duration=45.0),
... timeline=[Shot(id="s1", renderer="cutout", duration=45.0)],
... )
>>> from an.base import SCHEMA_VERSION
>>> scene.version == SCHEMA_VERSION
True
>>> scene.kind
'SceneIR'
>>> scene.timeline[0].renderer
'cutout'
Module Attributes
how long a jolt lasts, how far the frame jumps (a fraction of the frame HEIGHT, so it reads the same at any resolution: 0.015 is about 11 px at 720p), and how many times a second it jumps. |
|
The |
|
The union tag of the open member. |
|
the core kinds plus |
|
the broadcast convention (BBC / Netflix timed-text guidance: 42 characters, two lines). |
|
Caption type size as a fraction of frame height — a little under the title default, as captions are read while something else is watched. |
|
The property space a 2D stage engine's node lives in ( |
|
How long a specimen shot runs ( |
|
The id of a specimen shot. |
Functions
|
The stepped-timing policy |
|
A short shot showing the one stage entity |
|
The error for an action |
Classes
|
Reference to an entry in a project store. |
|
Camera state for a shot: a named move, or explicit keys. |
|
A camera that follows an entity (an#445): the frame keeps |
|
One camera pose at one time — the explicit door behind the named moves. |
|
A jolt of the frame inside a shot (an#429): seeded screen-space jitter. |
|
Captions for the whole film, built from the dialogue's word timings. |
|
A sound cue's time, taken from the picture (an#317): when a node reaches a frame row or column. |
|
Where a sound cue ends: at another cue's start, plus |
|
Composition: an empty span that consumes time. |
|
One line of spoken dialogue. |
|
An action of a kind the core does not define: the IR's one open member. |
|
Composition: repeat |
|
Scene metadata. |
|
Off-screen narration. |
|
Composition: run all children simultaneously starting at the same time. |
|
Pixel dimensions of the rendered output. |
|
Top-level Scene IR document. |
|
Composition: run children one after the other. |
|
Set a property to a value at a specific time. |
|
A single rendered unit. |
|
One sound placed on the timeline: an SFX hit, an ambience, a music bed. |
|
Where an entity stands on the stage, and how big it is. |
|
How a shot is ENTERED — from the previous shot, or (for the first shot) from nothing. |
|
Animate a property from a start value to an end value over a duration. |
|
A single mouth-shape keyframe in a viseme track. |
|
Aligned viseme track produced by the lip-sync stage. |
|
One loudness for every voice of the film (an#315). |
|
One word of a line and when it was spoken, in seconds from the line's start (like |
- an.ir.schema.Action#
the core kinds plus
ExtensionActionfor any otherkind(a registered genre kind validates to its own model through it).SerializeAsAnymakes a typed genre instance (aPlayAction) serialize with its own fields rather than the open member’s.- Type:
Every action
alias of
Annotated[Annotated[SetAction,Tag(tag=set)] |Annotated[TweenAction,Tag(tag=tween)] |Annotated[SequenceAction,Tag(tag=sequence)] |Annotated[ParallelAction,Tag(tag=parallel)] |Annotated[DelayAction,Tag(tag=delay)] |Annotated[LoopAction,Tag(tag=loop)] |Annotated[ExtensionAction,SerializeAsAny(),Tag(tag=extension)],Discriminator(discriminator=_action_tag, custom_error_type=None, custom_error_message=None, custom_error_context=None)]
- class an.ir.schema.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.
- an.ir.schema.CORE_ACTION_KINDS: tuple[str, ...] = ('set', 'tween', 'sequence', 'parallel', 'delay', 'loop')#
The
kindof every action the core defines (the static union members).
- class an.ir.schema.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.ir.schema.CameraFollow(**data)[source]#
Bases:
_IRModelA camera that follows an entity (an#445): the frame keeps
targetwhere it stood at the shot’s start.>>> CameraFollow(target="bob").axes 'x'
targetis an entity id or one of its nodes (bob/head);axesis which way the camera tracks it (x: a side-scrolling walk,xy: both). Resolved at compile time from the compiled motion (the target’s position at every frame), so planes parallax with it like any camera move.
- class an.ir.schema.CameraKey(**data)[source]#
Bases:
_IRModelOne camera pose at one time — the explicit door behind the named moves.
>>> CameraKey(at=1.0, x=-160.0, zoom=1.25).x -160.0
Sign convention, stated because every surveyed tool disagrees:
+xmoves the CAMERA right, which moves the content left.zoomis on-screen magnification, so1.25means “everything 25% bigger”, and it composes through the pivot — a push-in during a pan zooms toward what the camera is looking at rather than toward a fixed frame centre.rotationis camera roll in radians.easingdefaults to `None`, not `”ease_in_out”`, and that is not a style choice: today’s emitter puts"ease_in_out"on the first keyframe andnullon the terminal one, so a per-key default of"ease_in_out"would put it on both and move every camera scene’s contract hash. The named moves supply the easing they have always supplied.
- class an.ir.schema.CameraShake(**data)[source]#
Bases:
_IRModelA jolt of the frame inside a shot (an#429): seeded screen-space jitter.
>>> CameraShake(at=1.0).duration 0.4
Layered on whatever the camera does (a
move,keys, or nothing): the pan and zoom are the root’s pivot and scale, a shake is its screen position, so the two add without either knowing the other. The jitter is deterministic (seed) and, withdecay, falls linearly to rest by the end; the frame is exactly at rest beforeatand after it.- decay: bool#
Fall linearly to rest over the duration (an impact); off, it holds its amplitude to the end (a rumble).
- class an.ir.schema.Captions(**data)[source]#
Bases:
_IRModelCaptions for the whole film, built from the dialogue’s word timings.
Present = on;
meta.captionsunset (the default) is no captions and no trace in any document. One cue list (an.captions.caption_pages()) feeds BOTH outputs, so the picture and the sidecar cannot disagree:burn: each page is drawn as an overlay text block (camera-immune, placed atanchorin the title-safe area), shown for exactly the frames the sidecar says;sidecar: a SubRip.srtwritten through thecaptionsstore, next to the delivered mp4, in FILM time (a dissolve shortens the film, and every later cue moves with it).
>>> Captions().max_chars, Captions().anchor (42, 'bottom') >>> Captions(highlight="#ffcc00").highlight '#ffcc00'
- font: str | None#
None= the embedded face; else an ABSOLUTE font file path, or one relative to the project directory.
- highlight: str | None#
the word being spoken is drawn in this colour (karaoke);
Nonedraws every word incolor.- Type:
#rrggbb
- max_chars: int#
Line breaks are made HERE, by character count, and written into both the burned block and the sidecar — the same lines in both. 42 fits the title-safe width of a 16:9 or 4:3 frame at the default size; a square or portrait frame needs fewer (about 32 at 1:1) or a smaller
size— a line that does not fit is REFUSED before the render, never clipped.
- class an.ir.schema.CueAnchor(**data)[source]#
Bases:
_IRModelA sound cue’s time, taken from the picture (an#317): when a node reaches a frame row or column.
whenis a node path in a stage shot (crawl/line_7: a text block’s unit, a prop, a plane);reachesis{"y": px}or{"x": px}in the frame’s pixels (0 at the top or left), crossed in either direction by the node’s on-screen centre — the camera, parallax and a crawl’s tilt included. Resolved when the film is laid out, never written back into the scene: a layout change moves the sound with the picture.offsetis added after.shotnames the shot to look in, for a cue inmeta.sounds(default: the first shot that has the node).>>> CueAnchor(when="crawl/line_7", reaches={"y": 840}).axis ('y', 840.0)
- class an.ir.schema.CueUntil(**data)[source]#
Bases:
_IRModelWhere a sound cue ends: at another cue’s start, plus
offset(an#317).cueis thesoundkey of a cue in the same list (the shot’s, ormeta.sounds); the first such cue is meant.
- an.ir.schema.DEFAULT_CAPTION_MAX_CHARS: int = 42#
the broadcast convention (BBC / Netflix timed-text guidance: 42 characters, two lines).
- Type:
Characters per caption line and lines per caption page
- an.ir.schema.DEFAULT_CAPTION_SIZE: float = 0.05#
Caption type size as a fraction of frame height — a little under the title default, as captions are read while something else is watched.
- an.ir.schema.DFLT_SHAKE_DURATION: float = 0.4#
how long a jolt lasts, how far the frame jumps (a fraction of the frame HEIGHT, so it reads the same at any resolution: 0.015 is about 11 px at 720p), and how many times a second it jumps. Art direction, chosen to read as an impact rather than a tremor.
- Type:
A camera shake’s defaults (an#429)
- class an.ir.schema.DelayAction(**data)[source]#
Bases:
_ActionBaseComposition: an empty span that consumes time.
- class an.ir.schema.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.ir.schema.ExtensionAction(**data)[source]#
Bases:
_ActionBaseAn action of a kind the core does not define: the IR’s one open member.
The schema’s
Actionunion selects this for anykindother than the core’s (ADR 0001 decision 2). When a genre has REGISTERED that kind (an.genres.registry.register_action_kind()), validating a document yields the registered model instead — aPlayActionforkind: play— so code downstream sees typed actions. Before registration the action stays anExtensionAction: its fields are kept as extras and round-trip byte for byte, andresolved()(called byflatten,an validateand the compiler) refuses it naming the genre that provides it.Every genre’s action model subclasses this, which is what lets a typed instance sit in the union and serialize with its own fields.
>>> ExtensionAction(kind="wave", target="flag").model_dump() {'name': None, 'kind': 'wave', 'target': 'flag'}
- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- resolved(*, strict=True)[source]#
This action as its registered model.
A typed instance is returned as is. A bare
ExtensionActionis validated by the model its kind registered — or, unregistered, raisesUnregisteredKindError(strict) or comes back unchanged (strict=False).- Return type:
- class an.ir.schema.LoopAction(**data)[source]#
Bases:
_ActionBaseComposition: repeat
childcounttimes.
- class an.ir.schema.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.ir.schema.Narration(**data)[source]#
Bases:
_IRModelOff-screen narration. Same shape as Dialogue minus the speaker pin.
- class an.ir.schema.ParallelAction(**data)[source]#
Bases:
_ActionBaseComposition: run all children simultaneously starting at the same time.
- class an.ir.schema.Resolution(**data)[source]#
Bases:
_IRModelPixel dimensions of the rendered output.
- an.ir.schema.SPECIMEN_DURATION: float = 0.5#
How long a specimen shot runs (
an library sheetdraws its first frame).
- an.ir.schema.STAGE_NODE_SPACE: str = 'stage.node'#
The property space a 2D stage engine’s node lives in (
an.timing.spaces).
- class an.ir.schema.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.ir.schema.SequenceAction(**data)[source]#
Bases:
_ActionBaseComposition: run children one after the other.
- class an.ir.schema.SetAction(**data)[source]#
Bases:
_ActionBaseSet a property to a value at a specific time. Discrete, no tween.
- class an.ir.schema.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.
- class an.ir.schema.SoundCue(**data)[source]#
Bases:
_IRModelOne sound placed on the timeline: an SFX hit, an ambience, a music bed.
soundis a key in the project’ssoundsstore (an.sounds), where the bytes and their licence live — the IR never inlines audio.atis seconds from the start of whatever holds the cue: a shot’ssoundsare SHOT-local (they move with the shot, across transitions and re-orderings),meta.soundsare FILM time (a music bed under the whole thing).>>> SoundCue(sound="hit", at=1.2).gain_db 0.0 >>> SoundCue(sound="bed", loop=True, duck_db=-12).duck_db -12.0
- at: Annotated[float, FieldInfo(annotation=NoneType, required=True, metadata=[Ge(ge=0)])] | CueAnchor#
Seconds, or a
CueAnchorresolved from the picture when the film is laid out (an#317).
- duck_db: float | None#
Attenuation, in dB, while any dialogue line plays;
Nonenever ducks. A music bed usually wantsDEFAULT_DUCK_DB; an SFX hit wants none.
- duration: float | None#
the asset’s own length, or — when
loop— to the end of its shot (shot cue) or of the film (meta cue).- Type:
How long it plays.
None
- class an.ir.schema.StagePlacement(**data)[source]#
Bases:
_IRModelWhere an entity stands on the stage, and how big it is.
>>> StagePlacement(at=(120.0, -40.0), scale=0.5).at (120.0, -40.0) >>> StagePlacement().at is None True
atis in SCENE pixels relative to the stage centre — the same space the compiler already places characters in — so an author reads it off the same ruler as a camera pivot.scalemultiplies the rig’s own uniform scale.`after` landed in an#344 (a studio set’s sky disc between a sky plane and a holed wall plate): it names the anchor the entity is drawn right after.
depthis still not a field: an entity takes the depth of the plane it is placed after. The paragraphs below are the record of why both were deferred, kept becausedepth’s reason still holds.**Deliberately only two fields, and the reason has been re-stated because the first one expired.** #108 sketched
depthandafteras well, deferred on the grounds that they belong to a stage vocabulary that had not arrived. It arrived the same day: #109 landed the translating camera and #110 landed plane environments, and nothing revisited this paragraph — a deferral citing a condition outlives the condition silently, which is why the reason below cites a state instead (an#126).The current reason is simply that **nothing reads them and nothing has asked to.** Shipping either now would put a knob in the schema that a scene can set, that renders identically, and that says nothing — which is worse than an absent one, and is the same defect
an.styles.UNREACHABLE_ROLESrefuses by construction on the other side of the compiler.What each would cost, so the next reader does not re-derive it:
depthwould place a prop on a parallax plane, which means parenting the prop into that plane’s subtree — and_track_root_ofmakes entity identity the first path segment, so every animation target on that prop changes shape.afterwould order a prop against planes, which ischaracters_after’s problem a second time; that one needed the environment and character builders interleaved before any ordering could be expressed at all.
Neither is hard. Both are unmotivated, and an unmotivated knob in a versioned schema is a migration you owe later for a feature nobody used.
- after: str | None#
an environment plane (
"set/skyline") or another placed entity ("grid").None= the default band, where the environment’scharacters_aftercuts. A plane’s parallax carries an entity placed after it; an entity placed after an entity rides the default depth.- Type:
What this entity is drawn immediately after (an#344)
- at: tuple[Annotated[float, FieldInfo(annotation=NoneType, required=True, metadata=[AllowInfNan(allow_inf_nan=False)])], Annotated[float, FieldInfo(annotation=NoneType, required=True, metadata=[AllowInfNan(allow_inf_nan=False)])]] | None#
(x, y)in scene pixels from the stage centre.None= default layout.allow_inf_nan=Falseon both fields, and it is not pedantry: pydantic serializesinfandnanto JSON null, and re-validating that JSON raises — so a scene file written with either is corrupt one way, and the author finds out on the next load rather than at the edit that did it (an#108 review, M-1).gt=0already refusesscale=0andscale=-1; it does not refuseinf.
- class an.ir.schema.Transition(**data)[source]#
Bases:
_IRModelHow a shot is ENTERED — from the previous shot, or (for the first shot) from nothing.
>>> Transition(kind="dissolve", duration=0.5).duration 0.5 >>> Transition(kind="fade").color '#000000'
cut— the default, and what a shot with notransitionmeans.fade— throughcolor: the previous shot’s lastduration / 2fades to the colour and this shot’s firstduration / 2fades up from it. On the FIRST shot the wholedurationis a fade up from the colour. Holds the film’s length: nothing overlaps.dissolve— the previous shot’s lastdurationseconds and this shot’s firstdurationseconds are seen through each other. **The film getsdurationshorter** than the sum of its shots: both shots play in full, overlapped (the editor’s convention — the overlapped seconds are each shot’s “handle”). Dialogue stays in sync with its own shot’s picture; audio from both shots is heard in the overlap. Not allowed on the first shot.wipe— overlaps like a dissolve, but a hard edge sweeps across the frame indirection(the way the edge travels:leftbrings this shot in from the right), this shot on the side it has passed (an#390). The edge stands where a dissolve’s mix would:(j + 1) / (k + 1)of the way across on overlap framejofk. Not on the first shot.
- class an.ir.schema.TweenAction(**data)[source]#
Bases:
_ActionBaseAnimate a property from a start value to an end value over a duration.
- easing: str | tuple[float, float, float, float] | list[float] | None#
a tween that does not name an easing takes the scene’s
Meta.default_easing, and only when that is unset too the built-in"ease_in_out"this default spells. “Unset” is"easing" not in model_fields_set— the default stays the literal so every reader of.easingstill sees the curve a scene without a default draws — and the serializer below omits an unset easing, so the distinction survivesscene.json.Noneis an explicit LINEAR ramp (the evaluators’ reading of a null easing), not “unset”.
- model_config: ClassVar[ConfigDict] = {'extra': 'allow', 'populate_by_name': True}#
Configuration for the model, should be a dictionary conforming to [
ConfigDict][pydantic.config.ConfigDict].
- resolved_easing(default=None)[source]#
The easing this tween draws with under a scene default of
default— the ONE statement of the precedence **tween > scene (Meta.default_easing) > built-in ``”ease_in_out”``** (an#166).>>> TweenAction(target="a", property="x", to_value=1).resolved_easing("linear") 'linear' >>> TweenAction(target="a", property="x", to_value=1, easing="ease_in_out").resolved_easing("linear") 'ease_in_out' >>> TweenAction(target="a", property="x", to_value=1).resolved_easing(None) 'ease_in_out'
- Return type:
- class an.ir.schema.VisemeKeyframe(**data)[source]#
Bases:
_IRModelA single mouth-shape keyframe in a viseme track.
- class an.ir.schema.VisemeTrack(**data)[source]#
Bases:
_IRModelAligned viseme track produced by the lip-sync stage. Optional in P1.
- class an.ir.schema.VoiceLoudness(**data)[source]#
Bases:
_IRModelOne loudness for every voice of the film (an#315).
TTS voices arrive at very different levels (a 19 dB spread measured on one episode). Set, the render levels each voice: its integrated loudness over ALL its lines (EBU R128), one gain to
target_lufs(plus the voice document’sloudness_offset_db), peaks held atpeak_dbdBFS by a lookahead limiter — derived audio, content-keyed, never re-synthesised (an.audio.loudness). Unset, the default, levels nothing.>>> VoiceLoudness().target_lufs, VoiceLoudness().peak_db (-16.0, -1.5) >>> VoiceLoudness.model_validate(-20).target_lufs # a bare number is the target -20.0
- class an.ir.schema.WordTimingIR(**data)[source]#
Bases:
_IRModelOne word of a line and when it was spoken, in seconds from the line’s start (like
VisemeKeyframe, never absolute). Stamped by the audio pipeline from the provider’s word timings when it has them (an#96); JSON only —scene.mdnever carries it, the way it never carries visemes.
- an.ir.schema.resolve_step_hz(shot, scene_step_hz)[source]#
The stepped-timing policy
shotrenders under: its ownstep_hzwhen it declares one, else the scene’s, elseNone(smooth). The ONE statement of the shot-over-scene rule — the cutout renderer, the preview and the project renderer all call it (an#89 review: three copies).>>> resolve_step_hz(Shot(id="s", step_hz=10.0), 15.0) 10.0 >>> resolve_step_hz(Shot(id="s"), 15.0) 15.0 >>> resolve_step_hz(Shot(id="s"), None) is None True
- an.ir.schema.stage_specimen(ref)[source]#
A short shot showing the one stage entity
refcasts, on its own (an#347).- Return type:
>>> stage_specimen(AssetRef(kind="prop", id="lamp", store="props", ref="lamp")).entities[0].id 'lamp'
- an.ir.schema.unregistered_action_kind(kind, *, where='')[source]#
The error for an action
kindno loaded genre registered.It names the installed genres whose declaration provides the kind, read without loading them (
an.genres.providers_of()).- Return type: