an.genres#

Genres: what a kind of animation adds to the core, declared as one object.

ADR 0001 decisions 2-4 and the core study §2.15. A genre (cut-out animation, data visualisation, mathematical explainers, …) extends the core by registration, never by editing a core if kind == … chain. It is one plain, declarative Genre object — the shape shaping uses (“a new genre is one file”) — listing what it registers:

  • action kinds (ActionKind): a model, how it occupies time, how scene.md spells it;

  • entity kinds (EntityKind), each naming the property space its nodes live in, plus any new property spaces and field kinds for the timing kernel (an.timing);

  • semantic checks (SemanticCheck) that an validate runs;

  • md sugar (DialogueSugar) on scene.md dialogue lines;

  • capabilities and analysers (an.capabilities, ADR 0002): the capability names its methods require and the derivation of what its assets afford;

  • vocabulary entries — presets, methods, IR-field notes — and aspects with their default chains (an.semantic, ADR 0003).

Because the object is plain data, a genre is **inspectable before it is loaded**: available() reads every installed genre’s declaration without registering anything, which is how an unregistered kind’s error can name the package that provides it.

Discovery is explicit, never at import time (decision 3). Importing an registers no genre. load() reads the an.genres entry point group and registers each genre it finds; an.load(project), the an CLI and (later) the MCP entry call it, and anyone can. A package declares its genre as:

[project.entry-points."an.genres"]
cutout_animation = "cutan.genre:CUTOUT"

register_genre() installs a genre object directly (a test, a notebook, a genre defined in the same process).

>>> from an.genres import Genre, register_genre, installed, without_genres
>>> with without_genres():
...     installed()
()
>>> demo = Genre("demo_genre", title="Demo")
>>> with without_genres():
...     _ = register_genre(demo)
...     installed()
('demo_genre',)

Module Attributes

ENTRY_POINT_GROUP

The entry-point group a genre package declares its Genre under.

API_LEVEL

every hook, registry field and moved path a genre package may rely on.

IN_DISTRIBUTION_GENRES

Genres the an distribution itself ships, as entry-point values.

Functions

require_api_level(level, *, package)

Refuse, with an upgrade hint, when this an is older than package needs.

hook_modules(*[, exclude_owner])

The modules whose code the genres' registered hooks run, sorted and unique.

register_service(name, target, *[, owner, ...])

