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, howscene.mdspells 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) thatan validateruns;md sugar (
DialogueSugar) onscene.mddialogue 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
The entry-point group a genre package declares its |
|
every hook, registry field and moved path a genre package may rely on. |
|
Genres the |
Functions
|
Refuse, with an upgrade hint, when this |
|
The modules whose code the genres' registered hooks run, sorted and unique. |
|
Register a named service a genre offers the core (an#225). |
|
The registered service |
|
The registered service |
|
|
|
The registered kind called |
|
Registered action-kind names in registration order; |
|
Every discoverable genre's declaration, by name — read, never registered. |
|
|
|
|
|
|
|
The in-distribution genres ( |
|
The installed |
|
The installed genres (loaded or not) whose declaration passes |
The names of the genres registered in this process, in registration order. |
|
|
The package whose data root holds genre |
|
|
|
Register every discoverable genre. |
|
The installed genres (loaded or not) that declare |
|
Register everything |
Run a block with no genre registered (the core alone), then restore. |
Classes
|
One kind of action: its model, how it occupies time, how |
|
One step a genre adds to an engine's COMPILER (shot -> compiled document). |
|
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 |
|
What one entity's descriptor declares for the stage compiler's swap vocabulary. |
|
|
|
One kind of entity ( |
|
A genre: one plain, declarative object listing what it registers. |
|
One semantic-validation check: |
Exceptions
A genre package needs a newer |
|
The core asked a genre for a service nobody registered. |
|
A genre declaration is malformed, or its entry point does not resolve. |
|
A registration is malformed or collides with one already made. |
|
|
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 withrequire_api_level(), which is how “the genre declares the lowestanit 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
anolder than this constant: it readsgetattr(an.genres, "API_LEVEL", 0)before callingrequire_api_level(), and raises its own typed error when it is missing.Levels (one line each;
tests/test_genre_gate.pyholds 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
anprovides
- 3 = the public rig builder (an#338):
- 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:
objectOne kind of action: its model, how it occupies time, how
scene.mdspells it.modelvalidates a document’s action of this kind. A genre’s model subclassesan.ir.schema.ExtensionAction; the core’s are the static members of the schema’s union.durationis a LEAF’s natural length; a leaf flattens to oneFlatActionon[t, t + duration]and advances asequenceby it.flattenreplaces that default for kinds that place themselves differently (setdoes not advance the cursor) or that hold children (sequence):flatten(action, t, ctx) -> new cursor, wherectxisan.ir.compose.FlattenContext.childrenlists a composite’s child actions, so generic walkers (validation) reach every leaf without knowing the kind.read_md/write_mdare thescene.mdyaml actionshooks:read_md(item, index=i) -> action(start:already removed whenmd_start) andwrite_md(action) -> dict(withoutstart). A kind with noread_mdhas noscene.mdform.
- 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 withextent_resolver(vocab, *, products),expand(flat_list, *, vocab, fps, step_hz, default_easing, resolutions, products),view_of(entity_swaps, vocab, *, duration)andclip(action, *, anim_id, vocab, fps, view)– seean.stage.compile.ActionLowering.None: the compiler has nothing kind-specific to do. (an#225: this is how the compiler stops namingplay.)
- 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.semanticfolds 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:
objectOne 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, inorder. A pass withbuildsset is an ENTITY BUILDER instead: the compiler’s scene pass calls it for each entity of that kind (the cut-out genre’srigbuilds acharacter); builders with a lowerorderbuild all their entities first (the stage’s backdrop before the cast), equal orders in entity order.runis the callable, or"module:function", resolved on first use – so a genre is inspectable (an.genres.available()) without importing the engine its passes target. Whatrunreceives is the compiler’s business (the stage hands a pass itsCompileState, a builder the entity and the scene being built); the core never calls it.
- class an.genres.DialogueSugar(name, opener, field, parse, format, description='', fields=())[source]#
Bases:
objectscene.mdsugar on a dialogue line: one bracket pair, oneDialoguefield (and, declared infields, the parameters it may carry).parse(content) -> valueturns what is between the brackets into the field’s value (raiseValueError(why)to refuse it);format(line) -> str | Noneis the inverse (the content, without brackets, orNonewhen the line carries none). The cut-out genre’s[emotion]is one.A sugar that also sets other
Dialoguefields ([angry 0.4]: the emotion and itsemotion_intensity, an#253) names them infields; itsparsethen returns{field name: value}overfieldand any offields, andformatwrites them back.
- an.genres.ENTRY_POINT_GROUP: str = 'an.genres'#
The entry-point group a genre package declares its
Genreunder.
- 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:
objectOne kind of entity (
AssetRef.kind): what its nodes’ properties are.spacenames the registeredan.timing.spaces.PropertySpaceits nodes’ properties live in (None: the entity has no animatable nodes, as a voice);storeis the project-mall store itsrefkeys into.- descriptor_kind: str | None = None#
The
kindtag of the descriptor documentstoreholds 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
refis 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
refcasts — whatan library sheetdraws 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”) andwhole_entity(action, desc, prop, keys, entity_id, *, where, report, art_exists) -> bool(judge a swap on the entity ITSELF;Truewhen 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:
objectA genre: one plain, declarative object listing what it registers.
nameis the genre’s persisted slug (cutout_animation); it is also the owner of every entry it registers.packagenames 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.spacesarean.timing.spaces.PropertySpaceobjects andfield_kindsare(name, factory)pairs foran.timing.kinds.register_kind().capabilitiesarean.capabilities.Capabilityobjects,analysersan.capabilities.Analyserobjects,vocabularyan.semantic.Entryobjects (methods included) andaspectsan.semantic.Aspectobjects; 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.librarynames 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 notpackage: the distribution that ships a genre and the root its data lives under can differ (the cut-out genre ships insideantoday, while its library is alreadycutan’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.
>>> 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,ImportErrorA genre package needs a newer
anthan the one installed.
- exception an.genres.GenreError[source]#
Bases:
RegistryErrorA genre declaration is malformed, or its entry point does not resolve.
- an.genres.IN_DISTRIBUTION_GENRES: tuple[tuple[str, str], ...] = ()#
Genres the
andistribution itself ships, as entry-point values. They are found byload()WITHOUT relying on installed metadata: an editable install made before thean.genresgroup existed never refreshes itsdist-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 (onlyload()reads it, never an import). Every genre is external now: the cut-out genre moved tocutan(an#225) and comes through thean.genresentry point alone.
- exception an.genres.RegistryError[source]#
Bases:
ValueErrorA registration is malformed or collides with one already made.
- class an.genres.RuntimeScript(name, source, engine='stage', description='')[source]#
Bases:
objectJavaScript 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 leaveruntime.jsforcutan(P8).sourceis"package:relative/path.js", read withimportlib.resourceswhen the engine stages its runtime, so it ships in the genre’s wheel. The staged code is part of the shot cache’s key.
- class an.genres.SemanticCheck(name, run, stage='shot', order=0.0, description='')[source]#
Bases:
objectOne semantic-validation check:
run(ctx)adds findings toctx.report.ctxisan.ir.validate.ValidationContext; ashotcheck is called once per shot withctx.shotset. Within a stage, checks run byorder(then registration order), so a genre’s check lands exactly where it belongs in the report.
- exception an.genres.ServiceMissingError[source]#
Bases:
RegistryError,ImportErrorThe core asked a genre for a service nobody registered.
- class an.genres.SwapDeclaration(sets, descriptor=None, art_exists=None, scale=1.0)[source]#
Bases:
objectWhat one entity’s descriptor declares for the stage compiler’s swap vocabulary.
setsis{set name: {KEY: attachment name}}as declared;descriptoris the (migrated) document the declaration came from, kept for the kind’s own lowering;art_existsanswersrel_path -> art on disk(Nonewhen the store cannot say);scaleis the factor from the entity’s view box to scene pixels.
- exception an.genres.UnregisteredKindError(what, name, *, known=(), providers=(), where='')[source]#
Bases:
RegistryErrorA 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_names(*, owner=None)[source]#
Registered action-kind names in registration order;
owner’s only when given.
- 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 whatload()raises).
- an.genres.discovered_entry_points(*, entry_points=None, builtin=True)[source]#
The in-distribution genres (
builtin; none since the cut-out genre moved tocutan, an#225) merged withentry_points(default: the installed ones), de-duplicated by name — the in-distribution declaration first.- Return type:
>>> [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’sroot), or an entity whose kind is unregistered or declares no space, getsdefault— the timing defaultan.timing.spaces.DFLT_TIMELINE_SPACEwhenNone.The default EVALUATOR agrees since an#245: the stage’s compiled document records each entity whose kind declares another space (
meta.entity_spaces), andtimeline_from_compiledresolves 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.genresentry points (nothing is imported).- Return type:
- 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 (callload()first, as every entry point does).- Return type:
>>> 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.
- 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 outsidean, so a change to a genre’s code changes the key (an#294): the genre is reached by REGISTRATION, not by an import froman.>>> isinstance(hook_modules(), tuple) True
- an.genres.installed()[source]#
The names of the genres registered in this process, in registration order.
- an.genres.load(*, entry_points=None, builtin=True)[source]#
Register every discoverable genre. Idempotent.
Discoverable: the genres the
andistribution ships (IN_DISTRIBUTION_GENRES, unlessbuiltin=False) and thean.genresentry points (entry_pointsreplaces 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.
- an.genres.providers_of(kind, *, registry='action kinds')[source]#
The installed genres (loaded or not) that declare
kindinregistry(a key ofGenre.provides()). Used by the unregistered-kind errors.
- an.genres.register_genre(genre, *, replace=False, check_capabilities=True)[source]#
Register everything
genredeclares, owned bygenre.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 unlessreplace. All or nothing: a registration that fails part-way leaves no trace.check_capabilities=Falsedefers 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:
- 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.targetis 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 inmisc/docs/architecture_as_built.md.- Return type:
- an.genres.require_api_level(level, *, package)[source]#
Refuse, with an upgrade hint, when this
anis older thanpackageneeds.A genre package calls it at import (
require_api_level(3, package="cutan")).- Return type:
>>> 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:
- an.genres.service(name, default=None)[source]#
The registered service
name, resolved, ordefaultwhen none is.- Return type:
>>> service("no.such.service") is None True
- an.genres.services(prefix)[source]#
{name without the prefix: resolved service}for every service underprefix.>>> services("no.such.") {}
- an.genres.without_genres()[source]#
Run a block with no genre registered (the core alone), then restore.
>>> with without_genres(): ... action_kind("play") is None True
Modules
The core's open registries: action kinds, entity kinds, semantic checks, md sugar, compile passes. |