Register a named service a genre offers the core (an#225).

require_service(name, *[, what, extra])

The registered service name; a typed error naming the install when absent.

service(name[, default])

The registered service name, resolved, or default when none is.

services(prefix)

{name without the prefix: resolved service} for every service under prefix.

action_kind(name)

The registered kind called name, or None.

action_kind_names(*[, owner])

Registered action-kind names in registration order; owner's only when given.

available(*[, entry_points, builtin])

Every discoverable genre's declaration, by name — read, never registered.

entity_kind(name)

entity_kind_names(*[, owner])

entity_space_resolver(entities, *[, default])

target -> PropertySpace: the space its ENTITY's kind declares.

discovered_entry_points(*[, entry_points, ...])

The in-distribution genres (builtin; none since the cut-out genre moved to cutan, an#225) merged with entry_points (default: the installed ones), de-duplicated by name — the in-distribution declaration first.

genre_entry_points(*[, group])

The installed an.genres entry points (nothing is imported).

genres_declaring(test)

The installed genres (loaded or not) whose declaration passes test.

installed()

The names of the genres registered in this process, in registration order.

genre_library(name, *[, default])

The package whose data root holds genre name's library and projects.

installed_genre(name)

load(*[, entry_points, builtin])

Register every discoverable genre.

providers_of(kind, *[, registry])

The installed genres (loaded or not) that declare kind in registry (a key of Genre.provides()).

register_genre(genre, *[, replace, ...])

Register everything genre declares, owned by genre.name.

without_genres()

Run a block with no genre registered (the core alone), then restore.

Classes

ActionKind(name, model[, duration, flatten, ...])

One kind of action: its model, how it occupies time, how scene.md spells it.

CompilePass(name, run[, order, compiler, ...])

One step a genre adds to an engine's COMPILER (shot -> compiled document).

RuntimeScript(name, source[, engine, ...])

JavaScript a genre adds to an engine's RUNTIME (an#247; ADR 0001 decision 4, second batch): for the stage, code that registers visual kinds with window.anRegisterVisual(kind, make) -- how the cut-out mouth and eye leave runtime.js for cutan (P8).

SwapDeclaration(sets[, descriptor, ...])

What one entity's descriptor declares for the stage compiler's swap vocabulary.

DialogueSugar(name, opener, field, parse, format)

scene.md sugar on a dialogue line: one bracket pair, one Dialogue field (and, declared in fields, the parameters it may carry).

EntityKind(name[, space, store, ...])

One kind of entity (AssetRef.kind): what its nodes' properties are.

Genre(name[, title, description, package, ...])

A genre: one plain, declarative object listing what it registers.

SemanticCheck(name, run[, stage, order, ...])

One semantic-validation check: run(ctx) adds findings to ctx.report.

Exceptions

GenreAPILevelError

A genre package needs a newer an than the one installed.

ServiceMissingError

The core asked a genre for a service nobody registered.

GenreError

A genre declaration is malformed, or its entry point does not resolve.

RegistryError

A registration is malformed or collides with one already made.

UnregisteredKindError(what, name, *[, ...])

A document names a kind no loaded genre registered.

an.genres.API_LEVEL: int = 6#

every hook, registry field and moved path a genre package may rely on. Bumped by each change a genre needs (the P8 seam PRs and each move to cutan, an#225), never decremented. A genre states the lowest level it needs with require_api_level(), which is how “the genre declares the lowest an it supports” (ADR 0001 decision 8) is said without a version pin: an’s version is assigned by CI at merge, so no PR can name the release it ships in.

A genre package must also run against an an older than this constant: it reads getattr(an.genres, "API_LEVEL", 0) before calling require_api_level(), and raises its own typed error when it is missing.

Levels (one line each; tests/test_genre_gate.py holds the list complete):

1 = moved-module shims (an._shims.moved_to_package) and this check (an#296, P8 B0a). 2 = the cut-out genre’s move (an#225): Genre.services, ActionKind.lowering,

EntityKind.swap_declaration, an.stage.rig.

3 = the public rig builder (an#338): an.stage.rig.build_rig_subtree, rig_origin,

RigDocument (origin), omit_unset_rig_fields, an.stage.compile.note_raster_rig.

4 = the bones’ rest pose (an#339): an.stage.rig.register_rest_pose_migration,

rig_rest_problems, RigDocument.rest_rotation.

5 = nested chains (an#340): build_rig_subtree(skip_slots=), slot_parent_chain,

slot_node_paths, rig_problems, rig_affordances, RigError, rig.hierarchy.

6 = the rest pose’s protection said aloud (an#407): an.stage.rig.rest_pose_protection.

Type:

The level of the genre-facing API this an provides

class an.genres.ActionKind(name, model, duration=None, flatten=None, children=None, read_md=None, write_md=None, md_start=True, description='', version='1', lowering=None)[source]#

Bases: object

One kind of action: its model, how it occupies time, how scene.md spells it.

  • model validates a document’s action of this kind. A genre’s model subclasses an.ir.schema.ExtensionAction; the core’s are the static members of the schema’s union.

  • duration is a LEAF’s natural length; a leaf flattens to one FlatAction on [t, t + duration] and advances a sequence by it.

  • flatten replaces that default for kinds that place themselves differently (set does not advance the cursor) or that hold children (sequence): flatten(action, t, ctx) -> new cursor, where ctx is an.ir.compose.FlattenContext.

  • children lists a composite’s child actions, so generic walkers (validation) reach every leaf without knowing the kind.

  • read_md / write_md are the scene.md yaml actions hooks: read_md(item, index=i) -> action (start: already removed when md_start) and write_md(action) -> dict (without start). A kind with no read_md has no scene.md form.

lowering: Any = None#

How the STAGE compiler turns an action of this kind into clips, for a kind that is not a plain tween or set (the cut-out play): an object with extent_resolver(vocab, *, products), expand(flat_list, *, vocab, fps, step_hz, default_easing, resolutions, products), view_of(entity_swaps, vocab, *, duration) and clip(action, *, anim_id, vocab, fps, view) – see an.stage.compile.ActionLowering. None: the compiler has nothing kind-specific to do. (an#225: this is how the compiler stops naming play.)

md_start: bool = True#

Does start: in scene.md wrap this kind in sequence(delay(start), …)?

version: str = '1'#

bump when what an action of this kind compiles to changes for the same fields, so the shots that use it re-render visibly (an.semantic folds it into the shot digest).

Type:

The kind’s vocabulary version (ADR 0003)

class an.genres.CompilePass(name, run, order=0.0, compiler='stage', builds=None, description='', replace=False)[source]#

Bases: object

One step a genre adds to an engine’s COMPILER (shot -> compiled document).

A compiler (compiler: today "stage", an.stage.compile) runs its own passes plus every registered one, in order. A pass with builds set is an ENTITY BUILDER instead: the compiler’s scene pass calls it for each entity of that kind (the cut-out genre’s rig builds a character); builders with a lower order build all their entities first (the stage’s backdrop before the cast), equal orders in entity order.

run is the callable, or "module:function", resolved on first use – so a genre is inspectable (an.genres.available()) without importing the engine its passes target. What run receives is the compiler’s business (the stage hands a pass its CompileState, a builder the entity and the scene being built); the core never calls it.

replace: bool = False#

Explicitly take the place of the ENGINE’s own pass of this name (or its builder for builds). Without it a name or kind the engine already has is refused when the compiler runs; with it the replacement is recorded in the compiled document (review of an#270, S3).

resolve()[source]#

The callable run names.

Return type:

Callable[..., Any]

>>> CompilePass("p", "math:sqrt").resolve()(4.0)
2.0
class an.genres.DialogueSugar(name, opener, field, parse, format, description='', fields=())[source]#

Bases: object

scene.md sugar on a dialogue line: one bracket pair, one Dialogue field (and, declared in fields, the parameters it may carry).

parse(content) -> value turns what is between the brackets into the field’s value (raise ValueError(why) to refuse it); format(line) -> str | None is the inverse (the content, without brackets, or None when the line carries none). The cut-out genre’s [emotion] is one.

A sugar that also sets other Dialogue fields ([angry 0.4]: the emotion and its emotion_intensity, an#253) names them in fields; its parse then returns {field name: value} over field and any of fields, and format writes them back.

values(content)[source]#

{Dialogue field: value} for the bracket content (parse, checked against what the sugar declares).

Return type:

dict[str, Any]

an.genres.ENTRY_POINT_GROUP: str = 'an.genres'#

The entry-point group a genre package declares its Genre under.

class an.genres.EntityKind(name, space=None, store=None, description='', version='1', swap_declaration=None, descriptor_kind=None, placeholder_on_missing=False, swap_checks=None, specimen=None)[source]#

Bases: object

One kind of entity (AssetRef.kind): what its nodes’ properties are.

space names the registered an.timing.spaces.PropertySpace its nodes’ properties live in (None: the entity has no animatable nodes, as a voice); store is the project-mall store its ref keys into.

descriptor_kind: str | None = None#

The kind tag of the descriptor document store holds for this kind ("CharacterDescriptor"): what makes the validator treat the entity as a rig whose declared asset sets it can check (an#246).

placeholder_on_missing: bool = False#

A missing ref is NOT an error because the compiler draws a placeholder instead; the genre reports it with its own (warning) check.

specimen: Callable[[Any], Any] | None = None#

a short shot showing one entity of this kind on its own, the one ref casts — what an library sheet draws for a version of it (an#347). None: the sheet shows a labelled placeholder.

Type:

(ref: AssetRef) -> Shot

swap_checks: Any = None#

an object with missing_set_hint(prop) -> str (appended to “names no declared asset set”) and whole_entity(action, desc, prop, keys, entity_id, *, where, report, art_exists) -> bool (judge a swap on the entity ITSELF; True when handled). None: the generic per-node rule only.

Type:

Extra swap-reference checks for this kind

swap_declaration: Callable[[Any, Any], SwapDeclaration | None] | None = None#

what the entity’s descriptor declares for the stage compiler’s swap vocabulary (an#87). None: the kind declares nothing (its built nodes’ sets ARE its declaration).

Type:

(entity, mall) -> SwapDeclaration | None

version: str = '1'#

The kind’s vocabulary version (ADR 0003), as ActionKind.version.

class an.genres.Genre(name, title='', description='', package='', library='', action_kinds=(), entity_kinds=(), spaces=(), field_kinds=(), checks=(), dialogue_sugar=(), capabilities=(), analysers=(), vocabulary=(), aspects=(), compile_passes=(), runtime_scripts=(), services=<factory>)[source]#

Bases: object

A genre: one plain, declarative object listing what it registers.

name is the genre’s persisted slug (cutout_animation); it is also the owner of every entry it registers. package names the distribution that ships it, for error messages. Every collection defaults to empty, so a later phase adds a field (compile passes, vocabulary, capabilities) without touching any genre that does not use it.

spaces are an.timing.spaces.PropertySpace objects and field_kinds are (name, factory) pairs for an.timing.kinds.register_kind(). capabilities are an.capabilities.Capability objects, analysers an.capabilities.Analyser objects, vocabulary an.semantic.Entry objects (methods included) and aspects an.semantic.Aspect objects; registering the genre checks the whole (an.semantic.check_registry()): every aspect’s chain ends in a method that requires nothing, and every requirement names a registered capability.

library names the package whose data root holds the genre’s asset library and its projects (~/.local/share/<library>, ADR 0005, plan §1 decision 7); empty means the core’s (an). It is not package: the distribution that ships a genre and the root its data lives under can differ (the cut-out genre ships inside an today, while its library is already cutan’s). The core never names a genre’s library itself — a project made in a genre asks the genre (genre_library()).

compile_passes: tuple[CompilePass, ...] = ()#

Steps the genre adds to an engine’s compiler, and builders for its entity kinds (CompilePass; an#247).

provides()[source]#

What this genre registers, by registry, as names — without registering it.

Return type:

dict[str, tuple[str, ...]]

>>> Genre("g", action_kinds=()).provides()["action kinds"]
()
runtime_scripts: tuple[RuntimeScript, ...] = ()#

its visual kinds (RuntimeScript; an#247).

Type:

Code the genre adds to an engine’s runtime

services: dict[str, object]#

{name: object or "module:attr"} the genre offers the core by name (register_service(); an#225): the CLI namespaces it adds (cli.<namespace>), its lip-sync providers (lipsync.<name>), and the other places the core asks “is there a genre that does this?”.

exception an.genres.GenreAPILevelError[source]#

Bases: GenreError, ImportError

A genre package needs a newer an than the one installed.

exception an.genres.GenreError[source]#

Bases: RegistryError

A genre declaration is malformed, or its entry point does not resolve.

an.genres.IN_DISTRIBUTION_GENRES: tuple[tuple[str, str], ...] = ()#

Genres the an distribution itself ships, as entry-point values. They are found by load() WITHOUT relying on installed metadata: an editable install made before the an.genres group existed never refreshes its dist-info, and a missing entry point must never silently drop a genre that ships in the same distribution as the core (review-244 S1). Still explicit discovery (only load() reads it, never an import). Every genre is external now: the cut-out genre moved to cutan (an#225) and comes through the an.genres entry point alone.

exception an.genres.RegistryError[source]#

Bases: ValueError

A registration is malformed or collides with one already made.

class an.genres.RuntimeScript(name, source, engine='stage', description='')[source]#

Bases: object

JavaScript a genre adds to an engine’s RUNTIME (an#247; ADR 0001 decision 4, second batch): for the stage, code that registers visual kinds with window.anRegisterVisual(kind, make) – how the cut-out mouth and eye leave runtime.js for cutan (P8).

source is "package:relative/path.js", read with importlib.resources when the engine stages its runtime, so it ships in the genre’s wheel. The staged code is part of the shot cache’s key.

code()[source]#

The script’s code (UTF-8).

Return type:

str

>>> RuntimeScript("x", "an.stage.runtime:extensions.js").code().startswith("//")
True
class an.genres.SemanticCheck(name, run, stage='shot', order=0.0, description='')[source]#

Bases: object

One semantic-validation check: run(ctx) adds findings to ctx.report.

ctx is an.ir.validate.ValidationContext; a shot check is called once per shot with ctx.shot set. Within a stage, checks run by order (then registration order), so a genre’s check lands exactly where it belongs in the report.

exception an.genres.ServiceMissingError[source]#

Bases: RegistryError, ImportError

The core asked a genre for a service nobody registered.

class an.genres.SwapDeclaration(sets, descriptor=None, art_exists=None, scale=1.0)[source]#

Bases: object

What one entity’s descriptor declares for the stage compiler’s swap vocabulary.

sets is {set name: {KEY: attachment name}} as declared; descriptor is the (migrated) document the declaration came from, kept for the kind’s own lowering; art_exists answers rel_path -> art on disk (None when the store cannot say); scale is the factor from the entity’s view box to scene pixels.

exception an.genres.UnregisteredKindError(what, name, *, known=(), providers=(), where='')[source]#

Bases: RegistryError

A document names a kind no loaded genre registered.

The message names the installed genres that could provide it (read from their declarations, which are inspectable without loading them), so the fix is one line: an.genres.load(), or installing the package named.

an.genres.action_kind(name)[source]#

The registered kind called name, or None.

Return type:

ActionKind | None

an.genres.action_kind_names(*, owner=None)[source]#

Registered action-kind names in registration order; owner’s only when given.

Return type:

tuple[str, ...]

an.genres.available(*, entry_points=None, builtin=True)[source]#

Every discoverable genre’s declaration, by name — read, never registered.

The in-distribution genres plus the entry points (discovered_entry_points()). A genre whose entry point does not import is left out (its error is what load() raises).

Return type:

dict[str, Genre]

an.genres.discovered_entry_points(*, entry_points=None, builtin=True)[source]#

The in-distribution genres (builtin; none since the cut-out genre moved to cutan, an#225) merged with entry_points (default: the installed ones), de-duplicated by name — the in-distribution declaration first.

Return type:

tuple[EntryPoint, ...]

>>> [ep.name for ep in discovered_entry_points(entry_points=())]
[]
an.genres.entity_space_resolver(entities, *, default=None)[source]#

target -> PropertySpace: the space its ENTITY’s kind declares.

The ONE policy for “what are this target’s properties” (review-244 S7): an validate’s generic target check and the compiler’s keyframe check both use it. A target’s entity is its first path segment; a target with no entity (the stage camera’s root), or an entity whose kind is unregistered or declares no space, gets default — the timing default an.timing.spaces.DFLT_TIMELINE_SPACE when None.

The default EVALUATOR agrees since an#245: the stage’s compiled document records each entity whose kind declares another space (meta.entity_spaces), and timeline_from_compiled resolves by it.

>>> from an.ir.schema import AssetRef
>>> space_of = entity_space_resolver([AssetRef(kind="prop", id="lamp", store="props", ref="l")])
>>> space_of("lamp/shade").name, space_of("root").name
('stage.node', 'stage.node')
an.genres.genre_entry_points(*, group='an.genres')[source]#

The installed an.genres entry points (nothing is imported).

Return type:

tuple[EntryPoint, ...]

an.genres.genre_library(name, *, default='an')[source]#

The package whose data root holds genre name’s library and projects.

default (the core package) for no genre, an unknown one, or a genre that declares none. Reads the genres installed in this process (call load() first, as every entry point does).

Return type:

str

>>> genre_library(None)
'an'
an.genres.genres_declaring(test)[source]#

The installed genres (loaded or not) whose declaration passes test.

Each is named as genre (package). Reading declarations imports the genre modules but registers nothing.

Return type:

tuple[str, ...]

an.genres.hook_modules(*, exclude_owner='an')[source]#

The modules whose code the genres’ registered hooks run, sorted and unique.

Compile passes, lowerings, entity hooks, checks, services and runtime scripts of every owner but exclude_owner (the core’s own are walked from the renderer already). The shot cache’s code key starts its walk here for the packages outside an, so a change to a genre’s code changes the key (an#294): the genre is reached by REGISTRATION, not by an import from an.

Return type:

tuple[str, ...]

>>> isinstance(hook_modules(), tuple)
True
an.genres.installed()[source]#

The names of the genres registered in this process, in registration order.

Return type:

tuple[str, ...]

an.genres.load(*, entry_points=None, builtin=True)[source]#

Register every discoverable genre. Idempotent.

Discoverable: the genres the an distribution ships (IN_DISTRIBUTION_GENRES, unless builtin=False) and the an.genres entry points (entry_points replaces the installed ones — tests, a host that curates). Returns the names of the genres registered after the call. Never called at import time: the CLI, an.load(project) and the MCP entry call it, so a document naming a genre’s kind validates to that kind’s model.

Return type:

tuple[str, ...]

an.genres.providers_of(kind, *, registry='action kinds')[source]#

The installed genres (loaded or not) that declare kind in registry (a key of Genre.provides()). Used by the unregistered-kind errors.

Return type:

tuple[str, ...]

an.genres.register_genre(genre, *, replace=False, check_capabilities=True)[source]#

Register everything genre declares, owned by genre.name.

Idempotent for the same object: registering a genre that is already installed is a no-op, so load() can be called from every entry point. A different object under an installed name raises unless replace. All or nothing: a registration that fails part-way leaves no trace. check_capabilities=False defers the “every requirement names a registered capability” check to the caller (load() runs it once all genres are in, so a genre extending another loads in any order).

Return type:

Genre

an.genres.register_service(name, target, *, owner='an', replace=False)[source]#

Register a named service a genre offers the core (an#225).

The seam for the places where the core needs a genre’s code only when that genre is installed: a CLI namespace (cli.character), a provider factory (lipsync.offline), a licence lookup. target is the object itself or "module:attr" (imported on first use, so declaring a service imports no engine). The names a genre may use are the core’s contract, listed with their callers in misc/docs/architecture_as_built.md.

Return type:

Any

an.genres.require_api_level(level, *, package)[source]#

Refuse, with an upgrade hint, when this an is older than package needs.

A genre package calls it at import (require_api_level(3, package="cutan")).

Return type:

None

>>> require_api_level(1, package="demo")
>>> try:
...     require_api_level(API_LEVEL + 1, package="demo")
... except GenreAPILevelError as e:
...     print(str(e).startswith(f"demo needs an.genres API level {API_LEVEL + 1}"))
True
an.genres.require_service(name, *, what='', extra='cutout')[source]#

The registered service name; a typed error naming the install when absent.

Return type:

Any

an.genres.service(name, default=None)[source]#

The registered service name, resolved, or default when none is.

Return type:

Any

>>> service("no.such.service") is None
True
an.genres.services(prefix)[source]#

{name without the prefix: resolved service} for every service under prefix.

Return type:

dict[str, Any]

>>> services("no.such.")
{}
an.genres.without_genres()[source]#

Run a block with no genre registered (the core alone), then restore.

Return type:

Iterator[None]

>>> with without_genres():
...     action_kind("play") is None
True

Modules

registry

The core's open registries: action kinds, entity kinds, semantic checks, md sugar, compile passes.